@jini-ai/agent-runtime 0.4.3 → 0.4.5

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.
Files changed (40) hide show
  1. package/dist/agent-protocol/acp/models.d.ts +11 -0
  2. package/dist/agent-protocol/acp/models.js +12 -3
  3. package/dist/capabilities.d.ts +1 -8
  4. package/dist/capabilities.js +8 -1
  5. package/dist/claude-code-models.d.ts +13 -1
  6. package/dist/claude-code-models.js +14 -16
  7. package/dist/claude-segment-usage.d.ts +26 -0
  8. package/dist/claude-segment-usage.js +63 -0
  9. package/dist/claude-stream.d.ts +6 -0
  10. package/dist/claude-stream.js +19 -4
  11. package/dist/defs/claude.d.ts +2 -0
  12. package/dist/defs/claude.js +10 -2
  13. package/dist/defs/codebuddy.js +1 -1
  14. package/dist/defs/cursor-agent.js +1 -1
  15. package/dist/defs/opencode.js +1 -1
  16. package/dist/detection.d.ts +28 -4
  17. package/dist/detection.js +81 -42
  18. package/dist/diagnostics.d.ts +6 -1
  19. package/dist/diagnostics.js +2 -2
  20. package/dist/executables.d.ts +15 -9
  21. package/dist/executables.js +72 -64
  22. package/dist/index.d.ts +2 -1
  23. package/dist/index.js +2 -1
  24. package/dist/invocation.d.ts +24 -2
  25. package/dist/invocation.js +18 -6
  26. package/dist/launch.d.ts +2 -1
  27. package/dist/launch.js +2 -2
  28. package/dist/model-discovery-adapters.d.ts +5 -0
  29. package/dist/model-discovery-adapters.js +62 -49
  30. package/dist/model-discovery.js +2 -2
  31. package/dist/models.d.ts +9 -4
  32. package/dist/models.js +11 -11
  33. package/dist/probe-scheduler.d.ts +19 -0
  34. package/dist/probe-scheduler.js +30 -0
  35. package/dist/runtime-state.d.ts +41 -0
  36. package/dist/runtime-state.js +31 -0
  37. package/dist/types.d.ts +4 -0
  38. package/package.json +13 -8
  39. package/dist/defs/gemini.d.ts +0 -24
  40. package/dist/defs/gemini.js +0 -81
@@ -1,3 +1,12 @@
1
+ /** @module agent-protocol/acp/models
2
+ * ACP model detection, normalisation, and config-option selection. The primary
3
+ * export `detectAcpModels` spawns an ACP subprocess, performs the
4
+ * `initialize` + `session/new` handshake, and returns the available model list.
5
+ * Consumed by runtime adapter definitions. Depends on the core JSON-line
6
+ * stream and acp/types, acp/constants, acp/json, acp/rpc, and
7
+ * acp/session-params.
8
+ */
9
+ import { spawn } from 'node:child_process';
1
10
  import type { UnknownRecord } from './types.js';
2
11
  /** A single model entry exposed to the daemon's model-selection UI and to `session/set_model` calls. */
