@mono-agent/agent-runtime 0.19.1 → 0.20.1

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 (51) hide show
  1. package/MIGRATION.md +1 -1
  2. package/README.md +101 -4
  3. package/package.json +1 -1
  4. package/src/agent/sandbox-seam.js +16 -2
  5. package/src/agent/tools/bash.js +26 -4
  6. package/src/agent/tools/edit.js +72 -5
  7. package/src/agent/tools/exec.js +22 -4
  8. package/src/agent/tools/glob.js +65 -9
  9. package/src/agent/tools/grep.js +66 -11
  10. package/src/agent/tools/node-repl.js +5 -2
  11. package/src/agent/tools/pi-bridge.js +263 -48
  12. package/src/agent/tools/read.js +50 -10
  13. package/src/agent/tools/shared/path-resolver.js +67 -2
  14. package/src/agent/tools/shared/process-jobs.js +188 -0
  15. package/src/agent/tools/shared/process-runner.js +541 -30
  16. package/src/agent/tools/shared/protected-filesystem.js +150 -0
  17. package/src/agent/tools/web-search.js +63 -8
  18. package/src/agent/tools/write.js +52 -6
  19. package/src/ai/providers/acp.js +4 -0
  20. package/src/ai/providers/claude-cli.js +35 -2
  21. package/src/ai/providers/claude-sdk.js +12 -0
  22. package/src/ai/providers/codex-app.js +15 -2
  23. package/src/ai/providers/pi-native/stream-subscriber.js +29 -2
  24. package/src/ai/providers/pi-native/turn-runner.js +3 -0
  25. package/src/ai/providers/pi-native.js +7 -1
  26. package/src/ai/runtime/capabilities.js +2 -0
  27. package/src/ai/runtime/router.js +78 -6
  28. package/src/ai/streaming/codex-events.js +15 -0
  29. package/src/ai/streaming/opencode-events.js +5 -0
  30. package/src/ai/tool-lifecycle.js +347 -0
  31. package/src/ai/types.js +58 -0
  32. package/src/runtime.js +35 -21
  33. package/types/agent/sandbox-seam.d.ts +19 -6
  34. package/types/agent/tools/bash.d.ts +11 -26
  35. package/types/agent/tools/edit.d.ts +3 -2
  36. package/types/agent/tools/exec.d.ts +13 -26
  37. package/types/agent/tools/glob.d.ts +3 -2
  38. package/types/agent/tools/grep.d.ts +3 -2
  39. package/types/agent/tools/pi-bridge.d.ts +11 -4
  40. package/types/agent/tools/read.d.ts +3 -2
  41. package/types/agent/tools/shared/path-resolver.d.ts +8 -0
  42. package/types/agent/tools/shared/process-jobs.d.ts +64 -0
  43. package/types/agent/tools/shared/process-runner.d.ts +45 -3
  44. package/types/agent/tools/shared/protected-filesystem.d.ts +51 -0
  45. package/types/agent/tools/write.d.ts +3 -2
  46. package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -0
  47. package/types/ai/runtime/capabilities.d.ts +3 -0
  48. package/types/ai/streaming/codex-events.d.ts +1 -0
  49. package/types/ai/streaming/opencode-events.d.ts +1 -0
  50. package/types/ai/tool-lifecycle.d.ts +43 -0
  51. package/types/ai/types.d.ts +118 -0
@@ -100,10 +100,19 @@ const RESOLVER_PROTECTED_OPTION_KEYS = new Set([
100
100
  "model", "executionMode", "effort", "messages", "abortSignal", "onEvent",
101
101
  "sessionId", "providerSessionId", "sessionKeepAlive", "sessionIdleTimeoutMs",
102
102
  "diagnosticsSeed", "systemPromptPrefix", "sandboxPolicy", "sandboxEngine", "sandbox",
103
- "allowedTools", "disallowedTools", "permissionMode", "mcpServers", "skills",
103
+ "allowedTools", "disallowedTools", "permissionMode", "mcpServers", "mcpApps", "skills",
104
104
  "outputSchema", "nativeSubagents", "liveInput", "fastMode", "toolEnvironment",
105
+ "codexSandboxNetworkAccess",
105
106
  ]);
106
107
 
