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,487 @@
1
+ import { Type } from "typebox";
2
+ import { AgentExecutionFailure, GraphRuntime as CoreGraphRuntime } from "../runtime/graph-runtime.js";
3
+ import { RuntimeEventBus } from "../runtime/event-bus.js";
4
+ import { GraphCatalog } from "../host/graph-catalog.js";
5
+ import { preflightGraphCapabilities } from "../host/preflight.js";
6
+ import { ToolCatalog } from "../host/tool-catalog.js";
7
+ import { CONTEXT_SNAPSHOT_MESSAGE_TYPE, PiNodeContext } from "./pi-node-context.js";
8
+ import { COMPLETE_TOOL_NAME, createCompleteTool } from "./complete-tool.js";
9
+ import { defaultModelMessageFormatter, } from "./model-messages.js";
10
+ import { OUTPUT_CONTRACT_MESSAGE_TYPE } from "./output-contract.js";
11
+ import { FileRunStore } from "../replay/store.js";
12
+ import { Recorder, toRecordedJson } from "../replay/recorder.js";
13
+ // `pi` creates one ExtensionAPI facade per extension, so neither a module-local
14
+ // WeakSet nor a marker on `pi` can deduplicate physical SDK copies. Use the
15
+ // process-global registry shared by every ESM module copy instead. Isolated
16
+ // AgentSessions supply a registration scope because their tool registries are
17
+ // independent even though they run in the same JavaScript process.
18
+ const COMPLETE_TOOL_REGISTRY = Symbol.for("pi-loop-graph-sdk.complete-tool-registry");
19
+ export const defaultCompletionFeedbackFormatter = ({ decision }) => {
20
+ if (decision.decision === "accepted") {
21
+ if (decision.validation === "passed")
22
+ return "节点结果已通过检查并接受。";
23
+ return decision.completionStatus === "failed" ? "Agent 报告当前节点失败。" : "Agent 报告当前节点取消。";
24
+ }
25
+ if (decision.decision === "rejected")
26
+ return `节点结果未被接受:${decision.reason}`;
27
+ return `${decision.scope === "graph" ? "图" : "节点"}验收失败:${decision.reason}`;
28
+ };
29
+ export function createLoopGraphExtension(pi, options = {}) {
30
+ const limits = resolveLimits(options.limits);
31
+ const modelMessageFormatter = {
32
+ incompleteNode: options.modelMessageFormatter?.incompleteNode ?? defaultModelMessageFormatter.incompleteNode,
33
+ deadRun: options.modelMessageFormatter?.deadRun ?? defaultModelMessageFormatter.deadRun,
34
+ graphFailure: options.modelMessageFormatter?.graphFailure ?? defaultModelMessageFormatter.graphFailure,
35
+ };
36
+ const completionFeedbackFormatter = options.completionFeedbackFormatter ?? defaultCompletionFeedbackFormatter;
37
+ const catalog = new GraphCatalog();
38
+ const piToolCatalog = options.toolCatalog ? new ToolCatalog() : undefined;
39
+ let rootActive = false;
40
+ let activeNodeContext = null;
41
+ let activeRecorder = null;
42
+ const recordPiEvent = (event, scope) => {
43
+ if (activeRecorder && scope)
44
+ activeRecorder.record(event, scope);
45
+ };
46
+ // A long-lived Pi process can contain an older physical SDK copy which used
47
+ // this Symbol for a bare Set. Upgrade that value in place-compatible form so
48
+ // extensions loaded from mixed SDK revisions do not fail during startup.
49
+ const processState = globalThis;
50
+ const existingRegistry = processState[COMPLETE_TOOL_REGISTRY];
51
+ const completeToolRegistry = existingRegistry instanceof Set
52
+ ? {
53
+ global: existingRegistry,
54
+ scoped: new WeakMap(),
55
+ }
56
+ : existingRegistry ?? {
57
+ global: new Set(),
58
+ scoped: new WeakMap(),
59
+ };
60
+ processState[COMPLETE_TOOL_REGISTRY] = completeToolRegistry;
61
+ const registrationScope = options.protocolToolRegistrationScope;
62
+ const registeredProtocolTools = registrationScope
63
+ ? completeToolRegistry.scoped.get(registrationScope) ?? (() => {
64
+ const tools = new Set();
65
+ completeToolRegistry.scoped.set(registrationScope, tools);
66
+ return tools;
67
+ })()
68
+ : completeToolRegistry.global;
69
+ if (!registeredProtocolTools.has(COMPLETE_TOOL_NAME)) {
70
+ pi.registerTool(createCompleteTool());
71
+ registeredProtocolTools.add(COMPLETE_TOOL_NAME);
72
+ }
73
+ pi.on("tool_result", async (event) => {
74
+ const replayScope = activeNodeContext?.getReplayScope();
75
+ recordPiEvent({
76
+ domain: "tool",
77
+ type: "tool_result",
78
+ data: toRecordedJson({ toolName: event.toolName, input: event.input, content: event.content, isError: event.isError }, "forensic"),
79
+ }, replayScope ? { ...replayScope, toolCallId: event.toolCallId } : null);
80
+ if (event.toolName !== COMPLETE_TOOL_NAME || !activeNodeContext)
81
+ return;
82
+ const params = event.input;
83
+ if (!params || Object.keys(params).some((key) => key !== "result") || !isRecord(params.result)) {
84
+ const decision = {
85
+ decision: "rejected",
86
+ reason: "完成提交只能包含对象类型 result",
87
+ };
88
+ recordPiEvent({ domain: "completion", type: "completion.rejected", data: { reason: decision.reason, validatorStage: "protocol" } }, activeNodeContext.getReplayScope());
89
+ return {
90
+ content: [{ type: "text", text: completionFeedbackFormatter({ nodeId: "?", decision }) }],
91
+ details: decision,
92
+ isError: true,
93
+ };
94
+ }
95
+ const decision = await activeNodeContext.submitCompletion({
96
+ result: params.result,
97
+ });
98
+ return {
99
+ content: [{ type: "text", text: completionFeedbackFormatter({ nodeId: "?", decision }) }],
100
+ details: decision,
101
+ isError: decision.decision !== "accepted",
102
+ };
103
+ });
104
+ pi.on("agent_end", async () => {
105
+ await activeNodeContext?.onAgentEnd();
106
+ });
107
+ pi.on("turn_start", async (event) => {
108
+ recordPiEvent({ domain: "model", type: "model_turn_started", data: { turn: event.turnIndex } }, activeNodeContext?.getReplayScope());
109
+ });
110
+ pi.on("turn_end", async (event) => {
111
+ const message = event.message;
112
+ recordPiEvent({
113
+ domain: "model",
114
+ type: "model_turn_finished",
115
+ data: toRecordedJson({
116
+ turn: event.turnIndex,
117
+ provider: message.provider ?? "unknown",
118
+ model: message.model ?? "unknown",
119
+ usage: message.usage ?? {},
120
+ durationMs: message.durationMs,
121
+ retry: message.retry ?? 0,
122
+ message,
123
+ toolResults: event.toolResults,
124
+ }, "forensic"),
125
+ }, activeNodeContext?.getReplayScope());
126
+ });
127
+ pi.on("tool_execution_start", async (event) => {
128
+ const scope = activeNodeContext?.getReplayScope();
129
+ recordPiEvent({ domain: "tool", type: "tool_execution_started", data: toRecordedJson({ toolName: event.toolName, args: event.args }, "forensic") }, scope ? { ...scope, toolCallId: event.toolCallId } : null);
130
+ });
131
+ pi.on("tool_execution_end", async (event) => {
132
+ const scope = activeNodeContext?.getReplayScope();
133
+ recordPiEvent({ domain: "tool", type: "tool_execution_finished", data: toRecordedJson({ toolName: event.toolName, result: event.result, isError: event.isError }, "forensic") }, scope ? { ...scope, toolCallId: event.toolCallId } : null);
134
+ });
135
+ pi.on("session_compact", async (event) => {
136
+ activeNodeContext?.markContextCompacted();
137
+ recordPiEvent({ domain: "compaction", type: "compaction_finished", data: toRecordedJson(event, "forensic") }, activeNodeContext?.getReplayScope());
138
+ });
139
+ pi.on("context", async (event) => {
140
+ if (event.messages.some((item) => item.role === "compactionSummary")) {
141
+ activeNodeContext?.markContextCompacted();
142
+ }
143
+ const message = activeNodeContext?.getContextSnapshotMessage();
144
+ const contract = activeNodeContext?.getActiveOutputContractMessage();
145
+ const messages = event.messages.filter((item) => !(item.role === "custom" && (item.customType === CONTEXT_SNAPSHOT_MESSAGE_TYPE || item.customType === OUTPUT_CONTRACT_MESSAGE_TYPE)));
146
+ if (message) {
147
+ recordPiEvent({ domain: "context", type: "context_snapshot_projected", data: toRecordedJson(message, "forensic") }, activeNodeContext?.getReplayScope());
148
+ }
149
+ return { messages: [message, contract, ...messages].filter(Boolean) };
150
+ });
151
+ if (!options.runtimeOnly) {
152
+ pi.on("session_start", async (_event, context) => {
153
+ context.ui.notify("Loop Graph Extension 已加载", "info");
154
+ });
155
+ }
156
+ function registerCoreGraph(graph) {
157
+ syncPiBusinessTools(options.toolCatalog, piToolCatalog, pi);
158
+ preflightGraphCapabilities(graph, { ...options, toolCatalog: piToolCatalog });
159
+ catalog.register(graph);
160
+ }
161
+ async function executeGraph(graph, trigger, executionOptions = {}) {
162
+ if (rootActive)
163
+ throw new Error("同一 LoopGraphExtension instance 不支持并发 root executeGraph;请使用独立 Host");
164
+ rootActive = true;
165
+ const previousTools = saveActiveTools(pi);
166
+ const previousContext = activeNodeContext;
167
+ const previousRecorder = activeRecorder;
168
+ const eventBus = new RuntimeEventBus();
169
+ const recording = executionOptions.recording ?? options.recording ?? "replay";
170
+ const recorder = recording === "off" ? null : new Recorder({
171
+ mode: recording,
172
+ store: options.runStore ?? new FileRunStore(),
173
+ artifactThresholdBytes: options.artifactThresholdBytes,
174
+ pricingResolver: options.pricingResolver,
175
+ });
176
+ recorder?.attach(eventBus);
177
+ activeRecorder = recorder;
178
+ const nodeContext = new PiNodeContext(pi, limits.agentRunTimeoutMs, modelMessageFormatter, limits.completionValidationTimeoutMs, options.outputContractMaxBytes, (event) => recordPiEvent({
179
+ domain: event.type.startsWith("completion.") ? "completion" : "context",
180
+ type: event.type,
181
+ data: toRecordedJson(event, "forensic"),
182
+ }, nodeContext.getReplayScope()));
183
+ activeNodeContext = nodeContext;
184
+ try {
185
+ syncPiBusinessTools(options.toolCatalog, piToolCatalog, pi);
186
+ const input = trigger.source === "tool" || trigger.params ? (trigger.params ?? {}) : { args: trigger.args ?? "" };
187
+ const runtime = new CoreGraphRuntime({
188
+ eventBus,
189
+ catalog,
190
+ checkpointStore: options.runStore,
191
+ toolCatalog: piToolCatalog,
192
+ skillCatalog: options.skillCatalog,
193
+ unsafeToolResolver: options.unsafeToolResolver,
194
+ baseline: options.baseline,
195
+ maxStickyContextBytes: options.contextMaxBytes,
196
+ mechanisms: options.mechanisms,
197
+ createInvocationAgentHost: options.createInvocationAgentHost
198
+ ? (request) => options.createInvocationAgentHost(request, activeRecorder)
199
+ : undefined,
200
+ delegateGraph: options.createInvocationAgentHost
201
+ ? (request) => request.execute()
202
+ : undefined,
203
+ runAgent: (node, _input, context) => {
204
+ pi.setActiveTools(context.tools.map((tool) => tool.name));
205
+ return runPiAgent(node.prompt ?? node.subGoal, node.output, context, nodeContext);
206
+ },
207
+ runAgentFromCode: (request, _node, context) => {
208
+ pi.setActiveTools(context.tools.map((tool) => tool.name));
209
+ return runPiAgent(request.prompt, request.output, context, nodeContext);
210
+ },
211
+ });
212
+ const result = await runtime.execute(graph, input, {
213
+ signal: executionOptions.signal,
214
+ limits: executionOptions.limits,
215
+ maxSteps: executionOptions.maxSteps ?? limits.rootMaxSteps,
216
+ });
217
+ if (!recorder)
218
+ return result;
219
+ const finalized = await recorder.finalize(result);
220
+ if ((executionOptions.recordingRequired ?? options.recordingRequired) && finalized.replay.status !== "complete") {
221
+ return {
222
+ rootRunId: result.rootRunId,
223
+ graphId: result.graphId,
224
+ graphVersion: result.graphVersion,
225
+ steps: result.steps,
226
+ durationMs: result.durationMs,
227
+ status: "failed",
228
+ replay: finalized.replay,
229
+ failure: {
230
+ code: "persistence-failed",
231
+ phase: "host",
232
+ message: finalized.replay.issues?.join("; ") ?? "Replay recording failed",
233
+ retryable: true,
234
+ },
235
+ };
236
+ }
237
+ return { ...result, replay: finalized.replay };
238
+ }
239
+ finally {
240
+ nodeContext.reset();
241
+ restoreActiveTools(pi, previousTools);
242
+ activeNodeContext = previousContext;
243
+ activeRecorder = previousRecorder;
244
+ rootActive = false;
245
+ }
246
+ }
247
+ return {
248
+ registerGraph: registerCoreGraph,
249
+ exposeGraph,
250
+ executeGraph,
251
+ createAgentHost,
252
+ };
253
+ function createAgentHost(recorder = null) {
254
+ const nodeContext = new PiNodeContext(pi, limits.agentRunTimeoutMs, modelMessageFormatter, limits.completionValidationTimeoutMs, options.outputContractMaxBytes, (event) => recordPiEvent({
255
+ domain: event.type.startsWith("completion.") ? "completion" : "context",
256
+ type: event.type,
257
+ data: toRecordedJson(event, "forensic"),
258
+ }, nodeContext.getReplayScope()));
259
+ let disposed = false;
260
+ const run = async (prompt, output, execution) => {
261
+ if (disposed)
262
+ throw new Error("Invocation Agent Host 已释放");
263
+ const previousContext = activeNodeContext;
264
+ const previousRecorder = activeRecorder;
265
+ activeNodeContext = nodeContext;
266
+ activeRecorder = recorder;
267
+ try {
268
+ pi.setActiveTools(execution.tools.map((tool) => tool.name));
269
+ return await runPiAgent(prompt, output, execution, nodeContext);
270
+ }
271
+ finally {
272
+ activeNodeContext = previousContext;
273
+ activeRecorder = previousRecorder;
274
+ }
275
+ };
276
+ return {
277
+ runAgent: (node, _input, execution) => run(node.prompt ?? node.subGoal, node.output, execution),
278
+ runAgentFromCode: (request, _node, execution) => run(request.prompt, request.output, execution),
279
+ dispose() {
280
+ if (disposed)
281
+ return;
282
+ disposed = true;
283
+ nodeContext.reset();
284
+ },
285
+ };
286
+ }
287
+ function exposeGraph(ref, exposure) {
288
+ const graph = catalog.resolve(ref);
289
+ if (!graph)
290
+ throw new Error(`Graph not registered: ${ref.id}@${ref.version}`);
291
+ if (exposure.kind === "command") {
292
+ pi.registerCommand(exposure.name, {
293
+ description: exposure.description,
294
+ async handler(args, context) {
295
+ const input = (exposure.parseInput?.(args) ?? { args });
296
+ if (exposure.execution === "current-session") {
297
+ await executeGraph(graph, { source: "command", params: input });
298
+ }
299
+ else {
300
+ await executeExposedGraph(graph, input, context);
301
+ }
302
+ },
303
+ });
304
+ return;
305
+ }
306
+ pi.registerTool({
307
+ name: exposure.name,
308
+ label: exposure.name,
309
+ description: exposure.description ?? exposure.name,
310
+ parameters: exposure.parameters ?? Type.Record(Type.String(), Type.Unknown()),
311
+ async execute(_toolCallId, params, _signal, _onUpdate, context) {
312
+ const input = (exposure.parseInput?.(params) ?? params);
313
+ const result = exposure.execution === "current-session"
314
+ ? await executeGraph(graph, { source: "tool", params: input })
315
+ : await executeExposedGraph(graph, input, context);
316
+ const formatted = exposure.formatResult?.(result) ?? result;
317
+ return { content: [{ type: "text", text: typeof formatted === "string" ? formatted : JSON.stringify(formatted) }], details: formatted };
318
+ },
319
+ });
320
+ }
321
+ async function executeExposedGraph(graph, input, context) {
322
+ const [{ AuthStorage }, { createPiGraphHost }] = await Promise.all([
323
+ import("@earendil-works/pi-coding-agent"),
324
+ import("./isolated-graph-session.js"),
325
+ ]);
326
+ const authStorage = AuthStorage.create();
327
+ const host = await createPiGraphHost({
328
+ authStorage,
329
+ modelRegistry: context.modelRegistry,
330
+ cwd: context.cwd,
331
+ model: context.model,
332
+ graphs: catalog.values,
333
+ toolCatalog: options.toolCatalog,
334
+ skillCatalog: options.skillCatalog,
335
+ unsafeToolResolver: options.unsafeToolResolver,
336
+ baseline: options.baseline,
337
+ limits: options.limits,
338
+ outputContractMaxBytes: options.outputContractMaxBytes,
339
+ recording: options.recording,
340
+ recordingRequired: options.recordingRequired,
341
+ runStore: options.runStore,
342
+ artifactThresholdBytes: options.artifactThresholdBytes,
343
+ pricingResolver: options.pricingResolver,
344
+ });
345
+ try {
346
+ return await host.execute(graph, input, { signal: context.signal });
347
+ }
348
+ finally {
349
+ await host.dispose();
350
+ }
351
+ }
352
+ async function runPiAgent(prompt, outputSchema, execution, context) {
353
+ const stageId = execution.nodeVisit.stageId;
354
+ context.setCurrentNodeId(stageId);
355
+ context.setContextSnapshot(execution.snapshot);
356
+ context.setNodeCompletionValidator(async (result) => {
357
+ const validation = await execution.validateNodeCompletion(result);
358
+ return validation.valid ? { isValid: true } : { isValid: false, reason: validation.reason ?? "Node completion rejected" };
359
+ });
360
+ context.setRouteCompletionValidator(async (result) => {
361
+ const validation = await execution.validateRouteStructure(result);
362
+ return validation.valid ? { isValid: true } : { isValid: false, reason: validation.reason ?? "Route structure rejected" };
363
+ });
364
+ context.setMechanismLifecycle({
365
+ beforeAgentRun: async () => ({ blocked: false }),
366
+ validateCompletion: async (_agentRunId, completion) => {
367
+ const decision = await execution.validateMechanismCompletion(completion.result);
368
+ return decision.action === "allow"
369
+ ? { action: "allow" }
370
+ : { action: decision.action, reason: decision.reason };
371
+ },
372
+ afterAgentRun: () => undefined,
373
+ });
374
+ context.setPostMechanismCompletionValidator(async (result) => {
375
+ const validation = await execution.validateAgentChoice(result);
376
+ return validation.valid ? { isValid: true } : { isValid: false, reason: validation.reason ?? "Agent choice rejected" };
377
+ });
378
+ try {
379
+ const completion = await context.runAgent({
380
+ prompt,
381
+ outputSchema: outputSchema,
382
+ });
383
+ if (completion.status !== "ok") {
384
+ throw new AgentExecutionFailure(toAgentFailure(completion, stageId));
385
+ }
386
+ return completion.result;
387
+ }
388
+ finally {
389
+ context.setContextSnapshot(null);
390
+ context.setMechanismLifecycle(null);
391
+ }
392
+ }
393
+ }
394
+ function toAgentFailure(completion, stageId) {
395
+ const result = completion.result;
396
+ const reason = typeof result.reason === "string" ? result.reason : "Agent execution failed";
397
+ const runtimeFailure = isRecord(result.runtimeFailure) ? result.runtimeFailure : undefined;
398
+ const completionGate = isRecord(result.completionGate) ? result.completionGate : undefined;
399
+ const gateAction = completionGate?.action;
400
+ if (gateAction === "fail-node" || gateAction === "fail-graph") {
401
+ return {
402
+ code: "mechanism-failed",
403
+ phase: gateAction === "fail-graph" ? "graph" : "node",
404
+ message: reason,
405
+ retryable: false,
406
+ stageId,
407
+ };
408
+ }
409
+ const code = typeof runtimeFailure?.code === "string"
410
+ ? runtimeFailure.code
411
+ : "runtime-error";
412
+ const phase = typeof runtimeFailure?.phase === "string"
413
+ ? runtimeFailure.phase
414
+ : "agent";
415
+ return {
416
+ code,
417
+ phase,
418
+ message: reason,
419
+ retryable: runtimeFailure?.retryable === true,
420
+ stageId,
421
+ };
422
+ }
423
+ function resolveLimits(limits) {
424
+ const resolved = {
425
+ rootMaxSteps: limits?.rootMaxSteps ?? 100,
426
+ childMaxSteps: limits?.childMaxSteps ?? 50,
427
+ agentRunTimeoutMs: limits?.agentRunTimeoutMs ?? 300_000,
428
+ completionValidationTimeoutMs: limits?.completionValidationTimeoutMs ?? 60_000,
429
+ };
430
+ for (const [name, value] of Object.entries(resolved)) {
431
+ if (!Number.isInteger(value) || value <= 0)
432
+ throw new Error(`${name} 必须是正整数`);
433
+ }
434
+ return resolved;
435
+ }
436
+ function isRecord(value) {
437
+ return typeof value === "object" && value !== null && !Array.isArray(value);
438
+ }
439
+ function saveActiveTools(pi) {
440
+ try {
441
+ return pi.getActiveTools();
442
+ }
443
+ catch {
444
+ return [];
445
+ }
446
+ }
447
+ function restoreActiveTools(pi, tools) {
448
+ pi.setActiveTools(tools);
449
+ }
450
+ function syncPiBusinessTools(catalog, available, pi) {
451
+ if (!catalog || !available)
452
+ return;
453
+ const registered = new Set(pi.getAllTools().map((tool) => tool.name));
454
+ for (const name of catalog.names) {
455
+ const tool = catalog.resolve(name);
456
+ if (!tool || available.has(name))
457
+ continue;
458
+ if (registered.has(name)) {
459
+ available.register(tool);
460
+ continue;
461
+ }
462
+ if (!tool.execute)
463
+ continue;
464
+ const definition = {
465
+ name: tool.name,
466
+ label: tool.label ?? tool.name,
467
+ description: tool.description ?? tool.name,
468
+ parameters: tool.parameters ?? Type.Record(Type.String(), Type.Unknown()),
469
+ async execute(_toolCallId, params, signal, onUpdate, context) {
470
+ const result = await tool.execute(params, signal, onUpdate, context);
471
+ if (isAgentToolResult(result))
472
+ return result;
473
+ const text = typeof result === "string" ? result : (JSON.stringify(result) ?? String(result));
474
+ return {
475
+ content: [{ type: "text", text }],
476
+ details: result,
477
+ };
478
+ },
479
+ };
480
+ pi.registerTool(definition);
481
+ registered.add(name);
482
+ available.register(tool);
483
+ }
484
+ }
485
+ function isAgentToolResult(value) {
486
+ return isRecord(value) && Array.isArray(value.content);
487
+ }
@@ -0,0 +1,97 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { AgentRunRequest, AgentInstance, Mechanism, MechanismContext, MechanismDecisionLog, MechanismExec, MechanismEvents, MechanismFailurePolicy, MechanismScope } from "../type.js";
3
+ import type { NodeScopeDescriptor } from "../runtime.js";
4
+ export interface MechanismCleanupError {
5
+ mechanismName: string;
6
+ error: unknown;
7
+ }
8
+ export type MechanismFailurePhase = "createState" | "onNodeEnter" | "onNodeExit" | "onNodeError" | "beforeAgentRun" | "onTurnStart" | "onTurnEnd" | "onToolStart" | "onToolResult" | "beforeToolCall" | "afterToolResult" | "validateCompletion" | "tool_result" | "turn_start" | "turn_end";
9
+ export interface MechanismFailureRecord {
10
+ mechanismName: string;
11
+ phase: MechanismFailurePhase;
12
+ policy: MechanismFailurePolicy;
13
+ error: unknown;
14
+ reason: string;
15
+ scopeId: string;
16
+ }
17
+ export interface MechanismStateResolution {
18
+ state: unknown;
19
+ initializationFailed: boolean;
20
+ initializationError?: unknown;
21
+ }
22
+ export interface MechanismHookInvocation {
23
+ mechanism: Mechanism<any>;
24
+ context: MechanismContext<any>;
25
+ initializationFailure?: unknown;
26
+ }
27
+ export interface MechanismRunStartResult {
28
+ blocked: boolean;
29
+ reason?: string;
30
+ }
31
+ export interface MechanismRuntimeOptions {
32
+ execRoot?: string;
33
+ execTimeoutMs?: number;
34
+ execMaxOutputBytes?: number;
35
+ allowExecOutsideRoot?: boolean;
36
+ eventMaxBytes?: number;
37
+ completionValidationTimeoutMs?: number;
38
+ }
39
+ export type MechanismCompletionGateResult = {
40
+ action: "allow";
41
+ verifiedResult?: Readonly<{
42
+ checks: readonly import("../type.js").MechanismVerifiedResultEntry[];
43
+ }>;
44
+ } | {
45
+ action: "reject" | "fail-node" | "fail-graph";
46
+ reason: string;
47
+ };
48
+ /**
49
+ * mechanism state 的唯一所有者:每个 AgentInstance、每个 mechanism 对象一份。
50
+ * WeakMap 不延长 instance 或 mechanism definition 的生命周期。
51
+ */
52
+ export declare class MechanismStateStore {
53
+ private readonly states;
54
+ resolve(instance: AgentInstance, mechanism: Mechanism): MechanismStateResolution;
55
+ }
56
+ /** 同一 node visit 中全部 mechanism invocation 的所有者。 */
57
+ export declare class MechanismInvocationGroup {
58
+ private readonly descriptor;
59
+ private readonly runtimeScopeIsCurrent;
60
+ private readonly invocations;
61
+ private closed;
62
+ constructor(descriptor: NodeScopeDescriptor, runtimeScopeIsCurrent: () => boolean);
63
+ createScope(mechanismName: string): MechanismScope;
64
+ close(): Promise<MechanismCleanupError[]>;
65
+ }
66
+ /**
67
+ * pi 每类事件只注册一个底层 handler;node visit 内的订阅由 scope 托管。
68
+ * handler 控制性失败先记录,随后由图循环在安全检查点消费。
69
+ */
70
+ export declare class MechanismEventBroker {
71
+ private readonly reportFailure;
72
+ private readonly subscribers;
73
+ private readonly pendingFailures;
74
+ private readonly decisionTraces;
75
+ private activeRun;
76
+ private readonly pi;
77
+ private readonly options;
78
+ constructor(pi: ExtensionAPI, reportFailure: (failure: MechanismFailureRecord) => void, options?: MechanismRuntimeOptions);
79
+ createExec(scope: MechanismScope): MechanismExec;
80
+ createDecisionLog(scope: MechanismScope): MechanismDecisionLog;
81
+ beginAgentRun(agentRunId: number, request: AgentRunRequest, invocations: readonly MechanismHookInvocation[]): Promise<MechanismRunStartResult>;
82
+ endAgentRun(agentRunId: number): void;
83
+ validateCompletion(agentRunId: number, completion: import("../type.js").NodeCompletion): Promise<MechanismCompletionGateResult>;
84
+ createEvents(mechanismName: string, policy: MechanismFailurePolicy, scope: MechanismScope): MechanismEvents;
85
+ consumeControlFailures(scopeId: string): MechanismFailureRecord[];
86
+ private handleToolCall;
87
+ private handleToolResult;
88
+ private createToolResultView;
89
+ private invokeObservationHook;
90
+ private validateToolInput;
91
+ private recordHookFailure;
92
+ private recordCompletionDecisionFailure;
93
+ private recordDecision;
94
+ private subscribe;
95
+ private removeSubscriber;
96
+ private dispatch;
97
+ }