mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
Merge pull request #13225 from Kilo-Org/busy-computer
fix: remove agent requirements feature
This commit is contained in:
@@ -30,9 +30,7 @@ import { AbsolutePath, type DeepMutable } from "@opencode-ai/core/schema"
|
||||
// kilocode_change start
|
||||
import * as KiloAgent from "@/kilocode/agent"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import * as AgentRequirements from "@/kilocode/agent-requirements"
|
||||
import * as KiloReference from "@/kilocode/reference"
|
||||
import { MCP } from "@/mcp"
|
||||
// kilocode_change end
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
@@ -41,8 +39,6 @@ import { Reference } from "@opencode-ai/core/reference"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
|
||||
export type RequirementBlockedError = InstanceType<typeof AgentRequirements.BlockedError> // kilocode_change
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
name: Schema.String,
|
||||
// kilocode_change start
|
||||
@@ -67,7 +63,6 @@ export const Info = Schema.Struct({
|
||||
variant: Schema.optional(Schema.String),
|
||||
prompt: Schema.optional(Schema.String),
|
||||
options: Schema.Record(Schema.String, Schema.Unknown),
|
||||
requirements: Schema.optional(AgentRequirements.Requirements), // kilocode_change
|
||||
steps: Schema.optional(Schema.Finite),
|
||||
}).annotate({ identifier: "Agent" })
|
||||
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||
@@ -83,10 +78,6 @@ export interface Interface {
|
||||
readonly list: () => Effect.Effect<Info[]>
|
||||
readonly defaultInfo: () => Effect.Effect<Info>
|
||||
readonly defaultAgent: () => Effect.Effect<string>
|
||||
// kilocode_change start
|
||||
readonly requirementStatus: (agent: string) => Effect.Effect<AgentRequirements.Result>
|
||||
readonly guardRequirements: (agent: Info) => Effect.Effect<void, RequirementBlockedError>
|
||||
// kilocode_change end
|
||||
readonly generate: (input: {
|
||||
description: string
|
||||
model?: { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
||||
@@ -100,7 +91,7 @@ export interface Interface {
|
||||
>
|
||||
}
|
||||
|
||||
type State = Omit<Interface, "generate" | "requirementStatus" | "guardRequirements"> & { version: string } // kilocode_change
|
||||
type State = Omit<Interface, "generate"> & { version: string } // kilocode_change
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
|
||||
|
||||
@@ -113,7 +104,6 @@ const layer = Layer.effect(
|
||||
const auth = yield* Auth.Service
|
||||
const plugin = yield* Plugin.Service
|
||||
const skill = yield* Skill.Service
|
||||
const mcp = yield* MCP.Service // kilocode_change
|
||||
const provider = yield* Provider.Service
|
||||
const flags = yield* RuntimeFlags.Service // kilocode_change
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
@@ -376,7 +366,6 @@ const layer = Layer.effect(
|
||||
// kilocode_change start - carry metadata as typed fields, never as provider options
|
||||
item.displayName = value.displayName ?? item.displayName
|
||||
item.source = value.source ?? item.source
|
||||
item.requirements = value.requirements ?? item.requirements
|
||||
// kilocode_change end
|
||||
item.options = mergeDeep(item.options, value.options ?? {})
|
||||
item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {}))
|
||||
@@ -536,31 +525,6 @@ const layer = Layer.effect(
|
||||
return yield* select(yield* InstanceState.get(state))
|
||||
})
|
||||
|
||||
const requirementStatus = Effect.fn("Agent.requirementStatus")(function* (name: string) {
|
||||
const ctx = yield* InstanceState.context
|
||||
return yield* AgentRequirements.status({
|
||||
name,
|
||||
directory: ctx.directory,
|
||||
config,
|
||||
skills: skill,
|
||||
mcp,
|
||||
agents: { get: (agent) => current((s) => s.get(agent)) },
|
||||
})
|
||||
})
|
||||
|
||||
const guardRequirements = Effect.fn("Agent.guardRequirements")(function* (agent: Info) {
|
||||
const ctx = yield* InstanceState.context
|
||||
yield* AgentRequirements.guard({
|
||||
agent,
|
||||
directory: ctx.directory,
|
||||
config,
|
||||
skills: skill,
|
||||
mcp,
|
||||
agents: { get: (name) => current((s) => s.get(name)) },
|
||||
})
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
return Service.of({
|
||||
get: Effect.fn("Agent.get")(function* (agent: string) {
|
||||
return yield* current((s) => s.get(agent)) // kilocode_change
|
||||
@@ -574,10 +538,6 @@ const layer = Layer.effect(
|
||||
defaultAgent: Effect.fn("Agent.defaultAgent")(function* () {
|
||||
return yield* current((s) => s.defaultAgent()) // kilocode_change
|
||||
}),
|
||||
// kilocode_change start
|
||||
requirementStatus,
|
||||
guardRequirements,
|
||||
// kilocode_change end
|
||||
generate: Effect.fn("Agent.generate")(function* (input: {
|
||||
description: string
|
||||
model?: { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
||||
@@ -653,7 +613,7 @@ const locationServiceMapNode = LayerNode.make({
|
||||
export const node = LayerNode.make({
|
||||
service: Service,
|
||||
layer: layer,
|
||||
deps: [Config.node, Auth.node, Plugin.node, Skill.node, Provider.node, MCP.node, RuntimeFlags.node, locationServiceMapNode], // kilocode_change
|
||||
deps: [Config.node, Auth.node, Plugin.node, Skill.node, Provider.node, RuntimeFlags.node, locationServiceMapNode], // kilocode_change
|
||||
})
|
||||
|
||||
export * as Agent from "./agent"
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
import type { Agent } from "@/agent/agent"
|
||||
import type { Config } from "@/config/config"
|
||||
import type { MCP } from "@/mcp"
|
||||
import type { Skill } from "@/skill"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { Cause, Effect, Exit, Schema } from "effect"
|
||||
import { ConfigAgentV1 } from "@opencode-ai/core/v1/config/agent"
|
||||
|
||||
export const VSCodeExtension = ConfigAgentV1.VSCodeExtension
|
||||
export type VSCodeExtension = ConfigAgentV1.VSCodeExtension
|
||||
|
||||
export const Requirements = ConfigAgentV1.Requirements
|
||||
export type Requirements = ConfigAgentV1.Requirements
|
||||
|
||||
export const SkillItem = Schema.Struct({
|
||||
name: Schema.String,
|
||||
status: Schema.Literals(["ready", "missing", "error"]),
|
||||
message: Schema.optional(Schema.String),
|
||||
})
|
||||
export type SkillItem = Schema.Schema.Type<typeof SkillItem>
|
||||
|
||||
export const MCPItem = Schema.Struct({
|
||||
name: Schema.String,
|
||||
status: Schema.Literals(["ready", "missing", "error"]),
|
||||
message: Schema.optional(Schema.String),
|
||||
})
|
||||
export type MCPItem = Schema.Schema.Type<typeof MCPItem>
|
||||
|
||||
export const Result = Schema.Struct({
|
||||
agent: Schema.String,
|
||||
directory: Schema.String,
|
||||
enabled: Schema.Boolean,
|
||||
state: Schema.Literals(["disabled", "ready", "blocked", "error"]),
|
||||
skills: Schema.Array(SkillItem),
|
||||
mcps: Schema.Array(MCPItem),
|
||||
vscode_extensions: Schema.Array(VSCodeExtension),
|
||||
error: Schema.optional(
|
||||
Schema.Struct({
|
||||
code: Schema.Literals(["unknown_agent", "malformed_declaration", "discovery_failed", "mcp_status_failed"]),
|
||||
message: Schema.String,
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "AgentRequirementResult" })
|
||||
export type Result = Schema.Schema.Type<typeof Result>
|
||||
|
||||
export const BlockedError = NamedError.create("AgentRequirementError", {
|
||||
message: Schema.String,
|
||||
agent: Schema.String,
|
||||
directory: Schema.String,
|
||||
state: Schema.Literals(["blocked", "error"]),
|
||||
skills: Schema.mutable(Schema.Array(SkillItem)),
|
||||
mcps: Schema.mutable(Schema.Array(MCPItem)),
|
||||
vscode_extensions: Schema.mutable(Schema.Array(VSCodeExtension)),
|
||||
})
|
||||
|
||||
type AgentInfo = Pick<Agent.Info, "name"> & { requirements?: unknown }
|
||||
|
||||
type Services = {
|
||||
config: Pick<Config.Interface, "get">
|
||||
agents: { get: (agent: string) => Effect.Effect<AgentInfo | undefined> }
|
||||
skills: Pick<Skill.Interface, "all">
|
||||
mcp: Pick<MCP.Interface, "status">
|
||||
}
|
||||
|
||||
function enabled(cfg: Config.Info) {
|
||||
const experimental = cfg.experimental
|
||||
return experimental !== undefined && "agent_requirements" in experimental && experimental.agent_requirements === true
|
||||
}
|
||||
|
||||
function ready(input: { agent: string; directory: string; enabled: boolean }): Result {
|
||||
return {
|
||||
...input,
|
||||
state: input.enabled ? "ready" : "disabled",
|
||||
skills: [],
|
||||
mcps: [],
|
||||
vscode_extensions: [],
|
||||
}
|
||||
}
|
||||
|
||||
function malformed(agent: AgentInfo, directory: string, message: string): Result {
|
||||
return {
|
||||
agent: agent.name,
|
||||
directory,
|
||||
enabled: true,
|
||||
state: "error",
|
||||
skills: [],
|
||||
mcps: [],
|
||||
vscode_extensions: [],
|
||||
error: { code: "malformed_declaration", message },
|
||||
}
|
||||
}
|
||||
|
||||
function decode(agent: AgentInfo, directory: string): Requirements | Result {
|
||||
if (agent.requirements === undefined) return ready({ agent: agent.name, directory, enabled: true })
|
||||
|
||||
const decoded = Schema.decodeUnknownExit(Requirements)(agent.requirements, {
|
||||
errors: "all",
|
||||
propertyOrder: "original",
|
||||
})
|
||||
if (Exit.isFailure(decoded)) return malformed(agent, directory, Cause.pretty(decoded.cause))
|
||||
return decoded.value
|
||||
}
|
||||
|
||||
function item(name: string, status: MCP.Status | undefined): MCPItem {
|
||||
if (status?.status === "connected") return { name, status: "ready" }
|
||||
if (status?.status === "failed" || status?.status === "needs_client_registration") {
|
||||
return { name, status: "error", message: status.error }
|
||||
}
|
||||
return { name, status: "missing" }
|
||||
}
|
||||
|
||||
export function evaluate(input: {
|
||||
agent: AgentInfo
|
||||
directory: string
|
||||
enabled: boolean
|
||||
requirements: Requirements
|
||||
discovered?: ReadonlySet<string>
|
||||
discoveryError?: string
|
||||
mcp?: Readonly<Record<string, MCP.Status>>
|
||||
mcpError?: string
|
||||
}): Result {
|
||||
if (!input.enabled) return ready({ agent: input.agent.name, directory: input.directory, enabled: false })
|
||||
|
||||
const skills = (input.requirements.skills ?? []).map((skill) => ({
|
||||
name: skill,
|
||||
status: input.discoveryError
|
||||
? ("error" as const)
|
||||
: input.discovered?.has(skill)
|
||||
? ("ready" as const)
|
||||
: ("missing" as const),
|
||||
...(input.discoveryError ? { message: input.discoveryError } : {}),
|
||||
}))
|
||||
const mcps = (input.requirements.mcps ?? []).map((name) => {
|
||||
if (input.mcpError) return { name, status: "error" as const, message: input.mcpError }
|
||||
return item(name, input.mcp?.[name])
|
||||
})
|
||||
|
||||
if (input.discoveryError) {
|
||||
return {
|
||||
agent: input.agent.name,
|
||||
directory: input.directory,
|
||||
enabled: true,
|
||||
state: "error",
|
||||
skills,
|
||||
mcps,
|
||||
vscode_extensions: input.requirements.vscode_extensions ?? [],
|
||||
error: { code: "discovery_failed", message: input.discoveryError },
|
||||
}
|
||||
}
|
||||
|
||||
if (input.mcpError) {
|
||||
return {
|
||||
agent: input.agent.name,
|
||||
directory: input.directory,
|
||||
enabled: true,
|
||||
state: "error",
|
||||
skills,
|
||||
mcps,
|
||||
vscode_extensions: input.requirements.vscode_extensions ?? [],
|
||||
error: { code: "mcp_status_failed", message: input.mcpError },
|
||||
}
|
||||
}
|
||||
|
||||
const valid = skills.every((skill) => skill.status === "ready") && mcps.every((mcp) => mcp.status === "ready")
|
||||
return {
|
||||
agent: input.agent.name,
|
||||
directory: input.directory,
|
||||
enabled: true,
|
||||
state: valid ? "ready" : "blocked",
|
||||
skills,
|
||||
mcps,
|
||||
vscode_extensions: input.requirements.vscode_extensions ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
export const status = Effect.fn("AgentRequirements.status")(function* (
|
||||
input: Services & { name: string; directory: string },
|
||||
) {
|
||||
const cfg = yield* input.config.get()
|
||||
const active = enabled(cfg)
|
||||
if (!active) return ready({ agent: input.name, directory: input.directory, enabled: false })
|
||||
|
||||
const agent = yield* input.agents.get(input.name)
|
||||
if (!agent) {
|
||||
return {
|
||||
agent: input.name,
|
||||
directory: input.directory,
|
||||
enabled: true,
|
||||
state: "error",
|
||||
skills: [],
|
||||
mcps: [],
|
||||
vscode_extensions: [],
|
||||
error: { code: "unknown_agent", message: `Agent not found: ${input.name}` },
|
||||
} satisfies Result
|
||||
}
|
||||
|
||||
const requirements = decode(agent, input.directory)
|
||||
if ("state" in requirements) return requirements
|
||||
|
||||
const skillStatus: Effect.Effect<Skill.Info[]> = requirements.skills?.length ? input.skills.all() : Effect.succeed([])
|
||||
const mcpStatus: Effect.Effect<Record<string, MCP.Status>> = requirements.mcps?.length
|
||||
? input.mcp.status()
|
||||
: Effect.succeed({})
|
||||
const [discovered, mcp] = yield* Effect.all([skillStatus.pipe(Effect.exit), mcpStatus.pipe(Effect.exit)])
|
||||
const discoveredSet: ReadonlySet<string> | undefined = Exit.isSuccess(discovered)
|
||||
? new Set(discovered.value.map((skill) => skill.name))
|
||||
: undefined
|
||||
|
||||
if (Exit.isFailure(discovered)) {
|
||||
return evaluate({
|
||||
agent,
|
||||
directory: input.directory,
|
||||
enabled: active,
|
||||
requirements,
|
||||
discoveryError: Cause.pretty(discovered.cause),
|
||||
mcp: Exit.isSuccess(mcp) ? mcp.value : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
if (Exit.isFailure(mcp)) {
|
||||
return evaluate({
|
||||
agent,
|
||||
directory: input.directory,
|
||||
enabled: active,
|
||||
requirements,
|
||||
discovered: discoveredSet,
|
||||
mcpError: Cause.pretty(mcp.cause),
|
||||
})
|
||||
}
|
||||
|
||||
return evaluate({
|
||||
agent,
|
||||
directory: input.directory,
|
||||
enabled: active,
|
||||
requirements,
|
||||
discovered: discoveredSet,
|
||||
mcp: mcp.value,
|
||||
})
|
||||
})
|
||||
|
||||
export const guard = Effect.fn("AgentRequirements.guard")(function* (
|
||||
input: Services & { agent: AgentInfo; directory: string },
|
||||
) {
|
||||
const result = yield* status({ ...input, name: input.agent.name })
|
||||
const unsupported = Flag.KILO_CLIENT !== "vscode" && result.vscode_extensions.length > 0
|
||||
if (result.state === "disabled" || (result.state === "ready" && !unsupported)) return
|
||||
const state = result.state === "error" ? result.state : "blocked"
|
||||
|
||||
return yield* Effect.fail(
|
||||
new BlockedError({
|
||||
message: "Complete the required checks to use this agent first",
|
||||
agent: result.agent,
|
||||
directory: result.directory,
|
||||
state,
|
||||
skills: [...result.skills],
|
||||
mcps: [...result.mcps],
|
||||
vscode_extensions: [...result.vscode_extensions],
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -1,116 +0,0 @@
|
||||
import type { AgentRequirementResult } from "@kilocode/sdk/v2"
|
||||
import { isRecord } from "@/util/record"
|
||||
|
||||
type Item = AgentRequirementResult["skills"][number] | AgentRequirementResult["mcps"][number]
|
||||
|
||||
export type Client = {
|
||||
kilocode: {
|
||||
agentRequirements: (
|
||||
parameters: { agent: string; directory: string },
|
||||
options: { throwOnError: true },
|
||||
) => Promise<{ data: AgentRequirementResult }>
|
||||
}
|
||||
}
|
||||
|
||||
export type ErrorData = {
|
||||
message: string
|
||||
agent: string
|
||||
directory: string
|
||||
state: "blocked" | "error"
|
||||
skills: AgentRequirementResult["skills"]
|
||||
mcps: AgentRequirementResult["mcps"]
|
||||
vscode_extensions: AgentRequirementResult["vscode_extensions"]
|
||||
}
|
||||
|
||||
export type RequirementError = {
|
||||
name: "AgentRequirementError"
|
||||
data: ErrorData
|
||||
}
|
||||
|
||||
export type Check =
|
||||
| { ok: true; result: AgentRequirementResult }
|
||||
| { ok: false; result: AgentRequirementResult; error: RequirementError }
|
||||
|
||||
export async function check(input: { client: Client; agent: string; directory: string }): Promise<Check> {
|
||||
const response = await input.client.kilocode.agentRequirements(
|
||||
{ agent: input.agent, directory: input.directory },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
if (!blocked(response.data)) return { ok: true, result: response.data }
|
||||
return { ok: false, result: response.data, error: toError(response.data) }
|
||||
}
|
||||
|
||||
export function blocked(result: AgentRequirementResult) {
|
||||
if (!result.enabled || result.state === "disabled") return false
|
||||
if (result.vscode_extensions.length > 0) return true
|
||||
return result.state === "blocked" || result.state === "error"
|
||||
}
|
||||
|
||||
export function toError(result: AgentRequirementResult): RequirementError {
|
||||
const info = {
|
||||
message: result.error?.message ?? "Complete the required checks to use this agent first",
|
||||
agent: result.agent,
|
||||
directory: result.directory,
|
||||
state: result.state === "error" ? ("error" as const) : ("blocked" as const),
|
||||
skills: result.skills,
|
||||
mcps: result.mcps,
|
||||
vscode_extensions: result.vscode_extensions,
|
||||
}
|
||||
|
||||
return {
|
||||
name: "AgentRequirementError",
|
||||
data: {
|
||||
...info,
|
||||
message: format(info),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function data(input: unknown): ErrorData | undefined {
|
||||
if (!isRecord(input)) return undefined
|
||||
if (input.name !== "AgentRequirementError" || !isRecord(input.data)) return undefined
|
||||
|
||||
const info = input.data
|
||||
if (typeof info.message !== "string") return undefined
|
||||
if (typeof info.agent !== "string") return undefined
|
||||
if (typeof info.directory !== "string") return undefined
|
||||
if (info.state !== "blocked" && info.state !== "error") return undefined
|
||||
if (!Array.isArray(info.skills)) return undefined
|
||||
if (!Array.isArray(info.mcps)) return undefined
|
||||
if (!Array.isArray(info.vscode_extensions)) return undefined
|
||||
|
||||
return info as ErrorData
|
||||
}
|
||||
|
||||
export function format(input: ErrorData | RequirementError) {
|
||||
const info = "data" in input ? input.data : input
|
||||
const skills = pending(info.skills)
|
||||
const mcps = pending(info.mcps)
|
||||
const lines = [`Agent requirements are not met for "${info.agent}".`]
|
||||
|
||||
if (info.vscode_extensions.length) {
|
||||
lines.push(
|
||||
"",
|
||||
"VS Code extensions:",
|
||||
...info.vscode_extensions.map((ext) => `- ${ext.name} (${ext.id})`),
|
||||
"This agent requires VS Code extensions and is not supported in this CLI environment. Use the Kilo VS Code extension instead.",
|
||||
)
|
||||
}
|
||||
|
||||
if (skills.length) lines.push("", "Skills:", ...skills.map((skill) => `- ${item(skill)}`))
|
||||
if (mcps.length) lines.push("", "MCP servers:", ...mcps.map((mcp) => `- ${item(mcp)}`))
|
||||
|
||||
if (skills.length || mcps.length) {
|
||||
lines.push("", "Install the required skills and configure or connect the required MCP servers, then retry.")
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
function pending<T extends Item>(items: T[]) {
|
||||
return items.filter((item) => item.status !== "ready")
|
||||
}
|
||||
|
||||
function item(input: Item) {
|
||||
const status = input.status === "missing" ? "missing" : "error"
|
||||
return input.message ? `${input.name} (${status}: ${input.message})` : `${input.name} (${status})`
|
||||
}
|
||||
@@ -5,11 +5,9 @@ import { InstanceContextMiddleware } from "@/server/routes/instance/httpapi/midd
|
||||
import {
|
||||
WorkspaceRoutingMiddleware,
|
||||
WorkspaceRoutingQuery,
|
||||
WorkspaceRoutingQueryFields,
|
||||
} from "@/server/routes/instance/httpapi/middleware/workspace-routing"
|
||||
import { described } from "@/server/routes/instance/httpapi/groups/metadata"
|
||||
import { AnacondaDesktopApi } from "./anaconda-desktop"
|
||||
import { Result as AgentRequirementResult } from "@/kilocode/agent-requirements"
|
||||
import {
|
||||
Failure as AgentManagerFailure,
|
||||
Request as AgentManagerRequest,
|
||||
@@ -42,10 +40,6 @@ export const RemoveAgentPayload = Schema.Struct({
|
||||
scope: Schema.optional(Scope),
|
||||
})
|
||||
|
||||
export const AgentRequirementQuery = Schema.Struct({
|
||||
...WorkspaceRoutingQueryFields,
|
||||
agent: Schema.String,
|
||||
})
|
||||
export const NotebookReplyPayload = Schema.Struct({ result: NotebookResult })
|
||||
export const NotebookRejectPayload = Schema.Struct({ error: NotebookFailure })
|
||||
export const AgentManagerReplyPayload = Schema.Struct({ result: AgentManagerResult })
|
||||
@@ -53,7 +47,6 @@ export const AgentManagerRejectPayload = Schema.Struct({ error: AgentManagerFail
|
||||
|
||||
export const KilocodePaths = {
|
||||
heapSnapshot: `${root}/heap/snapshot`,
|
||||
agentRequirements: `${root}/agent/requirements`,
|
||||
commandFiles: `${root}/command/files`,
|
||||
removeCommand: `${root}/command/remove`,
|
||||
removeSkill: `${root}/skill/remove`,
|
||||
@@ -82,16 +75,6 @@ export const KilocodeApi = HttpApi.make("kilocode")
|
||||
description: "Write a heap snapshot for the CLI process to the log directory.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("agentRequirements", KilocodePaths.agentRequirements, {
|
||||
query: AgentRequirementQuery,
|
||||
success: described(AgentRequirementResult, "Agent requirement status"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "kilocode.agentRequirements",
|
||||
summary: "Check agent requirements",
|
||||
description: "Check whether the selected agent's requirements are available in the request directory.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("commandFiles", KilocodePaths.commandFiles, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(CommandFiles.Info), "Command files"),
|
||||
|
||||
@@ -42,12 +42,6 @@ export const kilocodeHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilocode"
|
||||
return yield* Effect.sync(() => HeapSnapshot.write())
|
||||
})
|
||||
|
||||
const agentRequirements = Effect.fn("KilocodeHttpApi.agentRequirements")(function* (ctx: {
|
||||
query: { agent: string }
|
||||
}) {
|
||||
return yield* agents.requirementStatus(ctx.query.agent)
|
||||
})
|
||||
|
||||
const commandFiles = Effect.fn("KilocodeHttpApi.commandFiles")(function* () {
|
||||
const instance = yield* InstanceState.context
|
||||
const dirs = yield* config.directories()
|
||||
@@ -177,7 +171,6 @@ export const kilocodeHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilocode"
|
||||
|
||||
return handlers
|
||||
.handle("heapSnapshot", heapSnapshot)
|
||||
.handle("agentRequirements", agentRequirements)
|
||||
.handle("commandFiles", commandFiles)
|
||||
.handle("removeCommand", removeCommand)
|
||||
.handle("removeSkill", removeSkill)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Image } from "@/image/image" // kilocode_change - classify user image validation defects
|
||||
import { busyMessage, isBusy } from "@/kilocode/database/sqlite-error" // kilocode_change
|
||||
import { KiloSessionHttpApi } from "@/kilocode/server/httpapi/session-fork" // kilocode_change
|
||||
import { BlockedError as AgentRequirementError } from "@/kilocode/agent-requirements" // kilocode_change
|
||||
import { KiloSessionPromptQueue } from "@/kilocode/session/prompt-queue" // kilocode_change
|
||||
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||
import { KiloViewers } from "@/kilocode/presence/service" // kilocode_change
|
||||
@@ -342,9 +341,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
// kilocode_change end
|
||||
yield* events.publish(Session.Event.Error, {
|
||||
sessionID: ctx.params.sessionID,
|
||||
error: AgentRequirementError.isInstance(error)
|
||||
? error.toObject()
|
||||
: busy // kilocode_change
|
||||
error: busy // kilocode_change
|
||||
? new NamedError.Unknown({ message: busyMessage }).toObject() // kilocode_change
|
||||
: new NamedError.Unknown({ message: Cause.pretty(cause) }).toObject(), // kilocode_change
|
||||
})
|
||||
|
||||
@@ -138,18 +138,14 @@ function isOrphanedInterruptedTool(part: SessionV1.ToolPart) {
|
||||
|
||||
export interface Interface {
|
||||
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
|
||||
// kilocode_change start - prompt can fail on unmet agent requirements
|
||||
readonly prompt: (
|
||||
input: PromptInput,
|
||||
) => Effect.Effect<SessionV1.WithParts, Image.Error | Agent.RequirementBlockedError>
|
||||
// kilocode_change end
|
||||
) => Effect.Effect<SessionV1.WithParts, Image.Error>
|
||||
readonly loop: (input: LoopInput) => Effect.Effect<SessionV1.WithParts>
|
||||
readonly shell: (input: ShellInput) => Effect.Effect<SessionV1.WithParts, Session.BusyError>
|
||||
// kilocode_change start - commands can fail on unmet agent requirements or resume errors
|
||||
readonly command: (
|
||||
input: CommandInput,
|
||||
) => Effect.Effect<SessionV1.WithParts, Image.Error | Agent.RequirementBlockedError | Error>
|
||||
// kilocode_change end
|
||||
) => Effect.Effect<SessionV1.WithParts, Image.Error | Error>
|
||||
readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
|
||||
}
|
||||
|
||||
@@ -825,8 +821,6 @@ export const layer = Layer.effect(
|
||||
yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
|
||||
throw error
|
||||
}
|
||||
yield* agents.guardRequirements(ag) // kilocode_change - enforce requirements before creating a turn
|
||||
|
||||
const model = input.model ?? ag.model ?? (yield* currentModel(input.sessionID))
|
||||
// kilocode_change start - retain the source session variant across Agent Manager's model-less fork handoff
|
||||
const stored = !input.model && !ag.model ? model : undefined
|
||||
@@ -1984,8 +1978,6 @@ export const layer = Layer.effect(
|
||||
yield* events.publish(Session.Event.Error, { sessionID: input.cmdInput.sessionID, error: error.toObject() })
|
||||
return yield* Effect.fail(error)
|
||||
}
|
||||
yield* agents.guardRequirements(agent)
|
||||
|
||||
// Resolve model
|
||||
const model = yield* Effect.gen(function* () {
|
||||
if (input.cmdInput.model) return Provider.parseModel(input.cmdInput.model)
|
||||
@@ -2408,8 +2400,6 @@ export const layer = Layer.effect(
|
||||
yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
|
||||
throw error
|
||||
}
|
||||
yield* agents.guardRequirements(agent) // kilocode_change - command agent overrides must satisfy requirements
|
||||
|
||||
// kilocode_change start
|
||||
const variant = KiloWorkflowVariant.resolve({
|
||||
command: cmd,
|
||||
|
||||
@@ -43,7 +43,6 @@ import { KiloSessionEvent } from "@/kilocode/session/event"
|
||||
import { SessionExport } from "@/kilocode/session-export"
|
||||
import * as SandboxPolicy from "@/kilocode/sandbox/policy"
|
||||
import { carryForkDiff } from "@/kilocode/session-portability/cumulative-diff" // kilocode_change
|
||||
import { BlockedError as AgentRequirementError } from "@/kilocode/agent-requirements"
|
||||
// kilocode_change end
|
||||
import { Effect, Layer, Option, Context, Schema, Types } from "effect"
|
||||
import { NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
@@ -399,8 +398,7 @@ export const Event = {
|
||||
sessionID: Schema.optional(SessionID),
|
||||
// Reuses SessionV1.Assistant.fields.error (already Schema.optional) so
|
||||
// the derived schema keeps the same discriminated-union shape on the event stream.
|
||||
// kilocode_change - carry pre-message requirement failures over session.error
|
||||
error: Schema.optional(Schema.Union([SessionV1.Assistant.fields.error, AgentRequirementError.EffectSchema])),
|
||||
error: SessionV1.Assistant.fields.error,
|
||||
},
|
||||
}),
|
||||
// kilocode_change start
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test"
|
||||
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
|
||||
import { ConfigAgentV1 } from "@opencode-ai/core/v1/config/agent" // kilocode_change
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
|
||||
import { Cause, Effect, Exit, Layer, Option } from "effect"
|
||||
@@ -1585,6 +1586,23 @@ test("config parser preserves permission order while rejecting unknown top-level
|
||||
}
|
||||
})
|
||||
|
||||
// kilocode_change start - preserve legacy agent requirement declarations without forwarding them
|
||||
test("config parser ignores legacy agent requirements", () => {
|
||||
const config = ConfigParse.schema(
|
||||
ConfigAgentV1.Info,
|
||||
{
|
||||
name: "demo",
|
||||
requirements: { skills: ["needed"] },
|
||||
custom: true,
|
||||
},
|
||||
"agent/demo.md",
|
||||
)
|
||||
|
||||
expect(config.options).toEqual({ custom: true })
|
||||
expect(config.options).not.toHaveProperty("requirements")
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
// MCP config merging tests
|
||||
|
||||
// kilocode_change start - regression for `env` alias on local MCP entries
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { AgentRequirementResult } from "@kilocode/sdk/v2"
|
||||
import { formatError as formatStreamError } from "@/cli/cmd/run/session-data"
|
||||
import * as Requirements from "@/kilocode/cli/agent-requirements"
|
||||
|
||||
const dir = "/tmp/agent-requirements-cli"
|
||||
|
||||
function result(input: Partial<AgentRequirementResult> = {}): AgentRequirementResult {
|
||||
return {
|
||||
agent: "demo",
|
||||
directory: dir,
|
||||
enabled: true,
|
||||
state: "ready",
|
||||
skills: [],
|
||||
mcps: [],
|
||||
vscode_extensions: [],
|
||||
...input,
|
||||
}
|
||||
}
|
||||
|
||||
function client(value: AgentRequirementResult): Requirements.Client {
|
||||
return {
|
||||
kilocode: {
|
||||
agentRequirements: async (parameters, options) => {
|
||||
expect(parameters).toEqual({ agent: value.agent, directory: value.directory })
|
||||
expect(options).toEqual({ throwOnError: true })
|
||||
return { data: value }
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function check(value: AgentRequirementResult) {
|
||||
return Requirements.check({ client: client(value), agent: value.agent, directory: value.directory })
|
||||
}
|
||||
|
||||
describe("CLI agent requirements", () => {
|
||||
test("allows disabled requirements", async () => {
|
||||
const output = await check(result({ enabled: false, state: "disabled" }))
|
||||
|
||||
expect(output.ok).toBe(true)
|
||||
})
|
||||
|
||||
test("allows ready requirements", async () => {
|
||||
const output = await check(
|
||||
result({
|
||||
skills: [{ name: "ready-skill", status: "ready" }],
|
||||
mcps: [{ name: "ready-mcp", status: "ready" }],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(output.ok).toBe(true)
|
||||
})
|
||||
|
||||
test("blocks missing skills", async () => {
|
||||
const output = await check(
|
||||
result({
|
||||
state: "blocked",
|
||||
skills: [
|
||||
{ name: "ready-skill", status: "ready" },
|
||||
{ name: "missing-skill", status: "missing" },
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(output.ok).toBe(false)
|
||||
if (output.ok) return
|
||||
expect(output.error).toMatchObject({
|
||||
name: "AgentRequirementError",
|
||||
data: { agent: "demo", state: "blocked" },
|
||||
})
|
||||
expect(output.error.data.skills).toContainEqual({ name: "missing-skill", status: "missing" })
|
||||
})
|
||||
|
||||
test("blocks missing and error MCPs", async () => {
|
||||
const output = await check(
|
||||
result({
|
||||
state: "blocked",
|
||||
mcps: [
|
||||
{ name: "missing-mcp", status: "missing" },
|
||||
{ name: "error-mcp", status: "error", message: "server crashed" },
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(output.ok).toBe(false)
|
||||
if (output.ok) return
|
||||
expect(output.error.data.mcps).toEqual([
|
||||
{ name: "missing-mcp", status: "missing" },
|
||||
{ name: "error-mcp", status: "error", message: "server crashed" },
|
||||
])
|
||||
})
|
||||
|
||||
test("blocks VS Code extension declarations", async () => {
|
||||
const output = await check(
|
||||
result({
|
||||
vscode_extensions: [{ name: "Sample Extension", id: "publisher.extension" }],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(output.ok).toBe(false)
|
||||
if (output.ok) return
|
||||
expect(output.error.data.state).toBe("blocked")
|
||||
expect(output.error.data.vscode_extensions).toEqual([{ name: "Sample Extension", id: "publisher.extension" }])
|
||||
const text = Requirements.format(output.error)
|
||||
expect(text).toContain("Use the Kilo VS Code extension instead")
|
||||
expect(text).not.toContain("Install the required skills and configure or connect the required MCP servers")
|
||||
})
|
||||
|
||||
test("formats grouped actionable guidance", () => {
|
||||
const text = Requirements.format(
|
||||
Requirements.toError(
|
||||
result({
|
||||
state: "error",
|
||||
error: { code: "discovery_failed", message: "skill scan failed" },
|
||||
skills: [
|
||||
{ name: "ready-skill", status: "ready" },
|
||||
{ name: "missing-skill", status: "missing" },
|
||||
{ name: "error-skill", status: "error", message: "skill scan failed" },
|
||||
],
|
||||
mcps: [
|
||||
{ name: "missing-mcp", status: "missing" },
|
||||
{ name: "error-mcp", status: "error", message: "auth failed" },
|
||||
],
|
||||
vscode_extensions: [{ name: "Sample Extension", id: "publisher.extension" }],
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(text).toContain('Agent requirements are not met for "demo".')
|
||||
expect(text).toContain("VS Code extensions:")
|
||||
expect(text).toContain("Sample Extension (publisher.extension)")
|
||||
expect(text).toContain("Skills:")
|
||||
expect(text).toContain("missing-skill (missing)")
|
||||
expect(text).toContain("error-skill (error: skill scan failed)")
|
||||
expect(text).not.toContain("ready-skill (ready)")
|
||||
expect(text).toContain("MCP servers:")
|
||||
expect(text).toContain("missing-mcp (missing)")
|
||||
expect(text).toContain("error-mcp (error: auth failed)")
|
||||
expect(text).toContain("Install the required skills and configure or connect the required MCP servers, then retry.")
|
||||
})
|
||||
|
||||
test("recognizes AgentRequirementError objects", () => {
|
||||
const error = Requirements.toError(result({ state: "blocked" }))
|
||||
|
||||
expect(Requirements.data(error)).toEqual(error.data)
|
||||
expect(Requirements.data({ name: "OtherError", data: error.data })).toBeUndefined()
|
||||
})
|
||||
|
||||
test("stores grouped guidance on AgentRequirementError message", () => {
|
||||
const error = Requirements.toError(
|
||||
result({
|
||||
state: "blocked",
|
||||
skills: [{ name: "missing-skill", status: "missing" }],
|
||||
}),
|
||||
)
|
||||
const text = Requirements.format(error)
|
||||
|
||||
expect(text).toContain('Agent requirements are not met for "demo".')
|
||||
expect(text).toContain("Skills:")
|
||||
expect(text).toContain("missing-skill (missing)")
|
||||
expect(error.data.message).toBe(text)
|
||||
})
|
||||
|
||||
test("formats AgentRequirementError in interactive session errors", () => {
|
||||
const error = Requirements.toError(
|
||||
result({
|
||||
state: "blocked",
|
||||
mcps: [{ name: "missing-mcp", status: "missing" }],
|
||||
}),
|
||||
)
|
||||
const text = Requirements.format(error)
|
||||
|
||||
expect(formatStreamError(error)).toBe(text)
|
||||
})
|
||||
})
|
||||
@@ -1,307 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Cause, Effect, Exit } from "effect"
|
||||
import { ConfigAgentV1 } from "@opencode-ai/core/v1/config/agent"
|
||||
import { ConfigParse } from "@/config/parse"
|
||||
import * as AgentRequirements from "@/kilocode/agent-requirements"
|
||||
import type { MCP } from "@/mcp"
|
||||
import type { Skill } from "@/skill"
|
||||
|
||||
type Agent = {
|
||||
name: string
|
||||
requirements?: unknown
|
||||
}
|
||||
|
||||
type Input = {
|
||||
active?: boolean
|
||||
agents?: Record<string, Agent>
|
||||
skills?: string[]
|
||||
skillError?: string
|
||||
mcp?: Record<string, MCP.Status>
|
||||
mcpError?: string
|
||||
}
|
||||
|
||||
const dir = "/tmp/agent-requirements"
|
||||
|
||||
function services(input: Input = {}) {
|
||||
return {
|
||||
config: {
|
||||
get: () => Effect.succeed({ experimental: input.active ? { agent_requirements: true } : {} }),
|
||||
},
|
||||
agents: {
|
||||
get: (name: string) => Effect.succeed(input.agents?.[name]),
|
||||
},
|
||||
skills: {
|
||||
all: () => {
|
||||
if (input.skillError) return Effect.die(new Error(input.skillError))
|
||||
const skills = (input.skills ?? []).map(
|
||||
(name) => ({ name, location: "test", content: "" }) satisfies Skill.Info,
|
||||
)
|
||||
return Effect.succeed(skills)
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
status: () => {
|
||||
if (input.mcpError) return Effect.die(new Error(input.mcpError))
|
||||
return Effect.succeed(input.mcp ?? {})
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function status(name: string, input: Input) {
|
||||
return Effect.runPromise(AgentRequirements.status({ ...services(input), name, directory: dir }))
|
||||
}
|
||||
|
||||
async function client(value: string | undefined, run: () => Promise<void>) {
|
||||
const prev = process.env.KILO_CLIENT
|
||||
try {
|
||||
if (value === undefined) delete process.env.KILO_CLIENT
|
||||
if (value !== undefined) process.env.KILO_CLIENT = value
|
||||
await run()
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.KILO_CLIENT
|
||||
if (prev !== undefined) process.env.KILO_CLIENT = prev
|
||||
}
|
||||
}
|
||||
|
||||
describe("agent requirements", () => {
|
||||
test("returns disabled when the experimental flag is absent", async () => {
|
||||
const result = await status("missing", {})
|
||||
|
||||
expect(result).toMatchObject({
|
||||
agent: "missing",
|
||||
directory: dir,
|
||||
enabled: false,
|
||||
state: "disabled",
|
||||
skills: [],
|
||||
mcps: [],
|
||||
vscode_extensions: [],
|
||||
})
|
||||
})
|
||||
|
||||
test("returns ready for agents without requirements", async () => {
|
||||
const result = await status("demo", { active: true, agents: { demo: { name: "demo" } } })
|
||||
|
||||
expect(result).toMatchObject({ agent: "demo", directory: dir, enabled: true, state: "ready" })
|
||||
})
|
||||
|
||||
test("reports unknown agents while requirements are enabled", async () => {
|
||||
const result = await status("missing", { active: true })
|
||||
|
||||
expect(result.state).toBe("error")
|
||||
expect(result.error?.code).toBe("unknown_agent")
|
||||
})
|
||||
|
||||
test("reports discovered and missing skills", async () => {
|
||||
const result = await status("demo", {
|
||||
active: true,
|
||||
agents: { demo: { name: "demo", requirements: { skills: ["ready", "absent"] } } },
|
||||
skills: ["ready"],
|
||||
})
|
||||
|
||||
expect(result.state).toBe("blocked")
|
||||
expect(result.skills).toEqual([
|
||||
{ name: "ready", status: "ready" },
|
||||
{ name: "absent", status: "missing" },
|
||||
])
|
||||
})
|
||||
|
||||
test("accepts non-empty marketplace skill and MCP IDs", async () => {
|
||||
const result = await status("demo", {
|
||||
active: true,
|
||||
agents: {
|
||||
demo: {
|
||||
name: "demo",
|
||||
requirements: { skills: ["skill with space"], mcps: ["mcp/with/slash"] },
|
||||
},
|
||||
},
|
||||
skills: ["skill with space"],
|
||||
mcp: { "mcp/with/slash": { status: "connected" } },
|
||||
})
|
||||
|
||||
expect(result.state).toBe("ready")
|
||||
expect(result.skills).toEqual([{ name: "skill with space", status: "ready" }])
|
||||
expect(result.mcps).toEqual([{ name: "mcp/with/slash", status: "ready" }])
|
||||
})
|
||||
|
||||
test("reports skill discovery failures as errors", async () => {
|
||||
const result = await status("demo", {
|
||||
active: true,
|
||||
agents: { demo: { name: "demo", requirements: { skills: ["needed"] } } },
|
||||
skillError: "skill scan failed",
|
||||
})
|
||||
|
||||
expect(result.state).toBe("error")
|
||||
expect(result.error?.code).toBe("discovery_failed")
|
||||
expect(result.skills[0]?.status).toBe("error")
|
||||
expect(result.skills[0]?.message).toContain("skill scan failed")
|
||||
})
|
||||
|
||||
test("reports malformed and duplicate declarations", async () => {
|
||||
const empty = await status("empty", {
|
||||
active: true,
|
||||
agents: { empty: { name: "empty", requirements: {} } },
|
||||
})
|
||||
const duplicate = await status("duplicate", {
|
||||
active: true,
|
||||
agents: { duplicate: { name: "duplicate", requirements: { skills: ["one", "one"] } } },
|
||||
})
|
||||
const legacy = await status("legacy", {
|
||||
active: true,
|
||||
agents: { legacy: { name: "legacy", requirements: { vscode_extensions: ["publisher.extension"] } } },
|
||||
})
|
||||
|
||||
expect(empty.error?.code).toBe("malformed_declaration")
|
||||
expect(duplicate.error?.code).toBe("malformed_declaration")
|
||||
expect(legacy.error?.code).toBe("malformed_declaration")
|
||||
})
|
||||
|
||||
test("validates VS Code extension objects", async () => {
|
||||
const valid = await status("valid", {
|
||||
active: true,
|
||||
agents: {
|
||||
valid: {
|
||||
name: "valid",
|
||||
requirements: { vscode_extensions: [{ name: "Sample Extension", id: "publisher.extension" }] },
|
||||
},
|
||||
},
|
||||
})
|
||||
const invalid = await status("invalid", {
|
||||
active: true,
|
||||
agents: {
|
||||
invalid: {
|
||||
name: "invalid",
|
||||
requirements: { vscode_extensions: [{ name: " ", id: "publisher.extension" }] },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(valid).toMatchObject({
|
||||
state: "ready",
|
||||
vscode_extensions: [{ name: "Sample Extension", id: "publisher.extension" }],
|
||||
})
|
||||
expect(invalid.state).toBe("error")
|
||||
expect(invalid.error?.code).toBe("malformed_declaration")
|
||||
})
|
||||
|
||||
test("reports MCP connected, missing, and error states", async () => {
|
||||
const result = await status("demo", {
|
||||
active: true,
|
||||
agents: {
|
||||
demo: {
|
||||
name: "demo",
|
||||
requirements: { mcps: ["connected", "disabled", "failed", "registration"] },
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
connected: { status: "connected" },
|
||||
disabled: { status: "disabled" },
|
||||
failed: { status: "failed", error: "server crashed" },
|
||||
registration: { status: "needs_client_registration", error: "OAuth required" },
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.state).toBe("blocked")
|
||||
expect(result.mcps).toEqual([
|
||||
{ name: "connected", status: "ready" },
|
||||
{ name: "disabled", status: "missing" },
|
||||
{ name: "failed", status: "error", message: "server crashed" },
|
||||
{ name: "registration", status: "error", message: "OAuth required" },
|
||||
])
|
||||
})
|
||||
|
||||
test("reports MCP status service failures", async () => {
|
||||
const result = await status("demo", {
|
||||
active: true,
|
||||
agents: { demo: { name: "demo", requirements: { mcps: ["needed"] } } },
|
||||
mcpError: "status unavailable",
|
||||
})
|
||||
|
||||
expect(result.state).toBe("error")
|
||||
expect(result.error?.code).toBe("mcp_status_failed")
|
||||
expect(result.mcps[0]?.status).toBe("error")
|
||||
expect(result.mcps[0]?.message).toContain("status unavailable")
|
||||
})
|
||||
|
||||
test("guards unmet requirements for all clients", async () => {
|
||||
const missing = {
|
||||
active: true,
|
||||
agents: { demo: { name: "demo", requirements: { skills: ["missing"] } } },
|
||||
}
|
||||
|
||||
await client("cli", async () => {
|
||||
const exit = await Effect.runPromiseExit(
|
||||
AgentRequirements.guard({ ...services(missing), agent: missing.agents.demo, directory: dir }),
|
||||
)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isSuccess(exit)) return
|
||||
const error = Cause.squash(exit.cause)
|
||||
expect(AgentRequirements.BlockedError.isInstance(error)).toBe(true)
|
||||
})
|
||||
|
||||
await client("vscode", async () => {
|
||||
const exit = await Effect.runPromiseExit(
|
||||
AgentRequirements.guard({ ...services(missing), agent: missing.agents.demo, directory: dir }),
|
||||
)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isSuccess(exit)) return
|
||||
const error = Cause.squash(exit.cause)
|
||||
expect(AgentRequirements.BlockedError.isInstance(error)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test("blocks VS Code extension requirements outside VS Code", async () => {
|
||||
const input = {
|
||||
active: true,
|
||||
agents: {
|
||||
demo: {
|
||||
name: "demo",
|
||||
requirements: { vscode_extensions: [{ name: "Jupyter", id: "ms-toolsai.jupyter" }] },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
await client("cli", async () => {
|
||||
const exit = await Effect.runPromiseExit(
|
||||
AgentRequirements.guard({ ...services(input), agent: input.agents.demo, directory: dir }),
|
||||
)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isSuccess(exit)) return
|
||||
const error = Cause.squash(exit.cause)
|
||||
expect(AgentRequirements.BlockedError.isInstance(error)).toBe(true)
|
||||
})
|
||||
|
||||
await client("vscode", async () => {
|
||||
await Effect.runPromise(AgentRequirements.guard({ ...services(input), agent: input.agents.demo, directory: dir }))
|
||||
})
|
||||
})
|
||||
|
||||
test("allows non-VS Code clients when requirements are ready", async () => {
|
||||
const input = {
|
||||
active: true,
|
||||
agents: { demo: { name: "demo", requirements: { skills: ["ready"], mcps: ["connected"] } } },
|
||||
skills: ["ready"],
|
||||
mcp: { connected: { status: "connected" as const } },
|
||||
}
|
||||
|
||||
await client("cli", async () => {
|
||||
await Effect.runPromise(AgentRequirements.guard({ ...services(input), agent: input.agents.demo, directory: dir }))
|
||||
})
|
||||
})
|
||||
|
||||
test("keeps requirements out of agent options", () => {
|
||||
const agent = ConfigParse.schema(
|
||||
ConfigAgentV1.Info,
|
||||
{
|
||||
name: "demo",
|
||||
requirements: { skills: ["needed"] },
|
||||
custom: true,
|
||||
},
|
||||
"agent/demo.md",
|
||||
)
|
||||
|
||||
expect(agent.requirements).toEqual({ skills: ["needed"] })
|
||||
expect(agent.options).toEqual({ custom: true })
|
||||
expect(agent.options).not.toHaveProperty("requirements")
|
||||
})
|
||||
})
|
||||
@@ -556,19 +556,6 @@ export const kiloScenarios: Scenario[] = [
|
||||
yield* Effect.promise(() => rm(body, { force: true }))
|
||||
}),
|
||||
),
|
||||
http.protected
|
||||
.get("/kilocode/agent/requirements", "kilocode.agentRequirements")
|
||||
.at((ctx) => ({ path: "/kilocode/agent/requirements?agent=httpapi-agent", headers: ctx.headers() }))
|
||||
.json(200, (body, ctx) => {
|
||||
object(body)
|
||||
check(body.agent === "httpapi-agent", "agent requirements should echo the requested agent")
|
||||
check(body.directory === ctx.directory, "agent requirements should use the routed workspace directory")
|
||||
check(body.enabled === false, "agent requirements should report disabled when the experiment is off")
|
||||
check(body.state === "disabled", "agent requirements should return the disabled state")
|
||||
array(body.skills)
|
||||
array(body.mcps)
|
||||
array(body.vscode_extensions)
|
||||
}),
|
||||
http.protected
|
||||
.get("/kilocode/command/files", "kilocode.commandFiles")
|
||||
.inProject({ git: true, init: command })
|
||||
|
||||
@@ -74,7 +74,6 @@ const fastAgents = Layer.mock(AgentSvc.Service)({
|
||||
list: () => Effect.succeed([agent]),
|
||||
defaultInfo: () => Effect.succeed(agent),
|
||||
defaultAgent: () => Effect.succeed(agent.name),
|
||||
guardRequirements: () => Effect.void,
|
||||
})
|
||||
|
||||
const summary = Layer.succeed(
|
||||
|
||||
@@ -16,17 +16,6 @@ const agents = Agent.Service.of({
|
||||
list: () => Effect.succeed([info]),
|
||||
defaultInfo: () => Effect.succeed(info),
|
||||
defaultAgent: () => Effect.succeed("code"),
|
||||
requirementStatus: () =>
|
||||
Effect.succeed({
|
||||
agent: "code",
|
||||
directory: "",
|
||||
enabled: false,
|
||||
state: "ready",
|
||||
skills: [],
|
||||
mcps: [],
|
||||
vscode_extensions: [],
|
||||
}),
|
||||
guardRequirements: () => Effect.void,
|
||||
generate: () => Effect.succeed({ identifier: "code", whenToUse: "", systemPrompt: "" }),
|
||||
})
|
||||
|
||||
|
||||
@@ -20,17 +20,6 @@ const agents = Agent.Service.of({
|
||||
list: () => Effect.succeed([agentInfo]),
|
||||
defaultInfo: () => Effect.succeed(agentInfo),
|
||||
defaultAgent: () => Effect.succeed("code"),
|
||||
requirementStatus: () =>
|
||||
Effect.succeed({
|
||||
agent: "code",
|
||||
directory: "",
|
||||
enabled: false,
|
||||
state: "ready",
|
||||
skills: [],
|
||||
mcps: [],
|
||||
vscode_extensions: [],
|
||||
}),
|
||||
guardRequirements: () => Effect.void,
|
||||
generate: () => Effect.succeed({ identifier: "code", whenToUse: "", systemPrompt: "" }),
|
||||
})
|
||||
|
||||
|
||||
@@ -26,17 +26,6 @@ const agents = Agent.Service.of({
|
||||
list: () => Effect.succeed([agentInfo]),
|
||||
defaultInfo: () => Effect.succeed(agentInfo),
|
||||
defaultAgent: () => Effect.succeed("code"),
|
||||
requirementStatus: () =>
|
||||
Effect.succeed({
|
||||
agent: "code",
|
||||
directory: "",
|
||||
enabled: false,
|
||||
state: "ready",
|
||||
skills: [],
|
||||
mcps: [],
|
||||
vscode_extensions: [],
|
||||
}),
|
||||
guardRequirements: () => Effect.void,
|
||||
generate: () => Effect.succeed({ identifier: "code", whenToUse: "", systemPrompt: "" }),
|
||||
})
|
||||
|
||||
|
||||
@@ -189,7 +189,6 @@ const fastAgents = Layer.mock(AgentSvc.Service)({
|
||||
list: () => Effect.succeed([agent]),
|
||||
defaultInfo: () => Effect.succeed(agent),
|
||||
defaultAgent: () => Effect.succeed(agent.name),
|
||||
guardRequirements: () => Effect.void,
|
||||
})
|
||||
|
||||
const processorCreateStarted: Deferred.Deferred<void>[] = []
|
||||
|
||||
Reference in New Issue
Block a user