minimal-agent 0.11.0 → 0.13.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 +5 -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/expressions.js +29 -8
- package/plugins/workflow-runner/src/expressions.ts +28 -10
- package/plugins/workflow-runner/src/index.js +50 -20
- package/plugins/workflow-runner/src/index.ts +75 -31
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/recovery.js +9 -2
- package/plugins/workflow-runner/src/recovery.ts +9 -2
- package/plugins/workflow-runner/src/runner.js +31 -5
- package/plugins/workflow-runner/src/runner.ts +33 -5
- package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -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 +91 -61
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
- package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
- package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
- package/plugins/workflow-runner/src/taskIdentity.js +197 -21
- package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.js +113 -7
- package/plugins/workflow-runner/src/types.ts +137 -14
- package/plugins/workflow-runner/src/workflowState.js +455 -54
- package/plugins/workflow-runner/src/workflowState.ts +508 -63
- package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
- package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
- package/plugins/workflow-runner/test/expressions.test.ts +42 -0
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
- package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
- package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
- package/plugins/workflow-runner/test/research.load.test.ts +328 -0
- package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
- package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
- package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
- package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
- package/plugins/workflow-runner/test/word.load.test.ts +9 -9
- package/skills/skill-creator/scripts/run_eval.py +3 -2
- package/src/cli/print.js +36 -4
- package/src/cli/print.ts +42 -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/config/configService.js +24 -0
- package/src/config/configService.ts +23 -0
- 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/infra/verification.js +25 -18
- package/src/infra/verification.ts +50 -18
- 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 +23 -8
- package/src/plugins/pluginRunner.ts +32 -13
- package/src/prompts/policyRegistry.js +55 -1
- package/src/prompts/policyRegistry.ts +55 -1
- 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/advisor/advisor.js +76 -9
- package/src/tools/advisor/advisor.ts +79 -9
- 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 +80 -15
- package/src/tools/bash/bash.ts +83 -15
- 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 +134 -27
- package/src/tools/powershell/powershell.ts +171 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +249 -29
- package/src/tools/read/read.ts +396 -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 +57 -0
- package/src/tools/shared/permanentDeleteGuard.ts +71 -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/webbrowser/browserControl.js +5 -1
- package/src/tools/webbrowser/browserControl.ts +7 -1
- package/src/tools/webbrowser/webbrowser.js +8 -4
- package/src/tools/webbrowser/webbrowser.ts +8 -4
- 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 +22 -4
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
- package/workflows/adaptive-research.yaml +5 -5
- package/workflows/book-review-short.yaml +5 -5
- package/workflows/e2e-write-greet.yaml +2 -2
- package/workflows/illustrate.yaml +12 -12
- package/workflows/plan.yaml +18 -18
- package/workflows/ppt.yaml +201 -201
- package/workflows/ralph-loop.yaml +3 -1
- package/workflows/research.md +449 -0
- package/workflows/research.yaml +873 -0
- package/workflows/webppt.yaml +47 -47
- package/workflows/word.yaml +80 -80
- package/workflows/youtube-shorts.yaml +25 -25
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
|
+
}
|
|
@@ -35,6 +35,9 @@ export const EXPLORE_ALLOWED_TOOLS = [
|
|
|
35
35
|
'Read',
|
|
36
36
|
'Glob',
|
|
37
37
|
'Grep',
|
|
38
|
+
'Snip',
|
|
39
|
+
'LSP',
|
|
40
|
+
'Skill',
|
|
38
41
|
'WebSearch',
|
|
39
42
|
'WebFetch',
|
|
40
43
|
'WebBrowser',
|
|
@@ -42,6 +45,8 @@ export const EXPLORE_ALLOWED_TOOLS = [
|
|
|
42
45
|
'ListMemories',
|
|
43
46
|
'ReviewMemories',
|
|
44
47
|
'SearchConversations',
|
|
48
|
+
'TaskGet',
|
|
49
|
+
'TaskList',
|
|
45
50
|
];
|
|
46
51
|
/** 借鉴官方 EXPLORE_AGENT「READ-ONLY MODE」文案中文化。 */
|
|
47
52
|
const EXPLORE_APPENDIX = `# 本次子任务类型:只读调研(explore)
|
|
@@ -53,6 +53,9 @@ export const EXPLORE_ALLOWED_TOOLS: readonly string[] = [
|
|
|
53
53
|
'Read',
|
|
54
54
|
'Glob',
|
|
55
55
|
'Grep',
|
|
56
|
+
'Snip',
|
|
57
|
+
'LSP',
|
|
58
|
+
'Skill',
|
|
56
59
|
'WebSearch',
|
|
57
60
|
'WebFetch',
|
|
58
61
|
'WebBrowser',
|
|
@@ -60,6 +63,8 @@ export const EXPLORE_ALLOWED_TOOLS: readonly string[] = [
|
|
|
60
63
|
'ListMemories',
|
|
61
64
|
'ReviewMemories',
|
|
62
65
|
'SearchConversations',
|
|
66
|
+
'TaskGet',
|
|
67
|
+
'TaskList',
|
|
63
68
|
];
|
|
64
69
|
|
|
65
70
|
/** 借鉴官方 EXPLORE_AGENT「READ-ONLY MODE」文案中文化。 */
|
package/src/tools/bash/bash.js
CHANGED
|
@@ -56,6 +56,8 @@ import { requestUserChoice } from '../../askBridge.js';
|
|
|
56
56
|
import { classifyCommand } from '../../irreversible/rules.js';
|
|
57
57
|
import { startBackgroundTask } from '../tasks/backgroundTaskService.js';
|
|
58
58
|
import { processExecutionMetadata } from '../tasks/processEvidence.js';
|
|
59
|
+
import { permanentDeleteReason, } from '../shared/permanentDeleteGuard.js';
|
|
60
|
+
import { ToolOutputStore } from '../shared/toolOutputStore.js';
|
|
59
61
|
// ---------------- 0. 常量 ----------------
|
|
60
62
|
const DEFAULT_TIMEOUT_MS = 120_000; // 2 min
|
|
61
63
|
const MAX_TIMEOUT_MS = 600_000; // 10 min
|
|
@@ -103,6 +105,16 @@ function detectWindowsBash() {
|
|
|
103
105
|
return null;
|
|
104
106
|
}
|
|
105
107
|
const WIN_BASH = detectWindowsBash();
|
|
108
|
+
/**
|
|
109
|
+
* 本工具这一刻**真正**把命令交给谁执行。
|
|
110
|
+
*
|
|
111
|
+
* 单一出处:description(告诉模型该写哪种语法)与 permanentDeleteGuard(按哪套动词
|
|
112
|
+
* 匹配删除命令)必须看同一个判断,否则就会出现「description 说这是 cmd、守卫却按
|
|
113
|
+
* bash 匹配」这种两头对不上的裂缝 —— del / rd 会因此直接穿过全部三层防护。
|
|
114
|
+
*/
|
|
115
|
+
function effectiveShellKind() {
|
|
116
|
+
return process.platform === 'win32' && !WIN_BASH ? 'cmd' : 'bash';
|
|
117
|
+
}
|
|
106
118
|
/**
|
|
107
119
|
* G1:stdout / stderr 即时缓冲上限(各自)。命令狂输出(如 cat 大文件 / yes)时,
|
|
108
120
|
* 在最终统一截断之前先把内存封顶,防 OOM。取最终展示截断阈值的 2 倍 —— 正常输出不受影响,
|
|
@@ -309,8 +321,11 @@ Some commands return non-zero exit codes for informational (non-error) reasons.
|
|
|
309
321
|
- \`test\` / \`[\` exit 1 → condition is false (the answer to a question, not a failure)
|
|
310
322
|
Only exit codes ≥ 2 from these commands indicate a real failure. For all other commands, non-zero exit codes are treated as failures normally.
|
|
311
323
|
|
|
312
|
-
# Safety
|
|
313
|
-
|
|
324
|
+
# Safety - three levels, from strict to lenient
|
|
325
|
+
|
|
326
|
+
## 1. Blocked outright (fails before execution - do NOT retry with variants)
|
|
327
|
+
|
|
328
|
+
Catastrophic commands (no legitimate use):
|
|
314
329
|
- \`rm -rf /\` and variants targeting root, $HOME, ~, or system directories (/etc, /usr, /bin, /Windows, /Users, /home, ...)
|
|
315
330
|
- Filesystem destruction: \`mkfs\`, \`format X:\`, \`dd of=/dev/sdX\`, redirects to raw block devices
|
|
316
331
|
- Power management: \`shutdown\`, \`reboot\`, \`halt\`, \`poweroff\`, \`init 0/6\`
|
|
@@ -318,9 +333,37 @@ The following command patterns are blocked at the tool level and will fail befor
|
|
|
318
333
|
- Pipe-to-shell from network: \`curl ... | sh\`, \`wget ... | bash\`, etc.
|
|
319
334
|
- \`chmod 777 /\`, Windows full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`
|
|
320
335
|
|
|
321
|
-
|
|
336
|
+
Permanent deletion (bypasses the OS Trash / Recycle Bin - use the Trash tool instead):
|
|
337
|
+
- \`rm\`, \`unlink\`, \`rmdir\` (any shell-level file/dir deletion, any target)
|
|
338
|
+
- \`find ... -delete\`
|
|
339
|
+
- \`git clean -f\` / \`git clean --force\` (\`-n\` / \`--dry-run\` is fine)
|
|
340
|
+
|
|
341
|
+
To delete files or directories use the **Trash** tool (\`action: "move"\` - recoverable); to clean Git untracked files use Trash with \`action: "git-clean"\`.
|
|
342
|
+
Do NOT work around this by switching to \`find -delete\`, Python \`os.remove\`, Node \`fs.rm\`, or other shell variants - they are blocked the same way.
|
|
343
|
+
If a permanent deletion is truly required, ask the user to run it themselves in their terminal.
|
|
344
|
+
|
|
345
|
+
## 2. Requires user confirmation (irreversible)
|
|
346
|
+
|
|
347
|
+
These have legitimate uses but destroy the last copy - execution pauses and asks the user to confirm. In non-interactive contexts (sub-agents, cron, batch mode) they are automatically cancelled.
|
|
348
|
+
- \`git push --force\` / \`-f\`, and \`git push origin +branch\` (+refspec = force push)
|
|
349
|
+
- \`git push --delete <branch>\` / \`git push :<branch>\` (deleting remote branches)
|
|
350
|
+
- \`git filter-branch\` / \`git filter-repo\` (rewriting history)
|
|
351
|
+
- \`git reflog expire --expire=now\` / \`git gc --prune=now\` (destroying reflog)
|
|
352
|
+
- \`shred -u\` / \`shred --remove\` (overwrite and delete - unrecoverable)
|
|
353
|
+
- SQL data destruction: \`DROP DATABASE\`, \`DROP TABLE\`, \`TRUNCATE TABLE\`
|
|
354
|
+
|
|
355
|
+
## 3. Warning only (command still runs)
|
|
356
|
+
|
|
357
|
+
These execute normally; a \`⚠️ 警告:\` line is prepended to the output as a reminder. They are NOT blocked.
|
|
358
|
+
- \`git reset --hard\` (reflog keeps it recoverable)
|
|
359
|
+
- \`git push --force-with-lease\` (rejects if the remote has unseen commits - the safe force-push form)
|
|
360
|
+
- \`git checkout -- .\` / \`git restore .\` (discards uncommitted changes)
|
|
361
|
+
- \`git branch -D\`, \`git stash drop\` / \`git stash clear\` (local, recoverable via reflog in most cases)
|
|
362
|
+
- \`git commit --amend\`, \`git commit --no-verify\`
|
|
363
|
+
- \`DELETE FROM ...\` (SQL row deletion; DROP/TRUNCATE are in level 2)
|
|
364
|
+
- \`kubectl delete\`, \`terraform destroy\`
|
|
322
365
|
|
|
323
|
-
|
|
366
|
+
Regular Git operations (commit, add, status, log, diff, pull, fetch, checkout <branch>, merge, rebase, etc.) run without extra restrictions.${process.platform !== 'win32'
|
|
324
367
|
? ''
|
|
325
368
|
: WIN_BASH
|
|
326
369
|
? `
|
|
@@ -347,6 +390,13 @@ async function call(input, signal, context) {
|
|
|
347
390
|
error: `安全检查拒绝执行:${forbiddenReason}\n命令:${command}`,
|
|
348
391
|
};
|
|
349
392
|
}
|
|
393
|
+
// 传**实际执行命令的 shell**,不是工具名:win32 上找不到 Git Bash 时下面走的是
|
|
394
|
+
// cmd.exe(见 WIN_BASH 探测),description 也会告诉模型「这里是 cmd」,模型于是写
|
|
395
|
+
// del / rd —— 按 'bash' 匹配的话这两个词一个都拦不住(黑名单与 irreversible 规则表
|
|
396
|
+
// 同样只认 POSIX 形态),等于三层全空。
|
|
397
|
+
const permanentDelete = permanentDeleteReason(command, effectiveShellKind());
|
|
398
|
+
if (permanentDelete)
|
|
399
|
+
return { ok: false, error: `永久删除检查拒绝执行:${permanentDelete}\n命令:${command}` };
|
|
350
400
|
// 4a-1. 不可逆动作闸门:毁掉「最后一份副本」的命令要**停下来问人**。
|
|
351
401
|
//
|
|
352
402
|
// 与上面黑名单的分工:黑名单是「灾难且无正当用途」→ 直接拒;这里是
|
|
@@ -389,18 +439,19 @@ async function call(input, signal, context) {
|
|
|
389
439
|
try {
|
|
390
440
|
const cwd = context?.cwd ?? getWorkingDir();
|
|
391
441
|
const task = await startBackgroundTask(WIN_BASH
|
|
392
|
-
? { command, executable: WIN_BASH, args: ['-c', command], cwd, env: sanitizedChildEnv(), sessionId: context?.sessionId, shellKind: 'bash' }
|
|
442
|
+
? { command, executable: WIN_BASH, args: ['-c', command], cwd, env: sanitizedChildEnv(), sessionId: context?.sessionId, workspaceId: context?.workspaceId, shellKind: 'bash' }
|
|
393
443
|
: {
|
|
394
444
|
command: process.platform === 'win32' ? `chcp 65001 >nul & ${command}` : command,
|
|
395
445
|
cwd,
|
|
396
446
|
env: sanitizedChildEnv(),
|
|
397
447
|
shell: true,
|
|
398
448
|
sessionId: context?.sessionId,
|
|
449
|
+
workspaceId: context?.workspaceId,
|
|
399
450
|
shellKind: 'bash',
|
|
400
451
|
}, context?.sessionState?.backgroundTasks);
|
|
401
452
|
return {
|
|
402
453
|
ok: true,
|
|
403
|
-
content: `${destructiveWarning ? `⚠️ 警告: ${destructiveWarning}\n` : ''}后台任务已启动。task_id=${task.id}\nlog=${task.logPath}\n使用 TaskOutput 增量读取输出,TaskStop 停止进程。`,
|
|
454
|
+
content: `${destructiveWarning ? `⚠️ 警告: ${destructiveWarning}\n` : ''}后台任务已启动。task_id=${task.id.replace(/^proc_/, '')}\nprocess_id=${task.id}\nlog=${task.logPath}\n使用 TaskOutput 增量读取输出,TaskStop 停止进程。`,
|
|
404
455
|
metadata: processExecutionMetadata({ shell: 'bash', mode: 'background', command, status: 'started', taskId: task.id }),
|
|
405
456
|
};
|
|
406
457
|
}
|
|
@@ -414,6 +465,10 @@ async function call(input, signal, context) {
|
|
|
414
465
|
// Windows 无 bash: cmd.exe,命令前缀 chcp 65001 强制 UTF-8 输出
|
|
415
466
|
let stdout = '';
|
|
416
467
|
let stderr = '';
|
|
468
|
+
const outputStore = new ToolOutputStore({
|
|
469
|
+
workspaceId: context?.workspaceId,
|
|
470
|
+
sessionId: context?.sessionId,
|
|
471
|
+
});
|
|
417
472
|
let exitCode = null;
|
|
418
473
|
let timedOut = false;
|
|
419
474
|
let killedBySignal = false;
|
|
@@ -456,14 +511,16 @@ async function call(input, signal, context) {
|
|
|
456
511
|
child.stdout?.setEncoding('utf8');
|
|
457
512
|
child.stderr?.setEncoding('utf8');
|
|
458
513
|
child.stdout?.on('data', (chunk) => {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
514
|
+
if (!outputStore.append('stdout', chunk)) {
|
|
515
|
+
child.stdout?.pause();
|
|
516
|
+
outputStore.onDrain(() => child.stdout?.resume());
|
|
517
|
+
}
|
|
463
518
|
});
|
|
464
519
|
child.stderr?.on('data', (chunk) => {
|
|
465
|
-
if (stderr
|
|
466
|
-
stderr
|
|
520
|
+
if (!outputStore.append('stderr', chunk)) {
|
|
521
|
+
child.stderr?.pause();
|
|
522
|
+
outputStore.onDrain(() => child.stderr?.resume());
|
|
523
|
+
}
|
|
467
524
|
});
|
|
468
525
|
child.on('error', (e) => {
|
|
469
526
|
ac.signal.removeEventListener('abort', onKill);
|
|
@@ -489,6 +546,9 @@ async function call(input, signal, context) {
|
|
|
489
546
|
clearTimeout(timer);
|
|
490
547
|
signal?.removeEventListener('abort', onAbort);
|
|
491
548
|
}
|
|
549
|
+
const storedOutput = await outputStore.finish();
|
|
550
|
+
stdout = storedOutput.stdout;
|
|
551
|
+
stderr = storedOutput.stderr;
|
|
492
552
|
// 4b-2. ATTACH_IMAGE sentinel:脚本可以打一行 `<<<ATTACH_IMAGE:path>>>` 让模型亲眼看图。
|
|
493
553
|
// 协议行**从 content 剥掉**(那是给宿主看的,不是给模型读的噪声);被拒的条目
|
|
494
554
|
// 如实写进输出(静默丢弃会让脚本作者以为图发出去了,无从 debug)。
|
|
@@ -518,13 +578,18 @@ async function call(input, signal, context) {
|
|
|
518
578
|
parts.push(`\n[被信号中断]`);
|
|
519
579
|
}
|
|
520
580
|
parts.push(`\n[exit code: ${exitCode === null ? 'killed' : exitCode}]`);
|
|
581
|
+
const outputReference = storedOutput.path
|
|
582
|
+
? `\n[完整输出: ${storedOutput.path}; bytes=${storedOutput.bytes}; sha256=${storedOutput.sha256}; complete=${storedOutput.complete}]`
|
|
583
|
+
: '';
|
|
521
584
|
let combinedOutput = parts.join('\n');
|
|
522
585
|
// 4d. 大小兜底
|
|
523
|
-
if (combinedOutput.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
586
|
+
if (combinedOutput.length + outputReference.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
587
|
+
const suffix = `\n\n... (输出超过 ${DEFAULT_MAX_RESULT_SIZE_CHARS} 字符,预览已截断)${outputReference}`;
|
|
524
588
|
combinedOutput =
|
|
525
|
-
combinedOutput.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) +
|
|
526
|
-
`\n\n... (输出超过 ${DEFAULT_MAX_RESULT_SIZE_CHARS} 字符,已截断)`;
|
|
589
|
+
combinedOutput.slice(0, Math.max(0, DEFAULT_MAX_RESULT_SIZE_CHARS - suffix.length)) + suffix;
|
|
527
590
|
}
|
|
591
|
+
else
|
|
592
|
+
combinedOutput += outputReference;
|
|
528
593
|
// 4e. 中断 / 超时仍然算硬失败
|
|
529
594
|
// 失败也带上已解析的附图:脚本挂掉前截的那张诊断图正是最该看的。
|
|
530
595
|
if (timedOut || killedBySignal) {
|