@mono-agent/agent-runtime 0.15.0 → 0.15.2
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 +163 -67
- package/README.md +67 -2
- package/package.json +1 -1
- package/src/agent/compaction.js +0 -11
- package/src/agent/prompt/skill-index.js +5 -1
- package/src/ai/index.js +7 -0
- package/src/ai/pi-interop.js +156 -0
- package/src/ai/providers/claude-cli.js +8 -7
- package/src/ai/providers/claude-sdk.js +2 -1
- package/src/ai/providers/codex-app.js +49 -20
- package/src/ai/providers/opencode-app.js +7 -6
- package/src/ai/runtime/capabilities.js +8 -0
- package/src/ai/runtime/live-input-events.js +94 -0
- package/src/ai/runtime/router.js +21 -0
- package/src/ai/runtime/tool-policy.js +22 -0
- package/src/ai/types.js +11 -1
- package/src/runtime.js +3 -0
- package/types/agent/compaction.d.ts +0 -2
- package/types/agent/prompt/skill-index.d.ts +3 -0
- package/types/ai/index.d.ts +1 -0
- package/types/ai/pi-interop.d.ts +113 -0
- package/types/ai/providers/claude-cli.d.ts +2 -0
- package/types/ai/providers/codex-app.d.ts +2 -0
- package/types/ai/providers/opencode-app.d.ts +2 -0
- package/types/ai/runtime/capabilities.d.ts +15 -10
- package/types/ai/runtime/live-input-events.d.ts +22 -0
- package/types/ai/runtime/tool-policy.d.ts +15 -0
- package/types/ai/types.d.ts +27 -1
- package/types/ai/backend.d.ts +0 -57
- package/types/ai/registry.d.ts +0 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Runtime-owned interoperability facade for Pi's built-in model and OAuth
|
|
2
|
+
// surfaces. Consumers should use these functions instead of importing pi-ai
|
|
3
|
+
// directly so the runtime's known-good Pi version remains authoritative.
|
|
4
|
+
|
|
5
|
+
import { getBuiltinModel, getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
|
|
6
|
+
import { getOAuthApiKey, getOAuthProvider } from "@earendil-works/pi-ai/oauth";
|
|
7
|
+
import { reasoningLevelsForPiModel as resolveReasoningLevels } from "./providers/pi-models.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* id: string,
|
|
12
|
+
* name: string,
|
|
13
|
+
* api: string,
|
|
14
|
+
* provider: string,
|
|
15
|
+
* baseUrl: string,
|
|
16
|
+
* reasoning: boolean,
|
|
17
|
+
* input: Array<"text"|"image">,
|
|
18
|
+
* cost: {
|
|
19
|
+
* input: number,
|
|
20
|
+
* output: number,
|
|
21
|
+
* cacheRead: number,
|
|
22
|
+
* cacheWrite: number,
|
|
23
|
+
* tiers?: Array<{
|
|
24
|
+
* inputTokensAbove: number,
|
|
25
|
+
* input: number,
|
|
26
|
+
* output: number,
|
|
27
|
+
* cacheRead: number,
|
|
28
|
+
* cacheWrite: number
|
|
29
|
+
* }>
|
|
30
|
+
* },
|
|
31
|
+
* contextWindow: number,
|
|
32
|
+
* maxTokens: number,
|
|
33
|
+
* thinkingLevelMap?: Object<string, string|null>,
|
|
34
|
+
* compat?: Object<string, *>,
|
|
35
|
+
* headers?: Object<string, string>,
|
|
36
|
+
* [key: string]: *
|
|
37
|
+
* }} PiBuiltinModelSnapshot
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {"none"|"minimal"|"low"|"medium"|"high"|"xhigh"|"max"} PiReasoningLevel
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {{
|
|
46
|
+
* refresh: string,
|
|
47
|
+
* access: string,
|
|
48
|
+
* expires: number,
|
|
49
|
+
* [key: string]: *
|
|
50
|
+
* }} PiOAuthCredentialsSnapshot
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Object} PiOAuthLoginCallbacks
|
|
55
|
+
* @property {(info: {url: string, instructions?: string}) => void} onAuth
|
|
56
|
+
* @property {(info: {userCode: string, verificationUri: string, intervalSeconds?: number, expiresInSeconds?: number}) => void} onDeviceCode
|
|
57
|
+
* @property {(prompt: {message: string, placeholder?: string, allowEmpty?: boolean}) => Promise<string>} onPrompt
|
|
58
|
+
* @property {(message: string) => void} [onProgress]
|
|
59
|
+
* @property {() => Promise<string>} [onManualCodeInput]
|
|
60
|
+
* @property {(prompt: {message: string, options: Array<{id: string, label: string}>}) => Promise<string|undefined>} onSelect
|
|
61
|
+
* @property {AbortSignal} [signal]
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clone provider-owned data before it crosses the public runtime boundary.
|
|
66
|
+
* Pi's built-in models and OAuth credentials are structured data on the
|
|
67
|
+
* supported version, and the package requires a Node release with
|
|
68
|
+
* `structuredClone`.
|
|
69
|
+
*
|
|
70
|
+
* @template T
|
|
71
|
+
* @param {T} value
|
|
72
|
+
* @returns {T}
|
|
73
|
+
*/
|
|
74
|
+
function cloneInteropValue(value) {
|
|
75
|
+
return structuredClone(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* List defensive snapshots of Pi's built-in models for one provider.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} providerId
|
|
82
|
+
* @returns {PiBuiltinModelSnapshot[]}
|
|
83
|
+
*/
|
|
84
|
+
export function listPiBuiltinModels(providerId) {
|
|
85
|
+
const models = getBuiltinModels(/** @type {any} */ (providerId));
|
|
86
|
+
return /** @type {PiBuiltinModelSnapshot[]} */ (cloneInteropValue(models));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read a defensive snapshot of one Pi built-in model.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} providerId
|
|
93
|
+
* @param {string} modelId
|
|
94
|
+
* @returns {PiBuiltinModelSnapshot|undefined}
|
|
95
|
+
*/
|
|
96
|
+
export function getPiBuiltinModel(providerId, modelId) {
|
|
97
|
+
const model = getBuiltinModel(
|
|
98
|
+
/** @type {any} */ (providerId),
|
|
99
|
+
/** @type {any} */ (modelId),
|
|
100
|
+
);
|
|
101
|
+
return model === undefined
|
|
102
|
+
? undefined
|
|
103
|
+
: /** @type {PiBuiltinModelSnapshot} */ (cloneInteropValue(model));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Translate Pi's model-native thinking levels to mono-agent effort spelling.
|
|
108
|
+
*
|
|
109
|
+
* @param {PiBuiltinModelSnapshot} model
|
|
110
|
+
* @returns {PiReasoningLevel[]}
|
|
111
|
+
*/
|
|
112
|
+
export function reasoningLevelsForPiModel(model) {
|
|
113
|
+
return /** @type {PiReasoningLevel[]} */ (resolveReasoningLevels(model));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve an OAuth-backed API key without allowing Pi to mutate the caller's
|
|
118
|
+
* credential record or returning Pi-owned credential objects.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} providerId
|
|
121
|
+
* @param {Object<string, PiOAuthCredentialsSnapshot>} credentials
|
|
122
|
+
* @returns {Promise<{apiKey: string, newCredentials: PiOAuthCredentialsSnapshot}|null>}
|
|
123
|
+
*/
|
|
124
|
+
export async function resolvePiOAuthApiKey(providerId, credentials) {
|
|
125
|
+
const result = await getOAuthApiKey(
|
|
126
|
+
providerId,
|
|
127
|
+
/** @type {any} */ (cloneInteropValue(credentials)),
|
|
128
|
+
);
|
|
129
|
+
if (!result) return null;
|
|
130
|
+
return {
|
|
131
|
+
apiKey: result.apiKey,
|
|
132
|
+
newCredentials: cloneInteropValue(result.newCredentials),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Run a supported Pi OAuth login flow without exposing Pi's mutable provider
|
|
138
|
+
* registry or provider instances.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} providerId
|
|
141
|
+
* @param {PiOAuthLoginCallbacks} callbacks
|
|
142
|
+
* @returns {Promise<PiOAuthCredentialsSnapshot>}
|
|
143
|
+
*/
|
|
144
|
+
export async function loginPiOAuth(providerId, callbacks) {
|
|
145
|
+
const provider = getOAuthProvider(providerId);
|
|
146
|
+
if (!provider || typeof provider.login !== "function") {
|
|
147
|
+
throw new Error(`Pi OAuth provider is unavailable: ${providerId}`);
|
|
148
|
+
}
|
|
149
|
+
for (const callbackName of ["onAuth", "onDeviceCode", "onPrompt", "onSelect"]) {
|
|
150
|
+
if (typeof callbacks?.[callbackName] !== "function") {
|
|
151
|
+
throw new TypeError(`loginPiOAuth requires callbacks.${callbackName}()`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const credentials = await provider.login(/** @type {any} */ ({ ...callbacks }));
|
|
155
|
+
return cloneInteropValue(credentials);
|
|
156
|
+
}
|
|
@@ -11,6 +11,10 @@ import { createStderrTail } from "../failure.js";
|
|
|
11
11
|
import { modelWithContextWindow } from "../runtime/context-windows.js";
|
|
12
12
|
import { readRuntimeBrand } from "../../agent/tools/shared/runtime-context.js";
|
|
13
13
|
import { buildCapabilitiesUsed } from "../runtime/capabilities-used.js";
|
|
14
|
+
import {
|
|
15
|
+
isAllowAllToolPolicy,
|
|
16
|
+
TOOL_POLICY_PROJECTED,
|
|
17
|
+
} from "../runtime/tool-policy.js";
|
|
14
18
|
import {
|
|
15
19
|
claudeNativeAgentDefinitions,
|
|
16
20
|
resolveClaudeAllowedTools,
|
|
@@ -28,13 +32,9 @@ const CLAUDE_CLI_EMPTY_TOOL_POLICY_UNSUPPORTED =
|
|
|
28
32
|
"Claude Code CLI cannot enforce an explicit empty allowedTools list: omitting --tools would restore Claude Code's default toolset. Use a specific non-empty allowlist, a denylist, or the Claude SDK for a no-tools run.";
|
|
29
33
|
|
|
30
34
|
function codexCliToolPolicyProblem(options) {
|
|
31
|
-
|
|
32
|
-
const disallowedTools = Array.isArray(options.disallowedTools) ? options.disallowedTools : [];
|
|
33
|
-
const exactAllowAll = allowedTools === null
|
|
34
|
-
|| (allowedTools.length === 1 && allowedTools[0] === "*");
|
|
35
|
-
return exactAllowAll && disallowedTools.length === 0
|
|
35
|
+
return isAllowAllToolPolicy(options.allowedTools, options.disallowedTools)
|
|
36
36
|
? null
|
|
37
|
-
: "Direct Codex CLI cannot enforce allowedTools/disallowedTools. Use
|
|
37
|
+
: "Direct Codex CLI cannot enforce allowedTools/disallowedTools. Use allow-all-only (omit allowedTools or include \"*\", with no disallowedTools) or another runtime.";
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function codexCliCapabilityMismatchResult(options, error, codexErrorCode, start) {
|
|
@@ -86,6 +86,7 @@ const DORMANT_CLI_CAPABILITIES = {
|
|
|
86
86
|
// The one-shot CLI bridge cannot add stdin messages after process launch.
|
|
87
87
|
supports_live_input: false,
|
|
88
88
|
supports_native_subagents: true,
|
|
89
|
+
tool_policy: TOOL_POLICY_PROJECTED,
|
|
89
90
|
};
|
|
90
91
|
|
|
91
92
|
function promptFromMessages(messages) {
|
|
@@ -465,7 +466,7 @@ export function buildCliCommand({
|
|
|
465
466
|
args.push("--tools", cliAllowedTools.join(","));
|
|
466
467
|
}
|
|
467
468
|
const autoAllowed = [
|
|
468
|
-
...(Array.isArray(cliAllowedTools) ? cliAllowedTools : []),
|
|
469
|
+
...(!cliAllowAll && Array.isArray(cliAllowedTools) ? cliAllowedTools : []),
|
|
469
470
|
...Object.keys(mcpServers || {}).map((name) => `mcp__${name}__*`),
|
|
470
471
|
];
|
|
471
472
|
if (autoAllowed.length) args.push("--allowedTools", shellList(autoAllowed));
|
|
@@ -748,6 +748,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
748
748
|
const prompt = options.liveInput
|
|
749
749
|
? livePromptMessages({ initialPrompt: promptString, liveInput: options.liveInput, sessionId: reusableProviderSessionId || randomUUID(), prompts: options.prompts })
|
|
750
750
|
: promptString;
|
|
751
|
+
const claudeAgentQuery = options.claudeAgentQuery ?? query;
|
|
751
752
|
const providerRequestStartedAt = Date.now();
|
|
752
753
|
emitEvent({
|
|
753
754
|
type: "provider_request_started",
|
|
@@ -805,7 +806,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
805
806
|
}
|
|
806
807
|
|
|
807
808
|
try {
|
|
808
|
-
stream =
|
|
809
|
+
stream = claudeAgentQuery({ prompt: /** @type {any} */ (prompt), options: queryOptions });
|
|
809
810
|
for await (const event of stream) {
|
|
810
811
|
const nextSessionId = sessionIdFromEvent(event);
|
|
811
812
|
if (nextSessionId) providerSessionId = nextSessionId;
|
|
@@ -10,6 +10,10 @@ import { buildCapabilitiesUsed } from "../runtime/capabilities-used.js";
|
|
|
10
10
|
import { resolveSandboxPolicy } from "../../agent/tools/shared/tool-context.js";
|
|
11
11
|
import { createSessionRegistry } from "../runtime/sessions.js";
|
|
12
12
|
import { createSessionLiveness } from "../runtime/session-liveness.js";
|
|
13
|
+
import {
|
|
14
|
+
isAllowAllToolPolicy,
|
|
15
|
+
TOOL_POLICY_ALLOW_ALL_ONLY,
|
|
16
|
+
} from "../runtime/tool-policy.js";
|
|
13
17
|
|
|
14
18
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
15
19
|
const DEFAULT_THREAD_START_ATTEMPTS = 2;
|
|
@@ -448,6 +452,7 @@ const CODEX_APP_CAPABILITIES = {
|
|
|
448
452
|
supports_live_input: true,
|
|
449
453
|
supports_native_subagents: true,
|
|
450
454
|
supports_fast_mode: true,
|
|
455
|
+
tool_policy: TOOL_POLICY_ALLOW_ALL_ONLY,
|
|
451
456
|
};
|
|
452
457
|
|
|
453
458
|
function promptFromMessages(messages) {
|
|
@@ -558,14 +563,9 @@ function codexToolPolicyProblem(options) {
|
|
|
558
563
|
}
|
|
559
564
|
return "Codex no-tool probe mode requires an empty tool policy, no MCP servers, and a disposable session.";
|
|
560
565
|
}
|
|
561
|
-
|
|
562
|
-
// Once a caller specifies a policy, require the exact wildcard contract.
|
|
563
|
-
// Extra entries can conceal a caller's mistaken belief that Codex enforces a
|
|
564
|
-
// mixed allowlist, which the app-server cannot project.
|
|
565
|
-
const effectiveAllowAll = allowedTools === null || (allowedTools.length === 1 && allowedTools[0] === "*");
|
|
566
|
-
return effectiveAllowAll && disallowedTools.length === 0
|
|
566
|
+
return isAllowAllToolPolicy(allowedTools, disallowedTools)
|
|
567
567
|
? null
|
|
568
|
-
: "Direct Codex cannot enforce allowedTools/disallowedTools. Use
|
|
568
|
+
: "Direct Codex cannot enforce allowedTools/disallowedTools. Use allow-all-only (omit allowedTools or include \"*\", with no disallowedTools) or another runtime.";
|
|
569
569
|
}
|
|
570
570
|
|
|
571
571
|
const CODEX_NO_TOOL_ACTION_ITEMS = new Set([
|
|
@@ -1143,7 +1143,9 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1143
1143
|
let serverRequestViolation = null;
|
|
1144
1144
|
let resolveTurn;
|
|
1145
1145
|
let resolveTurnReady;
|
|
1146
|
+
let resolveLiveInputStop;
|
|
1146
1147
|
let turnReadyResolved = false;
|
|
1148
|
+
let liveInputStopped = false;
|
|
1147
1149
|
const fileChangeSnapshots = new Map();
|
|
1148
1150
|
const codexItemContext = {
|
|
1149
1151
|
fileChangePayload: (raw) => createFileChangePayload(raw, {
|
|
@@ -1153,6 +1155,13 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1153
1155
|
};
|
|
1154
1156
|
const turnDone = new Promise((resolve) => { resolveTurn = resolve; });
|
|
1155
1157
|
const turnReady = new Promise((resolve) => { resolveTurnReady = resolve; });
|
|
1158
|
+
const liveInputStop = new Promise((resolve) => { resolveLiveInputStop = resolve; });
|
|
1159
|
+
|
|
1160
|
+
function stopLiveInput() {
|
|
1161
|
+
if (liveInputStopped) return;
|
|
1162
|
+
liveInputStopped = true;
|
|
1163
|
+
resolveLiveInputStop();
|
|
1164
|
+
}
|
|
1156
1165
|
|
|
1157
1166
|
function setActiveTurnId(turnId, { steerReady = false } = {}) {
|
|
1158
1167
|
activeTurnId = turnId || activeTurnId;
|
|
@@ -1175,6 +1184,21 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1175
1184
|
options.onEvent?.(safeEvent);
|
|
1176
1185
|
}
|
|
1177
1186
|
|
|
1187
|
+
function invokeLiveInputCallback(message, callbackName, ...args) {
|
|
1188
|
+
const callback = message?.[callbackName];
|
|
1189
|
+
if (typeof callback !== "function") return;
|
|
1190
|
+
try {
|
|
1191
|
+
callback.apply(message, args);
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
const detail = safeDiagnostic(err, 512);
|
|
1194
|
+
emitEvent({
|
|
1195
|
+
type: "runtime_warning",
|
|
1196
|
+
warning_kind: "live_input_callback_failed",
|
|
1197
|
+
message: safeDiagnostic(`Live-input ${callbackName} callback failed: ${detail}`, 1_024),
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1178
1202
|
const compactionTurnKey = (params = {}) => `${params.threadId || threadId || "thread"}:${params.turnId || activeTurnId || "turn"}`;
|
|
1179
1203
|
|
|
1180
1204
|
/**
|
|
@@ -1269,6 +1293,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1269
1293
|
client?.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => {});
|
|
1270
1294
|
}
|
|
1271
1295
|
turnCompleted = true;
|
|
1296
|
+
stopLiveInput();
|
|
1272
1297
|
resolveTurn({ id: activeTurnId, status: "interrupted" });
|
|
1273
1298
|
}
|
|
1274
1299
|
|
|
@@ -1293,6 +1318,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1293
1318
|
message: errorMessage,
|
|
1294
1319
|
});
|
|
1295
1320
|
turnCompleted = true;
|
|
1321
|
+
stopLiveInput();
|
|
1296
1322
|
resolveTurn({ id: activeTurnId, status: "interrupted" });
|
|
1297
1323
|
}
|
|
1298
1324
|
|
|
@@ -1323,6 +1349,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1323
1349
|
if (method === "turn/completed") {
|
|
1324
1350
|
setActiveTurnId(params.turn?.id);
|
|
1325
1351
|
turnCompleted = true;
|
|
1352
|
+
stopLiveInput();
|
|
1326
1353
|
if (params.turn?.status === "failed") {
|
|
1327
1354
|
errorMessage = safeDiagnostic(params.turn?.error?.message || params.turn?.error || "Codex turn failed");
|
|
1328
1355
|
failureKind = "provider_unavailable";
|
|
@@ -1482,6 +1509,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1482
1509
|
|
|
1483
1510
|
const abortHandler = () => {
|
|
1484
1511
|
abortRequested = true;
|
|
1512
|
+
stopLiveInput();
|
|
1485
1513
|
if (threadId && activeTurnId && !interruptSent) {
|
|
1486
1514
|
interruptSent = true;
|
|
1487
1515
|
client?.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => {});
|
|
@@ -1495,20 +1523,19 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1495
1523
|
if (!options.liveInput) return;
|
|
1496
1524
|
const iterator = options.liveInput[Symbol.asyncIterator]();
|
|
1497
1525
|
try {
|
|
1498
|
-
while (!turnCompleted) {
|
|
1526
|
+
while (!turnCompleted && !liveInputStopped) {
|
|
1499
1527
|
const next = await Promise.race([
|
|
1500
1528
|
iterator.next(),
|
|
1501
|
-
|
|
1529
|
+
liveInputStop.then(() => ({ done: true, value: undefined })),
|
|
1502
1530
|
]);
|
|
1503
|
-
if (next.done || turnCompleted) break;
|
|
1531
|
+
if (next.done || turnCompleted || liveInputStopped) break;
|
|
1504
1532
|
const message = next.value;
|
|
1505
1533
|
if (!threadId || !activeTurnId || !turnReadyResolved) {
|
|
1506
1534
|
await Promise.race([
|
|
1507
1535
|
turnReady,
|
|
1508
|
-
|
|
1509
|
-
client.closed.then((err) => { throw err; }),
|
|
1536
|
+
liveInputStop,
|
|
1510
1537
|
]);
|
|
1511
|
-
if (turnCompleted || !turnReadyResolved) break;
|
|
1538
|
+
if (turnCompleted || liveInputStopped || !turnReadyResolved) break;
|
|
1512
1539
|
}
|
|
1513
1540
|
const input = userTextInput(formatLiveInputGuidance(message.body, options.prompts));
|
|
1514
1541
|
try {
|
|
@@ -1518,16 +1545,15 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1518
1545
|
input,
|
|
1519
1546
|
});
|
|
1520
1547
|
activeTurnId = response?.turnId || activeTurnId;
|
|
1521
|
-
message
|
|
1548
|
+
invokeLiveInputCallback(message, "acknowledge");
|
|
1522
1549
|
} catch (err) {
|
|
1523
1550
|
const providerError = err?.responseError;
|
|
1524
1551
|
if (isNoActiveTurnToSteer(providerError || err)) {
|
|
1525
1552
|
await Promise.race([
|
|
1526
1553
|
turnReady,
|
|
1527
|
-
|
|
1528
|
-
client.closed.then((closedErr) => { throw closedErr; }),
|
|
1554
|
+
liveInputStop,
|
|
1529
1555
|
]);
|
|
1530
|
-
if (turnCompleted) break;
|
|
1556
|
+
if (turnCompleted || liveInputStopped) break;
|
|
1531
1557
|
try {
|
|
1532
1558
|
const response = await client.request("turn/steer", {
|
|
1533
1559
|
threadId,
|
|
@@ -1535,10 +1561,10 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1535
1561
|
input,
|
|
1536
1562
|
});
|
|
1537
1563
|
activeTurnId = response?.turnId || activeTurnId;
|
|
1538
|
-
message
|
|
1564
|
+
invokeLiveInputCallback(message, "acknowledge");
|
|
1539
1565
|
continue;
|
|
1540
1566
|
} catch (retryErr) {
|
|
1541
|
-
message
|
|
1567
|
+
invokeLiveInputCallback(message, "reject", retryErr);
|
|
1542
1568
|
const retryProviderError = retryErr?.responseError
|
|
1543
1569
|
? safeResponseError(retryErr.responseError)
|
|
1544
1570
|
: null;
|
|
@@ -1552,7 +1578,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1552
1578
|
break;
|
|
1553
1579
|
}
|
|
1554
1580
|
}
|
|
1555
|
-
message
|
|
1581
|
+
invokeLiveInputCallback(message, "reject", err);
|
|
1556
1582
|
emitEvent({
|
|
1557
1583
|
type: "runtime_warning",
|
|
1558
1584
|
warning_kind: isActiveTurnNotSteerable(providerError) ? "active_turn_not_steerable" : "live_input_rejected",
|
|
@@ -1789,6 +1815,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1789
1815
|
closedSignal.then((err) => {
|
|
1790
1816
|
if (!turnCompleted) {
|
|
1791
1817
|
prematureClose = true;
|
|
1818
|
+
stopLiveInput();
|
|
1792
1819
|
throw err || new Error("codex app-server closed");
|
|
1793
1820
|
}
|
|
1794
1821
|
return null;
|
|
@@ -1803,6 +1830,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1803
1830
|
}
|
|
1804
1831
|
} finally {
|
|
1805
1832
|
abortRaceCleanup();
|
|
1833
|
+
stopLiveInput();
|
|
1806
1834
|
}
|
|
1807
1835
|
turnCompleted = true;
|
|
1808
1836
|
await steerTask;
|
|
@@ -1923,6 +1951,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1923
1951
|
}),
|
|
1924
1952
|
};
|
|
1925
1953
|
} finally {
|
|
1954
|
+
stopLiveInput();
|
|
1926
1955
|
if (activeCompactions.size > 0) {
|
|
1927
1956
|
const cancelled = !!options.abortSignal?.aborted;
|
|
1928
1957
|
finalizeOpenCompactions(
|
|
@@ -3,6 +3,10 @@ import { estimateCost } from "../cost.js";
|
|
|
3
3
|
import { buildCapabilitiesUsed } from "../runtime/capabilities-used.js";
|
|
4
4
|
import { createApprovalManager, RISK_TIERS } from "../../agent/approval.js";
|
|
5
5
|
import { resolveSandboxPolicy } from "../../agent/tools/shared/tool-context.js";
|
|
6
|
+
import {
|
|
7
|
+
isAllowAllToolPolicy,
|
|
8
|
+
TOOL_POLICY_ALLOW_ALL_ONLY,
|
|
9
|
+
} from "../runtime/tool-policy.js";
|
|
6
10
|
import { createIsolatedOpencode } from "./opencode-server.js";
|
|
7
11
|
import {
|
|
8
12
|
toolUseEvent,
|
|
@@ -37,6 +41,7 @@ const OPENCODE_APP_CAPABILITIES = {
|
|
|
37
41
|
supports_live_input: false,
|
|
38
42
|
supports_native_subagents: false,
|
|
39
43
|
supports_fast_mode: false,
|
|
44
|
+
tool_policy: TOOL_POLICY_ALLOW_ALL_ONLY,
|
|
40
45
|
};
|
|
41
46
|
|
|
42
47
|
// How long to keep draining the event stream after session.prompt resolves, in
|
|
@@ -428,13 +433,9 @@ export function mapSpawnFailureKind(err) {
|
|
|
428
433
|
}
|
|
429
434
|
|
|
430
435
|
function opencodeToolPolicyProblem(options) {
|
|
431
|
-
|
|
432
|
-
const disallowedTools = Array.isArray(options.disallowedTools) ? options.disallowedTools : [];
|
|
433
|
-
const exactAllowAll = allowedTools === null
|
|
434
|
-
|| (allowedTools.length === 1 && allowedTools[0] === "*");
|
|
435
|
-
return exactAllowAll && disallowedTools.length === 0
|
|
436
|
+
return isAllowAllToolPolicy(options.allowedTools, options.disallowedTools)
|
|
436
437
|
? null
|
|
437
|
-
: "Direct OpenCode cannot enforce allowedTools/disallowedTools. Use
|
|
438
|
+
: "Direct OpenCode cannot enforce allowedTools/disallowedTools. Use allow-all-only (omit allowedTools or include \"*\", with no disallowedTools) or a Pi runtime (including pi:opencode-go:*).";
|
|
438
439
|
}
|
|
439
440
|
|
|
440
441
|
function opencodeCapabilityMismatchResult({
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TOOL_POLICY_ALLOW_ALL_ONLY,
|
|
3
|
+
TOOL_POLICY_PROJECTED,
|
|
4
|
+
} from "./tool-policy.js";
|
|
5
|
+
|
|
1
6
|
export const COMMON_CAPABILITIES = {
|
|
2
7
|
streaming: true,
|
|
3
8
|
structured_output: true,
|
|
@@ -9,6 +14,7 @@ export const COMMON_CAPABILITIES = {
|
|
|
9
14
|
supports_live_input: true,
|
|
10
15
|
supports_native_subagents: true,
|
|
11
16
|
supports_fast_mode: false,
|
|
17
|
+
tool_policy: TOOL_POLICY_PROJECTED,
|
|
12
18
|
};
|
|
13
19
|
|
|
14
20
|
export const RUNTIME_CAPABILITIES = {
|
|
@@ -36,6 +42,7 @@ export const RUNTIME_CAPABILITIES = {
|
|
|
36
42
|
// when options.sessionKeepAlive is set; resumed turns reuse the thread.
|
|
37
43
|
supports_session_resume: true,
|
|
38
44
|
supports_fast_mode: true,
|
|
45
|
+
tool_policy: TOOL_POLICY_ALLOW_ALL_ONLY,
|
|
39
46
|
},
|
|
40
47
|
opencode: {
|
|
41
48
|
runtime: "cli",
|
|
@@ -46,6 +53,7 @@ export const RUNTIME_CAPABILITIES = {
|
|
|
46
53
|
supports_skills: false,
|
|
47
54
|
supports_live_input: false,
|
|
48
55
|
supports_native_subagents: false,
|
|
56
|
+
tool_policy: TOOL_POLICY_ALLOW_ALL_ONLY,
|
|
49
57
|
},
|
|
50
58
|
};
|
|
51
59
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Metadata-only live-input acknowledgement instrumentation.
|
|
2
|
+
//
|
|
3
|
+
// Provider bridges already call message.acknowledge() only after their native
|
|
4
|
+
// steering boundary accepts guidance. Wrapping that callback here creates one
|
|
5
|
+
// adapter-neutral `live_input_applied` event without copying the guidance body
|
|
6
|
+
// into runtime telemetry. A wrapper owns one logical-run dedupe set and is
|
|
7
|
+
// intentionally reused by the fallback router across provider attempts.
|
|
8
|
+
|
|
9
|
+
// @ts-check
|
|
10
|
+
|
|
11
|
+
const LIVE_INPUT_APPLIED_INSTRUMENTED = Symbol("mono-agent.live-input-applied-instrumented");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {{body: string, id?: string, receivedAt?: string, acknowledge?: () => void, reject?: (reason?: unknown) => void}} RuntimeLiveInputMessage
|
|
15
|
+
* @typedef {{type: "live_input_applied", inputId: string, receivedAt?: string}} LiveInputAppliedEvent
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {AsyncIterable<RuntimeLiveInputMessage>|undefined} liveInput
|
|
20
|
+
* @param {(event: LiveInputAppliedEvent) => void} onApplied
|
|
21
|
+
* @returns {AsyncIterable<RuntimeLiveInputMessage>|undefined}
|
|
22
|
+
*/
|
|
23
|
+
export function instrumentLiveInputAppliedEvents(liveInput, onApplied) {
|
|
24
|
+
if (liveInput === undefined || isInstrumented(liveInput)) return liveInput;
|
|
25
|
+
|
|
26
|
+
const appliedInputIds = new Set();
|
|
27
|
+
const instrumented = {
|
|
28
|
+
[LIVE_INPUT_APPLIED_INSTRUMENTED]: true,
|
|
29
|
+
[Symbol.asyncIterator]() {
|
|
30
|
+
const iterator = liveInput[Symbol.asyncIterator]();
|
|
31
|
+
let ordinal = 0;
|
|
32
|
+
return {
|
|
33
|
+
async next() {
|
|
34
|
+
const next = await iterator.next();
|
|
35
|
+
if (next.done === true) return next;
|
|
36
|
+
ordinal += 1;
|
|
37
|
+
const message = next.value;
|
|
38
|
+
const inputId = stableInputId(message?.id, ordinal);
|
|
39
|
+
const receivedAt = typeof message?.receivedAt === "string" && message.receivedAt.length > 0
|
|
40
|
+
? message.receivedAt
|
|
41
|
+
: undefined;
|
|
42
|
+
const acknowledge = typeof message?.acknowledge === "function"
|
|
43
|
+
? message.acknowledge.bind(message)
|
|
44
|
+
: undefined;
|
|
45
|
+
return {
|
|
46
|
+
done: false,
|
|
47
|
+
value: {
|
|
48
|
+
...message,
|
|
49
|
+
acknowledge: () => {
|
|
50
|
+
acknowledge?.();
|
|
51
|
+
if (appliedInputIds.has(inputId)) return;
|
|
52
|
+
appliedInputIds.add(inputId);
|
|
53
|
+
try {
|
|
54
|
+
onApplied({
|
|
55
|
+
type: "live_input_applied",
|
|
56
|
+
inputId,
|
|
57
|
+
...(receivedAt === undefined ? {} : { receivedAt }),
|
|
58
|
+
});
|
|
59
|
+
} catch {
|
|
60
|
+
// Telemetry must never turn accepted guidance into a provider
|
|
61
|
+
// failure after the native steering call already succeeded.
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
async return(value) {
|
|
68
|
+
return typeof iterator.return === "function"
|
|
69
|
+
? iterator.return(value)
|
|
70
|
+
: { done: true, value };
|
|
71
|
+
},
|
|
72
|
+
async throw(error) {
|
|
73
|
+
if (typeof iterator.throw === "function") return iterator.throw(error);
|
|
74
|
+
throw error;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
return /** @type {AsyncIterable<RuntimeLiveInputMessage>} */ (instrumented);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @param {unknown} value */
|
|
83
|
+
function isInstrumented(value) {
|
|
84
|
+
return typeof value === "object"
|
|
85
|
+
&& value !== null
|
|
86
|
+
&& value[LIVE_INPUT_APPLIED_INSTRUMENTED] === true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @param {unknown} value @param {number} ordinal */
|
|
90
|
+
function stableInputId(value, ordinal) {
|
|
91
|
+
return typeof value === "string" && value.trim().length > 0
|
|
92
|
+
? value
|
|
93
|
+
: `anonymous:${ordinal}`;
|
|
94
|
+
}
|
package/src/ai/runtime/router.js
CHANGED
|
@@ -41,6 +41,8 @@ import { runtimeCapabilities } from "./capabilities.js";
|
|
|
41
41
|
import { buildTranscriptTailSnapshot, renderResumeSnapshot } from "../../agent/transcript.js";
|
|
42
42
|
import { passthroughSandbox } from "../../agent/sandbox-seam.js";
|
|
43
43
|
import { resolveRuntimeBrand } from "../../runtime-brand.js";
|
|
44
|
+
import { createObserverHub } from "../observer.js";
|
|
45
|
+
import { instrumentLiveInputAppliedEvents } from "./live-input-events.js";
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
48
|
* @typedef {import('../types.js').RuntimeModelRef} RuntimeModelRef
|
|
@@ -127,6 +129,22 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
|
|
|
127
129
|
* @returns {Promise<RuntimeResult>}
|
|
128
130
|
*/
|
|
129
131
|
async run(systemPrompt, options = {}) {
|
|
132
|
+
const liveInputHub = options.liveInput === undefined
|
|
133
|
+
? undefined
|
|
134
|
+
: createObserverHub({
|
|
135
|
+
observers: [
|
|
136
|
+
...(Array.isArray(host.observers) ? host.observers : []),
|
|
137
|
+
...(Array.isArray(options.observers) ? options.observers : []),
|
|
138
|
+
],
|
|
139
|
+
onEvent: options.onEvent,
|
|
140
|
+
});
|
|
141
|
+
if (options.liveInput !== undefined && liveInputHub !== undefined) {
|
|
142
|
+
options = {
|
|
143
|
+
...options,
|
|
144
|
+
liveInput: instrumentLiveInputAppliedEvents(options.liveInput, liveInputHub.emit),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
130
148
|
/** @type {Array<{model: RuntimeModelRef, failureKind: (string|null), requestId?: (string|null|undefined), retryableSubkind?: (string|null|undefined), requirements?: (Object<string,*>|null), routeSafety?: import('../types.js').RuntimeRouteSafetyMode, safetyContract?: import('../types.js').RuntimeRouteSafetyContract}>} */
|
|
131
149
|
const failoverHistory = [];
|
|
132
150
|
/** @type {RuntimeResult|null} */
|
|
@@ -374,6 +392,9 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
|
|
|
374
392
|
failoverHistory,
|
|
375
393
|
routeSafetyHistory,
|
|
376
394
|
};
|
|
395
|
+
} finally {
|
|
396
|
+
await liveInputHub?.flush();
|
|
397
|
+
}
|
|
377
398
|
},
|
|
378
399
|
chain: () => entries.slice(),
|
|
379
400
|
configureTools(next = {}) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @type {"projected"} */
|
|
4
|
+
export const TOOL_POLICY_PROJECTED = "projected";
|
|
5
|
+
/** @type {"allow_all_only"} */
|
|
6
|
+
export const TOOL_POLICY_ALLOW_ALL_ONLY = "allow_all_only";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* True when a tool policy is semantically unrestricted.
|
|
10
|
+
*
|
|
11
|
+
* An omitted allowlist and any allowlist containing the global `"*"` sentinel
|
|
12
|
+
* both mean allow-all. A denylist still makes the policy restrictive.
|
|
13
|
+
*
|
|
14
|
+
* @param {*} allowedTools
|
|
15
|
+
* @param {*} disallowedTools
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
export function isAllowAllToolPolicy(allowedTools, disallowedTools) {
|
|
19
|
+
const allowAll = !Array.isArray(allowedTools) || allowedTools.includes("*");
|
|
20
|
+
const hasDeniedTools = Array.isArray(disallowedTools) && disallowedTools.length > 0;
|
|
21
|
+
return allowAll && !hasDeniedTools;
|
|
22
|
+
}
|
package/src/ai/types.js
CHANGED
|
@@ -56,7 +56,12 @@
|
|
|
56
56
|
* branch ran for a particular command.
|
|
57
57
|
*/
|
|
58
58
|
|
|
59
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {"mono-agent-monotonic"|"mono-agent-policy"|"provider-representable"|"exact-allow-all"|"unsupported"} RuntimeRouteToolsContract
|
|
61
|
+
* `exact-allow-all` is a stable telemetry token. It describes an effective
|
|
62
|
+
* unrestricted contract, including mixed allowlists that contain `"*"`;
|
|
63
|
+
* it does not require the literal one-element array `["*"]`.
|
|
64
|
+
*/
|
|
60
65
|
|
|
61
66
|
/**
|
|
62
67
|
* @typedef {Object} RuntimeRouteSafetyContract
|
|
@@ -131,6 +136,7 @@
|
|
|
131
136
|
* @property {string} [executionMode] "sdk" (default) or "cli"; selects which bridge variant handles the model.
|
|
132
137
|
* @property {string} [sessionId] Host conversation/session key for resumable bridges.
|
|
133
138
|
* @property {string} [providerSessionId] Provider-owned resume id for resumable bridges.
|
|
139
|
+
* @property {typeof import("@anthropic-ai/claude-agent-sdk").query} [claudeAgentQuery] Advanced programmatic/test seam for the Claude SDK route; omitted runs use the runtime's pinned SDK query implementation.
|
|
134
140
|
* @property {boolean} [sessionKeepAlive] Keep resumable provider state alive after the turn.
|
|
135
141
|
* @property {number} [sessionIdleTimeoutMs] Idle TTL for resumable provider state.
|
|
136
142
|
* @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.
|
|
@@ -222,6 +228,10 @@
|
|
|
222
228
|
* @property {boolean} [supports_live_input]
|
|
223
229
|
* @property {boolean} [supports_native_subagents]
|
|
224
230
|
* @property {boolean} [supports_fast_mode]
|
|
231
|
+
* @property {"projected"|"allow_all_only"} [tool_policy] Whether the bridge can
|
|
232
|
+
* project named allow/deny policies or accepts only a semantically unrestricted
|
|
233
|
+
* policy. Built-in bridges always report this field; omission means unknown
|
|
234
|
+
* for custom structural capability objects.
|
|
225
235
|
*/
|
|
226
236
|
|
|
227
237
|
/**
|