2025-09-13 09:46:14 +00:00
import path from "path"
import os from "os"
import fs from "fs/promises"
2025-10-26 19:50:41 +00:00
import z from "zod"
2026-02-19 16:32:32 +00:00
import { Filesystem } from "../util/filesystem"
2026-03-11 23:40:50 +00:00
import { SessionID , MessageID , PartID } from "./schema"
2025-09-13 09:46:14 +00:00
import { MessageV2 } from "./message-v2"
import { Log } from "../util/log"
import { SessionRevert } from "./revert"
import { Session } from "."
import { Agent } from "../agent/agent"
import { Provider } from "../provider/provider"
2026-02-03 18:22:00 +00:00
import { type Tool as AITool , tool , jsonSchema , type ToolCallOptions , asSchema } from "ai"
2025-09-13 09:46:14 +00:00
import { SessionCompaction } from "./compaction"
import { Instance } from "../project/instance"
import { Bus } from "../bus"
import { ProviderTransform } from "../provider/transform"
import { SystemPrompt } from "./system"
2026-01-26 15:49:41 +00:00
import { InstructionPrompt } from "./instruction"
2025-09-13 09:46:14 +00:00
import { Plugin } from "../plugin"
import PROMPT_PLAN from "../session/prompt/plan.txt"
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
2025-12-05 18:26:44 +00:00
import MAX_STEPS from "../session/prompt/max-steps.txt"
2025-09-13 09:46:14 +00:00
import { defer } from "../util/defer"
import { ToolRegistry } from "../tool/registry"
import { MCP } from "../mcp"
import { LSP } from "../lsp"
import { ReadTool } from "../tool/read"
import { FileTime } from "../file/time"
2025-12-17 16:35:43 +00:00
import { Flag } from "../flag/flag"
2025-09-13 09:46:14 +00:00
import { ulid } from "ulid"
import { spawn } from "child_process"
import { Command } from "../command"
2026-03-09 18:52:25 +00:00
import { $ } from "bun"
import { pathToFileURL , fileURLToPath } from "url"
2025-09-27 06:53:20 +00:00
import { ConfigMarkdown } from "../config/markdown"
2025-10-23 20:28:20 +00:00
import { SessionSummary } from "./summary"
2025-11-24 17:56:00 +00:00
import { NamedError } from "@opencode-ai/util/error"
2025-11-17 15:57:18 +00:00
import { fn } from "@/util/fn"
import { SessionProcessor } from "./processor"
2026-01-07 19:28:13 +00:00
import { TaskTool } from "@/tool/task"
2026-01-01 22:54:11 +00:00
import { Tool } from "@/tool/tool"
import { PermissionNext } from "@/permission/next"
2025-11-17 15:57:18 +00:00
import { SessionStatus } from "./status"
2025-12-15 02:11:30 +00:00
import { LLM } from "./llm"
import { iife } from "@/util/iife"
2025-12-12 22:11:07 +00:00
import { Shell } from "@/shell/shell"
2026-01-20 05:19:21 +00:00
import { Truncate } from "@/tool/truncation"
2025-09-13 09:46:14 +00:00
2025-11-21 05:21:06 +00:00
// @ts-ignore
globalThis . AI_SDK_LOG_WARNINGS = false
2026-02-12 04:54:05 +00:00
const STRUCTURED_OUTPUT_DESCRIPTION = ` Use this tool to return your final response in the requested structured format.
IMPORTANT :
- You MUST call this tool exactly once at the end of your response
- The input must be valid JSON matching the required schema
- Complete all necessary research and tool calls BEFORE calling this tool
- This tool provides your final answer - no further actions are taken after calling it `
const STRUCTURED_OUTPUT_SYSTEM_PROMPT = ` IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema. `
2025-09-13 09:46:14 +00:00
export namespace SessionPrompt {
const log = Log . create ( { service : "session.prompt" } )
const state = Instance . state (
( ) = > {
2025-11-17 15:57:18 +00:00
const data : Record <
2025-09-13 09:46:14 +00:00
string ,
{
2025-11-17 15:57:18 +00:00
abort : AbortController
callbacks : {
resolve ( input : MessageV2.WithParts ) : void
2026-02-02 01:39:58 +00:00
reject ( reason? : any ) : void
2025-11-17 15:57:18 +00:00
} [ ]
}
> = { }
return data
2025-09-13 09:46:14 +00:00
} ,
2025-10-15 06:12:51 +00:00
async ( current ) = > {
2025-11-17 15:57:18 +00:00
for ( const item of Object . values ( current ) ) {
item . abort . abort ( )
}
2025-09-13 09:46:14 +00:00
} ,
)
2026-03-11 23:16:56 +00:00
export function assertNotBusy ( sessionID : SessionID ) {
2025-11-17 15:57:18 +00:00
const match = state ( ) [ sessionID ]
if ( match ) throw new Session . BusyError ( sessionID )
}
2025-09-13 09:46:14 +00:00
export const PromptInput = z . object ( {
2026-03-11 23:16:56 +00:00
sessionID : SessionID.zod ,
2026-03-11 23:30:17 +00:00
messageID : MessageID.zod.optional ( ) ,
2025-09-13 09:46:14 +00:00
model : z
. object ( {
providerID : z.string ( ) ,
modelID : z.string ( ) ,
} )
. optional ( ) ,
agent : z.string ( ) . optional ( ) ,
2025-10-25 19:56:54 +00:00
noReply : z.boolean ( ) . optional ( ) ,
2026-01-01 22:54:11 +00:00
tools : z
. record ( z . string ( ) , z . boolean ( ) )
. optional ( )
. describe (
"@deprecated tools and permissions have been merged, you can set permissions on the session itself now" ,
) ,
2026-02-12 04:54:05 +00:00
format : MessageV2.Format.optional ( ) ,
2025-12-15 02:11:30 +00:00
system : z.string ( ) . optional ( ) ,
2025-12-30 03:43:50 +00:00
variant : z.string ( ) . optional ( ) ,
2025-09-13 09:46:14 +00:00
parts : z.array (
z . discriminatedUnion ( "type" , [
MessageV2 . TextPart . omit ( {
messageID : true ,
sessionID : true ,
} )
. partial ( {
id : true ,
} )
2025-09-15 07:12:07 +00:00
. meta ( {
2025-09-13 09:46:14 +00:00
ref : "TextPartInput" ,
} ) ,
MessageV2 . FilePart . omit ( {
messageID : true ,
sessionID : true ,
} )
. partial ( {
id : true ,
} )
2025-09-15 07:12:07 +00:00
. meta ( {
2025-09-13 09:46:14 +00:00
ref : "FilePartInput" ,
} ) ,
MessageV2 . AgentPart . omit ( {
messageID : true ,
sessionID : true ,
} )
. partial ( {
id : true ,
} )
2025-09-15 07:12:07 +00:00
. meta ( {
2025-09-13 09:46:14 +00:00
ref : "AgentPartInput" ,
} ) ,
2025-11-17 15:57:18 +00:00
MessageV2 . SubtaskPart . omit ( {
messageID : true ,
sessionID : true ,
} )
. partial ( {
id : true ,
} )
. meta ( {
ref : "SubtaskPartInput" ,
} ) ,
2025-09-13 09:46:14 +00:00
] ) ,
) ,
} )
export type PromptInput = z . infer < typeof PromptInput >
2025-11-12 01:38:50 +00:00
2025-12-15 02:11:30 +00:00
export const prompt = fn ( PromptInput , async ( input ) = > {
const session = await Session . get ( input . sessionID )
await SessionRevert . cleanup ( session )
const message = await createUserMessage ( input )
await Session . touch ( input . sessionID )
2026-01-01 22:54:11 +00:00
// this is backwards compatibility for allowing `tools` to be specified when
// prompting
const permissions : PermissionNext.Ruleset = [ ]
for ( const [ tool , enabled ] of Object . entries ( input . tools ? ? { } ) ) {
permissions . push ( {
permission : tool ,
action : enabled ? "allow" : "deny" ,
pattern : "*" ,
} )
}
if ( permissions . length > 0 ) {
session . permission = permissions
2026-02-14 04:19:02 +00:00
await Session . setPermission ( { sessionID : session.id , permission : permissions } )
2026-01-01 22:54:11 +00:00
}
2025-12-15 02:11:30 +00:00
if ( input . noReply === true ) {
return message
}
2026-02-06 22:14:34 +00:00
return loop ( { sessionID : input.sessionID } )
2025-12-15 02:11:30 +00:00
} )
2025-11-12 01:38:50 +00:00
export async function resolvePromptParts ( template : string ) : Promise < PromptInput [ " parts " ] > {
const parts : PromptInput [ "parts" ] = [
{
type : "text" ,
text : template ,
} ,
]
2026-02-02 21:28:02 +00:00
const files = ConfigMarkdown . files ( template )
2025-11-26 17:26:05 +00:00
const seen = new Set < string > ( )
2026-02-02 21:28:02 +00:00
await Promise . all (
files . map ( async ( match ) = > {
const name = match [ 1 ]
if ( seen . has ( name ) ) return
2025-11-26 17:26:05 +00:00
seen . add ( name )
2025-11-12 01:38:50 +00:00
const filepath = name . startsWith ( "~/" )
? path . join ( os . homedir ( ) , name . slice ( 2 ) )
: path . resolve ( Instance . worktree , name )
const stats = await fs . stat ( filepath ) . catch ( ( ) = > undefined )
if ( ! stats ) {
const agent = await Agent . get ( name )
2026-02-02 21:28:02 +00:00
if ( agent ) {
parts . push ( {
type : "agent" ,
name : agent.name ,
} )
}
return
2025-11-12 01:38:50 +00:00
}
if ( stats . isDirectory ( ) ) {
2026-02-02 21:28:02 +00:00
parts . push ( {
2025-11-12 01:38:50 +00:00
type : "file" ,
2026-02-06 22:16:56 +00:00
url : pathToFileURL ( filepath ) . href ,
2025-11-12 01:38:50 +00:00
filename : name ,
mime : "application/x-directory" ,
2026-02-02 21:28:02 +00:00
} )
return
2025-11-12 01:38:50 +00:00
}
2026-02-02 21:28:02 +00:00
parts . push ( {
2025-11-12 01:38:50 +00:00
type : "file" ,
2026-02-06 22:16:56 +00:00
url : pathToFileURL ( filepath ) . href ,
2025-11-12 01:38:50 +00:00
filename : name ,
mime : "text/plain" ,
2026-02-02 21:28:02 +00:00
} )
2025-11-12 01:38:50 +00:00
} ) ,
)
return parts
}
2025-09-13 09:46:14 +00:00
2025-11-17 15:57:18 +00:00
function start ( sessionID : string ) {
const s = state ( )
if ( s [ sessionID ] ) return
const controller = new AbortController ( )
s [ sessionID ] = {
abort : controller ,
callbacks : [ ] ,
2025-10-25 19:56:54 +00:00
}
2025-11-17 15:57:18 +00:00
return controller . signal
}
2025-10-25 19:56:54 +00:00
2026-02-06 22:13:11 +00:00
function resume ( sessionID : string ) {
const s = state ( )
if ( ! s [ sessionID ] ) return
return s [ sessionID ] . abort . signal
}
2026-03-11 23:16:56 +00:00
export function cancel ( sessionID : SessionID ) {
2025-11-17 15:57:18 +00:00
log . info ( "cancel" , { sessionID } )
const s = state ( )
const match = s [ sessionID ]
2026-02-02 01:39:58 +00:00
if ( ! match ) {
SessionStatus . set ( sessionID , { type : "idle" } )
return
}
2025-11-17 15:57:18 +00:00
match . abort . abort ( )
delete s [ sessionID ]
SessionStatus . set ( sessionID , { type : "idle" } )
return
}
2026-02-06 22:13:11 +00:00
export const LoopInput = z . object ( {
2026-03-11 23:16:56 +00:00
sessionID : SessionID.zod ,
2026-02-06 22:13:11 +00:00
resume_existing : z.boolean ( ) . optional ( ) ,
} )
export const loop = fn ( LoopInput , async ( input ) = > {
const { sessionID , resume_existing } = input
const abort = resume_existing ? resume ( sessionID ) : start ( sessionID )
2025-11-17 15:57:18 +00:00
if ( ! abort ) {
return new Promise < MessageV2.WithParts > ( ( resolve , reject ) = > {
const callbacks = state ( ) [ sessionID ] . callbacks
callbacks . push ( { resolve , reject } )
2025-09-13 09:46:14 +00:00
} )
}
2025-10-05 04:38:41 +00:00
2025-11-17 15:57:18 +00:00
using _ = defer ( ( ) = > cancel ( sessionID ) )
2025-09-13 09:46:14 +00:00
2026-02-12 04:54:05 +00:00
// Structured output state
// Note: On session resumption, state is reset but outputFormat is preserved
// on the user message and will be retrieved from lastUser below
let structuredOutput : unknown | undefined
2025-11-17 15:57:18 +00:00
let step = 0
2026-01-01 22:54:11 +00:00
const session = await Session . get ( sessionID )
2025-11-17 15:57:18 +00:00
while ( true ) {
2025-11-21 05:21:06 +00:00
SessionStatus . set ( sessionID , { type : "busy" } )
2025-11-17 15:57:18 +00:00
log . info ( "loop" , { step , sessionID } )
if ( abort . aborted ) break
let msgs = await MessageV2 . filterCompacted ( MessageV2 . stream ( sessionID ) )
let lastUser : MessageV2.User | undefined
let lastAssistant : MessageV2.Assistant | undefined
let lastFinished : MessageV2.Assistant | undefined
let tasks : ( MessageV2 . CompactionPart | MessageV2 . SubtaskPart ) [ ] = [ ]
for ( let i = msgs . length - 1 ; i >= 0 ; i -- ) {
const msg = msgs [ i ]
if ( ! lastUser && msg . info . role === "user" ) lastUser = msg . info as MessageV2 . User
if ( ! lastAssistant && msg . info . role === "assistant" ) lastAssistant = msg . info as MessageV2 . Assistant
if ( ! lastFinished && msg . info . role === "assistant" && msg . info . finish )
lastFinished = msg . info as MessageV2 . Assistant
if ( lastUser && lastFinished ) break
const task = msg . parts . filter ( ( part ) = > part . type === "compaction" || part . type === "subtask" )
if ( task && ! lastFinished ) {
tasks . push ( . . . task )
}
}
2025-09-13 09:46:14 +00:00
2025-11-17 15:57:18 +00:00
if ( ! lastUser ) throw new Error ( "No user message found in stream. This should never happen." )
2025-11-21 21:51:32 +00:00
if (
lastAssistant ? . finish &&
! [ "tool-calls" , "unknown" ] . includes ( lastAssistant . finish ) &&
lastUser . id < lastAssistant . id
) {
2025-11-17 15:57:18 +00:00
log . info ( "exiting loop" , { sessionID } )
break
}
2025-09-13 09:46:14 +00:00
2025-11-17 15:57:18 +00:00
step ++
if ( step === 1 )
ensureTitle ( {
2026-01-01 22:54:11 +00:00
session ,
2025-11-17 15:57:18 +00:00
modelID : lastUser.model.modelID ,
providerID : lastUser.model.providerID ,
history : msgs ,
} )
2025-09-13 09:46:14 +00:00
2026-02-09 22:27:48 +00:00
const model = await Provider . getModel ( lastUser . model . providerID , lastUser . model . modelID ) . catch ( ( e ) = > {
if ( Provider . ModelNotFoundError . isInstance ( e ) ) {
const hint = e . data . suggestions ? . length ? ` Did you mean: ${ e . data . suggestions . join ( ", " ) } ? ` : ""
Bus . publish ( Session . Event . Error , {
sessionID ,
error : new NamedError . Unknown ( {
message : ` Model not found: ${ e . data . providerID } / ${ e . data . modelID } . ${ hint } ` ,
} ) . toObject ( ) ,
} )
}
throw e
} )
2025-11-17 15:57:18 +00:00
const task = tasks . pop ( )
// pending subtask
// TODO: centralize "invoke tool" logic
if ( task ? . type === "subtask" ) {
const taskTool = await TaskTool . init ( )
2026-01-15 23:18:39 +00:00
const taskModel = task . model ? await Provider . getModel ( task . model . providerID , task . model . modelID ) : model
2025-11-17 15:57:18 +00:00
const assistantMessage = ( await Session . updateMessage ( {
2026-03-11 23:30:17 +00:00
id : MessageID.ascending ( ) ,
2025-11-17 15:57:18 +00:00
role : "assistant" ,
parentID : lastUser.id ,
sessionID ,
mode : task.agent ,
2025-12-15 02:11:30 +00:00
agent : task.agent ,
2026-02-06 22:33:47 +00:00
variant : lastUser.variant ,
2025-11-17 15:57:18 +00:00
path : {
cwd : Instance.directory ,
root : Instance.worktree ,
} ,
cost : 0 ,
tokens : {
input : 0 ,
output : 0 ,
reasoning : 0 ,
cache : { read : 0 , write : 0 } ,
} ,
2026-01-15 23:18:39 +00:00
modelID : taskModel.id ,
providerID : taskModel.providerID ,
2025-11-17 15:57:18 +00:00
time : {
created : Date.now ( ) ,
} ,
} ) ) as MessageV2 . Assistant
let part = ( await Session . updatePart ( {
2026-03-11 23:40:50 +00:00
id : PartID.ascending ( ) ,
2025-11-17 15:57:18 +00:00
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 ,
2025-12-17 01:28:09 +00:00
command : task.command ,
2025-11-17 15:57:18 +00:00
} ,
time : {
start : Date.now ( ) ,
} ,
} ,
} ) ) as MessageV2 . ToolPart
2025-12-17 01:28:09 +00:00
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 } ,
)
2025-12-10 23:12:49 +00:00
let executionError : Error | undefined
2026-01-01 22:54:11 +00:00
const taskAgent = await Agent . get ( task . agent )
const taskCtx : Tool.Context = {
agent : task.agent ,
messageID : assistantMessage.id ,
sessionID : sessionID ,
abort ,
2026-01-07 04:29:17 +00:00
callID : part.callID ,
2026-01-07 19:28:13 +00:00
extra : { bypassAgentCheck : true } ,
2026-01-26 15:49:41 +00:00
messages : msgs ,
2026-01-01 22:54:11 +00:00
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
} )
2026-02-16 20:59:57 +00:00
const attachments = result ? . attachments ? . map ( ( attachment ) = > ( {
. . . attachment ,
2026-03-11 23:40:50 +00:00
id : PartID.ascending ( ) ,
2026-02-16 20:59:57 +00:00
sessionID ,
messageID : assistantMessage.id ,
} ) )
2025-12-17 01:28:09 +00:00
await Plugin . trigger (
"tool.execute.after" ,
{
tool : "task" ,
sessionID ,
callID : part.id ,
2026-02-12 14:54:47 +00:00
args : taskArgs ,
2025-12-17 01:28:09 +00:00
} ,
result ,
)
2025-11-17 15:57:18 +00:00
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 ,
2026-02-16 20:59:57 +00:00
attachments ,
2025-11-17 15:57:18 +00:00
time : {
. . . part . state . time ,
end : Date.now ( ) ,
} ,
} ,
} satisfies MessageV2 . ToolPart )
}
if ( ! result ) {
await Session . updatePart ( {
. . . part ,
state : {
status : "error" ,
2025-12-10 23:12:49 +00:00
error : executionError ? ` Tool execution failed: ${ executionError . message } ` : "Tool execution failed" ,
2025-11-17 15:57:18 +00:00
time : {
start : part.state.status === "running" ? part.state.time.start : Date.now ( ) ,
end : Date.now ( ) ,
} ,
metadata : part.metadata ,
input : part.state.input ,
} ,
} satisfies MessageV2 . ToolPart )
}
2025-12-16 21:42:21 +00:00
2026-01-21 04:51:54 +00:00
if ( task . command ) {
// 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
const summaryUserMsg : MessageV2.User = {
2026-03-11 23:30:17 +00:00
id : MessageID.ascending ( ) ,
2026-01-21 04:51:54 +00:00
sessionID ,
role : "user" ,
time : {
created : Date.now ( ) ,
} ,
agent : lastUser.agent ,
model : lastUser.model ,
}
await Session . updateMessage ( summaryUserMsg )
await Session . updatePart ( {
2026-03-11 23:40:50 +00:00
id : PartID.ascending ( ) ,
2026-01-21 04:51:54 +00:00
messageID : summaryUserMsg.id ,
sessionID ,
type : "text" ,
text : "Summarize the task tool output above and continue with your task." ,
synthetic : true ,
} satisfies MessageV2 . TextPart )
2025-12-16 21:42:21 +00:00
}
2025-11-17 15:57:18 +00:00
continue
}
2025-09-13 09:46:14 +00:00
2025-11-17 15:57:18 +00:00
// pending compaction
if ( task ? . type === "compaction" ) {
2025-11-18 18:09:50 +00:00
const result = await SessionCompaction . process ( {
2025-11-17 15:57:18 +00:00
messages : msgs ,
parentID : lastUser.id ,
abort ,
sessionID ,
2025-11-25 18:10:56 +00:00
auto : task.auto ,
2026-03-02 07:40:55 +00:00
overflow : task.overflow ,
2025-11-17 15:57:18 +00:00
} )
2025-11-18 18:09:50 +00:00
if ( result === "stop" ) break
2025-11-17 15:57:18 +00:00
continue
}
// context overflow, needs compaction
if (
lastFinished &&
lastFinished . summary !== true &&
2025-12-27 00:31:42 +00:00
( await SessionCompaction . isOverflow ( { tokens : lastFinished.tokens , model } ) )
2025-11-17 15:57:18 +00:00
) {
await SessionCompaction . create ( {
sessionID ,
2025-11-21 08:13:10 +00:00
agent : lastUser.agent ,
2025-11-17 15:57:18 +00:00
model : lastUser.model ,
2025-11-25 18:10:56 +00:00
auto : true ,
2025-11-17 15:57:18 +00:00
} )
continue
}
// normal processing
const agent = await Agent . get ( lastUser . agent )
2026-01-01 22:54:11 +00:00
const maxSteps = agent . steps ? ? Infinity
2025-12-05 18:26:44 +00:00
const isLastStep = step >= maxSteps
2026-01-13 20:55:48 +00:00
msgs = await insertReminders ( {
2025-11-17 15:57:18 +00:00
messages : msgs ,
agent ,
2026-01-13 20:55:48 +00:00
session ,
2025-11-17 15:57:18 +00:00
} )
2025-12-15 02:11:30 +00:00
2025-11-17 15:57:18 +00:00
const processor = SessionProcessor . create ( {
assistantMessage : ( await Session . updateMessage ( {
2026-03-11 23:30:17 +00:00
id : MessageID.ascending ( ) ,
2025-11-17 15:57:18 +00:00
parentID : lastUser.id ,
role : "assistant" ,
mode : agent.name ,
2025-12-15 02:11:30 +00:00
agent : agent.name ,
2026-02-06 22:33:47 +00:00
variant : lastUser.variant ,
2025-11-17 15:57:18 +00:00
path : {
cwd : Instance.directory ,
root : Instance.worktree ,
} ,
cost : 0 ,
tokens : {
input : 0 ,
output : 0 ,
reasoning : 0 ,
cache : { read : 0 , write : 0 } ,
} ,
2025-12-04 02:09:03 +00:00
modelID : model.id ,
2025-09-13 09:46:14 +00:00
providerID : model.providerID ,
2025-11-17 15:57:18 +00:00
time : {
created : Date.now ( ) ,
} ,
sessionID ,
} ) ) as MessageV2 . Assistant ,
sessionID : sessionID ,
2025-12-04 02:09:03 +00:00
model ,
2025-11-17 15:57:18 +00:00
abort ,
} )
2026-01-28 06:38:10 +00:00
using _ = defer ( ( ) = > InstructionPrompt . clear ( processor . message . id ) )
2026-01-07 04:29:17 +00:00
2026-01-07 19:28:13 +00:00
// Check if user explicitly invoked an agent via @ in this turn
const lastUserMsg = msgs . findLast ( ( m ) = > m . info . role === "user" )
const bypassAgentCheck = lastUserMsg ? . parts . some ( ( p ) = > p . type === "agent" ) ? ? false
2026-01-07 04:29:17 +00:00
2025-11-17 15:57:18 +00:00
const tools = await resolveTools ( {
agent ,
2026-01-01 22:54:11 +00:00
session ,
2025-12-04 02:09:03 +00:00
model ,
2025-11-17 15:57:18 +00:00
tools : lastUser.tools ,
processor ,
2026-01-07 19:28:13 +00:00
bypassAgentCheck ,
2026-01-26 15:49:41 +00:00
messages : msgs ,
2025-11-17 15:57:18 +00:00
} )
2026-02-12 04:54:05 +00:00
// Inject StructuredOutput tool if JSON schema mode enabled
if ( lastUser . format ? . type === "json_schema" ) {
tools [ "StructuredOutput" ] = createStructuredOutputTool ( {
schema : lastUser.format.schema ,
onSuccess ( output ) {
structuredOutput = output
} ,
} )
}
2025-10-24 16:37:23 +00:00
if ( step === 1 ) {
SessionSummary . summarize ( {
2025-11-17 15:57:18 +00:00
sessionID : sessionID ,
messageID : lastUser.id ,
2025-10-24 16:37:23 +00:00
} )
}
2025-11-17 15:57:18 +00:00
2026-01-03 04:42:56 +00:00
// Ephemerally wrap queued user messages with a reminder to stay on track
if ( step > 1 && lastFinished ) {
2026-02-20 00:19:53 +00:00
for ( const msg of msgs ) {
2026-01-03 04:42:56 +00:00
if ( msg . info . role !== "user" || msg . info . id <= lastFinished . id ) continue
for ( const part of msg . parts ) {
if ( part . type !== "text" || part . ignored || part . synthetic ) continue
if ( ! part . text . trim ( ) ) continue
part . text = [
"<system-reminder>" ,
"The user sent the following message:" ,
part . text ,
"" ,
"Please address this message and continue with your tasks." ,
"</system-reminder>" ,
] . join ( "\n" )
}
}
}
2026-02-20 00:19:53 +00:00
await Plugin . trigger ( "experimental.chat.messages.transform" , { } , { messages : msgs } )
2025-12-15 04:51:11 +00:00
2026-02-12 04:54:05 +00:00
// Build system prompt, adding structured output instruction if needed
2026-03-11 15:24:55 +00:00
const skills = await SystemPrompt . skills ( agent )
const system = [
. . . ( await SystemPrompt . environment ( model ) ) ,
. . . ( skills ? [ skills ] : [ ] ) ,
. . . ( await InstructionPrompt . system ( ) ) ,
]
2026-02-12 04:54:05 +00:00
const format = lastUser . format ? ? { type : "text" }
if ( format . type === "json_schema" ) {
system . push ( STRUCTURED_OUTPUT_SYSTEM_PROMPT )
}
2025-12-04 02:09:03 +00:00
const result = await processor . process ( {
2025-12-15 02:11:30 +00:00
user : lastUser ,
agent ,
abort ,
sessionID ,
2026-02-12 04:54:05 +00:00
system ,
2025-12-15 02:11:30 +00:00
messages : [
2026-02-20 00:19:53 +00:00
. . . MessageV2 . toModelMessages ( msgs , model ) ,
2025-12-15 02:11:30 +00:00
. . . ( isLastStep
? [
{
role : "assistant" as const ,
content : MAX_STEPS ,
} ,
]
: [ ] ) ,
] ,
tools ,
model ,
2026-02-12 04:54:05 +00:00
toolChoice : format.type === "json_schema" ? "required" : undefined ,
2025-12-04 02:09:03 +00:00
} )
2026-02-12 04:54:05 +00:00
// If structured output was captured, save it and exit immediately
// This takes priority because the StructuredOutput tool was called successfully
if ( structuredOutput !== undefined ) {
processor . message . structured = structuredOutput
processor . message . finish = processor . message . finish ? ? "stop"
await Session . updateMessage ( processor . message )
break
}
// Check if model finished (finish reason is not "tool-calls" or "unknown")
const modelFinished = processor . message . finish && ! [ "tool-calls" , "unknown" ] . includes ( processor . message . finish )
if ( modelFinished && ! processor . message . error ) {
if ( format . type === "json_schema" ) {
// Model stopped without calling StructuredOutput tool
processor . message . error = new MessageV2 . StructuredOutputError ( {
message : "Model did not produce structured output" ,
retries : 0 ,
} ) . toObject ( )
await Session . updateMessage ( processor . message )
break
}
}
2025-11-17 15:57:18 +00:00
if ( result === "stop" ) break
2026-01-01 18:03:18 +00:00
if ( result === "compact" ) {
await SessionCompaction . create ( {
sessionID ,
agent : lastUser.agent ,
model : lastUser.model ,
auto : true ,
2026-03-02 07:40:55 +00:00
overflow : ! processor . message . finish ,
2026-01-01 18:03:18 +00:00
} )
}
2025-11-17 15:57:18 +00:00
continue
}
SessionCompaction . prune ( { sessionID } )
for await ( const item of MessageV2 . stream ( sessionID ) ) {
if ( item . info . role === "user" ) continue
const queued = state ( ) [ sessionID ] ? . callbacks ? ? [ ]
for ( const q of queued ) {
q . resolve ( item )
2025-09-18 03:27:37 +00:00
}
2025-11-17 15:57:18 +00:00
return item
2025-09-13 09:46:14 +00:00
}
2025-11-17 15:57:18 +00:00
throw new Error ( "Impossible" )
} )
2025-09-13 09:46:14 +00:00
2026-03-11 23:16:56 +00:00
async function lastModel ( sessionID : SessionID ) {
2025-11-19 18:10:09 +00:00
for await ( const item of MessageV2 . stream ( sessionID ) ) {
if ( item . info . role === "user" && item . info . model ) return item . info . model
2025-09-13 09:46:14 +00:00
}
return Provider . defaultModel ( )
}
2026-02-12 04:54:05 +00:00
/** @internal Exported for testing */
export async function resolveTools ( input : {
2025-09-13 09:46:14 +00:00
agent : Agent.Info
2025-12-04 02:09:03 +00:00
model : Provider.Model
2026-01-01 22:54:11 +00:00
session : Session.Info
2025-09-13 09:46:14 +00:00
tools? : Record < string , boolean >
2025-11-17 15:57:18 +00:00
processor : SessionProcessor.Info
2026-01-07 19:28:13 +00:00
bypassAgentCheck : boolean
2026-01-26 15:49:41 +00:00
messages : MessageV2.WithParts [ ]
2025-09-13 09:46:14 +00:00
} ) {
2025-12-15 02:11:30 +00:00
using _ = log . time ( "resolveTools" )
2025-09-13 09:46:14 +00:00
const tools : Record < string , AITool > = { }
2026-01-01 22:54:11 +00:00
const context = ( args : any , options : ToolCallOptions ) : Tool . Context = > ( {
sessionID : input.session.id ,
abort : options.abortSignal ! ,
messageID : input.processor.message.id ,
callID : options.toolCallId ,
2026-01-07 19:28:13 +00:00
extra : { model : input.model , bypassAgentCheck : input.bypassAgentCheck } ,
2026-01-01 22:54:11 +00:00
agent : input.agent.name ,
2026-01-26 15:49:41 +00:00
messages : input.messages ,
2026-01-01 22:54:11 +00:00
metadata : async ( val : { title? : string ; metadata? : any } ) = > {
const match = input . processor . partFromToolCall ( options . toolCallId )
if ( match && match . state . status === "running" ) {
await Session . updatePart ( {
. . . match ,
state : {
title : val.title ,
metadata : val.metadata ,
status : "running" ,
input : args ,
time : {
start : Date.now ( ) ,
} ,
} ,
} )
}
} ,
async ask ( req ) {
await PermissionNext . ask ( {
. . . req ,
sessionID : input.session.id ,
tool : { messageID : input.processor.message.id , callID : options.toolCallId } ,
ruleset : PermissionNext.merge ( input . agent . permission , input . session . permission ? ? [ ] ) ,
} )
} ,
} )
2026-01-18 06:35:09 +00:00
for ( const item of await ToolRegistry . tools (
{ modelID : input.model.api.id , providerID : input.model.providerID } ,
input . agent ,
) ) {
2025-12-04 02:09:03 +00:00
const schema = ProviderTransform . schema ( input . model , z . toJSONSchema ( item . parameters ) )
2025-09-13 09:46:14 +00:00
tools [ item . id ] = tool ( {
id : item.id as any ,
description : item.description ,
2025-09-15 07:12:07 +00:00
inputSchema : jsonSchema ( schema as any ) ,
2025-09-13 09:46:14 +00:00
async execute ( args , options ) {
2026-01-01 22:54:11 +00:00
const ctx = context ( args , options )
2025-09-13 09:46:14 +00:00
await Plugin . trigger (
"tool.execute.before" ,
{
tool : item.id ,
2026-01-01 22:54:11 +00:00
sessionID : ctx.sessionID ,
callID : ctx.callID ,
2025-09-13 09:46:14 +00:00
} ,
{
args ,
} ,
)
2026-01-01 22:54:11 +00:00
const result = await item . execute ( args , ctx )
2026-02-16 20:59:57 +00:00
const output = {
. . . result ,
attachments : result.attachments?.map ( ( attachment ) = > ( {
. . . attachment ,
2026-03-11 23:40:50 +00:00
id : PartID.ascending ( ) ,
2026-02-16 20:59:57 +00:00
sessionID : ctx.sessionID ,
messageID : input.processor.message.id ,
} ) ) ,
}
2025-09-13 09:46:14 +00:00
await Plugin . trigger (
"tool.execute.after" ,
{
tool : item.id ,
2026-01-01 22:54:11 +00:00
sessionID : ctx.sessionID ,
callID : ctx.callID ,
2026-02-12 14:54:47 +00:00
args ,
2025-09-13 09:46:14 +00:00
} ,
2026-02-16 20:59:57 +00:00
output ,
2025-09-13 09:46:14 +00:00
)
2026-02-16 20:59:57 +00:00
return output
2025-09-13 09:46:14 +00:00
} ,
} )
}
2026-01-01 22:54:11 +00:00
2025-09-13 09:46:14 +00:00
for ( const [ key , item ] of Object . entries ( await MCP . tools ( ) ) ) {
const execute = item . execute
if ( ! execute ) continue
2025-11-26 06:58:20 +00:00
2026-02-03 18:22:00 +00:00
const transformed = ProviderTransform . schema ( input . model , asSchema ( item . inputSchema ) . jsonSchema )
item . inputSchema = jsonSchema ( transformed )
2025-11-26 06:58:20 +00:00
// Wrap execute to add plugin hooks and format output
2025-09-13 09:46:14 +00:00
item . execute = async ( args , opts ) = > {
2026-01-01 22:54:11 +00:00
const ctx = context ( args , opts )
2025-09-13 09:46:14 +00:00
await Plugin . trigger (
"tool.execute.before" ,
{
tool : key ,
2026-01-01 22:54:11 +00:00
sessionID : ctx.sessionID ,
2025-09-13 09:46:14 +00:00
callID : opts.toolCallId ,
} ,
{
args ,
} ,
)
2026-01-01 22:54:11 +00:00
await ctx . ask ( {
permission : key ,
metadata : { } ,
patterns : [ "*" ] ,
always : [ "*" ] ,
} )
2025-10-29 15:19:02 +00:00
const result = await execute ( args , opts )
2025-10-23 15:38:55 +00:00
2025-09-13 09:46:14 +00:00
await Plugin . trigger (
"tool.execute.after" ,
{
tool : key ,
2026-01-01 22:54:11 +00:00
sessionID : ctx.sessionID ,
2025-09-13 09:46:14 +00:00
callID : opts.toolCallId ,
2026-02-12 14:54:47 +00:00
args ,
2025-09-13 09:46:14 +00:00
} ,
result ,
)
2025-11-14 21:00:52 +00:00
const textParts : string [ ] = [ ]
2026-02-16 20:59:57 +00:00
const attachments : Omit < MessageV2.FilePart , " id " | " sessionID " | " messageID " > [ ] = [ ]
2025-11-14 21:00:52 +00:00
2025-11-26 06:58:20 +00:00
for ( const contentItem of result . content ) {
if ( contentItem . type === "text" ) {
textParts . push ( contentItem . text )
} else if ( contentItem . type === "image" ) {
2025-11-14 21:00:52 +00:00
attachments . push ( {
type : "file" ,
2025-11-26 06:58:20 +00:00
mime : contentItem.mimeType ,
url : ` data: ${ contentItem . mimeType } ;base64, ${ contentItem . data } ` ,
2025-11-14 21:00:52 +00:00
} )
2026-01-12 03:39:42 +00:00
} else if ( contentItem . type === "resource" ) {
const { resource } = contentItem
if ( resource . text ) {
textParts . push ( resource . text )
}
if ( resource . blob ) {
attachments . push ( {
type : "file" ,
mime : resource.mimeType ? ? "application/octet-stream" ,
url : ` data: ${ resource . mimeType ? ? "application/octet-stream" } ;base64, ${ resource . blob } ` ,
filename : resource.uri ,
} )
}
2025-11-14 21:00:52 +00:00
}
}
2025-10-23 15:38:55 +00:00
2026-01-20 05:19:21 +00:00
const truncated = await Truncate . output ( textParts . join ( "\n\n" ) , { } , input . agent )
const metadata = {
. . . ( result . metadata ? ? { } ) ,
truncated : truncated.truncated ,
. . . ( truncated . truncated && { outputPath : truncated.outputPath } ) ,
}
2025-09-13 09:46:14 +00:00
return {
2025-10-07 05:45:46 +00:00
title : "" ,
2026-01-20 05:19:21 +00:00
metadata ,
output : truncated.content ,
2026-02-20 00:26:29 +00:00
attachments : attachments.map ( ( attachment ) = > ( {
. . . attachment ,
2026-03-11 23:40:50 +00:00
id : PartID.ascending ( ) ,
2026-02-20 00:26:29 +00:00
sessionID : ctx.sessionID ,
messageID : input.processor.message.id ,
} ) ) ,
2025-11-14 21:00:52 +00:00
content : result.content , // directly return content to preserve ordering when outputting to model
2025-09-13 09:46:14 +00:00
}
}
tools [ key ] = item
}
2026-01-07 04:29:17 +00:00
2025-09-13 09:46:14 +00:00
return tools
}
2026-02-12 04:54:05 +00:00
/** @internal Exported for testing */
export function createStructuredOutputTool ( input : {
schema : Record < string , any >
onSuccess : ( output : unknown ) = > void
} ) : AITool {
// Remove $schema property if present (not needed for tool input)
const { $schema , . . . toolSchema } = input . schema
return tool ( {
id : "StructuredOutput" as any ,
description : STRUCTURED_OUTPUT_DESCRIPTION ,
inputSchema : jsonSchema ( toolSchema as any ) ,
async execute ( args ) {
// AI SDK validates args against inputSchema before calling execute()
input . onSuccess ( args )
return {
output : "Structured output captured successfully." ,
title : "Structured Output" ,
metadata : { valid : true } ,
}
} ,
toModelOutput ( result ) {
return {
type : "text" ,
value : result.output ,
}
} ,
} )
}
2025-09-13 09:46:14 +00:00
async function createUserMessage ( input : PromptInput ) {
2025-12-20 17:46:48 +00:00
const agent = await Agent . get ( input . agent ? ? ( await Agent . defaultAgent ( ) ) )
2026-02-01 20:12:30 +00:00
const model = input . model ? ? agent . model ? ? ( await lastModel ( input . sessionID ) )
2026-02-09 17:00:06 +00:00
const full =
! input . variant && agent . variant
? await Provider . getModel ( model . providerID , model . modelID ) . catch ( ( ) = > undefined )
: undefined
const variant = input . variant ? ? ( agent . variant && full ? . variants ? . [ agent . variant ] ? agent.variant : undefined )
2026-02-01 20:12:30 +00:00
2025-09-13 09:46:14 +00:00
const info : MessageV2.Info = {
2026-03-11 23:30:17 +00:00
id : input.messageID ? ? MessageID . ascending ( ) ,
2025-09-13 09:46:14 +00:00
role : "user" ,
sessionID : input.sessionID ,
time : {
created : Date.now ( ) ,
} ,
2025-11-17 15:57:18 +00:00
tools : input.tools ,
agent : agent.name ,
2026-02-01 20:12:30 +00:00
model ,
2025-12-16 16:52:22 +00:00
system : input.system ,
2026-02-12 04:54:05 +00:00
format : input.format ,
2026-02-01 20:12:30 +00:00
variant ,
2025-09-13 09:46:14 +00:00
}
2026-01-28 06:38:10 +00:00
using _ = defer ( ( ) = > InstructionPrompt . clear ( info . id ) )
2025-09-13 09:46:14 +00:00
2026-02-17 00:45:11 +00:00
type Draft < T > = T extends MessageV2 . Part ? Omit < T , " id " > & { id? : string } : never
const assign = ( part : Draft < MessageV2.Part > ) : MessageV2 . Part = > ( {
. . . part ,
2026-03-11 23:40:50 +00:00
id : part.id ? PartID . make ( part . id ) : PartID . ascending ( ) ,
2026-02-17 00:45:11 +00:00
} )
2025-09-13 09:46:14 +00:00
const parts = await Promise . all (
2026-02-17 00:45:11 +00:00
input . parts . map ( async ( part ) : Promise < Draft < MessageV2.Part > [ ] > = > {
2025-09-13 09:46:14 +00:00
if ( part . type === "file" ) {
2026-01-04 15:12:54 +00:00
// before checking the protocol we check if this is an mcp resource because it needs special handling
if ( part . source ? . type === "resource" ) {
const { clientName , uri } = part . source
log . info ( "mcp resource" , { clientName , uri , mime : part.mime } )
2026-02-17 00:45:11 +00:00
const pieces : Draft < MessageV2.Part > [ ] = [
2026-01-04 15:12:54 +00:00
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : ` Reading MCP resource: ${ part . filename } ( ${ uri } ) ` ,
} ,
]
try {
const resourceContent = await MCP . readResource ( clientName , uri )
if ( ! resourceContent ) {
throw new Error ( ` Resource not found: ${ clientName } / ${ uri } ` )
}
// Handle different content types
const contents = Array . isArray ( resourceContent . contents )
? resourceContent . contents
: [ resourceContent . contents ]
for ( const content of contents ) {
if ( "text" in content && content . text ) {
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : content.text as string ,
} )
} else if ( "blob" in content && content . blob ) {
// Handle binary content if needed
const mimeType = "mimeType" in content ? content.mimeType : part.mime
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : ` [Binary content: ${ mimeType } ] ` ,
} )
}
}
pieces . push ( {
. . . part ,
messageID : info.id ,
sessionID : input.sessionID ,
} )
} catch ( error : unknown ) {
log . error ( "failed to read MCP resource" , { error , clientName , uri } )
const message = error instanceof Error ? error.message : String ( error )
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : ` Failed to read MCP resource ${ part . filename } : ${ message } ` ,
} )
}
return pieces
}
2025-09-13 09:46:14 +00:00
const url = new URL ( part . url )
switch ( url . protocol ) {
case "data:" :
if ( part . mime === "text/plain" ) {
return [
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : ` Called the Read tool with the following input: ${ JSON . stringify ( { filePath : part.filename } )} ` ,
} ,
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : Buffer.from ( part . url , "base64url" ) . toString ( ) ,
} ,
{
. . . part ,
messageID : info.id ,
sessionID : input.sessionID ,
} ,
]
}
break
case "file:" :
2025-10-01 07:37:01 +00:00
log . info ( "file" , { mime : part.mime } )
2025-09-13 09:46:14 +00:00
// have to normalize, symbol search returns absolute paths
// Decode the pathname since URL constructor doesn't automatically decode it
2025-10-05 19:32:07 +00:00
const filepath = fileURLToPath ( part . url )
2026-02-19 16:32:32 +00:00
const s = Filesystem . stat ( filepath )
2025-10-01 07:37:01 +00:00
2026-02-19 16:32:32 +00:00
if ( s ? . isDirectory ( ) ) {
2025-10-01 07:37:01 +00:00
part . mime = "application/x-directory"
}
2025-09-13 09:46:14 +00:00
if ( part . mime === "text/plain" ) {
let offset : number | undefined = undefined
let limit : number | undefined = undefined
const range = {
start : url.searchParams.get ( "start" ) ,
end : url.searchParams.get ( "end" ) ,
}
if ( range . start != null ) {
2025-10-05 19:32:07 +00:00
const filePathURI = part . url . split ( "?" ) [ 0 ]
2025-09-13 09:46:14 +00:00
let start = parseInt ( range . start )
let end = range . end ? parseInt ( range . end ) : undefined
// some LSP servers (eg, gopls) don't give full range in
// workspace/symbol searches, so we'll try to find the
// symbol in the document to get the full range
if ( start === end ) {
2026-02-04 16:20:54 +00:00
const symbols = await LSP . documentSymbol ( filePathURI ) . catch ( ( ) = > [ ] )
2025-09-13 09:46:14 +00:00
for ( const symbol of symbols ) {
let range : LSP.Range | undefined
if ( "range" in symbol ) {
range = symbol . range
} else if ( "location" in symbol ) {
range = symbol . location . range
}
if ( range ? . start ? . line && range ? . start ? . line === start ) {
start = range . start . line
end = range ? . end ? . line ? ? start
break
}
}
}
2026-02-11 20:02:30 +00:00
offset = Math . max ( start , 1 )
2025-09-13 09:46:14 +00:00
if ( end ) {
2026-02-11 20:02:30 +00:00
limit = end - ( offset - 1 )
2025-09-13 09:46:14 +00:00
}
}
2025-10-01 07:37:01 +00:00
const args = { filePath : filepath , offset , limit }
2025-11-04 23:29:27 +00:00
2026-02-17 00:45:11 +00:00
const pieces : Draft < MessageV2.Part > [ ] = [
2025-09-13 09:46:14 +00:00
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : ` Called the Read tool with the following input: ${ JSON . stringify ( args ) } ` ,
} ,
]
2025-11-04 23:29:27 +00:00
await ReadTool . init ( )
. then ( async ( t ) = > {
2025-12-04 17:15:30 +00:00
const model = await Provider . getModel ( info . model . providerID , info . model . modelID )
2026-01-01 22:54:11 +00:00
const readCtx : Tool.Context = {
2025-11-04 23:29:27 +00:00
sessionID : input.sessionID ,
abort : new AbortController ( ) . signal ,
agent : input.agent ! ,
messageID : info.id ,
2025-12-04 17:15:30 +00:00
extra : { bypassCwdCheck : true , model } ,
2026-01-26 15:49:41 +00:00
messages : [ ] ,
2025-11-04 23:29:27 +00:00
metadata : async ( ) = > { } ,
2026-01-01 22:54:11 +00:00
ask : async ( ) = > { } ,
}
const result = await t . execute ( args , readCtx )
2025-11-22 18:47:57 +00:00
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : result.output ,
} )
if ( result . attachments ? . length ) {
pieces . push (
. . . result . attachments . map ( ( attachment ) = > ( {
. . . attachment ,
synthetic : true ,
filename : attachment.filename ? ? part . filename ,
messageID : info.id ,
sessionID : input.sessionID ,
} ) ) ,
)
} else {
pieces . push ( {
2025-11-04 23:29:27 +00:00
. . . part ,
messageID : info.id ,
sessionID : input.sessionID ,
2025-11-22 18:47:57 +00:00
} )
}
2025-11-04 23:29:27 +00:00
} )
. catch ( ( error ) = > {
log . error ( "failed to read file" , { error } )
const message = error instanceof Error ? error.message : error.toString ( )
Bus . publish ( Session . Event . Error , {
sessionID : input.sessionID ,
error : new NamedError . Unknown ( {
message ,
} ) . toObject ( ) ,
} )
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : ` Read tool failed to read ${ filepath } with the following error: ${ message } ` ,
} )
} )
return pieces
2025-09-13 09:46:14 +00:00
}
if ( part . mime === "application/x-directory" ) {
2026-02-13 05:20:33 +00:00
const args = { filePath : filepath }
2026-01-01 22:54:11 +00:00
const listCtx : Tool.Context = {
sessionID : input.sessionID ,
abort : new AbortController ( ) . signal ,
agent : input.agent ! ,
messageID : info.id ,
extra : { bypassCwdCheck : true } ,
2026-01-26 15:49:41 +00:00
messages : [ ] ,
2026-01-01 22:54:11 +00:00
metadata : async ( ) = > { } ,
ask : async ( ) = > { } ,
}
2026-02-13 05:20:33 +00:00
const result = await ReadTool . init ( ) . then ( ( t ) = > t . execute ( args , listCtx ) )
2025-09-13 09:46:14 +00:00
return [
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
2026-02-13 05:20:33 +00:00
text : ` Called the Read tool with the following input: ${ JSON . stringify ( args ) } ` ,
2025-09-13 09:46:14 +00:00
} ,
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : result.output ,
} ,
{
. . . part ,
messageID : info.id ,
sessionID : input.sessionID ,
} ,
]
}
2025-10-01 07:37:01 +00:00
FileTime . read ( input . sessionID , filepath )
2025-09-13 09:46:14 +00:00
return [
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
2026-02-19 16:32:32 +00:00
text : ` Called the Read tool with the following input: {"filePath":" ${ filepath } "} ` ,
2025-09-13 09:46:14 +00:00
synthetic : true ,
} ,
{
2026-02-17 00:45:11 +00:00
id : part.id ,
2025-09-13 09:46:14 +00:00
messageID : info.id ,
sessionID : input.sessionID ,
type : "file" ,
2026-02-19 16:32:32 +00:00
url : ` data: ${ part . mime } ;base64, ` + ( await Filesystem . readBytes ( filepath ) ) . toString ( "base64" ) ,
2025-09-13 09:46:14 +00:00
mime : part.mime ,
filename : part.filename ! ,
source : part.source ,
} ,
]
}
}
if ( part . type === "agent" ) {
2026-01-07 04:29:17 +00:00
// Check if this agent would be denied by task permission
const perm = PermissionNext . evaluate ( "task" , part . name , agent . permission )
const hint = perm . action === "deny" ? " . Invoked by user; guaranteed to exist." : ""
2025-09-13 09:46:14 +00:00
return [
{
. . . part ,
messageID : info.id ,
sessionID : input.sessionID ,
} ,
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
2026-01-07 04:29:17 +00:00
// An extra space is added here. Otherwise the 'Use' gets appended
// to user's last word; making a combined word
2025-09-13 09:46:14 +00:00
text :
2026-01-07 04:29:17 +00:00
" Use the above message and context to generate a prompt and call the task tool with subagent: " +
part . name +
hint ,
2025-09-13 09:46:14 +00:00
} ,
]
}
return [
{
. . . part ,
messageID : info.id ,
sessionID : input.sessionID ,
} ,
]
} ) ,
2026-02-17 00:45:11 +00:00
) . then ( ( x ) = > x . flat ( ) . map ( assign ) )
2025-09-13 09:46:14 +00:00
2025-09-16 02:44:07 +00:00
await Plugin . trigger (
"chat.message" ,
2025-11-09 05:29:27 +00:00
{
sessionID : input.sessionID ,
agent : input.agent ,
model : input.model ,
messageID : input.messageID ,
2026-01-04 06:28:52 +00:00
variant : input.variant ,
2025-11-09 05:29:27 +00:00
} ,
2025-09-16 02:44:07 +00:00
{
message : info ,
parts ,
} ,
)
2025-09-13 09:46:14 +00:00
await Session . updateMessage ( info )
for ( const part of parts ) {
await Session . updatePart ( part )
}
return {
info ,
parts ,
}
}
2026-01-13 20:55:48 +00:00
async function insertReminders ( input : { messages : MessageV2.WithParts [ ] ; agent : Agent.Info ; session : Session.Info } ) {
2025-09-13 09:46:14 +00:00
const userMessage = input . messages . findLast ( ( msg ) = > msg . info . role === "user" )
if ( ! userMessage ) return input . messages
2026-01-13 20:55:48 +00:00
// Original logic when experimental plan mode is disabled
if ( ! Flag . OPENCODE_EXPERIMENTAL_PLAN_MODE ) {
if ( input . agent . name === "plan" ) {
userMessage . parts . push ( {
2026-03-11 23:40:50 +00:00
id : PartID.ascending ( ) ,
2026-01-13 20:55:48 +00:00
messageID : userMessage.info.id ,
sessionID : userMessage.info.sessionID ,
type : "text" ,
text : PROMPT_PLAN ,
synthetic : true ,
} )
}
const wasPlan = input . messages . some ( ( msg ) = > msg . info . role === "assistant" && msg . info . agent === "plan" )
if ( wasPlan && input . agent . name === "build" ) {
userMessage . parts . push ( {
2026-03-11 23:40:50 +00:00
id : PartID.ascending ( ) ,
2026-01-13 20:55:48 +00:00
messageID : userMessage.info.id ,
sessionID : userMessage.info.sessionID ,
type : "text" ,
text : BUILD_SWITCH ,
synthetic : true ,
} )
}
return input . messages
2025-09-13 09:46:14 +00:00
}
2026-01-13 20:55:48 +00:00
// New plan mode logic when flag is enabled
const assistantMessage = input . messages . findLast ( ( msg ) = > msg . info . role === "assistant" )
// Switching from plan mode to build mode
if ( input . agent . name !== "plan" && assistantMessage ? . info . agent === "plan" ) {
const plan = Session . plan ( input . session )
2026-02-19 16:32:32 +00:00
const exists = await Filesystem . exists ( plan )
2026-01-13 20:55:48 +00:00
if ( exists ) {
const part = await Session . updatePart ( {
2026-03-11 23:40:50 +00:00
id : PartID.ascending ( ) ,
2026-01-13 20:55:48 +00:00
messageID : userMessage.info.id ,
sessionID : userMessage.info.sessionID ,
type : "text" ,
2026-01-14 13:21:17 +00:00
text :
BUILD_SWITCH + "\n\n" + ` A plan file exists at ${ plan } . You should execute on the plan defined within it ` ,
2026-01-13 20:55:48 +00:00
synthetic : true ,
} )
userMessage . parts . push ( part )
}
2026-01-14 13:21:17 +00:00
return input . messages
2026-01-13 20:55:48 +00:00
}
// Entering plan mode
if ( input . agent . name === "plan" && assistantMessage ? . info . agent !== "plan" ) {
const plan = Session . plan ( input . session )
2026-02-19 16:32:32 +00:00
const exists = await Filesystem . exists ( plan )
2026-01-13 20:55:48 +00:00
if ( ! exists ) await fs . mkdir ( path . dirname ( plan ) , { recursive : true } )
const part = await Session . updatePart ( {
2026-03-11 23:40:50 +00:00
id : PartID.ascending ( ) ,
2025-09-13 09:46:14 +00:00
messageID : userMessage.info.id ,
sessionID : userMessage.info.sessionID ,
type : "text" ,
2026-01-13 20:55:48 +00:00
text : ` <system-reminder>
2026-01-22 04:10:40 +00:00
Plan mode is active . The user indicated that they do not want you to execute yet -- you MUST NOT make any edits ( with the exception of the plan file mentioned below ) , run any non - readonly tools ( including changing configs or making commits ) , or otherwise make any changes to the system . This supersedes any other instructions you have received .
2026-01-13 20:55:48 +00:00
# # Plan File Info :
$ { exists ? ` A plan file already exists at ${ plan } . You can read it and make incremental edits using the edit tool. ` : ` No plan file exists yet. You should create your plan at ${ plan } using the write tool. ` }
You should build your plan incrementally by writing to or editing this file . NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ - ONLY actions .
# # Plan Workflow
# # # Phase 1 : Initial Understanding
Goal : Gain a comprehensive understanding of the user ' s request by reading through code and asking them questions . Critical : In this phase you should only use the explore subagent type .
1 . Focus on understanding the user ' s request and the code associated with their request
2 . * * Launch up to 3 explore agents IN PARALLEL * * ( single message , multiple tool calls ) to efficiently explore the codebase .
- Use 1 agent when the task is isolated to known files , the user provided specific file paths , or you ' re making a small targeted change .
- Use multiple agents when : the scope is uncertain , multiple areas of the codebase are involved , or you need to understand existing patterns before planning .
- Quality over quantity - 3 agents maximum , but you should try to use the minimum number of agents necessary ( usually just 1 )
- If using multiple agents : Provide each agent with a specific search focus or area to explore . Example : One agent searches for existing implementations , another explores related components , a third investigates testing patterns
3 . After exploring the code , use the question tool to clarify ambiguities in the user request up front .
# # # Phase 2 : Design
Goal : Design an implementation approach .
Launch general agent ( s ) to design the implementation based on the user ' s intent and your exploration results from Phase 1 .
You can launch up to 1 agent ( s ) in parallel .
* * Guidelines : * *
- * * Default * * : Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives
- * * Skip agents * * : Only for truly trivial tasks ( typo fixes , single - line changes , simple renames )
Examples of when to use multiple agents :
- The task touches multiple parts of the codebase
- It ' s a large refactor or architectural change
- There are many edge cases to consider
- You ' d benefit from exploring different approaches
Example perspectives by task type :
- New feature : simplicity vs performance vs maintainability
- Bug fix : root cause vs workaround vs prevention
- Refactoring : minimal change vs clean architecture
In the agent prompt :
- Provide comprehensive background context from Phase 1 exploration including filenames and code path traces
- Describe requirements and constraints
- Request a detailed implementation plan
# # # Phase 3 : Review
Goal : Review the plan ( s ) from Phase 2 and ensure alignment with the user ' s intentions .
1 . Read the critical files identified by agents to deepen your understanding
2 . Ensure that the plans align with the user ' s original request
3 . Use question tool to clarify any remaining questions with the user
# # # Phase 4 : Final Plan
Goal : Write your final plan to the plan file ( the only file you can edit ) .
- Include only your recommended approach , not all alternatives
- Ensure that the plan file is concise enough to scan quickly , but detailed enough to execute effectively
- Include the paths of critical files to be modified
- Include a verification section describing how to test the changes end - to - end ( run the code , use MCP tools , run tests )
# # # Phase 5 : Call plan_exit tool
At the very end of your turn , once you have asked the user questions and are happy with your final plan file - you should always call plan_exit to indicate to the user that you are done planning .
This is critical - your turn should only end with either asking the user a question or calling plan_exit . Do not stop unless it ' s for these 2 reasons .
* * Important : * * Use question tool to clarify requirements / approach , use plan_exit to request plan approval . Do NOT use question tool to ask "Is this plan okay?" - that ' s what plan_exit does .
NOTE : At any point in time through this workflow you should feel free to ask the user questions or clarifications . Don ' t make large assumptions about user intent . The goal is to present a well researched plan to the user , and tie any loose ends before implementation begins .
< / s y s t e m - r e m i n d e r > ` ,
2025-09-13 09:46:14 +00:00
synthetic : true ,
} )
2026-01-13 20:55:48 +00:00
userMessage . parts . push ( part )
return input . messages
2025-09-13 09:46:14 +00:00
}
return input . messages
}
export const ShellInput = z . object ( {
2026-03-11 23:16:56 +00:00
sessionID : SessionID.zod ,
2025-09-13 09:46:14 +00:00
agent : z.string ( ) ,
2025-11-19 18:10:09 +00:00
model : z
. object ( {
providerID : z.string ( ) ,
modelID : z.string ( ) ,
} )
. optional ( ) ,
2025-09-13 09:46:14 +00:00
command : z.string ( ) ,
} )
export type ShellInput = z . infer < typeof ShellInput >
export async function shell ( input : ShellInput ) {
2025-12-12 22:11:07 +00:00
const abort = start ( input . sessionID )
if ( ! abort ) {
throw new Session . BusyError ( input . sessionID )
}
2026-02-06 22:13:11 +00:00
using _ = defer ( ( ) = > {
// If no queued callbacks, cancel (the default)
const callbacks = state ( ) [ input . sessionID ] ? . callbacks ? ? [ ]
if ( callbacks . length === 0 ) {
cancel ( input . sessionID )
} else {
// Otherwise, trigger the session loop to process queued items
2026-02-06 22:14:34 +00:00
loop ( { sessionID : input.sessionID , resume_existing : true } ) . catch ( ( error ) = > {
log . error ( "session loop failed to resume after shell command" , { sessionID : input.sessionID , error } )
2026-02-06 22:13:11 +00:00
} )
}
} )
2025-12-12 22:11:07 +00:00
2025-09-13 09:46:14 +00:00
const session = await Session . get ( input . sessionID )
if ( session . revert ) {
2026-01-26 12:36:17 +00:00
await SessionRevert . cleanup ( session )
2025-09-13 09:46:14 +00:00
}
2025-11-17 15:57:18 +00:00
const agent = await Agent . get ( input . agent )
2025-11-19 18:10:09 +00:00
const model = input . model ? ? agent . model ? ? ( await lastModel ( input . sessionID ) )
2025-09-13 09:46:14 +00:00
const userMsg : MessageV2.User = {
2026-03-11 23:30:17 +00:00
id : MessageID.ascending ( ) ,
2025-09-13 09:46:14 +00:00
sessionID : input.sessionID ,
time : {
created : Date.now ( ) ,
} ,
role : "user" ,
2025-11-17 15:57:18 +00:00
agent : input.agent ,
model : {
providerID : model.providerID ,
modelID : model.modelID ,
} ,
2025-09-13 09:46:14 +00:00
}
await Session . updateMessage ( userMsg )
const userPart : MessageV2.Part = {
type : "text" ,
2026-03-11 23:40:50 +00:00
id : PartID.ascending ( ) ,
2025-09-13 09:46:14 +00:00
messageID : userMsg.id ,
sessionID : input.sessionID ,
text : "The following tool was executed by the user" ,
synthetic : true ,
}
await Session . updatePart ( userPart )
const msg : MessageV2.Assistant = {
2026-03-11 23:30:17 +00:00
id : MessageID.ascending ( ) ,
2025-09-13 09:46:14 +00:00
sessionID : input.sessionID ,
2025-10-22 19:01:13 +00:00
parentID : userMsg.id ,
2025-09-13 09:46:14 +00:00
mode : input.agent ,
2025-12-15 02:11:30 +00:00
agent : input.agent ,
2025-09-13 09:46:14 +00:00
cost : 0 ,
path : {
cwd : Instance.directory ,
root : Instance.worktree ,
} ,
time : {
created : Date.now ( ) ,
} ,
role : "assistant" ,
tokens : {
input : 0 ,
output : 0 ,
reasoning : 0 ,
cache : { read : 0 , write : 0 } ,
} ,
2025-11-17 15:57:18 +00:00
modelID : model.modelID ,
providerID : model.providerID ,
2025-09-13 09:46:14 +00:00
}
await Session . updateMessage ( msg )
const part : MessageV2.Part = {
type : "tool" ,
2026-03-11 23:40:50 +00:00
id : PartID.ascending ( ) ,
2025-09-13 09:46:14 +00:00
messageID : msg.id ,
sessionID : input.sessionID ,
tool : "bash" ,
callID : ulid ( ) ,
state : {
status : "running" ,
time : {
start : Date.now ( ) ,
} ,
input : {
command : input.command ,
} ,
} ,
}
await Session . updatePart ( part )
2025-12-12 22:11:07 +00:00
const shell = Shell . preferred ( )
const shellName = (
process . platform === "win32" ? path . win32 . basename ( shell , ".exe" ) : path . basename ( shell )
) . toLowerCase ( )
2025-09-13 09:46:14 +00:00
2025-10-13 22:37:35 +00:00
const invocations : Record < string , { args : string [ ] } > = {
nu : {
args : [ "-c" , input . command ] ,
} ,
fish : {
args : [ "-c" , input . command ] ,
} ,
zsh : {
args : [
"-c" ,
"-l" ,
`
[ [ - f ~ /.zshenv ]] && source ~/ . zshenv > / d e v / n u l l 2 > & 1 | | t r u e
[ [ - f "\${ZDOTDIR:-$HOME}/.zshrc" ] ] && source "\${ZDOTDIR:-$HOME}/.zshrc" > / d e v / n u l l 2 > & 1 | | t r u e
2025-12-16 17:32:31 +00:00
eval $ { JSON . stringify ( input . command ) }
2025-10-14 00:58:19 +00:00
` ,
2025-10-13 22:37:35 +00:00
] ,
} ,
bash : {
args : [
"-c" ,
"-l" ,
`
2025-12-16 17:32:31 +00:00
shopt - s expand_aliases
2025-10-13 22:37:35 +00:00
[ [ - f ~ /.bashrc ]] && source ~/ . bashrc > / d e v / n u l l 2 > & 1 | | t r u e
2025-12-16 17:32:31 +00:00
eval $ { JSON . stringify ( input . command ) }
2025-10-13 22:37:35 +00:00
` ,
] ,
} ,
2025-12-12 22:11:07 +00:00
// Windows cmd
cmd : {
2025-12-10 06:01:56 +00:00
args : [ "/c" , input . command ] ,
} ,
// Windows PowerShell
2025-12-12 22:11:07 +00:00
powershell : {
args : [ "-NoProfile" , "-Command" , input . command ] ,
} ,
pwsh : {
2025-12-10 06:01:56 +00:00
args : [ "-NoProfile" , "-Command" , input . command ] ,
} ,
2025-10-13 22:37:35 +00:00
// Fallback: any shell that doesn't match those above
2025-12-12 21:43:35 +00:00
// - No -l, for max compatibility
2025-10-13 22:37:35 +00:00
"" : {
2025-12-12 21:43:35 +00:00
args : [ "-c" , ` ${ input . command } ` ] ,
2025-10-13 22:37:35 +00:00
} ,
2025-09-13 09:46:14 +00:00
}
2025-10-14 00:58:19 +00:00
const matchingInvocation = invocations [ shellName ] ? ? invocations [ "" ]
2025-10-13 22:37:35 +00:00
const args = matchingInvocation ? . args
2025-09-13 09:46:14 +00:00
2026-02-03 21:18:41 +00:00
const cwd = Instance . directory
2026-02-18 20:15:14 +00:00
const shellEnv = await Plugin . trigger (
"shell.env" ,
{ cwd , sessionID : input.sessionID , callID : part.callID } ,
{ env : { } } ,
)
2025-09-13 09:46:14 +00:00
const proc = spawn ( shell , args , {
2026-02-03 21:18:41 +00:00
cwd ,
2025-12-10 06:01:56 +00:00
detached : process.platform !== "win32" ,
2026-03-11 03:33:06 +00:00
windowsHide : process.platform === "win32" ,
2025-09-13 09:46:14 +00:00
stdio : [ "ignore" , "pipe" , "pipe" ] ,
env : {
. . . process . env ,
2026-02-03 21:18:41 +00:00
. . . shellEnv . env ,
2025-09-13 09:46:14 +00:00
TERM : "dumb" ,
} ,
} )
let output = ""
proc . stdout ? . on ( "data" , ( chunk ) = > {
output += chunk . toString ( )
if ( part . state . status === "running" ) {
part . state . metadata = {
output : output ,
description : "" ,
}
Session . updatePart ( part )
}
} )
proc . stderr ? . on ( "data" , ( chunk ) = > {
output += chunk . toString ( )
if ( part . state . status === "running" ) {
part . state . metadata = {
output : output ,
description : "" ,
}
Session . updatePart ( part )
}
} )
2025-12-12 22:11:07 +00:00
let aborted = false
let exited = false
const kill = ( ) = > Shell . killTree ( proc , { exited : ( ) = > exited } )
if ( abort . aborted ) {
aborted = true
await kill ( )
}
const abortHandler = ( ) = > {
aborted = true
void kill ( )
}
abort . addEventListener ( "abort" , abortHandler , { once : true } )
2025-09-13 09:46:14 +00:00
await new Promise < void > ( ( resolve ) = > {
proc . on ( "close" , ( ) = > {
2025-12-12 22:11:07 +00:00
exited = true
abort . removeEventListener ( "abort" , abortHandler )
2025-09-13 09:46:14 +00:00
resolve ( )
} )
} )
2025-12-12 22:11:07 +00:00
if ( aborted ) {
output += "\n\n" + [ "<metadata>" , "User aborted the command" , "</metadata>" ] . join ( "\n" )
}
2025-09-13 09:46:14 +00:00
msg . time . completed = Date . now ( )
await Session . updateMessage ( msg )
if ( part . state . status === "running" ) {
part . state = {
status : "completed" ,
time : {
. . . part . state . time ,
end : Date.now ( ) ,
} ,
input : part.state.input ,
title : "" ,
metadata : {
output ,
description : "" ,
} ,
output ,
}
await Session . updatePart ( part )
}
return { info : msg , parts : [ part ] }
}
export const CommandInput = z . object ( {
2026-03-11 23:30:17 +00:00
messageID : MessageID.zod.optional ( ) ,
2026-03-11 23:16:56 +00:00
sessionID : SessionID.zod ,
2025-09-13 09:46:14 +00:00
agent : z.string ( ) . optional ( ) ,
model : z.string ( ) . optional ( ) ,
arguments : z.string ( ) ,
command : z.string ( ) ,
2025-12-30 03:43:50 +00:00
variant : z.string ( ) . optional ( ) ,
2026-01-05 19:06:57 +00:00
parts : z
. array (
z . discriminatedUnion ( "type" , [
MessageV2 . FilePart . omit ( {
messageID : true ,
sessionID : true ,
} ) . partial ( {
id : true ,
} ) ,
] ) ,
)
. optional ( ) ,
2025-09-13 09:46:14 +00:00
} )
export type CommandInput = z . infer < typeof CommandInput >
const bashRegex = /!`([^`]+)`/g
2026-01-05 19:06:57 +00:00
// Match [Image N] as single token, quoted strings, or non-space sequences
const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi
2025-10-29 19:54:24 +00:00
const placeholderRegex = /\$(\d+)/g
const quoteTrimRegex = /^["']|["']$/g
2025-09-13 09:46:14 +00:00
/ * *
* Regular expression to match @ file references in text
* Matches @ followed by file paths , excluding commas , periods at end of sentences , and backticks
* Does not match when preceded by word characters or backticks ( to avoid email addresses and quoted references )
* /
export async function command ( input : CommandInput ) {
log . info ( "command" , input )
const command = await Command . get ( input . command )
2025-12-20 17:46:48 +00:00
const agentName = command . agent ? ? input . agent ? ? ( await Agent . defaultAgent ( ) )
2025-09-13 09:46:14 +00:00
2025-10-29 19:54:24 +00:00
const raw = input . arguments . match ( argsRegex ) ? ? [ ]
const args = raw . map ( ( arg ) = > arg . replace ( quoteTrimRegex , "" ) )
2025-12-31 06:51:25 +00:00
const templateCommand = await command . template
const placeholders = templateCommand . match ( placeholderRegex ) ? ? [ ]
2025-10-29 19:54:24 +00:00
let last = 0
for ( const item of placeholders ) {
const value = Number ( item . slice ( 1 ) )
if ( value > last ) last = value
}
// Let the final placeholder swallow any extra arguments so prompts read naturally
2025-12-31 06:51:25 +00:00
const withArgs = templateCommand . replaceAll ( placeholderRegex , ( _ , index ) = > {
2025-10-29 19:54:24 +00:00
const position = Number ( index )
const argIndex = position - 1
if ( argIndex >= args . length ) return ""
if ( position === last ) return args . slice ( argIndex ) . join ( " " )
return args [ argIndex ]
} )
2026-01-20 17:12:43 +00:00
const usesArgumentsPlaceholder = templateCommand . includes ( "$ARGUMENTS" )
2025-10-29 19:54:24 +00:00
let template = withArgs . replaceAll ( "$ARGUMENTS" , input . arguments )
2025-09-13 09:46:14 +00:00
2026-01-20 17:12:43 +00:00
// If command doesn't explicitly handle arguments (no $N or $ARGUMENTS placeholders)
// but user provided arguments, append them to the template
if ( placeholders . length === 0 && ! usesArgumentsPlaceholder && input . arguments . trim ( ) ) {
template = template + "\n\n" + input . arguments
}
2025-09-27 06:53:20 +00:00
const shell = ConfigMarkdown . shell ( template )
if ( shell . length > 0 ) {
2025-09-13 09:46:14 +00:00
const results = await Promise . all (
2025-09-27 06:53:20 +00:00
shell . map ( async ( [ , cmd ] ) = > {
2025-09-13 09:46:14 +00:00
try {
2025-12-21 23:33:34 +00:00
return await $ ` ${ { raw : cmd } } ` . quiet ( ) . nothrow ( ) . text ( )
2025-09-13 09:46:14 +00:00
} catch ( error ) {
return ` Error executing command: ${ error instanceof Error ? error.message : String ( error ) } `
}
} ) ,
)
let index = 0
template = template . replace ( bashRegex , ( ) = > results [ index ++ ] )
}
2025-11-01 06:14:09 +00:00
template = template . trim ( )
2025-09-13 09:46:14 +00:00
2026-01-15 23:18:39 +00:00
const taskModel = await ( async ( ) = > {
2025-09-13 09:46:14 +00:00
if ( command . model ) {
return Provider . parseModel ( command . model )
}
if ( command . agent ) {
2025-09-13 17:47:18 +00:00
const cmdAgent = await Agent . get ( command . agent )
2026-01-01 17:39:21 +00:00
if ( cmdAgent ? . model ) {
2025-09-13 17:47:18 +00:00
return cmdAgent . model
2025-09-13 09:46:14 +00:00
}
}
2025-11-19 18:10:09 +00:00
if ( input . model ) return Provider . parseModel ( input . model )
return await lastModel ( input . sessionID )
2025-09-13 09:46:14 +00:00
} ) ( )
2025-12-18 16:35:40 +00:00
try {
2026-01-15 23:18:39 +00:00
await Provider . getModel ( taskModel . providerID , taskModel . modelID )
2025-12-18 16:35:40 +00:00
} catch ( e ) {
if ( Provider . ModelNotFoundError . isInstance ( e ) ) {
const { providerID , modelID , suggestions } = e . data
const hint = suggestions ? . length ? ` Did you mean: ${ suggestions . join ( ", " ) } ? ` : ""
Bus . publish ( Session . Event . Error , {
sessionID : input.sessionID ,
error : new NamedError . Unknown ( { message : ` Model not found: ${ providerID } / ${ modelID } . ${ hint } ` } ) . toObject ( ) ,
} )
}
throw e
}
2025-09-13 17:47:18 +00:00
const agent = await Agent . get ( agentName )
2026-01-01 17:39:21 +00:00
if ( ! agent ) {
const available = await Agent . list ( ) . then ( ( agents ) = > agents . filter ( ( a ) = > ! a . hidden ) . map ( ( a ) = > a . name ) )
const hint = available . length ? ` Available agents: ${ available . join ( ", " ) } ` : ""
const error = new NamedError . Unknown ( { message : ` Agent not found: " ${ agentName } ". ${ hint } ` } )
Bus . publish ( Session . Event . Error , {
sessionID : input.sessionID ,
error : error.toObject ( ) ,
} )
throw error
}
2025-09-13 17:47:18 +00:00
2026-01-05 19:06:57 +00:00
const templateParts = await resolvePromptParts ( template )
2026-01-15 23:18:39 +00:00
const isSubtask = ( agent . mode === "subagent" && command . subtask !== false ) || command . subtask === true
const parts = isSubtask
? [
{
type : "subtask" as const ,
agent : agent.name ,
description : command.description ? ? "" ,
command : input.command ,
model : {
providerID : taskModel.providerID ,
modelID : taskModel.modelID ,
2025-11-17 15:57:18 +00:00
} ,
2026-01-15 23:18:39 +00:00
// TODO: how can we make task tool accept a more complex input?
prompt : templateParts.find ( ( y ) = > y . type === "text" ) ? . text ? ? "" ,
} ,
]
: [ . . . templateParts , . . . ( input . parts ? ? [ ] ) ]
const userAgent = isSubtask ? ( input . agent ? ? ( await Agent . defaultAgent ( ) ) ) : agentName
const userModel = isSubtask
? input . model
? Provider . parseModel ( input . model )
: await lastModel ( input . sessionID )
: taskModel
2025-09-13 17:47:18 +00:00
2026-01-18 19:11:22 +00:00
await Plugin . trigger (
"command.execute.before" ,
{
command : input.command ,
sessionID : input.sessionID ,
arguments : input.arguments ,
} ,
{ parts } ,
)
2025-11-17 15:57:18 +00:00
const result = ( await prompt ( {
sessionID : input.sessionID ,
messageID : input.messageID ,
2026-01-15 23:18:39 +00:00
model : userModel ,
agent : userAgent ,
2025-11-17 15:57:18 +00:00
parts ,
2025-12-30 03:43:50 +00:00
variant : input.variant ,
2025-11-17 15:57:18 +00:00
} ) ) as MessageV2 . WithParts
2025-09-13 17:47:18 +00:00
2025-11-01 06:14:09 +00:00
Bus . publish ( Command . Event . Executed , {
name : input.command ,
2025-09-13 09:46:14 +00:00
sessionID : input.sessionID ,
2025-11-01 06:14:09 +00:00
arguments : input.arguments ,
messageID : result.info.id ,
2025-09-13 09:46:14 +00:00
} )
2025-11-01 06:14:09 +00:00
return result
2025-09-13 09:46:14 +00:00
}
async function ensureTitle ( input : {
session : Session.Info
history : MessageV2.WithParts [ ]
providerID : string
modelID : string
} ) {
if ( input . session . parentID ) return
2025-10-28 18:32:36 +00:00
if ( ! Session . isDefaultTitle ( input . session . title ) ) return
2026-01-06 19:30:31 +00:00
// Find first non-synthetic user message
const firstRealUserIdx = input . history . findIndex (
( m ) = > m . info . role === "user" && ! m . parts . every ( ( p ) = > "synthetic" in p && p . synthetic ) ,
)
if ( firstRealUserIdx === - 1 ) return
2025-09-13 09:46:14 +00:00
const isFirst =
2025-11-08 01:59:02 +00:00
input . history . filter ( ( m ) = > m . info . role === "user" && ! m . parts . every ( ( p ) = > "synthetic" in p && p . synthetic ) )
. length === 1
2025-09-13 09:46:14 +00:00
if ( ! isFirst ) return
2026-01-06 19:30:31 +00:00
// Gather all messages up to and including the first real user message for context
// This includes any shell/subtask executions that preceded the user's first prompt
const contextMessages = input . history . slice ( 0 , firstRealUserIdx + 1 )
const firstRealUser = contextMessages [ firstRealUserIdx ]
// For subtask-only messages (from command invocations), extract the prompt directly
// since toModelMessage converts subtask parts to generic "The following tool was executed by the user"
const subtaskParts = firstRealUser . parts . filter ( ( p ) = > p . type === "subtask" ) as MessageV2 . SubtaskPart [ ]
const hasOnlySubtaskParts = subtaskParts . length > 0 && firstRealUser . parts . every ( ( p ) = > p . type === "subtask" )
2025-12-15 02:11:30 +00:00
const agent = await Agent . get ( "title" )
if ( ! agent ) return
2026-01-20 22:39:00 +00:00
const model = await iife ( async ( ) = > {
if ( agent . model ) return await Provider . getModel ( agent . model . providerID , agent . model . modelID )
return (
( await Provider . getSmallModel ( input . providerID ) ) ? ? ( await Provider . getModel ( input . providerID , input . modelID ) )
)
} )
2025-12-15 02:11:30 +00:00
const result = await LLM . stream ( {
agent ,
2026-01-06 19:30:31 +00:00
user : firstRealUser.info as MessageV2 . User ,
2025-12-15 02:11:30 +00:00
system : [ ] ,
small : true ,
tools : { } ,
2026-01-20 22:39:00 +00:00
model ,
2025-12-15 02:11:30 +00:00
abort : new AbortController ( ) . signal ,
sessionID : input.session.id ,
retries : 2 ,
2025-09-13 09:46:14 +00:00
messages : [
2025-10-31 19:07:36 +00:00
{
2025-11-23 20:04:34 +00:00
role : "user" ,
content : "Generate a title for this conversation:\n" ,
2025-10-31 19:07:36 +00:00
} ,
2026-01-06 19:30:31 +00:00
. . . ( hasOnlySubtaskParts
? [ { role : "user" as const , content : subtaskParts.map ( ( p ) = > p . prompt ) . join ( "\n" ) } ]
2026-01-20 22:39:00 +00:00
: MessageV2 . toModelMessages ( contextMessages , model ) ) ,
2025-09-13 09:46:14 +00:00
] ,
} )
2025-12-15 02:11:30 +00:00
const text = await result . text . catch ( ( err ) = > log . error ( "failed to generate title" , { error : err } ) )
2026-02-14 04:19:02 +00:00
if ( text ) {
const cleaned = text
. replace ( /<think>[\s\S]*?<\/think>\s*/g , "" )
. split ( "\n" )
. map ( ( line ) = > line . trim ( ) )
. find ( ( line ) = > line . length > 0 )
if ( ! cleaned ) return
const title = cleaned . length > 100 ? cleaned . substring ( 0 , 97 ) + "..." : cleaned
return Session . setTitle ( { sessionID : input.session.id , title } )
}
2025-09-13 09:46:14 +00:00
}
}