2025-09-13 09:46:14 +00:00
import path from "path"
import os from "os"
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"
2026-04-27 18:33:33 +00:00
import * as Log from "@opencode-ai/core/util/log"
2025-09-13 09:46:14 +00:00
import { SessionRevert } from "./revert"
2026-04-16 03:41:34 +00:00
import * as Session from "./session"
2025-09-13 09:46:14 +00:00
import { Agent } from "../agent/agent"
2026-04-27 18:33:33 +00:00
import { Provider } from "@/provider/provider"
2026-03-12 13:27:52 +00:00
import { ModelID , ProviderID } from "../provider/schema"
2026-05-18 19:50:31 +00:00
import { type Tool as AITool , tool , jsonSchema } from "ai"
2026-04-16 20:11:05 +00:00
import type { JSONSchema7 } from "@ai-sdk/provider"
2025-09-13 09:46:14 +00:00
import { SessionCompaction } from "./compaction"
import { Bus } from "../bus"
import { SystemPrompt } from "./system"
2026-04-02 02:22:51 +00:00
import { Instruction } from "./instruction"
2025-09-13 09:46:14 +00:00
import { Plugin } from "../plugin"
2025-12-05 18:26:44 +00:00
import MAX_STEPS from "../session/prompt/max-steps.txt"
2026-04-27 18:33:33 +00:00
import { ToolRegistry } from "@/tool/registry"
2025-09-13 09:46:14 +00:00
import { MCP } from "../mcp"
2026-04-27 18:33:33 +00:00
import { LSP } from "@/lsp/lsp"
2025-09-13 09:46:14 +00:00
import { ulid } from "ulid"
2026-04-01 16:07:57 +00:00
import { ChildProcess , ChildProcessSpawner } from "effect/unstable/process"
2026-04-25 18:30:16 +00:00
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
2026-04-01 16:07:57 +00:00
import * as Stream from "effect/Stream"
2025-09-13 09:46:14 +00:00
import { Command } from "../command"
2026-03-09 18:52:25 +00:00
import { pathToFileURL , fileURLToPath } from "url"
2026-04-27 18:33:33 +00:00
import { Config } from "@/config/config"
import { ConfigMarkdown } from "@/config/markdown"
2025-10-23 20:28:20 +00:00
import { SessionSummary } from "./summary"
2026-04-25 14:59:17 +00:00
import { NamedError } from "@opencode-ai/core/util/error"
2025-11-17 15:57:18 +00:00
import { SessionProcessor } from "./processor"
2026-04-27 18:33:33 +00:00
import { Tool } from "@/tool/tool"
2026-03-21 04:51:35 +00:00
import { Permission } from "@/permission"
2025-11-17 15:57:18 +00:00
import { SessionStatus } from "./status"
2025-12-15 02:11:30 +00:00
import { LLM } from "./llm"
2025-12-12 22:11:07 +00:00
import { Shell } from "@/shell/shell"
2026-05-02 23:18:48 +00:00
import { ShellID } from "@/tool/shell/id"
2026-04-25 14:59:17 +00:00
import { AppFileSystem } from "@opencode-ai/core/filesystem"
2026-04-27 18:33:33 +00:00
import { Truncate } from "@/tool/truncate"
2026-05-14 04:06:07 +00:00
import { Image } from "@/image/image"
2026-03-13 11:58:24 +00:00
import { decodeDataUrl } from "@/util/data-url"
2026-04-27 18:33:33 +00:00
import { Process } from "@/util/process"
2026-04-30 23:28:46 +00:00
import { Cause , Effect , Exit , Latch , Layer , Option , Scope , Context , Schema , Types } from "effect"
2026-04-27 18:33:33 +00:00
import * as EffectLogger from "@opencode-ai/core/effect/logger"
import { InstanceState } from "@/effect/instance-state"
2026-04-10 23:36:13 +00:00
import { TaskTool , type TaskPromptOps } from "@/tool/task"
2026-04-09 20:03:40 +00:00
import { SessionRunState } from "./run-state"
2026-05-13 11:47:04 +00:00
import { RuntimeFlags } from "@/effect/runtime-flags"
2026-05-15 00:50:23 +00:00
import { EventV2 } from "@opencode-ai/core/event"
import { EventV2Bridge } from "@/event-v2-bridge"
import { SessionEvent } from "@opencode-ai/core/session-event"
2026-05-13 14:43:08 +00:00
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AgentAttachment , FileAttachment , ReferenceAttachment , Source } from "@opencode-ai/core/session-prompt"
2026-05-11 07:27:26 +00:00
import { Reference } from "@/reference/reference"
2026-05-03 02:09:48 +00:00
import * as DateTime from "effect/DateTime"
import { eq } from "@/storage/db"
import * as Database from "@/storage/db"
import { SessionTable } from "./session.sql"
2026-05-18 17:02:18 +00:00
import { referencePromptMetadata , referenceTextPart } from "./prompt/reference"
2026-05-18 16:36:38 +00:00
import { SessionReminders } from "./reminders"
2026-05-18 19:50:31 +00:00
import { SessionTools } from "./tools"
2026-05-18 18:41:36 +00:00
import { LLMEvent } from "@opencode-ai/llm"
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-05-11 02:59:20 +00:00
const decodeMessageInfo = Schema . decodeUnknownExit ( MessageV2 . Info )
const decodeMessagePart = Schema . decodeUnknownExit ( MessageV2 . Part )
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. `
2026-04-17 00:49:39 +00:00
const log = Log . create ( { service : "session.prompt" } )
const elog = EffectLogger . create ( { service : "session.prompt" } )
export interface Interface {
readonly cancel : ( sessionID : SessionID ) = > Effect . Effect < void >
2026-05-14 04:06:07 +00:00
readonly prompt : ( input : PromptInput ) = > Effect . Effect < MessageV2.WithParts , Image.Error >
2026-04-23 15:30:02 +00:00
readonly loop : ( input : LoopInput ) = > Effect . Effect < MessageV2.WithParts >
2026-05-14 01:28:04 +00:00
readonly shell : ( input : ShellInput ) = > Effect . Effect < MessageV2.WithParts , Session.BusyError >
2026-05-14 04:06:07 +00:00
readonly command : ( input : CommandInput ) = > Effect . Effect < MessageV2.WithParts , Image.Error >
2026-04-17 00:49:39 +00:00
readonly resolvePromptParts : ( template : string ) = > Effect . Effect < PromptInput [ " parts " ] >
}
2026-04-11 02:36:02 +00:00
2026-04-17 00:49:39 +00:00
export class Service extends Context . Service < Service , Interface > ( ) ( "@opencode/SessionPrompt" ) { }
export const layer = Layer . effect (
Service ,
Effect . gen ( function * ( ) {
const bus = yield * Bus . Service
const status = yield * SessionStatus . Service
const sessions = yield * Session . Service
const agents = yield * Agent . Service
const provider = yield * Provider . Service
const processor = yield * SessionProcessor . Service
const compaction = yield * SessionCompaction . Service
const plugin = yield * Plugin . Service
const commands = yield * Command . Service
2026-04-27 00:54:55 +00:00
const config = yield * Config . Service
2026-04-17 00:49:39 +00:00
const permission = yield * Permission . Service
const fsys = yield * AppFileSystem . Service
const mcp = yield * MCP . Service
const lsp = yield * LSP . Service
const registry = yield * ToolRegistry . Service
const truncate = yield * Truncate . Service
2026-05-14 04:06:07 +00:00
const image = yield * Image . Service
2026-04-17 00:49:39 +00:00
const spawner = yield * ChildProcessSpawner . ChildProcessSpawner
const scope = yield * Scope . Scope
const instruction = yield * Instruction . Service
const state = yield * SessionRunState . Service
const revert = yield * SessionRevert . Service
const summary = yield * SessionSummary . Service
const sys = yield * SystemPrompt . Service
const llm = yield * LLM . Service
2026-05-11 07:27:26 +00:00
const references = yield * Reference . Service
2026-05-15 00:50:23 +00:00
const events = yield * EventV2Bridge . Service
2026-05-13 11:47:04 +00:00
const flags = yield * RuntimeFlags . Service
2026-04-17 00:49:39 +00:00
const ops = Effect . fn ( "SessionPrompt.ops" ) ( function * ( ) {
return {
2026-05-05 02:36:06 +00:00
cancel : ( sessionID : SessionID ) = > cancel ( sessionID ) ,
2026-04-17 00:49:39 +00:00
resolvePromptParts : ( template : string ) = > resolvePromptParts ( template ) ,
2026-05-14 04:06:07 +00:00
prompt : ( input : PromptInput ) = > prompt ( input ) . pipe ( Effect . catch ( Effect . die ) ) ,
2026-05-14 16:40:15 +00:00
loop : ( input : LoopInput ) = > loop ( input ) ,
2026-04-17 00:49:39 +00:00
} satisfies TaskPromptOps
} )
2025-12-15 02:11:30 +00:00
2026-04-17 00:49:39 +00:00
const cancel = Effect . fn ( "SessionPrompt.cancel" ) ( function * ( sessionID : SessionID ) {
yield * elog . info ( "cancel" , { sessionID } )
yield * state . cancel ( sessionID )
} )
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
const resolvePromptParts = Effect . fn ( "SessionPrompt.resolvePromptParts" ) ( function * ( template : string ) {
const ctx = yield * InstanceState . context
2026-04-30 23:28:46 +00:00
const parts : Types.DeepMutable < PromptInput [ " parts " ] > = [ { type : "text" , text : template } ]
2026-04-17 00:49:39 +00:00
const files = ConfigMarkdown . files ( template )
const seen = new Set < string > ( )
2026-05-11 07:27:26 +00:00
const mentionSource = ( match : RegExpMatchArray ) = > {
const start = match . index ? ? 0
return { value : match [ 0 ] , start , end : start + match [ 0 ] . length }
}
2026-04-17 00:49:39 +00:00
yield * Effect . forEach (
files ,
Effect . fnUntraced ( function * ( match ) {
const name = match [ 1 ]
2026-05-11 07:27:26 +00:00
if ( ! name ) return
2026-04-17 00:49:39 +00:00
if ( seen . has ( name ) ) return
seen . add ( name )
2026-05-11 07:27:26 +00:00
const slash = name . indexOf ( "/" )
const alias = slash === - 1 ? name : name.slice ( 0 , slash )
const reference = yield * references . get ( alias )
if ( reference ) {
const source = mentionSource ( match )
if ( reference . kind === "invalid" ) {
parts . push (
referenceTextPart ( { reference , source , target : slash === - 1 ? undefined : name . slice ( slash + 1 ) } ) ,
)
return
}
yield * references . ensure ( reference . path )
if ( slash === - 1 ) {
parts . push ( referenceTextPart ( { reference , source } ) )
return
}
const target = name . slice ( slash + 1 )
const targetPath = path . resolve ( reference . path , target )
if ( ! AppFileSystem . contains ( reference . path , targetPath ) ) {
parts . push (
referenceTextPart ( {
reference ,
source ,
target ,
targetPath ,
problem : ` Path escapes configured reference @ ${ alias } : ${ target } ` ,
} ) ,
)
return
}
const info = yield * fsys . stat ( targetPath ) . pipe ( Effect . option )
if ( Option . isNone ( info ) ) {
parts . push (
referenceTextPart ( {
reference ,
source ,
target ,
targetPath ,
problem : ` Path does not exist inside configured reference @ ${ alias } : ${ target } ` ,
} ) ,
)
return
}
parts . push ( {
type : "file" ,
url : pathToFileURL ( targetPath ) . href ,
filename : name ,
mime : info.value.type === "Directory" ? "application/x-directory" : "text/plain" ,
} )
return
}
2026-04-17 00:49:39 +00:00
const filepath = name . startsWith ( "~/" )
? path . join ( os . homedir ( ) , name . slice ( 2 ) )
: path . resolve ( ctx . worktree , name )
const info = yield * fsys . stat ( filepath ) . pipe ( Effect . option )
if ( Option . isNone ( info ) ) {
const found = yield * agents . get ( name )
if ( found ) parts . push ( { type : "agent" , name : found.name } )
return
}
const stat = info . value
parts . push ( {
type : "file" ,
url : pathToFileURL ( filepath ) . href ,
filename : name ,
mime : stat.type === "Directory" ? "application/x-directory" : "text/plain" ,
2026-03-30 20:06:51 +00:00
} )
2026-04-17 00:49:39 +00:00
} ) ,
{ concurrency : "unbounded" , discard : true } ,
)
return parts
} )
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
const title = Effect . fn ( "SessionPrompt.ensureTitle" ) ( function * ( input : {
session : Session.Info
history : MessageV2.WithParts [ ]
providerID : ProviderID
modelID : ModelID
} ) {
if ( input . session . parentID ) return
if ( ! Session . isDefaultTitle ( input . session . title ) ) return
const real = ( m : MessageV2.WithParts ) = >
m . info . role === "user" && ! m . parts . every ( ( p ) = > "synthetic" in p && p . synthetic )
const idx = input . history . findIndex ( real )
if ( idx === - 1 ) return
if ( input . history . filter ( real ) . length !== 1 ) return
const context = input . history . slice ( 0 , idx + 1 )
const firstUser = context [ idx ]
if ( ! firstUser || firstUser . info . role !== "user" ) return
const firstInfo = firstUser . info
const subtasks = firstUser . parts . filter ( ( p ) : p is MessageV2 . SubtaskPart = > p . type === "subtask" )
const onlySubtasks = subtasks . length > 0 && firstUser . parts . every ( ( p ) = > p . type === "subtask" )
const ag = yield * agents . get ( "title" )
if ( ! ag ) return
const mdl = ag . model
? yield * provider . getModel ( ag . model . providerID , ag . model . modelID )
: ( ( yield * provider . getSmallModel ( input . providerID ) ) ? ?
( yield * provider . getModel ( input . providerID , input . modelID ) ) )
const msgs = onlySubtasks
? [ { role : "user" as const , content : subtasks.map ( ( p ) = > p . prompt ) . join ( "\n" ) } ]
: yield * MessageV2 . toModelMessagesEffect ( context , mdl )
const text = yield * llm
. stream ( {
agent : ag ,
user : firstInfo ,
system : [ ] ,
small : true ,
tools : { } ,
model : mdl ,
sessionID : input.session.id ,
retries : 2 ,
messages : [ { role : "user" , content : "Generate a title for this conversation:\n" } , . . . msgs ] ,
} )
. pipe (
2026-05-18 18:41:36 +00:00
Stream . filter ( LLMEvent . is . textDelta ) ,
2026-04-17 00:49:39 +00:00
Stream . map ( ( e ) = > e . text ) ,
Stream . mkString ,
Effect . orDie ,
)
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 t = cleaned . length > 100 ? cleaned . substring ( 0 , 97 ) + "..." : cleaned
yield * sessions
. setTitle ( { sessionID : input.session.id , title : t } )
. pipe ( Effect . catchCause ( ( cause ) = > elog . error ( "failed to generate title" , { error : Cause.squash ( cause ) } ) ) )
} )
2025-11-12 01:38:50 +00:00
2026-04-17 00:49:39 +00:00
const handleSubtask = Effect . fn ( "SessionPrompt.handleSubtask" ) ( function * ( input : {
task : MessageV2.SubtaskPart
model : Provider.Model
lastUser : MessageV2.User
sessionID : SessionID
session : Session.Info
msgs : MessageV2.WithParts [ ]
} ) {
const { task , model , lastUser , sessionID , session , msgs } = input
const ctx = yield * InstanceState . context
const promptOps = yield * ops ( )
const { task : taskTool } = yield * registry . named ( )
const taskModel = task . model ? yield * getModel ( task . model . providerID , task . model . modelID , sessionID ) : model
const assistantMessage : MessageV2.Assistant = yield * sessions . updateMessage ( {
id : MessageID.ascending ( ) ,
role : "assistant" ,
parentID : lastUser.id ,
sessionID ,
mode : task.agent ,
agent : task.agent ,
variant : lastUser.model.variant ,
path : { cwd : ctx.directory , root : ctx.worktree } ,
cost : 0 ,
tokens : { input : 0 , output : 0 , reasoning : 0 , cache : { read : 0 , write : 0 } } ,
modelID : taskModel.id ,
providerID : taskModel.providerID ,
time : { created : Date.now ( ) } ,
} )
let part : MessageV2.ToolPart = yield * sessions . updatePart ( {
id : PartID.ascending ( ) ,
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 ( ) } ,
} ,
2026-02-09 22:27:48 +00:00
} )
2026-04-17 00:49:39 +00:00
const taskArgs = {
prompt : task.prompt ,
description : task.description ,
subagent_type : task.agent ,
command : task.command ,
}
yield * plugin . trigger (
"tool.execute.before" ,
{ tool : TaskTool.id , sessionID , callID : part.id } ,
{ args : taskArgs } ,
)
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
const taskAgent = yield * agents . get ( task . agent )
if ( ! taskAgent ) {
const available = ( yield * agents . list ( ) ) . 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: " ${ task . agent } ". ${ hint } ` } )
yield * bus . publish ( Session . Event . Error , { sessionID , error : error.toObject ( ) } )
throw error
}
let error : Error | undefined
const taskAbort = new AbortController ( )
const result = yield * taskTool
. execute ( taskArgs , {
2025-12-15 02:11:30 +00:00
agent : task.agent ,
2025-11-17 15:57:18 +00:00
messageID : assistantMessage.id ,
2026-04-17 00:49:39 +00:00
sessionID ,
abort : taskAbort.signal ,
callID : part.callID ,
extra : { bypassAgentCheck : true , promptOps } ,
messages : msgs ,
metadata : ( val : { title? : string ; metadata? : Record < string , any > } ) = >
Effect . gen ( function * ( ) {
part = yield * sessions . updatePart ( {
. . . part ,
type : "tool" ,
state : { . . . part . state , . . . val } ,
} satisfies MessageV2 . ToolPart )
} ) ,
ask : ( req : any ) = >
permission
. ask ( {
. . . req ,
sessionID ,
ruleset : Permission.merge ( taskAgent . permission , session . permission ? ? [ ] ) ,
} )
. pipe ( Effect . orDie ) ,
2026-03-30 20:06:51 +00:00
} )
2026-04-17 00:49:39 +00:00
. pipe (
Effect . catchCause ( ( cause ) = > {
const defect = Cause . squash ( cause )
error = defect instanceof Error ? defect : new Error ( String ( defect ) )
log . error ( "subtask execution failed" , { error , agent : task.agent , description : task.description } )
return Effect . void
} ) ,
Effect . onInterrupt ( ( ) = >
Effect . gen ( function * ( ) {
taskAbort . abort ( )
assistantMessage . finish = "tool-calls"
assistantMessage . time . completed = Date . now ( )
yield * sessions . updateMessage ( assistantMessage )
if ( part . state . status === "running" ) {
yield * sessions . updatePart ( {
2026-04-11 03:12:04 +00:00
. . . part ,
2026-04-17 00:49:39 +00:00
state : {
status : "error" ,
error : "Cancelled" ,
time : { start : part.state.time.start , end : Date.now ( ) } ,
metadata : part.state.metadata ,
input : part.state.input ,
} ,
2026-04-11 03:12:04 +00:00
} satisfies MessageV2 . ToolPart )
2026-04-17 00:49:39 +00:00
}
2026-03-30 20:06:51 +00:00
} ) ,
2026-04-17 00:49:39 +00:00
) ,
2025-12-17 01:28:09 +00:00
)
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
const attachments = result ? . attachments ? . map ( ( attachment ) = > ( {
. . . attachment ,
id : PartID.ascending ( ) ,
sessionID ,
messageID : assistantMessage.id ,
} ) )
yield * plugin . trigger (
"tool.execute.after" ,
{ tool : TaskTool.id , sessionID , callID : part.id , args : taskArgs } ,
result ,
)
2025-12-16 21:42:21 +00:00
2026-04-17 00:49:39 +00:00
assistantMessage . finish = "tool-calls"
assistantMessage . time . completed = Date . now ( )
yield * sessions . updateMessage ( assistantMessage )
2025-11-17 15:57:18 +00:00
2026-04-17 00:49:39 +00:00
if ( result && part . state . status === "running" ) {
2026-03-30 20:06:51 +00:00
yield * sessions . updatePart ( {
2026-04-17 00:49:39 +00:00
. . . part ,
2026-03-30 20:06:51 +00:00
state : {
2026-04-17 00:49:39 +00:00
status : "completed" ,
input : part.state.input ,
title : result.title ,
metadata : result.metadata ,
output : result.output ,
attachments ,
time : { . . . part . state . time , end : Date.now ( ) } ,
2025-11-17 15:57:18 +00:00
} ,
2026-04-17 00:49:39 +00:00
} satisfies MessageV2 . ToolPart )
}
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
if ( ! result ) {
yield * sessions . updatePart ( {
. . . part ,
state : {
status : "error" ,
error : error ? ` Tool execution failed: ${ error . message } ` : "Tool execution failed" ,
time : {
start : part.state.status === "running" ? part.state.time.start : Date.now ( ) ,
end : Date.now ( ) ,
} ,
metadata : part.state.status === "pending" ? undefined : part . state . metadata ,
input : part.state.input ,
2026-03-30 20:06:51 +00:00
} ,
2026-04-17 00:49:39 +00:00
} satisfies MessageV2 . ToolPart )
}
if ( ! task . command ) return
const summaryUserMsg : MessageV2.User = {
id : MessageID.ascending ( ) ,
sessionID ,
role : "user" ,
time : { created : Date.now ( ) } ,
agent : lastUser.agent ,
model : lastUser.model ,
}
yield * sessions . updateMessage ( summaryUserMsg )
yield * sessions . updatePart ( {
id : PartID.ascending ( ) ,
messageID : summaryUserMsg.id ,
sessionID ,
type : "text" ,
text : "Summarize the task tool output above and continue with your task." ,
synthetic : true ,
} satisfies MessageV2 . TextPart )
} )
2026-02-12 04:54:05 +00:00
2026-04-27 20:47:18 +00:00
const shellImpl = Effect . fn ( "SessionPrompt.shellImpl" ) ( function * ( input : ShellInput , ready? : Latch.Latch ) {
return yield * Effect . uninterruptibleMask ( ( restore ) = >
2026-04-17 00:49:39 +00:00
Effect . gen ( function * ( ) {
2026-04-27 20:47:18 +00:00
const markReady = ready ? ready . open . pipe ( Effect . asVoid ) : Effect . void
const { msg , part , cwd } = yield * Effect . gen ( function * ( ) {
const ctx = yield * InstanceState . context
2026-05-06 01:33:47 +00:00
const session = yield * sessions . get ( input . sessionID ) . pipe ( Effect . orDie )
2026-04-27 20:47:18 +00:00
if ( session . revert ) {
yield * revert . cleanup ( session )
}
const agent = yield * agents . get ( input . agent )
if ( ! agent ) {
const available = ( yield * agents . list ( ) ) . 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: " ${ input . agent } ". ${ hint } ` } )
yield * bus . publish ( Session . Event . Error , { sessionID : input.sessionID , error : error.toObject ( ) } )
throw error
}
2026-05-10 22:53:14 +00:00
const model = input . model ? ? agent . model ? ? ( yield * currentModel ( input . sessionID ) )
2026-04-27 20:47:18 +00:00
const userMsg : MessageV2.User = {
id : input.messageID ? ? MessageID . ascending ( ) ,
sessionID : input.sessionID ,
time : { created : Date.now ( ) } ,
role : "user" ,
agent : input.agent ,
model : { providerID : model.providerID , modelID : model.modelID } ,
}
yield * sessions . updateMessage ( userMsg )
const userPart : MessageV2.Part = {
type : "text" ,
id : PartID.ascending ( ) ,
messageID : userMsg.id ,
sessionID : input.sessionID ,
text : "The following tool was executed by the user" ,
synthetic : true ,
}
yield * sessions . updatePart ( userPart )
const msg : MessageV2.Assistant = {
id : MessageID.ascending ( ) ,
sessionID : input.sessionID ,
parentID : userMsg.id ,
mode : input.agent ,
agent : input.agent ,
cost : 0 ,
path : { cwd : ctx.directory , root : ctx.worktree } ,
time : { created : Date.now ( ) } ,
role : "assistant" ,
tokens : { input : 0 , output : 0 , reasoning : 0 , cache : { read : 0 , write : 0 } } ,
modelID : model.modelID ,
providerID : model.providerID ,
}
2026-04-17 00:49:39 +00:00
yield * sessions . updateMessage ( msg )
2026-05-03 02:09:48 +00:00
const started = Date . now ( )
2026-04-27 20:47:18 +00:00
const part : MessageV2.ToolPart = {
type : "tool" ,
id : PartID.ascending ( ) ,
messageID : msg.id ,
sessionID : input.sessionID ,
2026-05-02 23:18:48 +00:00
tool : ShellID.ToolID ,
2026-04-27 20:47:18 +00:00
callID : ulid ( ) ,
state : {
status : "running" ,
2026-05-03 02:09:48 +00:00
time : { start : started } ,
2026-04-27 20:47:18 +00:00
input : { command : input.command } ,
} ,
2026-03-30 20:06:51 +00:00
}
2026-04-17 00:49:39 +00:00
yield * sessions . updatePart ( part )
2026-05-13 12:14:09 +00:00
if ( flags . experimentalEventSystem ) {
2026-05-15 00:50:23 +00:00
yield * events . publish ( SessionEvent . Shell . Started , {
2026-05-11 01:20:13 +00:00
sessionID : input.sessionID ,
timestamp : DateTime.makeUnsafe ( started ) ,
2026-05-15 00:50:23 +00:00
callID : part.callID ,
2026-05-11 01:20:13 +00:00
command : input.command ,
} )
}
2026-04-27 20:47:18 +00:00
return { msg , part , cwd : ctx.directory }
} ) . pipe ( Effect . ensuring ( markReady ) )
2026-04-17 00:49:39 +00:00
2026-04-27 20:47:18 +00:00
const cfg = yield * config . get ( )
const sh = Shell . preferred ( cfg . shell )
const args = Shell . args ( sh , input . command , cwd )
let output = ""
let aborted = false
const finish = Effect . uninterruptible (
Effect . gen ( function * ( ) {
if ( aborted ) {
output += "\n\n" + [ "<metadata>" , "User aborted the command" , "</metadata>" ] . join ( "\n" )
}
2026-05-03 02:09:48 +00:00
const completed = Date . now ( )
2026-05-13 12:14:09 +00:00
if ( flags . experimentalEventSystem ) {
2026-05-15 00:50:23 +00:00
yield * events . publish ( SessionEvent . Shell . Ended , {
2026-05-11 01:20:13 +00:00
sessionID : input.sessionID ,
timestamp : DateTime.makeUnsafe ( completed ) ,
callID : part.callID ,
output ,
} )
}
2026-04-27 20:47:18 +00:00
if ( ! msg . time . completed ) {
2026-05-03 02:09:48 +00:00
msg . time . completed = completed
2026-04-27 20:47:18 +00:00
yield * sessions . updateMessage ( msg )
}
if ( part . state . status === "running" ) {
part . state = {
status : "completed" ,
2026-05-03 02:09:48 +00:00
time : { . . . part . state . time , end : completed } ,
2026-04-27 20:47:18 +00:00
input : part.state.input ,
title : "" ,
metadata : { output , description : "" } ,
output ,
}
yield * sessions . updatePart ( part )
}
} ) ,
)
const exit = yield * restore (
Effect . gen ( function * ( ) {
const shellEnv = yield * plugin . trigger (
"shell.env" ,
{ cwd , sessionID : input.sessionID , callID : part.callID } ,
{ env : { } } ,
)
const cmd = ChildProcess . make ( sh , args , {
cwd ,
extendEnv : true ,
env : { . . . shellEnv . env , TERM : "dumb" } ,
stdin : "ignore" ,
forceKillAfter : "3 seconds" ,
} )
const handle = yield * spawner . spawn ( cmd )
yield * Stream . runForEach ( Stream . decodeText ( handle . all ) , ( chunk ) = >
Effect . gen ( function * ( ) {
output += chunk
if ( part . state . status === "running" ) {
part . state . metadata = { output , description : "" }
yield * sessions . updatePart ( part )
}
} ) ,
)
yield * handle . exitCode
} ) . pipe ( Effect . scoped , Effect . orDie ) ,
) . pipe ( Effect . exit )
if ( Exit . isFailure ( exit ) && Cause . hasInterrupts ( exit . cause ) && ! Cause . hasDies ( exit . cause ) ) {
2026-04-17 00:49:39 +00:00
aborted = true
2026-04-27 20:47:18 +00:00
}
yield * finish
2026-01-01 22:54:11 +00:00
2026-04-27 20:47:18 +00:00
if ( Exit . isFailure ( exit ) && ! aborted && ! Cause . hasInterruptsOnly ( exit . cause ) ) {
return yield * Effect . failCause ( exit . cause )
}
2026-04-01 00:07:58 +00:00
2026-04-27 20:47:18 +00:00
return { info : msg , parts : [ part ] }
} ) ,
)
2026-04-17 00:49:39 +00:00
} )
2026-01-20 05:19:21 +00:00
2026-04-17 00:49:39 +00:00
const getModel = Effect . fn ( "SessionPrompt.getModel" ) ( function * (
providerID : ProviderID ,
modelID : ModelID ,
sessionID : SessionID ,
) {
const exit = yield * provider . getModel ( providerID , modelID ) . pipe ( Effect . exit )
if ( Exit . isSuccess ( exit ) ) return exit . value
const err = Cause . squash ( exit . cause )
if ( Provider . ModelNotFoundError . isInstance ( err ) ) {
2026-05-13 22:23:09 +00:00
const hint = err . suggestions ? . length ? ` Did you mean: ${ err . suggestions . join ( ", " ) } ? ` : ""
2026-04-17 00:49:39 +00:00
yield * bus . publish ( Session . Event . Error , {
sessionID ,
error : new NamedError . Unknown ( {
2026-05-13 22:23:09 +00:00
message : ` Model not found: ${ err . providerID } / ${ err . modelID } . ${ hint } ` ,
2026-04-17 00:49:39 +00:00
} ) . toObject ( ) ,
} )
}
2026-05-13 22:23:09 +00:00
return yield * Effect . die ( err )
2026-04-17 00:49:39 +00:00
} )
2026-01-07 04:29:17 +00:00
2026-05-10 22:53:14 +00:00
const currentModel = Effect . fnUntraced ( function * ( sessionID : SessionID ) {
const current = Database . use ( ( db ) = >
db . select ( { model : SessionTable.model } ) . from ( SessionTable ) . where ( eq ( SessionTable . id , sessionID ) ) . get ( ) ,
)
if ( current ? . model ) {
return {
providerID : ProviderID.make ( current . model . providerID ) ,
modelID : ModelID.make ( current . model . id ) ,
. . . ( current . model . variant && current . model . variant !== "default" ? { variant : current.model.variant } : { } ) ,
}
}
2026-05-13 07:52:12 +00:00
const match = yield * sessions
. findMessage ( sessionID , ( m ) = > m . info . role === "user" && ! ! m . info . model )
. pipe ( Effect . orDie )
2026-04-17 00:49:39 +00:00
if ( Option . isSome ( match ) && match . value . info . role === "user" ) return match . value . info . model
return yield * provider . defaultModel ( )
} )
2026-02-12 04:54:05 +00:00
2026-04-17 00:49:39 +00:00
const createUserMessage = Effect . fn ( "SessionPrompt.createUserMessage" ) ( function * ( input : PromptInput ) {
2026-05-12 19:38:30 +00:00
const agentName = input . agent
const ag = agentName ? yield * agents . get ( agentName ) : yield * agents . defaultInfo ( )
2026-04-17 00:49:39 +00:00
if ( ! ag ) {
const available = ( yield * agents . list ( ) ) . 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 } ` } )
yield * bus . publish ( Session . Event . Error , { sessionID : input.sessionID , error : error.toObject ( ) } )
throw error
}
2026-05-10 22:53:14 +00:00
const current = Database . use ( ( db ) = >
db
. select ( { agent : SessionTable.agent , model : SessionTable.model } )
. from ( SessionTable )
. where ( eq ( SessionTable . id , input . sessionID ) )
. get ( ) ,
)
const model = input . model ? ? ag . model ? ? ( yield * currentModel ( input . sessionID ) )
2026-04-17 00:49:39 +00:00
const same = ag . model && model . providerID === ag . model . providerID && model . modelID === ag . model . modelID
const full =
! input . variant && ag . variant && same
2026-05-13 22:23:09 +00:00
? yield * provider
. getModel ( model . providerID , model . modelID )
. pipe ( Effect . catchIf ( Provider . ModelNotFoundError . isInstance , ( ) = > Effect . succeed ( undefined ) ) )
2026-04-17 00:49:39 +00:00
: undefined
const variant = input . variant ? ? ( ag . variant && full ? . variants ? . [ ag . variant ] ? ag.variant : undefined )
const info : MessageV2.User = {
id : input.messageID ? ? MessageID . ascending ( ) ,
role : "user" ,
sessionID : input.sessionID ,
time : { created : Date.now ( ) } ,
tools : input.tools ,
agent : ag.name ,
model : {
providerID : model.providerID ,
modelID : model.modelID ,
variant ,
} ,
system : input.system ,
format : input.format ,
}
2026-01-04 15:12:54 +00:00
2026-05-03 02:09:48 +00:00
if ( current ? . agent !== info . agent ) {
2026-05-15 00:50:23 +00:00
yield * events . publish ( SessionEvent . AgentSwitched , {
2026-05-11 01:20:13 +00:00
sessionID : input.sessionID ,
timestamp : DateTime.makeUnsafe ( info . time . created ) ,
agent : info.agent ,
} )
2026-05-03 02:09:48 +00:00
}
if (
current ? . model ? . providerID !== info . model . providerID ||
current . model . id !== info . model . modelID ||
2026-05-10 22:53:14 +00:00
( current . model . variant === "default" ? undefined : current . model . variant ) !== info . model . variant
2026-05-03 02:09:48 +00:00
) {
2026-05-15 00:50:23 +00:00
yield * events . publish ( SessionEvent . ModelSwitched , {
2026-05-11 01:20:13 +00:00
sessionID : input.sessionID ,
timestamp : DateTime.makeUnsafe ( info . time . created ) ,
model : {
2026-05-13 14:43:08 +00:00
id : ModelV2.ID.make ( info . model . modelID ) ,
providerID : ProviderV2.ID.make ( info . model . providerID ) ,
variant : ModelV2.VariantID.make ( info . model . variant ? ? "default" ) ,
2026-05-05 02:35:21 +00:00
} ,
2026-05-11 01:20:13 +00:00
} )
2026-05-03 02:09:48 +00:00
}
2026-04-17 00:49:39 +00:00
yield * Effect . addFinalizer ( ( ) = > instruction . clear ( info . id ) )
2026-01-04 15:12:54 +00:00
2026-04-17 00:49:39 +00:00
type Draft < T > = T extends MessageV2 . Part ? Omit < T , " id " > & { id? : string } : never
const assign = ( part : Draft < MessageV2.Part > ) : MessageV2 . Part = > ( {
. . . part ,
id : part.id ? PartID . make ( part . id ) : PartID . ascending ( ) ,
} )
2026-05-11 15:20:03 +00:00
const referenceContextFromFilePart = Effect . fnUntraced ( function * (
part : Extract < PromptInput [ " parts " ] [ number ] , { type : " file " } > ,
filepath : string ,
) {
const name = part . filename ? . replace ( /#\d+(?:-\d*)?$/ , "" )
if ( ! name ) return
const slash = name . indexOf ( "/" )
if ( slash === - 1 ) return
const reference = yield * references . get ( name . slice ( 0 , slash ) )
if ( ! reference || reference . kind === "invalid" ) return
if ( ! AppFileSystem . contains ( reference . path , filepath ) ) return
const target = path . relative ( reference . path , filepath ) . split ( path . sep ) . join ( "/" )
if ( ! target || target . startsWith ( "../" ) || target === ".." ) return
return referenceTextPart ( {
reference ,
source : part.source?.text ? ? { value : ` @ ${ name } ` , start : 0 , end : name.length + 1 } ,
target ,
targetPath : filepath ,
} )
} )
2026-04-17 00:49:39 +00:00
const resolvePart : ( part : PromptInput [ "parts" ] [ number ] ) = > Effect . Effect < Draft < MessageV2.Part > [ ] > = Effect . fn (
"SessionPrompt.resolveUserPart" ,
) ( function * ( part ) {
if ( part . type === "file" ) {
if ( part . source ? . type === "resource" ) {
const { clientName , uri } = part . source
log . info ( "mcp resource" , { clientName , uri , mime : part.mime } )
const pieces : Draft < MessageV2.Part > [ ] = [
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : ` Reading MCP resource: ${ part . filename } ( ${ uri } ) ` ,
} ,
]
const exit = yield * mcp . readResource ( clientName , uri ) . pipe ( Effect . exit )
if ( Exit . isSuccess ( exit ) ) {
const content = exit . value
if ( ! content ) throw new Error ( ` Resource not found: ${ clientName } / ${ uri } ` )
const items = Array . isArray ( content . contents ) ? content . contents : [ content . contents ]
for ( const c of items ) {
if ( "text" in c && c . text ) {
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : c.text ,
} )
} else if ( "blob" in c && c . blob ) {
const mime = "mimeType" in c ? c.mimeType : part.mime
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : ` [Binary content: ${ mime } ] ` ,
} )
2026-01-04 15:12:54 +00:00
}
}
2026-04-17 00:49:39 +00:00
pieces . push ( { . . . part , messageID : info.id , sessionID : input.sessionID } )
} else {
const error = Cause . squash ( exit . cause )
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 } ` ,
} )
2026-01-04 15:12:54 +00:00
}
2026-04-17 00:49:39 +00:00
return pieces
}
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 : decodeDataUrl ( part . url ) ,
} ,
{ . . . part , messageID : info.id , sessionID : input.sessionID } ,
]
}
break
case "file:" : {
log . info ( "file" , { mime : part.mime } )
const filepath = fileURLToPath ( part . url )
2026-05-11 15:20:03 +00:00
const referenceContext = yield * referenceContextFromFilePart ( part , filepath )
2026-04-30 23:28:46 +00:00
const mime = ( yield * fsys . isDir ( filepath ) ) ? "application/x-directory" : part . mime
2026-04-17 00:49:39 +00:00
const { read } = yield * registry . named ( )
const execRead = ( args : Parameters < typeof read.execute > [ 0 ] , extra? : Tool.Context [ "extra" ] ) = > {
const controller = new AbortController ( )
return read
. execute ( args , {
sessionID : input.sessionID ,
abort : controller.signal ,
agent : input.agent ! ,
messageID : info.id ,
extra : { bypassCwdCheck : true , . . . extra } ,
messages : [ ] ,
metadata : ( ) = > Effect . void ,
ask : ( ) = > Effect . void ,
} )
. pipe ( Effect . onInterrupt ( ( ) = > Effect . sync ( ( ) = > controller . abort ( ) ) ) )
}
2026-04-09 01:07:55 +00:00
2026-04-30 23:28:46 +00:00
if ( mime === "text/plain" ) {
2026-04-17 00:49:39 +00:00
let offset : number | undefined
let limit : number | undefined
const range = { start : url.searchParams.get ( "start" ) , end : url.searchParams.get ( "end" ) }
if ( range . start != null ) {
const filePathURI = part . url . split ( "?" ) [ 0 ]
let start = parseInt ( range . start )
let end = range . end ? parseInt ( range . end ) : undefined
if ( start === end ) {
2026-04-17 00:50:44 +00:00
const symbols = yield * lsp . documentSymbol ( filePathURI ) . pipe ( Effect . catch ( ( ) = > Effect . succeed ( [ ] ) ) )
2026-04-17 00:49:39 +00:00
for ( const symbol of symbols ) {
let r : LSP.Range | undefined
if ( "range" in symbol ) r = symbol . range
else if ( "location" in symbol ) r = symbol . location . range
if ( r ? . start ? . line && r ? . start ? . line === start ) {
start = r . start . line
end = r ? . end ? . line ? ? start
break
2025-09-13 09:46:14 +00:00
}
}
}
2026-04-17 00:49:39 +00:00
offset = Math . max ( start , 1 )
if ( end ) limit = end - ( offset - 1 )
2026-03-30 20:06:51 +00:00
}
2026-04-17 00:49:39 +00:00
const args = { filePath : filepath , offset , limit }
const pieces : Draft < MessageV2.Part > [ ] = [
2026-05-11 15:20:03 +00:00
. . . ( referenceContext
? [ { . . . referenceContext , messageID : info.id , sessionID : input.sessionID } ]
: [ ] ) ,
2026-04-17 00:49:39 +00:00
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : ` Called the Read tool with the following input: ${ JSON . stringify ( args ) } ` ,
} ,
]
const exit = yield * provider . getModel ( info . model . providerID , info . model . modelID ) . pipe (
Effect . flatMap ( ( mdl ) = > execRead ( args , { model : mdl } ) ) ,
Effect . exit ,
)
if ( Exit . isSuccess ( exit ) ) {
const result = exit . value
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : result.output ,
} )
if ( result . attachments ? . length ) {
pieces . push (
. . . result . attachments . map ( ( a ) = > ( {
. . . a ,
synthetic : true ,
filename : a.filename ? ? part . filename ,
2026-04-09 01:07:55 +00:00
messageID : info.id ,
sessionID : input.sessionID ,
2026-04-17 00:49:39 +00:00
} ) ) ,
)
} else {
2026-04-30 23:28:46 +00:00
pieces . push ( { . . . part , mime , messageID : info.id , sessionID : input.sessionID } )
2026-04-09 01:07:55 +00:00
}
2026-04-17 00:49:39 +00:00
} else {
const error = Cause . squash ( exit . cause )
log . error ( "failed to read file" , { error } )
const message = error instanceof Error ? error.message : String ( error )
yield * 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
}
2026-04-30 23:28:46 +00:00
if ( mime === "application/x-directory" ) {
2026-04-17 00:49:39 +00:00
const args = { filePath : filepath }
const exit = yield * execRead ( args ) . pipe ( Effect . exit )
if ( Exit . isFailure ( exit ) ) {
const error = Cause . squash ( exit . cause )
log . error ( "failed to read directory" , { error } )
const message = error instanceof Error ? error.message : String ( error )
yield * bus . publish ( Session . Event . Error , {
sessionID : input.sessionID ,
error : new NamedError . Unknown ( { message } ) . toObject ( ) ,
} )
2026-03-30 20:06:51 +00:00
return [
2026-05-11 15:20:03 +00:00
. . . ( referenceContext
? [ { . . . referenceContext , messageID : info.id , sessionID : input.sessionID } ]
: [ ] ) ,
2026-03-30 20:06:51 +00:00
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
2026-04-17 00:49:39 +00:00
text : ` Read tool failed to read ${ filepath } with the following error: ${ message } ` ,
2026-03-30 20:06:51 +00:00
} ,
]
2026-01-01 22:54:11 +00:00
}
2025-09-13 09:46:14 +00:00
return [
2026-05-11 15:20:03 +00:00
. . . ( referenceContext
? [ { . . . referenceContext , messageID : info.id , sessionID : input.sessionID } ]
: [ ] ) ,
2025-09-13 09:46:14 +00:00
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
2026-04-17 00:49:39 +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 ,
2026-04-17 00:49:39 +00:00
type : "text" ,
synthetic : true ,
text : exit.value.output ,
2025-09-13 09:46:14 +00:00
} ,
2026-04-30 23:28:46 +00:00
{ . . . part , mime , messageID : info.id , sessionID : input.sessionID } ,
2025-09-13 09:46:14 +00:00
]
}
2026-04-17 00:49:39 +00:00
return [
2026-05-11 15:20:03 +00:00
. . . ( referenceContext ? [ { . . . referenceContext , messageID : info.id , sessionID : input.sessionID } ] : [ ] ) ,
2026-04-17 00:49:39 +00:00
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : ` Called the Read tool with the following input: {"filePath":" ${ filepath } "} ` ,
} ,
{
id : part.id ,
messageID : info.id ,
sessionID : input.sessionID ,
type : "file" ,
url :
2026-04-30 23:28:46 +00:00
` data: ${ mime } ;base64, ` +
2026-04-17 00:49:39 +00:00
Buffer . from ( yield * fsys . readFile ( filepath ) . pipe ( Effect . catch ( Effect . die ) ) ) . toString ( "base64" ) ,
2026-04-30 23:28:46 +00:00
mime ,
2026-04-17 00:49:39 +00:00
filename : part.filename ! ,
source : part.source ,
} ,
]
2026-03-30 20:06:51 +00:00
}
}
2026-04-17 00:49:39 +00:00
}
2025-09-13 09:46:14 +00:00
2026-04-17 00:49:39 +00:00
if ( part . type === "agent" ) {
const perm = Permission . evaluate ( "task" , part . name , ag . permission )
const hint = perm . action === "deny" ? " . Invoked by user; guaranteed to exist." : ""
return [
{ . . . part , messageID : info.id , sessionID : input.sessionID } ,
{
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text :
" 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
2026-04-17 00:49:39 +00:00
return [ { . . . part , messageID : info.id , sessionID : input.sessionID } ]
} )
2026-03-30 20:06:51 +00:00
2026-05-10 06:48:19 +00:00
const resolvedParts = yield * Effect . forEach ( input . parts , resolvePart , { concurrency : "unbounded" } ) . pipe (
2026-04-17 00:49:39 +00:00
Effect . map ( ( x ) = > x . flat ( ) . map ( assign ) ) ,
)
2025-09-13 09:46:14 +00:00
2026-04-17 00:49:39 +00:00
yield * plugin . trigger (
"chat.message" ,
{
sessionID : input.sessionID ,
agent : input.agent ,
model : input.model ,
messageID : input.messageID ,
variant : input.variant ,
} ,
2026-05-10 06:48:19 +00:00
{ message : info , parts : resolvedParts } ,
)
2026-05-14 04:06:07 +00:00
const parts = yield * Effect . forEach ( resolvedParts , ( part ) = >
part . type === "file" && part . mime . startsWith ( "image/" )
? image . normalize ( part ) . pipe (
Effect . catchIf (
( error ) = > error instanceof Image . ResizerUnavailableError ,
( ) = > Effect . succeed ( part ) ,
) ,
)
: Effect . succeed ( part ) ,
)
2026-03-17 23:18:16 +00:00
2026-05-11 02:59:20 +00:00
const parsed = decodeMessageInfo ( info , { errors : "all" , propertyOrder : "original" } )
if ( Exit . isFailure ( parsed ) ) {
2026-04-17 00:49:39 +00:00
log . error ( "invalid user message before save" , {
sessionID : input.sessionID ,
messageID : info.id ,
agent : info.agent ,
model : info.model ,
2026-05-11 02:59:20 +00:00
cause : Cause.pretty ( parsed . cause ) ,
2026-04-17 00:49:39 +00:00
} )
}
parts . forEach ( ( part , index ) = > {
2026-05-11 02:59:20 +00:00
const p = decodeMessagePart ( part , { errors : "all" , propertyOrder : "original" } )
if ( Exit . isSuccess ( p ) ) return
2026-04-17 00:49:39 +00:00
log . error ( "invalid user part before save" , {
sessionID : input.sessionID ,
messageID : info.id ,
partID : part.id ,
partType : part.type ,
index ,
2026-05-11 02:59:20 +00:00
cause : Cause.pretty ( p . cause ) ,
2026-04-17 00:49:39 +00:00
part ,
2026-03-30 20:06:51 +00:00
} )
2026-04-17 00:49:39 +00:00
} )
2025-09-13 09:46:14 +00:00
2026-04-17 00:49:39 +00:00
yield * sessions . updateMessage ( info )
for ( const part of parts ) yield * sessions . updatePart ( part )
2026-05-03 02:09:48 +00:00
const nextPrompt = parts . reduce (
( result , part ) = > {
if ( part . type === "text" ) {
if ( part . synthetic ) result . synthetic . push ( part . text )
else result . text . push ( part . text )
2026-05-11 07:27:26 +00:00
const reference = referencePromptMetadata ( part . metadata ? . reference )
if ( reference ) {
result . references . push (
new ReferenceAttachment ( {
name : reference.name ,
kind : reference.kind ,
uri : reference.path ? pathToFileURL ( reference . path ) . href : undefined ,
repository : reference.repository ,
branch : reference.branch ,
target : reference.target ,
targetUri : reference.targetPath ? pathToFileURL ( reference . targetPath ) . href : undefined ,
problem : reference.problem ,
source : new Source ( {
start : reference.source.start ,
end : reference.source.end ,
text : reference.source.value ,
} ) ,
} ) ,
)
}
2026-05-03 02:09:48 +00:00
}
if ( part . type === "file" ) {
result . files . push (
new FileAttachment ( {
uri : part.url ,
mime : part.mime ,
name : part.filename ,
source : part.source
? new Source ( {
start : part.source.text.start ,
end : part.source.text.end ,
text : part.source.text.value ,
} )
: undefined ,
} ) ,
)
}
if ( part . type === "agent" ) {
result . agents . push (
new AgentAttachment ( {
name : part.name ,
source : part.source
? new Source ( {
start : part.source.start ,
end : part.source.end ,
text : part.source.value ,
} )
: undefined ,
} ) ,
)
}
return result
} ,
{
text : [ ] as string [ ] ,
files : [ ] as FileAttachment [ ] ,
agents : [ ] as AgentAttachment [ ] ,
2026-05-11 07:27:26 +00:00
references : [ ] as ReferenceAttachment [ ] ,
2026-05-03 02:09:48 +00:00
synthetic : [ ] as string [ ] ,
} ,
)
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
2026-05-13 12:14:09 +00:00
if ( flags . experimentalEventSystem ) {
2026-05-15 00:50:23 +00:00
yield * events . publish ( SessionEvent . Prompted , {
2026-05-03 02:09:48 +00:00
sessionID : input.sessionID ,
timestamp : DateTime.makeUnsafe ( info . time . created ) ,
2026-05-11 01:20:13 +00:00
prompt : {
text : nextPrompt.text.join ( "\n" ) ,
files : nextPrompt.files ,
agents : nextPrompt.agents ,
2026-05-11 07:27:26 +00:00
references : nextPrompt.references ,
2026-05-11 01:20:13 +00:00
} ,
2026-05-03 02:09:48 +00:00
} )
}
2026-05-11 01:20:13 +00:00
for ( const text of nextPrompt . synthetic ) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
2026-05-13 12:14:09 +00:00
if ( flags . experimentalEventSystem ) {
2026-05-15 00:50:23 +00:00
yield * events . publish ( SessionEvent . Synthetic , {
2026-05-11 01:20:13 +00:00
sessionID : input.sessionID ,
timestamp : DateTime.makeUnsafe ( info . time . created ) ,
text ,
} )
}
}
2025-09-13 09:46:14 +00:00
2026-04-17 00:49:39 +00:00
return { info , parts }
} , Effect . scoped )
2026-01-13 20:55:48 +00:00
2026-05-14 04:06:07 +00:00
const prompt : ( input : PromptInput ) = > Effect . Effect < MessageV2.WithParts , Image.Error > = Effect . fn (
"SessionPrompt.prompt" ,
) ( function * ( input : PromptInput ) {
const session = yield * sessions . get ( input . sessionID ) . pipe ( Effect . orDie )
yield * revert . cleanup ( session )
const message = yield * createUserMessage ( input )
yield * sessions . touch ( input . sessionID )
const permissions : Permission.Ruleset = [ ]
for ( const [ t , enabled ] of Object . entries ( input . tools ? ? { } ) ) {
permissions . push ( { permission : t , action : enabled ? "allow" : "deny" , pattern : "*" } )
}
if ( permissions . length > 0 ) {
session . permission = permissions
yield * sessions . setPermission ( { sessionID : session.id , permission : permissions } )
}
2026-04-17 00:49:39 +00:00
2026-05-14 04:06:07 +00:00
if ( input . noReply === true ) return message
return yield * loop ( { sessionID : input.sessionID } )
} )
2026-04-17 00:49:39 +00:00
const lastAssistant = Effect . fnUntraced ( function * ( sessionID : SessionID ) {
2026-05-13 07:52:12 +00:00
const match = yield * sessions . findMessage ( sessionID , ( m ) = > m . info . role !== "user" ) . pipe ( Effect . orDie )
2026-04-17 00:49:39 +00:00
if ( Option . isSome ( match ) ) return match . value
2026-05-13 07:52:12 +00:00
const msgs = yield * sessions . messages ( { sessionID , limit : 1 } ) . pipe ( Effect . orDie )
2026-04-17 00:49:39 +00:00
if ( msgs . length > 0 ) return msgs [ 0 ]
throw new Error ( "Impossible" )
} )
const runLoop : ( sessionID : SessionID ) = > Effect . Effect < MessageV2.WithParts > = Effect . fn ( "SessionPrompt.run" ) (
function * ( sessionID : SessionID ) {
const ctx = yield * InstanceState . context
const slog = elog . with ( { sessionID } )
2026-05-06 01:33:47 +00:00
let structured : unknown
2026-04-17 00:49:39 +00:00
let step = 0
2026-05-06 01:33:47 +00:00
const session = yield * sessions . get ( sessionID ) . pipe ( Effect . orDie )
2026-04-17 00:49:39 +00:00
while ( true ) {
yield * status . set ( sessionID , { type : "busy" } )
yield * slog . info ( "loop" , { step } )
let msgs = yield * MessageV2 . filterCompactedEffect ( sessionID )
2026-05-14 17:56:12 +00:00
const { user : lastUser , assistant : lastAssistant , finished : lastFinished , tasks } = MessageV2 . latest ( msgs )
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
if ( ! lastUser ) throw new Error ( "No user message found in stream. This should never happen." )
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
const lastAssistantMsg = msgs . findLast (
( msg ) = > msg . info . role === "assistant" && msg . info . id === lastAssistant ? . id ,
)
// Some providers return "stop" even when the assistant message contains tool calls.
// Keep the loop running so tool results can be sent back to the model.
// Skip provider-executed tool parts — those were fully handled within the
// provider's stream (e.g. DWS Agent Platform) and don't need a re-loop.
const hasToolCalls =
lastAssistantMsg ? . parts . some ( ( part ) = > part . type === "tool" && ! part . metadata ? . providerExecuted ) ? ? false
if (
lastAssistant ? . finish &&
! [ "tool-calls" ] . includes ( lastAssistant . finish ) &&
! hasToolCalls &&
lastUser . id < lastAssistant . id
) {
yield * slog . info ( "exiting loop" )
break
}
2026-01-13 20:55:48 +00:00
2026-04-17 00:49:39 +00:00
step ++
if ( step === 1 )
yield * title ( {
session ,
modelID : lastUser.model.modelID ,
providerID : lastUser.model.providerID ,
history : msgs ,
} ) . pipe ( Effect . ignore , Effect . forkIn ( scope ) )
2026-01-13 20:55:48 +00:00
2026-04-17 00:49:39 +00:00
const model = yield * getModel ( lastUser . model . providerID , lastUser . model . modelID , sessionID )
const task = tasks . pop ( )
2026-04-02 02:34:01 +00:00
2026-04-17 00:49:39 +00:00
if ( task ? . type === "subtask" ) {
yield * handleSubtask ( { task , model , lastUser , sessionID , session , msgs } )
continue
}
2026-01-13 20:55:48 +00:00
2026-04-17 00:49:39 +00:00
if ( task ? . type === "compaction" ) {
const result = yield * compaction . process ( {
messages : msgs ,
parentID : lastUser.id ,
sessionID ,
auto : task.auto ,
overflow : task.overflow ,
} )
if ( result === "stop" ) break
continue
}
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
if (
lastFinished &&
lastFinished . summary !== true &&
( yield * compaction . isOverflow ( { tokens : lastFinished.tokens , model } ) )
) {
yield * compaction . create ( { sessionID , agent : lastUser.agent , model : lastUser.model , auto : true } )
continue
}
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
const agent = yield * agents . get ( lastUser . agent )
if ( ! agent ) {
const available = ( yield * agents . list ( ) ) . 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: " ${ lastUser . agent } ". ${ hint } ` } )
yield * bus . publish ( Session . Event . Error , { sessionID , error : error.toObject ( ) } )
throw error
}
const maxSteps = agent . steps ? ? Infinity
const isLastStep = step >= maxSteps
2026-05-18 16:36:38 +00:00
msgs = yield * SessionReminders . apply ( { messages : msgs , agent , session } ) . pipe (
Effect . provideService ( RuntimeFlags . Service , flags ) ,
Effect . provideService ( AppFileSystem . Service , fsys ) ,
Effect . provideService ( Session . Service , sessions ) ,
)
2026-04-17 00:49:39 +00:00
const msg : MessageV2.Assistant = {
id : MessageID.ascending ( ) ,
parentID : lastUser.id ,
role : "assistant" ,
mode : agent.name ,
agent : agent.name ,
variant : lastUser.model.variant ,
path : { cwd : ctx.directory , root : ctx.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 ( ) } ,
sessionID ,
}
yield * sessions . updateMessage ( msg )
2026-05-14 06:19:11 +00:00
const finalizeInterruptedAssistant = Effect . gen ( function * ( ) {
if ( msg . time . completed ) return
msg . error ? ? = MessageV2 . fromError ( new DOMException ( "Aborted" , "AbortError" ) , {
providerID : msg.providerID ,
aborted : true ,
} )
msg . time . completed = Date . now ( )
yield * sessions . updateMessage ( msg )
2026-04-17 00:49:39 +00:00
} )
2026-03-30 20:06:51 +00:00
2026-05-14 06:19:11 +00:00
const handle = yield * processor
. create ( {
assistantMessage : msg ,
sessionID ,
model ,
} )
. pipe ( Effect . onInterrupt ( ( ) = > finalizeInterruptedAssistant ) )
2026-04-17 00:49:39 +00:00
const outcome : "break" | "continue" = yield * Effect . gen ( function * ( ) {
const lastUserMsg = msgs . findLast ( ( m ) = > m . info . role === "user" )
const bypassAgentCheck = lastUserMsg ? . parts . some ( ( p ) = > p . type === "agent" ) ? ? false
2026-05-18 19:50:31 +00:00
const promptOps = yield * ops ( )
2026-03-30 20:06:51 +00:00
2026-05-18 19:50:31 +00:00
const tools = yield * SessionTools . resolve ( {
2026-04-17 00:49:39 +00:00
agent ,
session ,
2026-03-30 20:06:51 +00:00
model ,
2026-04-17 00:49:39 +00:00
processor : handle ,
bypassAgentCheck ,
messages : msgs ,
2026-05-18 19:50:31 +00:00
promptOps ,
} ) . pipe (
Effect . provideService ( Plugin . Service , plugin ) ,
Effect . provideService ( Permission . Service , permission ) ,
Effect . provideService ( ToolRegistry . Service , registry ) ,
Effect . provideService ( MCP . Service , mcp ) ,
Effect . provideService ( Truncate . Service , truncate ) ,
)
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
if ( lastUser . format ? . type === "json_schema" ) {
tools [ "StructuredOutput" ] = createStructuredOutputTool ( {
schema : lastUser.format.schema ,
onSuccess ( output ) {
structured = output
} ,
2026-04-09 01:19:01 +00:00
} )
2026-04-17 00:49:39 +00:00
}
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
if ( step === 1 )
2026-04-17 00:50:44 +00:00
yield * summary . summarize ( { sessionID , messageID : lastUser.id } ) . pipe ( Effect . ignore , Effect . forkIn ( scope ) )
2026-04-17 00:49:39 +00:00
if ( step > 1 && lastFinished ) {
for ( const m of msgs ) {
if ( m . info . role !== "user" || m . info . id <= lastFinished . id ) continue
for ( const p of m . parts ) {
if ( p . type !== "text" || p . ignored || p . synthetic ) continue
if ( ! p . text . trim ( ) ) continue
p . text = [
"<system-reminder>" ,
"The user sent the following message:" ,
p . text ,
"" ,
"Please address this message and continue with your tasks." ,
"</system-reminder>" ,
] . join ( "\n" )
2026-03-30 20:06:51 +00:00
}
2026-04-09 01:19:01 +00:00
}
2026-04-17 00:49:39 +00:00
}
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
yield * plugin . trigger ( "experimental.chat.messages.transform" , { } , { messages : msgs } )
const [ skills , env , instructions , modelMsgs ] = yield * Effect . all ( [
sys . skills ( agent ) ,
2026-05-01 01:45:48 +00:00
sys . environment ( model ) ,
2026-04-17 00:49:39 +00:00
instruction . system ( ) . pipe ( Effect . orDie ) ,
MessageV2 . toModelMessagesEffect ( msgs , model ) ,
] )
2026-04-29 15:55:53 +00:00
const system = [ . . . env , . . . instructions , . . . ( skills ? [ skills ] : [ ] ) ]
2026-04-17 00:49:39 +00:00
const format = lastUser . format ? ? { type : "text" as const }
if ( format . type === "json_schema" ) system . push ( STRUCTURED_OUTPUT_SYSTEM_PROMPT )
const result = yield * handle . process ( {
user : lastUser ,
agent ,
permission : session.permission ,
sessionID ,
parentSessionID : session.parentID ,
system ,
messages : [ . . . modelMsgs , . . . ( isLastStep ? [ { role : "assistant" as const , content : MAX_STEPS } ] : [ ] ) ] ,
tools ,
model ,
toolChoice : format.type === "json_schema" ? "required" : undefined ,
} )
2026-04-09 01:19:01 +00:00
2026-04-17 00:49:39 +00:00
if ( structured !== undefined ) {
handle . message . structured = structured
handle . message . finish = handle . message . finish ? ? "stop"
yield * sessions . updateMessage ( handle . message )
return "break" as const
}
const finished = handle . message . finish && ! [ "tool-calls" , "unknown" ] . includes ( handle . message . finish )
if ( finished && ! handle . message . error ) {
if ( format . type === "json_schema" ) {
handle . message . error = new MessageV2 . StructuredOutputError ( {
message : "Model did not produce structured output" ,
retries : 0 ,
} ) . toObject ( )
2026-04-09 01:19:01 +00:00
yield * sessions . updateMessage ( handle . message )
return "break" as const
}
2026-04-17 00:49:39 +00:00
}
2026-03-30 20:06:51 +00:00
2026-04-17 00:49:39 +00:00
if ( result === "stop" ) return "break" as const
if ( result === "compact" ) {
yield * compaction . create ( {
sessionID ,
agent : lastUser.agent ,
model : lastUser.model ,
auto : true ,
overflow : ! handle . message . finish ,
} )
}
return "continue" as const
2026-05-14 06:19:11 +00:00
} ) . pipe (
Effect . ensuring ( instruction . clear ( handle . message . id ) ) ,
Effect . onInterrupt ( ( ) = > finalizeInterruptedAssistant ) ,
)
2026-04-17 00:49:39 +00:00
if ( outcome === "break" ) break
continue
2026-03-30 20:06:51 +00:00
}
2026-01-13 20:55:48 +00:00
2026-04-17 00:49:39 +00:00
yield * compaction . prune ( { sessionID } ) . pipe ( Effect . ignore , Effect . forkIn ( scope ) )
return yield * lastAssistant ( sessionID )
} ,
)
2026-01-13 20:55:48 +00:00
2026-04-23 15:30:02 +00:00
const loop : ( input : LoopInput ) = > Effect . Effect < MessageV2.WithParts > = Effect . fn ( "SessionPrompt.loop" ) ( function * (
input : LoopInput ,
) {
2026-04-17 00:49:39 +00:00
return yield * state . ensureRunning ( input . sessionID , lastAssistant ( input . sessionID ) , runLoop ( input . sessionID ) )
} )
2026-01-13 20:55:48 +00:00
2026-05-14 01:28:04 +00:00
const shell : ( input : ShellInput ) = > Effect . Effect < MessageV2.WithParts , Session.BusyError > = Effect . fn (
"SessionPrompt.shell" ,
) ( function * ( input : ShellInput ) {
const ready = yield * Latch . make ( )
return yield * state . startShell ( input . sessionID , lastAssistant ( input . sessionID ) , shellImpl ( input , ready ) , ready )
} )
2026-04-17 00:49:39 +00:00
const command = Effect . fn ( "SessionPrompt.command" ) ( function * ( input : CommandInput ) {
yield * elog . info ( "command" , { sessionID : input.sessionID , command : input.command , agent : input.agent } )
const cmd = yield * commands . get ( input . command )
if ( ! cmd ) {
const available = ( yield * commands . list ( ) ) . map ( ( c ) = > c . name )
const hint = available . length ? ` Available commands: ${ available . join ( ", " ) } ` : ""
const error = new NamedError . Unknown ( { message : ` Command not found: " ${ input . command } ". ${ hint } ` } )
yield * bus . publish ( Session . Event . Error , { sessionID : input.sessionID , error : error.toObject ( ) } )
throw error
}
2026-05-12 19:38:30 +00:00
const agentName = cmd . agent ? ? input . agent
2026-04-17 00:49:39 +00:00
const raw = input . arguments . match ( argsRegex ) ? ? [ ]
const args = raw . map ( ( arg ) = > arg . replace ( quoteTrimRegex , "" ) )
const templateCommand = yield * Effect . promise ( async ( ) = > cmd . template )
const placeholders = templateCommand . match ( placeholderRegex ) ? ? [ ]
let last = 0
for ( const item of placeholders ) {
const value = Number ( item . slice ( 1 ) )
if ( value > last ) last = value
}
const withArgs = templateCommand . replaceAll ( placeholderRegex , ( _ , index ) = > {
const position = Number ( index )
const argIndex = position - 1
if ( argIndex >= args . length ) return ""
if ( position === last ) return args . slice ( argIndex ) . join ( " " )
return args [ argIndex ]
} )
const usesArgumentsPlaceholder = templateCommand . includes ( "$ARGUMENTS" )
let template = withArgs . replaceAll ( "$ARGUMENTS" , input . arguments )
2026-01-13 20:55:48 +00:00
2026-04-17 00:49:39 +00:00
if ( placeholders . length === 0 && ! usesArgumentsPlaceholder && input . arguments . trim ( ) ) {
template = template + "\n\n" + input . arguments
}
2026-01-13 20:55:48 +00:00
2026-04-17 00:49:39 +00:00
const shellMatches = ConfigMarkdown . shell ( template )
if ( shellMatches . length > 0 ) {
2026-04-27 00:54:55 +00:00
const cfg = yield * config . get ( )
const sh = Shell . preferred ( cfg . shell )
2026-04-17 00:49:39 +00:00
const results = yield * Effect . promise ( ( ) = >
Promise . all (
shellMatches . map ( async ( [ , cmd ] ) = > ( await Process . text ( [ cmd ] , { shell : sh , nothrow : true } ) ) . text ) ,
) ,
)
let index = 0
template = template . replace ( bashRegex , ( ) = > results [ index ++ ] )
}
template = template . trim ( )
const taskModel = yield * Effect . gen ( function * ( ) {
if ( cmd . model ) return Provider . parseModel ( cmd . model )
if ( cmd . agent ) {
const cmdAgent = yield * agents . get ( cmd . agent )
if ( cmdAgent ? . model ) return cmdAgent . model
2026-03-30 20:06:51 +00:00
}
2026-04-17 00:49:39 +00:00
if ( input . model ) return Provider . parseModel ( input . model )
2026-05-10 22:53:14 +00:00
return yield * currentModel ( input . sessionID )
2026-04-17 00:49:39 +00:00
} )
2026-01-13 20:55:48 +00:00
2026-04-17 00:49:39 +00:00
yield * getModel ( taskModel . providerID , taskModel . modelID , input . sessionID )
2026-05-12 19:38:30 +00:00
const agent = agentName ? yield * agents . get ( agentName ) : yield * agents . defaultInfo ( )
2026-04-17 00:49:39 +00:00
if ( ! agent ) {
const available = ( yield * agents . list ( ) ) . 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 } ` } )
yield * bus . publish ( Session . Event . Error , { sessionID : input.sessionID , error : error.toObject ( ) } )
throw error
}
const templateParts = yield * resolvePromptParts ( template )
const isSubtask = ( agent . mode === "subagent" && cmd . subtask !== false ) || cmd . subtask === true
const parts = isSubtask
? [
{
type : "subtask" as const ,
agent : agent.name ,
description : cmd.description ? ? "" ,
command : input.command ,
model : { providerID : taskModel.providerID , modelID : taskModel.modelID } ,
prompt : templateParts.find ( ( y ) = > y . type === "text" ) ? . text ? ? "" ,
} ,
]
: [ . . . templateParts , . . . ( input . parts ? ? [ ] ) ]
2026-05-12 19:38:30 +00:00
const userAgent = isSubtask ? ( input . agent ? ? ( yield * agents . defaultInfo ( ) ) . name ) : agent . name
2026-04-17 00:49:39 +00:00
const userModel = isSubtask
? input . model
? Provider . parseModel ( input . model )
2026-05-10 22:53:14 +00:00
: yield * currentModel ( input . sessionID )
2026-04-17 00:49:39 +00:00
: taskModel
yield * plugin . trigger (
"command.execute.before" ,
{ command : input.command , sessionID : input.sessionID , arguments : input.arguments } ,
{ parts } ,
)
2026-01-13 20:55:48 +00:00
2026-04-17 00:49:39 +00:00
const result = yield * prompt ( {
sessionID : input.sessionID ,
messageID : input.messageID ,
model : userModel ,
agent : userAgent ,
parts ,
variant : input.variant ,
2026-03-30 20:06:51 +00:00
} )
2026-04-17 00:49:39 +00:00
yield * bus . publish ( Command . Event . Executed , {
name : input.command ,
sessionID : input.sessionID ,
arguments : input.arguments ,
messageID : result.info.id ,
2025-09-13 09:46:14 +00:00
} )
2026-04-17 00:49:39 +00:00
return result
} )
return Service . of ( {
cancel ,
prompt ,
loop ,
shell ,
command ,
resolvePromptParts ,
} )
} ) ,
)
export const defaultLayer = Layer . suspend ( ( ) = >
layer . pipe (
Layer . provide ( SessionRunState . defaultLayer ) ,
Layer . provide ( SessionStatus . defaultLayer ) ,
Layer . provide ( SessionCompaction . defaultLayer ) ,
Layer . provide ( SessionProcessor . defaultLayer ) ,
Layer . provide ( Command . defaultLayer ) ,
Layer . provide ( Permission . defaultLayer ) ,
Layer . provide ( MCP . defaultLayer ) ,
Layer . provide ( LSP . defaultLayer ) ,
Layer . provide ( ToolRegistry . defaultLayer ) ,
Layer . provide ( Truncate . defaultLayer ) ,
Layer . provide ( Provider . defaultLayer ) ,
2026-04-27 00:54:55 +00:00
Layer . provide ( Config . defaultLayer ) ,
2026-04-17 00:49:39 +00:00
Layer . provide ( Instruction . defaultLayer ) ,
Layer . provide ( AppFileSystem . defaultLayer ) ,
Layer . provide ( Plugin . defaultLayer ) ,
Layer . provide ( Session . defaultLayer ) ,
Layer . provide ( SessionRevert . defaultLayer ) ,
Layer . provide ( SessionSummary . defaultLayer ) ,
2026-05-14 04:06:07 +00:00
Layer . provide ( Image . defaultLayer ) ,
2026-04-17 00:49:39 +00:00
Layer . provide (
Layer . mergeAll (
2026-05-15 00:50:23 +00:00
EventV2Bridge . defaultLayer ,
2026-04-17 00:49:39 +00:00
Agent . defaultLayer ,
SystemPrompt . defaultLayer ,
LLM . defaultLayer ,
2026-05-11 07:27:26 +00:00
Reference . defaultLayer ,
2026-04-17 00:49:39 +00:00
Bus . layer ,
CrossSpawnSpawner . defaultLayer ,
2026-05-13 11:47:04 +00:00
RuntimeFlags . defaultLayer ,
2026-04-11 16:52:35 +00:00
) ,
2026-03-30 20:06:51 +00:00
) ,
2026-04-17 00:49:39 +00:00
) ,
)
2026-04-23 15:30:02 +00:00
const ModelRef = Schema . Struct ( {
providerID : ProviderID ,
modelID : ModelID ,
2026-04-17 00:49:39 +00:00
} )
2026-04-23 15:30:02 +00:00
export const PromptInput = Schema . Struct ( {
sessionID : SessionID ,
messageID : Schema.optional ( MessageID ) ,
model : Schema.optional ( ModelRef ) ,
agent : Schema.optional ( Schema . String ) ,
noReply : Schema.optional ( Schema . Boolean ) ,
tools : Schema.optional ( Schema . Record ( Schema . String , Schema . Boolean ) ) . annotate ( {
description :
"@deprecated tools and permissions have been merged, you can set permissions on the session itself now" ,
} ) ,
format : Schema.optional ( MessageV2 . Format ) ,
system : Schema.optional ( Schema . String ) ,
variant : Schema.optional ( Schema . String ) ,
parts : Schema.Array (
Schema . Union ( [
MessageV2 . TextPartInput ,
MessageV2 . FilePartInput ,
MessageV2 . AgentPartInput ,
MessageV2 . SubtaskPartInput ,
] ) . annotate ( { discriminator : "type" } ) ,
) ,
2026-05-11 20:49:08 +00:00
} )
2026-04-30 23:28:46 +00:00
export type PromptInput = Schema . Schema . Type < typeof PromptInput >
2026-04-17 00:49:39 +00:00
2026-04-23 15:30:02 +00:00
export class LoopInput extends Schema . Class < LoopInput > ( "SessionPrompt.LoopInput" ) ( {
sessionID : SessionID ,
2026-05-11 20:49:08 +00:00
} ) { }
2026-04-17 00:49:39 +00:00
2026-04-23 15:30:02 +00:00
export const ShellInput = Schema . Struct ( {
sessionID : SessionID ,
messageID : Schema.optional ( MessageID ) ,
agent : Schema.String ,
model : Schema.optional ( ModelRef ) ,
command : Schema.String ,
2026-05-11 20:49:08 +00:00
} )
2026-04-23 15:30:02 +00:00
export type ShellInput = Schema . Schema . Type < typeof ShellInput >
export const CommandInput = Schema . Struct ( {
messageID : Schema.optional ( MessageID ) ,
sessionID : SessionID ,
agent : Schema.optional ( Schema . String ) ,
model : Schema.optional ( Schema . String ) ,
arguments : Schema.String ,
command : Schema.String ,
variant : Schema.optional ( Schema . String ) ,
// Inlined (no identifier annotation) to keep the original SDK output — the
2026-04-22 03:17:23 +00:00
// PromptInput call site below references FilePartInput by ref via the
// Schema export in message-v2.ts.
2026-04-23 15:30:02 +00:00
parts : Schema.optional (
Schema . Array (
Schema . Union ( [
Schema . Struct ( {
id : Schema.optional ( PartID ) ,
type : Schema . Literal ( "file" ) ,
mime : Schema.String ,
filename : Schema.optional ( Schema . String ) ,
url : Schema.String ,
source : Schema.optional ( MessageV2 . FilePartSource ) ,
2026-04-17 00:49:39 +00:00
} ) ,
2026-04-23 15:30:02 +00:00
] ) . annotate ( { discriminator : "type" } ) ,
) ,
) ,
2026-05-11 20:49:08 +00:00
} )
2026-04-23 15:30:02 +00:00
export type CommandInput = Schema . Schema . Type < typeof CommandInput >
2026-04-17 00:49:39 +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 ( {
description : STRUCTURED_OUTPUT_DESCRIPTION ,
inputSchema : jsonSchema ( toolSchema as JSONSchema7 ) ,
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 ( { output } ) {
return {
type : "text" ,
value : output.output ,
}
} ,
2025-09-13 09:46:14 +00:00
} )
}
2026-04-17 00:49:39 +00:00
const bashRegex = /!`([^`]+)`/g
// Match [Image N] as single token, quoted strings, or non-space sequences
const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi
const placeholderRegex = /\$(\d+)/g
const quoteTrimRegex = /^["']|["']$/g
export * as SessionPrompt from "./prompt"