@jini-ai/agent-runtime 0.4.1 → 0.4.3

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 (127) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/acp-model-probe.d.ts +6 -27
  3. package/dist/acp-model-probe.js +6 -4
  4. package/dist/agent-protocol/acp/models.d.ts +8 -0
  5. package/dist/agent-protocol/acp/models.js +38 -6
  6. package/dist/agent-protocol/acp/session.js +15 -2
  7. package/dist/agent-protocol/pi-rpc/events.js +4 -0
  8. package/dist/amr-model-cache.d.ts +2 -12
  9. package/dist/amr-profile-resolver.d.ts +3 -14
  10. package/dist/anthropic-live-models.d.ts +1 -1
  11. package/dist/anthropic-live-models.js +0 -0
  12. package/dist/artifact-taxonomy.d.ts +3 -17
  13. package/dist/auth.d.ts +0 -6
  14. package/dist/auth.js +1 -30
  15. package/dist/claude-code-models.d.ts +3 -4
  16. package/dist/claude-code-models.js +5 -7
  17. package/dist/claude-stream.d.ts +0 -4
  18. package/dist/claude-stream.js +4 -4
  19. package/dist/copilot-stream.d.ts +0 -5
  20. package/dist/copilot-stream.js +0 -5
  21. package/dist/defs/aider.d.ts +15 -0
  22. package/dist/defs/aider.js +4 -2
  23. package/dist/defs/amp.d.ts +16 -0
  24. package/dist/defs/amp.js +5 -1
  25. package/dist/defs/amr.d.ts +15 -0
  26. package/dist/defs/amr.js +6 -15
  27. package/dist/defs/antigravity.d.ts +16 -1
  28. package/dist/defs/antigravity.js +3 -2
  29. package/dist/defs/auggie.d.ts +15 -0
  30. package/dist/defs/auggie.js +3 -0
  31. package/dist/defs/claude.d.ts +15 -0
  32. package/dist/defs/claude.js +26 -36
  33. package/dist/defs/cline.d.ts +15 -0
  34. package/dist/defs/cline.js +3 -0
  35. package/dist/defs/codebuddy.d.ts +15 -0
  36. package/dist/defs/codebuddy.js +3 -1
  37. package/dist/defs/codex.d.ts +15 -0
  38. package/dist/defs/codex.js +11 -48
  39. package/dist/defs/copilot.d.ts +15 -0
  40. package/dist/defs/copilot.js +5 -14
  41. package/dist/defs/crush.d.ts +15 -0
  42. package/dist/defs/crush.js +3 -0
  43. package/dist/defs/cursor-agent.d.ts +15 -0
  44. package/dist/defs/cursor-agent.js +3 -1
  45. package/dist/defs/deepseek.d.ts +15 -0
  46. package/dist/defs/deepseek.js +3 -1
  47. package/dist/defs/devin.d.ts +15 -0
  48. package/dist/defs/devin.js +3 -1
  49. package/dist/defs/droid.d.ts +15 -0
  50. package/dist/defs/droid.js +3 -0
  51. package/dist/defs/goose.d.ts +15 -0
  52. package/dist/defs/goose.js +3 -0
  53. package/dist/defs/grok-build.d.ts +15 -0
  54. package/dist/defs/grok-build.js +5 -9
  55. package/dist/defs/hermes.d.ts +15 -0
  56. package/dist/defs/hermes.js +3 -1
  57. package/dist/defs/index.d.ts +1 -6
  58. package/dist/defs/index.js +1 -6
  59. package/dist/defs/kilo.d.ts +15 -0
  60. package/dist/defs/kilo.js +3 -1
  61. package/dist/defs/kimi.d.ts +15 -0
  62. package/dist/defs/kimi.js +3 -1
  63. package/dist/defs/kiro.d.ts +15 -0
  64. package/dist/defs/kiro.js +3 -1
  65. package/dist/defs/mimo.d.ts +15 -0
  66. package/dist/defs/mimo.js +3 -1
  67. package/dist/defs/opencode.d.ts +15 -1
  68. package/dist/defs/opencode.js +2 -1
  69. package/dist/defs/pi.d.ts +15 -0
  70. package/dist/defs/pi.js +6 -5
  71. package/dist/defs/qoder.d.ts +15 -0
  72. package/dist/defs/qoder.js +3 -1
  73. package/dist/defs/qwen.d.ts +15 -0
  74. package/dist/defs/qwen.js +3 -1
  75. package/dist/defs/reasonix.d.ts +15 -0
  76. package/dist/defs/reasonix.js +7 -23
  77. package/dist/defs/shared.d.ts +3 -8
  78. package/dist/defs/shared.js +3 -8
  79. package/dist/defs/trae-cli.d.ts +15 -0
  80. package/dist/defs/trae-cli.js +3 -1
  81. package/dist/defs/vibe.d.ts +15 -0
  82. package/dist/defs/vibe.js +3 -1
  83. package/dist/detection.d.ts +23 -4
  84. package/dist/detection.js +41 -59
  85. package/dist/diagnostics.js +1 -9
  86. package/dist/env.js +5 -31
  87. package/dist/executables.js +0 -1
  88. package/dist/index.d.ts +10 -257
  89. package/dist/index.js +20 -278
  90. package/dist/json-event-stream.js +23 -29
  91. package/dist/launch.js +1 -5
  92. package/dist/log-file.js +0 -5
  93. package/dist/metadata.js +0 -4
  94. package/dist/mmd-routes.js +2 -6
  95. package/dist/model-discovery-adapters.d.ts +22 -0
  96. package/dist/model-discovery-adapters.js +391 -0
  97. package/dist/model-discovery-config.d.ts +17 -0
  98. package/dist/model-discovery-config.js +137 -0
  99. package/dist/model-discovery-sdk.d.ts +18 -0
  100. package/dist/model-discovery-sdk.js +56 -0
  101. package/dist/model-discovery-types.d.ts +139 -0
  102. package/dist/model-discovery-types.js +2 -0
  103. package/dist/model-discovery.d.ts +46 -0
  104. package/dist/model-discovery.js +258 -0
  105. package/dist/model-registry.d.ts +5 -36
  106. package/dist/models.d.ts +1 -4
  107. package/dist/models.js +3 -1
  108. package/dist/opencode-log.js +1 -3
  109. package/dist/paths.js +1 -4
  110. package/dist/pi-models.d.ts +1 -7
  111. package/dist/pi-models.js +1 -3
  112. package/dist/prompt-augmenter.d.ts +3 -17
  113. package/dist/prompt-augmenter.js +3 -17
  114. package/dist/providers/model-catalog.d.ts +2 -0
  115. package/dist/providers/model-catalog.js +76 -55
  116. package/dist/providers/sse-decode.d.ts +4 -48
  117. package/dist/providers/sse-decode.js +3 -108
  118. package/dist/qoder-stream.d.ts +0 -3
  119. package/dist/qoder-stream.js +0 -3
  120. package/dist/registry.d.ts +1 -1
  121. package/dist/registry.js +3 -3
  122. package/dist/resolution.js +1 -4
  123. package/dist/role-marker-guard.d.ts +0 -5
  124. package/dist/role-marker-guard.js +0 -5
  125. package/dist/telemetry-sink.d.ts +3 -14
  126. package/dist/types.d.ts +9 -2
  127. package/package.json +27 -6
