@mono-agent/agent-runtime 0.19.1 → 0.20.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.
- package/MIGRATION.md +1 -1
- package/README.md +101 -4
- package/package.json +1 -1
- package/src/agent/sandbox-seam.js +16 -2
- package/src/agent/tools/bash.js +26 -4
- package/src/agent/tools/edit.js +72 -5
- package/src/agent/tools/exec.js +22 -4
- package/src/agent/tools/glob.js +65 -9
- package/src/agent/tools/grep.js +66 -11
- package/src/agent/tools/node-repl.js +5 -2
- package/src/agent/tools/pi-bridge.js +263 -48
- package/src/agent/tools/read.js +50 -10
- package/src/agent/tools/shared/path-resolver.js +67 -2
- package/src/agent/tools/shared/process-jobs.js +188 -0
- package/src/agent/tools/shared/process-runner.js +541 -30
- package/src/agent/tools/shared/protected-filesystem.js +150 -0
- package/src/agent/tools/web-search.js +63 -8
- package/src/agent/tools/write.js +52 -6
- package/src/ai/providers/acp.js +4 -0
- package/src/ai/providers/claude-cli.js +35 -2
- package/src/ai/providers/claude-sdk.js +12 -0
- package/src/ai/providers/codex-app.js +15 -2
- package/src/ai/providers/pi-native/stream-subscriber.js +29 -2
- package/src/ai/providers/pi-native/turn-runner.js +3 -0
- package/src/ai/providers/pi-native.js +7 -1
- package/src/ai/runtime/capabilities.js +2 -0
- package/src/ai/runtime/router.js +78 -6
- package/src/ai/streaming/codex-events.js +15 -0
- package/src/ai/streaming/opencode-events.js +5 -0
- package/src/ai/tool-lifecycle.js +347 -0
- package/src/ai/types.js +58 -0
- package/src/runtime.js +35 -21
- package/types/agent/sandbox-seam.d.ts +19 -6
- package/types/agent/tools/bash.d.ts +11 -26
- package/types/agent/tools/edit.d.ts +3 -2
- package/types/agent/tools/exec.d.ts +13 -26
- package/types/agent/tools/glob.d.ts +3 -2
- package/types/agent/tools/grep.d.ts +3 -2
- package/types/agent/tools/pi-bridge.d.ts +11 -4
- package/types/agent/tools/read.d.ts +3 -2
- package/types/agent/tools/shared/path-resolver.d.ts +8 -0
- package/types/agent/tools/shared/process-jobs.d.ts +64 -0
- package/types/agent/tools/shared/process-runner.d.ts +45 -3
- package/types/agent/tools/shared/protected-filesystem.d.ts +51 -0
- package/types/agent/tools/write.d.ts +3 -2
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -0
- package/types/ai/runtime/capabilities.d.ts +3 -0
- package/types/ai/streaming/codex-events.d.ts +1 -0
- package/types/ai/streaming/opencode-events.d.ts +1 -0
- package/types/ai/tool-lifecycle.d.ts +43 -0
- package/types/ai/types.d.ts +118 -0
package/src/ai/runtime/router.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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.
|