Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/agent-manager-json-tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Allow Agent Manager to start sessions when a model returns the task list as a JSON-encoded array.
12 changes: 11 additions & 1 deletion packages/opencode/src/kilocode/tool/agent-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,17 @@ const AnswerParams = Schema.Struct({
}),
})

export const Params = Schema.Union([StartParams, ListParams, PromptParams, StopParams, MoveParams, AnswerParams])
export const Params = Schema.Union([
Schema.Struct({
...StartParams.fields,
tasks: Schema.Union([StartParams.fields.tasks, Schema.fromJsonString(StartParams.fields.tasks)]),
}),
ListParams,
PromptParams,
StopParams,
MoveParams,
AnswerParams,
])

// Anthropic rejects a top-level anyOf/oneOf/allOf, so the advertised schema has to
// stay one flat object while Params keeps the real per-operation validation. That
Expand Down
61 changes: 56 additions & 5 deletions packages/opencode/test/kilocode/agent-manager-tool.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { describe, expect, test } from "bun:test"
import { Effect, Layer, ManagedRuntime, Queue, Schema } from "effect"
import { Cause, Effect, Exit, Layer, ManagedRuntime, Queue, Schema } from "effect"
import { MessageID, SessionID } from "../../src/session/schema"
import { provideTmpdirInstance } from "../fixture/fixture"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
Expand Down Expand Up @@ -220,8 +220,58 @@ describe("agent_manager tool", () => {
// The advertised bounds on tasks must survive being made nullable.
const tasks = schema.properties?.tasks
expect(typeof tasks === "object" ? tasks.anyOf?.[0] : undefined).toEqual(
expect.objectContaining({ minItems: 1, maxItems: 20 }),
expect.objectContaining({ type: "array", minItems: 1, maxItems: 20 }),
)
expect(typeof tasks === "object" ? tasks.anyOf : undefined).toHaveLength(2)
})

test.each(["local", "worktree"])("decodes JSON-encoded tasks for %s sessions", (mode) => {
const tasks = [
{ prompt: 'Check "quoted" text\nC:\\repo', name: "Encoded" },
{ name: "Prepared", model: null },
]
expect(Schema.decodeUnknownSync(Params)({ mode, tasks: JSON.stringify(tasks), action: null })).toEqual({
mode,
tasks,
})
})

test("rejects invalid JSON-encoded tasks before permission or event publication", async () => {
const tool: Tool.Def = await init()
const permissions: unknown[] = []
const events: AgentManagerStart[] = []
await runtime.runPromise(
provideTmpdirInstance(() =>
Effect.gen(function* () {
const bus = yield* Bus.Service
const off = yield* bus.subscribeCallback(AgentManagerEvent.Start, (item) => events.push(item.properties))
yield* Effect.addFinalizer(() => Effect.sync(off))
for (const tasks of [
"[",
"null",
"{}",
'"[]"',
"[]",
"[null]",
"[{}]",
JSON.stringify([{ prompt: 42 }]),
JSON.stringify([{ name: "Prepared", model: "test/reasoning/model" }]),
JSON.stringify(Array.from({ length: 21 }, () => ({ prompt: "Fix" }))),
]) {
const result = yield* tool
.execute(
{ mode: "local", tasks },
{ ...ctx, ask: (input: unknown) => Effect.sync(() => permissions.push(input)) },
)
.pipe(Effect.exit)
expect(Exit.isFailure(result)).toBe(true)
if (Exit.isFailure(result)) expect(Cause.squash(result.cause)).toBeInstanceOf(Tool.InvalidArgumentsError)
}
}),
).pipe(Effect.scoped),
)
expect(permissions).toEqual([])
expect(events).toEqual([])
})

test("keeps session ID validation local", () => {
Expand Down Expand Up @@ -644,8 +694,9 @@ describe("agent_manager tool", () => {
expect(task?.variant).toBe("high")
})

test("publishes validated model and variant selections", async () => {
const tool = await init()
test.each(["array", "JSON-encoded"])("publishes validated selections from %s tasks", async (encoding) => {
const tool: Tool.Def = await init()
const tasks = [{ prompt: "Fix issue", model: "test/reasoning/model", variant: "high" }]

const event = await runtime.runPromise(
provideTmpdirInstance(() =>
Expand All @@ -660,7 +711,7 @@ describe("agent_manager tool", () => {
yield* tool.execute(
{
mode: "local",
tasks: [{ prompt: "Fix issue", model: "test/reasoning/model", variant: "high" }],
tasks: encoding === "array" ? tasks : JSON.stringify(tasks),
},
{ ...ctx, ask: () => Effect.void },
)
Expand Down
Loading