Skip to content
Open
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
13 changes: 13 additions & 0 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,19 @@ export namespace MessageV2 {
description: z.string(),
agent: z.string(),
command: z.string().optional(),
model: z
.object({
providerID: z.string(),
modelID: z.string(),
})
.optional(),
parentAgent: z.string().optional(),
parentModel: z
.object({
providerID: z.string(),
modelID: z.string(),
})
.optional(),
})
export type SubtaskPart = z.infer<typeof SubtaskPart>

Expand Down
293 changes: 160 additions & 133 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,148 +310,157 @@ export namespace SessionPrompt {
})

const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID)
const task = tasks.pop()

// pending subtask
const subtasks = tasks.filter((t): t is Extract<typeof t, { type: "subtask" }> => t.type === "subtask")
const otherTasks = tasks.filter((t) => t.type !== "subtask")
tasks.length = 0
tasks.push(...otherTasks)

// pending subtasks
// TODO: centralize "invoke tool" logic
if (task?.type === "subtask") {
if (subtasks.length > 0) {
const taskTool = await TaskTool.init()
const assistantMessage = (await Session.updateMessage({
id: Identifier.ascending("message"),
role: "assistant",
parentID: lastUser.id,
sessionID,
mode: task.agent,
agent: task.agent,
path: {
cwd: Instance.directory,
root: Instance.worktree,
},
cost: 0,
tokens: {
input: 0,
output: 0,
reasoning: 0,
cache: { read: 0, write: 0 },
},
modelID: model.id,
providerID: model.providerID,
time: {
created: Date.now(),
},
})) as MessageV2.Assistant
let part = (await Session.updatePart({
id: Identifier.ascending("part"),
messageID: assistantMessage.id,
sessionID: assistantMessage.sessionID,
type: "tool",
callID: ulid(),
tool: TaskTool.id,
state: {
status: "running",
input: {
prompt: task.prompt,
description: task.description,
subagent_type: task.agent,
command: task.command,

const executeSubtask = async (task: (typeof subtasks)[0]) => {
const assistantMessage = (await Session.updateMessage({
id: Identifier.ascending("message"),
role: "assistant",
parentID: lastUser.id,
sessionID,
mode: task.agent,
agent: task.agent,
path: {
cwd: Instance.directory,
root: Instance.worktree,
},
cost: 0,
tokens: {
input: 0,
output: 0,
reasoning: 0,
cache: { read: 0, write: 0 },
},
modelID: model.id,
providerID: model.providerID,
time: {
start: Date.now(),
created: Date.now(),
},
},
})) as MessageV2.ToolPart
const taskArgs = {
prompt: task.prompt,
description: task.description,
subagent_type: task.agent,
command: task.command,
}
await Plugin.trigger(
"tool.execute.before",
{
tool: "task",
sessionID,
callID: part.id,
},
{ args: taskArgs },
)
let executionError: Error | undefined
const taskAgent = await Agent.get(task.agent)
const taskCtx: Tool.Context = {
agent: task.agent,
messageID: assistantMessage.id,
sessionID: sessionID,
abort,
callID: part.callID,
extra: { bypassAgentCheck: true },
async metadata(input) {
})) as MessageV2.Assistant
let part = (await Session.updatePart({
id: Identifier.ascending("part"),
messageID: assistantMessage.id,
sessionID: assistantMessage.sessionID,
type: "tool",
callID: ulid(),
tool: TaskTool.id,
state: {
status: "running",
input: {
prompt: task.prompt,
description: task.description,
subagent_type: task.agent,
command: task.command,
},
time: {
start: Date.now(),
},
},
})) as MessageV2.ToolPart
const taskArgs = {
prompt: task.prompt,
description: task.description,
subagent_type: task.agent,
command: task.command,
}
await Plugin.trigger(
"tool.execute.before",
{
tool: "task",
sessionID,
callID: part.id,
},
{ args: taskArgs },
)
let executionError: Error | undefined
const taskAgent = await Agent.get(task.agent)
const taskCtx: Tool.Context = {
agent: task.agent,
messageID: assistantMessage.id,
sessionID: sessionID,
abort,
callID: part.callID,
extra: { bypassAgentCheck: true, model: task.model, userInvokedAgents: [task.agent] },
async metadata(input) {
await Session.updatePart({
...part,
type: "tool",
state: {
...part.state,
...input,
},
} satisfies MessageV2.ToolPart)
},
async ask(req) {
await PermissionNext.ask({
...req,
sessionID: sessionID,
ruleset: PermissionNext.merge(taskAgent.permission, session.permission ?? []),
})
},
}
const result = await taskTool.execute(taskArgs, taskCtx).catch((error) => {
executionError = error
log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
return undefined
})
await Plugin.trigger(
"tool.execute.after",
{
tool: "task",
sessionID,
callID: part.id,
},
result,
)
assistantMessage.finish = "tool-calls"
assistantMessage.time.completed = Date.now()
await Session.updateMessage(assistantMessage)
if (result && part.state.status === "running") {
await Session.updatePart({
...part,
type: "tool",
state: {
...part.state,
...input,
status: "completed",
input: part.state.input,
title: result.title,
metadata: result.metadata,
output: result.output,
attachments: result.attachments,
time: {
...part.state.time,
end: Date.now(),
},
},
} satisfies MessageV2.ToolPart)
},
async ask(req) {
await PermissionNext.ask({
...req,
sessionID: sessionID,
ruleset: PermissionNext.merge(taskAgent.permission, session.permission ?? []),
})
},
}
const result = await taskTool.execute(taskArgs, taskCtx).catch((error) => {
executionError = error
log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
return undefined
})
await Plugin.trigger(
"tool.execute.after",
{
tool: "task",
sessionID,
callID: part.id,
},
result,
)
assistantMessage.finish = "tool-calls"
assistantMessage.time.completed = Date.now()
await Session.updateMessage(assistantMessage)
if (result && part.state.status === "running") {
await Session.updatePart({
...part,
state: {
status: "completed",
input: part.state.input,
title: result.title,
metadata: result.metadata,
output: result.output,
attachments: result.attachments,
time: {
...part.state.time,
end: Date.now(),
},
},
} satisfies MessageV2.ToolPart)
}
if (!result) {
await Session.updatePart({
...part,
state: {
status: "error",
error: executionError ? `Tool execution failed: ${executionError.message}` : "Tool execution failed",
time: {
start: part.state.status === "running" ? part.state.time.start : Date.now(),
end: Date.now(),
}
if (!result) {
await Session.updatePart({
...part,
state: {
status: "error",
error: executionError ? `Tool execution failed: ${executionError.message}` : "Tool execution failed",
time: {
start: part.state.status === "running" ? part.state.time.start : Date.now(),
end: Date.now(),
},
metadata: part.metadata,
input: part.state.input,
},
metadata: part.metadata,
input: part.state.input,
},
} satisfies MessageV2.ToolPart)
} satisfies MessageV2.ToolPart)
}
}

await Promise.all(subtasks.map(executeSubtask))

// Add synthetic user message to prevent certain reasoning models from erroring
// If we create assistant messages w/ out user ones following mid loop thinking signatures
// will be missing and it can cause errors for models like gemini for example
Expand All @@ -462,8 +471,8 @@ export namespace SessionPrompt {
time: {
created: Date.now(),
},
agent: lastUser.agent,
model: lastUser.model,
agent: subtasks[0]?.parentAgent ?? lastUser.agent,
model: subtasks[0]?.parentModel ?? lastUser.model,
}
await Session.updateMessage(summaryUserMsg)
await Session.updatePart({
Expand All @@ -478,6 +487,8 @@ export namespace SessionPrompt {
continue
}

const task = otherTasks.pop()

// pending compaction
if (task?.type === "compaction") {
const result = await SessionCompaction.process({
Expand Down Expand Up @@ -812,7 +823,7 @@ export namespace SessionPrompt {
},
tools: input.tools,
agent: agent.name,
model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
model: input.model ?? agent.model ?? (await lastModel(input.sessionID)) ?? (await Provider.defaultModel()),
system: input.system,
variant: input.variant,
}
Expand Down Expand Up @@ -1222,7 +1233,7 @@ export namespace SessionPrompt {
SessionRevert.cleanup(session)
}
const agent = await Agent.get(input.agent)
const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
const model = input.model ?? agent.model ?? (await lastModel(input.sessionID)) ?? (await Provider.defaultModel())
const userMsg: MessageV2.User = {
id: Identifier.ascending("message"),
sessionID: input.sessionID,
Expand Down Expand Up @@ -1505,6 +1516,7 @@ export namespace SessionPrompt {
}
template = template.trim()

const sessionModel = await lastModel(input.sessionID)
const model = await (async () => {
if (command.model) {
return Provider.parseModel(command.model)
Expand All @@ -1516,7 +1528,7 @@ export namespace SessionPrompt {
}
}
if (input.model) return Provider.parseModel(input.model)
return await lastModel(input.sessionID)
return sessionModel ?? (await Provider.defaultModel())
})()

try {
Expand All @@ -1543,6 +1555,8 @@ export namespace SessionPrompt {
})
throw error
}
const parentAgent = input.agent ?? "build"
const parentModel = input.model ? Provider.parseModel(input.model) : sessionModel

const templateParts = await resolvePromptParts(template)
const parts =
Expand All @@ -1553,12 +1567,25 @@ export namespace SessionPrompt {
agent: agent.name,
description: command.description ?? "",
command: input.command,
model: { providerID: model.providerID, modelID: model.modelID },
parentAgent,
parentModel,
// TODO: how can we make task tool accept a more complex input?
prompt: templateParts.find((y) => y.type === "text")?.text ?? "",
},
]
: [...templateParts, ...(input.parts ?? [])]

await Plugin.trigger(
"command.execute.before",
{
command: input.command,
sessionID: input.sessionID,
arguments: input.arguments,
},
{ parts },
)

const result = (await prompt({
sessionID: input.sessionID,
messageID: input.messageID,
Expand Down
Loading