@@ -0,0 +1,139 @@
1
+ import type { RuntimeEnv, RuntimeModelOption } from './types.js';
2
+ import type { ProviderModelsInput } from './providers/model-catalog.js';
3
+ export type ModelSource = 'cli' | 'rpc' | 'provider-api' | 'cli-cache' | 'config-file' | 'offline-fallback';
4
+ export type ModelIdentityKind = 'concrete' | 'alias' | 'routing-mode';
5
+ export interface DiscoveredModel extends RuntimeModelOption {
6
+ identityKind: ModelIdentityKind;
7
+ resolvedId?: string;
8
+ provider?: string;
9
+ }
10
+ export interface ModelCatalogSnapshot {
11
+ models: DiscoveredModel[];
12
+ source: ModelSource;
13
+ freshness: 'fresh' | 'stale' | 'offline-fallback';
14
+ fetchedAt: string;
15
+ expiresAt: string;
16
+ coverage: 'account' | 'provider' | 'configured' | 'observed';
17
+ launchFingerprint: string;
18
+ diagnostics: readonly {
19
+ code: 'auth-missing' | 'unsupported-version' | 'timeout' | 'offline' | 'malformed-response';
20
+ message: string;
21
+ }[];
22
+ /** Native current selection, preserved even when it is an opaque route. */
23
+ defaultSelectionId?: string;
24
+ }
25
+ export type DefaultModelResolution = {
26
+ status: 'resolved';
27
+ id: string;
28
+ selectionId?: string;
29
+ source: ModelSource;
30
+ resolvedAt: string;
31
+ launchFingerprint: string;
32
+ } | {
33
+ status: 'unresolved';
34
+ selectionId?: string;
35
+ reason: string;
36
+ };
37
+ /** Represents the actual launch; fingerprints are opaque, never credential values. */
38
+ export interface ModelDiscoveryContext {
39
+ executable: string;
40
+ version?: string | null;
41
+ cwd: string;
42
+ profile?: string;
43
+ configFingerprint?: string;
44
+ projectRoot?: string;
45
+ configPath?: string;
46
+ accountScope?: string;
47
+ env: RuntimeEnv;
48
+ model?: string;
49
+ resumeSessionId?: string;
50
+ /** Host-supplied authoritative metadata for the resumed session. */
51
+ resumedModelId?: string;
52
+ settingSources?: readonly string[];
53
+ settings?: string;
54
+ }
55
+ export interface AgentModelDiscoveryPort {
56
+ discoverModels(requiredArgs: {
57
+ context: ModelDiscoveryContext;
58
+ }, optionalArgs?: {
59
+ signal?: AbortSignal;
60
+ force?: boolean;
61
+ staleWhileRevalidate?: boolean;
62
+ deps?: ModelDiscoveryDeps;
63
+ }): Promise<ModelCatalogSnapshot>;
64
+ resolveDefaultModel(requiredArgs: {
65
+ context: ModelDiscoveryContext;
66
+ catalog: ModelCatalogSnapshot;
67
+ }, optionalArgs?: {
68
+ signal?: AbortSignal;
69
+ deps?: ModelDiscoveryDeps;
70
+ }): Promise<DefaultModelResolution>;
71
+ }
72
+ export interface ModelProbeResult {
73
+ models: readonly RuntimeModelOption[];
74
+ defaultSelectionId?: string;
75
+ source: ModelSource;
76
+ coverage: ModelCatalogSnapshot['coverage'];
77
+ }
78
+ export interface ModelCacheEntry {
79
+ good?: ModelCatalogSnapshot;
80
+ retryAt?: number;
81
+ failed?: ModelCatalogSnapshot;
82
+ pending?: Promise<ModelCatalogSnapshot>;
83
+ }
84
+ /** All effects belong to ports. Credentials never enter snapshots or diagnostic strings. */
85
+ export interface ModelDiscoveryDeps {
86
+ process: {
87
+ run(input: {
88
+ context: ModelDiscoveryContext;
89
+ args: readonly string[];
90
+ stdin?: string;
91
+ signal: AbortSignal;
92
+ timeoutMs: number;
93
+ complete?: (stdout: string) => boolean;
94
+ terminateProcessTree?: boolean;
95
+ }): Promise<{
96
+ stdout: string;
97
+ stderr: string;
98
+ }>;
99
+ };
100
+ fs: {
101
+ read(path: string, options: {
102
+ signal: AbortSignal;
103
+ }): Promise<string | null>;
104
+ projectRoot?(input: {
105
+ cwd: string;
106
+ signal: AbortSignal;
107
+ }): Promise<string | null>;
108
+ };
109
+ http: {
110
+ list(input: ProviderModelsInput): Promise<import('./providers/types.js').ProviderModelsResponse>;
111
+ };
112
+ credential: {
113
+ connection(input: {
114
+ agentId: string;
115
+ context: ModelDiscoveryContext;
116
+ signal: AbortSignal;
117
+ }): Promise<ProviderModelsInput | null>;
118
+ };
119
+ acp: {
120
+ probe(input: {
121
+ context: ModelDiscoveryContext;
122
+ args: readonly string[];
123
+ signal: AbortSignal;
124
+ }): Promise<ModelProbeResult>;
125
+ };
126
+ /** Copilot/Droid SDK ports: no undocumented CLI flags or inference prompts. */
127
+ rpc: {
128
+ probe(input: {
129
+ agentId: string;
130
+ context: ModelDiscoveryContext;
131
+ signal: AbortSignal;
132
+ }): Promise<ModelProbeResult>;
133
+ };
134
+ clock: {
135
+ now(): number;
136
+ };
137
+ cache: Map<string, ModelCacheEntry>;
138
+ }
139
+ //# sourceMappingURL=model-discovery-types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=model-discovery-types.js.map
@@ -0,0 +1,46 @@
1
+ import type { RuntimeModelOption } from './types.js';
2
+ import type { AgentModelDiscoveryPort, DefaultModelResolution, DiscoveredModel, ModelCatalogSnapshot, ModelDiscoveryContext, ModelDiscoveryDeps } from './model-discovery-types.js';
3
+ export * from './model-discovery-types.js';
4
+ export declare const MODEL_CATALOG_TTL_MS: number;
5
+ export declare const MODEL_NEGATIVE_TTL_MS = 30000;
6
+ export declare function modelIdentityKind(id: string): DiscoveredModel['identityKind'];
7
+ export declare function normalizeDiscoveredModels(models: readonly RuntimeModelOption[]): DiscoveredModel[];
8
+ /** Hash all launch-affecting state, including env credentials; none of the raw material escapes. */
9
+ export declare function modelLaunchFingerprint(context: ModelDiscoveryContext, configMaterial?: string): string;
10
+ /** Cancellation always reaches the effect port. A broken port cannot hold callers indefinitely. */
11
+ export declare function withModelProbeTimeout<T>(work: (signal: AbortSignal) => Promise<T>, timeoutMs: number, parent?: AbortSignal): Promise<T>;
12
+ export declare class ModelProbeError extends Error {
13
+ readonly code: ModelCatalogSnapshot['diagnostics'][number]['code'];
14
+ constructor(code: ModelCatalogSnapshot['diagnostics'][number]['code']);
15
+ }
16
+ export interface AgentDiscoveryConfiguration {
17
+ fallbackModels: () => readonly RuntimeModelOption[];
18
+ parse?: (input: {
19
+ stdout: string;
20
+ }) => RuntimeModelOption[] | null;
21
+ args?: readonly string[];
22
+ }
23
+ export declare function createAgentModelDiscovery(agentId: string, configuration: AgentDiscoveryConfiguration): AgentModelDiscoveryPort;
24
+ export declare function offlineSnapshot(fingerprint: string, now: number, fallback: readonly RuntimeModelOption[], issue?: ModelCatalogSnapshot['diagnostics'][number]): ModelCatalogSnapshot;
25
+ /** Hosts may supply either discovery port independently; static definitions need no probes. */
26
+ type ModelDiscoveryDefinition = Partial<AgentModelDiscoveryPort> & {
27
+ id?: string;
28
+ fallbackModels?: readonly RuntimeModelOption[];
29
+ defaultModelEnvVar?: string;
30
+ supportsCustomModel?: boolean;
31
+ supportsConcreteModelSelection?: boolean;
32
+ };
33
+ export declare function modelDiscoveryForDef(def: ModelDiscoveryDefinition): AgentModelDiscoveryPort;
34
+ /** Resolve against the actual launch scope, pinning the same ID shown by the picker. */
35
+ export declare function resolveModelForLaunch({ def, context }: {
36
+ def: ModelDiscoveryDefinition;
37
+ context: ModelDiscoveryContext;
38
+ }, optional?: {
39
+ deps?: ModelDiscoveryDeps;
40
+ signal?: AbortSignal;
41
+ }): Promise<{
42
+ model: string;
43
+ catalog: ModelCatalogSnapshot;
44
+ resolution: DefaultModelResolution;
45
+ }>;
46
+ //# sourceMappingURL=model-discovery.d.ts.map
@@ -0,0 +1,258 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readModelConfiguration } from './model-discovery-config.js';
3
+ import { defaultModelDiscoveryDeps, probeAgentCatalog, probeCliCache, modelProbeTimeoutMs } from './model-discovery-adapters.js';
4
+ export * from './model-discovery-types.js';
5
+ export const MODEL_CATALOG_TTL_MS = 15 * 60_000;
6
+ export const MODEL_NEGATIVE_TTL_MS = 30_000;
7
+ const MAX_CATALOG_SCOPES = 128;
8
+ export function modelIdentityKind(id) {
9
+ const token = id.slice(id.lastIndexOf('/') + 1);
10
+ if (/^(default|auto|adaptive|smart|deep|rush|ultimate|premium|balanced|fast|vercel-ai-gateway|grok-build)$/i.test(token) || /(?:^|[-/])gateway$/i.test(id))
11
+ return 'routing-mode';
12
+ if (/^(opus|sonnet|haiku|fable|best|latest|claude|gemini|gpt|codex|swe)(?:\[.*\])?$/i.test(token) || /(?:^|[-/])latest$/i.test(id))
13
+ return 'alias';
14
+ return 'concrete';
15
+ }
16
+ export function normalizeDiscoveredModels(models) {
17
+ const seen = new Set();
18
+ return models.flatMap((row) => {
19
+ if (!row.id || row.id === 'default' || seen.has(row.id))
20
+ return [];
21
+ seen.add(row.id);
22
+ const model = row;
23
+ return [{ ...row, identityKind: model.identityKind ?? modelIdentityKind(row.id), ...(model.resolvedId ? { resolvedId: model.resolvedId } : {}) }];
24
+ });
25
+ }
26
+ /** Hash all launch-affecting state, including env credentials; none of the raw material escapes. */
27
+ export function modelLaunchFingerprint(context, configMaterial = '') {
28
+ return createHash('sha256').update(JSON.stringify({ ...context, env: Object.entries(context.env).sort(([a], [b]) => a.localeCompare(b)), configMaterial })).digest('hex');
29
+ }
30
+ class ProbeTimeout extends Error {
31
+ }
32
+ const probePools = new WeakMap();
33
+ /** Four simultaneous cold probes per host; queued callers keep cancellation responsive. */
34
+ async function boundedProbe(deps, work, signal) {
35
+ const pool = probePools.get(deps) || { active: 0, queue: [] };
36
+ probePools.set(deps, pool);
37
+ if (pool.active >= 4) {
38
+ await new Promise((resolve, reject) => {
39
+ const ready = () => { signal?.removeEventListener('abort', abort); resolve(); };
40
+ const abort = () => { const index = pool.queue.indexOf(ready); if (index >= 0)
41
+ pool.queue.splice(index, 1); reject(new ProbeTimeout('Probe cancelled')); };
42
+ pool.queue.push(ready);
43
+ if (signal?.aborted)
44
+ abort();
45
+ else
46
+ signal?.addEventListener('abort', abort, { once: true });
47
+ });
48
+ }
49
+ else
50
+ pool.active += 1;
51
+ try {
52
+ if (signal?.aborted)
53
+ throw new ProbeTimeout('Probe cancelled');
54
+ return await work();
55
+ }
56
+ finally {
57
+ const next = pool.queue.shift();
58
+ if (next)
59
+ next();
60
+ else
61
+ pool.active -= 1;
62
+ }
63
+ }
64
+ /** Cancellation always reaches the effect port. A broken port cannot hold callers indefinitely. */
65
+ export async function withModelProbeTimeout(work, timeoutMs, parent) {
66
+ if (parent?.aborted)
67
+ throw new ProbeTimeout('Probe cancelled');
68
+ const controller = new AbortController();
69
+ let timer;
70
+ let abortListener;
71
+ const stopped = new Promise((_resolve, reject) => {
72
+ abortListener = () => { controller.abort(); reject(new ProbeTimeout('Probe cancelled')); };
73
+ if (parent?.aborted)
74
+ abortListener();
75
+ else
76
+ parent?.addEventListener('abort', abortListener, { once: true });
77
+ timer = setTimeout(() => { controller.abort(); reject(new ProbeTimeout('Probe timed out')); }, timeoutMs);
78
+ });
79
+ try {
80
+ return await Promise.race([work(controller.signal), stopped]);
81
+ }
82
+ finally {
83
+ if (timer)
84
+ clearTimeout(timer);
85
+ if (abortListener)
86
+ parent?.removeEventListener('abort', abortListener);
87
+ }
88
+ }
89
+ function diagnostic(error) {
90
+ // Never stringify vendor errors: stderr/HTTP bodies may include credentials or OAuth URLs.
91
+ const code = error instanceof ProbeTimeout ? 'timeout' : error instanceof ModelProbeError ? error.code : 'offline';
92
+ return { code, message: ({ timeout: 'Model discovery timed out or was cancelled.', 'auth-missing': 'CLI credentials are unavailable.', 'unsupported-version': 'Native metadata discovery is unavailable for this version.', 'malformed-response': 'Native metadata contained no usable catalog.', offline: 'Model discovery could not reach its source.' })[code] };
93
+ }
94
+ export class ModelProbeError extends Error {
95
+ code;
96
+ constructor(code) {
97
+ super(code);
98
+ this.code = code;
99
+ }
100
+ }
101
+ export function createAgentModelDiscovery(agentId, configuration) {
102
+ async function scoped(context, deps, signal) {
103
+ const config = await withModelProbeTimeout((active) => readModelConfiguration(agentId, context, deps, active), 1000, signal);
104
+ return { config, key: `${agentId}:${modelLaunchFingerprint(context, config.fingerprintMaterial)}` };
105
+ }
106
+ const port = {
107
+ async discoverModels({ context }, { signal, force = false, staleWhileRevalidate = false, deps = defaultModelDiscoveryDeps } = {}) {
108
+ let state;
109
+ try {
110
+ state = await scoped(context, deps, signal);
111
+ }
112
+ catch (error) {
113
+ return offlineSnapshot(modelLaunchFingerprint(context), deps.clock.now(), configuration.fallbackModels(), diagnostic(error));
114
+ }
115
+ const { key, config } = state;
116
+ const fingerprint = key.slice(agentId.length + 1);
117
+ const now = deps.clock.now();
118
+ const cached = deps.cache.get(key) || {};
119
+ const lastGood = () => cached.failed || (cached.good && {
120
+ ...cached.good, freshness: Date.parse(cached.good.expiresAt) > now ? cached.good.freshness : 'stale',
121
+ });
122
+ if (cached.pending)
123
+ return staleWhileRevalidate && lastGood() || cached.pending;
124
+ if (!force && cached.retryAt && cached.retryAt > now && cached.failed)
125
+ return cached.failed;
126
+ if (!force && !staleWhileRevalidate && !cached.failed && cached.good && Date.parse(cached.good.expiresAt) > now)
127
+ return cached.good;
128
+ // A fresh host process may still have a CLI-owned disk catalog. Use its observed rows
129
+ // immediately; keep stale provenance and refresh without holding the first user prompt.
130
+ if (staleWhileRevalidate && !cached.good && ['codex', 'opencode', 'claude', 'codebuddy'].includes(agentId)) {
131
+ try {
132
+ const disk = await withModelProbeTimeout(active => probeCliCache(agentId, context, deps, active), 1000, signal);
133
+ const models = normalizeDiscoveredModels(disk.models);
134
+ if (models.length)
135
+ cached.good = { models, source: 'cli-cache', coverage: disk.coverage,
136
+ freshness: 'stale', fetchedAt: new Date(now).toISOString(), expiresAt: new Date(now).toISOString(),
137
+ launchFingerprint: fingerprint, diagnostics: [],
138
+ ...(config.selection ? { defaultSelectionId: config.selection } : {}) };
139
+ }
140
+ catch { /* No usable disk cache: keep the bounded cold-probe path below. */ }
141
+ // Another caller may have started the refresh while disk metadata was read.
142
+ const concurrent = deps.cache.get(key);
143
+ if (concurrent?.pending)
144
+ return concurrent.failed || concurrent.good || concurrent.pending;
145
+ }
146
+ const pending = (async () => {
147
+ try {
148
+ const result = await boundedProbe(deps, () => withModelProbeTimeout((active) => probeAgentCatalog(agentId, context, configuration, deps, active), modelProbeTimeoutMs(agentId), signal), signal);
149
+ const models = normalizeDiscoveredModels(result.models);
150
+ if (models.length === 0)
151
+ throw new ModelProbeError('malformed-response');
152
+ const fetched = deps.clock.now();
153
+ // Native default metadata outranks a config guess (Grok can route despite its TOML).
154
+ const nativeWins = result.source === 'rpc' && !['claude', 'codebuddy'].includes(agentId) || agentId === 'grok-build';
155
+ const selection = context.model || (nativeWins ? result.defaultSelectionId || config.selection : config.selection || result.defaultSelectionId);
156
+ const snapshot = { models, source: result.source, coverage: result.coverage,
157
+ fetchedAt: new Date(fetched).toISOString(), expiresAt: new Date(fetched + MODEL_CATALOG_TTL_MS).toISOString(),
158
+ freshness: result.source === 'cli-cache' ? 'stale' : 'fresh', launchFingerprint: fingerprint, diagnostics: [],
159
+ ...(selection ? { defaultSelectionId: selection } : {}) };
160
+ cached.good = snapshot;
161
+ delete cached.retryAt;
162
+ delete cached.failed;
163
+ return snapshot;
164
+ }
165
+ catch (error) {
166
+ cached.retryAt = deps.clock.now() + MODEL_NEGATIVE_TTL_MS;
167
+ cached.failed = cached.good ? { ...cached.good, freshness: 'stale', diagnostics: [diagnostic(error)] } : offlineSnapshot(fingerprint, deps.clock.now(), configuration.fallbackModels(), diagnostic(error));
168
+ return cached.failed;
169
+ }
170
+ finally {
171
+ delete cached.pending;
172
+ }
173
+ })();
174
+ cached.pending = pending;
175
+ deps.cache.set(key, cached);
176
+ // Evict settled scopes first; coalesced callers must retain their in-flight entry.
177
+ for (const [oldKey, entry] of deps.cache) {
178
+ if (deps.cache.size <= MAX_CATALOG_SCOPES)
179
+ break;
180
+ if (!entry.pending)
181
+ deps.cache.delete(oldKey);
182
+ }
183
+ // Explicit rescan still waits for live evidence. Launches retain the scoped last-good
184
+ // catalog while this coalesced, timeout-bounded refresh updates the cache in the background.
185
+ return staleWhileRevalidate && lastGood() || pending;
186
+ },
187
+ async resolveDefaultModel({ context, catalog }, { signal, deps = defaultModelDiscoveryDeps } = {}) {
188
+ let state;
189
+ try {
190
+ state = await scoped(context, deps, signal);
191
+ }
192
+ catch {
193
+ return { status: 'unresolved', reason: 'Launch configuration could not be read.' };
194
+ }
195
+ const fingerprint = state.key.slice(agentId.length + 1);
196
+ if (fingerprint !== catalog.launchFingerprint)
197
+ return { status: 'unresolved', reason: 'Launch configuration changed; refresh the catalog.' };
198
+ if (context.resumeSessionId && !context.resumedModelId && !context.model)
199
+ return { status: 'unresolved', reason: 'Resumed session model metadata is required.' };
200
+ const selection = context.model || context.resumedModelId || catalog.defaultSelectionId || state.config.selection;
201
+ if (!selection)
202
+ return { status: 'unresolved', reason: 'Native metadata did not resolve a default; pick a concrete model.' };
203
+ const row = catalog.models.find((model) => model.id === selection || model.label === selection);
204
+ if (catalog.freshness === 'offline-fallback' && row && row.id !== selection)
205
+ return { status: 'unresolved', selectionId: selection, reason: 'Offline hints cannot resolve a display name or alias.' };
206
+ const id = row?.resolvedId || (row?.identityKind === 'concrete' ? row.id : undefined);
207
+ if (!id || modelIdentityKind(id) !== 'concrete')
208
+ return { status: 'unresolved', selectionId: selection, reason: 'The configured selection has no concrete model evidence.' };
209
+ return { status: 'resolved', id, selectionId: selection, source: state.config.selection === selection ? 'config-file' : catalog.source, resolvedAt: new Date(deps.clock.now()).toISOString(), launchFingerprint: fingerprint };
210
+ },
211
+ };
212
+ return port;
213
+ }
214
+ export function offlineSnapshot(fingerprint, now, fallback, issue) {
215
+ return { models: normalizeDiscoveredModels(fallback), source: 'offline-fallback', freshness: 'offline-fallback', fetchedAt: new Date(now).toISOString(), expiresAt: new Date(now + MODEL_NEGATIVE_TTL_MS).toISOString(), coverage: 'configured', launchFingerprint: fingerprint, diagnostics: issue ? [issue] : [] };
216
+ }
217
+ export function modelDiscoveryForDef(def) {
218
+ return {
219
+ discoverModels: def.discoverModels?.bind(def) || (async ({ context }, { deps = defaultModelDiscoveryDeps } = {}) => {
220
+ const catalog = offlineSnapshot(modelLaunchFingerprint(context), deps.clock.now(), def.fallbackModels || []);
221
+ const configured = def.defaultModelEnvVar && context.env[def.defaultModelEnvVar]?.trim();
222
+ // Static hosts used their first concrete fallback before discovery existed. Preserve
223
+ // that contract while letting a declared env default or explicit selection take precedence.
224
+ const selection = context.model || context.resumedModelId || configured || catalog.models.find(row => row.identityKind === 'concrete')?.id;
225
+ return { ...catalog, ...(selection ? { defaultSelectionId: selection } : {}) };
226
+ }),
227
+ resolveDefaultModel: def.resolveDefaultModel?.bind(def) || (async ({ context, catalog }, { deps = defaultModelDiscoveryDeps } = {}) => {
228
+ if (context.resumeSessionId && !context.resumedModelId && !context.model)
229
+ return { status: 'unresolved', reason: 'Resumed session model metadata is required.' };
230
+ const configured = def.defaultModelEnvVar && context.env[def.defaultModelEnvVar]?.trim();
231
+ const selection = context.model || context.resumedModelId || configured || catalog.defaultSelectionId;
232
+ const row = catalog.models.find(model => model.id === selection || model.label === selection);
233
+ // A host's declared env default is authoritative even when its static hints lag behind.
234
+ const id = row?.resolvedId || (row?.identityKind === 'concrete' ? row.id : undefined)
235
+ || (configured === selection && configured ? configured : undefined);
236
+ if (!id || modelIdentityKind(id) !== 'concrete')
237
+ return { status: 'unresolved', ...(selection ? { selectionId: selection } : {}), reason: 'The configured selection has no concrete model evidence.' };
238
+ return { status: 'resolved', id, ...(selection ? { selectionId: selection } : {}), source: configured === selection ? 'config-file' : catalog.source,
239
+ resolvedAt: new Date(deps.clock.now()).toISOString(), launchFingerprint: catalog.launchFingerprint };
240
+ }),
241
+ };
242
+ }
243
+ /** Resolve against the actual launch scope, pinning the same ID shown by the picker. */
244
+ export async function resolveModelForLaunch({ def, context }, optional = {}) {
245
+ if (def.supportsConcreteModelSelection === false)
246
+ throw new Error('This CLI cannot pin a concrete starting model.');
247
+ const discovery = modelDiscoveryForDef(def);
248
+ const catalog = await discovery.discoverModels({ context }, { ...optional, staleWhileRevalidate: true });
249
+ // Custom input is allowed unless the definition explicitly disables it: an unlisted user ID is already
250
+ // the intended launch value. Never substitute a catalog default or reinterpret its spelling.
251
+ const resolution = context.model && def.supportsCustomModel !== false && !catalog.models.some(row => row.id === context.model)
252
+ ? { status: 'resolved', id: context.model, selectionId: context.model, source: 'config-file', resolvedAt: new Date((optional.deps || defaultModelDiscoveryDeps).clock.now()).toISOString(), launchFingerprint: catalog.launchFingerprint }
253
+ : await discovery.resolveDefaultModel({ context, catalog }, optional);
254
+ if (resolution.status !== 'resolved')
255
+ throw new Error(resolution.reason);
256
+ return { model: resolution.id, catalog, resolution };
257
+ }
258
+ //# sourceMappingURL=model-discovery.js.map
@@ -1,41 +1,10 @@
1
1
  /**
2
2
  * @module model-registry
3
- *
4
- * The provider/model/agent vocabulary that any Jini consumer picking "which
5
- * model" or "which coding agent" to run needs, plus the handful of pure
6
- * helpers (credential-status resolution, model-list merging, a stable cache
7
- * key, model-choice normalization against a live catalogue) those pickers
8
- * all independently re-derive today.
9
- *
10
- * Ported and generified from OD's `apps/web/src/state/config.ts`
11
- * (`KnownProvider`/`KNOWN_PROVIDERS`), `packages/contracts/src/api/registry.ts`
12
- * (`AgentInfo`/`AgentDiagnostic`/`AgentFixIntent`/`AgentModelOption`),
13
- * `packages/contracts/src/api/providerModels.ts` (`ProviderModelOption`),
14
- * `packages/contracts/src/api/app-config.ts` (`AgentModelPrefs`),
15
- * `apps/web/src/components/providerModelsCache.ts`, and
16
- * `apps/web/src/components/agentModelSelection.ts` — see
17
- * `packages/agent-runtime/archived provenance ledger` for the full per-symbol mapping.
18
- * No OD product-identity strings or OD-specific gating (e.g. the original's
19
- * hardcoded `agent.id === 'amr'` carve-out) survived the port.
20
- *
21
- * Named `model-registry.ts`, not `registry.ts`: this package already has a
22
- * `registry.ts` (the `BASE_AGENT_DEFS` static CLI-adapter catalog +
23
- * `getAgentDef(id)`) — a different concept that happens to share the word
24
- * "registry". `AgentDiagnostic`/`AgentDiagnosticSeverity`/`AgentFixIntent`
25
- * are NOT redefined here either, for the same reason `ModelOption` isn't:
26
- * this package already has real, more complete versions of all three
27
- * (`./types.js`, vendored from the same OD `packages/contracts/src/api/
28
- * registry.ts` source this module cites above — main's `AgentFixIntent`
29
- * adds a `launchOAuth` case this port's copy lacked, and its
30
- * `AgentDiagnostic.reason` is the specific `AgentDiagnosticReason` literal
31
- * union rather than a bare `string`). Reusing them keeps one canonical
32
- * diagnostic shape in the package instead of two divergent ones under the
33
- * same names. `ModelOption` alone is renamed (to `ModelCatalogOption`)
34
- * rather than reused, because the package's existing `ModelOption`
35
- * (`agent-protocol/acp/models.ts`, `{ id, label }`) is a narrower,
36
- * genuinely different shape for ACP model-probe results, not a compatible
37
- * version of this module's `{ id, label, hint, providerId, default, caps }`
38
- * model-catalogue entry.
3
+ * Provider/model/agent picker vocabulary and credential-status, merge, cache and choice helpers.
4
+ * registry.ts owns the CLI-adapter catalog, a distinct concept. Reuse the canonical protocol
5
+ * catalog types and runtime diagnostic types rather than redefining looser copies.
6
+ * ModelCatalogOption carries provider/hint/default/capability data; the protocol ACP ModelOption
7
+ * is a narrower id/label probe shape, so their names and contracts stay separate.
39
8
  */
