@jini-ai/agent-runtime 0.4.4 → 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.
package/dist/index.d.ts CHANGED
@@ -48,5 +48,6 @@ export * from './providers/index.js';
48
48
  /** ACP + pi-rpc protocol adapters over the shared JSON-line-stream core. See aliases above. */
49
49
  export { createJsonLineStream, type AcpMcpServerInput, type AcpPermissionDecision, type AcpPermissionHandler, type AcpPermissionOption, type AcpPermissionRequest, type AcpSessionController, type ModelOption, type AttachAcpSessionOptions, type AccountFailure, type AccountFailureClassifier, buildAcpSessionNewParams, normalizeModels, detectAcpModels, attachAcpSession, noopAccountFailureClassifier, mapPiRpcEvent, attachPiRpcSession, parsePiModels as parsePiRpcModels, type PiRpcSession, type PiRpcSessionOptions, } from './agent-protocol/index.js';
50
50
  export * from './model-discovery.js';
51
- export { defaultModelDiscoveryDeps, parseClaudeInitializeMetadata, parsePiRpcMetadata } from './model-discovery-adapters.js';
51
+ export { getRuntimeModelDiscoveryDeps, defaultModelDiscoveryDeps, parseClaudeInitializeMetadata, parsePiRpcMetadata } from './model-discovery-adapters.js';
52
+ export * from './runtime-state.js';
52
53
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -71,5 +71,6 @@ export * from './providers/index.js';
71
71
  /** ACP + pi-rpc protocol adapters over the shared JSON-line-stream core. See aliases above. */
72
72
  export { createJsonLineStream, buildAcpSessionNewParams, normalizeModels, detectAcpModels, attachAcpSession, noopAccountFailureClassifier, mapPiRpcEvent, attachPiRpcSession, parsePiModels as parsePiRpcModels, } from './agent-protocol/index.js';
73
73
  export * from './model-discovery.js';
74
- export { defaultModelDiscoveryDeps, parseClaudeInitializeMetadata, parsePiRpcMetadata } from './model-discovery-adapters.js';
74
+ export { getRuntimeModelDiscoveryDeps, defaultModelDiscoveryDeps, parseClaudeInitializeMetadata, parsePiRpcMetadata } from './model-discovery-adapters.js';
75
+ export * from './runtime-state.js';
75
76
  //# sourceMappingURL=index.js.map
@@ -1,11 +1,33 @@
1
+ /**
2
+ * @module invocation
3
+ *
4
+ * Short, read-only metadata probes (model-list / version / help /
5
+ * auth-status) for an agent CLI. Ported from OD's
6
+ * `apps/daemon/src/runtimes/invocation.ts` with one dependency swap:
7
+ * `createCommandInvocation` now comes from `@jini-ai/platform` instead of the
8
+ * OD workspace package it originally shipped from — the two are the same
9
+ * function (platform was already verbatim-lifted into `@jini-ai/platform`,
10
+ * see its `archived provenance ledger`).
11
+ */
12
+ import { execFile } from 'node:child_process';
1
13
  import type { RuntimeExecOptions } from './types.js';
14
+ declare const execFileP: typeof execFile.__promisify__;
15
+ type ProbeExecPort = (input: {
16
+ command: string;
17
+ args: string[];
18
+ options: RuntimeExecOptions & {
19
+ windowsVerbatimArguments?: boolean | undefined;
20
+ };
21
+ }, optional: Record<string, never>) => ReturnType<typeof execFileP>;
2
22
  export declare function execAgentFile({ command, args }: {
3
23
  command: string;
4
24
  args: string[];
5
- }, { options }?: {
25
+ }, { options, execute }?: {
6
26
  options?: RuntimeExecOptions;
7
- }): import("child_process").PromiseWithChild<{
27
+ execute?: ProbeExecPort;
28
+ }): Promise<{
8
29
  stdout: string | NonSharedBuffer;
9
30
  stderr: string | NonSharedBuffer;
10
31
  }>;
32
+ export {};
11
33
  //# sourceMappingURL=invocation.d.ts.map
@@ -10,6 +10,7 @@
10
10
  * see its `archived provenance ledger`).
11
11
  */
12
12
  import { execFile } from 'node:child_process';
13
+ import { scheduleProbeProcess } from './probe-scheduler.js';
13
14
  import os from 'node:os';
14
15
  import { promisify } from 'node:util';
15
16
  import { createCommandInvocation } from '@jini-ai/platform';
@@ -27,7 +28,7 @@ const execFileP = promisify(execFile);
27
28
  // running dev server. A probe writing a stray lockfile under the OS temp
28
29
  // dir is harmless. Actual agent runs spawn elsewhere with an explicit
29
30
  // project cwd and are unaffected.
