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,22 @@
1
+ import type { InvocationLimits } from "../core/limits.js";
2
+ export interface InvocationBudgetUsage {
3
+ readonly graphInvocations: number;
4
+ readonly nodeVisits: number;
5
+ readonly maxDepthReached: number;
6
+ }
7
+ export declare class InvocationBudgetExceededError extends Error {
8
+ readonly kind: "graph-depth" | "graph-invocations" | "node-visits";
9
+ constructor(kind: "graph-depth" | "graph-invocations" | "node-visits", message: string);
10
+ }
11
+ export declare class InvocationBudget {
12
+ readonly limits: InvocationLimits;
13
+ private graphInvocations;
14
+ private nodeVisits;
15
+ private maxDepthReached;
16
+ constructor(limits: InvocationLimits);
17
+ enterGraph(depth: number): void;
18
+ enterNode(): void;
19
+ get usage(): InvocationBudgetUsage;
20
+ /** Restore budget position from a saved checkpoint. New usage must be at least as high. */
21
+ restore(usage: InvocationBudgetUsage): void;
22
+ }
@@ -0,0 +1,52 @@
1
+ export class InvocationBudgetExceededError extends Error {
2
+ kind;
3
+ constructor(kind, message) {
4
+ super(message);
5
+ this.kind = kind;
6
+ this.name = "InvocationBudgetExceededError";
7
+ }
8
+ }
9
+ export class InvocationBudget {
10
+ limits;
11
+ graphInvocations = 0;
12
+ nodeVisits = 0;
13
+ maxDepthReached = 0;
14
+ constructor(limits) {
15
+ this.limits = limits;
16
+ }
17
+ enterGraph(depth) {
18
+ if (depth > this.limits.maxGraphDepth) {
19
+ throw new InvocationBudgetExceededError("graph-depth", `Graph depth ${depth} exceeds maxGraphDepth ${this.limits.maxGraphDepth}`);
20
+ }
21
+ if (this.graphInvocations + 1 > this.limits.maxGraphInvocations) {
22
+ throw new InvocationBudgetExceededError("graph-invocations", `Graph invocation count exceeds maxGraphInvocations ${this.limits.maxGraphInvocations}`);
23
+ }
24
+ this.graphInvocations += 1;
25
+ this.maxDepthReached = Math.max(this.maxDepthReached, depth);
26
+ }
27
+ enterNode() {
28
+ if (this.nodeVisits + 1 > this.limits.maxTotalNodeVisits) {
29
+ throw new InvocationBudgetExceededError("node-visits", `Node visit count exceeds maxTotalNodeVisits ${this.limits.maxTotalNodeVisits}`);
30
+ }
31
+ this.nodeVisits += 1;
32
+ }
33
+ get usage() {
34
+ return Object.freeze({
35
+ graphInvocations: this.graphInvocations,
36
+ nodeVisits: this.nodeVisits,
37
+ maxDepthReached: this.maxDepthReached,
38
+ });
39
+ }
40
+ /** Restore budget position from a saved checkpoint. New usage must be at least as high. */
41
+ restore(usage) {
42
+ if (usage.graphInvocations < 0 || usage.nodeVisits < 0 || usage.maxDepthReached < 0) {
43
+ throw new Error("Invalid budget usage");
44
+ }
45
+ if (usage.maxDepthReached > this.limits.maxGraphDepth) {
46
+ throw new InvocationBudgetExceededError("graph-depth", `Checkpoint depth ${usage.maxDepthReached} exceeds maxGraphDepth ${this.limits.maxGraphDepth}`);
47
+ }
48
+ this.graphInvocations = Math.max(this.graphInvocations, usage.graphInvocations);
49
+ this.nodeVisits = Math.max(this.nodeVisits, usage.nodeVisits);
50
+ this.maxDepthReached = Math.max(this.maxDepthReached, usage.maxDepthReached);
51
+ }
52
+ }
@@ -0,0 +1,92 @@
1
+ import type { ContextContribution } from "../core/context.js";
2
+ import { type JsonValue } from "../core/json.js";
3
+ import type { Mechanism, MechanismCompletionDecision, MechanismContext, MechanismDecisionTrace, MechanismFailurePolicy, MechanismHookName, MechanismInstallation, MechanismScope } from "../core/mechanism.js";
4
+ export interface MechanismRuntimeOptions {
5
+ readonly hookTimeoutMs?: number;
6
+ readonly execRoot?: string;
7
+ readonly execTimeoutMs?: number;
8
+ readonly execMaxOutputBytes?: number;
9
+ readonly allowExecOutsideRoot?: boolean;
10
+ readonly pi?: unknown;
11
+ }
12
+ export interface MechanismFailureRecord {
13
+ readonly mechanismName: string;
14
+ readonly installation: MechanismInstallation;
15
+ readonly hook: MechanismHookName | "createState" | "cleanup";
16
+ readonly policy: MechanismFailurePolicy;
17
+ readonly message: string;
18
+ readonly error: unknown;
19
+ readonly scopeId: string;
20
+ }
21
+ export declare class MechanismRuntimeError extends Error {
22
+ readonly failure: MechanismFailureRecord;
23
+ constructor(failure: MechanismFailureRecord);
24
+ }
25
+ interface Invocation {
26
+ readonly definition: Mechanism;
27
+ readonly installation: MechanismInstallation;
28
+ readonly context: MechanismContext;
29
+ readonly scope: ManagedScope;
30
+ }
31
+ export interface MechanismChain {
32
+ readonly invocations: readonly Invocation[];
33
+ }
34
+ export declare class MechanismRuntime {
35
+ private readonly warn?;
36
+ private readonly options;
37
+ private readonly activeNames;
38
+ private readonly decisions;
39
+ private readonly failures;
40
+ private unmanagedWarningEmitted;
41
+ private readonly contributions;
42
+ private agentRunHandles;
43
+ private naturalLifetime;
44
+ constructor(options?: MechanismRuntimeOptions, warn?: ((message: string) => void) | undefined);
45
+ get decisionTrace(): readonly MechanismDecisionTrace[];
46
+ get failureTrace(): readonly MechanismFailureRecord[];
47
+ get contextContributions(): readonly ContextContribution[];
48
+ open(installation: MechanismInstallation, scopeId: string, definitions: readonly Mechanism[], identity: {
49
+ rootRunId: string;
50
+ graphInvocationId?: string;
51
+ nodeVisitId?: string;
52
+ stageId?: string;
53
+ }, _contextState?: import("../core/context.js").ContextState): Promise<MechanismChain>;
54
+ beforeAgentRun(chains: readonly MechanismChain[], agentRunId: string, prompt: string): Promise<void>;
55
+ afterAgentRun(chains: readonly MechanismChain[], agentRunId: string): Promise<void>;
56
+ enter(chains: readonly MechanismChain[], hookName: "onRootEnter" | "onGraphEnter" | "onNodeEnter"): Promise<void>;
57
+ validateCompletion(chains: readonly MechanismChain[], agentRunId: string, completion: JsonValue): Promise<MechanismCompletionDecision>;
58
+ nodeExit(chains: readonly MechanismChain[], completion: JsonValue): Promise<void>;
59
+ nodeError(chains: readonly MechanismChain[], error: unknown): Promise<void>;
60
+ graphExit(chains: readonly MechanismChain[], error?: unknown): Promise<void>;
61
+ rootExit(chain: MechanismChain): Promise<void>;
62
+ close(chain: MechanismChain): Promise<void>;
63
+ /** Yield JSON-compatible snapshots for all mechanisms that implement snapshot. */
64
+ snapshotAll(chains: readonly MechanismChain[]): readonly {
65
+ readonly name: string;
66
+ readonly snapshot: JsonValue;
67
+ }[];
68
+ /** Restore mechanism state from a checkpoint. A declared restore hook is fail-closed. */
69
+ restoreState(chains: readonly MechanismChain[], saved: readonly {
70
+ readonly name: string;
71
+ readonly snapshot: JsonValue;
72
+ }[]): void;
73
+ private observe;
74
+ private observeInvocation;
75
+ private control;
76
+ private createContext;
77
+ private exec;
78
+ private validateDuplicates;
79
+ private record;
80
+ private error;
81
+ private failure;
82
+ }
83
+ declare class ManagedScope {
84
+ readonly scopeId: string;
85
+ private active;
86
+ private readonly controller;
87
+ private readonly cleanups;
88
+ readonly view: MechanismScope;
89
+ constructor(scopeId: string, installation: MechanismInstallation);
90
+ close(): Promise<unknown[]>;
91
+ }
92
+ export {};
@@ -0,0 +1,387 @@
1
+ import { execFile } from "node:child_process";
2
+ import * as path from "node:path";
3
+ import { promisify } from "node:util";
4
+ import { isJsonValue } from "../core/json.js";
5
+ const execFileAsync = promisify(execFile);
6
+ export class MechanismRuntimeError extends Error {
7
+ failure;
8
+ constructor(failure) {
9
+ super(failure.message);
10
+ this.failure = failure;
11
+ this.name = "MechanismRuntimeError";
12
+ }
13
+ }
14
+ export class MechanismRuntime {
15
+ warn;
16
+ options;
17
+ activeNames = [];
18
+ decisions = [];
19
+ failures = [];
20
+ unmanagedWarningEmitted = false;
21
+ contributions = new Map();
22
+ agentRunHandles = null;
23
+ naturalLifetime;
24
+ constructor(options = {}, warn) {
25
+ this.warn = warn;
26
+ this.options = {
27
+ hookTimeoutMs: options.hookTimeoutMs ?? 30_000,
28
+ execRoot: path.resolve(options.execRoot ?? process.cwd()),
29
+ execTimeoutMs: options.execTimeoutMs ?? 30_000,
30
+ execMaxOutputBytes: options.execMaxOutputBytes ?? 64 * 1024,
31
+ allowExecOutsideRoot: options.allowExecOutsideRoot ?? false,
32
+ pi: options.pi,
33
+ };
34
+ }
35
+ get decisionTrace() { return Object.freeze([...this.decisions]); }
36
+ get failureTrace() { return Object.freeze([...this.failures]); }
37
+ get contextContributions() { return Object.freeze([...this.contributions.values()]); }
38
+ async open(installation, scopeId, definitions, identity, _contextState) {
39
+ this.validateDuplicates(definitions);
40
+ const invocations = [];
41
+ try {
42
+ for (const definition of definitions) {
43
+ if (!definition.allowMultiple && this.activeNames.includes(definition.name)) {
44
+ throw this.error(definition, installation, "createState", scopeId, new Error(`Mechanism is already installed in the active chain: ${definition.name}`));
45
+ }
46
+ let state;
47
+ try {
48
+ state = definition.createState?.() ?? {};
49
+ if (!isJsonValue(state))
50
+ throw new Error("Mechanism state must be JSON-compatible");
51
+ }
52
+ catch (error) {
53
+ throw this.error(definition, installation, "createState", scopeId, error);
54
+ }
55
+ const scope = new ManagedScope(scopeId, installation);
56
+ const context = this.createContext(definition, installation, identity, state, scope);
57
+ invocations.push({ definition, installation, context, scope });
58
+ this.activeNames.push(definition.name);
59
+ }
60
+ return Object.freeze({ invocations: Object.freeze(invocations) });
61
+ }
62
+ catch (error) {
63
+ await this.close(Object.freeze({ invocations: Object.freeze(invocations) }));
64
+ throw error;
65
+ }
66
+ }
67
+ async beforeAgentRun(chains, agentRunId, prompt) {
68
+ this.agentRunHandles = [];
69
+ for (const invocation of flatten(chains)) {
70
+ const hook = invocation.definition.beforeAgentRun;
71
+ if (!hook)
72
+ continue;
73
+ this.naturalLifetime = "agent-run";
74
+ try {
75
+ await this.control(invocation, "beforeAgentRun", () => hook(Object.freeze({ ...invocation.context, agentRunId, prompt })));
76
+ }
77
+ finally {
78
+ this.naturalLifetime = undefined;
79
+ }
80
+ }
81
+ }
82
+ async afterAgentRun(chains, agentRunId) {
83
+ for (const invocation of [...flatten(chains)].reverse()) {
84
+ const hook = invocation.definition.afterAgentRun;
85
+ if (!hook)
86
+ continue;
87
+ await this.observeInvocation(invocation, "afterAgentRun", () => hook(Object.freeze({ ...invocation.context, agentRunId })));
88
+ }
89
+ for (const handle of this.agentRunHandles ?? [])
90
+ handle.dispose();
91
+ this.agentRunHandles = null;
92
+ }
93
+ async enter(chains, hookName) {
94
+ for (const invocation of flatten(chains)) {
95
+ const hook = invocation.definition[hookName];
96
+ if (hook)
97
+ await this.observeInvocation(invocation, hookName, () => hook(invocation.context));
98
+ }
99
+ }
100
+ async validateCompletion(chains, agentRunId, completion) {
101
+ for (const invocation of [...flatten(chains)].reverse()) {
102
+ const hook = invocation.definition.validateCompletion;
103
+ if (!hook)
104
+ continue;
105
+ const decision = await this.control(invocation, "validateCompletion", () => hook(Object.freeze({ ...invocation.context, agentRunId, completion })));
106
+ this.decisions.push(Object.freeze({
107
+ mechanismName: invocation.definition.name,
108
+ hook: "validateCompletion",
109
+ decision: decision.action,
110
+ reason: "reason" in decision ? decision.reason : undefined,
111
+ timestamp: Date.now(),
112
+ }));
113
+ if (decision.action !== "allow")
114
+ return decision;
115
+ }
116
+ return { action: "allow" };
117
+ }
118
+ async nodeExit(chains, completion) {
119
+ for (const invocation of [...flatten(chains)].reverse()) {
120
+ const hook = invocation.definition.onNodeExit;
121
+ if (hook)
122
+ await this.observeInvocation(invocation, "onNodeExit", () => hook(Object.freeze({ ...invocation.context, completion })));
123
+ }
124
+ }
125
+ async nodeError(chains, error) {
126
+ for (const invocation of [...flatten(chains)].reverse()) {
127
+ const hook = invocation.definition.onNodeError;
128
+ if (hook)
129
+ await this.observeInvocation(invocation, "onNodeError", () => hook(Object.freeze({ ...invocation.context, error })));
130
+ }
131
+ }
132
+ async graphExit(chains, error) {
133
+ for (const invocation of [...flatten(chains)].reverse()) {
134
+ const hook = error === undefined ? invocation.definition.onGraphExit : invocation.definition.onGraphError;
135
+ const name = error === undefined ? "onGraphExit" : "onGraphError";
136
+ if (hook)
137
+ await this.observeInvocation(invocation, name, () => hook(Object.freeze({ ...invocation.context, ...(error === undefined ? {} : { error }) })));
138
+ }
139
+ }
140
+ async rootExit(chain) {
141
+ for (const invocation of [...chain.invocations].reverse()) {
142
+ const hook = invocation.definition.onRootExit;
143
+ if (hook)
144
+ await this.observeInvocation(invocation, "onRootExit", () => hook(invocation.context));
145
+ }
146
+ }
147
+ async close(chain) {
148
+ for (const invocation of [...chain.invocations].reverse()) {
149
+ const errors = await invocation.scope.close();
150
+ for (const error of errors)
151
+ this.record(invocation, "cleanup", error);
152
+ const index = this.activeNames.lastIndexOf(invocation.definition.name);
153
+ if (index >= 0)
154
+ this.activeNames.splice(index, 1);
155
+ }
156
+ }
157
+ /** Yield JSON-compatible snapshots for all mechanisms that implement snapshot. */
158
+ snapshotAll(chains) {
159
+ const snapshots = [];
160
+ for (const invocation of flatten(chains)) {
161
+ if (invocation.definition.snapshot) {
162
+ try {
163
+ const snapshot = invocation.definition.snapshot(invocation.context.state);
164
+ if (!isJsonValue(snapshot))
165
+ throw new Error("Mechanism snapshot must be JSON-compatible");
166
+ snapshots.push({ name: invocation.definition.name, snapshot });
167
+ }
168
+ catch (error) {
169
+ // Observation: snapshots are best-effort; failure does not alter control flow.
170
+ const message = `Mechanism "${invocation.definition.name}" snapshot failed: ${error instanceof Error ? error.message : String(error)}`;
171
+ this.warn?.(message);
172
+ }
173
+ }
174
+ }
175
+ return Object.freeze(snapshots);
176
+ }
177
+ /** Restore mechanism state from a checkpoint. A declared restore hook is fail-closed. */
178
+ restoreState(chains, saved) {
179
+ const snapshotMap = new Map(saved.map((s) => [s.name, s.snapshot]));
180
+ for (const invocation of flatten(chains)) {
181
+ if (!invocation.definition.restore)
182
+ continue;
183
+ const snapshot = snapshotMap.get(invocation.definition.name);
184
+ if (snapshot === undefined)
185
+ continue;
186
+ try {
187
+ const restored = invocation.definition.restore(snapshot);
188
+ if (!isJsonValue(restored))
189
+ throw new Error("Restored mechanism state must be JSON-compatible");
190
+ invocation.context.state = restored;
191
+ }
192
+ catch (error) {
193
+ throw this.error(invocation.definition, invocation.installation, "createState", invocation.scope.scopeId, new Error(`checkpoint restore failed: ${error instanceof Error ? error.message : String(error)}`));
194
+ }
195
+ }
196
+ }
197
+ async observe(chain, hookName) {
198
+ for (const invocation of chain.invocations) {
199
+ const hook = invocation.definition[hookName];
200
+ if (hook)
201
+ await this.observeInvocation(invocation, hookName, () => hook(invocation.context));
202
+ }
203
+ }
204
+ async observeInvocation(invocation, hook, run) {
205
+ try {
206
+ await withTimeout(Promise.resolve(run()), this.options.hookTimeoutMs, hook);
207
+ }
208
+ catch (error) {
209
+ this.record(invocation, hook, error);
210
+ }
211
+ }
212
+ async control(invocation, hook, run) {
213
+ try {
214
+ return await withTimeout(Promise.resolve(run()), this.options.hookTimeoutMs, hook);
215
+ }
216
+ catch (error) {
217
+ throw this.error(invocation.definition, invocation.installation, hook, invocation.scope.scopeId, error);
218
+ }
219
+ }
220
+ createContext(definition, installation, identity, state, scope) {
221
+ const maxLifetime = installation === "host" ? "root-run" : installation === "graph" ? "graph-invocation" : "node-visit";
222
+ const defaultLifetime = installation === "host" ? "root-run" : installation === "graph" ? "graph-invocation" : "node-visit";
223
+ const context = {
224
+ add: (id, content, options = {}) => {
225
+ const lifetime = options.lifetime ?? this.naturalLifetime ?? defaultLifetime;
226
+ if (!lifetimeAllowed(lifetime, maxLifetime))
227
+ throw new Error(`${installation} Mechanism cannot create ${lifetime} contribution`);
228
+ const contributionId = `${scope.scopeId}:${definition.name}:${id}`;
229
+ if (this.contributions.has(contributionId))
230
+ throw new Error(`Context contribution already exists: ${contributionId}`);
231
+ const contribution = Object.freeze({
232
+ id: contributionId,
233
+ owner: installation,
234
+ scopeId: scope.scopeId,
235
+ lifetime,
236
+ retention: options.retention ?? "sticky",
237
+ content,
238
+ });
239
+ this.contributions.set(contributionId, contribution);
240
+ let active = true;
241
+ const handle = Object.freeze({
242
+ id: contributionId,
243
+ update: (next) => {
244
+ if (!active)
245
+ throw new Error(`Context contribution is disposed: ${contributionId}`);
246
+ this.contributions.set(contributionId, Object.freeze({ ...contribution, content: next }));
247
+ },
248
+ dispose: () => {
249
+ if (!active)
250
+ return;
251
+ active = false;
252
+ this.contributions.delete(contributionId);
253
+ },
254
+ });
255
+ scope.view.onCleanup(() => handle.dispose());
256
+ if (lifetime === "agent-run")
257
+ this.agentRunHandles?.push(handle);
258
+ return handle;
259
+ },
260
+ };
261
+ const result = {
262
+ ...identity,
263
+ state,
264
+ scope: scope.view,
265
+ context: Object.freeze(context),
266
+ exec: Object.freeze({ run: (file, args, options) => this.exec(file, args, options) }),
267
+ };
268
+ if (this.options.pi !== undefined)
269
+ Object.defineProperty(result, "pi", {
270
+ enumerable: true,
271
+ get: () => {
272
+ if (!this.unmanagedWarningEmitted) {
273
+ this.unmanagedWarningEmitted = true;
274
+ this.warn?.("Mechanism accessed unmanaged ctx.pi; scope, replay, and cleanup guarantees do not apply");
275
+ }
276
+ return this.options.pi;
277
+ },
278
+ });
279
+ return Object.freeze(result);
280
+ }
281
+ async exec(file, args = [], options = {}) {
282
+ const cwd = path.resolve(options.cwd ?? this.options.execRoot);
283
+ if (!this.options.allowExecOutsideRoot && !within(this.options.execRoot, cwd))
284
+ throw new Error("Mechanism exec cwd is outside execRoot");
285
+ try {
286
+ const result = await execFileAsync(file, [...args], { cwd, timeout: options.timeoutMs ?? this.options.execTimeoutMs, maxBuffer: this.options.execMaxOutputBytes * 2 });
287
+ return output(0, result.stdout, result.stderr, this.options.execMaxOutputBytes);
288
+ }
289
+ catch (error) {
290
+ const value = error;
291
+ return output(typeof value.code === "number" ? value.code : 1, value.stdout ?? "", value.stderr ?? String(error), this.options.execMaxOutputBytes);
292
+ }
293
+ }
294
+ validateDuplicates(definitions) {
295
+ const names = new Set();
296
+ for (const definition of definitions) {
297
+ if (!definition.name.trim())
298
+ throw new Error("Mechanism name is required");
299
+ if (!definition.allowMultiple && names.has(definition.name))
300
+ throw new Error(`Duplicate Mechanism installation: ${definition.name}`);
301
+ names.add(definition.name);
302
+ }
303
+ }
304
+ record(invocation, hook, error) {
305
+ const failure = this.failure(invocation.definition, invocation.installation, hook, invocation.scope.scopeId, error);
306
+ this.failures.push(failure);
307
+ return failure;
308
+ }
309
+ error(definition, installation, hook, scopeId, error) {
310
+ const failure = this.failure(definition, installation, hook, scopeId, error);
311
+ this.failures.push(failure);
312
+ return new MechanismRuntimeError(failure);
313
+ }
314
+ failure(definition, installation, hook, scopeId, error) {
315
+ return Object.freeze({
316
+ mechanismName: definition.name,
317
+ installation,
318
+ hook,
319
+ policy: definition.failurePolicy ?? (installation === "node" ? "fail-node" : "fail-graph"),
320
+ message: `Mechanism "${definition.name}" ${hook} failed: ${error instanceof Error ? error.message : String(error)}`,
321
+ error,
322
+ scopeId,
323
+ });
324
+ }
325
+ }
326
+ class ManagedScope {
327
+ scopeId;
328
+ active = true;
329
+ controller = new AbortController();
330
+ cleanups = [];
331
+ view;
332
+ constructor(scopeId, installation) {
333
+ this.scopeId = scopeId;
334
+ this.view = Object.freeze({
335
+ scopeId, installation, signal: this.controller.signal,
336
+ isActive: () => this.active,
337
+ onCleanup: (cleanup) => {
338
+ if (!this.active)
339
+ throw new Error("Mechanism scope is closed");
340
+ this.cleanups.push(cleanup);
341
+ },
342
+ });
343
+ }
344
+ async close() {
345
+ if (!this.active)
346
+ return [];
347
+ this.active = false;
348
+ this.controller.abort();
349
+ const errors = [];
350
+ for (const cleanup of [...this.cleanups].reverse())
351
+ try {
352
+ await cleanup();
353
+ }
354
+ catch (error) {
355
+ errors.push(error);
356
+ }
357
+ return errors;
358
+ }
359
+ }
360
+ function flatten(chains) { return chains.flatMap((chain) => [...chain.invocations]); }
361
+ function lifetimeAllowed(requested, maximum) {
362
+ const order = ["agent-run", "node-visit", "graph-invocation", "root-run"];
363
+ return order.indexOf(requested) <= order.indexOf(maximum);
364
+ }
365
+ function within(root, candidate) {
366
+ const relative = path.relative(root, candidate);
367
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
368
+ }
369
+ function output(exitCode, stdout, stderr, max) {
370
+ const truncate = (value) => {
371
+ const buffer = Buffer.from(value, "utf8");
372
+ return buffer.length <= max ? { value, truncated: false } : { value: buffer.subarray(0, max).toString("utf8"), truncated: true };
373
+ };
374
+ const out = truncate(stdout);
375
+ const err = truncate(stderr);
376
+ return Object.freeze({ exitCode, stdout: out.value, stderr: err.value, truncated: out.truncated || err.truncated });
377
+ }
378
+ async function withTimeout(promise, timeoutMs, hook) {
379
+ let timer;
380
+ try {
381
+ return await Promise.race([promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`${hook} timed out after ${timeoutMs}ms`)), timeoutMs); })]);
382
+ }
383
+ finally {
384
+ if (timer)
385
+ clearTimeout(timer);
386
+ }
387
+ }
@@ -0,0 +1,91 @@
1
+ import type { AgentInstance, ContextFrame, Graph, Mechanism, Node, NodeCompletion, NodeInput } from "./type.js";
2
+ export interface CallFrame {
3
+ instance: AgentInstance;
4
+ graph: Graph;
5
+ /** 图调用的上下文边界。 */
6
+ boundary: "root" | "call" | "compose";
7
+ /** compose 复用父 Instance,但 child Graph 的目标/机制只在此调用帧生效。 */
8
+ localGoal: string;
9
+ localMechanisms: readonly Mechanism[];
10
+ callBackground: Record<string, unknown>;
11
+ parentNodeId?: string;
12
+ /** 每个调用帧独立计数,避免同名子图节点污染 visit。 */
13
+ nodeVisits: Map<string, number>;
14
+ currentNodeId: string | null;
15
+ /**
16
+ * 节点瞬态状态也归属调用帧。子图返回时必须恢复仍在执行的父 graph node,
17
+ * 否则 context 投影会错误地认为父节点已经结束。
18
+ */
19
+ activeNode: Node | null;
20
+ activeInput: NodeInput | null;
21
+ activeScope: NodeScopeDescriptor | null;
22
+ isNodeActive: boolean;
23
+ /** 已被最近一次 pi compaction 原生上下文取代的 frame 前缀长度。 */
24
+ projectedFrameBase: number;
25
+ }
26
+ /** compose 调用在父 frames 上建立的受 Runtime 管理的临时区间。 */
27
+ export interface FrameSegmentScope {
28
+ id: string;
29
+ graphId: string;
30
+ parentNodeId: string;
31
+ instanceId: string;
32
+ baseIndex: number;
33
+ depth: number;
34
+ }
35
+ export interface NodeScopeDescriptor {
36
+ protocol: 2;
37
+ graphRunId: string;
38
+ instanceId: string;
39
+ scopeId: string;
40
+ graphId: string;
41
+ nodeId: string;
42
+ visit: number;
43
+ depth: number;
44
+ }
45
+ export declare class GraphRuntime {
46
+ callStack: CallFrame[];
47
+ isNodeActive: boolean;
48
+ /** 当前节点的语义作用域。details 用于匹配,不依赖消息正文。 */
49
+ currentScope: NodeScopeDescriptor | null;
50
+ currentNode: Node | null;
51
+ currentInput: NodeInput | null;
52
+ readonly graphRunId: `${string}-${string}-${string}-${string}-${string}`;
53
+ /** 当前 graph run 已发生的 compaction 次数,仅用于诊断和 checkpoint 观测。 */
54
+ compactionGeneration: number;
55
+ /**
56
+ * 共享 call/compose 活跃期间异常收到 session_compact 时设为 true。
57
+ * 此后本 session 投影中将持续过滤 compactionSummary,优先保证不泄漏。
58
+ */
59
+ compactionBoundaryViolated: boolean;
60
+ /** Runtime 控制平面的 frame → NodeScope 对齐表,不进入开发者 frame/LLM。 */
61
+ private readonly frameScopes;
62
+ get top(): CallFrame | null;
63
+ get topInstance(): AgentInstance | null;
64
+ get topGraph(): Graph | null;
65
+ get currentNodeId(): string | null;
66
+ pushGraph(graph: Graph, background: Record<string, unknown>, boundary?: CallFrame["boundary"], sharedInstance?: AgentInstance, parentNodeId?: string): AgentInstance;
67
+ popGraph(): CallFrame | undefined;
68
+ beginFrameSegment(graphId: string, parentNodeId: string): FrameSegmentScope;
69
+ readFrameSegment(scope: FrameSegmentScope): readonly ContextFrame[];
70
+ rollbackFrameSegment(scope: FrameSegmentScope): void;
71
+ closeFrameSegment(scope: FrameSegmentScope, completion: NodeCompletion): NodeCompletion;
72
+ nextScope(nodeId: string): NodeScopeDescriptor;
73
+ enterNode(nodeId: string, scope: NodeScopeDescriptor, input: NodeInput): Node;
74
+ exitNode(frame: ContextFrame): void;
75
+ /**
76
+ * 记录一次 session compaction。NodeScope 的身份(scopeId)不变。
77
+ * Runtime 只推进 projectedFrameBase;pi 原生 summary 与 recent messages
78
+ * 是压缩历史的权威替代,SDK 不重发 scope,也不遮挡 summary。
79
+ */
80
+ recordCompaction(projectedFrameBase?: number): number;
81
+ /** 当前 callStack 是否存在嵌套 call/compose(非 root-only)。 */
82
+ get hasActiveSharedCall(): boolean;
83
+ /** 共享调用边界被 compaction 切断后,继续运行会泄漏无法归属的 transcript。 */
84
+ assertNoCompactionBoundaryViolation(): void;
85
+ get completedFrameScopes(): readonly NodeScopeDescriptor[];
86
+ /** 只返回最近一次 compaction 后新生长、仍需单独投影的开发者 frames。 */
87
+ get projectedFrames(): ContextFrame[];
88
+ reset(): void;
89
+ private assertSegmentOwner;
90
+ private restoreActiveNodeFromTop;
91
+ }