108
+ class ResolverProtectedOptionError extends Error {
109
+ /** @param {string} key */
110
+ constructor(key) {
111
+ super(`route attempt resolver cannot override ${key}`);
112
+ this.name = "ResolverProtectedOptionError";
113
+ }
114
+ }
115
+
107
116
  /**
108
117
  * @param {Object} [options]
109
118
  * @param {AgentRuntimeHostOptions} [options.host]
@@ -178,8 +187,37 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
178
187
  const entrySafetyContract = routeSafetyContract(
179
188
  routeSafety,
180
189
  entry,
181
- effectivePiSandboxPolicy(effectiveToolOptions, options),
190
+ entry.model.sdk === "pi"
191
+ ? effectivePiSandboxPolicy(effectiveToolOptions, options)
192
+ : undefined,
182
193
  );
194
+ // Internal protected roots are enforced by mono-agent's Pi tool layer
195
+ // and SRT projection. Provider-native non-Pi routes deliberately drop
196
+ // that layer, so attempting one would turn the router into a confused
197
+ // deputy for private host state. Reject the route before resolution or
198
+ // provider construction; a later Pi entry may still satisfy the run.
199
+ if (
200
+ entry.model.sdk !== "pi"
201
+ && attemptCarriesProtectedRoots(effectiveToolOptions, options)
202
+ ) {
203
+ const failure = safetyUnavailableResult();
204
+ lastRouteSkip = failure;
205
+ failoverHistory.push({
206
+ model: entry.model,
207
+ failureKind: "safety_unavailable",
208
+ routeSafety,
209
+ safetyContract: entrySafetyContract,
210
+ });
211
+ const unavailableRecord = routeSafetyRecord(
212
+ i,
213
+ entry,
214
+ entrySafetyContract,
215
+ "safety_unavailable",
216
+ );
217
+ routeSafetyHistory.push(unavailableRecord);
218
+ emit(options, { type: "provider_route_safety", ...unavailableRecord });
219
+ continue;
220
+ }
183
221
  if (!entrySatisfiesRequirements(entry, options)) {
184
222
  lastRouteSkip = {
185
223
  text: null,
@@ -763,6 +801,37 @@ function effectiveRouterToolOptions(host, configuredTools) {
763
801
  return effective;
764
802
  }
765
803
 
804
+ /**
805
+ * A protected-root policy is host-internal and intentionally has no public
806
+ * provider-native projection. Treat a present malformed value or an accessor
807
+ * failure as protected so untrusted option shapes cannot turn this gate into a
808
+ * fail-open boundary. Empty arrays preserve ordinary provider-native routing.
809
+ *
810
+ * @param {unknown} policy
811
+ * @returns {boolean}
812
+ */
813
+ function sandboxPolicyHasProtectedRoots(policy) {
814
+ if (policy === null || typeof policy !== "object") return false;
815
+ try {
816
+ if (!("protectedRoots" in policy)) return false;
817
+ const protectedRoots = /** @type {{protectedRoots?: unknown}} */ (policy).protectedRoots;
818
+ if (protectedRoots === undefined) return false;
819
+ return !Array.isArray(protectedRoots) || protectedRoots.length > 0;
820
+ } catch {
821
+ return true;
822
+ }
823
+ }
824
+
825
+ /**
826
+ * @param {import('../types.js').AgentRuntimeToolOptions} toolOptions
827
+ * @param {Object<string, *>} runOptions
828
+ * @returns {boolean}
829
+ */
830
+ function attemptCarriesProtectedRoots(toolOptions, runOptions) {
831
+ return sandboxPolicyHasProtectedRoots(toolOptions.sandboxPolicy)
832
+ || sandboxPolicyHasProtectedRoots(runOptions.sandboxPolicy);
833
+ }
834
+
766
835
  /**
767
836
  * A resolver-supplied Pi runtime is allowed to own credentials/provider
768
837
  * lifecycle, never the route's safety posture. Replace its complete mutable
@@ -850,7 +919,7 @@ function mergeAttemptOptions(base, resolved) {
850
919
  if (resolved === undefined) return merged;
851
920
  for (const [key, value] of Object.entries(resolved)) {
852
921
  if (RESOLVER_PROTECTED_OPTION_KEYS.has(key)) {
853
- throw new Error(`route attempt resolver cannot override ${key}`);
922
+ throw new ResolverProtectedOptionError(key);
854
923
  }
855
924
  if (value !== undefined) merged[key] = value;
856
925
  }
@@ -980,11 +1049,14 @@ function applyEntryEffort(options, effort) {
980
1049
  /** @param {unknown} error @returns {RuntimeResult} */
