@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.
@@ -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
- const allowedTools = Array.isArray(options.allowedTools) ? options.allowedTools : null;
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 exact allow-all ([\"*\"] with no disallowedTools) or another runtime.";
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 = query({ prompt: /** @type {any} */ (prompt), options: queryOptions });
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
- // `undefined` retains the public runtime's documented allow-all default.
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 exact allow-all ([\"*\"] with no disallowedTools) or another runtime.";
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
- turnDone.then(() => ({ done: true, value: undefined })),
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
- turnDone,
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.acknowledge?.();
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
- turnDone,
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.acknowledge?.();
1564
+ invokeLiveInputCallback(message, "acknowledge");
1539
1565
  continue;
1540
1566
  } catch (retryErr) {
1541
- message.reject?.(retryErr);
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.reject?.(err);
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
- const allowedTools = Array.isArray(options.allowedTools) ? options.allowedTools : null;
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 exact allow-all ([\"*\"] with no disallowedTools) or a Pi runtime (including pi:opencode-go:*).";
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
+ }
@@ -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
- /** @typedef {"mono-agent-monotonic"|"mono-agent-policy"|"provider-representable"|"exact-allow-all"|"unsupported"} RuntimeRouteToolsContract */
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
  /**