From 1e216153cdf907356ae2355e553d405b8b3553a3 Mon Sep 17 00:00:00 2001 From: wzh656 Date: Mon, 13 Jul 2026 20:37:20 +0800 Subject: [PATCH 1/3] feat: allow subagent resume after interruption via task_id When a subagent (task tool) is interrupted: - Mark the tool part metadata with interrupted=true and preserve sessionId - Show a descriptive error message with the subagent session ID and instructions to resume by calling task with task_id - When task_id is provided and the child session has prior assistant messages, use continue instead of prompt to resume from where the subagent left off, rather than starting fresh This prevents total task loss on interruption - the model can now continue the subagent's work instead of re-spawning a new one. --- bun.lock | 2 +- packages/core/src/i18n.ts | 10 ++++++-- packages/miaopan-code/src/session/prompt.ts | 27 ++++++++++++++++---- packages/miaopan-code/src/tool/task.ts | 18 +++++++++++++ packages/miaopan-code/test/tool/task.test.ts | 16 ++++++++++++ 5 files changed, 65 insertions(+), 8 deletions(-) diff --git a/bun.lock b/bun.lock index 3ef257d7f..1bef81a0c 100644 --- a/bun.lock +++ b/bun.lock @@ -174,7 +174,7 @@ }, "packages/miaopan-code": { "name": "miaopan-code", - "version": "0.1.3", + "version": "0.2.3", "bin": { "miaopan-code": "./bin/miaopan-code", }, diff --git a/packages/core/src/i18n.ts b/packages/core/src/i18n.ts index 042381e6f..bcfc3b5ab 100644 --- a/packages/core/src/i18n.ts +++ b/packages/core/src/i18n.ts @@ -1196,6 +1196,11 @@ export const messages = { }, "tool.error.skill": { "zh-CN": "无法加载技能 {{name}}", en: "Unable to load skill {{name}}" }, "tool.error.task_cancelled": { "zh-CN": "任务已取消", en: "Task cancelled" }, + "tool.error.subagent_interrupted": { + "zh-CN": + "子代理任务被中断。要继续此任务,请使用相同的 subagent_type 并设置 task_id 为 {{sessionId}} 重新调用 task 工具,子代理将从上次中断处继续执行{{detail}}", + en: "The subagent task was interrupted. To resume, call the task tool again with the same subagent_type and task_id set to {{sessionId}}; the subagent will continue from where it was interrupted{{detail}}", + }, "tool.error.unknown_agent": { "zh-CN": "未知代理类型:{{type}} 不是有效的代理类型", en: "Unknown agent type: {{type}} is not a valid agent type", @@ -2396,8 +2401,9 @@ export const messages = { en: 'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.', }, "tool.param.task_resume": { - "zh-CN": "仅在要恢复之前的任务时设置;传入之前的 task_id 可继续同一代理会话,而不是创建新会话", - en: "Set this only when resuming a previous task; pass a prior task_id to continue the same subagent session instead of creating a fresh one", + "zh-CN": + "仅在要恢复之前的任务时设置;传入之前的 task_id 可继续同一代理会话,而不是创建新会话。如果子代理被中断,传入此参数可从上次中断处继续执行", + en: "Set this only when resuming a previous task; pass a prior task_id to continue the same subagent session instead of creating a fresh one. If the subagent was interrupted, passing this will resume from where it left off", }, "tool.param.task_background": { "zh-CN": "在后台运行代理;完成后会收到通知。不要休眠、轮询或主动检查进度", diff --git a/packages/miaopan-code/src/session/prompt.ts b/packages/miaopan-code/src/session/prompt.ts index 98c79450f..0601804db 100644 --- a/packages/miaopan-code/src/session/prompt.ts +++ b/packages/miaopan-code/src/session/prompt.ts @@ -141,6 +141,7 @@ const layer = Layer.effect( cancel: (sessionID: SessionID) => cancel(sessionID), resolvePromptParts: (template: string) => resolvePromptParts(template), prompt: (input: PromptInput) => prompt(input).pipe(Effect.catch(Effect.die)), + continue: (input: LoopInput) => continueSession(input).pipe(Effect.catch(Effect.die)), } satisfies TaskPromptOps }) @@ -370,13 +371,19 @@ const layer = Layer.effect( assistantMessage.time.completed = Date.now() yield* sessions.updateMessage(assistantMessage) if (part.state.status === "running") { + const cfg = yield* config.get() + const sessionId = part.state.metadata?.sessionId as string | undefined yield* sessions.updatePart({ ...part, state: { status: "error", - error: t((yield* config.get()).language, "error.tool_cancelled"), + error: t(cfg.language, "error.tool_cancelled"), time: { start: part.state.time.start, end: Date.now() }, - metadata: part.state.metadata, + metadata: { + ...(part.state.metadata ?? {}), + interrupted: true, + ...(sessionId ? { sessionId } : {}), + }, input: part.state.input, }, } satisfies SessionV1.ToolPart) @@ -418,13 +425,23 @@ const layer = Layer.effect( } if (!result) { + const stateMetadata = + part.state.status === "pending" ? undefined : part.state.metadata + const wasInterrupted = stateMetadata?.interrupted === true + const sessionId = stateMetadata?.sessionId as string | undefined + const cfg = yield* config.get() yield* sessions.updatePart({ ...part, state: { status: "error", - error: t((yield* config.get()).language, "error.tool_execution_failed", { - detail: error ? `: ${error.message}` : "", - }), + error: wasInterrupted + ? t(cfg.language, "tool.error.subagent_interrupted", { + detail: error ? `: ${error.message}` : "", + sessionId: sessionId ?? "", + }) + : t(cfg.language, "error.tool_execution_failed", { + detail: error ? `: ${error.message}` : "", + }), time: { start: part.state.status === "running" ? part.state.time.start : Date.now(), end: Date.now(), diff --git a/packages/miaopan-code/src/tool/task.ts b/packages/miaopan-code/src/tool/task.ts index 5960cd0ce..bb2e47e0f 100644 --- a/packages/miaopan-code/src/tool/task.ts +++ b/packages/miaopan-code/src/tool/task.ts @@ -23,6 +23,7 @@ export interface TaskPromptOps { cancel(sessionID: SessionID): Effect.Effect resolvePromptParts(template: string): Effect.Effect prompt(input: SessionPrompt.PromptInput): Effect.Effect + continue(input: SessionPrompt.LoopInput): Effect.Effect } const id = "task" @@ -205,6 +206,23 @@ export const TaskTool = Tool.define( const oai = parentMessage.info.role === "user" ? parentMessage.info.oai : undefined const runTask = Effect.fn("TaskTool.runTask")(function* () { + // If resuming an existing subagent session, check whether it has a + // prior assistant message. If so, the session was interrupted and we + // should continue from where it left off instead of sending a fresh + // prompt. + const isResume = session !== undefined + if (isResume) { + const childMessages = yield* sessions.messages({ sessionID: nextSession.id }).pipe(Effect.orDie) + const hasAssistant = childMessages.some((m) => m.info.role === "assistant") + if (hasAssistant) { + const result = yield* ops.continue({ + sessionID: nextSession.id, + }) + const text = result.parts.findLast((item) => item.type === "text")?.text ?? "" + if (!builtinReview) return text + return Review.renderOutput(Review.parseOutput(text), language) + } + } const parts = yield* ops.resolvePromptParts(params.prompt) const result = yield* ops.prompt({ messageID: MessageID.ascending(), diff --git a/packages/miaopan-code/test/tool/task.test.ts b/packages/miaopan-code/test/tool/task.test.ts index 27ae0da78..0b726ef03 100644 --- a/packages/miaopan-code/test/tool/task.test.ts +++ b/packages/miaopan-code/test/tool/task.test.ts @@ -126,6 +126,10 @@ function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void; opts?.onPrompt?.(input) return reply(input, opts?.text ?? "done") }), + continue: (input) => + Effect.sync(() => + reply({ ...input, messageID: MessageID.ascending(), parts: [{ type: "text" as const, text: "continued" }] }, opts?.text ?? "continued"), + ), } } @@ -521,6 +525,11 @@ describe("tool.task", () => { ready.resolve(input) return cancelled.promise }).pipe(Effect.as(reply(input, "cancelled"))), + continue: (input) => + Effect.promise(() => { + ready.resolve({ ...input, messageID: MessageID.ascending(), parts: [{ type: "text" as const, text: "continued" }] } as SessionPrompt.PromptInput) + return cancelled.promise + }).pipe(Effect.as(reply({ ...input, messageID: MessageID.ascending(), parts: [{ type: "text" as const, text: "continued" }] } as SessionPrompt.PromptInput, "cancelled"))), } const fiber = yield* def @@ -712,6 +721,13 @@ describe("tool.task", () => { return reply(input, "background done") }) }, + continue: (input) => + Effect.gen(function* () { + runs += 1 + yield* Deferred.succeed(ready, undefined) + yield* Deferred.await(done) + return reply({ ...input, messageID: MessageID.ascending(), parts: [{ type: "text" as const, text: "continued" }] } as SessionPrompt.PromptInput, "background continued") + }), } const fiber = yield* def From b35a06080ca6effedc1d11b464f430c1ea1cc492 Mon Sep 17 00:00:00 2001 From: wzh656 Date: Mon, 13 Jul 2026 20:37:20 +0800 Subject: [PATCH 2/3] fix: only continue interrupted subagents, add tests - Check last assistant message error status instead of any assistant message, so non-interrupted sessions with new prompts still get fresh prompts via ops.prompt() - Handle missing sessionId in error message gracefully - Add tests: interrupted resume uses continue, non-interrupted resume uses prompt --- packages/miaopan-code/src/session/prompt.ts | 10 +- packages/miaopan-code/src/tool/task.ts | 12 +- packages/miaopan-code/test/tool/task.test.ts | 132 +++++++++++++++++++ 3 files changed, 144 insertions(+), 10 deletions(-) diff --git a/packages/miaopan-code/src/session/prompt.ts b/packages/miaopan-code/src/session/prompt.ts index 0601804db..0d4a96793 100644 --- a/packages/miaopan-code/src/session/prompt.ts +++ b/packages/miaopan-code/src/session/prompt.ts @@ -435,10 +435,12 @@ const layer = Layer.effect( state: { status: "error", error: wasInterrupted - ? t(cfg.language, "tool.error.subagent_interrupted", { - detail: error ? `: ${error.message}` : "", - sessionId: sessionId ?? "", - }) + ? sessionId + ? t(cfg.language, "tool.error.subagent_interrupted", { + detail: error ? `: ${error.message}` : "", + sessionId, + }) + : t(cfg.language, "error.tool_cancelled") : t(cfg.language, "error.tool_execution_failed", { detail: error ? `: ${error.message}` : "", }), diff --git a/packages/miaopan-code/src/tool/task.ts b/packages/miaopan-code/src/tool/task.ts index bb2e47e0f..793fd6d94 100644 --- a/packages/miaopan-code/src/tool/task.ts +++ b/packages/miaopan-code/src/tool/task.ts @@ -206,15 +206,15 @@ export const TaskTool = Tool.define( const oai = parentMessage.info.role === "user" ? parentMessage.info.oai : undefined const runTask = Effect.fn("TaskTool.runTask")(function* () { - // If resuming an existing subagent session, check whether it has a - // prior assistant message. If so, the session was interrupted and we - // should continue from where it left off instead of sending a fresh - // prompt. + // If resuming an existing subagent session, check whether it was + // interrupted (last assistant message has an error). If so, continue + // from where it left off instead of sending a fresh prompt. const isResume = session !== undefined if (isResume) { const childMessages = yield* sessions.messages({ sessionID: nextSession.id }).pipe(Effect.orDie) - const hasAssistant = childMessages.some((m) => m.info.role === "assistant") - if (hasAssistant) { + const lastAssistant = childMessages.findLast((m) => m.info.role === "assistant") + const wasInterrupted = lastAssistant?.info.role === "assistant" && lastAssistant.info.error !== undefined + if (wasInterrupted) { const result = yield* ops.continue({ sessionID: nextSession.id, }) diff --git a/packages/miaopan-code/test/tool/task.test.ts b/packages/miaopan-code/test/tool/task.test.ts index 0b726ef03..deec9314c 100644 --- a/packages/miaopan-code/test/tool/task.test.ts +++ b/packages/miaopan-code/test/tool/task.test.ts @@ -280,6 +280,138 @@ describe("tool.task", () => { }), ) + it.instance("execute continues an interrupted subagent via task_id", () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + const child = yield* sessions.create({ parentID: chat.id, title: "Interrupted child" }) + // Seed the child with a user message and an interrupted assistant message + const childUser = yield* sessions.updateMessage({ + id: MessageID.ascending(), + role: "user", + sessionID: child.id, + agent: "general", + model: ref, + time: { created: Date.now() }, + }) + const childAssistant: SessionV1.Assistant = { + id: MessageID.ascending(), + role: "assistant", + parentID: childUser.id, + sessionID: child.id, + mode: "general", + agent: "general", + cost: 0, + path: { cwd: "/tmp", root: "/tmp" }, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: ref.modelID, + providerID: ref.providerID, + time: { created: Date.now() }, + error: { name: "MessageAbortedError", data: { message: "Aborted" } }, + } + yield* sessions.updateMessage(childAssistant) + + const tool = yield* TaskTool + const def = yield* tool.init() + let continued = false + let prompted = false + const promptOps: TaskPromptOps = { + ...stubOps({ text: "resumed" }), + continue: () => Effect.sync(() => { continued = true; return reply({ sessionID: child.id, parts: [{ type: "text" as const, text: "continued" }] } as SessionPrompt.PromptInput, "continued") }), + prompt: () => Effect.sync(() => { prompted = true; return reply({ sessionID: child.id, parts: [{ type: "text" as const, text: "prompted" }] } as SessionPrompt.PromptInput, "prompted") }), + } + + const result = yield* def.execute( + { + description: "resume work", + prompt: "continue the previous task", + subagent_type: "general", + task_id: child.id, + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + + expect(continued).toBe(true) + expect(prompted).toBe(false) + expect(result.output).toContain("continued") + }), + ) + + it.instance("execute sends fresh prompt when resuming a non-interrupted session", () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + const child = yield* sessions.create({ parentID: chat.id, title: "Completed child" }) + // Seed the child with a user message and a completed (no error) assistant message + const childUser = yield* sessions.updateMessage({ + id: MessageID.ascending(), + role: "user", + sessionID: child.id, + agent: "general", + model: ref, + time: { created: Date.now() }, + }) + const childAssistant: SessionV1.Assistant = { + id: MessageID.ascending(), + role: "assistant", + parentID: childUser.id, + sessionID: child.id, + mode: "general", + agent: "general", + cost: 0, + path: { cwd: "/tmp", root: "/tmp" }, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: ref.modelID, + providerID: ref.providerID, + time: { created: Date.now(), completed: Date.now() }, + finish: "stop", + } + yield* sessions.updateMessage(childAssistant) + + const tool = yield* TaskTool + const def = yield* tool.init() + let continued = false + let prompted = false + const promptOps: TaskPromptOps = { + ...stubOps({ text: "fresh" }), + continue: () => Effect.sync(() => { continued = true; return reply({ sessionID: child.id, parts: [{ type: "text" as const, text: "continued" }] } as SessionPrompt.PromptInput, "continued") }), + prompt: (input) => Effect.sync(() => { prompted = true; return reply(input, "fresh") }), + } + + const result = yield* def.execute( + { + description: "new task", + prompt: "do something different", + subagent_type: "general", + task_id: child.id, + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + + expect(continued).toBe(false) + expect(prompted).toBe(true) + expect(result.output).toContain("fresh") + }), + ) + it.instance("plan mode reapplies read-only restrictions when resuming a task session", () => Effect.gen(function* () { const sessions = yield* Session.Service From 0a76e4e28a95c55d7c73240bbb44af2b990fc6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=BC=E7=95=94?= <2173679279@qq.com> Date: Tue, 14 Jul 2026 16:55:59 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(miaopan-code):=20=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E4=B8=AD=E6=96=AD=E7=9A=84=E5=AD=90=E4=BB=A3?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/miaopan-code/src/session/prompt.ts | 3 +- packages/miaopan-code/src/tool/task.ts | 18 +-- packages/miaopan-code/test/tool/task.test.ts | 154 ++++++++++++++++++- 3 files changed, 156 insertions(+), 19 deletions(-) diff --git a/packages/miaopan-code/src/session/prompt.ts b/packages/miaopan-code/src/session/prompt.ts index 0d4a96793..30b7fcf5b 100644 --- a/packages/miaopan-code/src/session/prompt.ts +++ b/packages/miaopan-code/src/session/prompt.ts @@ -425,8 +425,7 @@ const layer = Layer.effect( } if (!result) { - const stateMetadata = - part.state.status === "pending" ? undefined : part.state.metadata + const stateMetadata = part.state.status === "pending" ? undefined : part.state.metadata const wasInterrupted = stateMetadata?.interrupted === true const sessionId = stateMetadata?.sessionId as string | undefined const cfg = yield* config.get() diff --git a/packages/miaopan-code/src/tool/task.ts b/packages/miaopan-code/src/tool/task.ts index 793fd6d94..4e80de321 100644 --- a/packages/miaopan-code/src/tool/task.ts +++ b/packages/miaopan-code/src/tool/task.ts @@ -11,7 +11,7 @@ import { Agent } from "../agent/agent" import { deriveSubagentSessionPermission } from "../agent/subagent-permissions" import type { SessionPrompt } from "../session/prompt" import { Config } from "@/config/config" -import { Effect, Exit, Schema, Scope } from "effect" +import { Effect, Exit, Option, Schema, Scope } from "effect" import { EffectBridge } from "@/effect/bridge" import { RuntimeFlags } from "@/effect/runtime-flags" import { Database } from "@miaopan-code/core/database/database" @@ -206,14 +206,14 @@ export const TaskTool = Tool.define( const oai = parentMessage.info.role === "user" ? parentMessage.info.oai : undefined const runTask = Effect.fn("TaskTool.runTask")(function* () { - // If resuming an existing subagent session, check whether it was - // interrupted (last assistant message has an error). If so, continue - // from where it left off instead of sending a fresh prompt. - const isResume = session !== undefined - if (isResume) { - const childMessages = yield* sessions.messages({ sessionID: nextSession.id }).pipe(Effect.orDie) - const lastAssistant = childMessages.findLast((m) => m.info.role === "assistant") - const wasInterrupted = lastAssistant?.info.role === "assistant" && lastAssistant.info.error !== undefined + if (session) { + const lastAssistant = yield* sessions + .findMessage(nextSession.id, (message) => message.info.role === "assistant") + .pipe(Effect.orDie) + const wasInterrupted = + Option.isSome(lastAssistant) && + lastAssistant.value.info.role === "assistant" && + lastAssistant.value.info.error?.name === "MessageAbortedError" if (wasInterrupted) { const result = yield* ops.continue({ sessionID: nextSession.id, diff --git a/packages/miaopan-code/test/tool/task.test.ts b/packages/miaopan-code/test/tool/task.test.ts index deec9314c..d70300e57 100644 --- a/packages/miaopan-code/test/tool/task.test.ts +++ b/packages/miaopan-code/test/tool/task.test.ts @@ -128,7 +128,10 @@ function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void; }), continue: (input) => Effect.sync(() => - reply({ ...input, messageID: MessageID.ascending(), parts: [{ type: "text" as const, text: "continued" }] }, opts?.text ?? "continued"), + reply( + { ...input, messageID: MessageID.ascending(), parts: [{ type: "text" as const, text: "continued" }] }, + opts?.text ?? "continued", + ), ), } } @@ -317,8 +320,28 @@ describe("tool.task", () => { let prompted = false const promptOps: TaskPromptOps = { ...stubOps({ text: "resumed" }), - continue: () => Effect.sync(() => { continued = true; return reply({ sessionID: child.id, parts: [{ type: "text" as const, text: "continued" }] } as SessionPrompt.PromptInput, "continued") }), - prompt: () => Effect.sync(() => { prompted = true; return reply({ sessionID: child.id, parts: [{ type: "text" as const, text: "prompted" }] } as SessionPrompt.PromptInput, "prompted") }), + continue: () => + Effect.sync(() => { + continued = true + return reply( + { + sessionID: child.id, + parts: [{ type: "text" as const, text: "continued" }], + } as SessionPrompt.PromptInput, + "continued", + ) + }), + prompt: () => + Effect.sync(() => { + prompted = true + return reply( + { + sessionID: child.id, + parts: [{ type: "text" as const, text: "prompted" }], + } as SessionPrompt.PromptInput, + "prompted", + ) + }), } const result = yield* def.execute( @@ -383,8 +406,22 @@ describe("tool.task", () => { let prompted = false const promptOps: TaskPromptOps = { ...stubOps({ text: "fresh" }), - continue: () => Effect.sync(() => { continued = true; return reply({ sessionID: child.id, parts: [{ type: "text" as const, text: "continued" }] } as SessionPrompt.PromptInput, "continued") }), - prompt: (input) => Effect.sync(() => { prompted = true; return reply(input, "fresh") }), + continue: () => + Effect.sync(() => { + continued = true + return reply( + { + sessionID: child.id, + parts: [{ type: "text" as const, text: "continued" }], + } as SessionPrompt.PromptInput, + "continued", + ) + }), + prompt: (input) => + Effect.sync(() => { + prompted = true + return reply(input, "fresh") + }), } const result = yield* def.execute( @@ -412,6 +449,85 @@ describe("tool.task", () => { }), ) + it.instance("execute sends fresh prompt when the previous subagent failed without interruption", () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + const child = yield* sessions.create({ parentID: chat.id, title: "Failed child" }) + const childUser = yield* sessions.updateMessage({ + id: MessageID.ascending(), + role: "user", + sessionID: child.id, + agent: "general", + model: ref, + time: { created: Date.now() }, + }) + yield* sessions.updateMessage({ + id: MessageID.ascending(), + role: "assistant", + parentID: childUser.id, + sessionID: child.id, + mode: "general", + agent: "general", + cost: 0, + path: { cwd: "/tmp", root: "/tmp" }, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: ref.modelID, + providerID: ref.providerID, + time: { created: Date.now(), completed: Date.now() }, + error: { name: "ProviderAuthError", data: { providerID: ref.providerID, message: "Failed" } }, + } satisfies SessionV1.Assistant) + + const tool = yield* TaskTool + const def = yield* tool.init() + let continued = false + let prompted = false + const promptOps: TaskPromptOps = { + ...stubOps({ text: "fresh" }), + continue: (input) => { + continued = true + return Effect.succeed( + reply( + { + ...input, + messageID: MessageID.ascending(), + parts: [{ type: "text", text: "continued" }], + }, + "continued", + ), + ) + }, + prompt: (input) => { + prompted = true + return Effect.succeed(reply(input, "fresh")) + }, + } + + const result = yield* def.execute( + { + description: "retry failed work", + prompt: "try a different approach", + subagent_type: "general", + task_id: child.id, + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + + expect(continued).toBe(false) + expect(prompted).toBe(true) + expect(result.output).toContain("fresh") + }), + ) + it.instance("plan mode reapplies read-only restrictions when resuming a task session", () => Effect.gen(function* () { const sessions = yield* Session.Service @@ -659,9 +775,24 @@ describe("tool.task", () => { }).pipe(Effect.as(reply(input, "cancelled"))), continue: (input) => Effect.promise(() => { - ready.resolve({ ...input, messageID: MessageID.ascending(), parts: [{ type: "text" as const, text: "continued" }] } as SessionPrompt.PromptInput) + ready.resolve({ + ...input, + messageID: MessageID.ascending(), + parts: [{ type: "text" as const, text: "continued" }], + } as SessionPrompt.PromptInput) return cancelled.promise - }).pipe(Effect.as(reply({ ...input, messageID: MessageID.ascending(), parts: [{ type: "text" as const, text: "continued" }] } as SessionPrompt.PromptInput, "cancelled"))), + }).pipe( + Effect.as( + reply( + { + ...input, + messageID: MessageID.ascending(), + parts: [{ type: "text" as const, text: "continued" }], + } as SessionPrompt.PromptInput, + "cancelled", + ), + ), + ), } const fiber = yield* def @@ -858,7 +989,14 @@ describe("tool.task", () => { runs += 1 yield* Deferred.succeed(ready, undefined) yield* Deferred.await(done) - return reply({ ...input, messageID: MessageID.ascending(), parts: [{ type: "text" as const, text: "continued" }] } as SessionPrompt.PromptInput, "background continued") + return reply( + { + ...input, + messageID: MessageID.ascending(), + parts: [{ type: "text" as const, text: "continued" }], + } as SessionPrompt.PromptInput, + "background continued", + ) }), }