3
12
  export interface ModelOption {
@@ -22,6 +31,8 @@ export interface DetectAcpModelsOptions {
22
31
  defaultModelOption?: ModelOption;
23
32
  signal?: AbortSignal;
24
33
  onSessionResult?: (result: UnknownRecord) => void;
34
+ /** Process effect port; metadata tests use a child fake rather than replacing modules. */
35
+ spawnProcess?: typeof spawn;
25
36
  }
26
37
  /**
27
38
  * Normalises a raw config-option field value to a lowercase, whitespace- and
@@ -191,10 +191,10 @@ export function currentModelFromSessionResult(result) {
191
191
  * @returns A promise resolving to the available `ModelOption[]`.
192
192
  */
193
193
  export async function detectAcpModels(requiredArgs, optionalArgs = {}) {
194
- const { bin, args, cwd = process.cwd(), env = process.env, timeoutMs = DEFAULT_TIMEOUT_MS, clientName = 'agent-runtime-detect', clientVersion = 'runtime-adapter', defaultModelOption = { id: 'default', label: 'Default (CLI config)' }, signal, onSessionResult, } = { ...optionalArgs, ...requiredArgs };
194
+ const { bin, args, cwd = process.cwd(), env = process.env, timeoutMs = DEFAULT_TIMEOUT_MS, clientName = 'agent-runtime-detect', clientVersion = 'runtime-adapter', defaultModelOption = { id: 'default', label: 'Default (CLI config)' }, signal, onSessionResult, spawnProcess = spawn, } = { ...optionalArgs, ...requiredArgs };
195
195
  const effectiveTimeoutMs = resolveAcpTimeoutMs(env, timeoutMs);
196
196
  return await new Promise((resolve, reject) => {
197
- const child = spawn(bin, args, {
197
+ const child = spawnProcess(bin, args, {
198
198
  cwd,
199
199
  stdio: ['pipe', 'pipe', 'pipe'],
200
200
  env: { ...env },
@@ -202,6 +202,7 @@ export async function detectAcpModels(requiredArgs, optionalArgs = {}) {
202
202
  child.stdout.setEncoding('utf8');
203
203
  child.stderr.setEncoding('utf8');
204
204
  let settled = false;
205
+ let closed = false;
205
206
  let stderrBuf = '';
206
207
  let stdoutBytes = 0;
207
208
  let expectedId = 1;
@@ -223,7 +224,14 @@ export async function detectAcpModels(requiredArgs, optionalArgs = {}) {
223
224
  const killTimer = setTimeout(() => { if (child.exitCode === null)
224
225
  child.kill('SIGKILL'); }, 250);
225
226
  killTimer.unref();
226
- fn(value);
227
+ // The process scheduler must retain its slot until exit, including the SIGKILL grace
228
+ // period. Returning at metadata receipt would overlap this child with the next probe.
229
+ if (closed) {
230
+ clearTimeout(killTimer);
231
+ fn(value);
232
+ }
233
+ else
234
+ child.once('close', () => { clearTimeout(killTimer); fn(value); });
227
235
  };
228
236
  const fail = (message) => {
229
237
  finish(reject, new Error(message));
@@ -289,6 +297,7 @@ export async function detectAcpModels(requiredArgs, optionalArgs = {}) {
289
297
  });
290
298
  child.on('error', (err) => fail(`spawn failed: ${err.message}`));
291
299
  child.on('close', (code, signal) => {
300
+ closed = true;
292
301
  parser.flush();
293
302
  if (!settled) {
294
303
  const errTail = stderrBuf.trim();
@@ -1,9 +1,2 @@
1
- /**
2
- * @module capabilities
3
- *
4
- * Per-agent `--help`-probed capability flags, cached by agent id. Ported
5
- * verbatim from OD's `apps/daemon/src/runtimes/capabilities.ts`.
6
- */
7
- import type { RuntimeCapabilityMap } from './types.js';
8
- export declare const agentCapabilities: Map<string, RuntimeCapabilityMap>;
1
+ export declare const agentCapabilities: Map<string, import("./types.js").RuntimeCapabilityMap>;
9
2
  //# sourceMappingURL=capabilities.d.ts.map
@@ -1,2 +1,9 @@
1
- export const agentCapabilities = new Map();
1
+ /**
2
+ * @module capabilities
3
+ *
4
+ * Per-agent `--help`-probed capability flags, cached by agent id. Ported
5
+ * verbatim from OD's `apps/daemon/src/runtimes/capabilities.ts`.
6
+ */
7
+ import { defaultRuntimeState } from './runtime-state.js';
8
+ export const agentCapabilities = defaultRuntimeState.capabilities;
2
9
  //# sourceMappingURL=capabilities.js.map
@@ -1,3 +1,4 @@
1
+ import { type AgentRuntimeState } from './runtime-state.js';
1
2
  import type { RuntimeEnv, RuntimeModelOption } from './types.js';
2
3
  /** The single stdin line that asks the CLI for its initialize payload (which carries `models`). */
3
4
  export declare const CLAUDE_CODE_INITIALIZE_REQUEST = "{\"type\":\"control_request\",\"request_id\":\"jini-model-probe\",\"request\":{\"subtype\":\"initialize\"}}\n";
@@ -58,5 +59,16 @@ export declare function setClaudeCodeModelIoForTesting(io: ClaudeCodeModelIo | n
58
59
  * @complexity O(f + l) plus, at most once per {@link CLAUDE_CODE_MODEL_CACHE_TTL_MS} per cache key,
59
60
  * one CLI spawn bounded by {@link CLAUDE_CODE_INIT_TIMEOUT_MS} and one file read.
60
61
  */
61
- export declare function loadClaudeCodeModels(bin: string, env: RuntimeEnv, _fallbackModels: readonly RuntimeModelOption[], now?: () => number): Promise<RuntimeModelOption[] | null>;
62
+ export interface ClaudeCodeModelsOptions {
63
+ state?: AgentRuntimeState;
64
+ io?: ClaudeCodeModelIo;
65
+ now?: () => number;
66
+ }
67
+ export declare function loadClaudeCodeModels(required: {
68
+ bin: string;
69
+ env: RuntimeEnv;
70
+ fallbackModels?: readonly RuntimeModelOption[];
71
+ }, optional?: ClaudeCodeModelsOptions): Promise<RuntimeModelOption[] | null>;
72
+ /** Published positional form remains additive compatibility. */
73
+ export declare function loadClaudeCodeModels(bin: string, env: RuntimeEnv, fallbackModels: readonly RuntimeModelOption[], now?: () => number): Promise<RuntimeModelOption[] | null>;
62
74
  //# sourceMappingURL=claude-code-models.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { defaultRuntimeState } from './runtime-state.js';
1
2
  /**
2
3
  * @module claude-code-models
3
4
  *
@@ -193,7 +194,7 @@ const defaultIo = {
193
194
  readConfigFile: (path) => readFile(path, 'utf8').catch(() => null),
194
195
  };
195
196
  let activeIo = defaultIo;
196
- const cache = new Map();
197
+ const cache = defaultRuntimeState.claudeModels;
197
198
  /** Test-only: install fake I/O (or `null` to restore the real one) and clear the process cache. */
198
199
  export function setClaudeCodeModelIoForTesting(io) {
199
200
  activeIo = io ?? defaultIo;
@@ -209,13 +210,13 @@ function cacheKeyFor(bin, env) {
209
210
  return `${bin}\u0000${resolveClaudeConfigPath(env)}\u0000${fingerprint}`;
210
211
  }
211
212
  /** Source (1), then source (2). Never rejects. */
212
- async function discoverClaudeCodeModels(bin, env) {
213
+ async function discoverClaudeCodeModels(bin, env, io) {
213
214
  try {
214
- const stdout = await activeIo.runInitialize(bin, env);
215
+ const stdout = await io.runInitialize(bin, env);
215
216
  const fromCli = stdout ? parseClaudeInitializeModels(stdout) : null;
216
217
  if (fromCli)
217
218
  return fromCli;
218
- const raw = await activeIo.readConfigFile(resolveClaudeConfigPath(env));
219
+ const raw = await io.readConfigFile(resolveClaudeConfigPath(env));
219
220
  const cached = raw === null ? null : parseClaudeCodePickerCache(raw);
220
221
  if (!cached?.ok)
221
222
  return null;
@@ -227,21 +228,18 @@ async function discoverClaudeCodeModels(bin, env) {
227
228
  return null;
228
229
  }
229
230
  }
230
- /**
231
- * Returns the `claude` model list from the CLI's own picker catalog, or `null` when discovery fails. Needs no API key.
232
- *
233
- * @param bin - The resolved `claude` (or fork) binary detection found.
234
- * @param env - The composed environment for the agent — what a spawn would actually see.
235
- * @param _fallbackModels - Compatibility argument; packaged rows are offline fallback only.
236
- * @param now - Injectable clock for TTL tests.
237
- * @complexity O(f + l) plus, at most once per {@link CLAUDE_CODE_MODEL_CACHE_TTL_MS} per cache key,
238
- * one CLI spawn bounded by {@link CLAUDE_CODE_INIT_TIMEOUT_MS} and one file read.
239
- */
240
- export async function loadClaudeCodeModels(bin, env, _fallbackModels, now = Date.now) {
231
+ export async function loadClaudeCodeModels(required, optional = {}, _fallbackModels = [], clock = Date.now) {
232
+ const bin = typeof required === 'string' ? required : required.bin;
233
+ const env = typeof required === 'string' ? optional : required.env;
234
+ const options = typeof required === 'string' ? {} : optional;
235
+ const state = options.state ?? defaultRuntimeState;
236
+ const cache = state.claudeModels;
237
+ const io = options.io ?? (state === defaultRuntimeState ? activeIo : defaultIo);
238
+ const now = options.now ?? clock;
241
239
  const key = cacheKeyFor(bin, env);
242
240
  const cached = cache.get(key);
243
241
  const pending = cached && cached.expiresAt > now() ? cached.value : null;
244
- const value = pending ?? discoverClaudeCodeModels(bin, env);
242
+ const value = pending ?? discoverClaudeCodeModels(bin, env, io);
245
243
  if (!pending)
246
244
  cache.set(key, { expiresAt: now() + CLAUDE_CODE_MODEL_CACHE_TTL_MS, value });
247
245
  const live = await value;
@@ -0,0 +1,26 @@
1
+ /** Token counters are per segment; Claude's dollar counter spans the session.
2
+ * Aborted results can zero counters already streamed. */
3
+ export declare function createClaudeSegmentUsage(_required: Record<string, never>, { nowMs, costBasis }?: {
4
+ nowMs?: () => number;
5
+ costBasis?: 'session' | 'run';
6
+ }): {
7
+ observe: ({ messageId, usage }: {
8
+ messageId: string;
9
+ usage: unknown;
10
+ }, _optional?: {}) => void;
11
+ finish: ({ result, interrupted }: {
12
+ result: Record<string, unknown>;
13
+ interrupted: boolean;
14
+ }, _optional?: {}) => {
15
+ cumulativeCostUsd?: number;
16
+ costSessionId?: string;
17
+ usage: {} | null;
18
+ costUsd: {} | null;
19
+ durationMs: {} | null;
20
+ };
21
+ setSession: ({ id }: {
22
+ id: unknown;
23
+ }, _optional?: {}) => void;
24
+ hasObserved: () => boolean;
25
+ };
26
+ //# sourceMappingURL=claude-segment-usage.d.ts.map
@@ -0,0 +1,63 @@
1
+ /** Token counters are per segment; Claude's dollar counter spans the session.
2
+ * Aborted results can zero counters already streamed. */
3
+ export function createClaudeSegmentUsage(_required, { nowMs = Date.now, costBasis = 'session' } = {}) {
4
+ const messages = new Map();
5
+ let startedAt = nowMs();
6
+ let sessionId;
7
+ /** Capture the CLI's identity so durable chat usage can reconcile resumed runs. */
8
+ function setSession({ id }, _optional = {}) {
9
+ if (typeof id === 'string' && id.length > 0)
10
+ sessionId = id;
11
+ }
12
+ /** Merge cumulative counters for one message; wrappers repeat the same streamed usage.
13
+ * O(k) per snapshot in k reported counters; retained state is O(m·k) for m messages in a segment. */
14
+ function observe({ messageId, usage }, _optional = {}) {
15
+ if (!usage || typeof usage !== 'object')
16
+ return;
17
+ const previous = messages.get(messageId) ?? {};
18
+ for (const [key, value] of Object.entries(usage)) {
19
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
20
+ previous[key] = Math.max(previous[key] ?? 0, value);
21
+ }
22
+ if (Object.keys(previous).length)
23
+ messages.set(messageId, previous);
24
+ }
25
+ /** Produce one segment's totals and release its snapshots. Never infer a monetary charge
26
+ * from token counts: only CLI-reported costs are eligible. O(m·k), no external effects. */
27
+ function finish({ result, interrupted }, _optional = {}) {
28
+ const usage = { ...((result.usage && typeof result.usage === 'object') ? result.usage : {}) };
29
+ const observed = {};
30
+ for (const counters of messages.values()) {
31
+ for (const [key, value] of Object.entries(counters))
32
+ observed[key] = (observed[key] ?? 0) + value;
33
+ }
34
+ for (const [key, value] of Object.entries(observed)) {
35
+ const reported = usage[key];
36
+ usage[key] = typeof reported === 'number' && Number.isFinite(reported) ? Math.max(reported, value) : value;
37
+ }
38
+ const modelCosts = result.modelUsage && typeof result.modelUsage === 'object' ? Object.values(result.modelUsage) : [];
39
+ const reportedModelCost = modelCosts.reduce((sum, model) => {
40
+ const cost = model && typeof model === 'object' ? model.costUSD : undefined;
41
+ return sum + (typeof cost === 'number' && Number.isFinite(cost) && cost >= 0 ? cost : 0);
42
+ }, 0);
43
+ const costUsd = reportedModelCost > 0 ? Math.max(typeof result.total_cost_usd === 'number' ? result.total_cost_usd : 0, reportedModelCost) : result.total_cost_usd ?? null;
44
+ const endedAt = nowMs();
45
+ const duration = result.duration_ms;
46
+ const durationMs = typeof duration === 'number' && duration > 0 ? duration
47
+ : messages.size || interrupted ? Math.max(0, endedAt - startedAt) : duration ?? null;
48
+ messages.clear();
49
+ startedAt = endedAt;
50
+ // Preserve the cumulative snapshot rather than calling it a per-run charge. A zeroed
51
+ // aborted result still uses the existing model-cost fallback for its interrupted segment.
52
+ const cumulative = costBasis === 'session' && sessionId !== undefined &&
53
+ typeof costUsd === 'number' && Number.isFinite(costUsd) && costUsd >= 0 &&
54
+ !(interrupted && result.total_cost_usd === 0);
55
+ return { usage: Object.keys(usage).length ? usage : result.usage ?? null,
56
+ costUsd: cumulative ? null : costUsd, durationMs,
57
+ ...(costBasis === 'session' && sessionId !== undefined ? { costSessionId: sessionId } : {}),
58
+ ...(cumulative ? { cumulativeCostUsd: costUsd } : {}),
59
+ };
60
+ }
61
+ return { observe, finish, setSession, hasObserved: () => messages.size > 0 };
62
+ }
63
+ //# sourceMappingURL=claude-segment-usage.js.map
@@ -82,6 +82,8 @@ export type ClaudeStreamEvent = {
82
82
  type: 'usage';
83
83
  usage: unknown;
84
84
  costUsd: unknown;
85
+ cumulativeCostUsd?: number;
86
+ costSessionId?: string;
85
87
  durationMs: unknown;
86
88
  stopReason: string | null;
87
89
  } | {
@@ -157,6 +159,10 @@ export declare function applyInputJsonDelta(state: BlockState | undefined, parti
157
159
  export declare function emitAssistantErrorIfPresent(obj: Record<string, unknown>, content: unknown[], onEvent: EventSink): void;
158
160
  interface ClaudeStreamHandlerOptions {
159
161
  suppressHtmlArtifactsAfterFileWrite?: boolean;
162
+ /** Clock used only when an interrupted segment omits its duration. */
163
+ nowMs?: () => number;
164
+ /** Claude's results span a session; compatible CLIs can retain per-run cost semantics. */
165
+ costBasis?: 'session' | 'run';
160
166
  }
161
167
  export declare function createClaudeStreamHandler({ onEvent }: {
162
168
  onEvent: EventSink;
@@ -37,6 +37,7 @@
37
37
  * exercised only through `feed`/`flush` in the test suite.
38
38
  */
39
39
  import { createRoleMarkerGuard } from './role-marker-guard.js';
40
+ import { createClaudeSegmentUsage } from './claude-segment-usage.js';
40
41
  function isRecord(value) {
41
42
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
42
43
  }
@@ -338,9 +339,13 @@ export function emitAssistantErrorIfPresent(obj, content, onEvent) {
338
339
  }
339
340
  export function createClaudeStreamHandler({ onEvent }, options = {}) {
340
341
  let buffer = '';
342
+ const segmentUsage = createClaudeSegmentUsage({}, {
343
+ ...(options.nowMs === undefined ? {} : { nowMs: options.nowMs }),
344
+ ...(options.costBasis === undefined ? {} : { costBasis: options.costBasis }),
345
+ });
341
346
  // Per-content-block scratch, keyed by `${messageId}:${blockIndex}`.
342
347
  const blocks = new Map();
343
- // Tool uses already emitted from streamed `input_json_delta` data.
348
+ // Tool uses already emitted from streamed `input_json_delta` data OR full wrappers.
344
349
  // Claude Code still repeats them in the final assistant wrapper, often with
345
350
  // empty `{}` inputs, so we suppress that duplicate emission.
346
351
  const streamedToolUseIds = new Set();
@@ -404,6 +409,11 @@ export function createClaudeStreamHandler({ onEvent }, options = {}) {
404
409
  }
405
410
  }
406
411
  function emitToolUse(id, name, input) {
412
+ if (typeof id === 'string') {
413
+ if (streamedToolUseIds.has(id))
414
+ return;
415
+ streamedToolUseIds.add(id);
416
+ }
407
417
  if (emitCanonicalTaskSnapshot(id, name, input, taskRegistry, onEvent))
408
418
  return;
409
419
  if (isFileWriteToolUse(name, input)) {
@@ -566,6 +576,9 @@ export function createClaudeStreamHandler({ onEvent }, options = {}) {
566
576
  }
567
577
  }
568
578
  flushPendingArtifactText();
579
+ // A hard cancel has no result frame. Flush only counters actually observed, once.
580
+ if (segmentUsage.hasObserved())
581
+ onEvent({ type: 'usage', ...segmentUsage.finish({ result: {}, interrupted: true }), stopReason: null });
569
582
  }
570
583
  function handleToolUseBlock(block) {
571
584
  if (typeof block.id === 'string' && streamedToolUseIds.has(block.id))
@@ -625,6 +638,7 @@ export function createClaudeStreamHandler({ onEvent }, options = {}) {
625
638
  if (typeof message.model === 'string')
626
639
  onEvent({ type: 'status', label: 'model', model: message.model });
627
640
  const { textMsgId, thinkingMsgId, textAlreadyStreamed, thinkingAlreadyStreamed } = resolveAssistantMessageIds(message);
641
+ segmentUsage.observe({ messageId: currentMessageId ?? `anon${anonymousMessageEpoch}`, usage: message.usage });
628
642
  // Per-turn `stop_reason` is emitted as `turn_end` AFTER the content
629
643
  // blocks have been processed (see below). When `--include-partial-
630
644
  // messages` is unsupported, tool_use events surface only from the
@@ -664,15 +678,14 @@ export function createClaudeStreamHandler({ onEvent }, options = {}) {
664
678
  }
665
679
  onEvent({
666
680
  type: 'usage',
667
- usage: obj.usage ?? null,
668
- costUsd: obj.total_cost_usd ?? null,
669
- durationMs: obj.duration_ms ?? null,
681
+ ...segmentUsage.finish({ result: obj, interrupted: stopReason?.startsWith('aborted') === true }),
670
682
  stopReason,
671
683
  });
672
684
  }
673
685
  function handleObject(obj) {
674
686
  if (!isRecord(obj))
675
687
  return;
688
+ segmentUsage.setSession({ id: obj.session_id });
676
689
  switch (obj.type) {
677
690
  case 'system':
678
691
  handleSystemMessage(obj, onEvent);
@@ -703,6 +716,7 @@ export function createClaudeStreamHandler({ onEvent }, options = {}) {
703
716
  // New message boundary — see `anonymousMessageEpoch`'s declaration.
704
717
  if (currentMessageId === null)
705
718
  anonymousMessageEpoch += 1;
719
+ segmentUsage.observe({ messageId: currentMessageId ?? `anon${anonymousMessageEpoch}`, usage: isRecord(ev.message) ? ev.message.usage : undefined });
706
720
  currentMessageStreamedText = false;
707
721
  currentMessageStreamedThinking = false;
708
722
  if (typeof ev.ttft_ms === 'number') {
@@ -786,6 +800,7 @@ export function createClaudeStreamHandler({ onEvent }, options = {}) {
786
800
  handleContentBlockStop(ev);
787
801
  return;
788
802
  case 'message_delta':
803
+ segmentUsage.observe({ messageId: currentMessageId ?? `anon${anonymousMessageEpoch}`, usage: ev.usage ?? (isRecord(ev.delta) ? ev.delta.usage : undefined) });
789
804
  if (isRecord(ev.delta))
790
805
  handleMessageDeltaEvent(ev.delta);
791
806
  return;
@@ -37,9 +37,11 @@ export declare const claudeAgentDef: {
37
37
  promptViaStdin: true;
38
38
  promptInputFormat: "stream-json";
39
39
  streamFormat: string;
40
+ usageCostBasis: "session";
40
41
  externalMcpInjection: "claude-mcp-json";
41
42
  env: {
42
43
  ENABLE_TOOL_SEARCH: string;
44
+ CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: string;
43
45
  };
44
46
  systemPromptDelivery: {
45
47
  strategy: "append-flag";
@@ -157,7 +157,7 @@ export const claudeAgentDef = {
157
157
  // default), which has no length cap. Mirrors the codex/opencode/
158
158
  // cursor/qwen entries below.
159
159
  buildArgs: ({ prompt: _prompt, imagePaths: _imagePaths }, { extraAllowedDirs = [], options = {}, runtimeContext = {} } = {}) => {
160
- const caps = agentCapabilities.get('claude') || {};
160
+ const caps = (options.capabilities ?? agentCapabilities.get('claude')) || {};
161
161
  // `--input-format stream-json` lets the daemon stream multiple JSONL
162
162
  // messages into stdin instead of closing it after the initial prompt,
163
163
  // keeping the turn open so the daemon can stream further user messages
@@ -243,6 +243,7 @@ export const claudeAgentDef = {
243
243
  promptViaStdin: true,
244
244
  promptInputFormat: 'stream-json',
245
245
  streamFormat: 'claude-stream-json',
246
+ usageCostBasis: 'session',
246
247
  // Claude Code auto-loads `.mcp.json` from the project cwd at spawn,
247
248
  // so the daemon writes the user's external MCP servers there before
248
249
  // launching (server.ts handles the cwd guard).
@@ -254,7 +255,14 @@ export const claudeAgentDef = {
254
255
  // code.claude.com/docs/en/mcp ("`false` — All MCP tools loaded upfront, no deferral"); verified
255
256
  // in the installed 2.1.283 binary (a defined-falsy value selects the "standard" tool mode).
256
257
  // A default only: a run's own env can still set ENABLE_TOOL_SEARCH (see agent-executor).
257
- env: { ENABLE_TOOL_SEARCH: 'false' },
258
+ env: {
259
+ ENABLE_TOOL_SEARCH: 'false',
260
+ // 2026-10-10 plannie incident: the CLI stopped two background theme subagents ten minutes
261
+ // after the reply ended. Installed Claude Code 2.1.296 confirms 0 waits indefinitely.
262
+ // Complements the daemon's stdin hold while background tasks run (agent-executor/events.ts).
263
+ // A default only: the host can override it through the run's explicit env passthrough.
264
+ CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: '0',
265
+ },
258
266
  // Formerly inline in `buildArgs` above (see the comment there) — moved to the declarative
259
267
  // shape every def now uses to receive `RuntimeBuildOptions.systemPromptOverlay`. Same flag,
260
268
  // same probe gate (`capabilityFlags['--append-system-prompt']` above), same "append, never
@@ -79,7 +79,7 @@ export const codebuddyAgentDef = {
79
79
  { id: 'max', label: 'Max' },
80
80
  ],
81
81
  buildArgs: ({ prompt: _prompt, imagePaths: _imagePaths }, { extraAllowedDirs = [], options = {}, runtimeContext = {} } = {}) => {
82
- const caps = agentCapabilities.get('codebuddy') || {};
82
+ const caps = (options.capabilities ?? agentCapabilities.get('codebuddy')) || {};
83
83
  // Same stdin strategy as Claude Code: `--input-format stream-json`
84
84
  // enables JSONL stdin so the daemon can answer AskUserQuestion tool
85
85
  // calls. `--output-format stream-json` gives us SSE-style events.
@@ -67,7 +67,7 @@ export const cursorAgentDef = {
67
67
  // which then surfaces as "your message only contains '-'". Keep stdin
68
68
  // piped for prompt delivery, but do not append a fake prompt arg.
69
69
  buildArgs: ({ prompt: _prompt, imagePaths: _imagePaths }, { extraAllowedDirs: _extra, options = {}, runtimeContext = {} } = {}) => {
70
- const caps = agentCapabilities.get('cursor-agent') || {};
70
+ const caps = (options.capabilities ?? agentCapabilities.get('cursor-agent')) || {};
71
71
  const args = [];
72
72
  args.push('--print', '--output-format', 'stream-json', '--stream-partial-output');
73
73
  // See `RuntimeBuildOptions.permissionMode`'s doc: bypass is the default
@@ -58,7 +58,7 @@ export const opencodeAgentDef = {
58
58
  // (`skipPermissions`, probed from `--help` text) AND whether the caller wants it — see
59
59
  // `RuntimeBuildOptions.permissionMode`'s doc. Bypass remains the default (unchanged
60
60
  // behavior) unless a caller explicitly opts into a restricted run.
61
- if (agentCapabilities.get('opencode')?.skipPermissions && options.permissionMode !== 'restricted') {
61
+ if ((options.capabilities ?? agentCapabilities.get('opencode'))?.skipPermissions && options.permissionMode !== 'restricted') {
62
62
  args.push(SKIP_PERMISSIONS_FLAG);
63
63
  }
64
64
  // Capture-style resume: OpenCode mints its own session id (reported on
@@ -1,6 +1,7 @@
1
+ import { type AgentRuntimeState } from './runtime-state.js';
1
2
  import type { ModelDiscoveryContext, ModelDiscoveryDeps, ModelCatalogSnapshot, DefaultModelResolution } from './model-discovery-types.js';
2
3
  import { type AmrProfileResolver } from './amr-profile-resolver.js';
3
- import type { AgentDiagnostic, DetectedAgent, RuntimeAgentDef, RuntimeModelSource, RuntimeModelOption } from './types.js';
4
+ import type { AgentDiagnostic, DetectedAgent, RuntimeAgentDef, RuntimeCapabilityMap, RuntimeModelSource, RuntimeModelOption } from './types.js';
4
5
  type FetchedRuntimeModels = {
5
6
  models: RuntimeModelOption[];
6
7
  source: RuntimeModelSource;
@@ -30,6 +31,14 @@ export declare function ensureAgentCapabilities({ def, launchPath, env }: {
30
31
  def: RuntimeAgentDef;
31
32
  launchPath: string;
32
33
  env: NodeJS.ProcessEnv;
34
+ }, { state, probe }?: {
35
+ state?: AgentRuntimeState;
36
+ probe?: (required: {
37
+ helpArgs: readonly string[];
38
+ capabilityFlags: Readonly<Record<string, string>>;
39
+ launchPath: string;
40
+ env: NodeJS.ProcessEnv;
41
+ }, optional: Record<string, never>) => Promise<RuntimeCapabilityMap>;
33
42
  }): Promise<void>;
34
43
  /** Result of {@link probeAgentModels}: the models to surface and where they came from. */
35
44
  export type ProbedAgentModels = FetchedRuntimeModels;
@@ -47,7 +56,9 @@ export type ProbedAgentModels = FetchedRuntimeModels;
47
56
  */
48
57
  export declare function probeAgentModels({ def }: {
49
58
  def: RuntimeAgentDef;
50
- }, { configuredEnv, amrProfileResolver, context, deps, force }?: {
59
+ }, { configuredEnv, amrProfileResolver, context, deps, force, state, env }?: {
60
+ state?: AgentRuntimeState;
61
+ env?: NodeJS.ProcessEnv;
51
62
  configuredEnv?: Record<string, string>;
52
63
  amrProfileResolver?: AmrProfileResolver;
53
64
  context?: ModelDiscoveryContext;
@@ -58,13 +69,23 @@ export interface AgentDetectionModelOptions {
58
69
  context?: ModelDiscoveryContext;
59
70
  deps?: ModelDiscoveryDeps;
60
71
  force?: boolean;
72
+ state?: AgentRuntimeState;
73
+ env?: NodeJS.ProcessEnv;
74
+ probeAgent?: (input: {
75
+ def: RuntimeAgentDef;
76
+ configuredEnv: Record<string, string>;
77
+ amrProfileResolver: AmrProfileResolver;
78
+ }, options: AgentDetectionModelOptions) => Promise<DetectedAgent>;
61
79
  }
62
- export declare function detectAgents({}: {}, { configuredEnvByAgent, amrProfileResolver, modelContextByAgent, modelDiscoveryDeps, force }?: {
80
+ export declare function detectAgents({}: {}, { configuredEnvByAgent, amrProfileResolver, modelContextByAgent, modelDiscoveryDeps, force, state, env, probeAgent }?: {
81
+ state?: AgentRuntimeState;
82
+ env?: NodeJS.ProcessEnv;
63
83
  configuredEnvByAgent?: Record<string, Record<string, string>>;
64
84
  amrProfileResolver?: AmrProfileResolver;
65
85
  modelContextByAgent?: Record<string, ModelDiscoveryContext>;
66
86
  modelDiscoveryDeps?: ModelDiscoveryDeps;
67
87
  force?: boolean;
88
+ probeAgent?: AgentDetectionModelOptions['probeAgent'];
68
89
  }): Promise<(Omit<RuntimeAgentDef, "discoverModels" | "resolveDefaultModel" | "buildArgs" | "listModels" | "fetchModels" | "deriveReasoningOptions" | "fallbackModels" | "fallbackModelsAssertedAt" | "helpArgs" | "capabilityFlags" | "fallbackBins" | "versionProbeTimeoutMs" | "maxPromptArgBytes" | "env" | "inactivityTimeoutMs" | "authProbe" | "needsAgentLogFile" | "stdoutPolicy" | "runtimeLock"> & {
69
90
  models: RuntimeModelOption[];
70
91
  modelsSource: RuntimeModelSource;
@@ -77,12 +98,15 @@ export declare function detectAgents({}: {}, { configuredEnvByAgent, amrProfileR
77
98
  version?: string | null;
78
99
  diagnostics?: AgentDiagnostic[];
79
100
  })[]>;
80
- export declare function detectAgentsStream({}: {}, { configuredEnvByAgent, amrProfileResolver, modelContextByAgent, modelDiscoveryDeps, force }?: {
101
+ export declare function detectAgentsStream({}: {}, { configuredEnvByAgent, amrProfileResolver, modelContextByAgent, modelDiscoveryDeps, force, state, env, probeAgent }?: {
102
+ state?: AgentRuntimeState;
103
+ env?: NodeJS.ProcessEnv;
81
104
  configuredEnvByAgent?: Record<string, Record<string, string>>;
82
105
  amrProfileResolver?: AmrProfileResolver;
83
106
  modelContextByAgent?: Record<string, ModelDiscoveryContext>;
84
107
  modelDiscoveryDeps?: ModelDiscoveryDeps;
85
108
  force?: boolean;
109
+ probeAgent?: AgentDetectionModelOptions['probeAgent'];
86
110
  }): AsyncGenerator<DetectedAgent>;
87
111
  export {};
88
112
  //# sourceMappingURL=detection.d.ts.map