pi-loop-graph-sdk 0.2.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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README-zh.md +416 -0
  3. package/README.md +414 -0
  4. package/ROADMAP.md +60 -0
  5. package/dist/adapter/complete-tool.d.ts +3 -0
  6. package/dist/adapter/complete-tool.js +53 -0
  7. package/dist/adapter/debug-log.d.ts +38 -0
  8. package/dist/adapter/debug-log.js +151 -0
  9. package/dist/adapter/extension.d.ts +2 -0
  10. package/dist/adapter/extension.js +11 -0
  11. package/dist/adapter/graph-execution-host.d.ts +73 -0
  12. package/dist/adapter/graph-execution-host.js +181 -0
  13. package/dist/adapter/isolated-graph-session.d.ts +75 -0
  14. package/dist/adapter/isolated-graph-session.js +313 -0
  15. package/dist/adapter/loop-graph-extension.d.ts +96 -0
  16. package/dist/adapter/loop-graph-extension.js +487 -0
  17. package/dist/adapter/mechanism-runtime.d.ts +97 -0
  18. package/dist/adapter/mechanism-runtime.js +670 -0
  19. package/dist/adapter/model-messages.d.ts +17 -0
  20. package/dist/adapter/model-messages.js +11 -0
  21. package/dist/adapter/observability.d.ts +88 -0
  22. package/dist/adapter/observability.js +31 -0
  23. package/dist/adapter/output-contract.d.ts +12 -0
  24. package/dist/adapter/output-contract.js +87 -0
  25. package/dist/adapter/pi-node-context.d.ts +132 -0
  26. package/dist/adapter/pi-node-context.js +619 -0
  27. package/dist/adapter/projection.d.ts +121 -0
  28. package/dist/adapter/projection.js +169 -0
  29. package/dist/adapter/skill-content.d.ts +16 -0
  30. package/dist/adapter/skill-content.js +16 -0
  31. package/dist/advanced.d.ts +32 -0
  32. package/dist/advanced.js +17 -0
  33. package/dist/builders/graph.d.ts +27 -0
  34. package/dist/builders/graph.js +39 -0
  35. package/dist/builders/node.d.ts +8 -0
  36. package/dist/builders/node.js +9 -0
  37. package/dist/builders/refs.d.ts +5 -0
  38. package/dist/builders/refs.js +10 -0
  39. package/dist/builders/route.d.ts +11 -0
  40. package/dist/builders/route.js +18 -0
  41. package/dist/core/context.d.ts +73 -0
  42. package/dist/core/context.js +229 -0
  43. package/dist/core/graph.d.ts +172 -0
  44. package/dist/core/graph.js +57 -0
  45. package/dist/core/json.d.ts +8 -0
  46. package/dist/core/json.js +46 -0
  47. package/dist/core/limits.d.ts +7 -0
  48. package/dist/core/limits.js +14 -0
  49. package/dist/core/mechanism.d.ts +88 -0
  50. package/dist/core/mechanism.js +5 -0
  51. package/dist/core/result.d.ts +41 -0
  52. package/dist/core/result.js +1 -0
  53. package/dist/core/schema.d.ts +8 -0
  54. package/dist/core/schema.js +23 -0
  55. package/dist/core/skill.d.ts +12 -0
  56. package/dist/core/skill.js +1 -0
  57. package/dist/host/baseline.d.ts +11 -0
  58. package/dist/host/baseline.js +4 -0
  59. package/dist/host/graph-catalog.d.ts +8 -0
  60. package/dist/host/graph-catalog.js +24 -0
  61. package/dist/host/graph-host.d.ts +53 -0
  62. package/dist/host/graph-host.js +181 -0
  63. package/dist/host/preflight.d.ts +17 -0
  64. package/dist/host/preflight.js +81 -0
  65. package/dist/host/skill-catalog.d.ts +24 -0
  66. package/dist/host/skill-catalog.js +92 -0
  67. package/dist/host/tool-catalog.d.ts +27 -0
  68. package/dist/host/tool-catalog.js +33 -0
  69. package/dist/index.d.ts +21 -0
  70. package/dist/index.js +10 -0
  71. package/dist/replay/checkpoint.d.ts +40 -0
  72. package/dist/replay/checkpoint.js +57 -0
  73. package/dist/replay/events.d.ts +40 -0
  74. package/dist/replay/events.js +1 -0
  75. package/dist/replay/finalizer.d.ts +26 -0
  76. package/dist/replay/finalizer.js +117 -0
  77. package/dist/replay/html.d.ts +3 -0
  78. package/dist/replay/html.js +270 -0
  79. package/dist/replay/index.d.ts +13 -0
  80. package/dist/replay/index.js +7 -0
  81. package/dist/replay/model.d.ts +81 -0
  82. package/dist/replay/model.js +1 -0
  83. package/dist/replay/parser.d.ts +3 -0
  84. package/dist/replay/parser.js +332 -0
  85. package/dist/replay/recorder.d.ts +30 -0
  86. package/dist/replay/recorder.js +195 -0
  87. package/dist/replay/store.d.ts +41 -0
  88. package/dist/replay/store.js +94 -0
  89. package/dist/router.d.ts +4 -0
  90. package/dist/router.js +61 -0
  91. package/dist/runtime/event-bus.d.ts +101 -0
  92. package/dist/runtime/event-bus.js +18 -0
  93. package/dist/runtime/graph-runtime.d.ts +173 -0
  94. package/dist/runtime/graph-runtime.js +1293 -0
  95. package/dist/runtime/invocation-budget.d.ts +22 -0
  96. package/dist/runtime/invocation-budget.js +52 -0
  97. package/dist/runtime/mechanism-runtime.d.ts +92 -0
  98. package/dist/runtime/mechanism-runtime.js +387 -0
  99. package/dist/runtime.d.ts +91 -0
  100. package/dist/runtime.js +258 -0
  101. package/dist/tools-resolve.d.ts +20 -0
  102. package/dist/tools-resolve.js +52 -0
  103. package/dist/type.d.ts +593 -0
  104. package/dist/type.js +30 -0
  105. package/dist/validate.d.ts +25 -0
  106. package/dist/validate.js +203 -0
  107. package/package.json +69 -0
