@mono-agent/agent-runtime 0.3.0 → 0.4.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.
- package/ARCHITECTURE.md +75 -9
- package/MIGRATION.md +293 -0
- package/README.md +46 -20
- package/package.json +104 -10
- package/src/agent/allowlists.js +3 -0
- package/src/agent/approval.js +3 -0
- package/src/agent/compaction.js +231 -654
- package/src/agent/index.js +1 -1
- package/src/agent/prompt/skill-index.js +6 -0
- package/src/agent/sandbox-seam.js +227 -0
- package/src/agent/tool-bloat.js +8 -2
- package/src/agent/tools/bash.js +16 -8
- package/src/agent/tools/edit.js +7 -3
- package/src/agent/tools/glob.js +11 -5
- package/src/agent/tools/grep.js +11 -5
- package/src/agent/tools/pi-bridge.js +272 -54
- package/src/agent/tools/read.js +28 -3
- package/src/agent/tools/shared/output-truncation.js +8 -3
- package/src/agent/tools/shared/path-resolver.js +24 -18
- package/src/agent/tools/shared/ripgrep.js +33 -6
- package/src/agent/tools/shared/runtime-context.js +60 -50
- package/src/agent/tools/shared/tool-context.js +157 -0
- package/src/agent/tools/web-fetch.js +65 -18
- package/src/agent/tools/web-search.js +11 -4
- package/src/agent/tools/write.js +7 -3
- package/src/agent/transcript.js +16 -1
- package/src/ai/cost.js +128 -3
- package/src/ai/failure.js +96 -4
- package/src/ai/index.js +1 -0
- package/src/ai/live-input-prompt.js +11 -1
- package/src/ai/providers/claude-cli.js +6 -2
- package/src/ai/providers/claude-sdk.js +55 -12
- package/src/ai/providers/codex-app.js +36 -23
- package/src/ai/providers/opencode-app.js +2 -2
- package/src/ai/providers/opencode-discovery.js +2 -2
- package/src/ai/providers/pi-errors.js +65 -0
- package/src/ai/providers/pi-events.js +5 -0
- package/src/ai/providers/pi-models.js +21 -4
- package/src/ai/providers/pi-native/compaction-driver.js +394 -0
- package/src/ai/providers/pi-native/result-builder.js +312 -0
- package/src/ai/providers/pi-native/session-lifecycle.js +438 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +148 -0
- package/src/ai/providers/pi-native/structured-output.js +130 -0
- package/src/ai/providers/pi-native/turn-runner.js +278 -0
- package/src/ai/providers/pi-native.js +754 -0
- package/src/ai/runtime/capabilities.js +3 -0
- package/src/ai/runtime/model-refs.js +30 -0
- package/src/ai/runtime/registry.js +33 -2
- package/src/ai/runtime/router.js +119 -19
- package/src/ai/runtime/session-liveness.js +110 -0
- package/src/ai/runtime/sessions.js +19 -2
- package/src/ai/types.js +252 -20
- package/src/index.js +1 -1
- package/src/pi-auth.js +147 -16
- package/src/runtime-brand.js +21 -1
- package/src/runtime.js +75 -10
- package/types/agent/allowlists.d.ts +25 -0
- package/types/agent/approval.d.ts +30 -0
- package/types/agent/compaction.d.ts +97 -0
- package/types/agent/index.d.ts +5 -0
- package/types/agent/prompt/skill-index.d.ts +19 -0
- package/types/agent/sandbox-seam.d.ts +148 -0
- package/types/agent/tool-bloat.d.ts +22 -0
- package/types/agent/tools/bash.d.ts +16 -0
- package/types/agent/tools/edit.d.ts +14 -0
- package/types/agent/tools/glob.d.ts +16 -0
- package/types/agent/tools/grep.d.ts +22 -0
- package/types/agent/tools/index.d.ts +10 -0
- package/types/agent/tools/pi-bridge.d.ts +144 -0
- package/types/agent/tools/read.d.ts +19 -0
- package/types/agent/tools/shared/constants.d.ts +13 -0
- package/types/agent/tools/shared/dedup.d.ts +8 -0
- package/types/agent/tools/shared/output-truncation.d.ts +22 -0
- package/types/agent/tools/shared/path-resolver.d.ts +6 -0
- package/types/agent/tools/shared/ripgrep.d.ts +38 -0
- package/types/agent/tools/shared/runtime-context.d.ts +27 -0
- package/types/agent/tools/shared/tool-context.d.ts +48 -0
- package/types/agent/tools/web-fetch.d.ts +13 -0
- package/types/agent/tools/web-search.d.ts +11 -0
- package/types/agent/tools/write.d.ts +12 -0
- package/types/agent/transcript.d.ts +45 -0
- package/types/ai/backend.d.ts +41 -0
- package/types/ai/cost.d.ts +96 -0
- package/types/ai/failure.d.ts +117 -0
- package/types/ai/file-change-stats.d.ts +75 -0
- package/types/ai/index.d.ts +7 -0
- package/types/ai/live-input-prompt.d.ts +6 -0
- package/types/ai/observer.d.ts +68 -0
- package/types/ai/providers/claude-cli.d.ts +211 -0
- package/types/ai/providers/claude-sdk.d.ts +66 -0
- package/types/ai/providers/claude-subagents.d.ts +2 -0
- package/types/ai/providers/codex-app.d.ts +151 -0
- package/types/ai/providers/opencode-app.d.ts +95 -0
- package/types/ai/providers/opencode-discovery.d.ts +4 -0
- package/types/ai/providers/pi-errors.d.ts +3 -0
- package/types/ai/providers/pi-events.d.ts +24 -0
- package/types/ai/providers/pi-messages.d.ts +5 -0
- package/types/ai/providers/pi-models.d.ts +57 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +86 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +168 -0
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +62 -0
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +50 -0
- package/types/ai/providers/pi-native/structured-output.d.ts +69 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +60 -0
- package/types/ai/providers/pi-native.d.ts +18 -0
- package/types/ai/registry.d.ts +1 -0
- package/types/ai/runtime/capabilities-used.d.ts +21 -0
- package/types/ai/runtime/capabilities.d.ts +33 -0
- package/types/ai/runtime/context-windows.d.ts +8 -0
- package/types/ai/runtime/fast-mode.d.ts +2 -0
- package/types/ai/runtime/model-refs.d.ts +35 -0
- package/types/ai/runtime/registry.d.ts +38 -0
- package/types/ai/runtime/router.d.ts +56 -0
- package/types/ai/runtime/session-liveness.d.ts +55 -0
- package/types/ai/runtime/sessions.d.ts +38 -0
- package/types/ai/streaming/codex-events.d.ts +30 -0
- package/types/ai/streaming/opencode-events.d.ts +41 -0
- package/types/ai/types.d.ts +702 -0
- package/types/index.d.ts +7 -0
- package/types/pi-auth.d.ts +28 -0
- package/types/runtime-brand.d.ts +30 -0
- package/types/runtime.d.ts +10 -0
- package/src/ai/providers/pi-sdk.js +0 -1310
|
@@ -24,6 +24,9 @@ export const RUNTIME_CAPABILITIES = {
|
|
|
24
24
|
// The pi bridge keeps a pi-agent-core Session transcript per provider
|
|
25
25
|
// session id and seeds Agent initialState.messages on resume.
|
|
26
26
|
supports_session_resume: true,
|
|
27
|
+
// The pi-native bridge does not (yet) wire native subagents / an AskAgent
|
|
28
|
+
// tool, so advertise no support rather than letting callers expect it.
|
|
29
|
+
supports_native_subagents: false,
|
|
27
30
|
},
|
|
28
31
|
codex: {
|
|
29
32
|
runtime: "cli",
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('../types.js').RuntimeModelRef} RuntimeModelRef */
|
|
4
|
+
|
|
1
5
|
const RESERVED_RUNTIME_IDS = new Set(["openai", "vercel", "claude-code", "codex-cli"]);
|
|
2
6
|
const ACTIVE_RUNTIME_IDS = new Set(["claude", "pi", "codex", "opencode"]);
|
|
3
7
|
|
|
@@ -14,6 +18,10 @@ function rejectTierAlias(model) {
|
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
20
|
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} value
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
17
25
|
export function canonicalizeLegacyModelReference(value) {
|
|
18
26
|
if (!value || typeof value !== "string") throw new Error("model reference required");
|
|
19
27
|
|
|
@@ -42,15 +50,27 @@ export function canonicalizeLegacyModelReference(value) {
|
|
|
42
50
|
return value;
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} value
|
|
55
|
+
* @returns {RuntimeModelRef}
|
|
56
|
+
*/
|
|
45
57
|
export function normalizeRuntimeModelReference(value) {
|
|
46
58
|
return parseRuntimeModelReference(canonicalizeLegacyModelReference(value));
|
|
47
59
|
}
|
|
48
60
|
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} value
|
|
63
|
+
* @returns {RuntimeModelRef["sdk"]}
|
|
64
|
+
*/
|
|
49
65
|
export function sdkFromModelReference(value) {
|
|
50
66
|
const parsed = parseRuntimeModelReference(value);
|
|
51
67
|
return parsed.sdk;
|
|
52
68
|
}
|
|
53
69
|
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} value
|
|
72
|
+
* @returns {RuntimeModelRef}
|
|
73
|
+
*/
|
|
54
74
|
export function parseRuntimeModelReference(value) {
|
|
55
75
|
if (!value || typeof value !== "string") throw new Error("model reference required");
|
|
56
76
|
|
|
@@ -107,6 +127,11 @@ export const RESERVED_RUNTIME_KINDS = [...RESERVED_RUNTIME_IDS];
|
|
|
107
127
|
|
|
108
128
|
// Returns null when the combo is fine; otherwise a short reason string the
|
|
109
129
|
// UI / API can show.
|
|
130
|
+
/**
|
|
131
|
+
* @param {string|RuntimeModelRef} modelRefOrParsed
|
|
132
|
+
* @param {string} executionMode
|
|
133
|
+
* @returns {string|null}
|
|
134
|
+
*/
|
|
110
135
|
export function executionModeIncompatibilityReason(modelRefOrParsed, executionMode) {
|
|
111
136
|
let parsed;
|
|
112
137
|
try {
|
|
@@ -139,6 +164,11 @@ export function executionModeIncompatibilityReason(modelRefOrParsed, executionMo
|
|
|
139
164
|
return `sdk \`${parsed.sdk}\` is not supported under CLI execution mode.`;
|
|
140
165
|
}
|
|
141
166
|
|
|
167
|
+
/**
|
|
168
|
+
* @param {string|RuntimeModelRef} modelRefOrParsed
|
|
169
|
+
* @param {string} executionMode
|
|
170
|
+
* @returns {boolean}
|
|
171
|
+
*/
|
|
142
172
|
export function isModelCompatibleWithExecutionMode(modelRefOrParsed, executionMode) {
|
|
143
173
|
return executionModeIncompatibilityReason(modelRefOrParsed, executionMode) === null;
|
|
144
174
|
}
|
|
@@ -1,8 +1,29 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
import { COMMON_CAPABILITIES, runtimeCapabilities } from "./capabilities.js";
|
|
2
4
|
|
|
5
|
+
/**
|
|
6
|
+
* `RuntimeModelRef` is referenced inline (not aliased with a top-level
|
|
7
|
+
* `@typedef`) so this barrel does not re-export a second `RuntimeModelRef`
|
|
8
|
+
* type alongside model-refs.js's — that duplicate `export *` re-export is a
|
|
9
|
+
* TS2308 ambiguity. The canonical export stays in model-refs.js/types.js.
|
|
10
|
+
* @typedef {import('../types.js').RuntimeBridge} RuntimeBridge
|
|
11
|
+
* @typedef {import('../types.js').RuntimeBridgeDescriptor} RuntimeBridgeDescriptor
|
|
12
|
+
* @typedef {import('../types.js').RuntimeBridgeId} RuntimeBridgeId
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} BridgeSpec
|
|
17
|
+
* @property {RuntimeBridgeId} id
|
|
18
|
+
* @property {(ref: (import('../types.js').RuntimeModelRef|undefined), options?: Object) => boolean} supports
|
|
19
|
+
* @property {(ref?: import('../types.js').RuntimeModelRef) => Object} capabilities
|
|
20
|
+
* @property {(options?: Object) => Promise<RuntimeBridge>} load
|
|
21
|
+
*/
|
|
22
|
+
|
|
3
23
|
// CLI bridges are checked first when execution_mode='cli'. Without that flag
|
|
4
24
|
// the resolver falls through to the SDK bridges below, preserving the
|
|
5
25
|
// pre-Phase-2 behaviour for any agent that hasn't opted in.
|
|
26
|
+
/** @type {Object<string, BridgeSpec>} */
|
|
6
27
|
const builtinBridgeSpecs = {
|
|
7
28
|
"claude-code": {
|
|
8
29
|
id: "claude-code",
|
|
@@ -35,10 +56,15 @@ const builtinBridgeSpecs = {
|
|
|
35
56
|
id: "pi",
|
|
36
57
|
supports: (ref) => ref?.sdk === "pi",
|
|
37
58
|
capabilities: () => runtimeCapabilities("pi"),
|
|
38
|
-
|
|
59
|
+
// The pi-native AgentHarness bridge is the sole pi runtime path. The
|
|
60
|
+
// hand-rolled pi-sdk bridge was removed once native reached parity.
|
|
61
|
+
load: async () => (await import("../providers/pi-native.js")).piNativeRuntimeBridge,
|
|
39
62
|
},
|
|
40
63
|
};
|
|
41
64
|
|
|
65
|
+
/**
|
|
66
|
+
* @returns {Array<RuntimeBridgeDescriptor>}
|
|
67
|
+
*/
|
|
42
68
|
export function listRuntimeBridges() {
|
|
43
69
|
return Object.values(builtinBridgeSpecs).map((bridge) => ({
|
|
44
70
|
id: bridge.id,
|
|
@@ -47,9 +73,14 @@ export function listRuntimeBridges() {
|
|
|
47
73
|
}));
|
|
48
74
|
}
|
|
49
75
|
|
|
76
|
+
/**
|
|
77
|
+
* @param {import('../types.js').RuntimeModelRef} modelRef
|
|
78
|
+
* @param {Object} [options]
|
|
79
|
+
* @returns {Promise<RuntimeBridge>}
|
|
80
|
+
*/
|
|
50
81
|
export async function resolveRuntimeBridge(modelRef, options = {}) {
|
|
51
82
|
for (const spec of Object.values(builtinBridgeSpecs)) {
|
|
52
|
-
if (spec.supports(modelRef, options)) return spec.load();
|
|
83
|
+
if (spec.supports(modelRef, options)) return spec.load(options);
|
|
53
84
|
}
|
|
54
85
|
throw new Error(`unsupported sdk: ${modelRef?.sdk || "unknown"}`);
|
|
55
86
|
}
|
package/src/ai/runtime/router.js
CHANGED
|
@@ -24,25 +24,74 @@
|
|
|
24
24
|
// Result:
|
|
25
25
|
// The success run's result, with `failoverHistory` appended describing every
|
|
26
26
|
// prior attempt: [{ model, failureKind, requestId, retryableSubkind }].
|
|
27
|
-
// If every entry in the chain fails, returns the last
|
|
28
|
-
// `failureKind: "provider_unavailable_exhausted"`.
|
|
27
|
+
// If every eligible retryable/auth entry in the chain fails, returns the last
|
|
28
|
+
// result with `failureKind: "provider_unavailable_exhausted"`. Terminal
|
|
29
|
+
// non-retryable failures are returned as-is with their failover history.
|
|
30
|
+
|
|
31
|
+
// @ts-check
|
|
29
32
|
|
|
30
33
|
import { createRuntime } from "../../runtime.js";
|
|
31
|
-
import { retryableProviderFailureInfo } from "../failure.js";
|
|
34
|
+
import { isProviderAuthFailureText, retryableProviderFailureInfo } from "../failure.js";
|
|
32
35
|
import { runtimeCapabilities } from "./capabilities.js";
|
|
33
36
|
import { buildTranscriptTailSnapshot, renderResumeSnapshot } from "../../agent/transcript.js";
|
|
37
|
+
import { resolveRuntimeBrand } from "../../runtime-brand.js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {import('../types.js').RuntimeModelRef} RuntimeModelRef
|
|
41
|
+
* @typedef {import('../types.js').AgentRuntimeHostOptions} AgentRuntimeHostOptions
|
|
42
|
+
* @typedef {import('../types.js').AgentRuntimeInstance} AgentRuntimeInstance
|
|
43
|
+
* @typedef {import('../types.js').RuntimeRunOptions} RuntimeRunOptions
|
|
44
|
+
* @typedef {import('../types.js').RuntimeResult} RuntimeResult
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {Object} RouterChainEntryInput
|
|
49
|
+
* A chain entry as accepted by createRouterRuntime: either the shorthand bare
|
|
50
|
+
* RuntimeModelRef, or the full `{model, executionMode?, requires?}` form.
|
|
51
|
+
* @property {RuntimeModelRef} model
|
|
52
|
+
* @property {string} [executionMode]
|
|
53
|
+
* @property {Object<string, *>} [requires]
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} RouterChainEntry
|
|
58
|
+
* @property {RuntimeModelRef} model
|
|
59
|
+
* @property {string|null} executionMode
|
|
60
|
+
* @property {Object<string, *>|null} requires
|
|
61
|
+
*/
|
|
34
62
|
|
|
63
|
+
/**
|
|
64
|
+
* @param {Object} [options]
|
|
65
|
+
* @param {AgentRuntimeHostOptions} [options.host]
|
|
66
|
+
* @param {ReadonlyArray<RuntimeModelRef|RouterChainEntryInput>} [options.chain]
|
|
67
|
+
* @returns {AgentRuntimeInstance & {chain: () => Array<RouterChainEntry>}}
|
|
68
|
+
*/
|
|
35
69
|
export function createRouterRuntime({ host = {}, chain = [] } = {}) {
|
|
36
70
|
const entries = normaliseChain(chain);
|
|
37
71
|
if (entries.length === 0) {
|
|
38
72
|
throw new Error("createRouterRuntime requires a non-empty chain");
|
|
39
73
|
}
|
|
40
74
|
const inner = createRuntime(host);
|
|
75
|
+
// The router builds transcript-tail snapshots outside the inner runtime's
|
|
76
|
+
// bridge call (which is where the per-instance toolContext lives), so resolve
|
|
77
|
+
// the host brand here to stamp the snapshot schema id with the same brand the
|
|
78
|
+
// inner runtime uses — createRuntime no longer publishes it to a process global.
|
|
79
|
+
const runtimeBrand = resolveRuntimeBrand(host.runtimeBrand);
|
|
41
80
|
|
|
42
81
|
return {
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} systemPrompt
|
|
84
|
+
* @param {Partial<RuntimeRunOptions>} [options] Optional so a bare `{}` call
|
|
85
|
+
* is legal; the router always overrides `model`/`executionMode` per chain
|
|
86
|
+
* entry (see AgentRuntimeInstance.run for the public, model-required contract).
|
|
87
|
+
* @returns {Promise<RuntimeResult>}
|
|
88
|
+
*/
|
|
43
89
|
async run(systemPrompt, options = {}) {
|
|
90
|
+
/** @type {Array<{model: RuntimeModelRef, failureKind: (string|null), requestId?: (string|null|undefined), retryableSubkind?: (string|null|undefined), requirements?: (Object<string,*>|null)}>} */
|
|
44
91
|
const failoverHistory = [];
|
|
92
|
+
/** @type {RuntimeResult|null} */
|
|
45
93
|
let lastResult = null;
|
|
94
|
+
/** @type {*} */
|
|
46
95
|
let resumeSnapshot = null;
|
|
47
96
|
|
|
48
97
|
for (let i = 0; i < entries.length; i += 1) {
|
|
@@ -103,6 +152,8 @@ export function createRouterRuntime({ host = {}, chain = [] } = {}) {
|
|
|
103
152
|
};
|
|
104
153
|
}
|
|
105
154
|
|
|
155
|
+
result = normalizeProviderAuthFailure(result);
|
|
156
|
+
|
|
106
157
|
const retryability = retryableProviderFailureInfo({
|
|
107
158
|
errorText: result.error || "",
|
|
108
159
|
stderrTail: result.stderrTail || "",
|
|
@@ -129,15 +180,19 @@ export function createRouterRuntime({ host = {}, chain = [] } = {}) {
|
|
|
129
180
|
});
|
|
130
181
|
lastResult = result;
|
|
131
182
|
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
183
|
+
// Provider auth is terminal for one provider, but chain-retryable: a
|
|
184
|
+
// fallback provider may have working credentials. Other non-retryable
|
|
185
|
+
// provider/request errors remain terminal.
|
|
186
|
+
const shouldFallback = (retryability.retryable || result.failureKind === "provider_auth")
|
|
187
|
+
&& !result.cancelled;
|
|
188
|
+
if (!shouldFallback) {
|
|
189
|
+
return { ...result, failoverHistory };
|
|
190
|
+
}
|
|
136
191
|
|
|
137
192
|
// Build a transcript-tail snapshot from this run's events so the
|
|
138
193
|
// next provider can continue. If the run produced no usable events,
|
|
139
194
|
// skip the snapshot (the next attempt starts fresh).
|
|
140
|
-
const snapshot = buildTranscriptTailSnapshot(result.events);
|
|
195
|
+
const snapshot = buildTranscriptTailSnapshot(result.events, { runtimeBrand });
|
|
141
196
|
if (snapshot) resumeSnapshot = snapshot;
|
|
142
197
|
}
|
|
143
198
|
|
|
@@ -145,13 +200,13 @@ export function createRouterRuntime({ host = {}, chain = [] } = {}) {
|
|
|
145
200
|
text: null,
|
|
146
201
|
events: [],
|
|
147
202
|
error: "router chain exhausted with no executions",
|
|
148
|
-
failureKind: "
|
|
203
|
+
failureKind: "skipped_capability_mismatch",
|
|
149
204
|
cancelled: false,
|
|
150
205
|
usage: {},
|
|
151
206
|
};
|
|
152
207
|
return {
|
|
153
208
|
...exhaustedResult,
|
|
154
|
-
failureKind: "provider_unavailable_exhausted",
|
|
209
|
+
failureKind: lastResult ? "provider_unavailable_exhausted" : exhaustedResult.failureKind,
|
|
155
210
|
failoverHistory,
|
|
156
211
|
};
|
|
157
212
|
},
|
|
@@ -168,9 +223,15 @@ export function createRouterRuntime({ host = {}, chain = [] } = {}) {
|
|
|
168
223
|
};
|
|
169
224
|
}
|
|
170
225
|
|
|
226
|
+
/**
|
|
227
|
+
* @param {ReadonlyArray<*>} chain ReadonlyArray<RuntimeModelRef|RouterChainEntryInput>, loosened
|
|
228
|
+
* here because distinguishing the two shapes is a runtime duck-type check
|
|
229
|
+
* (`entry.sdk && entry.model`), not something a union type narrows cleanly.
|
|
230
|
+
* @returns {Array<RouterChainEntry>}
|
|
231
|
+
*/
|
|
171
232
|
function normaliseChain(chain) {
|
|
172
233
|
if (!Array.isArray(chain)) return [];
|
|
173
|
-
return chain
|
|
234
|
+
return /** @type {Array<RouterChainEntry>} */ (chain
|
|
174
235
|
.map((entry) => {
|
|
175
236
|
if (!entry) return null;
|
|
176
237
|
if (entry.sdk && entry.model) {
|
|
@@ -186,29 +247,68 @@ function normaliseChain(chain) {
|
|
|
186
247
|
}
|
|
187
248
|
return null;
|
|
188
249
|
})
|
|
189
|
-
.filter(Boolean);
|
|
250
|
+
.filter(Boolean));
|
|
190
251
|
}
|
|
191
252
|
|
|
253
|
+
/**
|
|
254
|
+
* @param {RouterChainEntry} entry
|
|
255
|
+
* @param {Partial<RuntimeRunOptions>} options
|
|
256
|
+
* @returns {boolean}
|
|
257
|
+
*/
|
|
192
258
|
function entrySatisfiesRequirements(entry, options) {
|
|
193
259
|
const requires = entry.requires;
|
|
194
|
-
|
|
260
|
+
// Synthesize effective requirements: merge the entry's own `requires` with
|
|
261
|
+
// requirements inferred from per-run options, so a chain entry that carries
|
|
262
|
+
// no explicit `requires` (the agent-host + runtime-adapter paths cannot carry
|
|
263
|
+
// one today) still respects option-implied capability needs. Each option
|
|
264
|
+
// inference defers to an explicit entry pin, never overriding it.
|
|
265
|
+
const effectiveRequires = { ...(requires || null) };
|
|
266
|
+
// Honour request-time outputSchema → require structured_output unless the
|
|
267
|
+
// entry already pins it.
|
|
268
|
+
if (options.outputSchema && effectiveRequires.structured_output === undefined) {
|
|
269
|
+
effectiveRequires.structured_output = true;
|
|
270
|
+
}
|
|
271
|
+
// Honour native-subagent teammates → require supports_native_subagents unless
|
|
272
|
+
// the entry already pins it. A pi entry (supports_native_subagents:false) then
|
|
273
|
+
// fails here rather than silently dropping the teammates.
|
|
274
|
+
if (
|
|
275
|
+
Array.isArray(options.nativeSubagents?.teammates)
|
|
276
|
+
&& options.nativeSubagents.teammates.length > 0
|
|
277
|
+
&& effectiveRequires.supports_native_subagents === undefined
|
|
278
|
+
) {
|
|
279
|
+
effectiveRequires.supports_native_subagents = true;
|
|
280
|
+
}
|
|
281
|
+
if (Object.keys(effectiveRequires).length === 0) return true;
|
|
195
282
|
let caps;
|
|
196
283
|
try {
|
|
197
284
|
caps = runtimeCapabilities(entry.model);
|
|
198
285
|
} catch {
|
|
199
286
|
return false;
|
|
200
287
|
}
|
|
201
|
-
for (const [key, expected] of Object.entries(
|
|
288
|
+
for (const [key, expected] of Object.entries(effectiveRequires)) {
|
|
202
289
|
if (caps[key] !== expected) return false;
|
|
203
290
|
}
|
|
204
|
-
// Honour request-time outputSchema → require structured_output unless
|
|
205
|
-
// already specified.
|
|
206
|
-
if (options.outputSchema && requires.structured_output !== false && caps.structured_output === false) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
291
|
return true;
|
|
210
292
|
}
|
|
211
293
|
|
|
294
|
+
/**
|
|
295
|
+
* @param {Partial<RuntimeRunOptions>} callOptions
|
|
296
|
+
* @param {import('../types.js').RuntimeEvent} event
|
|
297
|
+
* @returns {void}
|
|
298
|
+
*/
|
|
212
299
|
function emit(callOptions, event) {
|
|
213
300
|
try { callOptions.onEvent?.(event); } catch { /* swallow */ }
|
|
214
301
|
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @param {RuntimeResult} result
|
|
305
|
+
* @returns {RuntimeResult}
|
|
306
|
+
*/
|
|
307
|
+
function normalizeProviderAuthFailure(result) {
|
|
308
|
+
if (result.cancelled) return result;
|
|
309
|
+
const failureKind = result.failureKind || null;
|
|
310
|
+
if (failureKind && failureKind !== "provider_unavailable") return result;
|
|
311
|
+
const haystack = `${result.error || ""}\n${result.stderrTail || ""}`;
|
|
312
|
+
if (!isProviderAuthFailureText(haystack)) return result;
|
|
313
|
+
return { ...result, failureKind: "provider_auth" };
|
|
314
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Synchronous session-liveness primitives over a createSessionRegistry.
|
|
3
|
+
//
|
|
4
|
+
// A provider bridge owns session DURABILITY (pi keeps a Session transcript,
|
|
5
|
+
// codex keeps an app-server thread); this kernel layer owns session LIVENESS —
|
|
6
|
+
// the concurrency claim that keeps two turns from driving one session at once.
|
|
7
|
+
// The primitives here are deliberately SYNCHRONOUS: each does its registry
|
|
8
|
+
// get -> check -> set in a single await-free span, so the "busy claim must be
|
|
9
|
+
// await-free" invariant (I1) and the create-on-miss reservation race (R8) are
|
|
10
|
+
// enforced by construction rather than by careful inline sequencing.
|
|
11
|
+
//
|
|
12
|
+
// The registry stays the storage + idle-TTL + dispose fan-out layer (its
|
|
13
|
+
// exports are unchanged, worklab-compatible); liveness only adds the claim /
|
|
14
|
+
// reserve / release / adoptIfPresent seam that pi-native's session-lifecycle and
|
|
15
|
+
// codex-app both consume.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} SessionRegistryLike
|
|
19
|
+
* @property {(id: string) => any} get
|
|
20
|
+
* @property {(id: string, value: any, opts?: {idleTimeoutMs?: number}) => void} set
|
|
21
|
+
* @property {(id: string) => boolean} delete
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {{ok: true, entry: any} | {ok: false, reason: "busy" | "missing"}} ClaimResult
|
|
26
|
+
* @typedef {{ok: true, id: string, release: () => void, commit: (entry: any) => void} | {ok: false, entry: any}} ReserveResult
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} SessionLiveness
|
|
31
|
+
* @property {(id: string) => ClaimResult} claim
|
|
32
|
+
* @property {(id: string) => any} adoptIfPresent
|
|
33
|
+
* @property {(id: string, seed: any, ttl: number|undefined) => ReserveResult} reserve
|
|
34
|
+
* @property {(id: string) => void} release
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wrap a session registry with the synchronous liveness primitives. The
|
|
39
|
+
* explicit return typedef pins the claim/reserve discriminated unions so
|
|
40
|
+
* callers narrow on `ok` (object-literal inference would otherwise widen `ok`
|
|
41
|
+
* to boolean and break narrowing).
|
|
42
|
+
* @param {SessionRegistryLike} registry
|
|
43
|
+
* @returns {SessionLiveness}
|
|
44
|
+
*/
|
|
45
|
+
export function createSessionLiveness(registry) {
|
|
46
|
+
return {
|
|
47
|
+
/**
|
|
48
|
+
* Claim a live entry for a turn. The registry get -> busy check -> set-busy
|
|
49
|
+
* span is await-free (I1): a concurrent claim of a busy entry loses and is
|
|
50
|
+
* told `busy`, never adopting the session mid-turn.
|
|
51
|
+
* @param {string} id
|
|
52
|
+
* @returns {{ok: true, entry: any} | {ok: false, reason: "busy" | "missing"}}
|
|
53
|
+
*/
|
|
54
|
+
claim(id) {
|
|
55
|
+
const entry = registry.get(id);
|
|
56
|
+
if (!entry) return { ok: false, reason: "missing" };
|
|
57
|
+
if (entry.busy) return { ok: false, reason: "busy" };
|
|
58
|
+
entry.busy = true;
|
|
59
|
+
return { ok: true, entry };
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Re-read the registry after an awaited step (F4): a second concurrent cold
|
|
64
|
+
* resume could have reopened+inserted its own entry in the await window, so
|
|
65
|
+
* the caller adopts whatever is present (possibly busy) instead of racing a
|
|
66
|
+
* second insert. Returns null when nothing is present.
|
|
67
|
+
* @param {string} id
|
|
68
|
+
* @returns {any | null}
|
|
69
|
+
*/
|
|
70
|
+
adoptIfPresent(id) {
|
|
71
|
+
return registry.get(id) ?? null;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reserve an id with a BUSY placeholder before an awaited create (R8). The
|
|
76
|
+
* get -> check -> set span is await-free, so a second concurrent first turn
|
|
77
|
+
* observes the placeholder (busy) and loses the reservation — exactly one
|
|
78
|
+
* create per durable id. On loss the concurrent entry is returned so the
|
|
79
|
+
* caller can fall into the busy-claim path.
|
|
80
|
+
* @param {string} id
|
|
81
|
+
* @param {any} seed BUSY placeholder entry to insert.
|
|
82
|
+
* @param {number|undefined} ttl per-entry idle timeout.
|
|
83
|
+
* @returns {{ok: true, id: string, release: () => void, commit: (entry: any) => void} | {ok: false, entry: any}}
|
|
84
|
+
*/
|
|
85
|
+
reserve(id, seed, ttl) {
|
|
86
|
+
const concurrent = registry.get(id);
|
|
87
|
+
if (concurrent) return { ok: false, entry: concurrent };
|
|
88
|
+
registry.set(id, seed, { idleTimeoutMs: ttl });
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
id,
|
|
92
|
+
// Drop the placeholder (drop / error / abort paths). delete() removes
|
|
93
|
+
// without running onEvict, matching the raw registry.delete it replaces.
|
|
94
|
+
release: () => { registry.delete(id); },
|
|
95
|
+
// Overwrite the placeholder with the finalized entry on the keep-alive
|
|
96
|
+
// success path (same id, same ttl).
|
|
97
|
+
commit: (entry) => { registry.set(id, entry, { idleTimeoutMs: ttl }); },
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Release a session's liveness (remove without running onEvict — the caller
|
|
103
|
+
* has already cleaned up, or is dropping an uncommitted session).
|
|
104
|
+
* @param {string} id
|
|
105
|
+
*/
|
|
106
|
+
release(id) {
|
|
107
|
+
registry.delete(id);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Provider session registries.
|
|
2
2
|
//
|
|
3
3
|
// Bridges that support continuous provider sessions (codex-app keeps the
|
|
4
|
-
// app-server subprocess + thread alive, pi-
|
|
5
|
-
// register their live sessions here, keyed by provider session id.
|
|
4
|
+
// app-server subprocess + thread alive, pi-native keeps a pi Session
|
|
5
|
+
// transcript) register their live sessions here, keyed by provider session id.
|
|
6
|
+
// The host
|
|
6
7
|
// owns session lifetime policy (which conversation maps to which session,
|
|
7
8
|
// when to resume, when to retire); these registries only make sure nothing
|
|
8
9
|
// leaks if the host forgets: every entry carries an idle TTL backstop with an
|
|
@@ -24,6 +25,13 @@ function normalizeTtl(value, fallback) {
|
|
|
24
25
|
return n;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} [options]
|
|
30
|
+
* @param {number} [options.idleTimeoutMs]
|
|
31
|
+
* @param {(value: any, reason: string) => (void | Promise<void>)} [options.onEvict]
|
|
32
|
+
* @param {() => number} [options.now]
|
|
33
|
+
* @param {(value: any) => boolean} [options.isBusy]
|
|
34
|
+
*/
|
|
27
35
|
export function createSessionRegistry({ idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS, onEvict, now = Date.now, isBusy } = {}) {
|
|
28
36
|
const entries = new Map();
|
|
29
37
|
const defaultTtlMs = normalizeTtl(idleTimeoutMs, DEFAULT_IDLE_TIMEOUT_MS);
|
|
@@ -69,6 +77,11 @@ export function createSessionRegistry({ idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS,
|
|
|
69
77
|
}
|
|
70
78
|
return entry.value;
|
|
71
79
|
},
|
|
80
|
+
/**
|
|
81
|
+
* @param {any} id
|
|
82
|
+
* @param {any} value
|
|
83
|
+
* @param {{idleTimeoutMs?: number}} [options]
|
|
84
|
+
*/
|
|
72
85
|
set(id, value, { idleTimeoutMs: entryTtl } = {}) {
|
|
73
86
|
const previous = entries.get(id);
|
|
74
87
|
if (previous) clearTimeout(previous.timer);
|
|
@@ -76,6 +89,10 @@ export function createSessionRegistry({ idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS,
|
|
|
76
89
|
entries.set(id, entry);
|
|
77
90
|
armTimer(id, entry);
|
|
78
91
|
},
|
|
92
|
+
/**
|
|
93
|
+
* @param {any} id
|
|
94
|
+
* @param {{idleTimeoutMs?: number}} [options]
|
|
95
|
+
*/
|
|
79
96
|
touch(id, { idleTimeoutMs: entryTtl } = {}) {
|
|
80
97
|
const entry = entries.get(id);
|
|
81
98
|
if (!entry) return;
|