c0de-agent 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/config.js +4 -1
- package/dist/core/loop/compaction.d.ts +31 -0
- package/dist/core/loop/compaction.js +137 -0
- package/dist/core/loop/persist.d.ts +11 -0
- package/dist/core/loop/persist.js +107 -0
- package/dist/core/loop/segment.d.ts +8 -0
- package/dist/core/loop/segment.js +72 -0
- package/dist/core/loop/stream-collect.d.ts +47 -0
- package/dist/core/loop/stream-collect.js +183 -0
- package/dist/core/loop/subagent.d.ts +12 -0
- package/dist/core/loop/subagent.js +171 -0
- package/dist/core/loop/todo.d.ts +5 -0
- package/dist/core/loop/todo.js +26 -0
- package/dist/core/loop.d.ts +2 -20
- package/dist/core/loop.js +12 -658
- package/dist/core/prompt-registry.d.ts +1 -1
- package/dist/core/prompt-registry.js +10 -0
- package/dist/core/slash.js +1 -1
- package/dist/core/todo-tags.d.ts +46 -0
- package/dist/core/todo-tags.js +192 -0
- package/dist/core/workflows/builtins.js +4 -4
- package/dist/core/workflows/discovery.js +4 -1
- package/dist/llm/registry.d.ts +30 -3
- package/dist/llm/registry.js +25 -7
- package/dist/llm/retry.d.ts +4 -2
- package/dist/llm/retry.js +7 -6
- package/dist/llm/schema/errors.d.ts +13 -3
- package/dist/llm/schema/errors.js +35 -3
- package/dist/llm/transport.js +5 -1
- package/dist/project/resolve.js +2 -2
- package/dist/server/routes/chat.js +8 -2
- package/dist/server/routes/todo.js +5 -2
- package/dist/server/server.d.ts +4 -2
- package/dist/server/server.js +20 -12
- package/dist/session/squash.js +31 -24
- package/dist/shared/types/agent.d.ts +11 -0
- package/dist/tools/builtin/todo.d.ts +7 -0
- package/dist/tools/builtin/todo.js +23 -16
- package/package.json +2 -1
|
@@ -3,7 +3,7 @@ declare const ROLE_DESCRIPTION = "You are c0de-agent, an open-source AI coding a
|
|
|
3
3
|
declare const ENGINEERING_PRINCIPLES = "# Engineering Principles\n- Optimize for correctness first, then for the next maintainer six months out.\n- You have agency and taste: delete code that isn't pulling its weight, refuse unnecessary abstractions, prefer boring when it's called for.\n- Consider what code compiles to. Never allocate avoidably; no needless copies or computation.\n- You are not alone in this repo. Treat unexpected changes as the user's work and adapt \u2014 never revert edits you did not make unless explicitly asked.";
|
|
4
4
|
declare const TOOL_USAGE = "# Tool Usage\nYou have dedicated tools \u2014 prefer them over shell commands for file operations:\n- Reading a file or listing a directory \u2192 `read` (NOT `cat`, `head`, `tail`).\n- Finding files by name or pattern \u2192 `glob` (NOT `find`, `ls -R`).\n- Searching file contents \u2192 `grep` (NOT shell `grep`/`rg`/`ack`, NOT `awk`/`sed`).\n- Modifying files \u2192 `edit`/`write` (NOT `sed`, `echo >`, heredocs).\n\nReserve `bash` for genuine command execution: builds, tests, git, or short pipelines that compute a fact (`wc -l`, `git status`, `diff`, a checksum). Never explore a codebase with `find`/`ls`/`cat` when `read`/`glob`/`grep` can.\n\nBatch independent tool calls in a single response \u2014 parallelize file reads and independent lookups. Only sequence when one call's result informs the next.\n\nWhen referencing code, use the `file_path:line_number` pattern so the user can navigate directly.";
|
|
5
5
|
declare const CODEBASE = "# Working with the Codebase\nBefore changing files, understand the existing conventions. Mimic code style, reuse existing utilities, and follow established patterns.\n- NEVER assume a library is available, even if well known. Check package.json (or equivalent) and neighboring files first.\n- When creating a new component, look at existing ones first for naming, typing, and framework conventions.\n- When editing, read the surrounding context (especially imports) to make the change idiomatic. Never introduce code that exposes or logs secrets.";
|
|
6
|
-
declare const EXECUTION_WORKFLOW = "# Execution Workflow\n1. Scope \u2014 plan before touching files; research existing code and conventions.\n2. Research \u2014 read sections, not snippets. Reuse existing patterns; a second convention beside an existing one is prohibited.\n3. Decompose \u2014 break multi-step work into steps and track them with the `todo` tool; skip for trivial requests. Plan only what makes the request work.\n4. Implement \u2014 fix problems at the source. Remove obsolete code \u2014 no leftover comments, aliases, or re-exports. Prefer editing existing files over new ones.\n5. Verify \u2014 never yield non-trivial work without proof: run the relevant tests. Prefer testing behavior, not plumbing. Don't test defaults.\n6. Cleanup \u2014 changelog, tests, docs, and removing scaffolding are the LAST phase, gated on the request demonstrably working. Never pre-plan cleanup before the request works.";
|
|
6
|
+
declare const EXECUTION_WORKFLOW = "# Execution Workflow\n1. Scope \u2014 plan before touching files; research existing code and conventions.\n2. Research \u2014 read sections, not snippets. Reuse existing patterns; a second convention beside an existing one is prohibited.\n3. Decompose \u2014 break multi-step work into steps and track them with the `todo` tool; skip for trivial requests. Plan only what makes the request work.\n\nYou can also manage todos inline via tags in your text output (saves a tool-call round-trip). Tags use `phase-task` sequence numbers visible in the todo summary (e.g. `1-2` = phase 1, task 2; `1` = all tasks in phase 1). Operations:\n - `<todo:init><todo:phase name=\"Name\"><todo:item>Task</todo:item></todo:phase></todo:init>` \u2014 create/replace the list\n - `<todo:start seq=\"1-1\" />` \u2014 mark task in progress (task-level seq only)\n - `<todo:done seq=\"1-2\" />` \u2014 complete a task (or entire phase: `seq=\"1\"`)\n - `<todo:drop seq=\"2-1\" />` \u2014 abandon a task\n - `<todo:rm seq=\"1-1\" />` \u2014 remove a task\n - `<todo:append phase=\"1\"><todo:item>New</todo:item></todo:append>` \u2014 add tasks (phase is 1-based index)\n - `<todo:view />` \u2014 request current state\nTags remain visible in your output text; they are not stripped. Both tags and the todo tool update the same state \u2014 use whichever is more convenient.\n4. Implement \u2014 fix problems at the source. Remove obsolete code \u2014 no leftover comments, aliases, or re-exports. Prefer editing existing files over new ones.\n5. Verify \u2014 never yield non-trivial work without proof: run the relevant tests. Prefer testing behavior, not plumbing. Don't test defaults.\n6. Cleanup \u2014 changelog, tests, docs, and removing scaffolding are the LAST phase, gated on the request demonstrably working. Never pre-plan cleanup before the request works.";
|
|
7
7
|
declare const VERIFICATION = "# Verification & Evidence\n- Never yield non-trivial work without proof: tests, builds, or QA.\n- Run lint and typecheck after changes if the project provides them.\n- Every claim about code, tools, or tests must be grounded. Mark anything not directly observed as [INFERENCE].\n- Verification claims must match what was actually exercised. A passing typecheck does not prove an integration.";
|
|
8
8
|
declare const DELIVERY_CONTRACT = "# Delivery Contract\n- \"Done\" means the deliverable behaves as specified end-to-end \u2014 not that a scaffold compiles.\n- Never yield unless complete. A phase boundary is never a yield point.\n- Never suppress tests to make code pass. Never fabricate outputs.\n- Never substitute an easier problem: don't infer extra scope, don't treat the symptom unless asked.\n- Never ship stubs, placeholders, mocks, no-ops, or fake fallbacks as finished work.\n- Default to clean cutover: migrate every caller; leave no shims or aliases.";
|
|
9
9
|
declare const GIT_SAFETY = "# Git & Safety\n- NEVER commit unless the user explicitly asks. Committing is too proactive.\n- NEVER use `git reset --hard` or `git checkout --` unless explicitly approved.\n- Do not amend a commit unless asked.\n- You may be in a dirty worktree. NEVER revert changes you did not make; if unrelated changes conflict with your task, stop and ask.";
|
|
@@ -38,6 +38,16 @@ const EXECUTION_WORKFLOW = `# Execution Workflow
|
|
|
38
38
|
1. Scope — plan before touching files; research existing code and conventions.
|
|
39
39
|
2. Research — read sections, not snippets. Reuse existing patterns; a second convention beside an existing one is prohibited.
|
|
40
40
|
3. Decompose — break multi-step work into steps and track them with the \`todo\` tool; skip for trivial requests. Plan only what makes the request work.
|
|
41
|
+
|
|
42
|
+
You can also manage todos inline via tags in your text output (saves a tool-call round-trip). Tags use \`phase-task\` sequence numbers visible in the todo summary (e.g. \`1-2\` = phase 1, task 2; \`1\` = all tasks in phase 1). Operations:
|
|
43
|
+
- \`<todo:init><todo:phase name="Name"><todo:item>Task</todo:item></todo:phase></todo:init>\` — create/replace the list
|
|
44
|
+
- \`<todo:start seq="1-1" />\` — mark task in progress (task-level seq only)
|
|
45
|
+
- \`<todo:done seq="1-2" />\` — complete a task (or entire phase: \`seq="1"\`)
|
|
46
|
+
- \`<todo:drop seq="2-1" />\` — abandon a task
|
|
47
|
+
- \`<todo:rm seq="1-1" />\` — remove a task
|
|
48
|
+
- \`<todo:append phase="1"><todo:item>New</todo:item></todo:append>\` — add tasks (phase is 1-based index)
|
|
49
|
+
- \`<todo:view />\` — request current state
|
|
50
|
+
Tags remain visible in your output text; they are not stripped. Both tags and the todo tool update the same state — use whichever is more convenient.
|
|
41
51
|
4. Implement — fix problems at the source. Remove obsolete code — no leftover comments, aliases, or re-exports. Prefer editing existing files over new ones.
|
|
42
52
|
5. Verify — never yield non-trivial work without proof: run the relevant tests. Prefer testing behavior, not plumbing. Don't test defaults.
|
|
43
53
|
6. Cleanup — changelog, tests, docs, and removing scaffolding are the LAST phase, gated on the request demonstrably working. Never pre-plan cleanup before the request works.`;
|
package/dist/core/slash.js
CHANGED
|
@@ -132,7 +132,7 @@ const workflowCommand = {
|
|
|
132
132
|
// registry 是 server 单例(含 builtin + global + server-cwd),不含其他项目的工作流。
|
|
133
133
|
const projectWorkflows = await discoverWorkflows(ctx.cwd);
|
|
134
134
|
const projectByName = new Map(projectWorkflows.map((w) => [w.meta.name, w]));
|
|
135
|
-
const resolveEntry = (name) => registry
|
|
135
|
+
const resolveEntry = (name) => registry?.get(name) ?? projectByName.get(name);
|
|
136
136
|
if (subcommand === 'list') {
|
|
137
137
|
// 合并 registry + 项目级(去重:同名项目级覆盖)
|
|
138
138
|
const byName = new Map(registry.list().map((w) => [w.meta.name, w]));
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type TodoItem, type TodoPhase } from '../tools/builtin/todo.js';
|
|
2
|
+
/** A parsed todo tag, before seq resolution. */
|
|
3
|
+
type ParsedTodoTag = {
|
|
4
|
+
op: 'init';
|
|
5
|
+
phases: {
|
|
6
|
+
name: string;
|
|
7
|
+
items: string[];
|
|
8
|
+
}[];
|
|
9
|
+
} | {
|
|
10
|
+
op: 'start';
|
|
11
|
+
seq: string | undefined;
|
|
12
|
+
} | {
|
|
13
|
+
op: 'done';
|
|
14
|
+
seq: string | undefined;
|
|
15
|
+
} | {
|
|
16
|
+
op: 'drop';
|
|
17
|
+
seq: string | undefined;
|
|
18
|
+
} | {
|
|
19
|
+
op: 'rm';
|
|
20
|
+
seq: string | undefined;
|
|
21
|
+
} | {
|
|
22
|
+
op: 'append';
|
|
23
|
+
phaseSeq: number;
|
|
24
|
+
items: string[];
|
|
25
|
+
} | {
|
|
26
|
+
op: 'view';
|
|
27
|
+
};
|
|
28
|
+
/** Result of applying todo tags to phases. */
|
|
29
|
+
type ApplyTodoTagsResult = {
|
|
30
|
+
phases: TodoPhase[];
|
|
31
|
+
errors: string[];
|
|
32
|
+
hasView: boolean;
|
|
33
|
+
};
|
|
34
|
+
/** Parse all todo tags from text, preserving document order. */
|
|
35
|
+
export declare function parseTodoTags(text: string): ParsedTodoTag[];
|
|
36
|
+
/** Resolve a seq string ("1-2" or "1") to a phase and/or task.
|
|
37
|
+
* Returns undefined if out of bounds or unparseable. */
|
|
38
|
+
export declare function resolveSeq(phases: TodoPhase[], seq: string | undefined): {
|
|
39
|
+
phase: TodoPhase;
|
|
40
|
+
task?: TodoItem;
|
|
41
|
+
} | undefined;
|
|
42
|
+
/** Parse todo tags from text, resolve all seqs against the original snapshot,
|
|
43
|
+
* then apply sequentially. Tags are applied in document order.
|
|
44
|
+
* Does NOT mutate the input phases (clones first). */
|
|
45
|
+
export declare function applyTodoTags(phases: TodoPhase[], text: string): ApplyTodoTagsResult;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// todo-tags: parse <todo:*> tags from assistant text and apply them to todo phases.
|
|
2
|
+
// Acts as a format-conversion layer on top of todo.ts's applyParams state machine.
|
|
3
|
+
// Tags are NOT stripped from the text — they remain visible to the user.
|
|
4
|
+
import { applyParams, clonePhases, } from '../tools/builtin/todo.js';
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// Tag parser
|
|
7
|
+
// =============================================================================
|
|
8
|
+
/** Unified regex matching self-closing and container todo tags.
|
|
9
|
+
* Group 1 = tag name, Group 2 = attributes, Group 3 = inner content (undefined for self-closing).
|
|
10
|
+
* Backreference \1 ensures container close tag matches open tag. */
|
|
11
|
+
const TAG_RE = /<todo:(\w+)([^>]*?)(?:\/>|>([\s\S]*?)<\/todo:\1>)/g;
|
|
12
|
+
/** Parse <todo:item> tags from inner content. */
|
|
13
|
+
function parseItems(inner) {
|
|
14
|
+
const items = [];
|
|
15
|
+
const itemRe = /<todo:item>([\s\S]*?)<\/todo:item>/g;
|
|
16
|
+
for (const m of inner.matchAll(itemRe)) {
|
|
17
|
+
items.push((m[1] ?? '').trim());
|
|
18
|
+
}
|
|
19
|
+
return items;
|
|
20
|
+
}
|
|
21
|
+
/** Parse <todo:phase> blocks from init inner content. */
|
|
22
|
+
function parseInitInner(inner) {
|
|
23
|
+
const phases = [];
|
|
24
|
+
const phaseRe = /<todo:phase\s+name="([^"]+)">([\s\S]*?)<\/todo:phase>/g;
|
|
25
|
+
for (const m of inner.matchAll(phaseRe)) {
|
|
26
|
+
phases.push({ name: (m[1] ?? '').trim(), items: parseItems(m[2] ?? '') });
|
|
27
|
+
}
|
|
28
|
+
// Flat items without phase wrapper → default phase
|
|
29
|
+
if (phases.length === 0) {
|
|
30
|
+
const flat = parseItems(inner);
|
|
31
|
+
if (flat.length > 0)
|
|
32
|
+
phases.push({ name: 'Tasks', items: flat });
|
|
33
|
+
}
|
|
34
|
+
return phases;
|
|
35
|
+
}
|
|
36
|
+
/** Parse all todo tags from text, preserving document order. */
|
|
37
|
+
export function parseTodoTags(text) {
|
|
38
|
+
const tags = [];
|
|
39
|
+
for (const m of text.matchAll(TAG_RE)) {
|
|
40
|
+
const op = m[1] ?? '';
|
|
41
|
+
const attrs = m[2] ?? '';
|
|
42
|
+
const inner = m[3];
|
|
43
|
+
// Skip structural tags parsed as part of init/append
|
|
44
|
+
if (op === 'phase' || op === 'item')
|
|
45
|
+
continue;
|
|
46
|
+
switch (op) {
|
|
47
|
+
case 'init':
|
|
48
|
+
tags.push({ op: 'init', phases: parseInitInner(inner ?? '') });
|
|
49
|
+
break;
|
|
50
|
+
case 'append': {
|
|
51
|
+
const phaseMatch = /phase="([^"]+)"/.exec(attrs);
|
|
52
|
+
const phaseSeq = phaseMatch ? Number.parseInt(phaseMatch[1] ?? '', 10) : Number.NaN;
|
|
53
|
+
tags.push({ op: 'append', phaseSeq, items: parseItems(inner ?? '') });
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case 'start':
|
|
57
|
+
case 'done':
|
|
58
|
+
case 'drop':
|
|
59
|
+
case 'rm': {
|
|
60
|
+
const seqMatch = /seq="([^"]+)"/.exec(attrs);
|
|
61
|
+
tags.push({ op, seq: seqMatch?.[1]?.trim() });
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case 'view':
|
|
65
|
+
tags.push({ op: 'view' });
|
|
66
|
+
break;
|
|
67
|
+
// Unknown tag names are silently ignored
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return tags;
|
|
71
|
+
}
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// Seq resolver
|
|
74
|
+
// =============================================================================
|
|
75
|
+
/** Resolve a seq string ("1-2" or "1") to a phase and/or task.
|
|
76
|
+
* Returns undefined if out of bounds or unparseable. */
|
|
77
|
+
export function resolveSeq(phases, seq) {
|
|
78
|
+
if (!seq)
|
|
79
|
+
return undefined;
|
|
80
|
+
const parts = seq.split('-').map((s) => Number.parseInt(s.trim(), 10));
|
|
81
|
+
const phaseNum = parts[0];
|
|
82
|
+
if (phaseNum === undefined || Number.isNaN(phaseNum))
|
|
83
|
+
return undefined;
|
|
84
|
+
const phaseIdx = phaseNum - 1;
|
|
85
|
+
if (phaseIdx < 0 || phaseIdx >= phases.length)
|
|
86
|
+
return undefined;
|
|
87
|
+
const phase = phases[phaseIdx];
|
|
88
|
+
if (!phase)
|
|
89
|
+
return undefined;
|
|
90
|
+
if (parts.length === 1)
|
|
91
|
+
return { phase };
|
|
92
|
+
const taskNum = parts[1];
|
|
93
|
+
if (taskNum === undefined || Number.isNaN(taskNum))
|
|
94
|
+
return undefined;
|
|
95
|
+
const taskIdx = taskNum - 1;
|
|
96
|
+
if (taskIdx < 0 || taskIdx >= phase.tasks.length)
|
|
97
|
+
return undefined;
|
|
98
|
+
const task = phase.tasks[taskIdx];
|
|
99
|
+
if (!task)
|
|
100
|
+
return undefined;
|
|
101
|
+
return { phase, task };
|
|
102
|
+
}
|
|
103
|
+
// =============================================================================
|
|
104
|
+
// Tag → TodoInput converter
|
|
105
|
+
// =============================================================================
|
|
106
|
+
/** Convert a parsed tag to a TodoInput, resolving seq against the given phases snapshot. */
|
|
107
|
+
function tagToTodoInput(phases, tag) {
|
|
108
|
+
switch (tag.op) {
|
|
109
|
+
case 'init':
|
|
110
|
+
return {
|
|
111
|
+
input: {
|
|
112
|
+
op: 'init',
|
|
113
|
+
list: tag.phases.map((p) => ({ phase: p.name, items: p.items })),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
case 'append': {
|
|
117
|
+
if (Number.isNaN(tag.phaseSeq) || tag.phaseSeq < 1 || tag.phaseSeq > phases.length) {
|
|
118
|
+
return { error: `Phase ${tag.phaseSeq} not found (have ${phases.length} phases)` };
|
|
119
|
+
}
|
|
120
|
+
const targetPhase = phases[tag.phaseSeq - 1];
|
|
121
|
+
if (!targetPhase) {
|
|
122
|
+
return { error: `Phase ${tag.phaseSeq} not found (have ${phases.length} phases)` };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
input: {
|
|
126
|
+
op: 'append',
|
|
127
|
+
phase: targetPhase.name,
|
|
128
|
+
items: tag.items,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
case 'view':
|
|
133
|
+
return { input: { op: 'view' } };
|
|
134
|
+
case 'start': {
|
|
135
|
+
const resolved = resolveSeq(phases, tag.seq);
|
|
136
|
+
if (!resolved)
|
|
137
|
+
return { error: `Invalid seq "${tag.seq}"` };
|
|
138
|
+
if (!resolved.task)
|
|
139
|
+
return { error: 'start requires a task-level seq (e.g. "1-2"), not phase-level' };
|
|
140
|
+
return { input: { op: 'start', task: resolved.task.content } };
|
|
141
|
+
}
|
|
142
|
+
case 'done':
|
|
143
|
+
case 'drop':
|
|
144
|
+
case 'rm': {
|
|
145
|
+
const resolved = resolveSeq(phases, tag.seq);
|
|
146
|
+
if (!resolved)
|
|
147
|
+
return { error: `Invalid seq "${tag.seq}"` };
|
|
148
|
+
if (!resolved.task)
|
|
149
|
+
return { input: { op: tag.op, phase: resolved.phase.name } };
|
|
150
|
+
return { input: { op: tag.op, task: resolved.task.content } };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// =============================================================================
|
|
155
|
+
// Main entry: applyTodoTags
|
|
156
|
+
// =============================================================================
|
|
157
|
+
/** Parse todo tags from text, resolve all seqs against the original snapshot,
|
|
158
|
+
* then apply sequentially. Tags are applied in document order.
|
|
159
|
+
* Does NOT mutate the input phases (clones first). */
|
|
160
|
+
export function applyTodoTags(phases, text) {
|
|
161
|
+
const tags = parseTodoTags(text);
|
|
162
|
+
if (tags.length === 0)
|
|
163
|
+
return { phases, errors: [], hasView: false };
|
|
164
|
+
// Pre-resolve all seqs against the original snapshot (LLM writes all tags
|
|
165
|
+
// based on one view; rm shifting must not corrupt later references).
|
|
166
|
+
const inputs = [];
|
|
167
|
+
const errors = [];
|
|
168
|
+
let hasView = false;
|
|
169
|
+
for (const tag of tags) {
|
|
170
|
+
if (tag.op === 'view') {
|
|
171
|
+
hasView = true;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const { input, error } = tagToTodoInput(phases, tag);
|
|
175
|
+
if (error)
|
|
176
|
+
errors.push(error);
|
|
177
|
+
else if (input)
|
|
178
|
+
inputs.push(input);
|
|
179
|
+
}
|
|
180
|
+
// Apply sequentially on a clone
|
|
181
|
+
let current = clonePhases(phases);
|
|
182
|
+
for (const input of inputs) {
|
|
183
|
+
const result = applyParams(current, input);
|
|
184
|
+
if (result.errors.length > 0) {
|
|
185
|
+
errors.push(...result.errors);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
current = result.phases;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { phases: current, errors, hasView };
|
|
192
|
+
}
|
|
@@ -33,7 +33,7 @@ export default async function workflow(ctx) {
|
|
|
33
33
|
|
|
34
34
|
const allFindings = scans
|
|
35
35
|
.filter((r) => r.ok)
|
|
36
|
-
.flatMap((r) => { try { return JSON.parse(r.output).findings ?? [] } catch { return [] } })
|
|
36
|
+
.flatMap((r) => { try { return JSON.parse(r.output).findings ?? [] } catch (e) { console.warn('[workflow] 子 agent 输出非 JSON,已跳过该结果:', e instanceof Error ? e.message : String(e)); return [] } })
|
|
37
37
|
|
|
38
38
|
progress(\`交叉验证 \${allFindings.length} 个发现...\`, { phase: 'verify' })
|
|
39
39
|
const verified = await runSubagents('reviewer', allFindings.map((f) => ({
|
|
@@ -46,7 +46,7 @@ export default async function workflow(ctx) {
|
|
|
46
46
|
|
|
47
47
|
const confirmed = verified
|
|
48
48
|
.filter((r) => r.ok)
|
|
49
|
-
.map((r) => { try { return JSON.parse(r.output) } catch { return null } })
|
|
49
|
+
.map((r) => { try { return JSON.parse(r.output) } catch (e) { console.warn('[workflow] 子 agent 输出非 JSON,已跳过该结果:', e instanceof Error ? e.message : String(e)); return null } })
|
|
50
50
|
.filter((v) => v?.confirmed)
|
|
51
51
|
|
|
52
52
|
progress('生成报告...', { phase: 'report' })
|
|
@@ -85,7 +85,7 @@ export default async function workflow(ctx) {
|
|
|
85
85
|
|
|
86
86
|
const allFindings = reviews
|
|
87
87
|
.filter((r) => r.ok)
|
|
88
|
-
.flatMap((r) => { try { return JSON.parse(r.output).findings ?? [] } catch { return [] } })
|
|
88
|
+
.flatMap((r) => { try { return JSON.parse(r.output).findings ?? [] } catch (e) { console.warn('[workflow] 子 agent 输出非 JSON,已跳过该结果:', e instanceof Error ? e.message : String(e)); return [] } })
|
|
89
89
|
|
|
90
90
|
progress('合并去重并生成报告...', { phase: 'merge' })
|
|
91
91
|
const seen = new Set()
|
|
@@ -133,7 +133,7 @@ export default async function workflow(ctx) {
|
|
|
133
133
|
|
|
134
134
|
const allItems = analyses
|
|
135
135
|
.filter((r) => r.ok)
|
|
136
|
-
.flatMap((r) => { try { return JSON.parse(r.output).items ?? [] } catch { return [] } })
|
|
136
|
+
.flatMap((r) => { try { return JSON.parse(r.output).items ?? [] } catch (e) { console.warn('[workflow] 子 agent 输出非 JSON,已跳过该结果:', e instanceof Error ? e.message : String(e)); return [] } })
|
|
137
137
|
|
|
138
138
|
progress('生成迁移报告...', { phase: 'report' })
|
|
139
139
|
const high = allItems.filter((i) => i.impact === 'high').length
|
|
@@ -99,7 +99,10 @@ async function saveWorkflow(name, source, target = 'project', projectDir) {
|
|
|
99
99
|
}
|
|
100
100
|
catch (e) {
|
|
101
101
|
await unlink(filePath);
|
|
102
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
error: `Failed to load workflow: ${e instanceof Error ? e.message : String(e)}`,
|
|
105
|
+
};
|
|
103
106
|
}
|
|
104
107
|
}
|
|
105
108
|
export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow };
|
package/dist/llm/registry.d.ts
CHANGED
|
@@ -4,13 +4,26 @@ import type { Model } from './schema/options.js';
|
|
|
4
4
|
type RouteEntry = ReturnType<typeof openAICompatRoute> & {
|
|
5
5
|
models: Record<string, ModelCapabilities>;
|
|
6
6
|
};
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Atomic, replaceable snapshot of every route + role binding.
|
|
9
|
+
*
|
|
10
|
+
* `Registry.table` is the single mutable field on a registry; `rebuildRegistry`
|
|
11
|
+
* swaps it in one reference assignment, so a `resolveRoute` /
|
|
12
|
+
* `resolveModelByRole` call firing at any moment observes a fully
|
|
13
|
+
* self-consistent table — either the complete previous set or the complete
|
|
14
|
+
* new set — never the half-cleared / half-registered intermediate that
|
|
15
|
+
* clear()-then-register would expose to a concurrent reader.
|
|
16
|
+
*/
|
|
17
|
+
type RouteTable = {
|
|
8
18
|
routes: Map<string, RouteEntry>;
|
|
9
19
|
roles: Map<string, {
|
|
10
20
|
provider: string;
|
|
11
21
|
model: string;
|
|
12
22
|
}>;
|
|
13
23
|
};
|
|
24
|
+
type Registry = {
|
|
25
|
+
table: RouteTable;
|
|
26
|
+
};
|
|
14
27
|
declare const createRegistry: () => Registry;
|
|
15
28
|
type ProviderInput = {
|
|
16
29
|
name: string;
|
|
@@ -54,7 +67,21 @@ declare const resolveModelByRole: (registry: Registry, role: ModelRole) => {
|
|
|
54
67
|
};
|
|
55
68
|
/** Bind a role to a (provider, model) pair. */
|
|
56
69
|
declare const setRole: (registry: Registry, role: ModelRole, provider: string, model: string) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Atomically rebuild the registry's routes + roles.
|
|
72
|
+
*
|
|
73
|
+
* `builder` populates a *detached* next registry via the normal
|
|
74
|
+
* `registerProvider` / `setRole` API; only after it returns is the live
|
|
75
|
+
* `table` pointer swapped in a single reference assignment. A reader that runs
|
|
76
|
+
* at any point during the build therefore sees the previous complete table,
|
|
77
|
+
* and the moment `rebuildRegistry` returns it sees the new complete table —
|
|
78
|
+
* never the partial intermediate the builder is still filling.
|
|
79
|
+
*
|
|
80
|
+
* Prefer this over clear()-then-register whenever the registry is shared with
|
|
81
|
+
* live request handlers (e.g. config hot-reload).
|
|
82
|
+
*/
|
|
83
|
+
declare const rebuildRegistry: (registry: Registry, builder: (next: Registry) => void) => void;
|
|
57
84
|
/** Default role + a starter catalog of well-known models. */
|
|
58
85
|
declare const builtinCapabilities: Record<string, Record<string, ModelCapabilities>>;
|
|
59
|
-
export type { ProviderInput, Registry, ResolveResult, RouteEntry };
|
|
60
|
-
export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, registerProvider, resolveModelByRole, resolveRoute, setRole, };
|
|
86
|
+
export type { ProviderInput, Registry, ResolveResult, RouteEntry, RouteTable };
|
|
87
|
+
export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, rebuildRegistry, registerProvider, resolveModelByRole, resolveRoute, setRole, };
|
package/dist/llm/registry.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { openAICompatRoute } from './protocols/openai-compat.js';
|
|
2
2
|
import { llmError } from './schema/errors.js';
|
|
3
3
|
import { model as makeModel } from './schema/options.js';
|
|
4
|
-
const createRegistry = () => ({ routes: new Map(), roles: new Map() });
|
|
4
|
+
const createRegistry = () => ({ table: { routes: new Map(), roles: new Map() } });
|
|
5
5
|
const registerProvider = (registry, input) => {
|
|
6
|
-
registry.routes.set(input.name, {
|
|
6
|
+
registry.table.routes.set(input.name, {
|
|
7
7
|
...openAICompatRoute({
|
|
8
8
|
id: input.name,
|
|
9
9
|
provider: input.name,
|
|
@@ -58,7 +58,7 @@ function overrideToCapabilities(overrides) {
|
|
|
58
58
|
* Throws NoRoute when the provider is unknown.
|
|
59
59
|
*/
|
|
60
60
|
const resolveRoute = (registry, provider, modelId) => {
|
|
61
|
-
const route = registry.routes.get(provider);
|
|
61
|
+
const route = registry.table.routes.get(provider);
|
|
62
62
|
if (route === undefined) {
|
|
63
63
|
throw llmError('LLM', 'resolve', {
|
|
64
64
|
_tag: 'NoRoute',
|
|
@@ -80,9 +80,9 @@ const resolveRoute = (registry, provider, modelId) => {
|
|
|
80
80
|
/** Resolve the (provider, model) configured for a given role. */
|
|
81
81
|
const resolveModelByRole = (registry, role) => {
|
|
82
82
|
const key = role._tag;
|
|
83
|
-
const entry = registry.roles.get(key);
|
|
83
|
+
const entry = registry.table.roles.get(key);
|
|
84
84
|
if (entry === undefined) {
|
|
85
|
-
const fallback = registry.roles.get('default');
|
|
85
|
+
const fallback = registry.table.roles.get('default');
|
|
86
86
|
if (fallback === undefined) {
|
|
87
87
|
throw llmError('LLM', 'resolve', {
|
|
88
88
|
_tag: 'NoRoute',
|
|
@@ -97,7 +97,25 @@ const resolveModelByRole = (registry, role) => {
|
|
|
97
97
|
};
|
|
98
98
|
/** Bind a role to a (provider, model) pair. */
|
|
99
99
|
const setRole = (registry, role, provider, model) => {
|
|
100
|
-
registry.roles.set(role._tag, { provider, model });
|
|
100
|
+
registry.table.roles.set(role._tag, { provider, model });
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Atomically rebuild the registry's routes + roles.
|
|
104
|
+
*
|
|
105
|
+
* `builder` populates a *detached* next registry via the normal
|
|
106
|
+
* `registerProvider` / `setRole` API; only after it returns is the live
|
|
107
|
+
* `table` pointer swapped in a single reference assignment. A reader that runs
|
|
108
|
+
* at any point during the build therefore sees the previous complete table,
|
|
109
|
+
* and the moment `rebuildRegistry` returns it sees the new complete table —
|
|
110
|
+
* never the partial intermediate the builder is still filling.
|
|
111
|
+
*
|
|
112
|
+
* Prefer this over clear()-then-register whenever the registry is shared with
|
|
113
|
+
* live request handlers (e.g. config hot-reload).
|
|
114
|
+
*/
|
|
115
|
+
const rebuildRegistry = (registry, builder) => {
|
|
116
|
+
const next = createRegistry();
|
|
117
|
+
builder(next);
|
|
118
|
+
registry.table = next.table;
|
|
101
119
|
};
|
|
102
120
|
/** Default role + a starter catalog of well-known models. */
|
|
103
121
|
const builtinCapabilities = {
|
|
@@ -133,4 +151,4 @@ const builtinCapabilities = {
|
|
|
133
151
|
},
|
|
134
152
|
},
|
|
135
153
|
};
|
|
136
|
-
export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, registerProvider, resolveModelByRole, resolveRoute, setRole, };
|
|
154
|
+
export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, rebuildRegistry, registerProvider, resolveModelByRole, resolveRoute, setRole, };
|
package/dist/llm/retry.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LLMErrorReason } from './schema/errors.js';
|
|
1
|
+
import type { LLMErrorReason, RetryPolicy } from './schema/errors.js';
|
|
2
2
|
declare const RETRY_INITIAL_DELAY = 2000;
|
|
3
3
|
declare const RETRY_MAX_DELAY_NO_HEADERS = 30000;
|
|
4
4
|
declare const RETRY_MAX_DELAY = 2147483647;
|
|
@@ -11,10 +11,12 @@ declare const delay: (attempt: number, error?: unknown) => number;
|
|
|
11
11
|
type Retryable = {
|
|
12
12
|
message: string;
|
|
13
13
|
reason: LLMErrorReason;
|
|
14
|
+
/** Per-reason policy: caps extra retries and per-attempt delay. */
|
|
15
|
+
policy: RetryPolicy;
|
|
14
16
|
};
|
|
15
17
|
/**
|
|
16
18
|
* Decide whether a thrown error is retryable. Returns undefined when not retryable
|
|
17
|
-
* (e.g. context overflow, auth, invalid request).
|
|
19
|
+
* (e.g. context overflow, auth, invalid request, mid-stream Transport failure).
|
|
18
20
|
*/
|
|
19
21
|
declare const retryable: (error: unknown) => Retryable | undefined;
|
|
20
22
|
type RetryOptions = {
|
package/dist/llm/retry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isLLMError, reasonRetryAfterMs,
|
|
1
|
+
import { isLLMError, reasonRetryAfterMs, retryPolicy } from './schema/errors.js';
|
|
2
2
|
const RETRY_INITIAL_DELAY = 2_000;
|
|
3
3
|
const RETRY_BACKOFF_FACTOR = 2;
|
|
4
4
|
const RETRY_MAX_DELAY_NO_HEADERS = 30_000;
|
|
@@ -43,15 +43,16 @@ const delay = (attempt, error) => {
|
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
45
45
|
* Decide whether a thrown error is retryable. Returns undefined when not retryable
|
|
46
|
-
* (e.g. context overflow, auth, invalid request).
|
|
46
|
+
* (e.g. context overflow, auth, invalid request, mid-stream Transport failure).
|
|
47
47
|
*/
|
|
48
48
|
const retryable = (error) => {
|
|
49
49
|
if (!isLLMError(error))
|
|
50
50
|
return undefined;
|
|
51
51
|
const reason = error.reason;
|
|
52
|
-
|
|
52
|
+
const policy = retryPolicy(reason);
|
|
53
|
+
if (!policy)
|
|
53
54
|
return undefined;
|
|
54
|
-
return { message: error.message, reason };
|
|
55
|
+
return { message: error.message, reason, policy };
|
|
55
56
|
};
|
|
56
57
|
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
57
58
|
/**
|
|
@@ -67,13 +68,13 @@ const withRetry = async (fn, options) => {
|
|
|
67
68
|
}
|
|
68
69
|
catch (error) {
|
|
69
70
|
const canRetry = retryable(error);
|
|
70
|
-
if (!canRetry || attempt >= options.maxRetries)
|
|
71
|
+
if (!canRetry || attempt >= Math.min(options.maxRetries, canRetry.policy.maxRetries))
|
|
71
72
|
throw error;
|
|
72
73
|
attempt += 1;
|
|
73
74
|
const fallbackReason = isLLMError(error)
|
|
74
75
|
? error.reason
|
|
75
76
|
: { _tag: 'InvalidRequest', message: '' };
|
|
76
|
-
const delayMs = reasonRetryAfterMs(fallbackReason) ?? delay(attempt, error);
|
|
77
|
+
const delayMs = Math.min(reasonRetryAfterMs(fallbackReason) ?? delay(attempt, error), canRetry.policy.maxDelay);
|
|
77
78
|
options.onRetry?.({ attempt, delayMs, error });
|
|
78
79
|
await sleep(delayMs);
|
|
79
80
|
}
|
|
@@ -64,7 +64,17 @@ type LLMErrorReason = {
|
|
|
64
64
|
};
|
|
65
65
|
/** Human-readable message extracted from any reason. */
|
|
66
66
|
declare const reasonMessage: (reason: LLMErrorReason) => string;
|
|
67
|
-
/**
|
|
67
|
+
/** Per-reason retry policy. `maxRetries` caps extra attempts; `maxDelay` caps each delay (ms). */
|
|
68
|
+
type RetryPolicy = {
|
|
69
|
+
maxRetries: number;
|
|
70
|
+
maxDelay: number;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Retry policy for a reason, or undefined when the reason is not retryable.
|
|
74
|
+
* Single source of truth consulted by both `reasonRetryable` and the retry loop.
|
|
75
|
+
*/
|
|
76
|
+
declare const retryPolicy: (reason: LLMErrorReason) => RetryPolicy | undefined;
|
|
77
|
+
/** Whether a reason is retryable (RateLimit / ProviderInternal / transient Transport). */
|
|
68
78
|
declare const reasonRetryable: (reason: LLMErrorReason) => boolean;
|
|
69
79
|
/** Retry-after delay in ms for reasons that carry one. */
|
|
70
80
|
declare const reasonRetryAfterMs: (reason: LLMErrorReason) => number | undefined;
|
|
@@ -84,5 +94,5 @@ type ToolFailure = {
|
|
|
84
94
|
};
|
|
85
95
|
declare const toolFailure: (message: string, metadata?: Record<string, unknown>) => ToolFailure;
|
|
86
96
|
declare const isToolFailure: (e: unknown) => e is ToolFailure;
|
|
87
|
-
export type { HttpContext, LLMError, LLMErrorReason, ToolFailure };
|
|
88
|
-
export { isLLMError, isToolFailure, llmError, reasonMessage, reasonRetryAfterMs, reasonRetryable, toolFailure, };
|
|
97
|
+
export type { HttpContext, LLMError, LLMErrorReason, RetryPolicy, ToolFailure };
|
|
98
|
+
export { isLLMError, isToolFailure, llmError, reasonMessage, reasonRetryAfterMs, reasonRetryable, retryPolicy, toolFailure, };
|
|
@@ -7,8 +7,40 @@ const reasonMessage = (reason) => {
|
|
|
7
7
|
return reason.message;
|
|
8
8
|
}
|
|
9
9
|
};
|
|
10
|
-
/**
|
|
11
|
-
const
|
|
10
|
+
/** Maximum extra retries for transient Transport (network) errors (3 total attempts). */
|
|
11
|
+
const TRANSPORT_MAX_RETRIES = 2;
|
|
12
|
+
/** Per-attempt delay ceiling (ms) for Transport retries, to avoid long stalls. */
|
|
13
|
+
const TRANSPORT_MAX_DELAY = 5_000;
|
|
14
|
+
/**
|
|
15
|
+
* Transport error kinds that are safe to retry — failures occurring before the
|
|
16
|
+
* provider begins streaming a response (connection / DNS / TCP reset / timeout):
|
|
17
|
+
* the request never reached the server (or the server never started processing),
|
|
18
|
+
* so retrying cannot duplicate cost or yield partial output. A mid-stream
|
|
19
|
+
* disconnect (e.g. a future `stream_interrupted` kind) is intentionally excluded,
|
|
20
|
+
* since the provider already started billing/producing.
|
|
21
|
+
*/
|
|
22
|
+
const RETRYABLE_TRANSPORT_KINDS = new Set(['network']);
|
|
23
|
+
/**
|
|
24
|
+
* Retry policy for a reason, or undefined when the reason is not retryable.
|
|
25
|
+
* Single source of truth consulted by both `reasonRetryable` and the retry loop.
|
|
26
|
+
*/
|
|
27
|
+
const retryPolicy = (reason) => {
|
|
28
|
+
switch (reason._tag) {
|
|
29
|
+
case 'RateLimit':
|
|
30
|
+
case 'ProviderInternal':
|
|
31
|
+
// Defer entirely to the caller's limits; existing behavior is unchanged.
|
|
32
|
+
return { maxRetries: Number.POSITIVE_INFINITY, maxDelay: Number.POSITIVE_INFINITY };
|
|
33
|
+
case 'Transport':
|
|
34
|
+
if (reason.kind !== undefined && RETRYABLE_TRANSPORT_KINDS.has(reason.kind)) {
|
|
35
|
+
return { maxRetries: TRANSPORT_MAX_RETRIES, maxDelay: TRANSPORT_MAX_DELAY };
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
default:
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/** Whether a reason is retryable (RateLimit / ProviderInternal / transient Transport). */
|
|
43
|
+
const reasonRetryable = (reason) => retryPolicy(reason) !== undefined;
|
|
12
44
|
/** Retry-after delay in ms for reasons that carry one. */
|
|
13
45
|
const reasonRetryAfterMs = (reason) => {
|
|
14
46
|
if (reason._tag === 'RateLimit' || reason._tag === 'ProviderInternal') {
|
|
@@ -30,4 +62,4 @@ const toolFailure = (message, metadata) => ({
|
|
|
30
62
|
metadata,
|
|
31
63
|
});
|
|
32
64
|
const isToolFailure = (e) => typeof e === 'object' && e !== null && e._tag === 'ToolFailure';
|
|
33
|
-
export { isLLMError, isToolFailure, llmError, reasonMessage, reasonRetryAfterMs, reasonRetryable, toolFailure, };
|
|
65
|
+
export { isLLMError, isToolFailure, llmError, reasonMessage, reasonRetryAfterMs, reasonRetryable, retryPolicy, toolFailure, };
|
package/dist/llm/transport.js
CHANGED
|
@@ -33,7 +33,11 @@ const sseFraming = async function* (stream) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
finally {
|
|
36
|
-
|
|
36
|
+
// Cancel the underlying transport and release the exclusive lock on every
|
|
37
|
+
// exit path (normal completion, thrown error, or early consumer break).
|
|
38
|
+
// cancel() rejects on an already-closed/errored stream — swallow that so it
|
|
39
|
+
// never masks the original error propagating through the generator.
|
|
40
|
+
await reader.cancel().catch(() => { });
|
|
37
41
|
}
|
|
38
42
|
};
|
|
39
43
|
/** Join all `data:` lines of an SSE block into one string. */
|
package/dist/project/resolve.js
CHANGED
|
@@ -59,14 +59,14 @@ export function getGitStatus(cwd) {
|
|
|
59
59
|
let i = 0;
|
|
60
60
|
while (i < tokens.length) {
|
|
61
61
|
const token = tokens[i];
|
|
62
|
-
if (token
|
|
62
|
+
if (!token)
|
|
63
63
|
break; // 末尾空 token
|
|
64
64
|
const xy = token.slice(0, 2);
|
|
65
65
|
const code = classifyStatus(xy);
|
|
66
66
|
const isRename = xy[0] === 'R' || xy[0] === 'C';
|
|
67
67
|
// 重命名/复制:"XY oldpath\0newpath",状态挂在 newpath
|
|
68
68
|
if (isRename && i + 1 < tokens.length) {
|
|
69
|
-
const newPath = tokens[i + 1];
|
|
69
|
+
const newPath = tokens[i + 1] ?? '';
|
|
70
70
|
map[normalizePath(newPath)] = code;
|
|
71
71
|
i += 2;
|
|
72
72
|
}
|
|
@@ -307,10 +307,17 @@ function createChatRoute(ctx) {
|
|
|
307
307
|
});
|
|
308
308
|
}
|
|
309
309
|
finally {
|
|
310
|
+
// 先从 agentManager 注销:必须最先执行,确保客户端断开使 writeSSE reject
|
|
311
|
+
// 时 agent 状态(state/deps/AbortController)不会泄漏到 Map(unregister 仅做
|
|
312
|
+
// Map.delete、不依赖 state,放最前安全)。
|
|
313
|
+
ctx.agentManager.unregister(sessionId);
|
|
310
314
|
// agentLoop 的 error/abort/max_turns 路径不 yield done,在此补发。
|
|
311
315
|
// 正常完成路径已在循环中 yield done,doneSent=true 时跳过避免重复。
|
|
316
|
+
// 客户端已断开时 writeSSE 会 reject,吞掉以保证后续 updateSessionLastRun 执行。
|
|
312
317
|
if (!doneSent) {
|
|
313
|
-
await stream
|
|
318
|
+
await stream
|
|
319
|
+
.writeSSE({ event: 'done', data: JSON.stringify({ _tag: 'done' }) })
|
|
320
|
+
.catch(() => { });
|
|
314
321
|
}
|
|
315
322
|
// 无论正常完成、错误还是 abort,只要服务还活着就标记 completed。
|
|
316
323
|
// 只有服务崩溃/重启才会留下 status='running' → 下次加载检测为 interrupted。
|
|
@@ -321,7 +328,6 @@ function createChatRoute(ctx) {
|
|
|
321
328
|
model: resolvedModel,
|
|
322
329
|
startedAt: runStartedAt,
|
|
323
330
|
}).catch(() => { });
|
|
324
|
-
ctx.agentManager.unregister(sessionId);
|
|
325
331
|
}
|
|
326
332
|
});
|
|
327
333
|
});
|