981
1050
  function safetyUnavailableResult(error) {
982
1051
  // Host resolvers may handle credentials. Never echo their exception text
983
- // into persisted results or route telemetry.
984
- void error;
1052
+ // into persisted results or route telemetry. ResolverProtectedOptionError is
1053
+ // constructed only from a repository-owned allowlist key, so it is safe and
1054
+ // useful to expose for a rejected logical-request override.
985
1055
  return {
986
1056
  text: null,
987
- error: "The route safety contract could not be established before execution.",
1057
+ error: error instanceof ResolverProtectedOptionError
1058
+ ? error.message
1059
+ : "The route safety contract could not be established before execution.",
988
1060
  failureKind: "safety_unavailable",
989
1061
  events: [],
990
1062
  cancelled: false,
@@ -1,3 +1,5 @@
1
+ import { toolLifecycleMetadata } from "../tool-lifecycle.js";
2
+
1
3
  const CODEX_ITEM_EVENTS = new Set(["item.started", "item.completed"]);
2
4
 
3
5
  export function normalizeCodexItemType(type) {
@@ -32,6 +34,17 @@ function itemFailed(item) {
32
34
  );
33
35
  }
34
36
 
37
+ function itemLifecycle(item) {
38
+ const exitCode = item?.exit_code ?? item?.exitCode;
39
+ if (typeof exitCode === "number" && exitCode !== 0) {
40
+ return toolLifecycleMetadata({ state: "exit_nonzero", failure_kind: "runtime_error", detail_code: `exit_${exitCode}` });
41
+ }
42
+ if (itemFailed(item)) {
43
+ return toolLifecycleMetadata({ state: "error", failure_kind: "runtime_error", detail_code: "codex_item_failed" });
44
+ }
45
+ return toolLifecycleMetadata({ state: "success" });
46
+ }
47
+
35
48
  function commandOutput(item) {
36
49
  return item?.aggregated_output ?? item?.aggregatedOutput ?? item?.output ?? "";
37
50
  }
@@ -97,6 +110,7 @@ export function normalizeCodexItemEvent(raw, context = {}) {
97
110
  tool_use_id: id,
98
111
  content: mcpResultContent(item),
99
112
  is_error: itemFailed(item),
113
+ tool_lifecycle: itemLifecycle(item),
100
114
  }],
101
115
  },
102
116
  };
@@ -118,6 +132,7 @@ export function normalizeCodexItemEvent(raw, context = {}) {
118
132
  tool_use_id: id,
119
133
  content: commandOutput(item),
120
134
  is_error: itemFailed(item),
135
+ tool_lifecycle: itemLifecycle(item),
121
136
  }],
122
137
  },
123
138
  };
@@ -7,6 +7,8 @@
7
7
  // ReasoningPart { type:"reasoning", text }
8
8
  // TextPart { type:"text", text }
9
9
 
10
+ import { toolLifecycleMetadata } from "../tool-lifecycle.js";
11
+
10
12
  export function toolUseEvent(part) {
11
13
  return {
12
14
  type: "assistant",
@@ -27,6 +29,9 @@ export function toolResultEvent(part) {
27
29
  tool_use_id: part.callID,
28
30
  content: isError ? (state.error || "") : (state.output || ""),
29
31
  is_error: isError,
32
+ tool_lifecycle: toolLifecycleMetadata(isError
33
+ ? { state: "error", failure_kind: "runtime_error", detail_code: "opencode_tool_error" }
34
+ : { state: "success" }),
30
35
  }],
31
36
  },
32
37
  };
