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,670 @@
1
+ import * as path from "node:path";
2
+ import Schema from "typebox/schema";
3
+ /**
4
+ * mechanism state 的唯一所有者:每个 AgentInstance、每个 mechanism 对象一份。
5
+ * WeakMap 不延长 instance 或 mechanism definition 的生命周期。
6
+ */
7
+ export class MechanismStateStore {
8
+ states = new WeakMap();
9
+ resolve(instance, mechanism) {
10
+ let instanceStates = this.states.get(instance);
11
+ if (!instanceStates) {
12
+ instanceStates = new WeakMap();
13
+ this.states.set(instance, instanceStates);
14
+ }
15
+ const existing = instanceStates.get(mechanism);
16
+ if (existing)
17
+ return existing;
18
+ let record;
19
+ try {
20
+ record = {
21
+ state: mechanism.createState ? mechanism.createState() : {},
22
+ initializationFailed: false,
23
+ };
24
+ }
25
+ catch (initializationError) {
26
+ record = { state: {}, initializationFailed: true, initializationError };
27
+ }
28
+ instanceStates.set(mechanism, record);
29
+ return record;
30
+ }
31
+ }
32
+ /** 一个 mechanism 在单次 node visit 内拥有的托管生命周期。 */
33
+ class MechanismInvocation {
34
+ mechanismName;
35
+ active = true;
36
+ controller = new AbortController();
37
+ cleanups = [];
38
+ scope;
39
+ constructor(mechanismName, descriptor, runtimeScopeIsCurrent) {
40
+ this.mechanismName = mechanismName;
41
+ this.scope = Object.freeze({
42
+ scopeId: descriptor.scopeId,
43
+ visit: descriptor.visit,
44
+ signal: this.controller.signal,
45
+ isActive: () => this.active && runtimeScopeIsCurrent(),
46
+ onCleanup: (cleanup) => {
47
+ if (!this.active) {
48
+ throw new Error(`mechanism ${this.mechanismName} 的 scope 已失效,不能再注册 cleanup`);
49
+ }
50
+ this.cleanups.push(cleanup);
51
+ },
52
+ });
53
+ }
54
+ async close() {
55
+ if (!this.active)
56
+ return [];
57
+ this.active = false;
58
+ this.controller.abort();
59
+ const errors = [];
60
+ for (let index = this.cleanups.length - 1; index >= 0; index--) {
61
+ try {
62
+ await this.cleanups[index]();
63
+ }
64
+ catch (error) {
65
+ errors.push({ mechanismName: this.mechanismName, error });
66
+ }
67
+ }
68
+ this.cleanups.length = 0;
69
+ return errors;
70
+ }
71
+ }
72
+ /** 同一 node visit 中全部 mechanism invocation 的所有者。 */
73
+ export class MechanismInvocationGroup {
74
+ descriptor;
75
+ runtimeScopeIsCurrent;
76
+ invocations = [];
77
+ closed = false;
78
+ constructor(descriptor, runtimeScopeIsCurrent) {
79
+ this.descriptor = descriptor;
80
+ this.runtimeScopeIsCurrent = runtimeScopeIsCurrent;
81
+ }
82
+ createScope(mechanismName) {
83
+ if (this.closed)
84
+ throw new Error("mechanism invocation group 已关闭");
85
+ const invocation = new MechanismInvocation(mechanismName, this.descriptor, this.runtimeScopeIsCurrent);
86
+ this.invocations.push(invocation);
87
+ return invocation.scope;
88
+ }
89
+ async close() {
90
+ if (this.closed)
91
+ return [];
92
+ this.closed = true;
93
+ const errors = [];
94
+ for (let index = this.invocations.length - 1; index >= 0; index--) {
95
+ errors.push(...await this.invocations[index].close());
96
+ }
97
+ this.invocations.length = 0;
98
+ return errors;
99
+ }
100
+ }
101
+ class BrokerSubscription {
102
+ subscriber;
103
+ remove;
104
+ constructor(subscriber, remove) {
105
+ this.subscriber = subscriber;
106
+ this.remove = remove;
107
+ }
108
+ get disposed() {
109
+ return this.subscriber.disposed;
110
+ }
111
+ dispose() {
112
+ if (this.subscriber.disposed)
113
+ return;
114
+ this.subscriber.disposed = true;
115
+ this.remove(this.subscriber);
116
+ }
117
+ }
118
+ /**
119
+ * pi 每类事件只注册一个底层 handler;node visit 内的订阅由 scope 托管。
120
+ * handler 控制性失败先记录,随后由图循环在安全检查点消费。
121
+ */
122
+ export class MechanismEventBroker {
123
+ reportFailure;
124
+ subscribers = new Map([
125
+ ["tool_result", []],
126
+ ["turn_start", []],
127
+ ["turn_end", []],
128
+ ]);
129
+ pendingFailures = [];
130
+ decisionTraces = new Map();
131
+ activeRun = null;
132
+ pi;
133
+ options;
134
+ constructor(pi, reportFailure, options = {}) {
135
+ this.reportFailure = reportFailure;
136
+ this.pi = pi;
137
+ this.options = {
138
+ execRoot: path.resolve(options.execRoot ?? process.cwd()),
139
+ execTimeoutMs: options.execTimeoutMs ?? 30_000,
140
+ execMaxOutputBytes: options.execMaxOutputBytes ?? 64 * 1024,
141
+ allowExecOutsideRoot: options.allowExecOutsideRoot ?? false,
142
+ eventMaxBytes: options.eventMaxBytes ?? 64 * 1024,
143
+ completionValidationTimeoutMs: options.completionValidationTimeoutMs ?? 60_000,
144
+ };
145
+ for (const [name, value] of Object.entries({
146
+ execTimeoutMs: this.options.execTimeoutMs,
147
+ execMaxOutputBytes: this.options.execMaxOutputBytes,
148
+ eventMaxBytes: this.options.eventMaxBytes,
149
+ completionValidationTimeoutMs: this.options.completionValidationTimeoutMs,
150
+ })) {
151
+ if (!Number.isInteger(value) || value <= 0) {
152
+ throw new Error(`MechanismRuntimeOptions.${name} 必须是正整数`);
153
+ }
154
+ }
155
+ pi.on("tool_call", async (event) => this.handleToolCall(event));
156
+ pi.on("tool_execution_start", async (event) => {
157
+ const run = this.activeRun;
158
+ if (!run)
159
+ return;
160
+ const snapshot = Object.freeze({
161
+ ...snapshotEvent(event),
162
+ agentRunId: run.agentRunId,
163
+ });
164
+ await this.invokeObservationHook("onToolStart", "onToolStart", snapshot);
165
+ });
166
+ pi.on("tool_result", async (event) => {
167
+ return this.handleToolResult(event);
168
+ });
169
+ pi.on("turn_start", async (event) => {
170
+ const agentRunId = this.activeRun?.agentRunId ?? null;
171
+ const snapshot = Object.freeze({ ...snapshotEvent(event), agentRunId });
172
+ await this.dispatch("turn_start", snapshot);
173
+ if (agentRunId !== null)
174
+ await this.invokeObservationHook("onTurnStart", "onTurnStart", snapshot);
175
+ });
176
+ pi.on("turn_end", async (event) => {
177
+ const agentRunId = this.activeRun?.agentRunId ?? null;
178
+ const snapshot = Object.freeze({ ...snapshotEvent(event), agentRunId });
179
+ await this.dispatch("turn_end", snapshot);
180
+ if (agentRunId !== null)
181
+ await this.invokeObservationHook("onTurnEnd", "onTurnEnd", snapshot);
182
+ });
183
+ }
184
+ createExec(scope) {
185
+ return Object.freeze({
186
+ run: async (command, args = [], runOptions = {}) => {
187
+ if (!scope.isActive())
188
+ throw new Error("mechanism scope 已失效,不能执行命令");
189
+ const timeout = runOptions.timeoutMs ?? this.options.execTimeoutMs;
190
+ const maxOutputBytes = runOptions.maxOutputBytes ?? this.options.execMaxOutputBytes;
191
+ if (!Number.isFinite(timeout) || timeout <= 0)
192
+ throw new Error("exec timeoutMs 必须是正数");
193
+ if (!Number.isInteger(maxOutputBytes) || maxOutputBytes <= 0) {
194
+ throw new Error("exec maxOutputBytes 必须是正整数");
195
+ }
196
+ const cwd = path.resolve(runOptions.cwd ?? this.options.execRoot);
197
+ if (!this.options.allowExecOutsideRoot && !isWithinPath(this.options.execRoot, cwd)) {
198
+ throw new Error(`exec cwd 超出受控根目录: ${cwd}`);
199
+ }
200
+ const result = await this.pi.exec(command, [...args], {
201
+ cwd,
202
+ timeout,
203
+ signal: scope.signal,
204
+ });
205
+ const stdout = truncateUtf8(result.stdout, maxOutputBytes);
206
+ const stderr = truncateUtf8(result.stderr, maxOutputBytes);
207
+ return Object.freeze({
208
+ stdout: stdout.value,
209
+ stderr: stderr.value,
210
+ code: result.code,
211
+ killed: result.killed,
212
+ stdoutTruncated: stdout.truncated,
213
+ stderrTruncated: stderr.truncated,
214
+ });
215
+ },
216
+ });
217
+ }
218
+ createDecisionLog(scope) {
219
+ if (!this.decisionTraces.has(scope.scopeId))
220
+ this.decisionTraces.set(scope.scopeId, []);
221
+ scope.onCleanup(() => { this.decisionTraces.delete(scope.scopeId); });
222
+ return Object.freeze({
223
+ list: () => Object.freeze([...(this.decisionTraces.get(scope.scopeId) ?? [])]),
224
+ });
225
+ }
226
+ async beginAgentRun(agentRunId, request, invocations) {
227
+ if (this.activeRun)
228
+ throw new Error("同一 Session 不支持重叠的 runAgent mechanism 生命周期");
229
+ this.activeRun = { agentRunId, invocations };
230
+ const requestView = snapshotEvent({
231
+ prompt: request.prompt,
232
+ ...(request.skill === undefined ? {} : { skill: request.skill }),
233
+ ...(request.outputSchema === undefined ? {} : { outputSchema: request.outputSchema }),
234
+ });
235
+ for (const invocation of invocations) {
236
+ if (invocation.initializationFailure || !invocation.mechanism.beforeAgentRun)
237
+ continue;
238
+ try {
239
+ const context = Object.freeze({
240
+ ...invocation.context,
241
+ agentRunId,
242
+ request: requestView,
243
+ });
244
+ await invocation.mechanism.beforeAgentRun(context);
245
+ }
246
+ catch (error) {
247
+ const failure = this.recordHookFailure(invocation, "beforeAgentRun", error);
248
+ if (failure.policy !== "continue")
249
+ return { blocked: true, reason: failure.reason };
250
+ }
251
+ }
252
+ return { blocked: false };
253
+ }
254
+ endAgentRun(agentRunId) {
255
+ if (this.activeRun?.agentRunId === agentRunId)
256
+ this.activeRun = null;
257
+ }
258
+ async validateCompletion(agentRunId, completion) {
259
+ const run = this.activeRun;
260
+ if (!run || run.agentRunId !== agentRunId || completion.status !== "ok") {
261
+ return { action: "allow" };
262
+ }
263
+ const checks = [];
264
+ const completionView = Object.freeze({
265
+ nodeId: completion.nodeId,
266
+ status: completion.status,
267
+ result: snapshotEvent(completion.result),
268
+ });
269
+ for (const invocation of run.invocations) {
270
+ const hook = invocation.mechanism.validateCompletion;
271
+ if (invocation.initializationFailure || !hook)
272
+ continue;
273
+ let decision;
274
+ try {
275
+ const context = Object.freeze({
276
+ ...invocation.context,
277
+ agentRunId,
278
+ completion: completionView,
279
+ });
280
+ decision = await withTimeoutAndSignal(Promise.resolve(hook(context)), this.options.completionValidationTimeoutMs, invocation.context.scope.signal, `mechanism "${invocation.mechanism.name}" completion 验收超时`);
281
+ }
282
+ catch (error) {
283
+ const failure = this.recordHookFailure(invocation, "validateCompletion", error);
284
+ if (failure.policy === "continue")
285
+ continue;
286
+ return {
287
+ action: failure.policy === "fail-graph" ? "fail-graph" : "fail-node",
288
+ reason: failure.reason,
289
+ };
290
+ }
291
+ if (decision.action === "allow") {
292
+ if (decision.verifiedResult) {
293
+ checks.push(Object.freeze({
294
+ mechanismName: invocation.mechanism.name,
295
+ result: snapshotEvent(decision.verifiedResult),
296
+ }));
297
+ }
298
+ continue;
299
+ }
300
+ if (decision.action === "reject") {
301
+ return { action: "reject", reason: decision.reason };
302
+ }
303
+ const failure = this.recordCompletionDecisionFailure(invocation, decision);
304
+ return { action: decision.action, reason: failure.reason };
305
+ }
306
+ if (checks.length === 0)
307
+ return { action: "allow" };
308
+ return {
309
+ action: "allow",
310
+ verifiedResult: Object.freeze({ checks: Object.freeze(checks) }),
311
+ };
312
+ }
313
+ createEvents(mechanismName, policy, scope) {
314
+ return Object.freeze({
315
+ onToolResult: (handler) => this.subscribe("tool_result", mechanismName, policy, scope, handler),
316
+ onTurnStart: (handler) => this.subscribe("turn_start", mechanismName, policy, scope, handler),
317
+ onTurnEnd: (handler) => this.subscribe("turn_end", mechanismName, policy, scope, handler),
318
+ });
319
+ }
320
+ consumeControlFailures(scopeId) {
321
+ const consumed = [];
322
+ for (let index = this.pendingFailures.length - 1; index >= 0; index--) {
323
+ if (this.pendingFailures[index].scopeId !== scopeId)
324
+ continue;
325
+ consumed.unshift(this.pendingFailures[index]);
326
+ this.pendingFailures.splice(index, 1);
327
+ }
328
+ return consumed;
329
+ }
330
+ async handleToolCall(event) {
331
+ const run = this.activeRun;
332
+ if (!run)
333
+ return;
334
+ let currentInput = snapshotEvent(event.input);
335
+ for (const invocation of run.invocations) {
336
+ const hook = invocation.mechanism.beforeToolCall;
337
+ if (invocation.initializationFailure || !hook)
338
+ continue;
339
+ const eventView = Object.freeze({
340
+ type: "tool_call",
341
+ toolCallId: event.toolCallId,
342
+ toolName: event.toolName,
343
+ input: currentInput,
344
+ agentRunId: run.agentRunId,
345
+ });
346
+ let decision;
347
+ try {
348
+ decision = await hook(Object.freeze({
349
+ ...invocation.context,
350
+ agentRunId: run.agentRunId,
351
+ event: eventView,
352
+ }));
353
+ }
354
+ catch (error) {
355
+ const failure = this.recordHookFailure(invocation, "beforeToolCall", error);
356
+ if (failure.policy !== "continue")
357
+ return { block: true, reason: failure.reason };
358
+ continue;
359
+ }
360
+ if (!decision || decision.action === "allow") {
361
+ this.recordDecision(invocation, event, run.agentRunId, "tool-allow");
362
+ continue;
363
+ }
364
+ if (decision.action === "deny") {
365
+ const reason = decision.reason.trim() || `mechanism ${invocation.mechanism.name} 阻止了工具调用`;
366
+ this.recordDecision(invocation, event, run.agentRunId, "tool-deny", reason);
367
+ return { block: true, reason };
368
+ }
369
+ if (event.toolName === "__graph_complete__") {
370
+ const reason = "__graph_complete__ 使用固定 ABI,不允许一般 mechanism patch";
371
+ this.recordDecision(invocation, event, run.agentRunId, "tool-deny", reason);
372
+ return { block: true, reason };
373
+ }
374
+ const patched = snapshotEvent(decision.input);
375
+ const validationError = this.validateToolInput(event.toolName, patched);
376
+ if (validationError) {
377
+ const reason = `工具参数 patch 被拒绝: ${validationError}`;
378
+ this.recordDecision(invocation, event, run.agentRunId, "tool-deny", reason);
379
+ return { block: true, reason };
380
+ }
381
+ currentInput = patched;
382
+ this.recordDecision(invocation, event, run.agentRunId, "tool-patch");
383
+ }
384
+ if (currentInput !== event.input) {
385
+ const mutableInput = event.input;
386
+ for (const key of Object.keys(mutableInput))
387
+ delete mutableInput[key];
388
+ Object.assign(mutableInput, currentInput);
389
+ }
390
+ }
391
+ async handleToolResult(event) {
392
+ const run = this.activeRun;
393
+ let content = event.content;
394
+ let isError = event.isError;
395
+ if (run) {
396
+ for (const invocation of run.invocations) {
397
+ const hook = invocation.mechanism.afterToolResult;
398
+ if (invocation.initializationFailure || !hook)
399
+ continue;
400
+ const view = this.createToolResultView(event, run.agentRunId, content, isError);
401
+ let decision;
402
+ try {
403
+ decision = await hook(Object.freeze({
404
+ ...invocation.context,
405
+ agentRunId: run.agentRunId,
406
+ event: view,
407
+ }));
408
+ }
409
+ catch (error) {
410
+ const failure = this.recordHookFailure(invocation, "afterToolResult", error);
411
+ if (failure.policy !== "continue") {
412
+ content = [{ type: "text", text: failure.reason }];
413
+ isError = true;
414
+ }
415
+ continue;
416
+ }
417
+ if (!decision || decision.action === "keep") {
418
+ this.recordDecision(invocation, event, run.agentRunId, "tool-result-keep");
419
+ continue;
420
+ }
421
+ if (decision.content)
422
+ content = [...decision.content];
423
+ if (decision.isError !== undefined)
424
+ isError = decision.isError;
425
+ this.recordDecision(invocation, event, run.agentRunId, "tool-result-replace");
426
+ }
427
+ }
428
+ const agentRunId = run?.agentRunId ?? null;
429
+ const finalView = this.createToolResultView(event, agentRunId, content, isError);
430
+ await this.dispatch("tool_result", finalView);
431
+ if (run)
432
+ await this.invokeObservationHook("onToolResult", "onToolResult", finalView);
433
+ if (content !== event.content || isError !== event.isError)
434
+ return { content, isError };
435
+ }
436
+ createToolResultView(event, agentRunId, content, isError) {
437
+ const budgeted = snapshotWithBudget({ ...event, content, isError }, this.options.eventMaxBytes);
438
+ return Object.freeze({
439
+ ...budgeted.value,
440
+ agentRunId,
441
+ truncated: budgeted.truncated,
442
+ });
443
+ }
444
+ async invokeObservationHook(hookName, phase, event) {
445
+ const run = this.activeRun;
446
+ if (!run)
447
+ return;
448
+ for (const invocation of run.invocations) {
449
+ const hook = invocation.mechanism[hookName];
450
+ if (invocation.initializationFailure || !hook)
451
+ continue;
452
+ try {
453
+ await hook(Object.freeze({
454
+ ...invocation.context,
455
+ agentRunId: run.agentRunId,
456
+ event,
457
+ }));
458
+ }
459
+ catch (error) {
460
+ this.recordHookFailure(invocation, phase, error);
461
+ }
462
+ }
463
+ }
464
+ validateToolInput(toolName, input) {
465
+ const tool = this.pi.getAllTools().find((candidate) => candidate.name === toolName);
466
+ if (!tool?.parameters)
467
+ return `工具 ${toolName} 没有可用 schema`;
468
+ try {
469
+ const validator = Schema.Compile(tool.parameters);
470
+ const [isValid, errors] = validator.Errors(input);
471
+ if (isValid)
472
+ return null;
473
+ return errors.slice(0, 3).map((item) => `${item.instancePath || "$"} ${item.message}`).join("; ");
474
+ }
475
+ catch (error) {
476
+ return `工具 ${toolName} schema 无法安全编译: ${error instanceof Error ? error.message : String(error)}`;
477
+ }
478
+ }
479
+ recordHookFailure(invocation, phase, error) {
480
+ const message = error instanceof Error ? error.message : String(error);
481
+ const failure = {
482
+ mechanismName: invocation.mechanism.name,
483
+ phase,
484
+ policy: invocation.mechanism.failurePolicy ?? "continue",
485
+ error,
486
+ reason: `mechanism "${invocation.mechanism.name}" ${phase} 失败: ${message}`,
487
+ scopeId: invocation.context.scope.scopeId,
488
+ };
489
+ this.reportFailure(failure);
490
+ if (failure.policy !== "continue" && invocation.context.scope.isActive()) {
491
+ this.pendingFailures.push(failure);
492
+ }
493
+ return failure;
494
+ }
495
+ recordCompletionDecisionFailure(invocation, decision) {
496
+ const policy = decision.action;
497
+ const failure = {
498
+ mechanismName: invocation.mechanism.name,
499
+ phase: "validateCompletion",
500
+ policy,
501
+ error: new Error(decision.reason),
502
+ reason: `mechanism "${invocation.mechanism.name}" completion gate ${decision.action}: ${decision.reason}`,
503
+ scopeId: invocation.context.scope.scopeId,
504
+ };
505
+ this.reportFailure(failure);
506
+ if (invocation.context.scope.isActive())
507
+ this.pendingFailures.push(failure);
508
+ return failure;
509
+ }
510
+ recordDecision(invocation, event, agentRunId, decision, reason) {
511
+ const list = this.decisionTraces.get(invocation.context.scope.scopeId);
512
+ if (!list)
513
+ return;
514
+ list.push(Object.freeze({
515
+ timestamp: Date.now(),
516
+ agentRunId,
517
+ mechanismName: invocation.mechanism.name,
518
+ toolName: event.toolName,
519
+ toolCallId: event.toolCallId,
520
+ decision,
521
+ ...(reason === undefined ? {} : { reason }),
522
+ }));
523
+ }
524
+ subscribe(eventName, mechanismName, policy, scope, handler) {
525
+ const subscriber = {
526
+ eventName,
527
+ mechanismName,
528
+ policy,
529
+ scope,
530
+ handler: (event) => handler(event),
531
+ disposed: false,
532
+ };
533
+ this.subscribers.get(eventName).push(subscriber);
534
+ const subscription = new BrokerSubscription(subscriber, (item) => this.removeSubscriber(item));
535
+ scope.onCleanup(() => subscription.dispose());
536
+ return subscription;
537
+ }
538
+ removeSubscriber(subscriber) {
539
+ const list = this.subscribers.get(subscriber.eventName);
540
+ const index = list.indexOf(subscriber);
541
+ if (index >= 0)
542
+ list.splice(index, 1);
543
+ }
544
+ async dispatch(eventName, event) {
545
+ const snapshot = [...this.subscribers.get(eventName)];
546
+ for (const subscriber of snapshot) {
547
+ if (subscriber.disposed || !subscriber.scope.isActive())
548
+ continue;
549
+ try {
550
+ await subscriber.handler(event);
551
+ }
552
+ catch (error) {
553
+ const message = error instanceof Error ? error.message : String(error);
554
+ const failure = {
555
+ mechanismName: subscriber.mechanismName,
556
+ phase: eventName,
557
+ policy: subscriber.policy,
558
+ error,
559
+ reason: `mechanism "${subscriber.mechanismName}" ${eventName} handler 失败: ${message}`,
560
+ scopeId: subscriber.scope.scopeId,
561
+ };
562
+ this.reportFailure(failure);
563
+ if (subscriber.policy !== "continue" && subscriber.scope.isActive()) {
564
+ this.pendingFailures.push(failure);
565
+ }
566
+ }
567
+ }
568
+ }
569
+ }
570
+ function snapshotEvent(value) {
571
+ return snapshotValue(value, new WeakMap());
572
+ }
573
+ function snapshotValue(value, seen) {
574
+ if (value === null || typeof value !== "object")
575
+ return value;
576
+ const cached = seen.get(value);
577
+ if (cached)
578
+ return cached;
579
+ if (Array.isArray(value)) {
580
+ const copy = [];
581
+ seen.set(value, copy);
582
+ for (const item of value)
583
+ copy.push(snapshotValue(item, seen));
584
+ return Object.freeze(copy);
585
+ }
586
+ if (value instanceof Date)
587
+ return Object.freeze(new Date(value.getTime()));
588
+ if (value instanceof Map) {
589
+ const copy = new Map();
590
+ seen.set(value, copy);
591
+ for (const [key, item] of value) {
592
+ copy.set(snapshotValue(key, seen), snapshotValue(item, seen));
593
+ }
594
+ return copy;
595
+ }
596
+ if (value instanceof Set) {
597
+ const copy = new Set();
598
+ seen.set(value, copy);
599
+ for (const item of value)
600
+ copy.add(snapshotValue(item, seen));
601
+ return copy;
602
+ }
603
+ const copy = {};
604
+ seen.set(value, copy);
605
+ for (const key of Object.keys(value)) {
606
+ copy[key] = snapshotValue(value[key], seen);
607
+ }
608
+ return Object.freeze(copy);
609
+ }
610
+ function isWithinPath(root, candidate) {
611
+ const relative = path.relative(root, candidate);
612
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
613
+ }
614
+ function truncateUtf8(value, maxBytes) {
615
+ const bytes = Buffer.from(value, "utf8");
616
+ if (bytes.length <= maxBytes)
617
+ return { value, truncated: false };
618
+ return { value: bytes.subarray(0, maxBytes).toString("utf8"), truncated: true };
619
+ }
620
+ function snapshotWithBudget(value, maxBytes) {
621
+ let remaining = maxBytes;
622
+ let truncated = false;
623
+ const visit = (item, seen) => {
624
+ if (typeof item === "string") {
625
+ const result = truncateUtf8(item, Math.max(0, remaining));
626
+ remaining -= Buffer.byteLength(result.value, "utf8");
627
+ if (result.truncated)
628
+ truncated = true;
629
+ return result.value;
630
+ }
631
+ if (item === null || typeof item !== "object")
632
+ return item;
633
+ const cached = seen.get(item);
634
+ if (cached)
635
+ return cached;
636
+ if (Array.isArray(item)) {
637
+ const copy = [];
638
+ seen.set(item, copy);
639
+ for (const child of item)
640
+ copy.push(visit(child, seen));
641
+ return Object.freeze(copy);
642
+ }
643
+ const copy = {};
644
+ seen.set(item, copy);
645
+ for (const key of Object.keys(item))
646
+ copy[key] = visit(item[key], seen);
647
+ return Object.freeze(copy);
648
+ };
649
+ return { value: visit(value, new WeakMap()), truncated };
650
+ }
651
+ async function withTimeoutAndSignal(promise, timeoutMs, signal, timeoutMessage) {
652
+ if (signal.aborted)
653
+ throw new Error("mechanism scope 已取消");
654
+ let timeout;
655
+ let abortHandler;
656
+ const guard = new Promise((_resolve, reject) => {
657
+ timeout = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
658
+ abortHandler = () => reject(new Error("mechanism scope 已取消"));
659
+ signal.addEventListener("abort", abortHandler, { once: true });
660
+ });
661
+ try {
662
+ return await Promise.race([promise, guard]);
663
+ }
664
+ finally {
665
+ if (timeout)
666
+ clearTimeout(timeout);
667
+ if (abortHandler)
668
+ signal.removeEventListener("abort", abortHandler);
669
+ }
670
+ }
@@ -0,0 +1,17 @@
1
+ export interface IncompleteNodeMessageInput {
2
+ nodeId: string;
3
+ completeToolName: "__graph_complete__";
4
+ }
5
+ export interface DeadRunMessageInput {
6
+ nodeId: string | null;
7
+ }
8
+ export interface GraphFailureMessageInput {
9
+ graphId: string;
10
+ reason: string;
11
+ }
12
+ export interface ModelMessageFormatter {
13
+ incompleteNode(input: IncompleteNodeMessageInput): string;
14
+ deadRun(input: DeadRunMessageInput): string;
15
+ graphFailure(input: GraphFailureMessageInput): string;
16
+ }
17
+ export declare const defaultModelMessageFormatter: ModelMessageFormatter;
@@ -0,0 +1,11 @@
1
+ export const defaultModelMessageFormatter = {
2
+ incompleteNode() {
3
+ return "Agent finished without calling __graph_complete__.";
4
+ },
5
+ deadRun() {
6
+ return "[系统] 当前图已终止,你的后续操作不会被接收。";
7
+ },
8
+ graphFailure(input) {
9
+ return `[系统] 图 "${input.graphId}" 因错误意外终止:${input.reason}。当前节点已失效,请停止相关图工作。`;
10
+ },
11
+ };