minimal-agent 0.11.0 → 0.12.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/package.json +4 -1
- package/plugins/workflow-runner/plugin.js +27 -15
- package/plugins/workflow-runner/plugin.ts +34 -20
- package/plugins/workflow-runner/src/agentCommon.js +13 -12
- package/plugins/workflow-runner/src/agentCommon.ts +22 -13
- package/plugins/workflow-runner/src/index.js +5 -3
- package/plugins/workflow-runner/src/index.ts +31 -13
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
- package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.ts +26 -7
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/src/cli/print.js +19 -4
- package/src/cli/print.ts +17 -4
- package/src/cli/streamJson.js +19 -4
- package/src/cli/streamJson.ts +17 -4
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configFile.js +2 -21
- package/src/config/configFile.ts +11 -24
- package/src/config/configManagement.js +1 -1
- package/src/config/configManagement.ts +2 -2
- package/src/context/compact.js +4 -0
- package/src/context/compact.ts +4 -0
- package/src/context/contextBundle.js +38 -13
- package/src/context/contextBundle.ts +47 -13
- package/src/harness/stallGuard.js +19 -4
- package/src/harness/stallGuard.ts +30 -7
- package/src/loop.js +95 -11
- package/src/loop.ts +125 -16
- package/src/main.js +8 -1
- package/src/main.tsx +7 -1
- package/src/memory/constants.js +10 -0
- package/src/memory/constants.ts +13 -0
- package/src/memory/episodicMemory.js +274 -2
- package/src/memory/episodicMemory.ts +306 -1
- package/src/memory/fileStore.js +24 -4
- package/src/memory/fileStore.ts +22 -2
- package/src/memory/memoryService.js +79 -12
- package/src/memory/memoryService.ts +108 -9
- package/src/memory/repository.js +415 -85
- package/src/memory/repository.ts +437 -86
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.ts +16 -0
- package/src/plugins/pluginApi.ts +30 -6
- package/src/plugins/pluginRunner.js +6 -2
- package/src/plugins/pluginRunner.ts +12 -7
- package/src/prompts/policyRegistry.js +27 -0
- package/src/prompts/policyRegistry.ts +27 -0
- package/src/prompts/promptAssembly.js +39 -7
- package/src/prompts/promptAssembly.ts +145 -59
- package/src/session/runAgentTurn.js +43 -21
- package/src/session/runAgentTurn.ts +68 -26
- package/src/session/runTurn.js +11 -46
- package/src/session/runTurn.ts +12 -46
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +5 -0
- package/src/session/sessionState.ts +14 -0
- package/src/tools/agent/agent.js +143 -14
- package/src/tools/agent/agent.ts +149 -14
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +5 -0
- package/src/tools/agent/agentTypes.ts +5 -0
- package/src/tools/bash/bash.js +31 -11
- package/src/tools/bash/bash.ts +29 -10
- package/src/tools/capabilities.js +5 -0
- package/src/tools/capabilities.ts +11 -1
- package/src/tools/edit/edit.js +7 -4
- package/src/tools/edit/edit.ts +7 -4
- package/src/tools/edit/multi-edit.js +4 -2
- package/src/tools/edit/multi-edit.ts +4 -2
- package/src/tools/glob/glob.js +156 -40
- package/src/tools/glob/glob.ts +185 -46
- package/src/tools/index.js +35 -17
- package/src/tools/index.ts +81 -20
- package/src/tools/lsp/lsp.js +165 -22
- package/src/tools/lsp/lsp.ts +206 -36
- package/src/tools/powershell/powershell.js +116 -26
- package/src/tools/powershell/powershell.ts +155 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +244 -29
- package/src/tools/read/read.ts +391 -59
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +2 -1
- package/src/tools/shared/fileActivity.ts +3 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +79 -28
- package/src/tools/tasks/backgroundTaskService.ts +165 -49
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +64 -14
- package/src/tools/tasks/taskOutput.ts +102 -23
- package/src/tools/tasks/taskStop.js +48 -8
- package/src/tools/tasks/taskStop.ts +69 -9
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +6 -2
- package/src/tools/write/write.ts +6 -2
- package/src/types.ts +12 -0
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
package/src/tools/agent/agent.js
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
* src/tools/agent/agent.ts —— Agent 工具(fork 子 agent / 多 agent 并行)
|
|
4
4
|
* ------------------------------------------------------------
|
|
5
5
|
* 借鉴 Claude Code 官方 AgentTool/ForkAgent 的核心思想,按 minimal-agent
|
|
6
|
-
*
|
|
6
|
+
* 内核架构做可恢复的前台/后台等效实现:
|
|
7
7
|
* - 官方 fork = 「隔离 context 嵌套跑同一主循环 + 摘要回传(完整历史不进
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - 同轮混入任一非并发安全工具(如 Edit)会整轮退化串行——这是想要的安全
|
|
13
|
-
* 语义(写操作与并行 fork 不该交错)。
|
|
8
|
+
* 父上下文)」。本工具在隔离 history 中嵌套运行同一主循环,并将私有历史、
|
|
9
|
+
* 进度和结果写入会话运行存储;前台、后台和重启后续接共用同一执行内核。
|
|
10
|
+
* - 同轮多个 Agent 调用仍由 loop.ts 的并行 worker 执行;write scopes 与可选
|
|
11
|
+
* worktree 负责隔离真正可并行的写任务。
|
|
14
12
|
*
|
|
15
13
|
* 已知限制(如实声明):
|
|
16
14
|
* - 子 agent 的 LoopEvent 不外泄:-p / stream-json 宿主只看到本工具的一对
|
|
@@ -27,6 +25,8 @@
|
|
|
27
25
|
* ============================================================
|
|
28
26
|
*/
|
|
29
27
|
import { randomUUID } from 'node:crypto';
|
|
28
|
+
import { EventEmitter } from 'node:events';
|
|
29
|
+
import { access } from 'node:fs/promises';
|
|
30
30
|
import { withAskSource } from '../../askBridge.js';
|
|
31
31
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
32
32
|
import { z } from 'zod';
|
|
@@ -41,6 +41,7 @@ import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
|
41
41
|
import { SUBAGENT_EXCLUDED_TOOLS, resolveAgentType } from './agentTypes.js';
|
|
42
42
|
import { acquireSubagentLease, normalizeWriteScopes } from './coordinator.js';
|
|
43
43
|
import { commitWorktreeChanges, createAgentWorktree, hasWorktreeChanges, removeAgentWorktree, worktreeChangedFiles, worktreeDiffStat, } from './worktree.js';
|
|
44
|
+
import { appendAgentEvent, loadAgentRecord, saveAgentRecord } from './agentTaskService.js';
|
|
44
45
|
let corePromise = null;
|
|
45
46
|
/** 首次调用时才真正加载 loop/tools——此刻模块图早已初始化完毕,环安全。 */
|
|
46
47
|
function loadCore() {
|
|
@@ -79,13 +80,16 @@ const inputSchema = z.object({
|
|
|
79
80
|
.max(32)
|
|
80
81
|
.optional()
|
|
81
82
|
.describe('general 预计修改的工作目录相对路径。范围互不重叠时可并行;缺省时按整个工作目录串行。explore 不需要。'),
|
|
83
|
+
run_in_background: z.boolean().optional().describe('立即返回 agent_id,子任务在后台运行;用 TaskOutput/TaskStop 管理'),
|
|
84
|
+
resume_agent_id: z.string().regex(/^agent_[0-9a-f-]{36}$/i).optional().describe('继续当前会话中此前完成、中断或失败的 Agent 历史'),
|
|
82
85
|
});
|
|
83
86
|
const parameters = toToolParameters(inputSchema);
|
|
84
87
|
// ---------------- 2. Description(给 LLM 看的工具说明) ----------------
|
|
85
88
|
// 借鉴官方 AgentTool prompt 文案(任务书自包含 / 同轮并行 / 何时不用)。
|
|
86
|
-
const description = `- Launches
|
|
89
|
+
const description = `- Launches an isolated sub-agent for one self-contained task. Every run returns an agent_id; run_in_background=true continues without blocking, and resume_agent_id continues its persisted private history across user turns or an app restart.
|
|
87
90
|
- The sub-agent CANNOT see this conversation. The \`prompt\` must be fully self-contained: background, exact file paths, what to look for / do, and the expected output format.
|
|
88
91
|
- **To parallelize, emit multiple Agent calls in ONE assistant message** — they run concurrently. This is the main value of this tool.
|
|
92
|
+
- Background Agent progress/results are read with TaskOutput and stopped with TaskStop. Do not poll: TaskOutput(block=true) waits efficiently.
|
|
89
93
|
- Use for: exploring/researching several independent areas of a codebase at once, parallel information gathering from multiple sources, independent analyses that will be synthesized afterwards, and (with agent_type:'general') genuinely parallelizable write/execute work.
|
|
90
94
|
- Do NOT use for: single targeted lookups that one or two direct tool calls would answer (a fork starts cold and re-derives context — slower and costlier), or strictly sequential work. Only general execution agents may ask the user, and only when genuinely blocked.
|
|
91
95
|
- agent_type: 'explore' (default) = strictly read-only research; 'general' = read-write execution. For direct writes, declare relative write_scopes; disjoint scopes may run concurrently, overlapping or omitted scopes are serialized.
|
|
@@ -237,10 +241,10 @@ export async function runAgentTask(input, signal, ctx, deps = {}) {
|
|
|
237
241
|
// allowedTools 只裁「下发给模型的清单」;guardedExecute 是真正的硬防线(见下)。
|
|
238
242
|
const allowedTools = def.resolveAllowedTools(ALL_TOOLS.map((t) => t.name));
|
|
239
243
|
// ---- worktree 隔离(仅 general 显式要求时) ----
|
|
240
|
-
let wt = null;
|
|
244
|
+
let wt = deps.existingWorktree ?? null;
|
|
241
245
|
const isolation = input.isolation ?? ctx.subagentConfig?.defaultWriteIsolation ?? 'none';
|
|
242
|
-
if (def.type === 'general' && isolation === 'worktree') {
|
|
243
|
-
const created = await createAgentWorktree(`fork-${randomUUID().slice(0, 8)}
|
|
246
|
+
if (def.type === 'general' && isolation === 'worktree' && !wt) {
|
|
247
|
+
const created = await createAgentWorktree(`fork-${randomUUID().slice(0, 8)}`, ctx.cwd);
|
|
244
248
|
if (!created.ok) {
|
|
245
249
|
return {
|
|
246
250
|
ok: false,
|
|
@@ -248,8 +252,9 @@ export async function runAgentTask(input, signal, ctx, deps = {}) {
|
|
|
248
252
|
};
|
|
249
253
|
}
|
|
250
254
|
wt = created;
|
|
255
|
+
await deps.onWorktreeCreated?.(created);
|
|
251
256
|
}
|
|
252
|
-
const cwd = wt?.path ?? getWorkingDir();
|
|
257
|
+
const cwd = wt?.path ?? ctx.cwd ?? getWorkingDir();
|
|
253
258
|
let writeScopes;
|
|
254
259
|
try {
|
|
255
260
|
writeScopes = def.type === 'general'
|
|
@@ -288,6 +293,7 @@ export async function runAgentTask(input, signal, ctx, deps = {}) {
|
|
|
288
293
|
surface: 'subagent',
|
|
289
294
|
cwd,
|
|
290
295
|
workspaceId: ctx.workspaceId,
|
|
296
|
+
sessionId: ctx.sessionId,
|
|
291
297
|
interactiveAsk: childRuntime.interactiveAsk,
|
|
292
298
|
memoryAccess: childMemoryAccess,
|
|
293
299
|
currentUserInput: input.prompt,
|
|
@@ -303,7 +309,7 @@ export async function runAgentTask(input, signal, ctx, deps = {}) {
|
|
|
303
309
|
ctx.reportProgress?.(line, detail);
|
|
304
310
|
};
|
|
305
311
|
// ---- 子 history:一条 system(压缩按 role 定位,必须存在)+ runQuery 自己 push 任务书 ----
|
|
306
|
-
const history = [
|
|
312
|
+
const history = deps.initialHistory?.length ? [...deps.initialHistory] : [
|
|
307
313
|
{ role: 'system', content: await buildSubSystemPrompt(cwd, def, subTools) },
|
|
308
314
|
];
|
|
309
315
|
const historyLenBefore = history.length;
|
|
@@ -529,6 +535,7 @@ export async function runAgentTask(input, signal, ctx, deps = {}) {
|
|
|
529
535
|
finally {
|
|
530
536
|
stopTimeout();
|
|
531
537
|
releaseLease?.();
|
|
538
|
+
await deps.captureHistory?.(history);
|
|
532
539
|
}
|
|
533
540
|
// ---- 结局映射(直接查 signal 状态,不依赖事件旗标) ----
|
|
534
541
|
if (ownerCancelController.signal.aborted) {
|
|
@@ -625,6 +632,128 @@ export async function runAgentTask(input, signal, ctx, deps = {}) {
|
|
|
625
632
|
async function call(input, signal) {
|
|
626
633
|
return runAgentTask(input, signal, {});
|
|
627
634
|
}
|
|
635
|
+
export async function runManagedAgentTask(input, signal, ctx, deps = {}) {
|
|
636
|
+
if (!ctx.sessionState)
|
|
637
|
+
return runAgentTask(input, signal, ctx);
|
|
638
|
+
const id = input.resume_agent_id ?? `agent_${randomUUID()}`;
|
|
639
|
+
let initialHistory;
|
|
640
|
+
let resumedWorktree;
|
|
641
|
+
let coreInput = { ...input, run_in_background: undefined, resume_agent_id: undefined };
|
|
642
|
+
if (input.resume_agent_id) {
|
|
643
|
+
const live = ctx.sessionState.agentTasks.tasks.get(id);
|
|
644
|
+
if (live?.status === 'running')
|
|
645
|
+
return { ok: false, error: `Agent ${id} 正在运行,不能并发恢复` };
|
|
646
|
+
const saved = await loadAgentRecord(ctx, id);
|
|
647
|
+
if (!saved)
|
|
648
|
+
return { ok: false, error: `找不到可恢复 Agent:${id}` };
|
|
649
|
+
initialHistory = saved.history;
|
|
650
|
+
resumedWorktree = saved.worktree && await access(saved.worktree.path)
|
|
651
|
+
.then(() => saved.worktree)
|
|
652
|
+
.catch(() => undefined);
|
|
653
|
+
coreInput = {
|
|
654
|
+
...saved.input,
|
|
655
|
+
prompt: input.prompt,
|
|
656
|
+
description: input.description || saved.description,
|
|
657
|
+
run_in_background: undefined,
|
|
658
|
+
resume_agent_id: undefined,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const abort = new AbortController();
|
|
662
|
+
const combined = signal ? AbortSignal.any([signal, abort.signal]) : abort.signal;
|
|
663
|
+
let settle;
|
|
664
|
+
const completion = new Promise((resolvePromise) => { settle = resolvePromise; });
|
|
665
|
+
const task = {
|
|
666
|
+
id,
|
|
667
|
+
status: 'running',
|
|
668
|
+
description: coreInput.description,
|
|
669
|
+
input: coreInput,
|
|
670
|
+
createdAt: Date.now(),
|
|
671
|
+
updatedAt: Date.now(),
|
|
672
|
+
abort,
|
|
673
|
+
completion,
|
|
674
|
+
events: new EventEmitter(),
|
|
675
|
+
};
|
|
676
|
+
ctx.sessionState.agentTasks.tasks.set(id, task);
|
|
677
|
+
await saveAgentRecord(ctx, task);
|
|
678
|
+
await appendAgentEvent(ctx, id, { phase: 'started', resumed: Boolean(input.resume_agent_id) });
|
|
679
|
+
const run = async () => {
|
|
680
|
+
let captured = initialHistory;
|
|
681
|
+
let lastPersistedProgressAt = 0;
|
|
682
|
+
const managedContext = {
|
|
683
|
+
...ctx,
|
|
684
|
+
reportProgress: (line, detail) => {
|
|
685
|
+
ctx.reportProgress?.(line, detail);
|
|
686
|
+
task.progress = line;
|
|
687
|
+
task.events.emit('change');
|
|
688
|
+
if (Date.now() - lastPersistedProgressAt >= 1_000) {
|
|
689
|
+
lastPersistedProgressAt = Date.now();
|
|
690
|
+
void saveAgentRecord(ctx, task).catch(() => undefined);
|
|
691
|
+
void appendAgentEvent(ctx, id, { phase: 'progress', message: line })
|
|
692
|
+
.catch(() => undefined);
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
try {
|
|
697
|
+
const result = await runAgentTask(coreInput, combined, managedContext, {
|
|
698
|
+
...deps,
|
|
699
|
+
initialHistory,
|
|
700
|
+
existingWorktree: resumedWorktree,
|
|
701
|
+
onWorktreeCreated: async (worktree) => {
|
|
702
|
+
task.worktree = worktree;
|
|
703
|
+
await saveAgentRecord(ctx, task);
|
|
704
|
+
},
|
|
705
|
+
captureHistory: (history) => { captured = [...history]; },
|
|
706
|
+
});
|
|
707
|
+
task.result = result;
|
|
708
|
+
task.history = captured;
|
|
709
|
+
task.status = abort.signal.aborted ? 'stopped' : result.ok ? 'completed' : 'failed';
|
|
710
|
+
if (task.worktree && !await access(task.worktree.path).then(() => true).catch(() => false)) {
|
|
711
|
+
task.worktree = undefined;
|
|
712
|
+
}
|
|
713
|
+
task.updatedAt = Date.now();
|
|
714
|
+
await saveAgentRecord(ctx, task);
|
|
715
|
+
await appendAgentEvent(ctx, id, { phase: task.status, ok: result.ok });
|
|
716
|
+
return result;
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
task.status = abort.signal.aborted ? 'stopped' : 'failed';
|
|
720
|
+
task.result = { ok: false, error: error.message };
|
|
721
|
+
task.history = captured;
|
|
722
|
+
task.updatedAt = Date.now();
|
|
723
|
+
await saveAgentRecord(ctx, task).catch(() => undefined);
|
|
724
|
+
await appendAgentEvent(ctx, id, { phase: task.status, error: error.message })
|
|
725
|
+
.catch(() => undefined);
|
|
726
|
+
return task.result;
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
task.events.emit('change');
|
|
730
|
+
settle();
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
if (input.run_in_background) {
|
|
734
|
+
void run();
|
|
735
|
+
return {
|
|
736
|
+
ok: true,
|
|
737
|
+
content: `后台 Agent 已启动。agent_id=${id}\n使用 TaskOutput 获取进度/结果,TaskStop 停止。`,
|
|
738
|
+
metadata: {
|
|
739
|
+
kind: 'agent', agentId: id, status: 'running',
|
|
740
|
+
agentType: input.agent_type ?? 'explore', description: input.description,
|
|
741
|
+
isolation: input.isolation ?? 'none', turns: 0, toolCalls: 0,
|
|
742
|
+
failedToolCalls: 0, durationMs: 0, changedFiles: [],
|
|
743
|
+
},
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
const result = await run();
|
|
747
|
+
if (result.ok && result.metadata?.kind === 'agent') {
|
|
748
|
+
result.metadata.agentId = id;
|
|
749
|
+
result.metadata.status = 'completed';
|
|
750
|
+
result.content = `agent_id=${id}\n${result.content}`;
|
|
751
|
+
}
|
|
752
|
+
else if (!result.ok) {
|
|
753
|
+
result.error = `agent_id=${id}\n${result.error}`;
|
|
754
|
+
}
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
628
757
|
// ---------------- 5. 导出 ----------------
|
|
629
758
|
export const agentTool = {
|
|
630
759
|
name: 'Agent',
|
|
@@ -638,5 +767,5 @@ export const agentTool = {
|
|
|
638
767
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
639
768
|
interruptBehavior: () => 'block',
|
|
640
769
|
call,
|
|
641
|
-
callWithContext:
|
|
770
|
+
callWithContext: runManagedAgentTask,
|
|
642
771
|
};
|
package/src/tools/agent/agent.ts
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
* src/tools/agent/agent.ts —— Agent 工具(fork 子 agent / 多 agent 并行)
|
|
4
4
|
* ------------------------------------------------------------
|
|
5
5
|
* 借鉴 Claude Code 官方 AgentTool/ForkAgent 的核心思想,按 minimal-agent
|
|
6
|
-
*
|
|
6
|
+
* 内核架构做可恢复的前台/后台等效实现:
|
|
7
7
|
* - 官方 fork = 「隔离 context 嵌套跑同一主循环 + 摘要回传(完整历史不进
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - 同轮混入任一非并发安全工具(如 Edit)会整轮退化串行——这是想要的安全
|
|
13
|
-
* 语义(写操作与并行 fork 不该交错)。
|
|
8
|
+
* 父上下文)」。本工具在隔离 history 中嵌套运行同一主循环,并将私有历史、
|
|
9
|
+
* 进度和结果写入会话运行存储;前台、后台和重启后续接共用同一执行内核。
|
|
10
|
+
* - 同轮多个 Agent 调用仍由 loop.ts 的并行 worker 执行;write scopes 与可选
|
|
11
|
+
* worktree 负责隔离真正可并行的写任务。
|
|
14
12
|
*
|
|
15
13
|
* 已知限制(如实声明):
|
|
16
14
|
* - 子 agent 的 LoopEvent 不外泄:-p / stream-json 宿主只看到本工具的一对
|
|
@@ -28,6 +26,8 @@
|
|
|
28
26
|
*/
|
|
29
27
|
|
|
30
28
|
import { randomUUID } from 'node:crypto';
|
|
29
|
+
import { EventEmitter } from 'node:events';
|
|
30
|
+
import { access } from 'node:fs/promises';
|
|
31
31
|
import { withAskSource } from '../../askBridge.js';
|
|
32
32
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
33
33
|
import { z } from 'zod';
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
worktreeChangedFiles,
|
|
60
60
|
worktreeDiffStat,
|
|
61
61
|
} from './worktree.js';
|
|
62
|
+
import { appendAgentEvent, loadAgentRecord, saveAgentRecord, type AgentTaskRecord, type LiveAgentTask } from './agentTaskService.js';
|
|
62
63
|
|
|
63
64
|
// ---------------- 0. 环内模块懒加载(见文件头「循环依赖纪律」) ----------------
|
|
64
65
|
|
|
@@ -116,6 +117,8 @@ const inputSchema = z.object({
|
|
|
116
117
|
.describe(
|
|
117
118
|
'general 预计修改的工作目录相对路径。范围互不重叠时可并行;缺省时按整个工作目录串行。explore 不需要。',
|
|
118
119
|
),
|
|
120
|
+
run_in_background: z.boolean().optional().describe('立即返回 agent_id,子任务在后台运行;用 TaskOutput/TaskStop 管理'),
|
|
121
|
+
resume_agent_id: z.string().regex(/^agent_[0-9a-f-]{36}$/i).optional().describe('继续当前会话中此前完成、中断或失败的 Agent 历史'),
|
|
119
122
|
});
|
|
120
123
|
type AgentInput = z.infer<typeof inputSchema>;
|
|
121
124
|
|
|
@@ -124,9 +127,10 @@ const parameters = toToolParameters(inputSchema);
|
|
|
124
127
|
// ---------------- 2. Description(给 LLM 看的工具说明) ----------------
|
|
125
128
|
// 借鉴官方 AgentTool prompt 文案(任务书自包含 / 同轮并行 / 何时不用)。
|
|
126
129
|
|
|
127
|
-
const description = `- Launches
|
|
130
|
+
const description = `- Launches an isolated sub-agent for one self-contained task. Every run returns an agent_id; run_in_background=true continues without blocking, and resume_agent_id continues its persisted private history across user turns or an app restart.
|
|
128
131
|
- The sub-agent CANNOT see this conversation. The \`prompt\` must be fully self-contained: background, exact file paths, what to look for / do, and the expected output format.
|
|
129
132
|
- **To parallelize, emit multiple Agent calls in ONE assistant message** — they run concurrently. This is the main value of this tool.
|
|
133
|
+
- Background Agent progress/results are read with TaskOutput and stopped with TaskStop. Do not poll: TaskOutput(block=true) waits efficiently.
|
|
130
134
|
- Use for: exploring/researching several independent areas of a codebase at once, parallel information gathering from multiple sources, independent analyses that will be synthesized afterwards, and (with agent_type:'general') genuinely parallelizable write/execute work.
|
|
131
135
|
- Do NOT use for: single targeted lookups that one or two direct tool calls would answer (a fork starts cold and re-derives context — slower and costlier), or strictly sequential work. Only general execution agents may ask the user, and only when genuinely blocked.
|
|
132
136
|
- agent_type: 'explore' (default) = strictly read-only research; 'general' = read-write execution. For direct writes, declare relative write_scopes; disjoint scopes may run concurrently, overlapping or omitted scopes are serialized.
|
|
@@ -222,6 +226,10 @@ export function extractCoveredFiles(
|
|
|
222
226
|
export interface AgentTaskDeps {
|
|
223
227
|
chat?: RunQueryOptions['chat'];
|
|
224
228
|
executeTool?: RunQueryOptions['executeTool'];
|
|
229
|
+
initialHistory?: RunQueryOptions['history'];
|
|
230
|
+
captureHistory?: (history:RunQueryOptions['history'])=>void|Promise<void>;
|
|
231
|
+
existingWorktree?: AgentWorktree;
|
|
232
|
+
onWorktreeCreated?: (worktree: AgentWorktree) => void | Promise<void>;
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
function positiveInt(value: string | undefined): number | undefined {
|
|
@@ -296,10 +304,10 @@ export async function runAgentTask(
|
|
|
296
304
|
const allowedTools = def.resolveAllowedTools(ALL_TOOLS.map((t) => t.name));
|
|
297
305
|
|
|
298
306
|
// ---- worktree 隔离(仅 general 显式要求时) ----
|
|
299
|
-
let wt: AgentWorktree | null = null;
|
|
307
|
+
let wt: AgentWorktree | null = deps.existingWorktree ?? null;
|
|
300
308
|
const isolation = input.isolation ?? ctx.subagentConfig?.defaultWriteIsolation ?? 'none';
|
|
301
|
-
if (def.type === 'general' && isolation === 'worktree') {
|
|
302
|
-
const created = await createAgentWorktree(`fork-${randomUUID().slice(0, 8)}
|
|
309
|
+
if (def.type === 'general' && isolation === 'worktree' && !wt) {
|
|
310
|
+
const created = await createAgentWorktree(`fork-${randomUUID().slice(0, 8)}`, ctx.cwd);
|
|
303
311
|
if (!created.ok) {
|
|
304
312
|
return {
|
|
305
313
|
ok: false,
|
|
@@ -307,8 +315,9 @@ export async function runAgentTask(
|
|
|
307
315
|
};
|
|
308
316
|
}
|
|
309
317
|
wt = created;
|
|
318
|
+
await deps.onWorktreeCreated?.(created);
|
|
310
319
|
}
|
|
311
|
-
const cwd = wt?.path ?? getWorkingDir();
|
|
320
|
+
const cwd = wt?.path ?? ctx.cwd ?? getWorkingDir();
|
|
312
321
|
let writeScopes: string[];
|
|
313
322
|
try {
|
|
314
323
|
writeScopes = def.type === 'general'
|
|
@@ -348,6 +357,7 @@ export async function runAgentTask(
|
|
|
348
357
|
surface: 'subagent',
|
|
349
358
|
cwd,
|
|
350
359
|
workspaceId: ctx.workspaceId,
|
|
360
|
+
sessionId: ctx.sessionId,
|
|
351
361
|
interactiveAsk: childRuntime.interactiveAsk,
|
|
352
362
|
memoryAccess: childMemoryAccess,
|
|
353
363
|
currentUserInput: input.prompt,
|
|
@@ -364,7 +374,7 @@ export async function runAgentTask(
|
|
|
364
374
|
};
|
|
365
375
|
|
|
366
376
|
// ---- 子 history:一条 system(压缩按 role 定位,必须存在)+ runQuery 自己 push 任务书 ----
|
|
367
|
-
const history: RunQueryOptions['history'] = [
|
|
377
|
+
const history: RunQueryOptions['history'] = deps.initialHistory?.length ? [...deps.initialHistory] : [
|
|
368
378
|
{ role: 'system', content: await buildSubSystemPrompt(cwd, def, subTools) },
|
|
369
379
|
];
|
|
370
380
|
const historyLenBefore = history.length;
|
|
@@ -582,6 +592,7 @@ export async function runAgentTask(
|
|
|
582
592
|
} finally {
|
|
583
593
|
stopTimeout();
|
|
584
594
|
releaseLease?.();
|
|
595
|
+
await deps.captureHistory?.(history);
|
|
585
596
|
}
|
|
586
597
|
|
|
587
598
|
// ---- 结局映射(直接查 signal 状态,不依赖事件旗标) ----
|
|
@@ -684,6 +695,130 @@ async function call(input: AgentInput, signal?: AbortSignal): Promise<ToolResult
|
|
|
684
695
|
return runAgentTask(input, signal, {});
|
|
685
696
|
}
|
|
686
697
|
|
|
698
|
+
export async function runManagedAgentTask(
|
|
699
|
+
input: AgentInput,
|
|
700
|
+
signal: AbortSignal | undefined,
|
|
701
|
+
ctx: ToolCallContext,
|
|
702
|
+
deps: AgentTaskDeps = {},
|
|
703
|
+
): Promise<ToolResult> {
|
|
704
|
+
if (!ctx.sessionState) return runAgentTask(input, signal, ctx);
|
|
705
|
+
const id = input.resume_agent_id ?? `agent_${randomUUID()}`;
|
|
706
|
+
let initialHistory: RunQueryOptions['history'] | undefined;
|
|
707
|
+
let resumedWorktree: AgentWorktree | undefined;
|
|
708
|
+
let coreInput = { ...input, run_in_background: undefined, resume_agent_id: undefined };
|
|
709
|
+
if (input.resume_agent_id) {
|
|
710
|
+
const live = ctx.sessionState.agentTasks.tasks.get(id);
|
|
711
|
+
if (live?.status === 'running') return { ok: false, error: `Agent ${id} 正在运行,不能并发恢复` };
|
|
712
|
+
const saved = await loadAgentRecord(ctx, id);
|
|
713
|
+
if (!saved) return { ok: false, error: `找不到可恢复 Agent:${id}` };
|
|
714
|
+
initialHistory = saved.history;
|
|
715
|
+
resumedWorktree = saved.worktree && await access(saved.worktree.path)
|
|
716
|
+
.then(() => saved.worktree)
|
|
717
|
+
.catch(() => undefined);
|
|
718
|
+
coreInput = {
|
|
719
|
+
...(saved.input as AgentInput),
|
|
720
|
+
prompt: input.prompt,
|
|
721
|
+
description: input.description || saved.description,
|
|
722
|
+
run_in_background: undefined,
|
|
723
|
+
resume_agent_id: undefined,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const abort = new AbortController();
|
|
727
|
+
const combined = signal ? AbortSignal.any([signal, abort.signal]) : abort.signal;
|
|
728
|
+
let settle!: () => void;
|
|
729
|
+
const completion = new Promise<void>((resolvePromise) => { settle = resolvePromise; });
|
|
730
|
+
const task: LiveAgentTask = {
|
|
731
|
+
id,
|
|
732
|
+
status: 'running',
|
|
733
|
+
description: coreInput.description,
|
|
734
|
+
input: coreInput as unknown as Record<string, unknown>,
|
|
735
|
+
createdAt: Date.now(),
|
|
736
|
+
updatedAt: Date.now(),
|
|
737
|
+
abort,
|
|
738
|
+
completion,
|
|
739
|
+
events: new EventEmitter(),
|
|
740
|
+
};
|
|
741
|
+
ctx.sessionState.agentTasks.tasks.set(id, task);
|
|
742
|
+
await saveAgentRecord(ctx, task);
|
|
743
|
+
await appendAgentEvent(ctx, id, { phase: 'started', resumed: Boolean(input.resume_agent_id) });
|
|
744
|
+
|
|
745
|
+
const run = async (): Promise<ToolResult> => {
|
|
746
|
+
let captured = initialHistory;
|
|
747
|
+
let lastPersistedProgressAt = 0;
|
|
748
|
+
const managedContext: ToolCallContext = {
|
|
749
|
+
...ctx,
|
|
750
|
+
reportProgress: (line, detail) => {
|
|
751
|
+
ctx.reportProgress?.(line, detail);
|
|
752
|
+
task.progress = line;
|
|
753
|
+
task.events.emit('change');
|
|
754
|
+
if (Date.now() - lastPersistedProgressAt >= 1_000) {
|
|
755
|
+
lastPersistedProgressAt = Date.now();
|
|
756
|
+
void saveAgentRecord(ctx, task).catch(() => undefined);
|
|
757
|
+
void appendAgentEvent(ctx, id, { phase: 'progress', message: line })
|
|
758
|
+
.catch(() => undefined);
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
try {
|
|
763
|
+
const result = await runAgentTask(coreInput, combined, managedContext, {
|
|
764
|
+
...deps,
|
|
765
|
+
initialHistory,
|
|
766
|
+
existingWorktree: resumedWorktree,
|
|
767
|
+
onWorktreeCreated: async (worktree) => {
|
|
768
|
+
task.worktree = worktree;
|
|
769
|
+
await saveAgentRecord(ctx, task);
|
|
770
|
+
},
|
|
771
|
+
captureHistory: (history) => { captured = [...history]; },
|
|
772
|
+
});
|
|
773
|
+
task.result = result;
|
|
774
|
+
task.history = captured;
|
|
775
|
+
task.status = abort.signal.aborted ? 'stopped' : result.ok ? 'completed' : 'failed';
|
|
776
|
+
if (task.worktree && !await access(task.worktree.path).then(() => true).catch(() => false)) {
|
|
777
|
+
task.worktree = undefined;
|
|
778
|
+
}
|
|
779
|
+
task.updatedAt = Date.now();
|
|
780
|
+
await saveAgentRecord(ctx, task);
|
|
781
|
+
await appendAgentEvent(ctx, id, { phase: task.status, ok: result.ok });
|
|
782
|
+
return result;
|
|
783
|
+
} catch (error) {
|
|
784
|
+
task.status = abort.signal.aborted ? 'stopped' : 'failed';
|
|
785
|
+
task.result = { ok: false, error: (error as Error).message };
|
|
786
|
+
task.history = captured;
|
|
787
|
+
task.updatedAt = Date.now();
|
|
788
|
+
await saveAgentRecord(ctx, task).catch(() => undefined);
|
|
789
|
+
await appendAgentEvent(ctx, id, { phase: task.status, error: (error as Error).message })
|
|
790
|
+
.catch(() => undefined);
|
|
791
|
+
return task.result;
|
|
792
|
+
} finally {
|
|
793
|
+
task.events.emit('change');
|
|
794
|
+
settle();
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
if (input.run_in_background) {
|
|
799
|
+
void run();
|
|
800
|
+
return {
|
|
801
|
+
ok: true,
|
|
802
|
+
content: `后台 Agent 已启动。agent_id=${id}\n使用 TaskOutput 获取进度/结果,TaskStop 停止。`,
|
|
803
|
+
metadata: {
|
|
804
|
+
kind: 'agent', agentId: id, status: 'running',
|
|
805
|
+
agentType: input.agent_type ?? 'explore', description: input.description,
|
|
806
|
+
isolation: input.isolation ?? 'none', turns: 0, toolCalls: 0,
|
|
807
|
+
failedToolCalls: 0, durationMs: 0, changedFiles: [],
|
|
808
|
+
},
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
const result = await run();
|
|
812
|
+
if (result.ok && result.metadata?.kind === 'agent') {
|
|
813
|
+
result.metadata.agentId = id;
|
|
814
|
+
result.metadata.status = 'completed';
|
|
815
|
+
result.content = `agent_id=${id}\n${result.content}`;
|
|
816
|
+
} else if (!result.ok) {
|
|
817
|
+
result.error = `agent_id=${id}\n${result.error}`;
|
|
818
|
+
}
|
|
819
|
+
return result;
|
|
820
|
+
}
|
|
821
|
+
|
|
687
822
|
// ---------------- 5. 导出 ----------------
|
|
688
823
|
|
|
689
824
|
export const agentTool: Tool<AgentInput> = {
|
|
@@ -698,5 +833,5 @@ export const agentTool: Tool<AgentInput> = {
|
|
|
698
833
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
699
834
|
interruptBehavior: () => 'block',
|
|
700
835
|
call,
|
|
701
|
-
callWithContext:
|
|
836
|
+
callWithContext: runManagedAgentTask,
|
|
702
837
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readRuntimeJson, sessionRuntimeDir, withRuntimeFileLock, writeRuntimeJson, } from '../../session/runtimeStore.js';
|
|
4
|
+
export function createAgentTaskState() {
|
|
5
|
+
return { tasks: new Map() };
|
|
6
|
+
}
|
|
7
|
+
function dir(ctx, id) {
|
|
8
|
+
return join(sessionRuntimeDir(ctx), 'agents', id);
|
|
9
|
+
}
|
|
10
|
+
export async function appendAgentEvent(ctx, id, event) {
|
|
11
|
+
const path = join(dir(ctx, id), 'events.jsonl');
|
|
12
|
+
await mkdir(dir(ctx, id), { recursive: true, mode: 0o700 });
|
|
13
|
+
await withRuntimeFileLock(path, () => appendFile(path, `${JSON.stringify({ at: Date.now(), ...event })}\n`, {
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
mode: 0o600,
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
export async function saveAgentRecord(ctx, record) {
|
|
19
|
+
const persisted = {
|
|
20
|
+
id: record.id,
|
|
21
|
+
status: record.status,
|
|
22
|
+
description: record.description,
|
|
23
|
+
input: record.input,
|
|
24
|
+
createdAt: record.createdAt,
|
|
25
|
+
updatedAt: record.updatedAt,
|
|
26
|
+
...(record.progress ? { progress: record.progress } : {}),
|
|
27
|
+
...(record.worktree ? { worktree: record.worktree } : {}),
|
|
28
|
+
...(record.result ? { result: record.result } : {}),
|
|
29
|
+
};
|
|
30
|
+
const recordDir = dir(ctx, record.id);
|
|
31
|
+
await withRuntimeFileLock(join(recordDir, '.record'), async () => {
|
|
32
|
+
await writeRuntimeJson(join(recordDir, 'state.json'), persisted);
|
|
33
|
+
if (record.history) {
|
|
34
|
+
await writeRuntimeJson(join(recordDir, 'history.json'), record.history);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export async function loadAgentRecord(ctx, id) {
|
|
39
|
+
const state = await readRuntimeJson(join(dir(ctx, id), 'state.json'), undefined);
|
|
40
|
+
if (!state)
|
|
41
|
+
return undefined;
|
|
42
|
+
const history = await readRuntimeJson(join(dir(ctx, id), 'history.json'), undefined);
|
|
43
|
+
return {
|
|
44
|
+
...state,
|
|
45
|
+
status: state.status === 'running' ? 'interrupted' : state.status,
|
|
46
|
+
history,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function getLiveAgentTask(state, id) {
|
|
50
|
+
return state?.tasks.get(id);
|
|
51
|
+
}
|
|
52
|
+
export async function stopAllAgentTasks(state) {
|
|
53
|
+
const running = [...state.tasks.values()].filter((t) => t.status === 'running');
|
|
54
|
+
running.forEach((t) => t.abort.abort());
|
|
55
|
+
await Promise.race([
|
|
56
|
+
Promise.allSettled(running.map((t) => t.completion)).then(() => undefined),
|
|
57
|
+
new Promise((r) => setTimeout(r, 5000)),
|
|
58
|
+
]);
|
|
59
|
+
state.tasks.clear();
|
|
60
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import type { Message, ToolCallContext, ToolResult } from '../../types.js';
|
|
5
|
+
import type { AgentWorktree } from './worktree.js';
|
|
6
|
+
import {
|
|
7
|
+
readRuntimeJson,
|
|
8
|
+
sessionRuntimeDir,
|
|
9
|
+
withRuntimeFileLock,
|
|
10
|
+
writeRuntimeJson,
|
|
11
|
+
} from '../../session/runtimeStore.js';
|
|
12
|
+
|
|
13
|
+
export type AgentTaskStatus =
|
|
14
|
+
'running' | 'completed' | 'failed' | 'stopped' | 'interrupted';
|
|
15
|
+
export interface AgentTaskRecord {
|
|
16
|
+
id: string;
|
|
17
|
+
status: AgentTaskStatus;
|
|
18
|
+
description: string;
|
|
19
|
+
input: Record<string, unknown>;
|
|
20
|
+
createdAt: number;
|
|
21
|
+
updatedAt: number;
|
|
22
|
+
progress?: string;
|
|
23
|
+
worktree?: AgentWorktree;
|
|
24
|
+
result?: ToolResult;
|
|
25
|
+
history?: Message[];
|
|
26
|
+
}
|
|
27
|
+
export interface LiveAgentTask extends AgentTaskRecord {
|
|
28
|
+
abort: AbortController;
|
|
29
|
+
completion: Promise<void>;
|
|
30
|
+
events: EventEmitter;
|
|
31
|
+
}
|
|
32
|
+
export interface AgentTaskState {
|
|
33
|
+
tasks: Map<string, LiveAgentTask>;
|
|
34
|
+
}
|
|
35
|
+
export function createAgentTaskState(): AgentTaskState {
|
|
36
|
+
return { tasks: new Map() };
|
|
37
|
+
}
|
|
38
|
+
function dir(ctx: ToolCallContext, id: string) {
|
|
39
|
+
return join(sessionRuntimeDir(ctx), 'agents', id);
|
|
40
|
+
}
|
|
41
|
+
export async function appendAgentEvent(
|
|
42
|
+
ctx: ToolCallContext,
|
|
43
|
+
id: string,
|
|
44
|
+
event: Record<string, unknown>,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
const path = join(dir(ctx, id), 'events.jsonl');
|
|
47
|
+
await mkdir(dir(ctx, id), { recursive: true, mode: 0o700 });
|
|
48
|
+
await withRuntimeFileLock(path, () =>
|
|
49
|
+
appendFile(path, `${JSON.stringify({ at: Date.now(), ...event })}\n`, {
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
mode: 0o600,
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
export async function saveAgentRecord(
|
|
56
|
+
ctx: ToolCallContext,
|
|
57
|
+
record: AgentTaskRecord,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const persisted: AgentTaskRecord = {
|
|
60
|
+
id: record.id,
|
|
61
|
+
status: record.status,
|
|
62
|
+
description: record.description,
|
|
63
|
+
input: record.input,
|
|
64
|
+
createdAt: record.createdAt,
|
|
65
|
+
updatedAt: record.updatedAt,
|
|
66
|
+
...(record.progress ? { progress: record.progress } : {}),
|
|
67
|
+
...(record.worktree ? { worktree: record.worktree } : {}),
|
|
68
|
+
...(record.result ? { result: record.result } : {}),
|
|
69
|
+
};
|
|
70
|
+
const recordDir = dir(ctx, record.id);
|
|
71
|
+
await withRuntimeFileLock(join(recordDir, '.record'), async () => {
|
|
72
|
+
await writeRuntimeJson(join(recordDir, 'state.json'), persisted);
|
|
73
|
+
if (record.history) {
|
|
74
|
+
await writeRuntimeJson(join(recordDir, 'history.json'), record.history);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function loadAgentRecord(
|
|
79
|
+
ctx: ToolCallContext,
|
|
80
|
+
id: string,
|
|
81
|
+
): Promise<AgentTaskRecord | undefined> {
|
|
82
|
+
const state = await readRuntimeJson<AgentTaskRecord | undefined>(
|
|
83
|
+
join(dir(ctx, id), 'state.json'),
|
|
84
|
+
undefined,
|
|
85
|
+
);
|
|
86
|
+
if (!state) return undefined;
|
|
87
|
+
const history = await readRuntimeJson<Message[] | undefined>(
|
|
88
|
+
join(dir(ctx, id), 'history.json'),
|
|
89
|
+
undefined,
|
|
90
|
+
);
|
|
91
|
+
return {
|
|
92
|
+
...state,
|
|
93
|
+
status: state.status === 'running' ? 'interrupted' : state.status,
|
|
94
|
+
history,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function getLiveAgentTask(
|
|
98
|
+
state: AgentTaskState | undefined,
|
|
99
|
+
id: string,
|
|
100
|
+
): LiveAgentTask | undefined {
|
|
101
|
+
return state?.tasks.get(id);
|
|
102
|
+
}
|
|
103
|
+
export async function stopAllAgentTasks(state: AgentTaskState): Promise<void> {
|
|
104
|
+
const running = [...state.tasks.values()].filter(
|
|
105
|
+
(t) => t.status === 'running',
|
|
106
|
+
);
|
|
107
|
+
running.forEach((t) => t.abort.abort());
|
|
108
|
+
await Promise.race([
|
|
109
|
+
Promise.allSettled(running.map((t) => t.completion)).then(() => undefined),
|
|
110
|
+
new Promise<void>((r) => setTimeout(r, 5000)),
|
|
111
|
+
]);
|
|
112
|
+
state.tasks.clear();
|
|
113
|
+
}
|