From 0b9d653ec7dc305cec5a7d602a1573b78a505110 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Mon, 23 Feb 2026 23:37:48 -0500 Subject: [PATCH 1/3] feat: add ignored property to slash commands frontmatter --- packages/opencode/src/command/index.ts | 2 + packages/opencode/src/config/config.ts | 1 + packages/opencode/src/session/prompt.ts | 121 ++++++++++++++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 2 + 4 files changed, 126 insertions(+) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index dce7ac8bbc3..5dd4c2fa22f 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -32,6 +32,7 @@ export namespace Command { // https://zod.dev/v4/changelog?id=zfunction template: z.promise(z.string()).or(z.string()), subtask: z.boolean().optional(), + ignored: z.boolean().optional(), hints: z.array(z.string()), }) .meta({ @@ -92,6 +93,7 @@ export namespace Command { return command.template }, subtask: command.subtask, + ignored: command.ignored, hints: hints(command.template), } } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index aad0fd76c4b..7b1eefbf22e 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -670,6 +670,7 @@ export namespace Config { agent: z.string().optional(), model: ModelId.optional(), subtask: z.boolean().optional(), + ignored: z.boolean().optional(), }) export type Command = z.infer diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 75bd3c9dfac..a3663b6c226 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -73,6 +73,7 @@ export namespace SessionPrompt { reject(reason?: any): void }[] } + > = {} return data }, @@ -80,6 +81,7 @@ export namespace SessionPrompt { for (const item of Object.values(current)) { item.abort.abort() } + }, ) @@ -172,15 +174,18 @@ export namespace SessionPrompt { pattern: "*", }) } + if (permissions.length > 0) { session.permission = permissions await Session.setPermission({ sessionID: session.id, permission: permissions }) } + if (input.noReply === true) { return message } + return loop({ sessionID: input.sessionID }) }) @@ -211,9 +216,11 @@ export namespace SessionPrompt { name: agent.name, }) } + return } + if (stats.isDirectory()) { parts.push({ type: "file", @@ -224,6 +231,7 @@ export namespace SessionPrompt { return } + parts.push({ type: "file", url: pathToFileURL(filepath).href, @@ -243,6 +251,7 @@ export namespace SessionPrompt { abort: controller, callbacks: [], } + return controller.signal } @@ -261,6 +270,7 @@ export namespace SessionPrompt { SessionStatus.set(sessionID, { type: "idle" }) return } + match.abort.abort() delete s[sessionID] SessionStatus.set(sessionID, { type: "idle" }) @@ -282,6 +292,7 @@ export namespace SessionPrompt { }) } + using _ = defer(() => cancel(sessionID)) // Structured output state @@ -312,8 +323,10 @@ export namespace SessionPrompt { if (task && !lastFinished) { tasks.push(...task) } + } + if (!lastUser) throw new Error("No user message found in stream. This should never happen.") if ( lastAssistant?.finish && @@ -324,6 +337,7 @@ export namespace SessionPrompt { break } + step++ if (step === 1) ensureTitle({ @@ -343,6 +357,7 @@ export namespace SessionPrompt { }).toObject(), }) } + throw e }) const task = tasks.pop() @@ -403,6 +418,7 @@ export namespace SessionPrompt { subagent_type: task.agent, command: task.command, } + await Plugin.trigger( "tool.execute.before", { @@ -440,6 +456,7 @@ export namespace SessionPrompt { }) }, } + const result = await taskTool.execute(taskArgs, taskCtx).catch((error) => { executionError = error log.error("subtask execution failed", { error, agent: task.agent, description: task.description }) @@ -481,6 +498,7 @@ export namespace SessionPrompt { }, } satisfies MessageV2.ToolPart) } + if (!result) { await Session.updatePart({ ...part, @@ -497,6 +515,7 @@ export namespace SessionPrompt { } satisfies MessageV2.ToolPart) } + if (task.command) { // Add synthetic user message to prevent certain reasoning models from erroring // If we create assistant messages w/ out user ones following mid loop thinking signatures @@ -511,6 +530,7 @@ export namespace SessionPrompt { agent: lastUser.agent, model: lastUser.model, } + await Session.updateMessage(summaryUserMsg) await Session.updatePart({ id: Identifier.ascending("part"), @@ -522,9 +542,11 @@ export namespace SessionPrompt { } satisfies MessageV2.TextPart) } + continue } + // pending compaction if (task?.type === "compaction") { const result = await SessionCompaction.process({ @@ -538,6 +560,7 @@ export namespace SessionPrompt { continue } + // context overflow, needs compaction if ( lastFinished && @@ -553,6 +576,7 @@ export namespace SessionPrompt { continue } + // normal processing const agent = await Agent.get(lastUser.agent) const maxSteps = agent.steps ?? Infinity @@ -619,6 +643,7 @@ export namespace SessionPrompt { }) } + if (step === 1) { SessionSummary.summarize({ sessionID: sessionID, @@ -626,6 +651,7 @@ export namespace SessionPrompt { }) } + // Ephemerally wrap queued user messages with a reminder to stay on track if (step > 1 && lastFinished) { for (const msg of msgs) { @@ -642,9 +668,12 @@ export namespace SessionPrompt { "", ].join("\n") } + } + } + await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) // Build system prompt, adding structured output instruction if needed @@ -654,6 +683,7 @@ export namespace SessionPrompt { system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT) } + const result = await processor.process({ user: lastUser, agent, @@ -685,6 +715,7 @@ export namespace SessionPrompt { break } + // Check if model finished (finish reason is not "tool-calls" or "unknown") const modelFinished = processor.message.finish && !["tool-calls", "unknown"].includes(processor.message.finish) @@ -698,8 +729,10 @@ export namespace SessionPrompt { await Session.updateMessage(processor.message) break } + } + if (result === "stop") break if (result === "compact") { await SessionCompaction.create({ @@ -709,8 +742,10 @@ export namespace SessionPrompt { auto: true, }) } + continue } + SessionCompaction.prune({ sessionID }) for await (const item of MessageV2.stream(sessionID)) { if (item.info.role === "user") continue @@ -718,8 +753,10 @@ export namespace SessionPrompt { for (const q of queued) { q.resolve(item) } + return item } + throw new Error("Impossible") }) @@ -727,6 +764,7 @@ export namespace SessionPrompt { for await (const item of MessageV2.stream(sessionID)) { if (item.info.role === "user" && item.info.model) return item.info.model } + return Provider.defaultModel() } @@ -767,6 +805,7 @@ export namespace SessionPrompt { }, }) } + }, async ask(req) { await PermissionNext.ask({ @@ -810,6 +849,7 @@ export namespace SessionPrompt { messageID: input.processor.message.id, })), } + await Plugin.trigger( "tool.execute.after", { @@ -825,6 +865,7 @@ export namespace SessionPrompt { }) } + for (const [key, item] of Object.entries(await MCP.tools())) { const execute = item.execute if (!execute) continue @@ -884,6 +925,7 @@ export namespace SessionPrompt { if (resource.text) { textParts.push(resource.text) } + if (resource.blob) { attachments.push({ type: "file", @@ -892,9 +934,12 @@ export namespace SessionPrompt { filename: resource.uri, }) } + } + } + const truncated = await Truncate.output(textParts.join("\n\n"), {}, input.agent) const metadata = { ...(result.metadata ?? {}), @@ -902,6 +947,7 @@ export namespace SessionPrompt { ...(truncated.truncated && { outputPath: truncated.outputPath }), } + return { title: "", metadata, @@ -914,10 +960,13 @@ export namespace SessionPrompt { })), content: result.content, // directly return content to preserve ordering when outputting to model } + } + tools[key] = item } + return tools } @@ -941,12 +990,14 @@ export namespace SessionPrompt { title: "Structured Output", metadata: { valid: true }, } + }, toModelOutput(result) { return { type: "text", value: result.output, } + }, }) } @@ -975,6 +1026,7 @@ export namespace SessionPrompt { format: input.format, variant, } + using _ = defer(() => InstructionPrompt.clear(info.id)) type Draft = T extends MessageV2.Part ? Omit & { id?: string } : never @@ -1007,6 +1059,7 @@ export namespace SessionPrompt { throw new Error(`Resource not found: ${clientName}/${uri}`) } + // Handle different content types const contents = Array.isArray(resourceContent.contents) ? resourceContent.contents @@ -1032,8 +1085,10 @@ export namespace SessionPrompt { text: `[Binary content: ${mimeType}]`, }) } + } + pieces.push({ ...part, messageID: info.id, @@ -1051,8 +1106,10 @@ export namespace SessionPrompt { }) } + return pieces } + const url = new URL(part.url) switch (url.protocol) { case "data:": @@ -1079,6 +1136,7 @@ export namespace SessionPrompt { }, ] } + break case "file:": log.info("file", { mime: part.mime }) @@ -1091,6 +1149,7 @@ export namespace SessionPrompt { part.mime = "application/x-directory" } + if (part.mime === "text/plain") { let offset: number | undefined = undefined let limit: number | undefined = undefined @@ -1098,6 +1157,7 @@ export namespace SessionPrompt { 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) @@ -1114,18 +1174,24 @@ export namespace SessionPrompt { } else if ("location" in symbol) { range = symbol.location.range } + if (range?.start?.line && range?.start?.line === start) { start = range.start.line end = range?.end?.line ?? start break } + } + } + offset = Math.max(start, 1) if (end) { limit = end - (offset - 1) } + } + const args = { filePath: filepath, offset, limit } const pieces: Draft[] = [ @@ -1151,6 +1217,7 @@ export namespace SessionPrompt { metadata: async () => {}, ask: async () => {}, } + const result = await t.execute(args, readCtx) pieces.push({ messageID: info.id, @@ -1176,6 +1243,7 @@ export namespace SessionPrompt { sessionID: input.sessionID, }) } + }) .catch((error) => { log.error("failed to read file", { error }) @@ -1198,6 +1266,7 @@ export namespace SessionPrompt { return pieces } + if (part.mime === "application/x-directory") { const args = { filePath: filepath } const listCtx: Tool.Context = { @@ -1210,6 +1279,7 @@ export namespace SessionPrompt { metadata: async () => {}, ask: async () => {}, } + const result = await ReadTool.init().then((t) => t.execute(args, listCtx)) return [ { @@ -1234,6 +1304,7 @@ export namespace SessionPrompt { ] } + FileTime.read(input.sessionID, filepath) return [ { @@ -1255,8 +1326,10 @@ export namespace SessionPrompt { }, ] } + } + if (part.type === "agent") { // Check if this agent would be denied by task permission const perm = PermissionNext.evaluate("task", part.name, agent.permission) @@ -1282,6 +1355,7 @@ export namespace SessionPrompt { ] } + return [ { ...part, @@ -1312,10 +1386,12 @@ export namespace SessionPrompt { await Session.updatePart(part) } + return { info, parts, } + } async function insertReminders(input: { messages: MessageV2.WithParts[]; agent: Agent.Info; session: Session.Info }) { @@ -1334,6 +1410,7 @@ export namespace SessionPrompt { synthetic: true, }) } + const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") if (wasPlan && input.agent.name === "build") { userMessage.parts.push({ @@ -1345,9 +1422,11 @@ export namespace SessionPrompt { synthetic: true, }) } + return input.messages } + // New plan mode logic when flag is enabled const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") @@ -1367,9 +1446,11 @@ export namespace SessionPrompt { }) userMessage.parts.push(part) } + return input.messages } + // Entering plan mode if (input.agent.name === "plan" && assistantMessage?.info.agent !== "plan") { const plan = Session.plan(input.session) @@ -1455,6 +1536,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the userMessage.parts.push(part) return input.messages } + return input.messages } @@ -1476,6 +1558,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the throw new Session.BusyError(input.sessionID) } + using _ = defer(() => { // If no queued callbacks, cancel (the default) const callbacks = state()[input.sessionID]?.callbacks ?? [] @@ -1487,12 +1570,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the log.error("session loop failed to resume after shell command", { sessionID: input.sessionID, error }) }) } + }) const session = await Session.get(input.sessionID) if (session.revert) { await SessionRevert.cleanup(session) } + const agent = await Agent.get(input.agent) const model = input.model ?? agent.model ?? (await lastModel(input.sessionID)) const userMsg: MessageV2.User = { @@ -1508,6 +1593,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the modelID: model.modelID, }, } + await Session.updateMessage(userMsg) const userPart: MessageV2.Part = { type: "text", @@ -1517,6 +1603,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the text: "The following tool was executed by the user", synthetic: true, } + await Session.updatePart(userPart) const msg: MessageV2.Assistant = { @@ -1543,6 +1630,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the modelID: model.modelID, providerID: model.providerID, } + await Session.updateMessage(msg) const part: MessageV2.Part = { type: "tool", @@ -1561,6 +1649,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the }, }, } + await Session.updatePart(part) const shell = Shell.preferred() const shellName = ( @@ -1614,6 +1703,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the }, } + const matchingInvocation = invocations[shellName] ?? invocations[""] const args = matchingInvocation?.args @@ -1643,8 +1733,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the output: output, description: "", } + Session.updatePart(part) } + }) proc.stderr?.on("data", (chunk) => { @@ -1654,8 +1746,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the output: output, description: "", } + Session.updatePart(part) } + }) let aborted = false @@ -1668,11 +1762,13 @@ NOTE: At any point in time through this workflow you should feel free to ask the await kill() } + const abortHandler = () => { aborted = true void kill() } + abort.addEventListener("abort", abortHandler, { once: true }) await new Promise((resolve) => { @@ -1686,6 +1782,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the if (aborted) { output += "\n\n" + ["", "User aborted the command", ""].join("\n") } + msg.time.completed = Date.now() await Session.updateMessage(msg) if (part.state.status === "running") { @@ -1703,8 +1800,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the }, output, } + await Session.updatePart(part) } + return { info: msg, parts: [part] } } @@ -1758,6 +1857,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the if (value > last) last = value } + // Let the final placeholder swallow any extra arguments so prompts read naturally const withArgs = templateCommand.replaceAll(placeholderRegex, (_, index) => { const position = Number(index) @@ -1775,6 +1875,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the template = template + "\n\n" + input.arguments } + const shell = ConfigMarkdown.shell(template) if (shell.length > 0) { const results = await Promise.all( @@ -1784,23 +1885,28 @@ NOTE: At any point in time through this workflow you should feel free to ask the } catch (error) { return `Error executing command: ${error instanceof Error ? error.message : String(error)}` } + }), ) let index = 0 template = template.replace(bashRegex, () => results[index++]) } + template = template.trim() const taskModel = await (async () => { if (command.model) { return Provider.parseModel(command.model) } + if (command.agent) { const cmdAgent = await Agent.get(command.agent) if (cmdAgent?.model) { return cmdAgent.model } + } + if (input.model) return Provider.parseModel(input.model) return await lastModel(input.sessionID) })() @@ -1816,8 +1922,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the error: new NamedError.Unknown({ message: `Model not found: ${providerID}/${modelID}.${hint}` }).toObject(), }) } + throw e } + const agent = await Agent.get(agentName) if (!agent) { const available = await Agent.list().then((agents) => agents.filter((a) => !a.hidden).map((a) => a.name)) @@ -1830,7 +1938,19 @@ NOTE: At any point in time through this workflow you should feel free to ask the throw error } + const templateParts = await resolvePromptParts(template) + + if (command.ignored) { + for (const part of templateParts) { + if (part.type === "text") { + part.ignored = true + } + + } + + } + const isSubtask = (agent.mode === "subagent" && command.subtask !== false) || command.subtask === true const parts = isSubtask ? [ @@ -1955,5 +2075,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned return Session.setTitle({ sessionID: input.session.id, title }) } + } } diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 4050ef15738..f272c164de7 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1711,6 +1711,7 @@ export type Config = { agent?: string model?: string subtask?: boolean + ignored?: boolean } } /** @@ -2245,6 +2246,7 @@ export type Command = { source?: "command" | "mcp" | "skill" template: string subtask?: boolean + ignored?: boolean hints: Array } From 36f029602a146cdf33dd20a07733d1622230c874 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Tue, 24 Feb 2026 08:21:55 -0500 Subject: [PATCH 2/3] docs: add defensive comments for ignored property during merges --- packages/opencode/src/command/index.ts | 9 +++++++++ packages/opencode/src/config/config.ts | 3 +++ packages/opencode/src/session/prompt.ts | 2 ++ 3 files changed, 14 insertions(+) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 5dd4c2fa22f..521cb806873 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -31,6 +31,13 @@ export namespace Command { // workaround for zod not supporting async functions natively so we use getters // https://zod.dev/v4/changelog?id=zfunction template: z.promise(z.string()).or(z.string()), + // AGENTS: The `ignored` property marks commands whose output should not be processed by the model. + // When merging this branch, ensure `ignored` is included in ALL places where commands are loaded: + // 1. The Info schema (here) + // 2. The Config.Command schema in config/config.ts + // 3. Any command loading loops (e.g., state(), loadFreshCommands(), etc.) + // 4. The prompt.ts code that marks text parts as ignored + // DO NOT clobber or omit this property during merges! subtask: z.boolean().optional(), ignored: z.boolean().optional(), hints: z.array(z.string()), @@ -82,6 +89,7 @@ export namespace Command { }, } + // AGENTS: When merging, ensure ALL command loading loops include `ignored: command.ignored` for (const [name, command] of Object.entries(cfg.command ?? {})) { result[name] = { name, @@ -93,6 +101,7 @@ export namespace Command { return command.template }, subtask: command.subtask, + // AGENTS: DO NOT omit this line when merging! See comment above on Info schema. ignored: command.ignored, hints: hints(command.template), } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 7b1eefbf22e..afd6263de90 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -669,6 +669,9 @@ export namespace Config { description: z.string().optional(), agent: z.string().optional(), model: ModelId.optional(), + // AGENTS: The `ignored` property marks commands whose output should not be processed by the model. + // When merging this branch, ensure `ignored` is included here AND in Command.Info schema. + // DO NOT clobber or omit this property during merges! subtask: z.boolean().optional(), ignored: z.boolean().optional(), }) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index a3663b6c226..b396db7c3cc 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1941,6 +1941,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the const templateParts = await resolvePromptParts(template) + // AGENTS: This code marks command output as ignored so the model does not process it. + // DO NOT remove this block when merging! It is essential for the ignored commands feature. if (command.ignored) { for (const part of templateParts) { if (part.type === "text") { From 7c605412d52cc1579f95dcc8af68bbb85b3259e7 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Tue, 24 Feb 2026 13:52:50 -0500 Subject: [PATCH 3/3] fix: prevent model from responding to ignored commands - Add noReply: command.ignored to prompt() call to skip model processing - Fix isSubtask declaration order (was used before defined) - Clean up formatting in message-v2.ts --- packages/opencode/src/session/message-v2.ts | 12 +- packages/opencode/src/session/prompt.ts | 117 +------------------- 2 files changed, 8 insertions(+), 121 deletions(-) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 178751a2227..6506a59b3d0 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -556,11 +556,13 @@ export namespace MessageV2 { } result.push(userMessage) for (const part of msg.parts) { - if (part.type === "text" && !part.ignored) - userMessage.parts.push({ - type: "text", - text: part.text, - }) + if (part.type === "text") { + if (!part.ignored) + userMessage.parts.push({ + type: "text", + text: part.text, + }) + } // text/plain and directory files are converted into text parts, ignore them if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory") userMessage.parts.push({ diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b396db7c3cc..9835dee470b 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -73,7 +73,6 @@ export namespace SessionPrompt { reject(reason?: any): void }[] } - > = {} return data }, @@ -81,7 +80,6 @@ export namespace SessionPrompt { for (const item of Object.values(current)) { item.abort.abort() } - }, ) @@ -174,18 +172,15 @@ export namespace SessionPrompt { pattern: "*", }) } - if (permissions.length > 0) { session.permission = permissions await Session.setPermission({ sessionID: session.id, permission: permissions }) } - if (input.noReply === true) { return message } - return loop({ sessionID: input.sessionID }) }) @@ -216,11 +211,9 @@ export namespace SessionPrompt { name: agent.name, }) } - return } - if (stats.isDirectory()) { parts.push({ type: "file", @@ -231,7 +224,6 @@ export namespace SessionPrompt { return } - parts.push({ type: "file", url: pathToFileURL(filepath).href, @@ -251,7 +243,6 @@ export namespace SessionPrompt { abort: controller, callbacks: [], } - return controller.signal } @@ -270,7 +261,6 @@ export namespace SessionPrompt { SessionStatus.set(sessionID, { type: "idle" }) return } - match.abort.abort() delete s[sessionID] SessionStatus.set(sessionID, { type: "idle" }) @@ -292,7 +282,6 @@ export namespace SessionPrompt { }) } - using _ = defer(() => cancel(sessionID)) // Structured output state @@ -323,10 +312,8 @@ export namespace SessionPrompt { if (task && !lastFinished) { tasks.push(...task) } - } - if (!lastUser) throw new Error("No user message found in stream. This should never happen.") if ( lastAssistant?.finish && @@ -337,7 +324,6 @@ export namespace SessionPrompt { break } - step++ if (step === 1) ensureTitle({ @@ -357,7 +343,6 @@ export namespace SessionPrompt { }).toObject(), }) } - throw e }) const task = tasks.pop() @@ -418,7 +403,6 @@ export namespace SessionPrompt { subagent_type: task.agent, command: task.command, } - await Plugin.trigger( "tool.execute.before", { @@ -456,7 +440,6 @@ export namespace SessionPrompt { }) }, } - const result = await taskTool.execute(taskArgs, taskCtx).catch((error) => { executionError = error log.error("subtask execution failed", { error, agent: task.agent, description: task.description }) @@ -498,7 +481,6 @@ export namespace SessionPrompt { }, } satisfies MessageV2.ToolPart) } - if (!result) { await Session.updatePart({ ...part, @@ -515,7 +497,6 @@ export namespace SessionPrompt { } satisfies MessageV2.ToolPart) } - if (task.command) { // Add synthetic user message to prevent certain reasoning models from erroring // If we create assistant messages w/ out user ones following mid loop thinking signatures @@ -530,7 +511,6 @@ export namespace SessionPrompt { agent: lastUser.agent, model: lastUser.model, } - await Session.updateMessage(summaryUserMsg) await Session.updatePart({ id: Identifier.ascending("part"), @@ -542,11 +522,9 @@ export namespace SessionPrompt { } satisfies MessageV2.TextPart) } - continue } - // pending compaction if (task?.type === "compaction") { const result = await SessionCompaction.process({ @@ -560,7 +538,6 @@ export namespace SessionPrompt { continue } - // context overflow, needs compaction if ( lastFinished && @@ -576,7 +553,6 @@ export namespace SessionPrompt { continue } - // normal processing const agent = await Agent.get(lastUser.agent) const maxSteps = agent.steps ?? Infinity @@ -643,7 +619,6 @@ export namespace SessionPrompt { }) } - if (step === 1) { SessionSummary.summarize({ sessionID: sessionID, @@ -651,7 +626,6 @@ export namespace SessionPrompt { }) } - // Ephemerally wrap queued user messages with a reminder to stay on track if (step > 1 && lastFinished) { for (const msg of msgs) { @@ -668,12 +642,9 @@ export namespace SessionPrompt { "", ].join("\n") } - } - } - await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) // Build system prompt, adding structured output instruction if needed @@ -683,7 +654,6 @@ export namespace SessionPrompt { system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT) } - const result = await processor.process({ user: lastUser, agent, @@ -715,7 +685,6 @@ export namespace SessionPrompt { break } - // Check if model finished (finish reason is not "tool-calls" or "unknown") const modelFinished = processor.message.finish && !["tool-calls", "unknown"].includes(processor.message.finish) @@ -729,10 +698,8 @@ export namespace SessionPrompt { await Session.updateMessage(processor.message) break } - } - if (result === "stop") break if (result === "compact") { await SessionCompaction.create({ @@ -742,10 +709,8 @@ export namespace SessionPrompt { auto: true, }) } - continue } - SessionCompaction.prune({ sessionID }) for await (const item of MessageV2.stream(sessionID)) { if (item.info.role === "user") continue @@ -753,10 +718,8 @@ export namespace SessionPrompt { for (const q of queued) { q.resolve(item) } - return item } - throw new Error("Impossible") }) @@ -764,7 +727,6 @@ export namespace SessionPrompt { for await (const item of MessageV2.stream(sessionID)) { if (item.info.role === "user" && item.info.model) return item.info.model } - return Provider.defaultModel() } @@ -805,7 +767,6 @@ export namespace SessionPrompt { }, }) } - }, async ask(req) { await PermissionNext.ask({ @@ -849,7 +810,6 @@ export namespace SessionPrompt { messageID: input.processor.message.id, })), } - await Plugin.trigger( "tool.execute.after", { @@ -865,7 +825,6 @@ export namespace SessionPrompt { }) } - for (const [key, item] of Object.entries(await MCP.tools())) { const execute = item.execute if (!execute) continue @@ -925,7 +884,6 @@ export namespace SessionPrompt { if (resource.text) { textParts.push(resource.text) } - if (resource.blob) { attachments.push({ type: "file", @@ -934,12 +892,9 @@ export namespace SessionPrompt { filename: resource.uri, }) } - } - } - const truncated = await Truncate.output(textParts.join("\n\n"), {}, input.agent) const metadata = { ...(result.metadata ?? {}), @@ -947,7 +902,6 @@ export namespace SessionPrompt { ...(truncated.truncated && { outputPath: truncated.outputPath }), } - return { title: "", metadata, @@ -960,13 +914,10 @@ export namespace SessionPrompt { })), content: result.content, // directly return content to preserve ordering when outputting to model } - } - tools[key] = item } - return tools } @@ -990,14 +941,12 @@ export namespace SessionPrompt { title: "Structured Output", metadata: { valid: true }, } - }, toModelOutput(result) { return { type: "text", value: result.output, } - }, }) } @@ -1026,7 +975,6 @@ export namespace SessionPrompt { format: input.format, variant, } - using _ = defer(() => InstructionPrompt.clear(info.id)) type Draft = T extends MessageV2.Part ? Omit & { id?: string } : never @@ -1059,7 +1007,6 @@ export namespace SessionPrompt { throw new Error(`Resource not found: ${clientName}/${uri}`) } - // Handle different content types const contents = Array.isArray(resourceContent.contents) ? resourceContent.contents @@ -1085,10 +1032,8 @@ export namespace SessionPrompt { text: `[Binary content: ${mimeType}]`, }) } - } - pieces.push({ ...part, messageID: info.id, @@ -1106,10 +1051,8 @@ export namespace SessionPrompt { }) } - return pieces } - const url = new URL(part.url) switch (url.protocol) { case "data:": @@ -1136,7 +1079,6 @@ export namespace SessionPrompt { }, ] } - break case "file:": log.info("file", { mime: part.mime }) @@ -1149,7 +1091,6 @@ export namespace SessionPrompt { part.mime = "application/x-directory" } - if (part.mime === "text/plain") { let offset: number | undefined = undefined let limit: number | undefined = undefined @@ -1157,7 +1098,6 @@ export namespace SessionPrompt { 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) @@ -1174,24 +1114,18 @@ export namespace SessionPrompt { } else if ("location" in symbol) { range = symbol.location.range } - if (range?.start?.line && range?.start?.line === start) { start = range.start.line end = range?.end?.line ?? start break } - } - } - offset = Math.max(start, 1) if (end) { limit = end - (offset - 1) } - } - const args = { filePath: filepath, offset, limit } const pieces: Draft[] = [ @@ -1217,7 +1151,6 @@ export namespace SessionPrompt { metadata: async () => {}, ask: async () => {}, } - const result = await t.execute(args, readCtx) pieces.push({ messageID: info.id, @@ -1243,7 +1176,6 @@ export namespace SessionPrompt { sessionID: input.sessionID, }) } - }) .catch((error) => { log.error("failed to read file", { error }) @@ -1266,7 +1198,6 @@ export namespace SessionPrompt { return pieces } - if (part.mime === "application/x-directory") { const args = { filePath: filepath } const listCtx: Tool.Context = { @@ -1279,7 +1210,6 @@ export namespace SessionPrompt { metadata: async () => {}, ask: async () => {}, } - const result = await ReadTool.init().then((t) => t.execute(args, listCtx)) return [ { @@ -1304,7 +1234,6 @@ export namespace SessionPrompt { ] } - FileTime.read(input.sessionID, filepath) return [ { @@ -1326,10 +1255,8 @@ export namespace SessionPrompt { }, ] } - } - if (part.type === "agent") { // Check if this agent would be denied by task permission const perm = PermissionNext.evaluate("task", part.name, agent.permission) @@ -1355,7 +1282,6 @@ export namespace SessionPrompt { ] } - return [ { ...part, @@ -1386,12 +1312,10 @@ export namespace SessionPrompt { await Session.updatePart(part) } - return { info, parts, } - } async function insertReminders(input: { messages: MessageV2.WithParts[]; agent: Agent.Info; session: Session.Info }) { @@ -1410,7 +1334,6 @@ export namespace SessionPrompt { synthetic: true, }) } - const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") if (wasPlan && input.agent.name === "build") { userMessage.parts.push({ @@ -1422,11 +1345,9 @@ export namespace SessionPrompt { synthetic: true, }) } - return input.messages } - // New plan mode logic when flag is enabled const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") @@ -1446,11 +1367,9 @@ export namespace SessionPrompt { }) userMessage.parts.push(part) } - return input.messages } - // Entering plan mode if (input.agent.name === "plan" && assistantMessage?.info.agent !== "plan") { const plan = Session.plan(input.session) @@ -1536,7 +1455,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the userMessage.parts.push(part) return input.messages } - return input.messages } @@ -1558,7 +1476,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the throw new Session.BusyError(input.sessionID) } - using _ = defer(() => { // If no queued callbacks, cancel (the default) const callbacks = state()[input.sessionID]?.callbacks ?? [] @@ -1570,14 +1487,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the log.error("session loop failed to resume after shell command", { sessionID: input.sessionID, error }) }) } - }) const session = await Session.get(input.sessionID) if (session.revert) { await SessionRevert.cleanup(session) } - const agent = await Agent.get(input.agent) const model = input.model ?? agent.model ?? (await lastModel(input.sessionID)) const userMsg: MessageV2.User = { @@ -1593,7 +1508,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the modelID: model.modelID, }, } - await Session.updateMessage(userMsg) const userPart: MessageV2.Part = { type: "text", @@ -1603,7 +1517,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the text: "The following tool was executed by the user", synthetic: true, } - await Session.updatePart(userPart) const msg: MessageV2.Assistant = { @@ -1630,7 +1543,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the modelID: model.modelID, providerID: model.providerID, } - await Session.updateMessage(msg) const part: MessageV2.Part = { type: "tool", @@ -1649,7 +1561,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the }, }, } - await Session.updatePart(part) const shell = Shell.preferred() const shellName = ( @@ -1703,7 +1614,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the }, } - const matchingInvocation = invocations[shellName] ?? invocations[""] const args = matchingInvocation?.args @@ -1733,10 +1643,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the output: output, description: "", } - Session.updatePart(part) } - }) proc.stderr?.on("data", (chunk) => { @@ -1746,10 +1654,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the output: output, description: "", } - Session.updatePart(part) } - }) let aborted = false @@ -1762,13 +1668,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the await kill() } - const abortHandler = () => { aborted = true void kill() } - abort.addEventListener("abort", abortHandler, { once: true }) await new Promise((resolve) => { @@ -1782,7 +1686,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the if (aborted) { output += "\n\n" + ["", "User aborted the command", ""].join("\n") } - msg.time.completed = Date.now() await Session.updateMessage(msg) if (part.state.status === "running") { @@ -1800,10 +1703,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the }, output, } - await Session.updatePart(part) } - return { info: msg, parts: [part] } } @@ -1857,7 +1758,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the if (value > last) last = value } - // Let the final placeholder swallow any extra arguments so prompts read naturally const withArgs = templateCommand.replaceAll(placeholderRegex, (_, index) => { const position = Number(index) @@ -1875,7 +1775,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the template = template + "\n\n" + input.arguments } - const shell = ConfigMarkdown.shell(template) if (shell.length > 0) { const results = await Promise.all( @@ -1885,28 +1784,23 @@ NOTE: At any point in time through this workflow you should feel free to ask the } catch (error) { return `Error executing command: ${error instanceof Error ? error.message : String(error)}` } - }), ) let index = 0 template = template.replace(bashRegex, () => results[index++]) } - template = template.trim() const taskModel = await (async () => { if (command.model) { return Provider.parseModel(command.model) } - if (command.agent) { const cmdAgent = await Agent.get(command.agent) if (cmdAgent?.model) { return cmdAgent.model } - } - if (input.model) return Provider.parseModel(input.model) return await lastModel(input.sessionID) })() @@ -1922,10 +1816,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the error: new NamedError.Unknown({ message: `Model not found: ${providerID}/${modelID}.${hint}` }).toObject(), }) } - throw e } - const agent = await Agent.get(agentName) if (!agent) { const available = await Agent.list().then((agents) => agents.filter((a) => !a.hidden).map((a) => a.name)) @@ -1938,21 +1830,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the throw error } - const templateParts = await resolvePromptParts(template) - - // AGENTS: This code marks command output as ignored so the model does not process it. - // DO NOT remove this block when merging! It is essential for the ignored commands feature. if (command.ignored) { for (const part of templateParts) { if (part.type === "text") { part.ignored = true } - } - } - const isSubtask = (agent.mode === "subagent" && command.subtask !== false) || command.subtask === true const parts = isSubtask ? [ @@ -1995,6 +1880,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the agent: userAgent, parts, variant: input.variant, + noReply: command.ignored, })) as MessageV2.WithParts Bus.publish(Command.Event.Executed, { @@ -2077,6 +1963,5 @@ NOTE: At any point in time through this workflow you should feel free to ask the const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned return Session.setTitle({ sessionID: input.session.id, title }) } - } }