30
- export function execAgentFile({ command, args }, { options = {} } = {}) {
31
+ export function execAgentFile({ command, args }, { options = {}, execute = (input) => execFileP(input.command, input.args, input.options) } = {}) {
31
32
  const invocation = createCommandInvocation(options.env
32
33
  ? {
33
34
  command,
@@ -38,10 +39,21 @@ export function execAgentFile({ command, args }, { options = {} } = {}) {
38
39
  command,
39
40
  args,
40
41
  });
41
- return execFileP(invocation.command, invocation.args, {
42
- ...options,
43
- cwd: options.cwd ?? os.tmpdir(),
44
- windowsVerbatimArguments: invocation.windowsVerbatimArguments,
45
- });
42
+ return scheduleProbeProcess({ work: async () => {
43
+ const pending = execute({ command: invocation.command, args: invocation.args, options: {
44
+ killSignal: 'SIGKILL', ...options, cwd: options.cwd ?? os.tmpdir(),
45
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments,
46
+ } }, {});
47
+ try {
48
+ return await pending;
49
+ }
50
+ finally {
51
+ // AbortError can precede close. Hold the shared slot until the child actually exits.
52
+ const child = pending.child;
53
+ if (child?.pid && child.exitCode === null && child.signalCode === null) {
54
+ await new Promise(resolve => { child.once('close', () => resolve()); });
55
+ }
56
+ }
57
+ } }, { priority: args.includes('--version') ? 'version' : 'metadata', signal: options.signal });
46
58
  }
47
59
  //# sourceMappingURL=invocation.js.map
package/dist/launch.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ExecutableResolutionOptions } from './executables.js';
1
2
  import { inspectAgentExecutableResolution } from './executables.js';
2
3
  import type { RuntimeAgentDef } from './types.js';
3
4
  export type AgentLaunchKind = 'selected' | 'codex-native';
@@ -9,7 +10,7 @@ export type AgentLaunchResolution = ReturnType<typeof inspectAgentExecutableReso
9
10
  };
10
11
  export declare function resolveAgentLaunch({ def }: {
11
12
  def: RuntimeAgentDef;
12
- }, { configuredEnv }?: {
13
+ }, options?: ExecutableResolutionOptions & {
13
14
  configuredEnv?: Record<string, string>;
14
15
  }): AgentLaunchResolution;
