@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,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — node-ops
|
|
3
|
+
*
|
|
4
|
+
* 单节点操作 free functions(D-12)。
|
|
5
|
+
*
|
|
6
|
+
* 2 个导出函数:
|
|
7
|
+
* - retryNode(run, callId, deps) — 重置 call + 主线程重跑(不 replaceRuntime)
|
|
8
|
+
* - skipNode(run, callId, deps) — 标记 call done + 占位 result
|
|
9
|
+
*
|
|
10
|
+
* **D.5(方案 A)**:retryNode 的语义是「重试单个失败 call」——只重置 call 状态 +
|
|
11
|
+
* 主线程直接调 executeAgentCall,worker 不重启,已完成调用不受影响(worker 重启是
|
|
12
|
+
* worker-error-retry handleWorkerError 的语义,不在本职责内)。
|
|
13
|
+
*
|
|
14
|
+
* **retryNode 不影响脚本流程**:worker 在首次失败结果被 postAgentResult 投递后即
|
|
15
|
+
* resolve 并删除该 callId 的 pending Promise(worker-script-builder agent-result 分支)。
|
|
16
|
+
* retryNode 的二次 postMessage 因此被 worker 丢弃——新结果只更新 trace/TUI,
|
|
17
|
+
* 回不到脚本(脚本早已带着首次结果往下走)。这是 D.5「不重启 worker」的直接后果:
|
|
18
|
+
* 要让新结果回到脚本必须重启 worker 重跑整个脚本(旧 orchestrator.ts 语义),
|
|
19
|
+
* 与「不干扰已完成调用」的设计意图冲突。故 retryNode 定位为「失败节点的诊断性重跑
|
|
20
|
+
* + trace 刷新」,不承诺改变脚本输出。tool-workflow 的描述已如实声明此语义。
|
|
21
|
+
*
|
|
22
|
+
* **G6-001**:retryNode 前置 status==="running"(paused 下拒绝,要 retry 先 resume)。
|
|
23
|
+
*
|
|
24
|
+
* 层归属:Engine。依赖 LifecycleDeps + WorkflowRun + executeAgentCall。
|
|
25
|
+
*
|
|
26
|
+
* 参考:domain-models.md §失败处理矩阵(retryNode 语义)、clarification.md D.5/G6-001。
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { postBudgetUpdate } from "./error-recovery.ts";
|
|
30
|
+
import { executeAgentCall } from "./execute-agent-call.ts";
|
|
31
|
+
import type { LifecycleDeps } from "./models/ports.ts";
|
|
32
|
+
import type { AgentResult } from "./models/types.ts";
|
|
33
|
+
import type { WorkflowRun } from "./models/workflow-run.ts";
|
|
34
|
+
|
|
35
|
+
// ── skipNode 占位结果 ────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/** skipNode 注入的占位结果(零 usage,避免污染 budget)。 */
|
|
38
|
+
const SKIP_PLACEHOLDER: AgentResult = {
|
|
39
|
+
content: "",
|
|
40
|
+
usage: {
|
|
41
|
+
input: 0,
|
|
42
|
+
output: 0,
|
|
43
|
+
cacheRead: 0,
|
|
44
|
+
cacheWrite: 0,
|
|
45
|
+
cost: 0,
|
|
46
|
+
contextTokens: 0,
|
|
47
|
+
turns: 0,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ── retryNode ────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 重试单个失败 agent call(诊断性重跑 + trace 刷新,不影响脚本流程)。
|
|
55
|
+
*
|
|
56
|
+
* **D.5 修复**:不 replaceRuntime、不重启 worker。只重置 call 状态(status=pending,
|
|
57
|
+
* attempts=0, result=undefined)+ 同步 trace 节点 + 主线程直接调 executeAgentCall。
|
|
58
|
+
* worker 仍在运行,已完成调用不受影响。
|
|
59
|
+
*
|
|
60
|
+
* **结果不回到脚本**:见文件头说明——worker 在首次失败结果投递后已 resolve 并删除
|
|
61
|
+
* 该 callId 的 pending Promise,本函数末尾的 postMessage 通常被 worker 丢弃。新结果
|
|
62
|
+
* 只反映在 trace/TUI,不改变脚本输出(若需让脚本拿新结果,须重启 worker 重跑整个
|
|
63
|
+
* 脚本,与 D.5 冲突,未采用)。
|
|
64
|
+
*
|
|
65
|
+
* 与 worker-error-retry的区别:
|
|
66
|
+
* - handleWorkerError:worker 本身崩溃 → replaceRuntime 重启整个 worker
|
|
67
|
+
* - retryNode:单个 call 失败 → 主线程重跑该 call,worker 不动
|
|
68
|
+
*
|
|
69
|
+
* **G6-001**:前置 status==="running"。paused 下抛错(要 retry 先 resume)。
|
|
70
|
+
*
|
|
71
|
+
* @param run WorkflowRun 聚合根
|
|
72
|
+
* @param callId 要重试的 call id(必须已存在于 run.state.calls)
|
|
73
|
+
* @param deps LifecycleDeps(runner 用于重跑 call)
|
|
74
|
+
* @throws run.state.status !== "running"(G6-001)
|
|
75
|
+
* @throws callId 不存在
|
|
76
|
+
*/
|
|
77
|
+
export async function retryNode(
|
|
78
|
+
run: WorkflowRun,
|
|
79
|
+
callId: number,
|
|
80
|
+
deps: LifecycleDeps,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
// G6-001:前置 status==="running"
|
|
83
|
+
if (run.state.status !== "running") {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`retryNode: requires status==="running" (current: ${run.state.status}, runId=${run.runId})`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const call = run.state.calls.get(callId);
|
|
90
|
+
if (!call) {
|
|
91
|
+
throw new Error(`retryNode: call ${callId} not found in run ${run.runId}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 重置 call 状态:done → pending(绕过 AgentCall 状态机守卫,因为是显式 reset 语义)
|
|
95
|
+
call.status = "pending";
|
|
96
|
+
call.attempts = 0;
|
|
97
|
+
call.result = undefined;
|
|
98
|
+
call.sessionId = undefined;
|
|
99
|
+
|
|
100
|
+
// 同步 trace 节点:回退到 pending
|
|
101
|
+
run.state.trace.update(callId, {
|
|
102
|
+
status: "pending",
|
|
103
|
+
result: undefined,
|
|
104
|
+
error: undefined,
|
|
105
|
+
completedAt: undefined,
|
|
106
|
+
sessionId: undefined,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 主线程重跑(不重启 worker)——executeAgentCall 内部 markRunning + runner.run
|
|
110
|
+
// G6-001 保证 status==="running" ⟺ runtime defined;retryNode 已守 status==="running"
|
|
111
|
+
// 前置,故 run.runtime 必存在。非空断言,不再用 fallback 掩盖不变式违反。
|
|
112
|
+
const signal = run.runtime!.controller.signal;
|
|
113
|
+
await executeAgentCall(call, deps.runner, run.state.budget, signal, run.state.trace);
|
|
114
|
+
|
|
115
|
+
// 回发结果给 worker(best-effort:worker 通常已在首次失败结果投递后 resolve 并删除
|
|
116
|
+
// 该 callId 的 pending Promise,故本 postMessage 多被丢弃——见文件头 D.5 说明)。
|
|
117
|
+
// 保留是为覆盖「executeAgentCall 已完成但 dispatchAgentCall.then 尚未投递结果」的
|
|
118
|
+
// 极窄竞态窗口,以及与 skipNode 的回发路径对称。结果无论如何都已写入 trace/TUI。
|
|
119
|
+
if (call.result) {
|
|
120
|
+
run.runtime?.worker.postMessage({
|
|
121
|
+
type: "agent-result",
|
|
122
|
+
callId,
|
|
123
|
+
result: call.result,
|
|
124
|
+
cached: false,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// D-12 regression fix (round-2 #1):retry 重跑消费 usage 后同步 worker $BUDGET
|
|
129
|
+
postBudgetUpdate(run);
|
|
130
|
+
|
|
131
|
+
await deps.store.save(run);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── skipNode ─────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 跳过单个 agent call(注入占位 result)。
|
|
138
|
+
*
|
|
139
|
+
* 标记 call.status="done" + 写入 SKIP_PLACEHOLDER result + 同步 trace 节点为 completed。
|
|
140
|
+
* 若 worker 仍活着,立即回发 agent-result(解锁 worker pending await)。
|
|
141
|
+
*
|
|
142
|
+
* 与 retryNode 的区别:skipNode 不重跑——直接用占位结果「假装完成」。
|
|
143
|
+
* 用于用户显式跳过失败节点继续执行的场景。
|
|
144
|
+
*
|
|
145
|
+
* 不要求 status==="running"——paused 下也可 skip(标记后 resume 时该 call 走 callCache
|
|
146
|
+
* replay)。但若 worker 已 terminate(runtime undefined),只标记不回发。
|
|
147
|
+
*
|
|
148
|
+
* @param run WorkflowRun 聚合根
|
|
149
|
+
* @param callId 要跳过的 call id(若不存在,仅注入到 calls Map 占位)
|
|
150
|
+
* @param deps LifecycleDeps(store 持久化)
|
|
151
|
+
*/
|
|
152
|
+
export async function skipNode(
|
|
153
|
+
run: WorkflowRun,
|
|
154
|
+
callId: number,
|
|
155
|
+
deps: LifecycleDeps,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
const call = run.state.calls.get(callId);
|
|
158
|
+
|
|
159
|
+
if (call) {
|
|
160
|
+
// 已有 call:标记 done + 占位 result(绕过状态机守卫,显式 skip 语义)
|
|
161
|
+
call.status = "done";
|
|
162
|
+
call.result = SKIP_PLACEHOLDER;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 同步 trace 节点
|
|
166
|
+
run.state.trace.update(callId, {
|
|
167
|
+
status: "completed",
|
|
168
|
+
result: SKIP_PLACEHOLDER,
|
|
169
|
+
completedAt: new Date().toISOString(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// 若 worker 仍活着,回发 agent-result(解锁 worker pending await)
|
|
173
|
+
if (run.runtime) {
|
|
174
|
+
try {
|
|
175
|
+
run.runtime.worker.postMessage({
|
|
176
|
+
type: "agent-result",
|
|
177
|
+
callId,
|
|
178
|
+
result: SKIP_PLACEHOLDER,
|
|
179
|
+
cached: true,
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
// P1-8: worker 可能在 has 与 postMessage 间 exit——预期竞态,不恢复
|
|
183
|
+
void err;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// D-12 regression fix (round-2 #1):skip 后同步 worker $BUDGET(占位 result 零 usage,
|
|
188
|
+
// 值不变,但保持 $BUDGET 与主线程一致)
|
|
189
|
+
postBudgetUpdate(run);
|
|
190
|
+
|
|
191
|
+
await deps.store.save(run);
|
|
192
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — 静态 lint
|
|
3
|
+
*
|
|
4
|
+
* 在执行前捕获常见的 workflow 脚本 API 误用。纯函数,零副作用,零 IO。
|
|
5
|
+
*
|
|
6
|
+
* 设计:
|
|
7
|
+
* - lint 是编排层关注(非技术资源),归属 Engine 层。
|
|
8
|
+
* - **entry-point 检查**:脚本必须含 agent/parallel/pipeline 之一,否则视为 error。
|
|
9
|
+
* WorkflowScript.validate 直接委托 lintScript,故 entry-point 检查必须在此。
|
|
10
|
+
* - LintFinding/LintResult 类型规范的 canonical 源在本文件。
|
|
11
|
+
*
|
|
12
|
+
* 检查项:
|
|
13
|
+
* 1. 必须含 agent/parallel/pipeline 入口(error)
|
|
14
|
+
* 2. agent 选项中 outputSchema 当 key 用 → 应为 schema(error)
|
|
15
|
+
* 3. result.output / result.parsedOutput / result.content → agent 返回未包装值(error)
|
|
16
|
+
* 4. readFileSync/writeFileSync 传状态 → 脆弱(warning)
|
|
17
|
+
* 5. unlinkSync 清理状态 → 与 subprocess 文件读竞态(warning)
|
|
18
|
+
* 6. 顶层未 await 的异步 IIFE + 内部调 agent/parallel/pipeline → 子进程被提前 kill(error)
|
|
19
|
+
*
|
|
20
|
+
* 层归属:Engine。
|
|
21
|
+
*
|
|
22
|
+
* 参考:domain-models.md §7(validate 语义)。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Lint 检查发现项。 */
|
|
26
|
+
export interface LintFinding {
|
|
27
|
+
/** error = 会导致运行时崩溃; warning = 可能的错误 */
|
|
28
|
+
severity: "error" | "warning";
|
|
29
|
+
line: number;
|
|
30
|
+
message: string;
|
|
31
|
+
suggestion: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Lint 检查结果。 */
|
|
35
|
+
export interface LintResult {
|
|
36
|
+
valid: boolean;
|
|
37
|
+
findings: LintFinding[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 必须命中其一——workflow 脚本不调用任何编排函数等于空跑。 */
|
|
41
|
+
const ENTRY_POINT_PATTERNS = [/\bagent\s*\(/, /\bparallel\s*\(/, /\bpipeline\s*\(/] as const;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 检查脚本是否含至少一个编排入口(agent/parallel/pipeline)。
|
|
45
|
+
* 无入口视为 error——空跑脚本无意义。
|
|
46
|
+
*/
|
|
47
|
+
function checkEntryPoint(source: string): LintFinding[] {
|
|
48
|
+
const hasEntryPoint = ENTRY_POINT_PATTERNS.some((p) => p.test(source));
|
|
49
|
+
if (hasEntryPoint) return [];
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
severity: "error",
|
|
53
|
+
line: 0,
|
|
54
|
+
message: "Workflow script must call agent(), parallel(), or pipeline() at least once.",
|
|
55
|
+
suggestion: "Add at least one agent(), parallel(), or pipeline() invocation.",
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 检查单行 lint 问题,返回该行的发现项(可能为空)。
|
|
62
|
+
*/
|
|
63
|
+
function checkLine(lineText: string, lineNum: number): LintFinding[] {
|
|
64
|
+
const results: LintFinding[] = [];
|
|
65
|
+
|
|
66
|
+
// 跳过注释行
|
|
67
|
+
const trimmed = lineText.trim();
|
|
68
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// result.output / result.parsedOutput / result.content
|
|
73
|
+
const resultAccessPatterns: Array<{ regex: RegExp; field: string }> = [
|
|
74
|
+
{ regex: /\bresult\s*\.\s*output\b/, field: "output" },
|
|
75
|
+
{ regex: /\bresult\s*\.\s*parsedOutput\b/, field: "parsedOutput" },
|
|
76
|
+
{ regex: /\bresult\s*\.\s*content\b/, field: "content" },
|
|
77
|
+
];
|
|
78
|
+
for (const p of resultAccessPatterns) {
|
|
79
|
+
if (p.regex.test(lineText)) {
|
|
80
|
+
results.push({
|
|
81
|
+
severity: "error",
|
|
82
|
+
line: lineNum,
|
|
83
|
+
message: `\`result.${p.field}\` does not exist. agent() returns the unwrapped value directly.`,
|
|
84
|
+
suggestion: "Use `const value = await agent(...)` and access `value` directly.",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 文件传状态(readFileSync of STATE)
|
|
90
|
+
if (/readFileSync\(.*STATE.*\)|readFileSync\(.*state.*\.json/i.test(lineText)) {
|
|
91
|
+
results.push({
|
|
92
|
+
severity: "warning",
|
|
93
|
+
line: lineNum,
|
|
94
|
+
message: "Reading a state file between agent calls is fragile (subprocess file access).",
|
|
95
|
+
suggestion: "Use agent() with `schema` to get structured output directly, avoiding file I/O for state passing.",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// unlinkSync 清理状态
|
|
100
|
+
if (/unlinkSync.*state/i.test(lineText)) {
|
|
101
|
+
results.push({
|
|
102
|
+
severity: "warning",
|
|
103
|
+
line: lineNum,
|
|
104
|
+
message: "unlinkSync in finally may race with agent subprocess file reads.",
|
|
105
|
+
suggestion: "Avoid file-based state passing; use agent() `schema` for structured output.",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return results;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 找出 source 中所有 agent 调用跨度,检查错误的选项 key。
|
|
114
|
+
*
|
|
115
|
+
* agent 调用可能跨多行:
|
|
116
|
+
* agent({
|
|
117
|
+
* prompt: ...,
|
|
118
|
+
* outputSchema, ← error: 应为 schema
|
|
119
|
+
* })
|
|
120
|
+
*
|
|
121
|
+
* 定位 agent 调用边界,检查 outputSchema 是否作为 key(非 value 如 `schema: outputSchema`)。
|
|
122
|
+
*/
|
|
123
|
+
function checkAgentCalls(source: string): LintFinding[] {
|
|
124
|
+
const findings: LintFinding[] = [];
|
|
125
|
+
const lines = source.split("\n");
|
|
126
|
+
|
|
127
|
+
let inAgentCall = false;
|
|
128
|
+
let depth = 0;
|
|
129
|
+
let agentStartLine = -1;
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < lines.length; i++) {
|
|
132
|
+
const line = lines[i];
|
|
133
|
+
const trimmed = line.trim();
|
|
134
|
+
|
|
135
|
+
// 跳过注释
|
|
136
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 检测 agent 调用开始
|
|
141
|
+
if (!inAgentCall && /\bagent\s*\(/.test(line)) {
|
|
142
|
+
inAgentCall = true;
|
|
143
|
+
depth = 0;
|
|
144
|
+
agentStartLine = i;
|
|
145
|
+
// 从 agent( 开始计括号
|
|
146
|
+
const afterAgent = line.replace(/^.*?\bagent\s*\(/, "(");
|
|
147
|
+
for (const ch of afterAgent) {
|
|
148
|
+
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
149
|
+
if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
150
|
+
}
|
|
151
|
+
if (depth <= 0) {
|
|
152
|
+
// 单行 agent 调用
|
|
153
|
+
checkAgentCallOptions(lines, agentStartLine, i, findings);
|
|
154
|
+
inAgentCall = false;
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (inAgentCall) {
|
|
160
|
+
for (const ch of line) {
|
|
161
|
+
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
162
|
+
if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
163
|
+
}
|
|
164
|
+
if (depth <= 0) {
|
|
165
|
+
checkAgentCallOptions(lines, agentStartLine, i, findings);
|
|
166
|
+
inAgentCall = false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return findings;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 检查 agent 调用内的错误选项 key。
|
|
176
|
+
* 只标记 outputSchema 作为 KEY(属性名)使用的情况,不标记作为 VALUE。
|
|
177
|
+
*
|
|
178
|
+
* Error: { outputSchema } ← 简写属性(outputSchema 是 key)
|
|
179
|
+
* Error: { outputSchema: ... } ← 显式 key
|
|
180
|
+
* OK: { schema: outputSchema } ← outputSchema 是 value,`schema` 是 key
|
|
181
|
+
* OK: const outputSchema = {} ← 变量声明(在 agent 调用外)
|
|
182
|
+
*/
|
|
183
|
+
function checkAgentCallOptions(
|
|
184
|
+
lines: string[],
|
|
185
|
+
startLine: number,
|
|
186
|
+
endLine: number,
|
|
187
|
+
findings: LintFinding[],
|
|
188
|
+
): void {
|
|
189
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
190
|
+
const line = lines[i];
|
|
191
|
+
|
|
192
|
+
// 跳过变量声明(const/let/var outputSchema = ...)
|
|
193
|
+
if (/\b(?:const|let|var)\s+outputSchema\b/.test(line)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 匹配:outputSchema 作为对象 key(简写或显式)
|
|
198
|
+
if (/\boutputSchema\s*[,\}]/.test(line) || /\boutputSchema\s*:/.test(line)) {
|
|
199
|
+
// 排除:outputSchema 作为 value(在另一个 key 的冒号后)
|
|
200
|
+
// e.g. "schema: outputSchema," — outputSchema 前是冒号
|
|
201
|
+
const beforeOutput = line.substring(0, line.indexOf("outputSchema"));
|
|
202
|
+
if (/:\s*$/.test(beforeOutput)) {
|
|
203
|
+
continue; // outputSchema 是 value,不是 key
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
findings.push({
|
|
207
|
+
severity: "error",
|
|
208
|
+
line: i + 1,
|
|
209
|
+
message: "`outputSchema` is not a valid agent() option.",
|
|
210
|
+
suggestion: "Use `schema` instead of `outputSchema`.",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── 顶层未 await 的异步 IIFE 检测 ───────────────────────────
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 匹配未 await 的 async IIFE 起点(粗筛)。
|
|
220
|
+
*
|
|
221
|
+
* 形式:`(async function`、`(async ()`、`(async (args)` 后跟 `=>`
|
|
222
|
+
* 不匹配:`await (async ...`(lookbehind 排除)
|
|
223
|
+
*/
|
|
224
|
+
const BARE_ASYNC_IIFE_PATTERN = /(^|[;\n\s{}(])(?<!await\s)\(async\s+(?:function\b|\(\)|\([^)]*\)\s*=>)/g;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 判断 IIFE 调用表达式是否被某个上下文「接住」(return/赋值/await 链等)。
|
|
228
|
+
*
|
|
229
|
+
* 返回 true 表示 IIFE 的 Promise 被接住(合法或可能合法);
|
|
230
|
+
* false 表示 IIFE 是孤立语句表达式(fire-and-forget)。
|
|
231
|
+
*
|
|
232
|
+
* 判断方法:扫描 IIFE 起点 `(async` 前的非空白 token:
|
|
233
|
+
* - 遇到 `=` `return` `await` `(` `[` `,` → 接住
|
|
234
|
+
* - 遇到 `;` `{` `}` 或行首 → 孤立语句
|
|
235
|
+
*
|
|
236
|
+
* 例:
|
|
237
|
+
* `const x = (async ...` → '=' 接住
|
|
238
|
+
* `return (async ...` → 'return' 接住
|
|
239
|
+
* `(async ...` 行首 → 孤立
|
|
240
|
+
* `}; (async ...` → 孤立(前一个语句结束后新起一个)
|
|
241
|
+
*/
|
|
242
|
+
function isIIFEAwaited(source: string, iifeStart: number): boolean {
|
|
243
|
+
let i = iifeStart - 1;
|
|
244
|
+
while (i >= 0) {
|
|
245
|
+
const ch = source[i];
|
|
246
|
+
if (/\s/.test(ch)) {
|
|
247
|
+
i--;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
// 当前字符是标识符字符 → 向前扫完整标识符
|
|
251
|
+
if (/[A-Za-z0-9_$]/.test(ch)) {
|
|
252
|
+
// return / await / yield / 变量名(如 `foo(async ...`)→ 接住
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
// 单字符操作符
|
|
256
|
+
if (ch === "=" || ch === "(" || ch === "[" || ch === "," || ch === "?" || ch === ":") {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
if (ch === ";" || ch === "{" || ch === "}" || ch === ")") {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
// 其他字符(如 `.` `+`),保守视为接住(避免误报)
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 检测未 await 的 async IIFE,且其内部调用了 agent/parallel/pipeline。
|
|
270
|
+
*
|
|
271
|
+
* 严重度分级:
|
|
272
|
+
* - **error**:IIFE 是孤立语句表达式(fire-and-forget)+ 内部调 agent。
|
|
273
|
+
* 这是 daily-news-impact 的 bug 模式——worker 外层 IIFE 不等内层就 post return,
|
|
274
|
+
* 主线程 transition done → releaseRuntime → controller.abort() → SIGKILL 子进程。
|
|
275
|
+
* - **warning**:IIFE 被 `=`/`return`/`(` 等接住(可能后续 await),但内部调 agent。
|
|
276
|
+
* 提醒作者确认 Promise 真的被 await,不阻断运行。
|
|
277
|
+
*
|
|
278
|
+
* 误报规避(不报):
|
|
279
|
+
* - await 前缀的 IIFE(lookbehind 排除)
|
|
280
|
+
* - IIFE 内不含 agent/parallel/pipeline(stock-screening 这类纯 execSync 合法)
|
|
281
|
+
*
|
|
282
|
+
* 局限:纯正则 + 括号配对,无法做数据流分析。「赋值后稍后 await」「return 给外层 await」
|
|
283
|
+
* 都识别为「接住」(warning 而非 error),避免阻断合法写法。
|
|
284
|
+
*/
|
|
285
|
+
function checkBareAsyncIIFE(source: string): LintFinding[] {
|
|
286
|
+
if (!ENTRY_POINT_PATTERNS.some((p) => p.test(source))) return [];
|
|
287
|
+
|
|
288
|
+
// 用 matchAll 检查所有 IIFE(脚本可能有多个,每个都需独立判断)
|
|
289
|
+
const findings: LintFinding[] = [];
|
|
290
|
+
for (const match of source.matchAll(BARE_ASYNC_IIFE_PATTERN)) {
|
|
291
|
+
const iifeStart = match.index ?? 0;
|
|
292
|
+
const finding = analyzeIIFE(source, iifeStart);
|
|
293
|
+
if (finding) findings.push(finding);
|
|
294
|
+
}
|
|
295
|
+
return findings;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 分析单个 IIFE 起点是否触发 finding。
|
|
300
|
+
*
|
|
301
|
+
* 返回 LintFinding(error 或 warning)或 undefined(IIFE 内无 agent/无闭合)。
|
|
302
|
+
* 详见 checkBareAsyncIIFE 的 [HISTORICAL] 教训记录。
|
|
303
|
+
*/
|
|
304
|
+
function analyzeIIFE(source: string, iifeStart: number): LintFinding | undefined {
|
|
305
|
+
const iifeLine = source.slice(0, iifeStart).split("\n").length;
|
|
306
|
+
|
|
307
|
+
const firstBrace = source.indexOf("{", iifeStart);
|
|
308
|
+
if (firstBrace === -1) return undefined;
|
|
309
|
+
|
|
310
|
+
let depth = 0;
|
|
311
|
+
let iifeEnd = -1;
|
|
312
|
+
for (let i = firstBrace; i < source.length; i++) {
|
|
313
|
+
const ch = source[i];
|
|
314
|
+
if (ch === "{") depth++;
|
|
315
|
+
else if (ch === "}") {
|
|
316
|
+
depth--;
|
|
317
|
+
if (depth === 0) {
|
|
318
|
+
iifeEnd = i;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (iifeEnd === -1) return undefined;
|
|
324
|
+
|
|
325
|
+
const iifeBody = source.slice(firstBrace, iifeEnd);
|
|
326
|
+
const hasAgentInside = ENTRY_POINT_PATTERNS.some((p) => p.test(iifeBody));
|
|
327
|
+
if (!hasAgentInside) return undefined;
|
|
328
|
+
|
|
329
|
+
const awaited = isIIFEAwaited(source, iifeStart);
|
|
330
|
+
if (awaited) {
|
|
331
|
+
return {
|
|
332
|
+
severity: "warning",
|
|
333
|
+
line: iifeLine,
|
|
334
|
+
message:
|
|
335
|
+
"Async IIFE wrapping agent() is assigned/returned but must be awaited. If the surrounding context does not await this Promise, the worker will post `return` early and kill in-flight agent() subprocesses.",
|
|
336
|
+
suggestion:
|
|
337
|
+
"Verify the surrounding code awaits this IIFE's Promise. When unsure, prefer top-level await directly (the worker already wraps your script in an async IIFE).",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
severity: "error",
|
|
343
|
+
line: iifeLine,
|
|
344
|
+
message:
|
|
345
|
+
"Top-level async IIFE is a fire-and-forget statement. The worker's outer IIFE will post `return` before agent() resolves, killing the subprocess via runtime abort.",
|
|
346
|
+
suggestion:
|
|
347
|
+
"Remove the IIFE wrapper and use top-level await directly (the worker already wraps your script in an async IIFE). Or `await` the IIFE: `await (async function main() { ... })();`.",
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 静态检查 workflow 脚本合法性。
|
|
353
|
+
*
|
|
354
|
+
* @param source 脚本源码(原始文件内容)
|
|
355
|
+
* @returns LintResult(valid = 无 error 级 finding)
|
|
356
|
+
*/
|
|
357
|
+
export function lintScript(source: string): LintResult {
|
|
358
|
+
const lines = source.split("\n");
|
|
359
|
+
const findings: LintFinding[] = [];
|
|
360
|
+
|
|
361
|
+
// 入口检查(必须有 agent/parallel/pipeline 之一)
|
|
362
|
+
findings.push(...checkEntryPoint(source));
|
|
363
|
+
|
|
364
|
+
// 逐行检查(result.output、文件传状态等)
|
|
365
|
+
for (let i = 0; i < lines.length; i++) {
|
|
366
|
+
findings.push(...checkLine(lines[i], i + 1));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// agent 调用上下文检查(outputSchema 作为 key)
|
|
370
|
+
findings.push(...checkAgentCalls(source));
|
|
371
|
+
|
|
372
|
+
// [HISTORICAL] 顶层未 await 的异步 IIFE + 内部调 agent——子进程被提前 kill。
|
|
373
|
+
// 教训来源:daily-news-impact.js 用 (async function main(){...})();() 包裹整个脚本,
|
|
374
|
+
// worker 外层 IIFE 不等内层 IIFE 就 postMessage("return"),主线程 transition done
|
|
375
|
+
// → release runtime → controller.abort() → spawn 后 2ms SIGKILL 子进程。
|
|
376
|
+
// 诊断耗时 4 轮:先后误判为 model 故障 / 工具缺失 / turn-signal abort / ConcurrencyGate 异常,
|
|
377
|
+
// 最终靠 worker-host → handleReturn → release → abort 的调用栈定位。
|
|
378
|
+
findings.push(...checkBareAsyncIIFE(source));
|
|
379
|
+
|
|
380
|
+
// 按行号排序,稳定输出
|
|
381
|
+
findings.sort((a, b) => a.line - b.line);
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
valid: !findings.some((f) => f.severity === "error"),
|
|
385
|
+
findings,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill path discovery — resolve a skill name to its directory or SKILL.md path.
|
|
3
|
+
*
|
|
4
|
+
* Symmetric to execution/agent-registry.ts (which discovers agents): this module owns
|
|
5
|
+
* the resource-discovery concern for skills across project / user / npm sources.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
|
|
12
|
+
// ── Skill path resolution (with npm dir cache) ─────────────────────
|
|
13
|
+
|
|
14
|
+
const skillCandidatesCache = new Map<string, string[]>();
|
|
15
|
+
|
|
16
|
+
/** List npm skill candidate paths — cached per npmSkillsDir. */
|
|
17
|
+
function getNpmSkillCandidates(npmSkillsDir: string): string[] {
|
|
18
|
+
const cached = skillCandidatesCache.get(npmSkillsDir);
|
|
19
|
+
if (cached) return cached;
|
|
20
|
+
|
|
21
|
+
const candidates: string[] = [];
|
|
22
|
+
try {
|
|
23
|
+
for (const pkg of fs.readdirSync(npmSkillsDir)) {
|
|
24
|
+
candidates.push(path.join(npmSkillsDir, pkg, "skills"));
|
|
25
|
+
}
|
|
26
|
+
} catch { /* npm dir not found — no npm skills available */ void undefined; }
|
|
27
|
+
skillCandidatesCache.set(npmSkillsDir, candidates);
|
|
28
|
+
return candidates;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a skill name to its directory or SKILL.md path.
|
|
33
|
+
* Search order:
|
|
34
|
+
* 1. Project-level: .agents/skills/<name>/
|
|
35
|
+
* 2. Global: ~/.pi/agent/skills/<name>/
|
|
36
|
+
* 3. npm packages: ~/.pi/agent/npm/node_modules/<pkg>/skills/<name>/
|
|
37
|
+
* Returns the directory path if found, undefined otherwise.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveSkillPath(skillName: string): string | undefined {
|
|
40
|
+
const candidates = [
|
|
41
|
+
// Project-level
|
|
42
|
+
path.resolve(process.cwd(), ".agents/skills", skillName),
|
|
43
|
+
// Global user skills
|
|
44
|
+
path.join(os.homedir(), ".pi/agent/skills", skillName),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// npm package skills (cached)
|
|
48
|
+
const npmSkillsDir = path.join(os.homedir(), ".pi/agent/npm/node_modules");
|
|
49
|
+
for (const pkgSkillsBase of getNpmSkillCandidates(npmSkillsDir)) {
|
|
50
|
+
candidates.push(path.join(pkgSkillsBase, skillName));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const dir of candidates) {
|
|
54
|
+
if (fs.existsSync(dir)) {
|
|
55
|
+
return dir;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|