diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 2fa3126d..27aea04d 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -73,7 +73,14 @@ import { Team } from "@/team" import { ActorRegistry } from "@/actor/registry" import { Metrics } from "@/metrics" import { resolveInvocationStyle, type ToolStyleConfig } from "../tool/invocation-style" -import { shouldAutoDream, shouldAutoDistill, DREAM_TASK, DISTILL_TASK, AUTO_DREAM_TITLE, AUTO_DISTILL_TITLE } from "./auto-dream" +import { + shouldAutoDream, + shouldAutoDistill, + DREAM_TASK, + DISTILL_TASK, + AUTO_DREAM_TITLE, + AUTO_DISTILL_TITLE, +} from "./auto-dream" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false @@ -83,8 +90,7 @@ globalThis.AI_SDK_LOG_WARNINGS = false // emit JSON and crash the shell parser). `memory` has no shell form, so it is // always JSON. Exported for unit testing. export function recallHintLines(toolCfg: ToolStyleConfig | undefined): string[] { - const taskHint = - resolveInvocationStyle(toolCfg, "task") === "shell" ? "- task list" : `- task({ operation: "list" })` + const taskHint = resolveInvocationStyle(toolCfg, "task") === "shell" ? "- task list" : `- task({ operation: "list" })` const actorHint = resolveInvocationStyle(toolCfg, "actor") === "shell" ? "- actor status " @@ -147,6 +153,14 @@ function stepSignature(parts: MessageV2.Part[]): string | undefined { return segments.join("\n") } +type DraftPart = T extends MessageV2.Part ? Omit & { id?: string } : never + +type ResolveUserPartCtx = { + input: PromptInput + info: MessageV2.User + agentPermission: Permission.Ruleset +} + const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format. IMPORTANT: @@ -226,7 +240,12 @@ export const layer = Layer.effect( // only needs to pass string IDs. const capture: typeof prefixCaptureRef.current = (input) => Effect.gen(function* () { - const empty = { system: [] as string[], tools: {} as Record, inheritedMessages: [] as ModelMessage[], parentPermission: [] as Permission.Ruleset } + const empty = { + system: [] as string[], + tools: {} as Record, + inheritedMessages: [] as ModelMessage[], + parentPermission: [] as Permission.Ruleset, + } const ag = yield* agents.get(input.agentName).pipe(Effect.catch(() => Effect.succeed(undefined))) if (!ag) return empty const model = yield* provider @@ -448,9 +467,7 @@ export const layer = Layer.effect( const userMessage = input.messages.findLast((msg) => msg.info.role === "user") if (!userMessage) return input.messages - const composeModeMsg = input.messages.find( - (msg) => msg.info.role === "user" && msg.info.agent === "compose", - ) + const composeModeMsg = input.messages.find((msg) => msg.info.role === "user" && msg.info.agent === "compose") if (composeModeMsg) { const composeModeBlock = composeSkillsBlock() composeModeMsg.parts.unshift({ @@ -1240,15 +1257,281 @@ NOTE: At any point in time through this workflow you should feel free to ask the }) const lastModel = Effect.fnUntraced(function* (sessionID: SessionID) { - const match = yield* sessions.findMessage( - sessionID, - (m) => m.info.role === "user" && !!m.info.model, - { agentID: "*" }, - ) + const match = yield* sessions.findMessage(sessionID, (m) => m.info.role === "user" && !!m.info.model, { + agentID: "*", + }) if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model return yield* provider.defaultModel() }) + const resolveUserPart = Effect.fn("SessionPrompt.resolveUserPart")(function* ( + part: PromptInput["parts"][number], + ctx: ResolveUserPartCtx, + ) { + const { info, input } = ctx + 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: DraftPart[] = [ + { + 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}]`, + }) + } + } + 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}`, + }) + } + 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) + if (yield* fsys.isDir(filepath)) part.mime = "application/x-directory" + + const { read } = yield* registry.named() + const execRead = (args: Parameters[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()))) + } + + if (part.mime === "text/plain") { + 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) { + const symbols = yield* lsp.documentSymbol(filePathURI).pipe(Effect.catch(() => Effect.succeed([]))) + 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 + } + } + } + offset = Math.max(start, 1) + if (end) limit = end - (offset - 1) + } + const args = { filePath: filepath, offset, limit } + const pieces: DraftPart[] = [ + { + 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, + messageID: info.id, + sessionID: input.sessionID, + })), + ) + } else { + pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID }) + } + } 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 + } + + if (part.mime === "application/x-directory") { + 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(), + }) + return [ + { + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `Read tool failed to read ${filepath} with the following error: ${message}`, + }, + ] + } + return [ + { + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `Called the Read tool with the following input: ${JSON.stringify(args)}`, + }, + { + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: exit.value.output, + }, + { ...part, messageID: info.id, sessionID: input.sessionID }, + ] + } + + 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: + `data:${part.mime};base64,` + + Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"), + mime: part.mime, + filename: part.filename!, + source: part.source, + }, + ] + } + } + } + + if (part.type === "agent") { + const perm = Permission.evaluate("task", part.name, ctx.agentPermission) + 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 actor tool with subagent: " + + part.name + + hint, + }, + ] + } + + return [{ ...part, messageID: info.id, sessionID: input.sessionID }] + }) + const createUserMessage = Effect.fn("SessionPrompt.createUserMessage")(function* (input: PromptInput) { const agentName = input.agent || (yield* agents.defaultAgent()) const ag = yield* agents.get(agentName) @@ -1298,282 +1581,20 @@ NOTE: At any point in time through this workflow you should feel free to ask the yield* Effect.addFinalizer(() => instruction.clear(info.id)) - type Draft = T extends MessageV2.Part ? Omit & { id?: string } : never - const assign = (part: Draft): MessageV2.Part => ({ + const assign = (part: DraftPart): MessageV2.Part => ({ ...part, id: part.id ? PartID.make(part.id) : PartID.ascending(), }) - const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect[]> = 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[] = [ - { - 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}]`, - }) - } - } - 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}`, - }) - } - 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) - if (yield* fsys.isDir(filepath)) part.mime = "application/x-directory" - - const { read } = yield* registry.named() - const execRead = (args: Parameters[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()))) - } - - if (part.mime === "text/plain") { - 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) { - const symbols = yield* lsp.documentSymbol(filePathURI).pipe(Effect.catch(() => Effect.succeed([]))) - 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 - } - } - } - offset = Math.max(start, 1) - if (end) limit = end - (offset - 1) - } - const args = { filePath: filepath, offset, limit } - const pieces: Draft[] = [ - { - 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, - messageID: info.id, - sessionID: input.sessionID, - })), - ) - } else { - pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID }) - } - } 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 - } - - if (part.mime === "application/x-directory") { - 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(), - }) - return [ - { - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: `Read tool failed to read ${filepath} with the following error: ${message}`, - }, - ] - } - return [ - { - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: `Called the Read tool with the following input: ${JSON.stringify(args)}`, - }, - { - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: exit.value.output, - }, - { ...part, messageID: info.id, sessionID: input.sessionID }, - ] - } - - 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: - `data:${part.mime};base64,` + - Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"), - mime: part.mime, - filename: part.filename!, - source: part.source, - }, - ] - } - } - } - - 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 actor tool with subagent: " + - part.name + - hint, - }, - ] - } - - return [{ ...part, messageID: info.id, sessionID: input.sessionID }] - }) - - const parts = yield* Effect.forEach(input.parts, resolvePart, { concurrency: "unbounded" }).pipe( - Effect.map((x) => x.flat().map(assign)), - ) - + const resolvePart = (part: PromptInput["parts"][number]) => + resolveUserPart(part, { + input, + info, + agentPermission: ag.permission, + }) as Effect.Effect[]> + const parts = yield* Effect.forEach(input.parts, resolvePart, { + concurrency: "unbounded", + }).pipe(Effect.map((x) => x.flat().map(assign))) yield* plugin.trigger( "chat.message", { @@ -1695,10 +1716,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the throw new Error("Impossible") }) - const runLoop: (sessionID: SessionID, agentID?: string, task_id?: string) => Effect.Effect = Effect.fn( - "SessionPrompt.run", - )( - function* (sessionID: SessionID, agentID?: string, task_id?: string) { + const runLoop: (sessionID: SessionID, agentID?: string, task_id?: string) => Effect.Effect = + Effect.fn("SessionPrompt.run")(function* (sessionID: SessionID, agentID?: string, task_id?: string) { const ctx = yield* InstanceState.context const slog = elog.with({ sessionID }) let structured: unknown | undefined @@ -2267,7 +2286,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the Effect.gen(function* () { const s = yield* svc.create({ title: AUTO_DREAM_TITLE }) const sp = yield* Service - yield* sp.prompt({ sessionID: s.id, agent: "dream", model: mdl, parts: [{ type: "text", text: DREAM_TASK }] }) + yield* sp.prompt({ + sessionID: s.id, + agent: "dream", + model: mdl, + parts: [{ type: "text", text: DREAM_TASK }], + }) }), ), ).catch((err) => log.error("auto-dream prompt failed", { error: String(err) })) @@ -2278,7 +2302,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the Effect.gen(function* () { const s = yield* svc.create({ title: AUTO_DISTILL_TITLE }) const sp = yield* Service - yield* sp.prompt({ sessionID: s.id, agent: "distill", model: mdl, parts: [{ type: "text", text: DISTILL_TASK }] }) + yield* sp.prompt({ + sessionID: s.id, + agent: "distill", + model: mdl, + parts: [{ type: "text", text: DISTILL_TASK }], + }) }), ), ).catch((err) => log.error("auto-distill prompt failed", { error: String(err) })) @@ -2366,9 +2395,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const lastUserMsg = msgs.findLast((m) => m.info.role === "user") if ( lastUserMsg && - !lastUserMsg.parts.some( - (p) => p.type === "text" && p.text?.includes("repeating the same action"), - ) + !lastUserMsg.parts.some((p) => p.type === "text" && p.text?.includes("repeating the same action")) ) { lastUserMsg.parts.push({ id: PartID.ascending(), @@ -2397,8 +2424,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the // summary, checkpoint-writer) are exempt from context management; // see docs/superpowers/specs/2026-04-28-bounded-computation-agents-design.md const agent = yield* agents.get(lastUser.agent) - const isBoundedComputation = - agent?.native === true && agent?.hidden === true + const isBoundedComputation = agent?.native === true && agent?.hidden === true // Fire background checkpoint writers for any newly-crossed thresholds // based on the latest completed assistant message's tokens. Must run @@ -2593,16 +2619,13 @@ NOTE: At any point in time through this workflow you should feel free to ask the // agent identity — which would diverge from the parent and break the // prefix cache. const actorRecord = lastUser.agentID - ? yield* actorRegistry.get(sessionID, lastUser.agentID).pipe( - Effect.orElseSucceed(() => undefined), - ) + ? yield* actorRegistry.get(sessionID, lastUser.agentID).pipe(Effect.orElseSucceed(() => undefined)) : undefined // v9 registers main as `mode: "main"` with `contextMode: "full"`. // Only spawned actors (subagent/peer) carry a frozen ForkContext; // main is the captor, never the captured. const isForkAgent = - actorRecord?.contextMode === "full" && - (actorRecord.mode === "subagent" || actorRecord.mode === "peer") + actorRecord?.contextMode === "full" && (actorRecord.mode === "subagent" || actorRecord.mode === "peer") // Fork path: read frozen ForkContext from Actor service (late-bound via // spawnRef to break the Actor → SessionPrompt → Actor layer cycle). @@ -2617,7 +2640,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the agentID: lastUser.agentID, }) yield* actorRegistry - .updateStatus(sessionID, lastUser.agentID!, { status: "idle", lastOutcome: "failure", lastError: "missing fork context" }) + .updateStatus(sessionID, lastUser.agentID!, { + status: "idle", + lastOutcome: "failure", + lastError: "missing fork context", + }) .pipe(Effect.ignore) return "break" as const } @@ -2664,10 +2691,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the agentID: lastUser.agentID, }) - if ( - result === "continue" && - (yield* autoContinueOutputLength({ lastUser, assistant: handle.message })) - ) { + if (result === "continue" && (yield* autoContinueOutputLength({ lastUser, assistant: handle.message }))) { return "continue" as const } @@ -2703,8 +2727,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the (forkClassification.type === "think-only" || forkClassification.type === "invalid") && format.type !== "json_schema" ) { - const reason = - forkClassification.type === "invalid" ? forkClassification.reason : "think-only" + const reason = forkClassification.type === "invalid" ? forkClassification.reason : "think-only" if (yield* autoContinueInvalidOutput({ lastUser, assistant: handle.message, reason })) return "continue" as const return "break" as const @@ -2762,17 +2785,13 @@ NOTE: At any point in time through this workflow you should feel free to ask the // task creates, etc.) so each step doesn't replay from the bare // user prompt. The watermark is for fork capture only (frozen // snapshot of parent-view at spawn time). - const { system: prebuiltSystem, inheritedMessages: modelMsgs } = - yield* buildLLMRequestPrefix({ - sessionID, - agent, - model, - msgs, - additions, - }).pipe( - Effect.provideService(LLM.Service, llm), - Effect.provideService(ToolRegistry.Service, registry), - ) + const { system: prebuiltSystem, inheritedMessages: modelMsgs } = yield* buildLLMRequestPrefix({ + sessionID, + agent, + model, + msgs, + additions, + }).pipe(Effect.provideService(LLM.Service, llm), Effect.provideService(ToolRegistry.Service, registry)) const maxModeCfg = (yield* config.get()).experimental?.maxMode const useMaxMode = agent.name === MaxMode.MAX_MODE_AGENT && maxModeCfg !== undefined && format.type !== "json_schema" @@ -2802,15 +2821,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the handle, llm, candidates: maxModeCfg?.candidates, - setStatus: (message) => - status.set(sessionID, message ? { type: "busy", message } : { type: "busy" }), + setStatus: (message) => status.set(sessionID, message ? { type: "busy", message } : { type: "busy" }), }) : yield* handle.process(processArgs) - if ( - result === "continue" && - (yield* autoContinueOutputLength({ lastUser, assistant: handle.message })) - ) { + if (result === "continue" && (yield* autoContinueOutputLength({ lastUser, assistant: handle.message }))) { return "continue" as const } @@ -2879,14 +2894,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the // boundary marker (never deletes). Prefer rebuild over compaction: // if a writer is running or finished, wait (bounded) and rebuild // from it. Fall back to compaction only when no boundary exists. - const writerRunning = yield* checkpoint.isWriterRunning(sessionID) - .pipe(Effect.catch(() => Effect.succeed(false))) - const hasCP = yield* checkpoint.hasCheckpoint(sessionID) + const writerRunning = yield* checkpoint + .isWriterRunning(sessionID) .pipe(Effect.catch(() => Effect.succeed(false))) + const hasCP = yield* checkpoint.hasCheckpoint(sessionID).pipe(Effect.catch(() => Effect.succeed(false))) if (writerRunning || hasCP) { yield* checkpoint.waitForWriter(sessionID).pipe(Effect.ignore) - const boundary2 = yield* checkpoint.lastBoundary(sessionID) + const boundary2 = yield* checkpoint + .lastBoundary(sessionID) .pipe(Effect.catch(() => Effect.succeed(undefined))) const boundary2Msg = boundary2 ? msgs.find((m) => m.info.id === boundary2) : undefined const inserted2 = boundary2 @@ -2945,18 +2961,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the } const final = yield* lastAssistant(sessionID, agentID) const finalIsError = final.info.role === "assistant" && !!final.info.error - const lastUserForMetrics = yield* sessions.findMessage( - sessionID, - (m) => m.info.role === "user", - { agentID: "*" }, - ) + const lastUserForMetrics = yield* sessions.findMessage(sessionID, (m) => m.info.role === "user", { + agentID: "*", + }) yield* publishAgentRequest( finalIsError ? "error" : "completed", Option.isSome(lastUserForMetrics) ? lastUserForMetrics.value.info.agent : final.info.agent, ) return final - }, - ) + }) const loop: (input: z.infer) => Effect.Effect = Effect.fn( "SessionPrompt.loop", @@ -3076,9 +3089,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the let parts: PromptInput["parts"] if (isSubtask) { - const promptText = cmd.source === "skill" - ? templateCommand + (input.arguments.trim() ? "\n\n" + input.arguments : "") - : (templateParts.find((y): y is typeof y & { type: "text"; text: string } => y.type === "text"))?.text ?? "" + const promptText = + cmd.source === "skill" + ? templateCommand + (input.arguments.trim() ? "\n\n" + input.arguments : "") + : (templateParts.find((y): y is typeof y & { type: "text"; text: string } => y.type === "text")?.text ?? "") parts = [ { type: "subtask" as const, @@ -3090,9 +3104,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the }, ] } else if (cmd.source === "skill") { - const visibleText = input.arguments.trim() - ? `/${input.command} ${input.arguments}` - : `/${input.command}` + const visibleText = input.arguments.trim() ? `/${input.command} ${input.arguments}` : `/${input.command}` const skillPart = { type: "text" as const, text: `\n${templateCommand}\n`, @@ -3210,8 +3222,12 @@ export const PromptInput = z.object({ ), agent: z.string().optional(), agentID: z.string().optional(), - task_id: z.string().optional() - .describe("If the spawning caller bound this prompt to a specific user-task (T4 etc), pass its TID. Propagates to Tool.Context.taskId so memory-path-guard allows writes to tasks//*.md."), + task_id: z + .string() + .optional() + .describe( + "If the spawning caller bound this prompt to a specific user-task (T4 etc), pass its TID. Propagates to Tool.Context.taskId so memory-path-guard allows writes to tasks//*.md.", + ), source: z.enum(["user", "spawn", "hook"]).optional(), provenance: MessageV2.Provenance.optional(), noReply: z.boolean().optional(),