40
9
  import type { AgentDefinition, CredentialStatus, ModelCatalogOption, ModelProvider } from '@jini-ai/protocol';
41
10
  export interface AgentModelChoice {
package/dist/models.d.ts CHANGED
@@ -1,9 +1,6 @@
1
1
  /**
2
2
  * @module models
3
- *
4
- * Model-option defaults, a per-agent "last surfaced to the UI" live-model
5
- * cache, and model-id sanitization. Ported verbatim from OD's
6
- * `apps/daemon/src/runtimes/models.ts` — no product coupling.
3
+ * Model-option defaults, a per-agent last-surfaced live-model cache and model-id sanitization.
7
4
  */
8
5
  import type { RuntimeAgentDef, RuntimeModelOption } from './types.js';
9
6
  export declare const DEFAULT_MODEL_OPTION: RuntimeModelOption;
package/dist/models.js CHANGED
@@ -86,7 +86,9 @@ export function sanitizeCustomModel({ id }) {
86
86
  const trimmed = id.trim();
87
87
  if (trimmed.length === 0 || trimmed.length > 200)
88
88
  return null;
89
- if (!/^[A-Za-z0-9][A-Za-z0-9._/:@-]*$/.test(trimmed))
89
+ // Catalog capability variants (e.g. Claude's [1m] context window) are native model IDs.
90
+ // argv is never a shell string; keep the existing leading-flag/whitespace/control guards.
91
+ if (!/^[A-Za-z0-9][A-Za-z0-9._/:@\[\]-]*$/.test(trimmed))
90
92
  return null;
91
93
  return trimmed;
92
94
  }
@@ -8,9 +8,7 @@
8
8
  * only in OpenCode's own session log (`service=llm … error={…}`). This
9
9
  * module recovers that signal so a chat UI can show "usage limit reached"
10
10
  * instead of a bare timeout. OpenCode-specific by design.
11
- *
12
- * Ported verbatim from OD's `apps/daemon/src/runtimes/opencode-log.ts`
13
- * — no product coupling (it only reads OpenCode's own on-disk log format).
11
+
14
12
  */
15
13
  import { readdirSync, readFileSync, statSync } from 'node:fs';
16
14
  import path from 'node:path';
package/dist/paths.js CHANGED
@@ -1,9 +1,6 @@
1
1
  /**
2
2
  * @module paths
3
- *
4
- * Home-relative path expansion for configured env values and file paths.
5
- * Ported verbatim from OD's `apps/daemon/src/runtimes/paths.ts` — no
6
- * product coupling.
3
+ * Home-relative expansion for configured environment values and file paths.
7
4
  */
8
5
  import path from 'node:path';
9
6
  import { homedir } from 'node:os';
@@ -1,12 +1,6 @@
1
1
  /**
2
2
  * @module pi-models
3
- *
4
- * Parses `pi --list-models`'s TSV table (read from stderr) into model
5
- * options. Ported verbatim (pure string parsing, no transport) from OD's
6
- * `apps/daemon/src/pi-rpc.ts#parsePiModels` — the surrounding file is the
7
- * ~700-line pi-rpc stdio transport (out of this task's scope, same
8
- * reasoning as `acp-model-probe.ts`), but this one function has no
9
- * dependency on it.
3
+ * Parse the pi --list-models whitespace table into model options, independently of the stdio transport.
10
4
  */
11
5
  import type { RuntimeModelOption } from './types.js';
12
6
  export declare function parsePiModels({ stdout }: {
package/dist/pi-models.js CHANGED
@@ -13,9 +13,7 @@ export function parsePiModels({ stdout }) {
13
13
  // The loop bound (`i < lines.length`) guarantees `lines[i]` is always
14
14
  // defined; the non-null assertion documents that runtime invariant
15
15
  // instead of a `noUncheckedIndexedAccess`-driven guard that could never
16
- // actually trigger (same treatment as this function's other port,
17
- // `agent-protocol/pi-rpc/models.ts` — see archived provenance ledger's "Barrel
18
- // merge" section for why two copies of this function exist).
16
+ // actually trigger.
19
17
  const line = lines[i];
20
18
  const parts = line.split(/\s+/);
21
19
  if (parts.length < 2)
@@ -1,22 +1,8 @@
1
1
  /**
2
2
  * @module prompt-augmenter
3
- *
4
- * Port replacing OD's `apps/daemon/src/runtimes/chat-prompt-inputs.ts` (design-system
5
- * selection resolution — `resolveEffectiveDesignSystemSelection`,
6
- * `designSystemIdFromPluginSnapshot`, `formatDesignFilesWorkspaceHint`,
7
- * Codex image-generation prompt overrides, comment-attachment rendering,
8
- * research-command-contract composition — all genuinely OD-product prompt
9
- * content) and the workspace-context-kind half of OD's
10
- * `runtimes/chat-run-context.ts` (not present on this branch as a separate
11
- * file, but the same `'design-system'`-as-a-context-kind concept lives
12
- * inline in `chat-prompt-inputs.ts`).
13
- *
14
- * None of that OD logic is ported — per the task charter, this file defines
15
- * only the injection seam. The shape below is r1b §1's proposed
16
- * `PromptAugmenter` signature, used as-is (it matched the real source: the
17
- * engine composes a base prompt and a generic `RunContextSelection`, then
18
- * calls out to the host for product-specific augmentation and an optional
19
- * system-prompt overlay).
3
+ * Product context, artifact conventions, research contracts and system overlays are host-owned.
4
+ * The engine composes a base prompt and generic RunContextSelection, then calls this port rather
5
+ * than depending on a product's design/annotation/workspace model.
20
6
  */
21
7
  /** A single item in the run's attached/selected workspace context. `kind` is host-defined — the engine treats it as an opaque string. */
22
8
  export interface WorkspaceContextItem {
@@ -1,22 +1,8 @@
1
1
  /**
2
2
  * @module prompt-augmenter
3
- *
4
- * Port replacing OD's `apps/daemon/src/runtimes/chat-prompt-inputs.ts` (design-system
5
- * selection resolution — `resolveEffectiveDesignSystemSelection`,
6
- * `designSystemIdFromPluginSnapshot`, `formatDesignFilesWorkspaceHint`,
7
- * Codex image-generation prompt overrides, comment-attachment rendering,
8
- * research-command-contract composition — all genuinely OD-product prompt
9
- * content) and the workspace-context-kind half of OD's
10
- * `runtimes/chat-run-context.ts` (not present on this branch as a separate
11
- * file, but the same `'design-system'`-as-a-context-kind concept lives
12
- * inline in `chat-prompt-inputs.ts`).
13
- *
14
- * None of that OD logic is ported — per the task charter, this file defines
15
- * only the injection seam. The shape below is r1b §1's proposed
16
- * `PromptAugmenter` signature, used as-is (it matched the real source: the
17
- * engine composes a base prompt and a generic `RunContextSelection`, then
18
- * calls out to the host for product-specific augmentation and an optional
19
- * system-prompt overlay).
3
+ * Product context, artifact conventions, research contracts and system overlays are host-owned.
4
+ * The engine composes a base prompt and generic RunContextSelection, then calls this port rather
5
+ * than depending on a product's design/annotation/workspace model.
20
6
  */
21
7
  /** Passes the base prompt through unchanged and adds no system overlay. */
22
8
  export const noopPromptAugmenter = {
@@ -20,6 +20,8 @@ export type ProviderModelsInput = ProviderModelsRequest & {
20
20
  requestInit?: Pick<RequestInit, 'dispatcher'>;
21
21
  /** Injectable DNS resolver for the base-URL SSRF guard; defaults to `node:dns`. */
22
22
  dnsLookup?: DnsLookupFn;
23
+ /** Injectable HTTP transport for adapter conformance; defaults to fetch. */
24
+ fetchFn?: typeof fetch;
23
25
  };
24
26
  /**
25
27
  * Fetches the live model catalog for a BYOK provider connection. Validates