@zhushanwen/pi-subagent-workflow 0.1.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/agents/context-builder.md +17 -0
- package/agents/general-purpose.md +16 -0
- package/agents/oracle.md +17 -0
- package/agents/planner.md +17 -0
- package/agents/researcher.md +17 -0
- package/agents/reviewer.md +17 -0
- package/agents/scout.md +17 -0
- package/agents/worker.md +16 -0
- package/examples/README.md +43 -0
- package/examples/chain.example.js +92 -0
- package/examples/map-reduce.example.js +99 -0
- package/examples/parallel.example.js +82 -0
- package/examples/scatter-gather.example.js +106 -0
- package/index.ts +1 -0
- package/package.json +66 -0
- package/skills/workflow-script-format/SKILL.md +328 -0
- package/src/execution/__tests__/agent-registry.test.ts +164 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
- package/src/execution/__tests__/config.test.ts +110 -0
- package/src/execution/__tests__/crash-recovery.test.ts +311 -0
- package/src/execution/__tests__/execute-nesting.test.ts +359 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
- package/src/execution/__tests__/execution-record.test.ts +959 -0
- package/src/execution/__tests__/finalized-marker.test.ts +82 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
- package/src/execution/__tests__/format.test.ts +320 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/list-component.test.ts +347 -0
- package/src/execution/__tests__/model-resolver.test.ts +356 -0
- package/src/execution/__tests__/output-collector.test.ts +61 -0
- package/src/execution/__tests__/path-encoding.test.ts +75 -0
- package/src/execution/__tests__/pi-invocation.test.ts +73 -0
- package/src/execution/__tests__/record-store.test.ts +545 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
- package/src/execution/__tests__/sdk-contract.test.ts +272 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +247 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
- package/src/execution/__tests__/spawn-args.test.ts +244 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/subagent-service.test.ts +678 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +381 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/tool-action.test.ts +330 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/worktree-manager.test.ts +423 -0
- package/src/execution/__tests__/worktree-registry.test.ts +161 -0
- package/src/execution/agent-registry.ts +252 -0
- package/src/execution/agent-result-mapper.ts +84 -0
- package/src/execution/alive-store.ts +92 -0
- package/src/execution/best-effort.ts +30 -0
- package/src/execution/concurrency-pool.ts +84 -0
- package/src/execution/config.ts +73 -0
- package/src/execution/execute-options-mapper.ts +86 -0
- package/src/execution/execution-record.ts +778 -0
- package/src/execution/finalized-marker.ts +51 -0
- package/src/execution/model-config-service.ts +225 -0
- package/src/execution/model-resolver.ts +247 -0
- package/src/execution/notifier.ts +168 -0
- package/src/execution/output-collector.ts +88 -0
- package/src/execution/path-encoding.ts +34 -0
- package/src/execution/pi-invocation.ts +70 -0
- package/src/execution/record-store.ts +350 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +98 -0
- package/src/execution/session-reconstructor.ts +450 -0
- package/src/execution/session-runner.ts +725 -0
- package/src/execution/spawn-event-adapter.ts +150 -0
- package/src/execution/subagent-service.ts +973 -0
- package/src/execution/subprocess-agent-runner.ts +108 -0
- package/src/execution/temp-prompt.ts +57 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +88 -0
- package/src/execution/types.ts +634 -0
- package/src/execution/worktree-manager.ts +285 -0
- package/src/execution/worktree-registry.ts +144 -0
- package/src/index.ts +454 -0
- package/src/interface/bg-notify-render.ts +286 -0
- package/src/interface/commands.ts +157 -0
- package/src/interface/format.ts +501 -0
- package/src/interface/gui-adapter.ts +136 -0
- package/src/interface/helpers.ts +110 -0
- package/src/interface/list-component.ts +643 -0
- package/src/interface/list-shared.ts +84 -0
- package/src/interface/list-view.ts +373 -0
- package/src/interface/reentry-guard.ts +30 -0
- package/src/interface/subagent-actions.ts +294 -0
- package/src/interface/subagent-tool.ts +294 -0
- package/src/interface/subagents.ts +30 -0
- package/src/interface/tool-render.ts +333 -0
- package/src/interface/tool-workflow-script.ts +351 -0
- package/src/interface/tool-workflow.ts +485 -0
- package/src/interface/views/WorkflowsView.ts +944 -0
- package/src/interface/views/detail-content.ts +298 -0
- package/src/interface/views/format.ts +320 -0
- package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
- package/src/orchestration/__tests__/config-loader.test.ts +381 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
- package/src/orchestration/__tests__/script-lint.test.ts +347 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
- package/src/orchestration/agent-opts-resolver.ts +128 -0
- package/src/orchestration/concurrency-gate.ts +69 -0
- package/src/orchestration/config-loader.ts +313 -0
- package/src/orchestration/error-recovery.ts +578 -0
- package/src/orchestration/execute-agent-call.ts +174 -0
- package/src/orchestration/jsonl-run-store.ts +292 -0
- package/src/orchestration/launcher.ts +368 -0
- package/src/orchestration/lifecycle.ts +373 -0
- package/src/orchestration/models/__tests__/budget.test.ts +367 -0
- package/src/orchestration/models/agent-call.ts +76 -0
- package/src/orchestration/models/budget.ts +148 -0
- package/src/orchestration/models/ports.ts +165 -0
- package/src/orchestration/models/run-runtime.ts +91 -0
- package/src/orchestration/models/run-spec.ts +54 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +102 -0
- package/src/orchestration/models/types.ts +242 -0
- package/src/orchestration/models/workflow-run.ts +275 -0
- package/src/orchestration/models/workflow-script-registry.ts +32 -0
- package/src/orchestration/models/workflow-script.ts +90 -0
- package/src/orchestration/node-ops.ts +192 -0
- package/src/orchestration/script-lint.ts +387 -0
- package/src/orchestration/skill-discovery.ts +60 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +93 -0
- package/src/orchestration/worker-script-builder.ts +281 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +128 -0
- package/src/shared/__tests__/resource-discovery.test.ts +226 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/resource-discovery.ts +535 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/execution/subprocess-agent-runner.ts
|
|
2
|
+
//
|
|
3
|
+
// Wave 4: SubprocessAgentRunner 委托重写
|
|
4
|
+
//
|
|
5
|
+
// 从"自己 spawn pi"改为"委托 SubagentService.executeAndAwait"。
|
|
6
|
+
// MF-3: 从 orchestration/infra 迁入 execution,同层委托 SubagentService。
|
|
7
|
+
//
|
|
8
|
+
// 接线层级:
|
|
9
|
+
// [跨模块 port] implements AgentRunner(orchestration/models/ports.ts)
|
|
10
|
+
// [模块内直调] mapToExecuteOptions + mergeTimeoutSignal(execute-options-mapper)
|
|
11
|
+
// [模块内直调] this.subagentService.executeAndAwait
|
|
12
|
+
//
|
|
13
|
+
// 设计基线:
|
|
14
|
+
// D-A2(映射归 adapter)/ D-A8(onEvent 桥接)/ D-A9(timeoutMs 合并 signal)/
|
|
15
|
+
// D-008(model 填底,不调 resolveModel)/ BC-9(timeoutMs 行为)/ BC-10(live-record 进度)
|
|
16
|
+
|
|
17
|
+
import type { AgentEvent } from "../shared/agent-event.ts";
|
|
18
|
+
import type { AgentRunner } from "../orchestration/models/ports.ts";
|
|
19
|
+
import type { AgentCallOpts, AgentResult } from "../orchestration/models/types.ts";
|
|
20
|
+
import type { ModelInfo } from "./model-resolver.ts";
|
|
21
|
+
import type { SubagentService } from "./subagent-service.ts";
|
|
22
|
+
import type { ExecuteOptions } from "./types.ts";
|
|
23
|
+
import { mapToExecuteOptions, mergeTimeoutSignal } from "./execute-options-mapper.ts";
|
|
24
|
+
|
|
25
|
+
// ── 构造依赖(per-session 注入)──
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SAR 构造参数。
|
|
29
|
+
*
|
|
30
|
+
* per-session(makeDeps 时创建,随 session 销毁):
|
|
31
|
+
* - subagentService: 进程单例(getSubagentService()),委托目标
|
|
32
|
+
* - ctxModel: 当前 session 主 agent 模型(D-008 model 填底,opts.model 空时用此)
|
|
33
|
+
*
|
|
34
|
+
* 不含 agentRegistry/sessionDir/activeTempFiles——resolveAgentOpts 在 orchestration 层
|
|
35
|
+
* 完成(D-A3),结果已填进 AgentCallOpts.skillPath/schemaEnv,SAR 收已解析的 opts。
|
|
36
|
+
*/
|
|
37
|
+
export interface SubprocessAgentRunnerDeps {
|
|
38
|
+
subagentService: SubagentService;
|
|
39
|
+
ctxModel?: ModelInfo;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* AgentRunner port 实现——委托 SubagentService.executeAndAwait。
|
|
44
|
+
*
|
|
45
|
+
* 层归属:execution(MF-3 从 orchestration 迁入)。implements orchestration 层 port。
|
|
46
|
+
*
|
|
47
|
+
* 行为契约(BC-1/BC-2/BC-9/BC-10):
|
|
48
|
+
* - opts 形状不变(AgentCallOpts,含 resolveAgentOpts 填的 skillPath/schemaEnv)
|
|
49
|
+
* - result 形状不变(workflow AgentResult: content/parsedOutput/usage/error/toolCalls)
|
|
50
|
+
* - 不 reject——失败信息入 result.error(与 executeAgentCall 契约一致)
|
|
51
|
+
* - timeoutMs 合并 signal(D-A9);onEvent 桥接 AgentEvent→workflow liveRecord(D-A8)
|
|
52
|
+
*/
|
|
53
|
+
export class SubprocessAgentRunner implements AgentRunner {
|
|
54
|
+
private readonly subagentService: SubagentService;
|
|
55
|
+
private readonly ctxModel: ModelInfo | undefined;
|
|
56
|
+
|
|
57
|
+
constructor(deps: SubprocessAgentRunnerDeps) {
|
|
58
|
+
this.subagentService = deps.subagentService;
|
|
59
|
+
this.ctxModel = deps.ctxModel;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 执行单次 agent 调用:委托 SubagentService.executeAndAwait。
|
|
64
|
+
*
|
|
65
|
+
* 接线链路:
|
|
66
|
+
* mergeTimeoutSignal → mapToExecuteOptions →
|
|
67
|
+
* this.subagentService.executeAndAwait → 返回 AgentResult
|
|
68
|
+
*
|
|
69
|
+
* 错误处理:不 reject。
|
|
70
|
+
* - executeAndAwait 内部失败 → 返回 AgentResult(success:false) → 已映射 error 字段
|
|
71
|
+
* - executeAndAwait throw(嵌套超限 BC-12)→ catch → AgentResult.error
|
|
72
|
+
* - spawn 级失败已由 runSpawn 内部收口为 failed AgentResult(不逃逸)
|
|
73
|
+
*/
|
|
74
|
+
async run(
|
|
75
|
+
opts: AgentCallOpts,
|
|
76
|
+
signal: AbortSignal,
|
|
77
|
+
onEvent?: (event: AgentEvent) => void,
|
|
78
|
+
): Promise<AgentResult> {
|
|
79
|
+
const startedAt = Date.now();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// ── D-A9: timeoutMs 合并 signal ──
|
|
83
|
+
const mergedSignal = mergeTimeoutSignal(signal, opts.timeoutMs);
|
|
84
|
+
|
|
85
|
+
// ── D-A2 + D-008: AgentCallOpts → ExecuteOptions 映射 ──
|
|
86
|
+
const mappedOpts: ExecuteOptions = mapToExecuteOptions(opts, this.ctxModel);
|
|
87
|
+
|
|
88
|
+
// ── D-A8: onEvent 桥接 ──
|
|
89
|
+
// executeAndAwait 发强类型 AgentEvent(session-runner handleSdkEvent 出口)。
|
|
90
|
+
// workflow 的 onEvent 闭包(error-recovery.ts dispatchAgentCall)类型已升级为
|
|
91
|
+
// (event: AgentEvent) => updateFromEvent(liveRecord, event)(D-005)。
|
|
92
|
+
// SAR 直接透传 onEvent——类型对齐后零桥接开销。
|
|
93
|
+
const bridgedOnEvent = onEvent;
|
|
94
|
+
|
|
95
|
+
// ── 核心委托 ──
|
|
96
|
+
return await this.subagentService.executeAndAwait(mappedOpts, mergedSignal, bridgedOnEvent);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
// executeAndAwait throw(嵌套超限 ForkDepthExceededError,BC-12)或未预期异常 → 不 reject,入 error。
|
|
99
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
100
|
+
return {
|
|
101
|
+
content: "",
|
|
102
|
+
durationMs: Date.now() - startedAt,
|
|
103
|
+
error: message,
|
|
104
|
+
toolCalls: [],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/core/temp-prompt.ts
|
|
2
|
+
//
|
|
3
|
+
// 将 agent systemPrompt 写入临时文件,供 pi CLI --append-system-prompt 使用。
|
|
4
|
+
// Core 叶子原语(仅依赖 node 内置)。
|
|
5
|
+
//
|
|
6
|
+
// pi CLI 的 --append-system-prompt 接受文件路径(非内联字符串),故 spawn 前
|
|
7
|
+
// 需把 systemPrompt 落盘。每次调用创建唯一临时目录,用完由 runSpawn 清理。
|
|
8
|
+
//
|
|
9
|
+
// 移植自 nicobailon subagent example,去掉 withFileMutationQueue(目录已唯一,
|
|
10
|
+
// 无并发写入风险,queue 冗余)。
|
|
11
|
+
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
|
|
16
|
+
/** 临时 prompt 文件创建结果。dir 供调用方清理。 */
|
|
17
|
+
export interface TempPromptFile {
|
|
18
|
+
/** 临时目录绝对路径(mkdtemp 创建,调用方负责删除)。 */
|
|
19
|
+
dir: string;
|
|
20
|
+
/** prompt 文件绝对路径(dir 下)。 */
|
|
21
|
+
filePath: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 将 systemPrompt 写入临时文件。
|
|
26
|
+
*
|
|
27
|
+
* @param agentName agent 名称(用于文件名,非法字符替换为 _)
|
|
28
|
+
* @param prompt systemPrompt 全文
|
|
29
|
+
* @returns 临时文件信息(dir + filePath)
|
|
30
|
+
*
|
|
31
|
+
* 安全:文件权限 0o600(仅 owner 读写),防止其他用户读取 prompt 内容。
|
|
32
|
+
* 目录用 mkdtemp 保证唯一,无需额外锁。
|
|
33
|
+
*/
|
|
34
|
+
export async function writePromptToTempFile(
|
|
35
|
+
agentName: string,
|
|
36
|
+
prompt: string,
|
|
37
|
+
): Promise<TempPromptFile> {
|
|
38
|
+
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-"));
|
|
39
|
+
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
40
|
+
const filePath = path.join(dir, `prompt-${safeName}.md`);
|
|
41
|
+
await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
|
|
42
|
+
return { dir, filePath };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 删除临时 prompt 目录(含文件)。best-effort,失败不抛。
|
|
47
|
+
*
|
|
48
|
+
* runSpawn 的 finally 调用,确保临时文件不泄漏。子进程可能仍持有文件句柄
|
|
49
|
+
* (--append-system-prompt 读取后即释放),rm 用 recursive 容错。
|
|
50
|
+
*/
|
|
51
|
+
export async function cleanupTempPrompt(file: TempPromptFile): Promise<void> {
|
|
52
|
+
try {
|
|
53
|
+
await fs.promises.rm(file.dir, { recursive: true, force: true });
|
|
54
|
+
} catch {
|
|
55
|
+
// best-effort:临时文件泄漏不影响功能,OS tmpdir 清理机制兜底
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/runtime/execution/tombstone-store.ts
|
|
2
|
+
//
|
|
3
|
+
// Cancelled 状态的 sidecar 持久化。
|
|
4
|
+
//
|
|
5
|
+
// 背景:用户 cancel background subagent 时 session.abort() → prompt() reject →
|
|
6
|
+
// session.jsonl 被中途截断(无最终 assistant message,无 stopReason 标记)。所以
|
|
7
|
+
// cancelled 状态无法从 session.jsonl 可靠检测。本模块在 session.jsonl 旁写一个
|
|
8
|
+
// `.cancelled` sidecar 文件(单行 JSON),collectRecords 重建时读它 override status。
|
|
9
|
+
//
|
|
10
|
+
// 设计:单条 cancelled = 单 sidecar,无全局 index 可损坏,无并发问题,被现有
|
|
11
|
+
// session-file-gc 的目录扫描自然清理(GC 已扩展为也清理 .cancelled)。YAGNI 更重方案。
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
|
|
15
|
+
// ============================================================
|
|
16
|
+
// 类型
|
|
17
|
+
// ============================================================
|
|
18
|
+
|
|
19
|
+
/** Tombstone 内容(单行 JSON)。 */
|
|
20
|
+
export interface CancelledTombstone {
|
|
21
|
+
id: string;
|
|
22
|
+
status: "cancelled";
|
|
23
|
+
agent: string;
|
|
24
|
+
startedAt: number;
|
|
25
|
+
endedAt: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============================================================
|
|
29
|
+
// 公开函数
|
|
30
|
+
// ============================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 在 session.jsonl 旁写 cancelled tombstone。
|
|
34
|
+
* best-effort:任何 I/O 错误静默(cancel 已成功,持久化是次要的——内存 record 即将被
|
|
35
|
+
* archive 淘汰,下次读靠 tombstone 标记 cancelled)。
|
|
36
|
+
*/
|
|
37
|
+
export function writeCancelledTombstone(sessionFile: string, meta: CancelledTombstone): void {
|
|
38
|
+
try {
|
|
39
|
+
const tombstonePath = `${sessionFile}.cancelled`;
|
|
40
|
+
fs.writeFileSync(tombstonePath, `${JSON.stringify(meta)}\n`, "utf-8");
|
|
41
|
+
} catch (_e) {
|
|
42
|
+
void _e; // 静默:写失败不阻断 cancel 主流程(status 已在内存 record 上设好)。
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 读 session.jsonl 旁的 cancelled tombstone。
|
|
48
|
+
* 返回 undefined:sidecar 不存在 / 损坏 / 解析失败。
|
|
49
|
+
*/
|
|
50
|
+
export function readCancelledTombstone(sessionFile: string): CancelledTombstone | undefined {
|
|
51
|
+
let raw: string;
|
|
52
|
+
try {
|
|
53
|
+
raw = fs.readFileSync(`${sessionFile}.cancelled`, "utf-8");
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined; // sidecar 不存在(正常——非 cancelled record 无 sidecar)。
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(raw) as Partial<CancelledTombstone>;
|
|
59
|
+
if (
|
|
60
|
+
typeof parsed.id === "string" &&
|
|
61
|
+
parsed.status === "cancelled" &&
|
|
62
|
+
typeof parsed.agent === "string" &&
|
|
63
|
+
typeof parsed.startedAt === "number" &&
|
|
64
|
+
typeof parsed.endedAt === "number"
|
|
65
|
+
) {
|
|
66
|
+
return parsed as CancelledTombstone;
|
|
67
|
+
}
|
|
68
|
+
return undefined; // 结构不合法 → 降级。
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined; // JSON 损坏 → 降级。
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/core/turn-limiter.ts
|
|
2
|
+
//
|
|
3
|
+
// soft/hard turn 限制器。maxTurns 到达 → steer 提醒收尾;
|
|
4
|
+
// graceTurns 后仍不结束 → abort。
|
|
5
|
+
|
|
6
|
+
/** steer 提醒消息:要求 agent 总结已完成/未完成/下一步,不得谎报完成。 */
|
|
7
|
+
const WRAP_UP_MESSAGE = [
|
|
8
|
+
"You have reached your turn limit. Wrap up now:",
|
|
9
|
+
"1. Summarize what you have completed (with evidence: file paths, command output).",
|
|
10
|
+
"2. List what remains undone and why.",
|
|
11
|
+
"3. State the single most important next step for whoever continues.",
|
|
12
|
+
"Do NOT claim the task is complete if any part remains unfinished.",
|
|
13
|
+
].join(" ");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 预防性 wrap-up 提示(启动时注入 --append-system-prompt)。
|
|
17
|
+
*
|
|
18
|
+
* spawn 模式下 runSpawn 无法在达 maxTurns 时运行时注入 steer(pi `--mode json` 是
|
|
19
|
+
* single-shot,无 stdin 命令通道;见 session-runner.ts limiter 注释)。作为短期补偿,
|
|
20
|
+
* 在启动时预置此预防性指令,让 agent 在感知接近上限时主动收尾。
|
|
21
|
+
*
|
|
22
|
+
* 长期方案是切到 pi `--mode rpc`(支持运行时 steer 命令),见 follow-up。
|
|
23
|
+
*/
|
|
24
|
+
export const WRAP_UP_HINT = [
|
|
25
|
+
`You have a turn limit. As you sense you are approaching it, proactively wrap up:`,
|
|
26
|
+
"1. Summarize what you have completed (with evidence: file paths, command output).",
|
|
27
|
+
"2. List what remains undone and why.",
|
|
28
|
+
"3. State the single most important next step for whoever continues.",
|
|
29
|
+
"Do NOT claim the task is complete if any part remains unfinished.",
|
|
30
|
+
].join(" ");
|
|
31
|
+
|
|
32
|
+
/** turn limiter 配置。 */
|
|
33
|
+
export interface TurnLimiterOptions {
|
|
34
|
+
maxTurns: number;
|
|
35
|
+
graceTurns: number;
|
|
36
|
+
steer: (msg: string) => void;
|
|
37
|
+
abort: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* soft/hard turn 限制。
|
|
42
|
+
*
|
|
43
|
+
* onTurnEnd(currentTurns):
|
|
44
|
+
* 已 aborted 或 maxTurns<=0(禁用)→ 直接 return
|
|
45
|
+
* currentTurns >= maxTurns 且未 steer → steer(WRAP_UP_MESSAGE)(仅一次)
|
|
46
|
+
* 已 steer 且 currentTurns >= maxTurns + graceTurns → abort()(仅一次)
|
|
47
|
+
*
|
|
48
|
+
* maxTurns<=0 表示不限(limit=Infinity,永不触发)。
|
|
49
|
+
* graceTurns<=0 时 steer 后下一 turn 即 abort。
|
|
50
|
+
*/
|
|
51
|
+
export interface TurnLimiter {
|
|
52
|
+
/** 每次 turn_end 调用。 */
|
|
53
|
+
onTurnEnd(currentTurns: number): void;
|
|
54
|
+
/** 是否已发过 steer(诊断用)。 */
|
|
55
|
+
readonly didSteer: boolean;
|
|
56
|
+
/** 是否已 abort(诊断用)。 */
|
|
57
|
+
readonly didAbort: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 工厂函数。 */
|
|
61
|
+
export function createTurnLimiter(opts: TurnLimiterOptions): TurnLimiter {
|
|
62
|
+
let steered = false;
|
|
63
|
+
let aborted = false;
|
|
64
|
+
const limit = opts.maxTurns > 0 ? opts.maxTurns : Infinity;
|
|
65
|
+
const grace = opts.graceTurns > 0 ? opts.graceTurns : 0;
|
|
66
|
+
|
|
67
|
+
const onTurnEnd = (turn: number): void => {
|
|
68
|
+
if (aborted || !Number.isFinite(limit)) return;
|
|
69
|
+
if (!steered && turn >= limit) {
|
|
70
|
+
steered = true;
|
|
71
|
+
opts.steer(WRAP_UP_MESSAGE);
|
|
72
|
+
}
|
|
73
|
+
if (steered && turn >= limit + grace) {
|
|
74
|
+
aborted = true;
|
|
75
|
+
opts.abort();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
onTurnEnd,
|
|
81
|
+
get didSteer(): boolean {
|
|
82
|
+
return steered;
|
|
83
|
+
},
|
|
84
|
+
get didAbort(): boolean {
|
|
85
|
+
return aborted;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|