2026-06-09 19:31:31 +00:00
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
2026-06-03 02:42:13 +00:00
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
2025-09-13 09:46:14 +00:00
import path from "path"
2026-06-03 02:42:13 +00:00
import { SessionV1 } from "@opencode-ai/core/v1/session"
2025-09-13 09:46:14 +00:00
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"
import { SessionRevert } from "./revert"
2026-06-01 05:00:40 +00:00
import { 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-05-31 01:08:38 +00:00
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 { 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"
2026-06-20 21:04:30 +00:00
import { MAX_STEPS_PROMPT } from "@opencode-ai/core/session/runner/max-steps"
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"
2026-06-14 10:46:39 +00:00
import { Shell } from "@opencode-ai/core/shell"
2026-05-02 23:18:48 +00:00
import { ShellID } from "@/tool/shell/id"
2026-06-02 20:09:26 +00:00
import { FSUtil } from "@opencode-ai/core/fs-util"
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 { 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 { EventV2Bridge } from "@/event-v2-bridge"
2026-05-31 01:08:38 +00:00
import { Database } from "@opencode-ai/core/database/database"
2026-05-13 14:43:08 +00:00
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
2026-05-31 01:08:38 +00:00
import { eq } from "drizzle-orm"
import { SessionTable } from "@opencode-ai/core/session/sql"
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-06-03 02:42:13 +00:00
const decodeMessageInfo = Schema . decodeUnknownExit ( SessionV1 . Info )
const decodeMessagePart = Schema . decodeUnknownExit ( SessionV1 . Part )
2026-06-23 08:19:42 +00:00
const MAX_MCP_RESOURCE_BLOB_BYTES = 10 * 1024 * 1024
const SUPPORTED_MCP_RESOURCE_ATTACHMENT_MIMES = new Set ( [
"application/pdf" ,
"image/gif" ,
"image/jpeg" ,
"image/png" ,
"image/webp" ,
] )
2026-05-11 02:59:20 +00:00
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-06-23 08:19:42 +00:00
function mcpResourceBase64Size ( value : string ) {
const trimmed = value . replace ( /\s/g , "" )
const padding = trimmed . endsWith ( "==" ) ? 2 : trimmed.endsWith ( "=" ) ? 1 : 0
return Math . max ( 0 , Math . floor ( ( trimmed . length * 3 ) / 4 ) - padding )
}
function formatMcpResourceBytes ( value : number ) {
if ( value < 1024 ) return ` ${ value } B `
if ( value < 1024 * 1024 ) return ` ${ Math . ceil ( value / 1024 ) } KB `
return ` ${ Math . ceil ( value / ( 1024 * 1024 ) ) } MB `
}
2026-06-03 02:42:13 +00:00
function isOrphanedInterruptedTool ( part : SessionV1.ToolPart ) {
2026-05-25 05:47:55 +00:00
// cleanup() marks abandoned tool_use blocks this way after retries/aborts.
// They are not pending work and must not trigger an assistant-prefill request.
return part . state . status === "error" && part . state . metadata ? . interrupted === true
}
2026-04-17 00:49:39 +00:00
export interface Interface {
readonly cancel : ( sessionID : SessionID ) = > Effect . Effect < void >
2026-06-03 02:42:13 +00:00
readonly prompt : ( input : PromptInput ) = > Effect . Effect < SessionV1.WithParts , Image.Error >
readonly loop : ( input : LoopInput ) = > Effect . Effect < SessionV1.WithParts >
readonly shell : ( input : ShellInput ) = > Effect . Effect < SessionV1.WithParts , Session.BusyError >
readonly command : ( input : CommandInput ) = > Effect . Effect < SessionV1.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" ) { }
2026-06-30 03:03:57 +00:00
const layer = Layer . effect (
2026-04-17 00:49:39 +00:00
Service ,
Effect . gen ( function * ( ) {
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
2026-06-02 20:09:26 +00:00
const fsys = yield * FSUtil . Service
2026-04-17 00:49:39 +00:00
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-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-05-31 01:08:38 +00:00
const database = yield * Database . Service
const { db } = database
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-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 ) {
2026-06-08 19:41:56 +00:00
yield * Effect . logInfo ( "cancel" , { "session.id" : sessionID } )
2026-04-17 00:49:39 +00:00
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-06-09 16:12:42 +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 > ( )
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
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
2026-06-03 02:42:13 +00:00
history : SessionV1.WithParts [ ]
2026-05-31 01:08:38 +00:00
providerID : ProviderV2.ID
2026-06-04 06:57:43 +00:00
modelID : ModelV2.ID
2026-04-17 00:49:39 +00:00
} ) {
if ( input . session . parentID ) return
if ( ! Session . isDefaultTitle ( input . session . title ) ) return
2026-06-03 02:42:13 +00:00
const real = ( m : SessionV1.WithParts ) = >
2026-04-17 00:49:39 +00:00
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
2026-06-03 02:42:13 +00:00
const subtasks = firstUser . parts . filter ( ( p ) : p is SessionV1 . SubtaskPart = > p . type === "subtask" )
2026-04-17 00:49:39 +00:00
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 } )
2026-06-08 19:41:56 +00:00
. pipe ( Effect . catchCause ( ( cause ) = > Effect . logError ( "failed to generate title" , { error : Cause.squash ( cause ) } ) ) )
2026-04-17 00:49:39 +00:00
} )
2025-11-12 01:38:50 +00:00
2026-04-17 00:49:39 +00:00
const handleSubtask = Effect . fn ( "SessionPrompt.handleSubtask" ) ( function * ( input : {
2026-06-03 02:42:13 +00:00
task : SessionV1.SubtaskPart
2026-04-17 00:49:39 +00:00
model : Provider.Model
2026-06-03 02:42:13 +00:00
lastUser : SessionV1.User
2026-04-17 00:49:39 +00:00
sessionID : SessionID
session : Session.Info
2026-06-03 02:42:13 +00:00
msgs : SessionV1.WithParts [ ]
2026-04-17 00:49:39 +00:00
} ) {
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
2026-06-03 02:42:13 +00:00
const assistantMessage : SessionV1.Assistant = yield * sessions . updateMessage ( {
2026-04-17 00:49:39 +00:00
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 ( ) } ,
} )
2026-06-03 02:42:13 +00:00
let part : SessionV1.ToolPart = yield * sessions . updatePart ( {
2026-04-17 00:49:39 +00:00
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 } ` } )
2026-05-31 01:08:38 +00:00
yield * events . publish ( Session . Event . Error , { sessionID , error : error.toObject ( ) } )
2026-04-17 00:49:39 +00:00
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 } ,
2026-06-03 02:42:13 +00:00
} satisfies SessionV1 . ToolPart )
2026-04-17 00:49:39 +00:00
} ) ,
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 ) )
2026-06-08 19:41:56 +00:00
return Effect . logError ( "subtask execution failed" , {
error ,
agent : task.agent ,
description : task.description ,
} )
2026-04-17 00:49:39 +00:00
} ) ,
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-06-03 02:42:13 +00:00
} satisfies SessionV1 . 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-06-03 02:42:13 +00:00
} satisfies SessionV1 . 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
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-06-03 02:42:13 +00:00
} satisfies SessionV1 . ToolPart )
2026-04-17 00:49:39 +00:00
}
if ( ! task . command ) return
2026-06-03 02:42:13 +00:00
const summaryUserMsg : SessionV1.User = {
2026-04-17 00:49:39 +00:00
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 ,
2026-06-03 02:42:13 +00:00
} satisfies SessionV1 . TextPart )
2026-04-17 00:49:39 +00:00
} )
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 } ` } )
2026-05-31 01:08:38 +00:00
yield * events . publish ( Session . Event . Error , { sessionID : input.sessionID , error : error.toObject ( ) } )
2026-04-27 20:47:18 +00:00
throw error
}
2026-05-10 22:53:14 +00:00
const model = input . model ? ? agent . model ? ? ( yield * currentModel ( input . sessionID ) )
2026-06-03 02:42:13 +00:00
const userMsg : SessionV1.User = {
2026-04-27 20:47:18 +00:00
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 )
2026-06-03 02:42:13 +00:00
const userPart : SessionV1.Part = {
2026-04-27 20:47:18 +00:00
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 )
2026-06-03 02:42:13 +00:00
const msg : SessionV1.Assistant = {
2026-04-27 20:47:18 +00:00
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-06-03 02:42:13 +00:00
const part : SessionV1.ToolPart = {
2026-04-27 20:47:18 +00:00
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-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-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 : "" ,
2026-06-23 02:18:06 +00:00
metadata : { output } ,
2026-04-27 20:47:18 +00:00
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" ) {
2026-06-23 02:18:06 +00:00
part . state . metadata = { output }
2026-04-27 20:47:18 +00:00
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 * (
2026-05-31 01:08:38 +00:00
providerID : ProviderV2.ID ,
2026-06-04 06:57:43 +00:00
modelID : ModelV2.ID ,
2026-04-17 00:49:39 +00:00
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-05-31 01:08:38 +00:00
yield * events . publish ( Session . Event . Error , {
2026-04-17 00:49:39 +00:00
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 ) {
2026-05-31 01:08:38 +00:00
const current = yield * db
. select ( { model : SessionTable.model } )
. from ( SessionTable )
. where ( eq ( SessionTable . id , sessionID ) )
. get ( )
. pipe ( Effect . orDie )
2026-05-10 22:53:14 +00:00
if ( current ? . model ) {
return {
2026-05-31 01:08:38 +00:00
providerID : ProviderV2.ID.make ( current . model . providerID ) ,
2026-06-04 06:57:43 +00:00
modelID : ModelV2.ID.make ( current . model . id ) ,
2026-05-10 22:53:14 +00:00
. . . ( 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
2026-05-22 18:23:23 +00:00
return yield * provider . defaultModel ( ) . pipe ( Effect . orDie )
2026-04-17 00:49:39 +00:00
} )
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 } ` } )
2026-05-31 01:08:38 +00:00
yield * events . publish ( Session . Event . Error , { sessionID : input.sessionID , error : error.toObject ( ) } )
2026-04-17 00:49:39 +00:00
throw error
}
2026-05-10 22:53:14 +00:00
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 )
2026-06-03 02:42:13 +00:00
const info : SessionV1.User = {
2026-04-17 00:49:39 +00:00
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-06-26 02:42:38 +00:00
const current = yield * sessions . get ( input . sessionID ) . pipe ( Effect . orDie )
2026-05-03 02:09:48 +00:00
if (
2026-06-26 02:42:38 +00:00
current . agent !== info . agent ||
current . model ? . providerID !== info . model . providerID ||
current . model ? . id !== info . model . modelID ||
( current . model ? . variant === "default" ? undefined : current . model ? . variant ) !== info . model . variant
2026-05-03 02:09:48 +00:00
) {
2026-06-26 02:42:38 +00:00
yield * sessions . setAgentModel ( {
2026-05-11 01:20:13 +00:00
sessionID : input.sessionID ,
2026-06-26 02:42:38 +00:00
agent : info.agent ,
2026-05-11 01:20:13 +00:00
model : {
2026-06-26 02:42:38 +00:00
id : info.model.modelID ,
providerID : info.model.providerID ,
variant : info.model.variant ? ? "default" ,
2026-05-05 02:35:21 +00:00
} ,
2026-06-26 02:42:38 +00:00
time : info.time.created ,
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-06-03 02:42:13 +00:00
type Draft < T > = T extends SessionV1 . Part ? Omit < T , " id " > & { id? : string } : never
const assign = ( part : Draft < SessionV1.Part > ) : SessionV1 . Part = > ( {
2026-04-17 00:49:39 +00:00
. . . part ,
id : part.id ? PartID . make ( part . id ) : PartID . ascending ( ) ,
} )
2026-06-03 02:42:13 +00:00
const resolvePart : ( part : PromptInput [ "parts" ] [ number ] ) = > Effect . Effect < Draft < SessionV1.Part > [ ] > = Effect . fn (
2026-04-17 00:49:39 +00:00
"SessionPrompt.resolveUserPart" ,
) ( function * ( part ) {
if ( part . type === "file" ) {
if ( part . source ? . type === "resource" ) {
const { clientName , uri } = part . source
2026-06-08 19:41:56 +00:00
yield * Effect . logInfo ( "mcp resource" , { clientName , uri , mime : part.mime } )
2026-06-03 02:42:13 +00:00
const pieces : Draft < SessionV1.Part > [ ] = [
2026-04-17 00:49:39 +00:00
{
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 ) {
2026-06-23 08:19:42 +00:00
if ( ! c || typeof c !== "object" ) continue
if ( "text" in c && typeof c . text === "string" && c . text ) {
2026-04-17 00:49:39 +00:00
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : c.text ,
} )
2026-06-23 08:19:42 +00:00
} else if ( "blob" in c && typeof c . blob === "string" && c . blob ) {
const mime = "mimeType" in c && typeof c . mimeType === "string" ? c.mimeType : part.mime
const filename = "uri" in c && typeof c . uri === "string" ? c.uri : part.filename
const size = mcpResourceBase64Size ( c . blob )
if ( ! SUPPORTED_MCP_RESOURCE_ATTACHMENT_MIMES . has ( mime ) ) {
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : ` [Binary MCP resource omitted: ${ filename ? ? uri } ( ${ mime } , ${ formatMcpResourceBytes ( size ) } ) is not a supported attachment type] ` ,
} )
continue
}
if ( size > MAX_MCP_RESOURCE_BLOB_BYTES ) {
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
text : ` [Binary MCP resource omitted: ${ filename ? ? uri } ( ${ mime } , ${ formatMcpResourceBytes ( size ) } ) exceeds ${ formatMcpResourceBytes ( MAX_MCP_RESOURCE_BLOB_BYTES ) } ] ` ,
} )
continue
}
2026-04-17 00:49:39 +00:00
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "text" ,
synthetic : true ,
2026-06-23 08:19:42 +00:00
text : ` [Binary MCP resource attached: ${ filename ? ? uri } ( ${ mime } )] ` ,
} )
pieces . push ( {
messageID : info.id ,
sessionID : input.sessionID ,
type : "file" ,
mime ,
filename ,
url : ` data: ${ mime } ;base64, ${ c . blob } ` ,
2026-04-17 00:49:39 +00:00
} )
2026-01-04 15:12:54 +00:00
}
}
2026-04-17 00:49:39 +00:00
} else {
const error = Cause . squash ( exit . cause )
2026-06-08 19:41:56 +00:00
yield * Effect . logError ( "failed to read MCP resource" , { error , clientName , uri } )
2026-04-17 00:49:39 +00:00
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:" : {
2026-06-08 19:41:56 +00:00
yield * Effect . logInfo ( "file" , { mime : part.mime } )
2026-04-17 00:49:39 +00:00
const filepath = fileURLToPath ( part . url )
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 }
2026-06-03 02:42:13 +00:00
const pieces : Draft < SessionV1.Part > [ ] = [
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 )
2026-06-08 19:41:56 +00:00
yield * Effect . logError ( "failed to read file" , { error , filepath } )
2026-04-17 00:49:39 +00:00
const message = error instanceof Error ? error.message : String ( error )
2026-05-31 01:08:38 +00:00
yield * events . publish ( Session . Event . Error , {
2026-04-17 00:49:39 +00:00
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 )
2026-06-08 19:41:56 +00:00
yield * Effect . logError ( "failed to read directory" , { error , filepath } )
2026-04-17 00:49:39 +00:00
const message = error instanceof Error ? error.message : String ( error )
2026-05-31 01:08:38 +00:00
yield * events . publish ( Session . Event . Error , {
2026-04-17 00:49:39 +00:00
sessionID : input.sessionID ,
error : new NamedError . Unknown ( { message } ) . toObject ( ) ,
} )
2026-03-30 20:06:51 +00:00
return [
{
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 [
{
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 [
{
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-06-09 16:08:58 +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-06-08 19:41:56 +00:00
yield * Effect . logError ( "invalid user message before save" , {
2026-04-17 00:49:39 +00:00
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
} )
}
2026-06-08 19:41:56 +00:00
for ( const [ index , part ] of parts . entries ( ) ) {
2026-05-11 02:59:20 +00:00
const p = decodeMessagePart ( part , { errors : "all" , propertyOrder : "original" } )
2026-06-08 19:41:56 +00:00
if ( Exit . isSuccess ( p ) ) continue
yield * Effect . logError ( "invalid user part before save" , {
2026-04-17 00:49:39 +00:00
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-06-08 19:41:56 +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 )
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-06-03 02:42:13 +00:00
const prompt : ( input : PromptInput ) = > Effect . Effect < SessionV1.WithParts , Image.Error > = Effect . fn (
2026-05-14 04:06:07 +00:00
"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 )
2026-06-03 02:42:13 +00:00
const permissions : PermissionV1.Rule [ ] = [ ]
2026-05-14 04:06:07 +00:00
for ( const [ t , enabled ] of Object . entries ( input . tools ? ? { } ) ) {
permissions . push ( { permission : t , action : enabled ? "allow" : "deny" , pattern : "*" } )
}
if ( permissions . length > 0 ) {
2026-06-08 17:33:27 +00:00
session . permission = permissions
yield * sessions . setPermission ( { sessionID : session.id , permission : permissions } )
2026-05-14 04:06:07 +00:00
}
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" )
} )
2026-06-03 02:42:13 +00:00
const runLoop : ( sessionID : SessionID ) = > Effect . Effect < SessionV1.WithParts > = Effect . fn ( "SessionPrompt.run" ) (
2026-04-17 00:49:39 +00:00
function * ( sessionID : SessionID ) {
const ctx = yield * InstanceState . context
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" } )
2026-06-08 19:41:56 +00:00
yield * Effect . logInfo ( "loop" , { "session.id" : sessionID , step } )
2026-04-17 00:49:39 +00:00
2026-05-31 01:08:38 +00:00
let msgs = yield * MessageV2 . filterCompactedEffect ( sessionID ) . pipe (
Effect . provideService ( Database . Service , database ) ,
)
2026-04-17 00:49:39 +00:00
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 ,
)
2026-05-25 05:47:55 +00:00
// 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, but ignore cleanup-marked interrupted orphans.
2026-04-17 00:49:39 +00:00
const hasToolCalls =
2026-05-25 05:47:55 +00:00
lastAssistantMsg ? . parts . some (
( part ) = > part . type === "tool" && ! part . metadata ? . providerExecuted && ! isOrphanedInterruptedTool ( part ) ,
) ? ? false
2026-04-17 00:49:39 +00:00
if (
lastAssistant ? . finish &&
! [ "tool-calls" ] . includes ( lastAssistant . finish ) &&
! hasToolCalls &&
lastUser . id < lastAssistant . id
) {
2026-05-25 05:47:55 +00:00
const orphan = lastAssistantMsg ? . parts . find (
2026-06-03 02:42:13 +00:00
( part ) : part is SessionV1 . ToolPart = > part . type === "tool" && isOrphanedInterruptedTool ( part ) ,
2026-05-25 05:47:55 +00:00
)
if ( orphan ) {
2026-06-08 19:41:56 +00:00
yield * Effect . logWarning ( "loop exit with orphaned interrupted tool" , {
"session.id" : sessionID ,
2026-05-25 05:47:55 +00:00
messageID : lastAssistant.id ,
tool : orphan.tool ,
callID : orphan.callID ,
} )
}
2026-06-08 19:41:56 +00:00
yield * Effect . logInfo ( "exiting loop" , { "session.id" : sessionID } )
2026-04-17 00:49:39 +00:00
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 } ` } )
2026-05-31 01:08:38 +00:00
yield * events . publish ( Session . Event . Error , { sessionID , error : error.toObject ( ) } )
2026-04-17 00:49:39 +00:00
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 ) ,
2026-06-02 20:09:26 +00:00
Effect . provideService ( FSUtil . Service , fsys ) ,
2026-05-18 16:36:38 +00:00
Effect . provideService ( Session . Service , sessions ) ,
)
2026-04-17 00:49:39 +00:00
2026-06-03 02:42:13 +00:00
const msg : SessionV1.Assistant = {
2026-04-17 00:49:39 +00:00
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
yield * plugin . trigger ( "experimental.chat.messages.transform" , { } , { messages : msgs } )
2026-06-24 16:54:19 +00:00
const [ skills , env , instructions , mcpInstructions , modelMsgs ] = yield * Effect . all ( [
2026-04-17 00:49:39 +00:00
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 ) ,
2026-06-24 16:54:19 +00:00
sys . mcp ( agent , session . permission ) ,
2026-04-17 00:49:39 +00:00
MessageV2 . toModelMessagesEffect ( msgs , model ) ,
] )
2026-06-24 16:54:19 +00:00
const system = [
. . . env ,
. . . instructions ,
. . . ( mcpInstructions ? [ mcpInstructions ] : [ ] ) ,
. . . ( 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 ,
2026-06-20 21:05:57 +00:00
messages : [
. . . modelMsgs ,
. . . ( isLastStep ? [ { role : "assistant" as const , content : MAX_STEPS_PROMPT } ] : [ ] ) ,
] ,
2026-04-17 00:49:39 +00:00
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 ) {
2026-06-11 15:41:11 +00:00
// Surface any content-filter finish (e.g. Anthropic stop_reason:
// refusal) as an error. These turns may have produced no visible
// output at all — previously the session went idle silently — or
// partial text that was cut off by the provider's filter.
if ( handle . message . finish === "content-filter" ) {
handle . message . error = new SessionV1 . ContentFilterError ( {
message : "The response was blocked by the provider's content filter" ,
} ) . toObject ( )
yield * sessions . updateMessage ( handle . message )
yield * events . publish ( Session . Event . Error , { sessionID , error : handle.message.error } )
return "break" as const
}
2026-04-17 00:49:39 +00:00
if ( format . type === "json_schema" ) {
2026-06-03 02:42:13 +00:00
handle . message . error = new SessionV1 . StructuredOutputError ( {
2026-04-17 00:49:39 +00:00
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-06-03 02:43:28 +00:00
const loop : ( input : LoopInput ) = > Effect . Effect < SessionV1.WithParts > = Effect . fn ( "SessionPrompt.loop" ) ( function * (
input : LoopInput ,
) {
return yield * state . ensureRunning ( input . sessionID , lastAssistant ( input . sessionID ) , runLoop ( input . sessionID ) )
} )
2026-01-13 20:55:48 +00:00
2026-06-03 02:42:13 +00:00
const shell : ( input : ShellInput ) = > Effect . Effect < SessionV1.WithParts , Session.BusyError > = Effect . fn (
2026-05-14 01:28:04 +00:00
"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 ) {
2026-06-08 19:41:56 +00:00
yield * Effect . logInfo ( "command" , {
"session.id" : input . sessionID ,
command : input.command ,
agent : input.agent ,
} )
2026-04-17 00:49:39 +00:00
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 } ` } )
2026-05-31 01:08:38 +00:00
yield * events . publish ( Session . Event . Error , { sessionID : input.sessionID , error : error.toObject ( ) } )
2026-04-17 00:49:39 +00:00
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 } ` } )
2026-05-31 01:08:38 +00:00
yield * events . publish ( Session . Event . Error , { sessionID : input.sessionID , error : error.toObject ( ) } )
2026-04-17 00:49:39 +00:00
throw error
}
const templateParts = yield * resolvePromptParts ( template )
2026-06-12 04:19:15 +00:00
const inputFiles = new Set (
input . parts ? . filter ( ( part ) = > new URL ( part . url ) . protocol === "file:" ) . map ( ( part ) = > fileURLToPath ( part . url ) ) ,
)
const uniqueTemplateParts = templateParts . filter (
( part ) = > part . type !== "file" || ! inputFiles . has ( fileURLToPath ( part . url ) ) ,
)
2026-04-17 00:49:39 +00:00
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 ? ? "" ,
} ,
]
2026-06-12 04:19:15 +00:00
: [ . . . uniqueTemplateParts , . . . ( input . parts ? ? [ ] ) ]
2026-04-17 00:49:39 +00:00
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-05-31 01:08:38 +00:00
yield * events . publish ( Command . Event . Executed , {
2026-04-17 00:49:39 +00:00
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 ,
} )
} ) ,
)
2026-04-23 15:30:02 +00:00
const ModelRef = Schema . Struct ( {
2026-05-31 01:08:38 +00:00
providerID : ProviderV2.ID ,
2026-06-04 06:57:43 +00:00
modelID : ModelV2.ID ,
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" ,
} ) ,
2026-06-03 02:42:13 +00:00
format : Schema.optional ( SessionV1 . Format ) ,
2026-04-23 15:30:02 +00:00
system : Schema.optional ( Schema . String ) ,
variant : Schema.optional ( Schema . String ) ,
parts : Schema.Array (
Schema . Union ( [
2026-06-03 02:42:13 +00:00
SessionV1 . TextPartInput ,
SessionV1 . FilePartInput ,
SessionV1 . AgentPartInput ,
SessionV1 . SubtaskPartInput ,
2026-04-23 15:30:02 +00:00
] ) . 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 ,
2026-06-03 02:42:13 +00:00
source : Schema.optional ( SessionV1 . 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
2026-06-25 18:34:17 +00:00
export const node = LayerNode . make ( {
service : Service ,
layer : layer ,
deps : [
SessionStatus . node ,
Session . node ,
Agent . node ,
Provider . node ,
SessionProcessor . node ,
SessionCompaction . node ,
Plugin . node ,
Command . node ,
Config . node ,
Permission . node ,
FSUtil . node ,
MCP . node ,
LSP . node ,
ToolRegistry . node ,
Truncate . node ,
Image . node ,
CrossSpawnSpawner . node ,
Instruction . node ,
SessionRunState . node ,
SessionRevert . node ,
SessionSummary . node ,
SystemPrompt . node ,
LLM . node ,
EventV2Bridge . node ,
RuntimeFlags . node ,
Database . node ,
] ,
} )
2026-06-09 19:31:31 +00:00
2026-04-17 00:49:39 +00:00
export * as SessionPrompt from "./prompt"