@@ -0,0 +1,347 @@
1
+ // Provider-neutral lifecycle persistence gate. Provider bridges may emit their
2
+ // own exact `tool_lifecycle` hint; absent fidelity falls back conservatively to
3
+ // success/error and never infers terminal state from prose.
4
+
5
+ // @ts-check
6
+
7
+ import { types as nodeUtilTypes } from "node:util";
8
+
9
+ const FAILURE_KINDS = new Set([
10
+ "provider_unavailable", "provider_unavailable_exhausted", "provider_auth", "skipped_capability_mismatch",
11
+ "context_limit", "usage_limit", "process_death", "runtime_error", "cancelled", "cancelled_user",
12
+ "cancelled_stale", "cancelled_shutdown", "cancelled_signal",
13
+ ]);
14
+ const HOST_HISTORY_METADATA = Symbol("mono-agent.host-tool-history");
15
+ const HOST_TOOL_LIFECYCLE_METADATA = Symbol("mono-agent.host-tool-lifecycle");
16
+
17
+ /**
18
+ * @param {{sink?: (event: any) => Promise<any>, onObserve?: (event: any) => void, onEvent?: (event: any) => void, abortSignal?: AbortSignal}} options
19
+ */
20
+ export function createToolLifecycleEventGate({ sink, onObserve, onEvent, abortSignal }) {
21
+ /** @type {Promise<void>} */
22
+ let tail = Promise.resolve();
23
+ let pendingDeliveries = 0;
24
+ /** @type {Map<string, any>} */
25
+ const timing = new Map();
26
+ /** @type {Map<string, any>} */
27
+ const approvals = new Map();
28
+
29
+ const emit = (event) => {
30
+ stripProviderLifecycleMetadata(event);
31
+ try { onObserve?.(event); } catch { /* observer callback semantics remain best-effort */ }
32
+ const requiresPersistence = typeof sink === "function" && eventNeedsPersistence(event);
33
+ if (!requiresPersistence && pendingDeliveries === 0) {
34
+ observeClassification(event, timing, approvals);
35
+ try { onEvent?.(event); } catch { /* host callback semantics remain best-effort */ }
36
+ return;
37
+ }
38
+
39
+ pendingDeliveries += 1;
40
+ const delivery = tail.then(async () => {
41
+ observeClassification(event, timing, approvals);
42
+ if (requiresPersistence) {
43
+ await persistEvent(event, sink, { timing, approvals, abortSignal });
44
+ }
45
+ try { onEvent?.(event); } catch { /* host callback semantics remain best-effort */ }
46
+ }).catch((error) => {
47
+ if (requiresPersistence) attachPersistenceFailure(event, error);
48
+ try { onEvent?.(event); } catch { /* host callback semantics remain best-effort */ }
49
+ });
50
+ tail = delivery.finally(() => { pendingDeliveries -= 1; });
51
+ };
52
+
53
+ return {
54
+ emit,
55
+ async flush() { await tail; },
56
+ };
57
+ }
58
+
59
+ /** @param {any} event */
60
+ function eventNeedsPersistence(event) {
61
+ if (!record(event) || (event.type !== "assistant" && event.type !== "user")) return false;
62
+ const message = event.message;
63
+ if (!record(message) || !Array.isArray(message.content)) return false;
64
+ return message.content.some((block) => {
65
+ if (!record(block) || hostHistoryMetadata(block.history)) return false;
66
+ if (event.type === "assistant" && block.type === "tool_use") {
67
+ return typeof block.id === "string" && typeof block.name === "string";
68
+ }
69
+ if (event.type === "user" && block.type === "tool_result") {
70
+ return typeof block.tool_use_id === "string" || typeof block.tool_call_id === "string";
71
+ }
72
+ return false;
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Pi-native exact classifier. It consumes structured tool result details, not
78
+ * error text, so timeout/signal/non-zero/cancellation fidelity cannot drift
79
+ * with provider wording.
80
+ * @param {{result?: any,isError?: boolean,aborted?: boolean,approval?: any}} input
81
+ */
82
+ export function classifyPiToolResult(input) {
83
+ const outcome = input.result?.details?.outcome;
84
+ const outcomeFailureKind = failureKind(outcome?.failureKind);
85
+ if (input.approval?.reason === "approval_timeout") {
86
+ return terminal("timeout", "runtime_error", "approval_timeout");
87
+ }
88
+ if (input.approval?.decision === "deny") {
89
+ return terminal("rejected", "runtime_error", boundedCode(input.approval.reason || "approval_denied"));
90
+ }
91
+ if (outcome?.timedOut === true || outcome?.timed_out === true || outcome?.code === "timeout") {
92
+ return terminal("timeout", "runtime_error", "tool_timeout");
93
+ }
94
+ if (typeof outcome?.signal === "string" && outcome.signal.length > 0) {
95
+ return terminal("signal", "process_death", boundedCode(outcome.signal));
96
+ }
97
+ const exitCode = Number(outcome?.exitCode ?? outcome?.exit_code);
98
+ if (Number.isFinite(exitCode) && exitCode !== 0) {
99
+ return terminal("exit_nonzero", "runtime_error", `exit_${String(exitCode)}`);
100
+ }
101
+ if (outcome?.code === "aborted" || input.aborted && input.isError && !failureOutranksAbort(outcomeFailureKind)) {
102
+ return terminal("cancelled", "cancelled", "abort_signal");
103
+ }
104
+ if (input.isError || outcome?.status === "error") {
105
+ return terminal("error", outcomeFailureKind, boundedCode(outcome?.code || "runtime_error"));
106
+ }
107
+ return terminal("success", undefined, undefined);
108
+ }
109
+
110
+ /** @param {any} event @param {Map<string,any>} timing @param {Map<string,any>} approvals */
111
+ function observeClassification(event, timing, approvals) {
112
+ if (!record(event)) return;
113
+ if (event.type === "tool_timing" && typeof event.tool_use_id === "string") {
114
+ timing.set(event.tool_use_id, event);
115
+ }
116
+ if (event.type === "tool_approval_denied" && typeof event.toolUseId === "string") {
117
+ approvals.set(event.toolUseId, event);
118
+ }
119
+ }
120
+
121
+ /** @param {any} event @param {(event:any)=>Promise<any>} sink @param {{timing:Map<string,any>,approvals:Map<string,any>,abortSignal?:AbortSignal}} context */
122
+ async function persistEvent(event, sink, context) {
123
+ if (!record(event) || (event.type !== "assistant" && event.type !== "user")) return;
124
+ const message = event.message;
125
+ if (!record(message) || !Array.isArray(message.content)) return;
126
+ for (const block of message.content) {
127
+ if (!record(block)) continue;
128
+ if (event.type === "assistant" && block.type === "tool_use") {
129
+ if (hostHistoryMetadata(block.history)) continue;
130
+ if (typeof block.id !== "string" || typeof block.name !== "string") continue;
131
+ const persisted = await safePersist(sink, {
132
+ phase: "invocation",
133
+ toolCallId: block.id,
134
+ toolName: block.name,
135
+ ...(Object.hasOwn(block, "input") ? { arguments: block.input } : {}),
136
+ });
137
+ block.history = historyMetadata(persisted, undefined);
138
+ continue;
139
+ }
140
+ if (event.type === "user" && block.type === "tool_result") {
141
+ if (hostHistoryMetadata(block.history)) continue;
142
+ const id = typeof block.tool_use_id === "string" ? block.tool_use_id
143
+ : typeof block.tool_call_id === "string" ? block.tool_call_id : undefined;
144
+ if (id === undefined) continue;
145
+ const classified = classifyGenericResult(block, context.timing.get(id), context.approvals.get(id), context.abortSignal);
146
+ const persisted = await safePersist(sink, {
147
+ phase: "result",
148
+ toolCallId: id,
149
+ ...(typeof block.name === "string" ? { toolName: block.name } : {}),
150
+ ...(Object.hasOwn(block, "content") ? { content: block.content } : {}),
151
+ ...classified,
152
+ ...(typeof context.timing.get(id)?.execution_ms === "number"
153
+ ? { executionMs: context.timing.get(id).execution_ms }
154
+ : {}),
155
+ artifacts: artifactPaths(block),
156
+ });
157
+ block.history = historyMetadata(persisted, classified.state);
158
+ context.timing.delete(id);
159
+ context.approvals.delete(id);
160
+ }
161
+ }
162
+ }
163
+
164
+ /** @param {any} block @param {any} timing @param {any} approval @param {AbortSignal|undefined} abortSignal */
165
+ function classifyGenericResult(block, timing, approval, abortSignal) {
166
+ const explicit = hostToolLifecycleMetadata(block.tool_lifecycle)
167
+ ? block.tool_lifecycle
168
+ : hostToolLifecycleMetadata(timing?.tool_lifecycle)
169
+ ? timing.tool_lifecycle
170
+ : undefined;
171
+ // A failure kind only outranks host cancellation when its trusted hint also
172
+ // supplies the terminal error state required by the lifecycle contract.
173
+ const explicitFailureOutranksAbort = record(explicit)
174
+ && explicit.state === "error"
175
+ && failureOutranksAbort(explicit.failure_kind);
176
+ if (approval?.reason === "approval_timeout") return terminal("timeout", "runtime_error", "approval_timeout");
177
+ if (approval?.decision === "deny") return terminal("rejected", "runtime_error", boundedCode(approval.reason || "approval_denied"));
178
+ if (record(explicit) && terminalState(explicit.state) && explicit.state !== "error") {
179
+ return explicit.state === "success"
180
+ ? terminal("success", undefined, undefined)
181
+ : terminal(
182
+ explicit.state,
183
+ explicit.state === "cancelled" && abortSignal?.aborted
184
+ ? cancellationFailureKind(abortSignal)
185
+ : lifecycleFailureKind(explicit.state, explicit.failure_kind),
186
+ boundedCode(explicit.detail_code || explicit.state),
187
+ );
188
+ }
189
+ if (timing?.timed_out === true) return terminal("timeout", "runtime_error", "tool_timeout");
190
+ if (typeof timing?.signal === "string" && timing.signal.length > 0) return terminal("signal", "process_death", boundedCode(timing.signal));
191
+ if (Number.isFinite(Number(timing?.exit_code)) && Number(timing.exit_code) !== 0) {
192
+ return terminal("exit_nonzero", "runtime_error", `exit_${String(Number(timing.exit_code))}`);
193
+ }
194
+ if (
195
+ abortSignal?.aborted
196
+ && block.is_error === true
197
+ && !explicitFailureOutranksAbort
198
+ ) {
199
+ return terminal("cancelled", cancellationFailureKind(abortSignal), "abort_signal");
200
+ }
201
+ if (record(explicit) && explicit.state === "error") {
202
+ return terminal("error", failureKind(explicit.failure_kind), boundedCode(explicit.detail_code || "provider_error"));
203
+ }
204
+ if (block.is_error === true || timing?.is_error === true) return terminal("error", failureKind(explicit?.failure_kind), boundedCode(explicit?.detail_code || "provider_error"));
205
+ return terminal("success", undefined, undefined);
206
+ }
207
+
208
+ /** @param {(event:any)=>Promise<any>} sink @param {any} event */
209
+ async function safePersist(sink, event) {
210
+ try {
211
+ return await sink(event) ?? { persistence: "failed", errorCode: "history_writer_unavailable" };
212
+ } catch (error) {
213
+ return { persistence: "failed", errorCode: error?.code || "history_write_failed" };
214
+ }
215
+ }
216
+
217
+ /** @param {any} persisted @param {string|undefined} terminalStateValue */
218
+ export function historyMetadata(persisted, terminalStateValue) {
219
+ const metadata = {
220
+ ...(typeof persisted?.recordId === "string" ? { recordId: persisted.recordId } : {}),
221
+ ...(Number.isFinite(Number(persisted?.sequence)) ? { sequence: Number(persisted.sequence) } : {}),
222
+ persistence: persisted?.persistence === "persisted" ? "persisted" : "failed",
223
+ ...(terminalStateValue === undefined ? {} : { terminalState: terminalStateValue }),
224
+ ...(typeof persisted?.truncated === "boolean" ? { truncated: persisted.truncated } : {}),
225
+ ...(Number.isFinite(Number(persisted?.originalBytes)) ? { originalBytes: Number(persisted.originalBytes) } : {}),
226
+ ...(Number.isFinite(Number(persisted?.retainedBytes)) ? { retainedBytes: Number(persisted.retainedBytes) } : {}),
227
+ ...(Array.isArray(persisted?.artifactReferences) ? { artifactReferences: persisted.artifactReferences } : {}),
228
+ ...(typeof persisted?.errorCode === "string" ? { errorCode: persisted.errorCode } : {}),
229
+ untrusted: true,
230
+ };
231
+ Object.defineProperty(metadata, HOST_HISTORY_METADATA, { value: true });
232
+ return metadata;
233
+ }
234
+
235
+ /** Mark a provider adapter's host-derived structured outcome as trusted input to the gate. @param {any} value */
236
+ export function toolLifecycleMetadata(value) {
237
+ const metadata = record(value) ? { ...value } : {};
238
+ Object.defineProperty(metadata, HOST_TOOL_LIFECYCLE_METADATA, { value: true });
239
+ return metadata;
240
+ }
241
+
242
+ /** Provider payloads cannot assert host persistence or choose a terminal state. @param {any} event */
243
+ function stripProviderLifecycleMetadata(event) {
244
+ if (!record(event) || !record(event.message) || !Array.isArray(event.message.content)) return;
245
+ for (const block of event.message.content) {
246
+ if (!record(block) || (block.type !== "tool_use" && block.type !== "tool_result")) continue;
247
+ if (Object.hasOwn(block, "history") && !hostHistoryMetadata(block.history)) delete block.history;
248
+ if (Object.hasOwn(block, "tool_lifecycle") && !hostToolLifecycleMetadata(block.tool_lifecycle)) {
249
+ delete block.tool_lifecycle;
250
+ }
251
+ }
252
+ }
253
+
254
+ /** @param {any} value */
255
+ function hostHistoryMetadata(value) {
256
+ return record(value) && value[HOST_HISTORY_METADATA] === true;
257
+ }
258
+
259
+ /** @param {any} value */
260
+ function hostToolLifecycleMetadata(value) {
261
+ return record(value) && value[HOST_TOOL_LIFECYCLE_METADATA] === true;
262
+ }
263
+
264
+ /** @param {any} event @param {unknown} error */
265
+ function attachPersistenceFailure(event, error) {
266
+ if (!record(event) || !record(event.message) || !Array.isArray(event.message.content)) return;
267
+ for (const block of event.message.content) {
268
+ if (record(block) && (block.type === "tool_use" || block.type === "tool_result")) {
269
+ const candidate = /** @type {any} */ (error);
270
+ const code = record(candidate) && typeof candidate.code === "string" ? candidate.code : "history_write_failed";
271
+ block.history = historyMetadata({ persistence: "failed", errorCode: code }, undefined);
272
+ }
273
+ }
274
+ }
275
+
276
+ /** @param {any} block */
277
+ function artifactPaths(block) {
278
+ const paths = new Set();
279
+ collectPaths(block.raw_result?.details?.tool_payload_saved_paths, paths);
280
+ collectPaths(block.raw_result?.tool_payload_saved_paths, paths);
281
+ collectPaths(block.tool_payload_saved_paths, paths);
282
+ return [...paths].slice(0, 32).map((path) => ({ path }));
283
+ }
284
+
285
+ /** @param {any} value @param {Set<string>} paths */
286
+ function collectPaths(value, paths) {
287
+ if (typeof value === "string" && value.length > 0) paths.add(value);
288
+ else if (Array.isArray(value)) for (const item of value) collectPaths(item, paths);
289
+ }
290
+
291
+ /** @param {string} state @param {string|undefined} failureKindValue @param {string|undefined} detailCode */
292
+ function terminal(state, failureKindValue, detailCode) {
293
+ return {
294
+ state,
295
+ ...(failureKindValue === undefined ? {} : { failureKind: failureKindValue }),
296
+ ...(detailCode === undefined ? {} : { detailCode }),
297
+ };
298
+ }
299
+
300
+ /** @param {any} value */
301
+ function failureKind(value) {
302
+ return typeof value === "string" && FAILURE_KINDS.has(value) ? value : "runtime_error";
303
+ }
304
+
305
+ /** @param {string} state @param {any} value */
306
+ function lifecycleFailureKind(state, value) {
307
+ if (typeof value === "string" && FAILURE_KINDS.has(value)) return value;
308
+ if (state === "signal" || state === "interrupted") return "process_death";
309
+ return state === "cancelled" ? "cancelled" : "runtime_error";
310
+ }
311
+
312
+ /** @param {any} value */
313
+ function failureOutranksAbort(value) {
314
+ const kind = failureKind(value);
315
+ return kind !== "runtime_error" && !kind.startsWith("cancelled");
316
+ }
317
+
318
+ /** @param {AbortSignal} signal */
319
+ function cancellationFailureKind(signal) {
320
+ const reason = signal.reason;
321
+ if (typeof reason !== "object" || reason === null || nodeUtilTypes.isProxy(reason)) {
322
+ return "cancelled";
323
+ }
324
+ try {
325
+ const descriptor = Object.getOwnPropertyDescriptor(reason, "channelUserCancel");
326
+ return descriptor !== undefined && "value" in descriptor && descriptor.value === true
327
+ ? "cancelled_user"
328
+ : "cancelled";
329
+ } catch {
330
+ return "cancelled";
331
+ }
332
+ }
333
+
334
+ /** @param {any} value */
335
+ function terminalState(value) {
336
+ return ["success", "rejected", "error", "exit_nonzero", "timeout", "signal", "cancelled", "interrupted"].includes(value);
337
+ }
338
+
339
+ /** @param {any} value */
340
+ function boundedCode(value) {
341
+ return String(value || "unknown").replace(/[^a-zA-Z0-9_.:-]/g, "_").slice(0, 160) || "unknown";
342
+ }
343
+
344
+ /** @param {any} value */
345
+ function record(value) {
346
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
347
+ }
package/src/ai/types.js CHANGED
@@ -54,6 +54,7 @@
54
54
  * @property {RuntimeModelRef} [model]
55
55
  * @property {string} [effort]
56
56
  * @property {Object<string, Object>} [mcpServers]
57
+ * @property {Object} [mcpApps] App-owned exact-connection MCP Apps registry (Pi-native only).
57
58
  */
58
59
 
59
60
  /**
@@ -116,6 +117,55 @@
116
117
  * provider_failover_started, context_compaction, ...) adds its own fields.
117
118
  */
118
119
 
120
+ /** @typedef {"success"|"rejected"|"error"|"exit_nonzero"|"timeout"|"signal"|"cancelled"|"interrupted"} RuntimeToolLifecycleTerminalState */
121
+
122
+ /**
123
+ * @typedef {Readonly<{
124
+ * phase: "invocation",
125
+ * toolCallId: string,
126
+ * toolName: string,
127
+ * arguments?: unknown,
128
+ * }>} RuntimeToolLifecycleInvocationEvent
129
+ * Provider-neutral invocation half sent to the host-owned lifecycle sink.
130
+ */
131
+
132
+ /**
133
+ * @typedef {Readonly<{
134
+ * phase: "result",
135
+ * toolCallId: string,
136
+ * toolName?: string,
137
+ * content?: unknown,
138
+ * state: RuntimeToolLifecycleTerminalState,
139
+ * failureKind?: string,
140
+ * detailCode?: string,
141
+ * executionMs?: number,
142
+ * artifacts?: ReadonlyArray<Readonly<{path: string, available?: boolean}>>,
143
+ * }>} RuntimeToolLifecycleResultEvent
144
+ * Provider-neutral terminal half sent to the host-owned lifecycle sink.
145
+ */
146
+
147
+ /** @typedef {RuntimeToolLifecycleInvocationEvent | RuntimeToolLifecycleResultEvent} RuntimeToolLifecycleEvent */
148
+
149
+ /**
150
+ * @typedef {Readonly<{
151
+ * recordId?: string,
152
+ * sequence?: number,
153
+ * persistence: "persisted"|"failed",
154
+ * truncated?: boolean,
155
+ * originalBytes?: number,
156
+ * retainedBytes?: number,
157
+ * artifactReferences?: ReadonlyArray<Readonly<{id: string, available: boolean}>>,
158
+ * errorCode?: string,
159
+ * }>} RuntimeToolLifecyclePersistence
160
+ * Bounded metadata returned after one lifecycle half becomes durable.
161
+ */
162
+
163
+ /**
164
+ * @callback RuntimeToolLifecycleSink
165
+ * @param {RuntimeToolLifecycleEvent} event
166
+ * @returns {Promise<RuntimeToolLifecyclePersistence|undefined>}
167
+ */
168
+
119
169
  /** @typedef {"uniform"|"per-route-native"} RuntimeRouteSafetyMode */
120
170
 
121
171
  /**
@@ -212,6 +262,7 @@
212
262
  * @property {AsyncIterable<{body: string, id?: string, receivedAt?: string, acknowledge?: () => void, reject?: (error?: unknown) => void}>} [liveInput] Stream of in-flight user messages for steering an active run. Providers acknowledge only after accepting a message into the active turn.
213
263
  * @property {ReadonlyArray<*>} [observers] Per-call observers (see RuntimeObserver) merged with host-level (createRuntime) observers.
214
264
  * @property {(event: RuntimeEvent) => void} [onEvent]
265
+ * @property {RuntimeToolLifecycleSink} [toolLifecycleSink] Awaited host-owned incremental lifecycle persistence boundary.
215
266
  * @property {ReadonlyArray<Object>} [messages]
216
267
  * @property {string} [effort]
217
268
  * @property {boolean} [fastMode]
@@ -252,10 +303,17 @@
252
303
  * @property {boolean} [codexLoadProjectDocs] Codex app-server only. Omitted/false starts the managed app-server with
253
304
  * `project_doc_max_bytes=0`, preventing automatic repository-instruction discovery. True restores Codex's native
254
305
  * project-doc loading defaults. An explicit `codexAppServerArgs` array wins over this convenience option.
306
+ * @property {boolean} [codexSandboxNetworkAccess] Codex app-server only, code-only. Strict `true` enables native
307
+ * network access for plan/read-only and default/acceptEdits/workspace-write turns; omitted or any other runtime
308
+ * value denies it. No-tool probes always deny network access, and bypass/danger-full-access remains unchanged. This
309
+ * is unrelated to `RuntimeRunOptions.sandboxPolicy`, which controls mono-agent's own sandbox and is not consumed by
310
+ * Codex's provider-owned tool loop. Default/acceptEdits workspace-write plus network true grants repository read and
311
+ * network egress in the same turn; prefer plan when only read-only browsing is needed.
255
312
  * @property {RuntimeNativeSubagentsOptions} [nativeSubagents] Caller-defined Claude native `Task` profiles. Direct
256
313
  * Codex owns its collaboration agents and rejects configured teammate definitions; `codexLoadProjectDocs` controls
257
314
  * whether Codex loads repository instructions for its own agents.
258
315
  * @property {RuntimeSubagentsOptions} [subagents] In-process `Agent` built-in: profiles, caps, and the nested-run callback.
316
+ * @property {import('../agent/tools/shared/process-jobs.js').ProcessJobsController} [processJobs] Pi-native-only structural process-job controller. When absent, Exec/Bash schemas and foreground behavior are unchanged.
259
317
  * @property {Object} [diagnosticsSeed] Set by createRouterRuntime (ai/runtime/router.js) with a `resume_snapshot` when
260
318
  * failing over mid-chain; a host-level coordinator may relay it forward (see agent/transcript.js), not read by any
261
319
  * bridge in this package today.