15
16
  export declare function applyAgentLaunchEnv({ env, launch }: {
package/dist/launch.js CHANGED
@@ -5,8 +5,8 @@
5
5
  import { accessSync, closeSync, constants, openSync, readdirSync, readSync, realpathSync, statSync } from 'node:fs';
6
6
  import path, { delimiter } from 'node:path';
7
7
  import { inspectAgentExecutableResolution, userToolchainBinDirs } from './executables.js';
8
- export function resolveAgentLaunch({ def }, { configuredEnv = {} } = {}) {
9
- const resolution = inspectAgentExecutableResolution({ def: def }, { configuredEnv: configuredEnv });
8
+ export function resolveAgentLaunch({ def }, options = {}) {
9
+ const resolution = inspectAgentExecutableResolution({ def: def }, options);
10
10
  if (!resolution.selectedPath) {
11
11
  return { ...resolution, launchPath: null, launchKind: 'selected', childPathPrepend: [], diagnostic: null };
12
12
  }
@@ -1,3 +1,4 @@
1
+ import { type AgentRuntimeState } from './runtime-state.js';
1
2
  import { type AgentDiscoveryConfiguration } from './model-discovery.js';
2
3
  import type { ModelDiscoveryContext, ModelDiscoveryDeps, ModelProbeResult } from './model-discovery-types.js';
3
4
  export declare function modelProbeTimeoutMs(agentId: string): number;
@@ -19,4 +20,8 @@ export declare function probeAgentCatalog(agentId: string, context: ModelDiscove
19
20
  export declare const defaultModelDiscoveryDeps: ModelDiscoveryDeps;
20
21
  /** Pi's native RPC supplies catalog AND current provider/id without making an inference call. */
21
22
  export declare function parsePiRpcMetadata(stdout: string): ModelProbeResult;
23
+ /** Stable dependency identity preserves the existing per-host single-probe budget as well as its cache. */
24
+ export declare function getRuntimeModelDiscoveryDeps({ state }: {
25
+ state: AgentRuntimeState;
26
+ }, _optional?: Record<string, never>): ModelDiscoveryDeps;
22
27
  //# sourceMappingURL=model-discovery-adapters.d.ts.map
@@ -1,3 +1,5 @@
1
+ import { defaultRuntimeState } from './runtime-state.js';
2
+ import { VENDOR_API_BASE_URLS } from '@jini-ai/integrations/media-providers/catalog';
1
3
  import { execFile } from 'node:child_process';
2
4
  import { readFile, stat } from 'node:fs/promises';
3
5
  import { homedir } from 'node:os';
@@ -7,6 +9,7 @@ import { detectAcpModelMetadata } from './agent-protocol/acp/models.js';
7
9
  import { CLAUDE_CODE_INITIALIZE_ARGS, CLAUDE_CODE_INITIALIZE_REQUEST } from './claude-code-models.js';
8
10
  import { listProviderModels } from './providers/model-catalog.js';
9
11
  import { execAgentFile } from './invocation.js';
12
+ import { scheduleProbeProcess } from './probe-scheduler.js';
10
13
  import { probeCopilotSdk, probeDroidSdk } from './model-discovery-sdk.js';
11
14
  import { parseSettingsJson, readModelConfiguration } from './model-discovery-config.js';
12
15
  import { ModelProbeError, modelIdentityKind, withModelProbeTimeout } from './model-discovery.js';
@@ -252,49 +255,53 @@ function runProcess(input) {
252
255
  cwd: input.context.cwd, env: input.context.env, signal: input.signal,
253
256
  timeout: input.timeoutMs, killSignal: 'SIGKILL', maxBuffer: 8 * 1024 * 1024,
254
257
  } }).then(({ stdout, stderr }) => ({ stdout: String(stdout), stderr: String(stderr) }));
255
- return new Promise((resolve, reject) => {
256
- let completed = false;
257
- let captured = '';
258
- const invocation = createCommandInvocation({ command: input.context.executable, args: [...input.args], env: input.context.env });
259
- const tree = input.terminateProcessTree && process.platform !== 'win32';
260
- let treeTimer;
261
- const killTree = () => { if (tree && child.pid) {
262
- try {
263
- process.kill(-child.pid, 'SIGKILL');
264
- }
265
- catch { }
266
- } };
267
- // execFile forwards `detached` to spawn at runtime, but its typings omit it; a typed
268
- // variable (not a literal) keeps the process-group option without a cast.
269
- const options = { cwd: input.context.cwd, env: input.context.env,
270
- timeout: input.timeoutMs, killSignal: 'SIGKILL', signal: input.signal, maxBuffer: 8 * 1024 * 1024,
271
- detached: Boolean(tree),
272
- windowsVerbatimArguments: invocation.windowsVerbatimArguments, };
273
- const child = execFile(invocation.command, invocation.args, options, (error, stdout, stderr) => {
274
- if (treeTimer)
275
- clearTimeout(treeTimer);
276
- input.signal.removeEventListener('abort', killTree);
277
- if (error)
278
- killTree();
279
- error && !completed ? reject(error) : resolve({ stdout: String(stdout), stderr: String(stderr) });
280
- });
281
- if (tree) {
282
- input.signal.addEventListener('abort', killTree, { once: true });
283
- treeTimer = setTimeout(killTree, input.timeoutMs);
284
- if (input.signal.aborted)
285
- killTree();
286
- }
287
- if (input.complete)
288
- child.stdout?.on('data', (chunk) => {
289
- captured += String(chunk);
290
- if (input.complete?.(captured)) {
291
- completed = true;
292
- child.kill('SIGKILL');
258
+ return scheduleProbeProcess({ work: () => new Promise((resolve, reject) => {
259
+ let completed = false;
260
+ let captured = '';
261
+ const invocation = createCommandInvocation({ command: input.context.executable, args: [...input.args], env: input.context.env });
262
+ const tree = input.terminateProcessTree && process.platform !== 'win32';
263
+ let treeTimer;
264
+ const killTree = () => { if (tree && child.pid) {
265
+ try {
266
+ process.kill(-child.pid, 'SIGKILL');
293
267
  }
268
+ catch { }
269
+ } };
270
+ // execFile forwards `detached` to spawn at runtime, but its typings omit it; a typed
271
+ // variable (not a literal) keeps the process-group option without a cast.
272
+ const options = { cwd: input.context.cwd, env: input.context.env,
273
+ timeout: input.timeoutMs, killSignal: 'SIGKILL', signal: input.signal, maxBuffer: 8 * 1024 * 1024,
274
+ detached: Boolean(tree),
275
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments, };
276
+ const child = execFile(invocation.command, invocation.args, options, (error, stdout, stderr) => {
277
+ if (treeTimer)
278
+ clearTimeout(treeTimer);
279
+ input.signal.removeEventListener('abort', killTree);
280
+ if (error)
281
+ killTree();
282
+ const settle = () => { error && !completed ? reject(error) : resolve({ stdout: String(stdout), stderr: String(stderr) }); };
283
+ if (child.pid && child.exitCode === null && child.signalCode === null)
284
+ child.once('close', settle);
285
+ else
286
+ settle();
294
287
  });
295
- child.stdin?.on('error', () => { });
296
- child.stdin?.end(input.stdin);
297
- });
288
+ if (tree) {
289
+ input.signal.addEventListener('abort', killTree, { once: true });
290
+ treeTimer = setTimeout(killTree, input.timeoutMs);
291
+ if (input.signal.aborted)
292
+ killTree();
293
+ }
294
+ if (input.complete)
295
+ child.stdout?.on('data', (chunk) => {
296
+ captured += String(chunk);
297
+ if (input.complete?.(captured)) {
298
+ completed = true;
299
+ child.kill('SIGKILL');
300
+ }
301
+ });
302
+ child.stdin?.on('error', () => { });
303
+ child.stdin?.end(input.stdin);
304
+ }) }, { signal: input.signal });
298
305
  }
299
306
  export const defaultModelDiscoveryDeps = {
300
307
  process: { run: runProcess },
@@ -319,29 +326,29 @@ export const defaultModelDiscoveryDeps = {
319
326
  // handles. BYOK env is usable only when it identifies this CLI's effective provider.
320
327
  const env = context.env;
321
328
  if (agentId === 'claude' && env.ANTHROPIC_API_KEY)
322
- return { protocol: 'anthropic', baseUrl: env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com', apiKey: env.ANTHROPIC_API_KEY };
329
+ return { protocol: 'anthropic', baseUrl: env.ANTHROPIC_BASE_URL || VENDOR_API_BASE_URLS.anthropic, apiKey: env.ANTHROPIC_API_KEY };
323
330
  const selected = context.model || (agentId === 'aider' ? env.AIDER_MODEL : agentId === 'qwen' ? env.QWEN_MODEL || env.OPENAI_MODEL : undefined);
324
331
  if (!selected)
325
332
  return null;
326
333
  if (/^(anthropic\/|claude-)/.test(selected) && env.ANTHROPIC_API_KEY)
327
- return { protocol: 'anthropic', baseUrl: env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com', apiKey: env.ANTHROPIC_API_KEY };
334
+ return { protocol: 'anthropic', baseUrl: env.ANTHROPIC_BASE_URL || VENDOR_API_BASE_URLS.anthropic, apiKey: env.ANTHROPIC_API_KEY };
328
335
  if (/^(gemini\/|google\/|gemini-)/.test(selected) && env.GEMINI_API_KEY)
329
- return { protocol: 'google', baseUrl: 'https://generativelanguage.googleapis.com', apiKey: env.GEMINI_API_KEY };
336
+ return { protocol: 'google', baseUrl: VENDOR_API_BASE_URLS.google, apiKey: env.GEMINI_API_KEY };
330
337
  if (/^openrouter\//.test(selected) && env.OPENROUTER_API_KEY)
331
- return { protocol: 'openai', baseUrl: env.OPENROUTER_API_BASE || 'https://openrouter.ai/api/v1', apiKey: env.OPENROUTER_API_KEY };
338
+ return { protocol: 'openai', baseUrl: env.OPENROUTER_API_BASE || VENDOR_API_BASE_URLS.openrouter, apiKey: env.OPENROUTER_API_KEY };
332
339
  if ((/^(openai\/|gpt-|o\d)/.test(selected) || agentId === 'qwen' && (env.OPENAI_MODEL || env.OPENAI_BASE_URL)) && env.OPENAI_API_KEY)
333
- return { protocol: 'openai', baseUrl: env.OPENAI_BASE_URL || env.OPENAI_API_BASE || 'https://api.openai.com/v1', apiKey: env.OPENAI_API_KEY };
340
+ return { protocol: 'openai', baseUrl: env.OPENAI_BASE_URL || env.OPENAI_API_BASE || VENDOR_API_BASE_URLS.openai, apiKey: env.OPENAI_API_KEY };
334
341
  return null;
335
342
  } },
336
343
  acp: { async probe({ context, args, signal }) {
337
344
  const invocation = createCommandInvocation({ command: context.executable, args: [...args], env: context.env });
338
- const result = await detectAcpModelMetadata({ bin: invocation.command, args: invocation.args }, { cwd: context.cwd, env: context.env, timeoutMs: 15_000, signal });
345
+ const result = await scheduleProbeProcess({ work: () => detectAcpModelMetadata({ bin: invocation.command, args: invocation.args }, { cwd: context.cwd, env: context.env, timeoutMs: 15_000, signal }) }, { signal });
339
346
  return { models: result.models, source: 'rpc', coverage: 'account', ...(result.currentModelId ? { defaultSelectionId: result.currentModelId } : {}) };
340
347
  } },
341
348
  rpc: { async probe({ agentId, context, signal }) {
342
349
  // UNVERIFIED locally: use official prompt-free SDK interfaces, not invented CLI flags.
343
350
  if (agentId === 'copilot')
344
- return probeCopilotSdk(context, signal);
351
+ return scheduleProbeProcess({ work: () => probeCopilotSdk(context, signal) }, { signal });
345
352
  if (agentId === 'droid')
346
353
  return probeDroidSdk(context, defaultModelDiscoveryDeps, signal);
347
354
  if (agentId !== 'pi')
@@ -358,7 +365,7 @@ export const defaultModelDiscoveryDeps = {
358
365
  } });
359
366
  return parsePiRpcMetadata(stdout);
360
367
  } },
361
- clock: { now: Date.now }, cache: new Map(),
368
+ clock: { now: Date.now }, cache: defaultRuntimeState.modelCatalog,
362
369
  };
363
370
  /** Pi's native RPC supplies catalog AND current provider/id without making an inference call. */
364
371
  export function parsePiRpcMetadata(stdout) {
@@ -388,4 +395,10 @@ export function parsePiRpcMetadata(stdout) {
388
395
  throw new ModelProbeError('malformed-response');
389
396
  return { models, source: 'rpc', coverage: 'account', ...(defaultSelectionId ? { defaultSelectionId } : {}) };
390
397
  }
398
+ /** Stable dependency identity preserves the existing per-host single-probe budget as well as its cache. */
399
+ export function getRuntimeModelDiscoveryDeps({ state }, _optional = {}) {
400
+ if (state === defaultRuntimeState)
401
+ return defaultModelDiscoveryDeps;
402
+ return state.discoveryDeps ??= { ...defaultModelDiscoveryDeps, cache: state.modelCatalog };
403
+ }
391
404
  //# sourceMappingURL=model-discovery-adapters.js.map
@@ -30,11 +30,11 @@ export function modelLaunchFingerprint(context, configMaterial = '') {
30
30
  class ProbeTimeout extends Error {
31
31
  }
32
32
  const probePools = new WeakMap();
33
- /** Four simultaneous cold probes per host; queued callers keep cancellation responsive. */
33
+ /** One cold catalog probe per host; queued callers keep cancellation responsive. */
34
34
  async function boundedProbe(deps, work, signal) {
35
35
  const pool = probePools.get(deps) || { active: 0, queue: [] };
36
36
  probePools.set(deps, pool);
37
- if (pool.active >= 4) {
37
+ if (pool.active >= 1) {
38
38
  await new Promise((resolve, reject) => {
39
39
  const ready = () => { signal?.removeEventListener('abort', abort); resolve(); };
40
40
  const abort = () => { const index = pool.queue.indexOf(ready); if (index >= 0)
package/dist/models.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type AgentRuntimeState } from './runtime-state.js';
1
2
  /**
2
3
  * @module models
3
4
  * Model-option defaults, a per-agent last-surfaced live-model cache and model-id sanitization.
@@ -7,13 +8,15 @@ export declare const DEFAULT_MODEL_OPTION: RuntimeModelOption;
7
8
  export declare function rememberLiveModels({ agentId, models }: {
8
9
  agentId: string;
9
10
  models: RuntimeModelOption[];
10
- }, { scope }?: {
11
+ }, { scope, state }?: {
11
12
  scope?: string | null;
13
+ state?: AgentRuntimeState;
12
14
  }): void;
13
15
  export declare function getRememberedLiveModels({ agentId }: {
14
16
  agentId: string;
15
- }, { scope }?: {
17
+ }, { scope, state }?: {
16
18
  scope?: string | null;
19
+ state?: AgentRuntimeState;
17
20
  }): RuntimeModelOption[];
18
21
  export declare function preferFreshLiveModels({ freshModels, rememberedModels }: {
19
22
  freshModels: RuntimeModelOption[];
@@ -22,15 +25,17 @@ export declare function preferFreshLiveModels({ freshModels, rememberedModels }:
22
25
  export declare function isKnownModel({ def, modelId }: {
23
26
  def: RuntimeAgentDef;
24
27
  modelId: string | null | undefined;
25
- }, { scope }?: {
28
+ }, { scope, state }?: {
26
29
  scope?: string | null;
30
+ state?: AgentRuntimeState;
27
31
  }): boolean;
28
32
  export declare function resolveModelForAgent({ def, resolved }: {
29
33
  def: RuntimeAgentDef;
30
34
  resolved: string | null;
31
- }, { env, liveModelScope }?: {
35
+ }, { env, liveModelScope, state }?: {
32
36
  env?: Record<string, string | undefined>;
33
37
  liveModelScope?: string | null;
38
+ state?: AgentRuntimeState;
34
39
  }): string | null;
35
40
  export declare function sanitizeCustomModel({ id }: {
36
41
  id: string | null | undefined;
package/dist/models.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { defaultRuntimeState } from './runtime-state.js';
1
2
  export const DEFAULT_MODEL_OPTION = {
2
3
  id: 'default',
3
4
  label: 'Default (CLI config)',
@@ -7,33 +8,32 @@ export const DEFAULT_MODEL_OPTION = {
7
8
  // recent live list (refreshed every detectAgents() call) and additionally
8
9
  // trust any value present in the static fallback. A model that's neither
9
10
  // gets rejected so a stale or hostile value can't smuggle arbitrary flags.
10
- const liveModelCache = new Map();
11
- const liveModelOrder = new Map();
11
+ // Host state carries these maps; legacy callers use defaultRuntimeState.
12
12
  function liveModelCacheKey(agentId, scope) {
13
13
  const trimmedScope = typeof scope === 'string' ? scope.trim() : '';
14
14
  return trimmedScope ? `${agentId}\0${trimmedScope}` : agentId;
15
15
  }
16
- export function rememberLiveModels({ agentId, models }, { scope } = {}) {
16
+ export function rememberLiveModels({ agentId, models }, { scope, state = defaultRuntimeState } = {}) {
17
17
  if (!Array.isArray(models))
18
18
  return;
19
19
  const ids = models
20
20
  .map((m) => m && m.id)
21
21
  .filter((id) => typeof id === 'string');
22
22
  const key = liveModelCacheKey(agentId, scope);
23
- liveModelCache.set(key, new Set(ids));
24
- liveModelOrder.set(key, ids);
23
+ state.liveModelCache.set(key, new Set(ids));
24
+ state.liveModelOrder.set(key, ids);
25
25
  }
26
- export function getRememberedLiveModels({ agentId }, { scope } = {}) {
27
- const ids = liveModelOrder.get(liveModelCacheKey(agentId, scope)) ?? [];
26
+ export function getRememberedLiveModels({ agentId }, { scope, state = defaultRuntimeState } = {}) {
27
+ const ids = state.liveModelOrder.get(liveModelCacheKey(agentId, scope)) ?? [];
28
28
  return ids.map((id) => ({ id, label: id }));
29
29
  }
30
30
  export function preferFreshLiveModels({ freshModels, rememberedModels }) {
31
31
  return freshModels.length > 0 ? freshModels : rememberedModels;
32
32
  }
33
- export function isKnownModel({ def, modelId }, { scope } = {}) {
33
+ export function isKnownModel({ def, modelId }, { scope, state = defaultRuntimeState } = {}) {
34
34
  if (!modelId)
35
35
  return false;
36
- const live = liveModelCache.get(liveModelCacheKey(def.id, scope));
36
+ const live = state.liveModelCache.get(liveModelCacheKey(def.id, scope));
37
37
  if (live && live.has(modelId))
38
38
  return true;
39
39
  if (Array.isArray(def.fallbackModels)) {
@@ -49,7 +49,7 @@ export function isKnownModel({ def, modelId }, { scope } = {}) {
49
49
  // surfaced to the UI, then fall back to the def's first concrete fallback
50
50
  // id so the spawn layer always has a real model to forward. Defs that DO
51
51
  // list 'default' (the common case) are left untouched.
52
- export function resolveModelForAgent({ def, resolved }, { env = process.env, liveModelScope } = {}) {
52
+ export function resolveModelForAgent({ def, resolved }, { env = process.env, liveModelScope, state = defaultRuntimeState } = {}) {
53
53
  if (resolved && resolved !== 'default')
54
54
  return resolved;
55
55
  // Host-process env override. Lets an operator pin a different fallback
@@ -66,7 +66,7 @@ export function resolveModelForAgent({ def, resolved }, { env = process.env, liv
66
66
  const fallbacks = def.fallbackModels;
67
67
  if (fallbacks.some((m) => m.id === 'default'))
68
68
  return resolved;
69
- const liveModels = liveModelOrder.get(liveModelCacheKey(def.id, liveModelScope)) ?? [];
69
+ const liveModels = state.liveModelOrder.get(liveModelCacheKey(def.id, liveModelScope)) ?? [];
70
70
  const firstLive = liveModels[0];
71
71
  if (firstLive)
72
72
  return firstLive;
@@ -0,0 +1,19 @@
1
+ type Priority = 'version' | 'metadata';
2
+ /** All metadata process adapters share one process-wide slot. Version checks precede queued
3
+ * metadata, so expensive discovery cannot delay cheap availability checks. Actual chat runs
4
+ * never use this queue. A slot is held until the child adapter settles, including cancellation.
5
+ * @complexity O(q) queued selection, bounded by the runtime registry's probe count. */
6
+ export declare function createProbeScheduler(_input: Record<string, never>, _options?: Record<string, never>): <T>({ work }: {
7
+ work: () => Promise<T>;
8
+ }, { priority, signal }?: {
9
+ priority?: Priority;
10
+ signal?: AbortSignal | undefined;
11
+ }) => Promise<T>;
12
+ export declare const scheduleProbeProcess: <T>({ work }: {
13
+ work: () => Promise<T>;
14
+ }, { priority, signal }?: {
15
+ priority?: Priority;
16
+ signal?: AbortSignal | undefined;
17
+ }) => Promise<T>;
18
+ export {};
19
+ //# sourceMappingURL=probe-scheduler.d.ts.map
@@ -0,0 +1,30 @@
1
+ /** All metadata process adapters share one process-wide slot. Version checks precede queued
2
+ * metadata, so expensive discovery cannot delay cheap availability checks. Actual chat runs
3
+ * never use this queue. A slot is held until the child adapter settles, including cancellation.
4
+ * @complexity O(q) queued selection, bounded by the runtime registry's probe count. */
5
+ export function createProbeScheduler(_input, _options = {}) {
6
+ const queue = [];
7
+ let running = false;
8
+ function drain() {
9
+ if (running || !queue.length)
10
+ return;
11
+ const version = queue.findIndex(entry => entry.priority === 'version');
12
+ const entry = queue.splice(version < 0 ? 0 : version, 1)[0];
13
+ running = true;
14
+ entry.start();
15
+ }
16
+ return function schedule({ work }, { priority = 'metadata', signal } = {}) {
17
+ return new Promise((resolve, reject) => {
18
+ queue.push({ priority, start: () => {
19
+ Promise.resolve().then(() => {
20
+ if (signal?.aborted)
21
+ throw new Error('Probe cancelled');
22
+ return work();
23
+ }).then(resolve, reject).finally(() => { running = false; drain(); });
24
+ } });
25
+ drain();
26
+ });
27
+ };
28
+ }
29
+ export const scheduleProbeProcess = createProbeScheduler({});
30
+ //# sourceMappingURL=probe-scheduler.js.map
@@ -0,0 +1,41 @@
1
+ /** Mutable runtime data belongs to one host. Compatibility functions use defaultRuntimeState. */
2
+ import type { RuntimeAgentDef, RuntimeCapabilityMap, RuntimeModelOption, DetectedAgent } from './types.js';
3
+ import type { ModelCacheEntry, ModelDiscoveryDeps } from './model-discovery-types.js';
4
+ export interface AgentRuntimeState {
5
+ /** One in-flight or settled complete detection per launch scope until explicit Rescan. */
6
+ readonly detections: Map<string, Promise<DetectedAgent>>;
7
+ readonly capabilities: Map<string, RuntimeCapabilityMap>;
8
+ /** One in-flight or settled `--help` probe per def id + launch path, so a run never re-spawns it. */
9
+ readonly capabilityProbes: Map<string, Promise<void>>;
10
+ readonly liveModelCache: Map<string, Set<string>>;
11
+ readonly liveModelOrder: Map<string, string[]>;
12
+ readonly claudeModels: Map<string, {
13
+ expiresAt: number;
14
+ value: Promise<readonly RuntimeModelOption[] | null>;
15
+ }>;
16
+ discoveryDeps?: ModelDiscoveryDeps;
17
+ readonly modelCatalog: Map<string, ModelCacheEntry>;
18
+ readonly executable: {
19
+ /** Env var names consulted by detection-home/resource-root resolution. Overridable via `configureExecutableResolutionEnv`. */
20
+ envNames: {
21
+ agentHomeEnvVar: string;
22
+ resourceRootEnvVar: string;
23
+ };
24
+ toolchainKey: string | null;
25
+ toolchainDirs: string[] | null;
26
+ toolchainDirsAt: number;
27
+ };
28
+ /** Clear settled caches after host operations finish; retain its configured env names. */
29
+ clear(required: Record<string, never>, optional: Record<string, never>): void;
30
+ }
31
+ /** Independent state, including in-flight probes, for each embedding host.
32
+ * Returns empty owned maps; no I/O. @complexity O(1) initialization; retained data grows with probes. */
33
+ export declare function createAgentRuntimeState(_required: Record<string, never>, _optional?: Record<string, never>): AgentRuntimeState;
34
+ export declare const defaultRuntimeState: AgentRuntimeState;
35
+ /** Bind the agent argument builder to the same capability state detection and launch use.
36
+ * Returns a definition copy without mutating the registry. @complexity O(1). */
37
+ export declare function bindRuntimeAgentDef({ def, state }: {
38
+ def: RuntimeAgentDef;
39
+ state: AgentRuntimeState;
40
+ }, _optional?: Record<string, never>): RuntimeAgentDef;
41
+ //# sourceMappingURL=runtime-state.d.ts.map
@@ -0,0 +1,31 @@
1
+ /** Independent state, including in-flight probes, for each embedding host.
2
+ * Returns empty owned maps; no I/O. @complexity O(1) initialization; retained data grows with probes. */
3
+ export function createAgentRuntimeState(_required, _optional = {}) {
4
+ const state = {
5
+ detections: new Map(), capabilities: new Map(), capabilityProbes: new Map(), liveModelCache: new Map(), liveModelOrder: new Map(),
6
+ claudeModels: new Map(), modelCatalog: new Map(),
7
+ executable: { envNames: { agentHomeEnvVar: 'AGENT_RUNTIME_HOME', resourceRootEnvVar: 'AGENT_RUNTIME_RESOURCE_ROOT' }, toolchainKey: null, toolchainDirs: null, toolchainDirsAt: 0 },
8
+ clear() {
9
+ state.detections.clear();
10
+ state.capabilities.clear();
11
+ state.capabilityProbes.clear();
12
+ state.liveModelCache.clear();
13
+ state.liveModelOrder.clear();
14
+ state.claudeModels.clear();
15
+ state.modelCatalog.clear();
16
+ state.executable.toolchainKey = null;
17
+ state.executable.toolchainDirs = null;
18
+ state.executable.toolchainDirsAt = 0;
19
+ },
20
+ };
21
+ return state;
22
+ }
23
+ export const defaultRuntimeState = createAgentRuntimeState({});
24
+ /** Bind the agent argument builder to the same capability state detection and launch use.
25
+ * Returns a definition copy without mutating the registry. @complexity O(1). */
26
+ export function bindRuntimeAgentDef({ def, state }, _optional = {}) {
27
+ return { ...def, buildArgs: (required, optional = {}) => def.buildArgs(required, {
28
+ ...optional, options: { ...optional.options, capabilities: state.capabilities.get(def.id) ?? {} },
29
+ }) };
30
+ }
31
+ //# sourceMappingURL=runtime-state.js.map
package/dist/types.d.ts CHANGED
@@ -75,6 +75,8 @@ export type RuntimeReasoningInModelId = {
75
75
  levels: readonly RuntimeReasoningOption[];
76
76
  };
77
77
  export type RuntimeBuildOptions = {
78
+ /** Host-scoped capability flags; absent retains the published compatibility cache. */
79
+ capabilities?: RuntimeCapabilityMap;
78
80
  model?: string | null;
79
81
  reasoning?: string | null;
80
82
  permissionMode?: 'bypass' | 'restricted';
@@ -291,6 +293,8 @@ export type RuntimeAgentDef = Partial<AgentModelDiscoveryPort> & {
291
293
  runtimeContext?: RuntimeContext;
292
294
  }) => string[];
293
295
  streamFormat: string;
296
+ /** Reported money spans a session or one run; absent preserves per-run adapter behavior. */
297
+ usageCostBasis?: 'session' | 'run';
294
298
  fallbackBins?: string[];
295
299
  versionProbeTimeoutMs?: number;
296
300
  helpArgs?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/agent-runtime",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Coding-agent runtime adapters: runtime registry, detection, launch, stream parsers, and the ACP/pi-rpc subprocess transport.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -77,20 +77,25 @@
77
77
  "devDependencies": {
78
78
  "@vitest/coverage-v8": "^2.1.9",
79
79
  "@jini-ai/core": "^0.4.0",
80
- "@jini-ai/oauth": "^0.2.1",
81
- "@jini-ai/platform": "^0.5.2",
82
- "@jini-ai/protocol": "^0.4.3"
80
+ "@jini-ai/oauth": "^0.2.2",
81
+ "@jini-ai/integrations": "^0.4.2",
82
+ "@jini-ai/platform": "^0.5.3",
83
+ "@jini-ai/protocol": "^0.4.4"
83
84
  },
84
85
  "dependencies": {
85
- "@jini-ai/core": "^0.4.2"
86
+ "@jini-ai/core": "^0.4.3"
86
87
  },
87
88
  "peerDependencies": {
89
+ "@jini-ai/integrations": "^0.4.2",
88
90
  "@github/copilot-sdk": "*",
89
- "@jini-ai/oauth": "^0.2.0",
90
- "@jini-ai/platform": "^0.5.1",
91
- "@jini-ai/protocol": "^0.4.1"
91
+ "@jini-ai/oauth": "^0.2.2",
92
+ "@jini-ai/platform": "^0.5.3",
93
+ "@jini-ai/protocol": "^0.4.4"
92
94
  },
93
95
  "peerDependenciesMeta": {
96
+ "@jini-ai/integrations": {
97
+ "optional": true
98
+ },
94
99
  "@github/copilot-sdk": {
95
100
  "optional": true
96
101
  },