@mono-agent/agent-runtime 0.4.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 +37 -16
- package/MIGRATION.md +185 -7
- package/README.md +24 -14
- package/package.json +103 -10
- package/src/agent/allowlists.js +3 -0
- package/src/agent/approval.js +3 -0
- package/src/agent/compaction.js +244 -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 +227 -45
- 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 +21 -10
- 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/live-input-prompt.js +11 -1
- package/src/ai/providers/claude-cli.js +2 -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-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 +415 -1106
- package/src/ai/runtime/model-refs.js +30 -0
- package/src/ai/runtime/registry.js +29 -0
- package/src/ai/runtime/router.js +96 -12
- package/src/ai/runtime/session-liveness.js +110 -0
- package/src/ai/runtime/sessions.js +16 -0
- package/src/ai/types.js +252 -19
- package/src/index.js +1 -1
- package/src/pi-auth.js +147 -16
- package/src/runtime-brand.js +21 -1
- package/src/runtime.js +72 -8
- 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 -35
|
@@ -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",
|
|
@@ -41,6 +62,9 @@ const builtinBridgeSpecs = {
|
|
|
41
62
|
},
|
|
42
63
|
};
|
|
43
64
|
|
|
65
|
+
/**
|
|
66
|
+
* @returns {Array<RuntimeBridgeDescriptor>}
|
|
67
|
+
*/
|
|
44
68
|
export function listRuntimeBridges() {
|
|
45
69
|
return Object.values(builtinBridgeSpecs).map((bridge) => ({
|
|
46
70
|
id: bridge.id,
|
|
@@ -49,6 +73,11 @@ export function listRuntimeBridges() {
|
|
|
49
73
|
}));
|
|
50
74
|
}
|
|
51
75
|
|
|
76
|
+
/**
|
|
77
|
+
* @param {import('../types.js').RuntimeModelRef} modelRef
|
|
78
|
+
* @param {Object} [options]
|
|
79
|
+
* @returns {Promise<RuntimeBridge>}
|
|
80
|
+
*/
|
|
52
81
|
export async function resolveRuntimeBridge(modelRef, options = {}) {
|
|
53
82
|
for (const spec of Object.values(builtinBridgeSpecs)) {
|
|
54
83
|
if (spec.supports(modelRef, options)) return spec.load(options);
|
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
|
+
*/
|
|
34
55
|
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} RouterChainEntry
|
|
58
|
+
* @property {RuntimeModelRef} model
|
|
59
|
+
* @property {string|null} executionMode
|
|
60
|
+
* @property {Object<string, *>|null} requires
|
|
61
|
+
*/
|
|
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,9 +247,14 @@ 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
|
|
@@ -225,6 +291,24 @@ function entrySatisfiesRequirements(entry, options) {
|
|
|
225
291
|
return true;
|
|
226
292
|
}
|
|
227
293
|
|
|
294
|
+
/**
|
|
295
|
+
* @param {Partial<RuntimeRunOptions>} callOptions
|
|
296
|
+
* @param {import('../types.js').RuntimeEvent} event
|
|
297
|
+
* @returns {void}
|
|
298
|
+
*/
|
|
228
299
|
function emit(callOptions, event) {
|
|
229
300
|
try { callOptions.onEvent?.(event); } catch { /* swallow */ }
|
|
230
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
|
+
}
|
|
@@ -25,6 +25,13 @@ function normalizeTtl(value, fallback) {
|
|
|
25
25
|
return n;
|
|
26
26
|
}
|
|
27
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
|
+
*/
|
|
28
35
|
export function createSessionRegistry({ idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS, onEvict, now = Date.now, isBusy } = {}) {
|
|
29
36
|
const entries = new Map();
|
|
30
37
|
const defaultTtlMs = normalizeTtl(idleTimeoutMs, DEFAULT_IDLE_TIMEOUT_MS);
|
|
@@ -70,6 +77,11 @@ export function createSessionRegistry({ idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS,
|
|
|
70
77
|
}
|
|
71
78
|
return entry.value;
|
|
72
79
|
},
|
|
80
|
+
/**
|
|
81
|
+
* @param {any} id
|
|
82
|
+
* @param {any} value
|
|
83
|
+
* @param {{idleTimeoutMs?: number}} [options]
|
|
84
|
+
*/
|
|
73
85
|
set(id, value, { idleTimeoutMs: entryTtl } = {}) {
|
|
74
86
|
const previous = entries.get(id);
|
|
75
87
|
if (previous) clearTimeout(previous.timer);
|
|
@@ -77,6 +89,10 @@ export function createSessionRegistry({ idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS,
|
|
|
77
89
|
entries.set(id, entry);
|
|
78
90
|
armTimer(id, entry);
|
|
79
91
|
},
|
|
92
|
+
/**
|
|
93
|
+
* @param {any} id
|
|
94
|
+
* @param {{idleTimeoutMs?: number}} [options]
|
|
95
|
+
*/
|
|
80
96
|
touch(id, { idleTimeoutMs: entryTtl } = {}) {
|
|
81
97
|
const entry = entries.get(id);
|
|
82
98
|
if (!entry) return;
|