Skip to content

Commit f7bf7ec

Browse files
committed
core: add subtask support to session system for delegating work to specialized agents
1 parent 1f436aa commit f7bf7ec

File tree

2 files changed

+109
-6
lines changed

2 files changed

+109
-6
lines changed

packages/opencode/src/session/message-v2.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ export namespace MessageV2 {
149149
})
150150
export type CompactionPart = z.infer<typeof CompactionPart>
151151

152+
export const SubtaskPart = PartBase.extend({
153+
type: z.literal("subtask"),
154+
prompt: z.string(),
155+
description: z.string(),
156+
agent: z.string(),
157+
})
158+
export type SubtaskPart = z.infer<typeof SubtaskPart>
159+
152160
export const RetryPart = PartBase.extend({
153161
type: z.literal("retry"),
154162
attempt: z.number(),
@@ -299,6 +307,7 @@ export namespace MessageV2 {
299307
export const Part = z
300308
.discriminatedUnion("type", [
301309
TextPart,
310+
SubtaskPart,
302311
ReasoningPart,
303312
FilePart,
304313
ToolPart,

packages/opencode/src/session/prompt.ts

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { fn } from "@/util/fn"
4949
import { SessionRetry } from "./retry"
5050
import { SessionProcessor } from "./processor"
5151
import { iife } from "@/util/iife"
52+
import { TaskTool } from "@/tool/task"
5253

5354
export namespace SessionPrompt {
5455
const log = Log.create({ service: "session.prompt" })
@@ -173,6 +174,16 @@ export namespace SessionPrompt {
173174
.meta({
174175
ref: "AgentPartInput",
175176
}),
177+
MessageV2.SubtaskPart.omit({
178+
messageID: true,
179+
sessionID: true,
180+
})
181+
.partial({
182+
id: true,
183+
})
184+
.meta({
185+
ref: "SubtaskPartInput",
186+
}),
176187
]),
177188
),
178189
})
@@ -290,17 +301,17 @@ export namespace SessionPrompt {
290301
let lastUser: MessageV2.User | undefined
291302
let lastAssistant: MessageV2.Assistant | undefined
292303
let lastFinished: MessageV2.Assistant | undefined
293-
let tasks: MessageV2.CompactionPart[] = []
304+
let tasks: (MessageV2.CompactionPart | MessageV2.SubtaskPart)[] = []
294305
for (let i = msgs.length - 1; i >= 0; i--) {
295306
const msg = msgs[i]
296307
if (!lastUser && msg.info.role === "user") lastUser = msg.info as MessageV2.User
297308
if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info as MessageV2.Assistant
298309
if (!lastFinished && msg.info.role === "assistant" && msg.info.finish)
299310
lastFinished = msg.info as MessageV2.Assistant
300311
if (lastUser && lastFinished) break
301-
const compaction = msg.parts.find((part) => part.type === "compaction")
302-
if (compaction && !lastFinished) {
303-
tasks.push(compaction)
312+
const task = msg.parts.filter((part) => part.type === "compaction" || part.type === "subtask")
313+
if (task && !lastFinished) {
314+
tasks.push(...task)
304315
}
305316
}
306317

@@ -313,6 +324,87 @@ export namespace SessionPrompt {
313324
const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID)
314325
const task = tasks.pop()
315326

327+
// pending subtask
328+
if (task?.type === "subtask") {
329+
const taskTool = await TaskTool.init()
330+
const assistantMessage = await Session.updateMessage({
331+
id: Identifier.ascending("message"),
332+
role: "assistant",
333+
parentID: lastUser.id,
334+
sessionID,
335+
mode: task.agent,
336+
path: {
337+
cwd: Instance.directory,
338+
root: Instance.worktree,
339+
},
340+
cost: 0,
341+
tokens: {
342+
input: 0,
343+
output: 0,
344+
reasoning: 0,
345+
cache: { read: 0, write: 0 },
346+
},
347+
modelID: model.modelID,
348+
providerID: model.providerID,
349+
time: {
350+
created: Date.now(),
351+
},
352+
})
353+
let part = await Session.updatePart({
354+
id: Identifier.ascending("part"),
355+
messageID: assistantMessage.id,
356+
sessionID: assistantMessage.sessionID,
357+
type: "tool",
358+
callID: ulid(),
359+
tool: TaskTool.id,
360+
state: {
361+
status: "running",
362+
input: {
363+
prompt: task.prompt,
364+
description: task.description,
365+
subagent_type: task.agent,
366+
},
367+
time: {
368+
start: Date.now(),
369+
},
370+
},
371+
})
372+
const result = await taskTool
373+
.execute(
374+
{
375+
prompt: task.prompt,
376+
description: task.description,
377+
subagent_type: task.agent,
378+
},
379+
{
380+
agent: task.agent,
381+
messageID: assistantMessage.id,
382+
sessionID: sessionID,
383+
abort,
384+
async metadata(input) {
385+
part = await Session.updatePart({
386+
...part,
387+
type: "tool",
388+
state: {
389+
...(part as any).state,
390+
...input,
391+
} as any,
392+
} as any)
393+
},
394+
},
395+
)
396+
.catch(() => {})
397+
await Session.updatePart({
398+
...part,
399+
state: {
400+
...(part as any).state,
401+
type: "completed",
402+
...result,
403+
},
404+
} as any)
405+
continue
406+
}
407+
316408
// pending compaction
317409
if (task?.type === "compaction") {
318410
await SessionCompaction.process({
@@ -1289,8 +1381,10 @@ export namespace SessionPrompt {
12891381

12901382
if ((agent.mode === "subagent" && command.subtask !== false) || command.subtask === true) {
12911383
parts.push({
1292-
type: "agent",
1293-
name: agent.name,
1384+
type: "subtask",
1385+
agent: agent.name,
1386+
description: command.description ?? "",
1387+
prompt: command.template,
12941388
})
12951389
}
12961390

0 commit comments

Comments
 (0)