@@ -0,0 +1,619 @@
1
+ // ============================================================
2
+ // PiNodeContext — Promise 桥接
3
+ // ============================================================
4
+ //
5
+ // 不注入 entry message(投影钩子负责),只做两件事:
6
+ // 1. 发送 prompt + triggerTurn
7
+ // 2. 等待 agent_end 返回 NodeCompletion
8
+ //
9
+ // 如何获取 NodeCompletion:
10
+ // - agent 调用 __graph_complete__ 工具
11
+ // - extension.ts 的 tool_result 钩子捕获参数 → submitCompletion()
12
+ // - extension.ts 的 agent_end 钩子 → onAgentEnd() → resolve Promise
13
+ // ============================================================
14
+ import { debugLog } from "./debug-log.js";
15
+ import { defaultModelMessageFormatter, } from "./model-messages.js";
16
+ import { OUTPUT_CONTRACT_MESSAGE_TYPE, prepareOutputContract, } from "./output-contract.js";
17
+ const DEFAULT_MAX_COMPLETION_REJECTIONS = 3;
18
+ export const CONTEXT_SNAPSHOT_MESSAGE_TYPE = "loop_graph_context";
19
+ export class PiNodeContext {
20
+ outputContractMaxBytes;
21
+ telemetry;
22
+ signal;
23
+ pi;
24
+ currentNodeId = null;
25
+ /** __graph_complete__ 捕获的 completion 列表(同节点内可能调多次) */
26
+ pendingCompletions = [];
27
+ completionFingerprints = new Set();
28
+ /** 活跃 run 的 resolve */
29
+ activeResolve = null;
30
+ activeRunId = 0;
31
+ nextRunId = 1;
32
+ agentRunTimeoutMs;
33
+ messageFormatter;
34
+ completionValidationTimeoutMs;
35
+ nodeValidateFn = undefined;
36
+ routeValidateFn = undefined;
37
+ postMechanismValidateFn = undefined;
38
+ mechanismLifecycle = null;
39
+ validationInFlight = null;
40
+ activeOutputContract = null;
41
+ activeOutputContractMessage = null;
42
+ activeContextSnapshot = null;
43
+ contextProjectionCount = 0;
44
+ foldableContextCompacted = false;
45
+ submissionQueue = Promise.resolve();
46
+ rejectionCount = 0;
47
+ completionState = "submitted";
48
+ get completionSubmissionState() {
49
+ return this.completionState;
50
+ }
51
+ constructor(pi, agentRunTimeoutMs = 5 * 60 * 1000, messageFormatter = defaultModelMessageFormatter, completionValidationTimeoutMs = 60_000, outputContractMaxBytes = 64 * 1024, telemetry) {
52
+ this.outputContractMaxBytes = outputContractMaxBytes;
53
+ this.telemetry = telemetry;
54
+ this.pi = pi;
55
+ this.agentRunTimeoutMs = agentRunTimeoutMs;
56
+ this.messageFormatter = messageFormatter;
57
+ this.completionValidationTimeoutMs = completionValidationTimeoutMs;
58
+ this.signal = new AbortController().signal;
59
+ // ── Provider 错误回流通道(单一监听器,生命周期跟实例走)──
60
+ // pi 没有 off,监听器只增不减。挪到构造函数注册一次,
61
+ // 回调读实例当前的 activeRunId/activeResolve,避免闭包泄漏。
62
+ // 排除 429(限流,pi 内部可能重试成功)。
63
+ pi.on("after_provider_response", (event, _ctx) => {
64
+ if (event.status >= 400 &&
65
+ event.status !== 429 &&
66
+ this.activeRunId !== 0 &&
67
+ this.activeResolve) {
68
+ this.activeResolve({
69
+ nodeId: this.currentNodeId ?? "unknown",
70
+ status: "failed",
71
+ result: { reason: `Provider error: HTTP ${event.status}` },
72
+ });
73
+ }
74
+ });
75
+ }
76
+ // ── NodeContext 接口 ──────────────────────────────────
77
+ runValidateFn = undefined;
78
+ async runAgent(request) {
79
+ // schema 配置错误必须在占用 active run 之前抛出,避免把 NodeContext
80
+ // 永久留在一个没有 Promise/timeout 可以收尾的运行状态。
81
+ const outputContract = prepareOutputContract(request.outputSchema, this.outputContractMaxBytes);
82
+ this.pendingCompletions = [];
83
+ this.completionFingerprints.clear();
84
+ const runId = this.nextRunId++;
85
+ this.activeRunId = runId;
86
+ this.runValidateFn = request.validateCompletion;
87
+ this.activeOutputContract = outputContract;
88
+ this.activeOutputContractMessage = outputContract
89
+ ? Object.freeze({
90
+ customType: OUTPUT_CONTRACT_MESSAGE_TYPE,
91
+ content: outputContract.modelText,
92
+ display: false,
93
+ details: Object.freeze({
94
+ protocol: 1,
95
+ agentRunId: runId,
96
+ schemaFingerprint: outputContract.fingerprint,
97
+ }),
98
+ })
99
+ : null;
100
+ if (outputContract) {
101
+ this.emitTelemetry({
102
+ type: "output_contract.prepared",
103
+ agentRunId: runId,
104
+ schemaFingerprint: outputContract.fingerprint,
105
+ schemaBytes: outputContract.byteSize,
106
+ });
107
+ }
108
+ try {
109
+ const start = this.mechanismLifecycle
110
+ ? await this.mechanismLifecycle.beforeAgentRun(runId, request)
111
+ : undefined;
112
+ if (start?.blocked) {
113
+ this.activeRunId = 0;
114
+ this.clearAgentRunArtifacts(runId);
115
+ this.mechanismLifecycle?.afterAgentRun(runId);
116
+ return {
117
+ nodeId: this.currentNodeId ?? "unknown",
118
+ status: "failed",
119
+ result: { reason: start.reason ?? "mechanism 阻止了 agent run" },
120
+ };
121
+ }
122
+ }
123
+ catch (error) {
124
+ this.activeRunId = 0;
125
+ this.clearAgentRunArtifacts(runId);
126
+ this.mechanismLifecycle?.afterAgentRun(runId);
127
+ throw error;
128
+ }
129
+ const promise = new Promise((res) => {
130
+ const timeout = setTimeout(() => {
131
+ if (this.activeRunId !== runId)
132
+ return;
133
+ this.activeRunId = 0;
134
+ this.activeResolve = null;
135
+ this.clearAgentRunArtifacts(runId);
136
+ res({
137
+ nodeId: this.currentNodeId ?? "unknown",
138
+ status: "failed",
139
+ result: {
140
+ reason: this.agentRunTimeoutMs === 5 * 60 * 1000
141
+ ? "Agent run timed out after 5 minutes"
142
+ : `Agent run timed out after ${this.agentRunTimeoutMs} ms`,
143
+ runtimeFailure: {
144
+ code: "agent-timeout",
145
+ phase: "agent",
146
+ retryable: true,
147
+ },
148
+ },
149
+ });
150
+ }, this.agentRunTimeoutMs);
151
+ this.activeResolve = (c) => {
152
+ clearTimeout(timeout);
153
+ this.activeRunId = 0;
154
+ this.activeResolve = null;
155
+ this.clearAgentRunArtifacts(runId);
156
+ res(c);
157
+ };
158
+ });
159
+ if (this.activeOutputContractMessage) {
160
+ this.pi.sendMessage(this.activeOutputContractMessage, {});
161
+ }
162
+ // 发送 prompt,触发 agent 运行
163
+ this.pi.sendMessage({
164
+ customType: "loop_graph_prompt",
165
+ content: request.prompt,
166
+ display: false,
167
+ }, { triggerTurn: true });
168
+ try {
169
+ return await promise;
170
+ }
171
+ catch (error) {
172
+ return {
173
+ nodeId: this.currentNodeId ?? "unknown",
174
+ status: "failed",
175
+ result: {
176
+ reason: error instanceof Error ? error.message : String(error),
177
+ },
178
+ };
179
+ }
180
+ finally {
181
+ this.mechanismLifecycle?.afterAgentRun(runId);
182
+ }
183
+ }
184
+ /**
185
+ * 直接执行 pi 平台上的工具。当前占用位,未实现。
186
+ *
187
+ * 纯代码节点不需要此方法——你可以在 execute 里直接
188
+ * import 并使用任何 Node.js 或第三方库:
189
+ *
190
+ * ```typescript
191
+ * execute: async (instance, input, ctx) => {
192
+ * const data = fs.readFileSync(input.data.path, "utf-8");
193
+ * const result = await fetch("https://api.example.com", {...});
194
+ * return { nodeId: "parse", status: "ok", result: { data, result } };
195
+ * }//讨论在有纯代码节点的前提下该功能是否必要
196
+ * ```
197
+ */
198
+ async callTool(_name, _input) {
199
+ throw new Error("PiNodeContext.callTool 未实现。纯代码节点请直接在 execute 中使用 Node.js API。");
200
+ }
201
+ // ── 供 extension.ts 调用 ──────────────────────────────
202
+ /** 当前节点内调用 __graph_complete__ 的次数 */
203
+ get completeCount() {
204
+ return this.pendingCompletions.length;
205
+ }
206
+ getActiveOutputContractMessage() {
207
+ return this.activeOutputContractMessage;
208
+ }
209
+ setContextSnapshot(snapshot) {
210
+ if (snapshot?.agentRunId !== this.activeContextSnapshot?.agentRunId) {
211
+ this.contextProjectionCount = 0;
212
+ this.foldableContextCompacted = false;
213
+ }
214
+ this.activeContextSnapshot = snapshot;
215
+ }
216
+ markContextCompacted() {
217
+ this.foldableContextCompacted = true;
218
+ }
219
+ getContextSnapshotMessage() {
220
+ const snapshot = this.activeContextSnapshot;
221
+ if (!snapshot)
222
+ return null;
223
+ const content = [];
224
+ for (const layer of snapshot.layers) {
225
+ if (layer.retention === "foldable" && this.foldableContextCompacted)
226
+ continue;
227
+ if (layer.retention === "transient" && this.contextProjectionCount > 0)
228
+ continue;
229
+ if (typeof layer.content === "string")
230
+ content.push({ type: "text", text: layer.content });
231
+ else
232
+ content.push(...layer.content.map((block) => ({ ...block })));
233
+ }
234
+ this.contextProjectionCount += 1;
235
+ return Object.freeze({
236
+ role: "custom",
237
+ customType: CONTEXT_SNAPSHOT_MESSAGE_TYPE,
238
+ content,
239
+ display: false,
240
+ details: Object.freeze({
241
+ rootRunId: snapshot.rootRunId,
242
+ graphInvocationId: snapshot.graphInvocationId,
243
+ nodeVisitId: snapshot.nodeVisitId,
244
+ agentRunId: snapshot.agentRunId,
245
+ memoryRevision: snapshot.memoryRevision,
246
+ }),
247
+ timestamp: Date.now(),
248
+ });
249
+ }
250
+ getReplayScope() {
251
+ const snapshot = this.activeContextSnapshot;
252
+ if (!snapshot)
253
+ return null;
254
+ return {
255
+ rootRunId: snapshot.rootRunId,
256
+ graphInvocationId: snapshot.graphInvocationId,
257
+ nodeVisitId: snapshot.nodeVisitId,
258
+ agentRunId: snapshot.agentRunId,
259
+ };
260
+ }
261
+ submitCompletion(params) {
262
+ const runId = this.activeRunId;
263
+ const schemaFingerprint = this.activeOutputContract?.fingerprint;
264
+ const startedAt = Date.now();
265
+ const submission = {
266
+ result: params.result,
267
+ };
268
+ this.completionState = "submitted";
269
+ this.emitTelemetry({
270
+ type: "completion.submitted",
271
+ agentRunId: runId,
272
+ schemaFingerprint,
273
+ });
274
+ const result = this.submissionQueue.then(() => this.processCompletionSubmission(submission, runId)).then((decision) => {
275
+ const durationMs = Date.now() - startedAt;
276
+ if (decision.decision === "accepted") {
277
+ this.completionState = "accepted";
278
+ this.emitTelemetry({
279
+ type: "completion.accepted",
280
+ agentRunId: runId,
281
+ completionStatus: decision.completionStatus,
282
+ schemaFingerprint,
283
+ durationMs,
284
+ });
285
+ }
286
+ else if (decision.decision === "rejected") {
287
+ this.completionState = "rejected";
288
+ this.emitTelemetry({
289
+ type: "completion.rejected",
290
+ agentRunId: runId,
291
+ reason: decision.reason,
292
+ validatorStage: decision.validatorStage,
293
+ schemaFingerprint,
294
+ durationMs,
295
+ });
296
+ }
297
+ else {
298
+ this.completionState = "failed";
299
+ this.emitTelemetry({
300
+ type: "completion.failed",
301
+ agentRunId: runId,
302
+ scope: decision.scope,
303
+ reason: decision.reason,
304
+ validatorStage: decision.validatorStage,
305
+ schemaFingerprint,
306
+ durationMs,
307
+ });
308
+ }
309
+ return decision;
310
+ });
311
+ this.submissionQueue = result.then(() => undefined, () => undefined);
312
+ return result;
313
+ }
314
+ onAgentEnd() {
315
+ if (this.validationInFlight)
316
+ return this.validationInFlight;
317
+ const work = this.submissionQueue.then(() => this.processAgentEnd());
318
+ this.validationInFlight = work;
319
+ return work.finally(() => {
320
+ if (this.validationInFlight === work) {
321
+ this.validationInFlight = null;
322
+ }
323
+ });
324
+ }
325
+ async processAgentEnd() {
326
+ if (this.activeRunId === 0) {
327
+ // 图已终止,agent 仍在跑 → 追加消息告知
328
+ this.pi.sendMessage({
329
+ customType: "loop_graph_dead",
330
+ content: this.messageFormatter.deadRun({ nodeId: this.currentNodeId }),
331
+ display: false,
332
+ }, {});
333
+ return;
334
+ }
335
+ const resolve = this.activeResolve;
336
+ if (!resolve)
337
+ return;
338
+ if (this.pendingCompletions.length > 0) {
339
+ const currentCompletions = this.pendingCompletions;
340
+ this.pendingCompletions = [];
341
+ this.completionFingerprints.clear();
342
+ // 取最后一次调用作为主 completion
343
+ const last = currentCompletions[currentCompletions.length - 1];
344
+ // 如果调了多次,把全部记录附在 result 里
345
+ const completion = {
346
+ ...last,
347
+ result: {
348
+ ...last.result,
349
+ ...(currentCompletions.length > 1
350
+ ? { allCompletions: currentCompletions }
351
+ : {}),
352
+ },
353
+ };
354
+ resolve(completion);
355
+ }
356
+ else {
357
+ resolve({
358
+ nodeId: this.currentNodeId ?? "unknown",
359
+ status: "failed",
360
+ result: {
361
+ reason: this.messageFormatter.incompleteNode({
362
+ nodeId: this.currentNodeId ?? "unknown",
363
+ completeToolName: "__graph_complete__",
364
+ }),
365
+ runtimeFailure: {
366
+ code: "agent-ended-without-completion",
367
+ phase: "agent",
368
+ retryable: true,
369
+ },
370
+ },
371
+ });
372
+ }
373
+ this.activeResolve = null;
374
+ this.activeRunId = 0;
375
+ this.clearAgentRunArtifacts();
376
+ }
377
+ async processCompletionSubmission(submission, runId) {
378
+ this.completionState = "validating";
379
+ const schemaFingerprint = this.activeOutputContract?.fingerprint;
380
+ if (runId === 0 || runId !== this.activeRunId) {
381
+ return { decision: "rejected", reason: "当前 Agent Run 已结束", schemaFingerprint };
382
+ }
383
+ const fingerprint = createCompletionFingerprint({
384
+ result: submission.result,
385
+ });
386
+ if (this.completionFingerprints.has(fingerprint)) {
387
+ return { decision: "rejected", reason: "重复提交相同节点结果", schemaFingerprint };
388
+ }
389
+ const completion = {
390
+ nodeId: this.currentNodeId ?? "unknown",
391
+ status: "ok",
392
+ result: { ...submission.result },
393
+ };
394
+ const validationStages = [
395
+ ["outputSchema", this.activeOutputContract?.validate],
396
+ ["agent-run", this.runValidateFn],
397
+ ["node", this.nodeValidateFn],
398
+ ["route", this.routeValidateFn],
399
+ ];
400
+ for (const [stage, validator] of validationStages) {
401
+ if (!validator)
402
+ continue;
403
+ const validation = await this.runValidationStage(stage, validator, completion.result, runId, schemaFingerprint);
404
+ if (!validation.isValid) {
405
+ const exhausted = this.rejectOrExhaust(runId, stage, schemaFingerprint);
406
+ if (exhausted)
407
+ return exhausted;
408
+ debugLog.agentRetry(this.currentNodeId ?? "?", validation.reason);
409
+ return {
410
+ decision: "rejected",
411
+ reason: validation.reason,
412
+ validatorStage: stage,
413
+ schemaFingerprint,
414
+ };
415
+ }
416
+ }
417
+ if (this.mechanismLifecycle) {
418
+ this.emitValidationStarted(runId, "mechanism", schemaFingerprint);
419
+ const gate = await this.mechanismLifecycle.validateCompletion(runId, completion);
420
+ if (gate.action === "reject") {
421
+ const exhausted = this.rejectOrExhaust(runId, "mechanism", schemaFingerprint);
422
+ if (exhausted)
423
+ return exhausted;
424
+ debugLog.agentRetry(this.currentNodeId ?? "?", gate.reason);
425
+ return {
426
+ decision: "rejected",
427
+ reason: gate.reason,
428
+ validatorStage: "mechanism",
429
+ schemaFingerprint,
430
+ };
431
+ }
432
+ if (gate.action === "fail-node" || gate.action === "fail-graph") {
433
+ this.completionFingerprints.add(fingerprint);
434
+ this.pendingCompletions.push({
435
+ nodeId: completion.nodeId,
436
+ status: "failed",
437
+ result: { reason: gate.reason, completionGate: { action: gate.action } },
438
+ });
439
+ return {
440
+ decision: "failed",
441
+ scope: gate.action === "fail-graph" ? "graph" : "node",
442
+ reason: gate.reason,
443
+ validatorStage: "mechanism",
444
+ schemaFingerprint,
445
+ };
446
+ }
447
+ if (gate.action === "allow" && gate.verifiedResult) {
448
+ completion.verifiedResult = gate.verifiedResult;
449
+ }
450
+ }
451
+ if (this.postMechanismValidateFn) {
452
+ const validation = await this.runValidationStage("agent-choice", this.postMechanismValidateFn, completion.result, runId, schemaFingerprint);
453
+ if (!validation.isValid) {
454
+ const exhausted = this.rejectOrExhaust(runId, "agent-choice", schemaFingerprint);
455
+ if (exhausted)
456
+ return exhausted;
457
+ debugLog.agentRetry(this.currentNodeId ?? "?", validation.reason);
458
+ return {
459
+ decision: "rejected",
460
+ reason: validation.reason,
461
+ validatorStage: "agent-choice",
462
+ schemaFingerprint,
463
+ };
464
+ }
465
+ }
466
+ this.completionFingerprints.add(fingerprint);
467
+ this.pendingCompletions.push(completion);
468
+ return {
469
+ decision: "accepted",
470
+ completionStatus: "ok",
471
+ validation: "passed",
472
+ schemaFingerprint,
473
+ };
474
+ }
475
+ rejectOrExhaust(runId, validatorStage, schemaFingerprint) {
476
+ this.rejectionCount += 1;
477
+ if (this.rejectionCount <= DEFAULT_MAX_COMPLETION_REJECTIONS)
478
+ return undefined;
479
+ const reason = `Agent Run 完成提交已达到最多 ${DEFAULT_MAX_COMPLETION_REJECTIONS} 次拒绝`;
480
+ this.completionState = "failed";
481
+ // Exhaustion is a Runtime terminal decision. Persist it so agent_end resolves
482
+ // the same failure instead of incorrectly reporting "without completion".
483
+ if (runId === this.activeRunId) {
484
+ this.pendingCompletions.push({
485
+ nodeId: this.currentNodeId ?? "unknown",
486
+ status: "failed",
487
+ result: {
488
+ reason,
489
+ completionGate: { action: "rejection-budget-exhausted" },
490
+ runtimeFailure: {
491
+ code: "validation-exhausted",
492
+ phase: "agent",
493
+ retryable: false,
494
+ },
495
+ },
496
+ });
497
+ }
498
+ return {
499
+ decision: "failed",
500
+ scope: "node",
501
+ reason,
502
+ validatorStage,
503
+ schemaFingerprint,
504
+ };
505
+ }
506
+ setCurrentNodeId(nodeId) {
507
+ this.currentNodeId = nodeId;
508
+ // 一个 NodeContext 在统一 Runtime 的 callStack 中复用。每次进入节点都
509
+ // 必须切断前一节点(或前一子图)的 completion,节点内多次 runAgent 则不会
510
+ // 再次调用本方法,仍可保留其 allCompletions 语义。
511
+ this.pendingCompletions = [];
512
+ this.completionFingerprints.clear();
513
+ this.runValidateFn = undefined;
514
+ this.nodeValidateFn = undefined;
515
+ this.routeValidateFn = undefined;
516
+ this.postMechanismValidateFn = undefined;
517
+ this.activeOutputContract = null;
518
+ this.activeOutputContractMessage = null;
519
+ this.activeContextSnapshot = null;
520
+ this.contextProjectionCount = 0;
521
+ this.foldableContextCompacted = false;
522
+ this.rejectionCount = 0;
523
+ this.completionState = "submitted";
524
+ }
525
+ setNodeCompletionValidator(validate) {
526
+ this.nodeValidateFn = validate;
527
+ }
528
+ setRouteCompletionValidator(validate) {
529
+ this.routeValidateFn = validate;
530
+ }
531
+ setPostMechanismCompletionValidator(validate) {
532
+ this.postMechanismValidateFn = validate;
533
+ }
534
+ setMechanismLifecycle(lifecycle) {
535
+ this.mechanismLifecycle = lifecycle;
536
+ }
537
+ async runValidationStage(stage, validator, result, runId, schemaFingerprint) {
538
+ this.emitValidationStarted(runId, stage, schemaFingerprint);
539
+ return runCompletionValidator(validator, result, this.completionValidationTimeoutMs);
540
+ }
541
+ emitValidationStarted(agentRunId, validatorStage, schemaFingerprint) {
542
+ this.emitTelemetry({
543
+ type: "completion.validation_started",
544
+ agentRunId,
545
+ validatorStage,
546
+ schemaFingerprint,
547
+ });
548
+ }
549
+ emitTelemetry(event) {
550
+ try {
551
+ this.telemetry?.(Object.freeze(event));
552
+ }
553
+ catch {
554
+ // telemetry 不能改变 Agent Run 控制流
555
+ }
556
+ }
557
+ clearAgentRunArtifacts(expectedRunId) {
558
+ const messageRunId = this.activeOutputContractMessage?.details?.agentRunId;
559
+ if (expectedRunId !== undefined && messageRunId !== undefined && messageRunId !== expectedRunId) {
560
+ return;
561
+ }
562
+ this.runValidateFn = undefined;
563
+ this.activeOutputContract = null;
564
+ this.activeOutputContractMessage = null;
565
+ }
566
+ reset() {
567
+ this.currentNodeId = null;
568
+ this.pendingCompletions = [];
569
+ this.completionFingerprints.clear();
570
+ this.activeRunId = 0;
571
+ this.activeResolve = null;
572
+ this.runValidateFn = undefined;
573
+ this.nodeValidateFn = undefined;
574
+ this.routeValidateFn = undefined;
575
+ this.postMechanismValidateFn = undefined;
576
+ this.mechanismLifecycle = null;
577
+ this.validationInFlight = null;
578
+ this.activeOutputContract = null;
579
+ this.activeOutputContractMessage = null;
580
+ this.rejectionCount = 0;
581
+ this.completionState = "submitted";
582
+ this.activeContextSnapshot = null;
583
+ this.submissionQueue = Promise.resolve();
584
+ this.rejectionCount = 0;
585
+ this.completionState = "submitted";
586
+ }
587
+ }
588
+ async function runCompletionValidator(validator, result, timeoutMs) {
589
+ let timeout;
590
+ try {
591
+ return await Promise.race([
592
+ Promise.resolve(validator(result)),
593
+ new Promise((resolve) => {
594
+ timeout = setTimeout(() => resolve({
595
+ isValid: false,
596
+ reason: `completion validation timed out after ${timeoutMs} ms`,
597
+ }), timeoutMs);
598
+ }),
599
+ ]);
600
+ }
601
+ catch (error) {
602
+ return {
603
+ isValid: false,
604
+ reason: `completion validator 异常: ${error instanceof Error ? error.message : String(error)}`,
605
+ };
606
+ }
607
+ finally {
608
+ if (timeout)
609
+ clearTimeout(timeout);
610
+ }
611
+ }
612
+ function createCompletionFingerprint(params) {
613
+ try {
614
+ return JSON.stringify(params.result);
615
+ }
616
+ catch {
617
+ return String(params.result);
618
+ }
619
+ }