Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

## Unreleased

## 2.0.17 - 2026-09-21

Dashboard keeps failed and interrupted drafts in the conversation, including after refresh. Completed turns keep the thinking-process fold. Interrupted subagents store completed tool calls and visible drafts in the task record for later lookup, without auto-waking the parent.

## 2.0.16 - 2026-09-18

Dashboard session search, cheap-model titles, and observational vision/thinking probes. Leftover project agent routes no longer block startup. Settings uses the saved API key for probes and a 64×64 image instead of a 1×1 PNG. Candidate release; GitHub tag pending.
Published 2026-09-18: [v2.0.16](https://github.com/wicm84266964/Ant-Code/releases/tag/v2.0.16).

Dashboard session search, cheap-model titles, and observational vision/thinking probes. Leftover project agent routes no longer block startup. Settings uses the saved API key for probes and a 64×64 image instead of a 1×1 PNG.

## 2.0.15 - 2026-09-18

Expand Down
37 changes: 37 additions & 0 deletions docs/releases/2.0.17-interrupt-draft-recovery_zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Ant Code v2.0.17:失败与中断后保留草稿、思考过程和子任务进展

发布日期:2026-09-21

本版本是 Dashboard 与子智能体恢复小版本。网关失败或上游断流时,对话区立即显示报错,并把已经流出的可见草稿写入会话;正常完成的一轮会保留「思考过程」折叠条。子智能体被停止或断流时,已完成的工具调用和可见草稿写入任务记录,主控稍后查阅即可捡回。候选阶段不提前声称 CI、tag 或 GitHub Release 已经完成。

## 对用户工作流的直接变化

- Dashboard 模型请求失败时,错误会出现在对话区,不必整页刷新。
- 第一个会话如果失败,刷新后仍打开该会话,不再回到「我将全力配合你的工作」空首页。
- 上游中途断开时,已经贴出的可见草稿留在对话里,并标成「已中断 · 非最终回复」;刷新后仍在。
- 正常最终回复到达后,「思考过程」折叠条与最终回复一起保留,刷新后两者都还在。
- 子智能体中断或断流时,不自动唤醒主控;任务记录里保留已完成工具和可见草稿,可用任务详情或 `/agents continue <task-id>` 继续。
- 子智能体发给网关的请求改为流式,便于在断流时截取可见草稿。内部 thinking 正文仍不作为可见汇报展开。

## 配置、数据与安全边界

- 不改变权限模式、凭据存储格式或网关配置格式。
- 会话 transcript 会多出中断草稿和思考过程条目;现有 session 可继续打开,无需重建。
- 子智能体任务记录会写入中断时的 output、toolCalls 和 continuationPrompt。
- 自动化测试不能代表所有私有研究数据或每一个外部模型供应商都已验证。

## 升级与产物位置

```sh
npm ci
npm run verify:install
npm link
ant-code --version
```

`ant-code --version` 应显示 `2.0.17`。正在运行的 Dashboard 需要重启并硬刷新浏览器。现有网关配置和 `.lab-agent` 会话不必重建。

## 验证、限制与待完成门禁

- 开发仓类型检查 0 诊断。本轮相关单元测试(Dashboard UI、会话失败落盘、子智能体中断保留)已通过。候选阶段的完整 `verify:release`、正式仓 CI、tag 和 GitHub Release 完成后才会记入发布记录。
- 已知限制:模型内部 thinking 正文默认不在 Dashboard 展开;子智能体中断不自动唤醒主控。最终结论以正式仓同一提交的 CI 和 Release 门禁为准。
4 changes: 2 additions & 2 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ant-code/cli",
"version": "2.0.16",
"version": "2.0.17",
"description": "Ant Code local coding agent development package.",
"type": "module",
"private": true,
Expand Down
87 changes: 87 additions & 0 deletions src/agents/continuation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,93 @@ export function createPartialSubagentResult(options: {
};
}

export function createInterruptedSubagentResult(options: {
profile: { name?: string; mode?: unknown; [key: string]: unknown };
query: string;
reason?: { code?: string; message?: string; [key: string]: unknown };
tools?: ContinuationTool[];
draftText?: unknown;
draftThinkingBytes?: unknown;
model?: string;
mode?: string;
interrupted?: boolean;
}) {
const interrupted = options.interrupted !== false;
const completed = summarizeCompletedTools(options.tools ?? []);
const draftText = String(options.draftText ?? "").trim();
const thinkingBytes = Number(options.draftThinkingBytes);
const reason = options.reason && typeof options.reason === "object"
? options.reason
: { code: "AGENT_INTERRUPTED", message: "Subagent was interrupted." };
const message = String(reason.message ?? "Subagent was interrupted.");
const continuationPrompt = [
`继续这个 ${options.profile.name} 子任务。`,
"",
"原始任务:",
options.query,
"",
"中断原因:",
message,
"",
"已完成摘要:",
completed.length ? completed.map((item) => `- ${item}`).join("\n") : "- 尚无成功工具结果。",
"",
draftText ? "中断时已有可见草稿,请在此基础上继续,不要从头重做。" : "中断时没有可见正文草稿。",
"",
"请从 remaining 工作继续,避免重复已完成步骤,并返回结构化阶段结果。"
].join("\n");
const output = formatInterruptedOutput({
message,
completed,
draftText,
thinkingBytes: Number.isFinite(thinkingBytes) && thinkingBytes > 0 ? thinkingBytes : 0
});
return {
ok: false,
interrupted,
status: interrupted ? "interrupted" : "failed",
profile: options.profile.name,
mode: options.mode ?? options.profile.mode,
modelDriven: true,
model: options.model,
query: options.query,
output,
outputFull: output,
outputTruncated: output.length > MAX_PARTIAL_OUTPUT,
continuationPrompt,
tools: options.tools ?? [],
error: {
code: String(reason.code ?? "AGENT_INTERRUPTED"),
message
}
};
}

function formatInterruptedOutput(options: {
message: string;
completed: string[];
draftText: string;
thinkingBytes: number;
}) {
const text = [
"子智能体已中断",
"",
`原因:${options.message}`,
"",
"已完成工具:",
...(options.completed.length ? options.completed.map((item) => `- ${item}`) : ["- 尚无成功工具结果。"]),
"",
options.draftText
? `中断草稿(非最终回复):\n${options.draftText}`
: options.thinkingBytes > 0
? `中断草稿:无可见正文。内部思考已产生 ${options.thinkingBytes} 字节,未作为可见正文保留。`
: "中断草稿:无可见正文。",
"",
"主控稍后读取该任务记录即可捡回上述内容。可用 /agents continue <task-id> 继续。"
].join("\n");
return text.length <= MAX_PARTIAL_OUTPUT ? text : `${text.slice(0, MAX_PARTIAL_OUTPUT)}\n...[interrupted output truncated]`;
}

function summarizeCompletedTools(tools: ContinuationTool[] = []) {
return tools
.filter((tool) => tool.ok === true)
Expand Down
105 changes: 83 additions & 22 deletions src/agents/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { suggestValidationCommands } from "../core/validation-suggestions.ts";
import { accumulateProviderUsage } from "../core/provider-usage.ts";
import { createBudgetTracker, checkBudget, recordBudgetToolResult, resolveAgentBudget, resolveAgentModel } from "./budget.ts";
import { buildContextPack, formatContextPack, hasWriteScope } from "./context-pack.ts";
import { createPartialSubagentResult } from "./continuation.ts";
import { createInterruptedSubagentResult, createPartialSubagentResult } from "./continuation.ts";
import { formatOutputContract, summarizeContractResult } from "./contracts.ts";
import { createPlanPackageStore, extractPlanPackage } from "./plan-package-store.ts";
import { getAgentProfile, listAgentProfileLabels, type AgentProfile } from "./profiles.ts";
Expand Down Expand Up @@ -335,7 +335,7 @@ export async function runSubagent(options: {
finishedAt: new Date().toISOString(),
latestProgress: persistedPlanPackage?.ok
? `子智能体已完成;计划包已保存到 ${persistedPlanPackage.path}`
: result.partial ? "子智能体阶段性暂停,可继续" : result.ok ? "子智能体已完成" : (isPlainObject(result.error) ? String(result.error.message ?? "子智能体失败") : "子智能体失败"),
: result.partial ? "子智能体阶段性暂停,可继续" : result.ok ? "子智能体已完成" : result.interrupted ? "子智能体已中断,已保留工具记录和草稿" : (isPlainObject(result.error) ? String(result.error.message ?? "子智能体失败") : "子智能体失败"),
toolCalls: result.tools ?? [],
outputSummary: summarizeAgentOutput(result),
output: persistedOutput?.preview ?? result.output ?? "",
Expand Down Expand Up @@ -513,6 +513,11 @@ async function runModelSubagent(options: {
let finalReportRequested = false;
let lastPromptProgress: Record<string, unknown> = {};
let providerUsageProgress: Record<string, unknown> = {};
const draftCapture = {
text: "",
thinking: "",
thinkingBytes: 0
};

try {
for (let round = 0; ; round += 1) {
Expand Down Expand Up @@ -541,12 +546,10 @@ async function runModelSubagent(options: {
});
}
if (options.signal?.aborted) {
return {
ok: false,
profile: options.profile.name,
interrupted: true,
error: { code: "AGENT_INTERRUPTED", message: "Subagent was interrupted." }
};
return haltedSubagentResult(options, toolExecutions, draftCapture, {
code: "AGENT_INTERRUPTED",
message: "Subagent was interrupted."
}, true);
}

const promptEstimate = estimatePromptPayload({
Expand All @@ -567,20 +570,30 @@ async function runModelSubagent(options: {
})
});

draftCapture.text = "";
draftCapture.thinking = "";
draftCapture.thinkingBytes = 0;
const response = await options.gateway.sendChat({
messages,
tools: finalReportRequested ? [] : toolDefinitions,
toolResults,
sessionId,
stream: false,
signal: options.signal
stream: true,
signal: options.signal,
onEvent: (event) => captureSubagentDraftEvent(draftCapture, event)
});

if (!response.ok) {
const interrupted = options.signal?.aborted === true || isSubagentStreamHalt(response.error);
await options.taskStore?.updateTask(options.taskId, {
latestProgress: response.error?.message ?? "模型请求失败",
latestProgress: interrupted
? "子智能体已中断,已保留工具记录和草稿"
: (response.error?.message ?? "模型请求失败"),
error: response.error ?? null
});
if (interrupted || toolExecutions.length > 0 || draftCapture.text.trim() || draftCapture.thinkingBytes > 0) {
return haltedSubagentResult(options, toolExecutions, draftCapture, response.error, interrupted);
}
return {
ok: false,
profile: options.profile.name,
Expand Down Expand Up @@ -792,14 +805,10 @@ async function runModelSubagent(options: {
});
}
if (options.signal?.aborted) {
return {
ok: false,
profile: options.profile.name,
mode: options.profile.mode,
interrupted: true,
error: { code: "AGENT_INTERRUPTED", message: "Subagent was interrupted." },
tools: toolExecutions
};
return haltedSubagentResult(options, toolExecutions, draftCapture, {
code: "AGENT_INTERRUPTED",
message: "Subagent was interrupted."
}, true);
}
if (finalReportReason && !finalReportRequested && shouldRequestFinalReportAfterBudgetHit(options.profile, finalReportReason)) {
finalReportRequested = true;
Expand Down Expand Up @@ -970,7 +979,7 @@ function summarizeAgentOutput(result: Record<string, unknown> | SubagentResult)
if (!result) {
return "";
}
if (result.ok && result.output) {
if (result.output) {
return truncateText(String(result.output)).split(/\r?\n/).slice(0, 8).join("\n");
}
if (result.summary) {
Expand All @@ -980,6 +989,53 @@ function summarizeAgentOutput(result: Record<string, unknown> | SubagentResult)
return asString(error?.message) ?? "";
}

function haltedSubagentResult(
options: {
profile: AgentProfile;
query: string;
config: LabAgentConfig;
},
tools: Array<Record<string, unknown>>,
draft: { text: string; thinkingBytes: number },
error: unknown,
interrupted: boolean
) {
const record = isPlainObject(error) ? error : EMPTY_RECORD;
return createInterruptedSubagentResult({
profile: options.profile,
query: options.query,
reason: {
code: String(record.code ?? (interrupted ? "AGENT_INTERRUPTED" : "AGENT_HALTED")),
message: String(record.message ?? (interrupted ? "Subagent was interrupted." : "Subagent stopped before a final report."))
},
tools,
draftText: draft.text,
draftThinkingBytes: draft.thinkingBytes,
model: options.config.modelAlias,
mode: asString(options.profile.mode),
interrupted
});
}

function captureSubagentDraftEvent(capture: { text: string; thinking: string; thinkingBytes: number }, event: Record<string, unknown>) {
if (event.type === "text_delta" && typeof event.text === "string" && event.text) {
capture.text += event.text;
return;
}
if (event.type === "thinking_delta" && typeof event.text === "string" && event.text) {
capture.thinking += event.text;
capture.thinkingBytes += Number(event.bytes ?? Buffer.byteLength(event.text, "utf8"));
}
}

function isSubagentStreamHalt(error: unknown) {
const code = String(isPlainObject(error) ? error.code ?? "" : "");
return code === "AGENT_INTERRUPTED"
|| code === "GATEWAY_STREAM_INTERRUPTED"
|| code === "UPSTREAM_STREAM_ABORTED"
|| code === "GATEWAY_RESPONSE_PARSE_ERROR";
}

async function emitSubagentHook(
options: { cwd: string; env?: NodeJS.ProcessEnv; hooksTrusted?: boolean; parentSessionId?: string },
config: unknown,
Expand Down Expand Up @@ -1421,13 +1477,18 @@ function formatSubagentOutput(value: unknown) {
}

async function persistResultOutput(taskStore: ReturnType<typeof createAgentTaskStore> | undefined, taskId: string, result: SubagentResult) {
if (!result || typeof result.outputFull !== "string" || result.outputFull.length === 0) {
const full = typeof result.outputFull === "string" && result.outputFull
? result.outputFull
: (result.interrupted === true || result.partial === true) && typeof result.output === "string"
? result.output
: "";
if (!full) {
return null;
}
if (typeof taskStore?.writeTaskOutput !== "function") {
return null;
}
return taskStore.writeTaskOutput(taskId, result.outputFull);
return taskStore.writeTaskOutput(taskId, full);
}

async function persistPlannerPlanPackage(options: {
Expand Down
3 changes: 3 additions & 0 deletions src/core/session-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,9 @@ export function messagesForModelContext(messages: unknown = []): SessionMessage[
if (!message || typeof message !== "object") {
return [];
}
if ("thinkingProcess" in message && message.thinkingProcess === true) {
return [];
}
const { interruptedDraft: _interruptedDraft, attachments: _attachments, ...rest } = message;
return [rest];
});
Expand Down
Loading
Loading