Skip to content
Closed
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
34 changes: 22 additions & 12 deletions packages/opencode/src/tool/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,33 @@ export const TaskTool = Tool.define(
const parentAgent = parent.agent
? yield* agent.get(parent.agent).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
: undefined

// Hoisted above sessions.create() so model context is available at row-creation time.
const msg = yield* MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }).pipe(
Effect.provideService(Database.Service, database),
Effect.orDie,
)
if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message"))
const variant = msg.info.variant

// Session.Model shape ({ id, providerID }) for sessions.create() row attribution.
// Agent config model uses { modelID }, while Session.Model uses { id } — convert here.
const sessionModel = next.model
? { id: next.model.modelID, providerID: next.model.providerID }
: { id: msg.info.modelID, providerID: msg.info.providerID }
// Prompt-invocation shape ({ modelID, providerID }) — same as agent config model shape.
const model = {
modelID: next.model?.modelID ?? msg.info.modelID,
providerID: next.model?.providerID ?? msg.info.providerID,
}

const nextSession =
session ??
(yield* sessions.create({
parentID: ctx.sessionID,
title: params.description + ` (@${next.name} subagent)`,
agent: next.name,
model: sessionModel,
permission: [
...deriveSubagentSessionPermission({
parentSessionPermission: parent.permission ?? [],
Expand All @@ -141,18 +163,6 @@ export const TaskTool = Tool.define(
})) ?? []),
],
}))

const msg = yield* MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }).pipe(
Effect.provideService(Database.Service, database),
Effect.orDie,
)
if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message"))
const variant = msg.info.variant

const model = next.model ?? {
modelID: msg.info.modelID,
providerID: msg.info.providerID,
}
const metadata = {
parentSessionId: ctx.sessionID,
sessionId: nextSession.id,
Expand Down
33 changes: 33 additions & 0 deletions packages/opencode/test/tool/task.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,37 @@ describe("tool.task", () => {
}),
)

it.instance("execute populates agent and model on the child session row", () =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const { chat, assistant } = yield* seed()
const tool = yield* TaskTool
const def = yield* tool.init()

const result = yield* def.execute(
{ description: "test task", prompt: "hello", subagent_type: "general" },
{
sessionID: chat.id,
messageID: assistant.id,
agent: "build",
abort: new AbortController().signal,
extra: { promptOps: stubOps() },
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
},
)

const child = yield* sessions.get(result.metadata.sessionId)
// Test A: agent must equal the dispatched subagent name
expect(child.agent).toBe("general")
// Test B: model must be populated (falls back to parent message's ref model)
expect(child.model).toBeDefined()
expect(child.model?.id).toBe(ref.modelID)
expect(child.model?.providerID).toBe(ref.providerID)
}),
)

it.instance(
"execute shapes child permissions for task, todowrite, and primary tools",
() =>
Expand Down Expand Up @@ -429,6 +460,8 @@ describe("tool.task", () => {
action: "allow",
},
])
expect(child.agent).toBe("reviewer")
expect(child.model).toBeDefined()
expect(seen?.tools).toEqual({
todowrite: false,
bash: false,
Expand Down
Loading