@pellux/goodvibes-agent 1.2.0 → 1.3.0

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 (77) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/dist/package/main.js +19016 -12684
  3. package/docs/README.md +3 -3
  4. package/docs/connected-host.md +2 -2
  5. package/docs/getting-started.md +11 -9
  6. package/docs/tools-and-commands.md +12 -10
  7. package/docs/voice-and-live-tts.md +2 -2
  8. package/package.json +1 -1
  9. package/release/release-notes.md +17 -5
  10. package/release/release-readiness.json +36 -36
  11. package/src/agent/competitive-feature-inventory.ts +42 -44
  12. package/src/agent/setup-wizard-artifact-receipts.ts +366 -0
  13. package/src/agent/setup-wizard.ts +245 -9
  14. package/src/input/agent-workspace-categories.ts +2 -2
  15. package/src/input/agent-workspace-onboarding-categories.ts +4 -4
  16. package/src/input/agent-workspace-settings.ts +52 -1
  17. package/src/input/agent-workspace-setup-snapshot.ts +20 -1
  18. package/src/input/agent-workspace-setup.ts +32 -5
  19. package/src/input/agent-workspace-snapshot.ts +23 -3
  20. package/src/input/agent-workspace.ts +5 -1
  21. package/src/input/setup-wizard-live-receipts.ts +76 -0
  22. package/src/renderer/agent-workspace-context-lines.ts +21 -6
  23. package/src/renderer/agent-workspace.ts +46 -10
  24. package/src/runtime/tool-permission-safety.ts +1 -1
  25. package/src/tools/agent-harness-agent-orchestration-policy.ts +75 -0
  26. package/src/tools/agent-harness-agent-orchestration.ts +216 -128
  27. package/src/tools/agent-harness-autonomy-live-records.ts +154 -0
  28. package/src/tools/agent-harness-autonomy-queue-types.ts +1 -1
  29. package/src/tools/agent-harness-autonomy-queue.ts +19 -8
  30. package/src/tools/agent-harness-autonomy-watcher-read-models.ts +509 -0
  31. package/src/tools/agent-harness-background-processes.ts +8 -4
  32. package/src/tools/agent-harness-browser-cockpit-route.ts +188 -34
  33. package/src/tools/agent-harness-browser-control.ts +12 -4
  34. package/src/tools/agent-harness-browser-pwa-read-models.ts +622 -0
  35. package/src/tools/agent-harness-device-live-read-models.ts +366 -0
  36. package/src/tools/agent-harness-execution-posture.ts +3 -0
  37. package/src/tools/agent-harness-interactive-runtime-records.ts +421 -0
  38. package/src/tools/agent-harness-local-model-benchmarks.ts +71 -1
  39. package/src/tools/agent-harness-local-model-endpoints.ts +469 -354
  40. package/src/tools/agent-harness-local-model-smoke.ts +277 -0
  41. package/src/tools/agent-harness-local-model-url.ts +78 -0
  42. package/src/tools/agent-harness-media-posture.ts +27 -12
  43. package/src/tools/agent-harness-memory-external-providers.ts +796 -0
  44. package/src/tools/agent-harness-memory-posture.ts +253 -137
  45. package/src/tools/agent-harness-memory-provider-certification.ts +219 -0
  46. package/src/tools/agent-harness-memory-refinement.ts +340 -0
  47. package/src/tools/agent-harness-mode-catalog.ts +4 -2
  48. package/src/tools/agent-harness-model-provider-health.ts +139 -42
  49. package/src/tools/agent-harness-model-readiness.ts +31 -5
  50. package/src/tools/agent-harness-model-routing-types.ts +61 -0
  51. package/src/tools/agent-harness-model-routing.ts +31 -6
  52. package/src/tools/agent-harness-pairing-posture.ts +30 -9
  53. package/src/tools/agent-harness-personal-ops-certification.ts +116 -0
  54. package/src/tools/agent-harness-personal-ops-lanes.ts +81 -15
  55. package/src/tools/agent-harness-personal-ops-operations.ts +225 -0
  56. package/src/tools/agent-harness-personal-ops-provider-records.ts +358 -0
  57. package/src/tools/agent-harness-personal-ops-provider-task-records.ts +321 -0
  58. package/src/tools/agent-harness-personal-ops-records.ts +176 -224
  59. package/src/tools/agent-harness-personal-ops-types.ts +19 -1
  60. package/src/tools/agent-harness-personal-ops.ts +18 -11
  61. package/src/tools/agent-harness-remote-read-models.ts +541 -0
  62. package/src/tools/agent-harness-research-briefing.ts +26 -7
  63. package/src/tools/agent-harness-research-live-read-models.ts +500 -0
  64. package/src/tools/agent-harness-research-runs.ts +15 -3
  65. package/src/tools/agent-harness-research-workflow.ts +92 -19
  66. package/src/tools/agent-harness-setup-model-helpers.ts +1 -0
  67. package/src/tools/agent-harness-setup-smoke.ts +26 -1
  68. package/src/tools/agent-harness-tool-schema.ts +23 -1
  69. package/src/tools/agent-harness-tool-types.ts +5 -0
  70. package/src/tools/agent-harness-tool.ts +9 -1
  71. package/src/tools/agent-harness-workspace-actions.ts +1 -1
  72. package/src/tools/agent-memory-tool.ts +40 -1
  73. package/src/tools/agent-model-compare-run.ts +13 -0
  74. package/src/tools/agent-research-runner.ts +367 -0
  75. package/src/tools/agent-research-tool.ts +7 -179
  76. package/src/tools/agent-route-planner-candidates-surfaces.ts +1 -1
  77. package/src/version.ts +1 -1
@@ -1,87 +1,11 @@
1
1
  import type { CommandContext } from '../input/command-registry.ts';
2
- import type { AgentHarnessModelRoutingArgs, LocalModelDetection, LocalModelEndpointSource, LocalModelServerDefaultEndpoint, LocalModelServerEndpoint, LocalModelServerHealthMap, LocalModelSmokeTarget, MutableLocalModelServerEndpoint } from './agent-harness-model-routing-types.ts';
2
+ import type { LocalModelDetection, LocalModelEndpointSource, LocalModelServerDefaultEndpoint, LocalModelServerDiagnosticsPublication, LocalModelServerEndpoint, LocalModelServerHealthMap, LocalModelServerServingDiagnostics, MutableLocalModelServerEndpoint } from './agent-harness-model-routing-types.ts';
3
3
  import { listProviderIds, listProviderRegistryProviders, listRegistryModels, modelDisplayName, modelModelId, modelProviderId, modelRegistryKey, readProviderModels } from './agent-harness-model-catalog.ts';
4
4
  import { previewHarnessText } from './agent-harness-text.ts';
5
5
  import { readLimit, readRecord, readString } from './agent-harness-model-routing-utils.ts';
6
+ import { extractUrls, isPrivateOrLocalUrl, localStackFor, modelsUrlFor, normalizeLocalBaseUrl, parseUrlCandidate } from './agent-harness-local-model-url.ts';
6
7
 
7
- export function localStackFor(value: string): string | null {
8
- const normalized = value.toLowerCase();
9
- if (/ollama[-_\s]?cloud/.test(normalized)) return null;
10
- if (/\bollama\b/.test(normalized)) return 'ollama';
11
- if (/llama[.-]?cpp|llamacpp/.test(normalized)) return 'llama.cpp';
12
- if (/\bvllm\b/.test(normalized)) return 'vllm';
13
- if (/lm[-_\s]?studio/.test(normalized)) return 'openai-compatible';
14
- if (/localai|text-generation-inference|\btgi\b/.test(normalized)) return 'openai-compatible';
15
- if (/localhost|127\.0\.0\.1|\[?::1\]?/.test(normalized)) return 'openai-compatible';
16
- if (/openai-compatible|openai compatible|custom-provider|custom provider/.test(normalized)) return 'openai-compatible';
17
- return null;
18
- }
19
-
20
- export function cleanUrlCandidate(value: string): string {
21
- return value.trim().replace(/[),.;]+$/g, '');
22
- }
23
-
24
- export function extractUrls(value: string): readonly string[] {
25
- const matches = value.match(/https?:\/\/[^\s"'`<>]+/gi) ?? [];
26
- return [...new Set(matches.map(cleanUrlCandidate).filter(Boolean))];
27
- }
28
-
29
- export function parseUrlCandidate(raw: string): URL | null {
30
- const trimmed = cleanUrlCandidate(raw);
31
- if (!trimmed) return null;
32
- const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
33
- try {
34
- return new URL(withScheme);
35
- } catch {
36
- return null;
37
- }
38
- }
39
-
40
- export function isPrivateOrLocalHost(hostname: string): boolean {
41
- const host = hostname.replace(/^\[|\]$/g, '').toLowerCase();
42
- if (!host) return false;
43
- if (host === 'localhost' || host === '0.0.0.0' || host === '::1') return true;
44
- if (host.endsWith('.local')) return true;
45
- if (host.includes(':')) return host.startsWith('fc') || host.startsWith('fd') || host.startsWith('fe80:');
46
- const octets = host.split('.').map((entry) => Number(entry));
47
- if (octets.length === 4 && octets.every((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 255)) {
48
- const [first, second] = octets as [number, number, number, number];
49
- return first === 10
50
- || first === 127
51
- || (first === 172 && second >= 16 && second <= 31)
52
- || (first === 192 && second === 168)
53
- || (first === 169 && second === 254)
54
- || first === 0;
55
- }
56
- return !host.includes('.');
57
- }
58
-
59
- export function isPrivateOrLocalUrl(raw: string): boolean {
60
- const url = parseUrlCandidate(raw);
61
- if (!url || !/^https?:$/.test(url.protocol)) return false;
62
- return isPrivateOrLocalHost(url.hostname);
63
- }
64
-
65
- export function normalizeLocalBaseUrl(raw: string, stackHint?: string | null): string | null {
66
- const url = parseUrlCandidate(raw);
67
- if (!url || !/^https?:$/.test(url.protocol)) return null;
68
- const stack = stackHint ?? localStackFor(raw) ?? (isPrivateOrLocalHost(url.hostname) ? 'openai-compatible' : null);
69
- if (!isPrivateOrLocalUrl(url.href)) return null;
70
-
71
- let pathname = url.pathname.replace(/\/+$/g, '');
72
- if (pathname.endsWith('/models')) pathname = pathname.slice(0, -'/models'.length);
73
- if (pathname.endsWith('/api/tags')) pathname = pathname.slice(0, -'/api/tags'.length);
74
- const needsOpenAiPath = stack === 'ollama' || stack === 'llama.cpp' || stack === 'vllm' || stack === 'openai-compatible';
75
- if (needsOpenAiPath && (!pathname || pathname === '/')) pathname = '/v1';
76
- url.pathname = pathname || '';
77
- url.search = '';
78
- url.hash = '';
79
- return url.toString().replace(/\/+$/g, '');
80
- }
81
-
82
- export function modelsUrlFor(baseUrl: string): string {
83
- return `${baseUrl.replace(/\/+$/g, '')}/models`;
84
- }
8
+ export { cleanUrlCandidate, extractUrls, isPrivateOrLocalHost, isPrivateOrLocalUrl, localStackFor, modelsUrlFor, normalizeLocalBaseUrl, parseUrlCandidate } from './agent-harness-local-model-url.ts';
85
9
 
86
10
  export function localProviderNameFor(providerId: string | null, stack: string | null, fallback: string): string {
87
11
  const seed = providerId || stack || fallback;
@@ -128,6 +52,448 @@ export function localEndpointDiagnostics(endpoint: MutableLocalModelServerEndpoi
128
52
  };
129
53
  }
130
54
 
55
+ interface LocalModelServingReadModelSource {
56
+ readonly path: string;
57
+ readonly source: unknown;
58
+ }
59
+
60
+ interface LocalModelServingDiagnosticRecord extends LocalModelServerServingDiagnostics {
61
+ readonly baseUrl: string | null;
62
+ readonly modelsUrl: string | null;
63
+ readonly providerId: string | null;
64
+ readonly stack: string | null;
65
+ }
66
+
67
+ interface LocalModelServingDiagnosticIndex {
68
+ readonly records: readonly LocalModelServingDiagnosticRecord[];
69
+ readonly publication: Omit<LocalModelServerDiagnosticsPublication, 'matchedEndpointCount'>;
70
+ }
71
+
72
+ const LOCAL_MODEL_SERVING_READ_MODEL_PATHS = [
73
+ 'context.platform.readModels.localModelServers',
74
+ 'context.platform.readModels.localModelServing',
75
+ 'context.platform.readModels.localModelDiagnostics',
76
+ 'context.platform.readModels.models.localServers',
77
+ 'context.platform.readModels.models.servingDiagnostics',
78
+ 'context.platform.readModels.localModels.servingDiagnostics',
79
+ 'context.platform.readModels.ollama.servingDiagnostics',
80
+ 'context.platform.readModels.llamaCpp.servingDiagnostics',
81
+ 'context.platform.readModels.vllm.servingDiagnostics',
82
+ 'context.platform.readModels.localAi.servingDiagnostics',
83
+ 'context.platform.readModels.openAiCompatible.servingDiagnostics',
84
+ 'context.platform.localModelServing',
85
+ 'context.clients.operator.models.servingDiagnostics',
86
+ 'context.clients.operator.localModelServingDiagnostics',
87
+ ] as const;
88
+
89
+ const LOCAL_MODEL_SECRET_PATTERNS: readonly [RegExp, string][] = [
90
+ [/("?\b(?:api[-_]?key|apikey|token|secret|password|passwd|credential|authorization)\b"?\s*:\s*)("[^"]*"|'[^']*'|[^\s,}]+)/gi, '$1"<redacted>"'],
91
+ [/\b([A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTHORIZATION|BEARER)[A-Z0-9_]*)=("[^"]*"|'[^']*'|[^\s]+)/gi, '$1=<redacted>'],
92
+ [/(\b(?:token|secret|password|passwd|api[-_]?key|apikey|authorization|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,}]+)/gi, '$1<redacted>'],
93
+ [/(Authorization:\s*Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, '$1<redacted>'],
94
+ ];
95
+
96
+ function redactLocalModelDiagnosticText(value: string): string {
97
+ return LOCAL_MODEL_SECRET_PATTERNS.reduce((text, [pattern, replacement]) => text.replace(pattern, replacement), value);
98
+ }
99
+
100
+ function readNumber(value: unknown): number | null {
101
+ const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
102
+ return typeof parsed === 'number' && Number.isFinite(parsed) ? parsed : null;
103
+ }
104
+
105
+ function readBoolean(value: unknown): boolean | null {
106
+ if (typeof value === 'boolean') return value;
107
+ if (typeof value === 'string') {
108
+ const normalized = value.trim().toLowerCase();
109
+ if (['true', 'yes', '1', 'supported', 'enabled'].includes(normalized)) return true;
110
+ if (['false', 'no', '0', 'unsupported', 'disabled'].includes(normalized)) return false;
111
+ }
112
+ return null;
113
+ }
114
+
115
+ function readStringArray(value: unknown): readonly string[] {
116
+ const values = Array.isArray(value) ? value : [];
117
+ return [...new Set(values.map((entry) => {
118
+ if (typeof entry === 'string') return entry.trim();
119
+ const record = readRecord(entry);
120
+ return readString(record.id) || readString(record.name) || readString(record.model) || readString(record.modelId);
121
+ }).filter(Boolean))].slice(0, 12);
122
+ }
123
+
124
+ function firstString(record: Record<string, unknown>, keys: readonly string[]): string {
125
+ for (const key of keys) {
126
+ const value = readString(record[key]);
127
+ if (value) return value;
128
+ }
129
+ return '';
130
+ }
131
+
132
+ function firstNumber(record: Record<string, unknown>, keys: readonly string[]): number | null {
133
+ for (const key of keys) {
134
+ const value = readNumber(record[key]);
135
+ if (value !== null) return value;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ function firstBoolean(record: Record<string, unknown>, keys: readonly string[]): boolean | null {
141
+ for (const key of keys) {
142
+ const value = readBoolean(record[key]);
143
+ if (value !== null) return value;
144
+ }
145
+ return null;
146
+ }
147
+
148
+ function isoFromDiagnosticValue(value: unknown): string | null {
149
+ const timestamp = typeof value === 'number' && Number.isFinite(value)
150
+ ? value
151
+ : typeof value === 'string' && value.trim()
152
+ ? Date.parse(value)
153
+ : Number.NaN;
154
+ return Number.isNaN(timestamp) ? null : new Date(timestamp).toISOString();
155
+ }
156
+
157
+ function localServingStatus(record: Record<string, unknown>, loadedModelCount: number): LocalModelServerServingDiagnostics['status'] {
158
+ const value = firstString(record, ['status', 'healthStatus', 'state', 'result', 'outcome']).toLowerCase().replace(/[_\s]+/g, '-');
159
+ if (['ready', 'ok', 'healthy', 'running', 'online', 'passed', 'success', 'succeeded', 'available'].includes(value)) return 'ready';
160
+ if (['blocked', 'missing', 'unavailable', 'unreachable', 'offline', 'not-running', 'needs-setup'].includes(value)) return 'blocked';
161
+ if (['degraded', 'attention', 'warning', 'slow', 'resource-pressure', 'limited'].includes(value)) return 'attention';
162
+ if (loadedModelCount > 0) return 'ready';
163
+ return 'unknown';
164
+ }
165
+
166
+ function localResourcePressure(record: Record<string, unknown>): LocalModelServerServingDiagnostics['resourcePressure'] {
167
+ const value = firstString(record, ['resourcePressure', 'pressure', 'memoryPressure', 'vramPressure']).toLowerCase().replace(/[_\s]+/g, '-');
168
+ if (['low', 'ok', 'normal', 'healthy'].includes(value)) return 'low';
169
+ if (['moderate', 'medium', 'warning', 'attention'].includes(value)) return 'moderate';
170
+ if (['high', 'critical', 'exhausted', 'oom'].includes(value)) return 'high';
171
+ const usage = firstNumber(record, ['memoryUsagePercent', 'memoryPercent', 'vramUsagePercent', 'gpuMemoryPercent']);
172
+ if (usage !== null) return usage >= 90 ? 'high' : usage >= 70 ? 'moderate' : 'low';
173
+ return 'unknown';
174
+ }
175
+
176
+ function localResourceSummary(record: Record<string, unknown>, pressure: LocalModelServerServingDiagnostics['resourcePressure']): string | undefined {
177
+ const explicit = firstString(record, ['resourceSummary', 'resources', 'resourceDetail']);
178
+ if (explicit) return redactLocalModelDiagnosticText(previewHarnessText(explicit, 180));
179
+ const parts = [
180
+ firstNumber(record, ['memoryUsagePercent', 'memoryPercent']) !== null ? `memory ${firstNumber(record, ['memoryUsagePercent', 'memoryPercent'])}%` : '',
181
+ firstNumber(record, ['vramUsagePercent', 'gpuMemoryPercent']) !== null ? `vram ${firstNumber(record, ['vramUsagePercent', 'gpuMemoryPercent'])}%` : '',
182
+ firstNumber(record, ['cpuPercent', 'cpuUsagePercent']) !== null ? `cpu ${firstNumber(record, ['cpuPercent', 'cpuUsagePercent'])}%` : '',
183
+ ].filter(Boolean);
184
+ if (parts.length > 0) return `${pressure} pressure; ${parts.join(', ')}.`;
185
+ return undefined;
186
+ }
187
+
188
+ function firstStringFromRecords(records: readonly Record<string, unknown>[], keys: readonly string[]): string {
189
+ for (const record of records) {
190
+ const value = firstString(record, keys);
191
+ if (value) return value;
192
+ }
193
+ return '';
194
+ }
195
+
196
+ function localServingSchemaStatus(record: Record<string, unknown>): LocalModelServerServingDiagnostics['schemaStatus'] {
197
+ const records = [record, readRecord(record.schema), readRecord(record.contract), readRecord(record.receipt)];
198
+ const explicit = firstStringFromRecords(records, ['schemaStatus', 'receiptSchemaStatus', 'certificationStatus']).toLowerCase().replace(/[_\s]+/g, '-');
199
+ if (['certified', 'valid', 'verified', 'schema-certified'].includes(explicit)) return 'certified';
200
+ const schemaVersion = firstStringFromRecords(records, ['schemaVersion', 'receiptSchemaVersion', 'contractVersion']);
201
+ const provenance = firstStringFromRecords(records, ['methodId', 'sourceTool', 'actionId']);
202
+ const publicationGuarantee = firstStringFromRecords(records, ['publicationGuarantee', 'hostPublicationGuarantee', 'servingPublicationGuarantee']);
203
+ return schemaVersion && provenance && publicationGuarantee ? 'certified' : 'legacy';
204
+ }
205
+
206
+ function routeTextFromDiagnosticValue(value: unknown): string {
207
+ const direct = readString(value);
208
+ if (direct) return previewHarnessText(redactLocalModelDiagnosticText(direct), 260);
209
+ const record = readRecord(value);
210
+ const route = firstString(record, ['route', 'modelRoute', 'operatorRoute', 'methodRoute', 'commandRoute']);
211
+ return route ? previewHarnessText(redactLocalModelDiagnosticText(route), 260) : '';
212
+ }
213
+
214
+ function diagnosticRouteFromRecord(record: Record<string, unknown>, keys: readonly string[]): string {
215
+ for (const key of keys) {
216
+ const route = routeTextFromDiagnosticValue(record[key]);
217
+ if (route) return route;
218
+ }
219
+ for (const nestedKey of ['routes', 'actions', 'controls', 'operatorRoutes', 'hostRoutes', 'receiptRoutes']) {
220
+ const nested = readRecord(record[nestedKey]);
221
+ for (const key of keys) {
222
+ const route = routeTextFromDiagnosticValue(nested[key]);
223
+ if (route) return route;
224
+ }
225
+ }
226
+ return '';
227
+ }
228
+
229
+ function diagnosticPublicationGuarantee(record: Record<string, unknown>): string {
230
+ const value = firstStringFromRecords(
231
+ [record, readRecord(record.schema), readRecord(record.contract), readRecord(record.receipt)],
232
+ ['publicationGuarantee', 'hostPublicationGuarantee', 'servingPublicationGuarantee'],
233
+ );
234
+ return value ? previewHarnessText(redactLocalModelDiagnosticText(value), 220) : '';
235
+ }
236
+
237
+ function localServingProvenance(
238
+ record: Record<string, unknown>,
239
+ startRoute: string,
240
+ repairRoute: string,
241
+ ): readonly string[] {
242
+ const explicit = readStringArray(record.provenance);
243
+ const values = [
244
+ ...explicit,
245
+ firstStringFromRecords([record, readRecord(record.receipt)], ['methodId']) ? `method ${firstStringFromRecords([record, readRecord(record.receipt)], ['methodId'])}` : '',
246
+ firstString(record, ['actionId']) ? `action ${firstString(record, ['actionId'])}` : '',
247
+ firstString(record, ['sourceTool']) ? `sourceTool ${firstString(record, ['sourceTool'])}` : '',
248
+ startRoute ? `start ${startRoute}` : '',
249
+ repairRoute ? `repair ${repairRoute}` : '',
250
+ ];
251
+ return [...new Set(values
252
+ .map((entry) => previewHarnessText(redactLocalModelDiagnosticText(entry), 220))
253
+ .filter(Boolean))]
254
+ .slice(0, 8);
255
+ }
256
+
257
+ function setupServingReadModelSources(context: CommandContext): readonly LocalModelServingReadModelSource[] {
258
+ const contextRecord = context as unknown as Record<string, unknown>;
259
+ const platform = readRecord(contextRecord.platform);
260
+ const clients = readRecord(contextRecord.clients);
261
+ const readModels = readRecord(platform.readModels);
262
+ const modelReadModels = readRecord(readModels.models);
263
+ const localModels = readRecord(readModels.localModels);
264
+ const ollama = readRecord(readModels.ollama);
265
+ const llamaCpp = readRecord(readModels.llamaCpp);
266
+ const vllm = readRecord(readModels.vllm);
267
+ const localAi = readRecord(readModels.localAi);
268
+ const openAiCompatible = readRecord(readModels.openAiCompatible);
269
+ const operator = readRecord(clients.operator);
270
+ const operatorModels = readRecord(operator.models);
271
+ return [
272
+ { path: 'context.platform.readModels.localModelServers', source: readModels.localModelServers },
273
+ { path: 'context.platform.readModels.localModelServing', source: readModels.localModelServing },
274
+ { path: 'context.platform.readModels.localModelDiagnostics', source: readModels.localModelDiagnostics },
275
+ { path: 'context.platform.readModels.models.localServers', source: modelReadModels.localServers },
276
+ { path: 'context.platform.readModels.models.servingDiagnostics', source: modelReadModels.servingDiagnostics },
277
+ { path: 'context.platform.readModels.localModels.servingDiagnostics', source: localModels.servingDiagnostics },
278
+ { path: 'context.platform.readModels.ollama.servingDiagnostics', source: ollama.servingDiagnostics },
279
+ { path: 'context.platform.readModels.llamaCpp.servingDiagnostics', source: llamaCpp.servingDiagnostics },
280
+ { path: 'context.platform.readModels.vllm.servingDiagnostics', source: vllm.servingDiagnostics },
281
+ { path: 'context.platform.readModels.localAi.servingDiagnostics', source: localAi.servingDiagnostics },
282
+ { path: 'context.platform.readModels.openAiCompatible.servingDiagnostics', source: openAiCompatible.servingDiagnostics },
283
+ { path: 'context.platform.localModelServing', source: platform.localModelServing },
284
+ { path: 'context.clients.operator.models.servingDiagnostics', source: operatorModels.servingDiagnostics },
285
+ { path: 'context.clients.operator.localModelServingDiagnostics', source: operator.localModelServingDiagnostics },
286
+ ];
287
+ }
288
+
289
+ function readServingSnapshot(source: unknown): unknown {
290
+ if (typeof source === 'function') {
291
+ const value = (source as () => unknown)();
292
+ return value instanceof Promise ? null : value;
293
+ }
294
+ const record = readRecord(source);
295
+ for (const methodName of ['getSnapshot', 'snapshot', 'list', 'listDiagnostics', 'listServingDiagnostics', 'listLocalServers', 'listServers', 'readSnapshot']) {
296
+ const method = record[methodName];
297
+ if (typeof method === 'function') {
298
+ const value = (method as () => unknown)();
299
+ return value instanceof Promise ? null : value;
300
+ }
301
+ }
302
+ return source;
303
+ }
304
+
305
+ function diagnosticEntriesFromSnapshot(snapshot: unknown): readonly unknown[] {
306
+ if (Array.isArray(snapshot)) return snapshot;
307
+ if (snapshot instanceof Map) return [...snapshot.entries()].map(([id, entry]) => {
308
+ const entryRecord = readRecord(entry);
309
+ return Object.keys(entryRecord).length > 0 && !readString(entryRecord.id) ? { ...entryRecord, id } : entry;
310
+ });
311
+ const record = readRecord(snapshot);
312
+ for (const key of ['localModelServers', 'localServers', 'servingDiagnostics', 'diagnostics', 'servers', 'endpoints', 'records', 'items', 'entries']) {
313
+ const value = record[key];
314
+ if (value instanceof Map) return [...value.entries()].map(([id, entry]) => {
315
+ const entryRecord = readRecord(entry);
316
+ return Object.keys(entryRecord).length > 0 && !readString(entryRecord.id) ? { ...entryRecord, id } : entry;
317
+ });
318
+ if (Array.isArray(value)) return value;
319
+ const valueRecord = readRecord(value);
320
+ if (Object.keys(valueRecord).length > 0) {
321
+ return Object.entries(valueRecord).map(([id, entry]) => {
322
+ const entryRecord = readRecord(entry);
323
+ return Object.keys(entryRecord).length > 0 && !readString(entryRecord.id) ? { ...entryRecord, id } : entry;
324
+ });
325
+ }
326
+ }
327
+ return Object.keys(record).length > 0 ? [record] : [];
328
+ }
329
+
330
+ function isLocalServingDiagnosticRecord(record: Record<string, unknown>): boolean {
331
+ const base = firstString(record, ['baseUrl', 'baseURL', 'endpoint', 'url', 'modelsUrl', 'serverUrl']);
332
+ const providerId = firstString(record, ['providerId', 'provider', 'providerName']);
333
+ if (!base && !providerId) return false;
334
+ return Boolean(
335
+ firstString(record, ['serverVersion', 'version', 'resourcePressure', 'status', 'healthStatus', 'startReceiptId', 'repairReceiptId', 'receiptId'])
336
+ || readStringArray(record.loadedModels).length > 0
337
+ || readStringArray(record.models).length > 0
338
+ || firstNumber(record, ['loadedModelCount', 'modelCount', 'contextWindowTokens', 'memoryUsagePercent', 'vramUsagePercent']) !== null,
339
+ );
340
+ }
341
+
342
+ function normalizeServingDiagnosticRecord(value: unknown, source: string): LocalModelServingDiagnosticRecord | null {
343
+ const record = {
344
+ ...readRecord(readRecord(value).metadata),
345
+ ...readRecord(value),
346
+ };
347
+ if (!isLocalServingDiagnosticRecord(record)) return null;
348
+ const stack = firstString(record, ['stack', 'serverType', 'backend']) || localStackFor(JSON.stringify(record)) || null;
349
+ const rawBaseUrl = firstString(record, ['baseUrl', 'baseURL', 'endpoint', 'url', 'serverUrl']);
350
+ const rawModelsUrl = firstString(record, ['modelsUrl', 'modelListUrl']);
351
+ const baseUrl = rawBaseUrl
352
+ ? normalizeLocalBaseUrl(rawBaseUrl, stack)
353
+ : rawModelsUrl
354
+ ? normalizeLocalBaseUrl(rawModelsUrl, stack)
355
+ : null;
356
+ const modelsUrl = rawModelsUrl
357
+ ? modelsUrlFor(normalizeLocalBaseUrl(rawModelsUrl, stack) ?? rawModelsUrl.replace(/\/models\/?$/i, ''))
358
+ : baseUrl
359
+ ? modelsUrlFor(baseUrl)
360
+ : null;
361
+ const loadedModels = readStringArray(record.loadedModels).length > 0
362
+ ? readStringArray(record.loadedModels)
363
+ : readStringArray(record.models).length > 0
364
+ ? readStringArray(record.models)
365
+ : readStringArray(record.modelIds);
366
+ const loadedModelCount = firstNumber(record, ['loadedModelCount', 'modelCount']) ?? loadedModels.length;
367
+ const status = localServingStatus(record, loadedModelCount);
368
+ const resourcePressure = localResourcePressure(record);
369
+ const summary = redactLocalModelDiagnosticText(firstString(record, ['summary', 'detail', 'message']))
370
+ || `${providerIdLabel(firstString(record, ['providerId', 'provider', 'providerName']))} local serving diagnostics are ${status}.`;
371
+ const serverVersion = firstString(record, ['serverVersion', 'version']);
372
+ const contextWindowTokens = firstNumber(record, ['contextWindowTokens', 'contextWindow', 'maxContextTokens']);
373
+ const toolSupport = firstBoolean(record, ['toolSupport', 'toolCalling', 'supportsTools', 'supportsToolCalling']);
374
+ const startReceiptId = firstString(record, ['startReceiptId', 'startupReceiptId']);
375
+ const repairReceiptId = firstString(record, ['repairReceiptId', 'restartReceiptId']);
376
+ const receiptStatus = firstString(record, ['receiptStatus', 'receiptOutcome']);
377
+ const schemaStatus = localServingSchemaStatus(record);
378
+ const schemaVersion = firstStringFromRecords(
379
+ [record, readRecord(record.schema), readRecord(record.contract), readRecord(record.receipt)],
380
+ ['schemaVersion', 'receiptSchemaVersion', 'contractVersion'],
381
+ );
382
+ const publicationGuarantee = diagnosticPublicationGuarantee(record);
383
+ const publisher = firstString(record, ['publisher', 'publisherId', 'daemonId', 'hostId']);
384
+ const startRoute = diagnosticRouteFromRecord(record, ['startRoute', 'startupRoute', 'startModelServerRoute', 'startServerRoute', 'startActionRoute', 'startCommandRoute']);
385
+ const repairRoute = diagnosticRouteFromRecord(record, ['repairRoute', 'restartRoute', 'restartServerRoute', 'repairModelServerRoute', 'repairActionRoute', 'repairCommandRoute']);
386
+ const provenance = localServingProvenance(record, startRoute, repairRoute);
387
+ const missingSignals = [
388
+ ...(schemaStatus === 'certified' ? [] : ['Certified local serving diagnostics schema is not published.']),
389
+ ...(serverVersion ? [] : ['Server version is not published.']),
390
+ ...(loadedModelCount > 0 ? [] : ['Loaded model detail is not published.']),
391
+ ...(contextWindowTokens !== null ? [] : ['Context-window support is not published.']),
392
+ ...(toolSupport !== null ? [] : ['Tool-support capability is not published.']),
393
+ ...(resourcePressure !== 'unknown' ? [] : ['Resource pressure is not published.']),
394
+ ...(startReceiptId || repairReceiptId ? [] : ['Start/repair receipt ids are not published.']),
395
+ ...(startRoute || repairRoute ? [] : ['Host-published start/repair execution routes are not published.']),
396
+ ];
397
+ return {
398
+ status,
399
+ source,
400
+ summary: previewHarnessText(summary, 220),
401
+ schemaStatus,
402
+ ...(schemaVersion ? { schemaVersion: previewHarnessText(redactLocalModelDiagnosticText(schemaVersion), 80) } : {}),
403
+ ...(provenance.length > 0 ? { provenance } : {}),
404
+ ...(publicationGuarantee ? { publicationGuarantee } : {}),
405
+ ...(publisher ? { publisher: previewHarnessText(redactLocalModelDiagnosticText(publisher), 80) } : {}),
406
+ ...(serverVersion ? { serverVersion: previewHarnessText(serverVersion, 80) } : {}),
407
+ ...(loadedModelCount > 0 ? { loadedModelCount } : {}),
408
+ loadedModels: loadedModels.map((model) => previewHarnessText(model, 96)),
409
+ ...(contextWindowTokens !== null ? { contextWindowTokens } : {}),
410
+ ...(toolSupport !== null ? { toolSupport } : {}),
411
+ resourcePressure,
412
+ ...(localResourceSummary(record, resourcePressure) ? { resourceSummary: localResourceSummary(record, resourcePressure) } : {}),
413
+ lastCheckedAt: isoFromDiagnosticValue(firstString(record, ['lastCheckedAt', 'checkedAt', 'updatedAt', 'timestamp']) || record.lastCheckedAt || record.checkedAt || record.updatedAt || record.timestamp),
414
+ ...(startReceiptId ? { startReceiptId } : {}),
415
+ ...(repairReceiptId ? { repairReceiptId } : {}),
416
+ ...(receiptStatus ? { receiptStatus } : {}),
417
+ ...(startRoute ? { startRoute } : {}),
418
+ ...(repairRoute ? { repairRoute } : {}),
419
+ inspectRoute: firstString(record, ['inspectRoute', 'modelRoute', 'route']) || 'models action:"local" includeParameters:true',
420
+ missingSignals,
421
+ policy: 'Read-only daemon-published local serving diagnostics. Agent consumes certified version/model/capability/resource/receipt metadata when present, and surfaces start/repair routes only when the host publishes exact confirmed routes; provider edits, refresh, smoke, benchmark, and route changes remain separate visible confirmed actions.',
422
+ baseUrl,
423
+ modelsUrl,
424
+ providerId: firstString(record, ['providerId', 'provider', 'providerName']) || null,
425
+ stack,
426
+ };
427
+ }
428
+
429
+ function providerIdLabel(providerId: string): string {
430
+ return providerId || 'Detected';
431
+ }
432
+
433
+ function localServingDiagnosticsIndex(context: CommandContext): LocalModelServingDiagnosticIndex {
434
+ const records: LocalModelServingDiagnosticRecord[] = [];
435
+ const sourcePaths = new Set<string>();
436
+ let sawSource = false;
437
+ let sawError = false;
438
+ for (const entry of setupServingReadModelSources(context)) {
439
+ if (entry.source === undefined || entry.source === null) continue;
440
+ sawSource = true;
441
+ try {
442
+ const sourceRecords = diagnosticEntriesFromSnapshot(readServingSnapshot(entry.source))
443
+ .flatMap((value) => {
444
+ const record = normalizeServingDiagnosticRecord(value, entry.path);
445
+ return record ? [record] : [];
446
+ });
447
+ if (sourceRecords.length > 0) sourcePaths.add(entry.path);
448
+ records.push(...sourceRecords);
449
+ } catch {
450
+ sawError = true;
451
+ }
452
+ }
453
+ const status = records.length > 0
454
+ ? 'published-read-model'
455
+ : sawError
456
+ ? 'read-model-error'
457
+ : sawSource
458
+ ? 'read-model-empty'
459
+ : 'not-published';
460
+ const missingSignals = records.length > 0
461
+ ? []
462
+ : ['No daemon-published local serving diagnostics read model is available to Agent yet.'];
463
+ return {
464
+ records,
465
+ publication: {
466
+ status,
467
+ requiredPaths: LOCAL_MODEL_SERVING_READ_MODEL_PATHS,
468
+ sourcePaths: [...sourcePaths],
469
+ recordCount: records.length,
470
+ missingSignals,
471
+ policy: 'Agent reads local serving diagnostics only from SDK/daemon read models. It does not probe local servers until the user confirms models action:"smoke".',
472
+ },
473
+ };
474
+ }
475
+
476
+ function servingDiagnosticForEndpoint(
477
+ records: readonly LocalModelServingDiagnosticRecord[],
478
+ endpoint: MutableLocalModelServerEndpoint,
479
+ ): LocalModelServingDiagnosticRecord | null {
480
+ return records.find((record) => record.baseUrl === endpoint.baseUrl || record.modelsUrl === modelsUrlFor(endpoint.baseUrl))
481
+ ?? records.find((record) => Boolean(record.providerId && endpoint.providerId && record.providerId === endpoint.providerId))
482
+ ?? null;
483
+ }
484
+
485
+ export function localModelServerEndpoints(
486
+ context: CommandContext,
487
+ includeParameters: boolean,
488
+ ): readonly LocalModelServerEndpoint[] {
489
+ const servingDiagnostics = localServingDiagnosticsIndex(context);
490
+ return collectLocalServerEndpointCandidates(context).map((endpoint) => describeLocalServerEndpoint(
491
+ endpoint,
492
+ includeParameters,
493
+ servingDiagnosticForEndpoint(servingDiagnostics.records, endpoint),
494
+ ));
495
+ }
496
+
131
497
  export function localModelServerDefaults(): readonly LocalModelServerDefaultEndpoint[] {
132
498
  const defaults = [
133
499
  {
@@ -338,7 +704,11 @@ export function collectLocalServerEndpointCandidates(context: CommandContext): r
338
704
  return [...endpoints.values()].sort((left, right) => left.baseUrl.localeCompare(right.baseUrl));
339
705
  }
340
706
 
341
- export function describeLocalServerEndpoint(endpoint: MutableLocalModelServerEndpoint, includeParameters = false): LocalModelServerEndpoint {
707
+ export function describeLocalServerEndpoint(
708
+ endpoint: MutableLocalModelServerEndpoint,
709
+ includeParameters = false,
710
+ servingDiagnostics?: LocalModelServerServingDiagnostics | null,
711
+ ): LocalModelServerEndpoint {
342
712
  const modelsUrl = modelsUrlFor(endpoint.baseUrl);
343
713
  const providerExists = Boolean(endpoint.providerId) || endpoint.modelRoutes.size > 0;
344
714
  const notes = new Set(endpoint.notes);
@@ -366,6 +736,7 @@ export function describeLocalServerEndpoint(endpoint: MutableLocalModelServerEnd
366
736
  refreshRoute: 'agent_harness mode:"run_command" command:"/refresh-models" confirm:true explicitUserRequest:"Refresh models after verifying the local server."',
367
737
  addProviderRoute: providerExists ? null : localProviderAddRoute(endpoint.providerId, endpoint.stack, endpoint.baseUrl),
368
738
  notes: [...notes],
739
+ ...(servingDiagnostics ? { servingDiagnostics } : {}),
369
740
  ...(includeParameters ? { diagnostics: localEndpointDiagnostics(endpoint, providerExists) } : {}),
370
741
  };
371
742
  }
@@ -374,10 +745,20 @@ export function localModelServerHealthMap(
374
745
  context: CommandContext,
375
746
  includeParameters: boolean,
376
747
  ): LocalModelServerHealthMap {
377
- const endpoints = collectLocalServerEndpointCandidates(context).map((endpoint) => describeLocalServerEndpoint(endpoint, includeParameters));
748
+ const servingDiagnostics = localServingDiagnosticsIndex(context);
749
+ const rawEndpoints = collectLocalServerEndpointCandidates(context);
750
+ const matchedEndpointCount = rawEndpoints
751
+ .filter((endpoint) => Boolean(servingDiagnosticForEndpoint(servingDiagnostics.records, endpoint)))
752
+ .length;
753
+ const endpoints = rawEndpoints.map((endpoint) => describeLocalServerEndpoint(
754
+ endpoint,
755
+ includeParameters,
756
+ servingDiagnosticForEndpoint(servingDiagnostics.records, endpoint),
757
+ ));
378
758
  const returned = endpoints.slice(0, includeParameters ? 8 : 3);
379
759
  const suggestedDefaults = localModelServerDefaults().slice(0, includeParameters ? 4 : 2);
380
760
  const first = returned[0];
761
+ const hasMatchedDiagnostics = matchedEndpointCount > 0;
381
762
  return {
382
763
  status: endpoints.length > 0 ? 'candidate-endpoints' : 'no-local-endpoints',
383
764
  liveProbe: 'not-run',
@@ -385,8 +766,14 @@ export function localModelServerHealthMap(
385
766
  returnedEndpoints: returned.length,
386
767
  endpoints: returned,
387
768
  suggestedDefaults,
769
+ daemonDiagnostics: {
770
+ ...servingDiagnostics.publication,
771
+ matchedEndpointCount,
772
+ missingSignals: hasMatchedDiagnostics ? [] : servingDiagnostics.publication.missingSignals,
773
+ },
388
774
  nextActions: endpoints.length > 0
389
775
  ? [
776
+ ...(hasMatchedDiagnostics ? ['Review published local serving diagnostics before smoke, refresh, benchmark, or provider changes.'] : []),
390
777
  `Smoke test ${first?.modelsUrl ?? 'the detected model-list endpoint'} before benchmark or route changes.`,
391
778
  'Refresh the model catalog after the local server is running and reachable.',
392
779
  'Run the local benchmark comparison before making a local route the default.',
@@ -396,278 +783,6 @@ export function localModelServerHealthMap(
396
783
  'Add or select the provider route only after the server is reachable.',
397
784
  'Refresh models and run the local benchmark before changing the default route.',
398
785
  ],
399
- policy: 'Read-only local endpoint map. It derives candidate model-list URLs, smoke commands, and confirmed route hints from registry/env metadata; it does not probe the network, install servers, download models, add providers, refresh models, benchmark, or change routes.',
400
- };
401
- }
402
-
403
- function localModelSmokeTargetFromEndpoint(endpoint: LocalModelServerEndpoint): LocalModelSmokeTarget {
404
- return {
405
- kind: endpoint.kind,
406
- id: endpoint.id,
407
- label: `Local model server ${endpoint.baseUrl}`,
408
- providerId: endpoint.providerId,
409
- stack: endpoint.stack,
410
- baseUrl: endpoint.baseUrl,
411
- modelsUrl: endpoint.modelsUrl,
412
- smokeCommand: endpoint.smokeCommand,
413
- smokeRoute: endpoint.smokeRoute,
414
- refreshRoute: endpoint.refreshRoute,
415
- addProviderRoute: endpoint.addProviderRoute,
416
- source: endpoint.sources.join(', ') || 'local-endpoint',
417
- notes: endpoint.notes,
418
- };
419
- }
420
-
421
- function localModelSmokeTargetFromDefault(endpoint: LocalModelServerDefaultEndpoint): LocalModelSmokeTarget {
422
- return {
423
- kind: 'suggested-local-server',
424
- id: endpoint.id,
425
- label: endpoint.label,
426
- providerId: null,
427
- stack: endpoint.stack,
428
- baseUrl: endpoint.baseUrl,
429
- modelsUrl: endpoint.modelsUrl,
430
- smokeCommand: endpoint.smokeCommand,
431
- smokeRoute: localEndpointSmokeRoute(endpoint.id),
432
- refreshRoute: 'agent_harness mode:"run_command" command:"/refresh-models" confirm:true explicitUserRequest:"Refresh models after verifying the local server."',
433
- addProviderRoute: endpoint.addProviderRoute,
434
- source: 'suggested-default',
435
- notes: [endpoint.startHint],
436
- };
437
- }
438
-
439
- function localSmokeTargetSearchText(target: LocalModelSmokeTarget): string {
440
- return [
441
- target.kind,
442
- target.id,
443
- target.label,
444
- target.providerId ?? '',
445
- target.stack ?? '',
446
- target.baseUrl,
447
- target.modelsUrl,
448
- target.source,
449
- ...target.notes,
450
- ].join('\n').toLowerCase();
451
- }
452
-
453
- function localModelSmokeLookup(args: AgentHarnessModelRoutingArgs): string {
454
- const fields = readRecord(args.fields);
455
- return readString(args.modelRouteId)
456
- || readString(args.target)
457
- || readString(args.query)
458
- || readString(fields.endpointId)
459
- || readString(fields.modelRouteId)
460
- || readString(fields.baseUrl)
461
- || readString(fields.modelsUrl);
462
- }
463
-
464
- function localModelSmokeTargets(context: CommandContext, args: AgentHarnessModelRoutingArgs): Record<string, unknown> | readonly LocalModelSmokeTarget[] {
465
- const endpoints = collectLocalServerEndpointCandidates(context)
466
- .map((endpoint) => localModelSmokeTargetFromEndpoint(describeLocalServerEndpoint(endpoint, true)));
467
- const defaults = localModelServerDefaults().map(localModelSmokeTargetFromDefault);
468
- const lookup = localModelSmokeLookup(args);
469
- const allTargets = [...endpoints, ...defaults];
470
- if (lookup) {
471
- const normalized = lookup.toLowerCase();
472
- const exact = allTargets.filter((target) => target.id === lookup || target.baseUrl === lookup || target.modelsUrl === lookup);
473
- if (exact.length === 1) return exact;
474
- if (exact.length > 1) {
475
- return {
476
- status: 'ambiguous',
477
- input: lookup,
478
- candidates: exact.slice(0, 8).map((target) => ({
479
- kind: target.kind,
480
- id: target.id,
481
- label: target.label,
482
- baseUrl: target.baseUrl,
483
- modelsUrl: target.modelsUrl,
484
- })),
485
- };
486
- }
487
- const searched = allTargets.filter((target) => localSmokeTargetSearchText(target).includes(normalized));
488
- if (searched.length === 1) return searched;
489
- if (searched.length > 1) {
490
- return {
491
- status: 'ambiguous',
492
- input: lookup,
493
- candidates: searched.slice(0, 8).map((target) => ({
494
- kind: target.kind,
495
- id: target.id,
496
- label: target.label,
497
- baseUrl: target.baseUrl,
498
- modelsUrl: target.modelsUrl,
499
- })),
500
- };
501
- }
502
- return {
503
- status: 'missing_lookup',
504
- input: lookup,
505
- usage: 'Unknown local model endpoint. Use models action:"local" includeParameters:true to inspect local endpoint ids, or omit the lookup to check detected/default local servers.',
506
- };
507
- }
508
- const pool = endpoints.length ? endpoints : defaults;
509
- return pool.slice(0, readLimit(args.limit, 4));
510
- }
511
-
512
- function readSmokeTimeoutMs(value: unknown): number {
513
- const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
514
- if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return 1500;
515
- return Math.max(250, Math.min(10000, Math.trunc(parsed)));
516
- }
517
-
518
- function localSmokeNetworkScope(modelsUrl: string): { readonly allowed: boolean; readonly scope: string; readonly reason?: string } {
519
- const url = parseUrlCandidate(modelsUrl);
520
- if (!url || !/^https?:$/.test(url.protocol)) return { allowed: false, scope: 'invalid-url', reason: 'The model-list URL is not a valid HTTP(S) URL.' };
521
- const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
522
- if (host === '0.0.0.0') {
523
- return { allowed: false, scope: 'bind-all-host', reason: '0.0.0.0 is a bind address, not a client URL. Use 127.0.0.1 or the intended LAN host.' };
524
- }
525
- if (!isPrivateOrLocalUrl(url.href)) {
526
- return { allowed: false, scope: 'non-local-host', reason: 'Local model smoke only probes loopback, local-name, or private LAN endpoints.' };
527
- }
528
- if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return { allowed: true, scope: 'loopback' };
529
- if (host.endsWith('.local') || !host.includes('.')) return { allowed: true, scope: 'local-name' };
530
- return { allowed: true, scope: 'private-lan' };
531
- }
532
-
533
- function extractModelIdsFromPayload(payload: unknown): readonly string[] {
534
- const record = readRecord(payload);
535
- const candidates = Array.isArray(record.data)
536
- ? record.data
537
- : Array.isArray(record.models)
538
- ? record.models
539
- : Array.isArray(payload)
540
- ? payload
541
- : [];
542
- const ids = candidates.map((entry) => {
543
- if (typeof entry === 'string') return entry;
544
- const item = readRecord(entry);
545
- return readString(item.id) || readString(item.name) || readString(item.model);
546
- }).filter(Boolean);
547
- return [...new Set(ids)].slice(0, 12);
548
- }
549
-
550
- function safeSmokeError(error: unknown): string {
551
- const message = error instanceof Error ? error.message : String(error);
552
- return previewHarnessText(message.replace(/https?:\/\/\S+/g, '[redacted-url]'), 180);
553
- }
554
-
555
- async function smokeOneLocalModelTarget(target: LocalModelSmokeTarget, timeoutMs: number): Promise<Record<string, unknown>> {
556
- const network = localSmokeNetworkScope(target.modelsUrl);
557
- if (!network.allowed) {
558
- return {
559
- ...target,
560
- status: 'blocked',
561
- liveProbe: 'confirmed',
562
- networkScope: network.scope,
563
- failure: network.reason,
564
- nextActions: ['Inspect the endpoint route and correct the base URL before running smoke again.'],
565
- };
566
- }
567
-
568
- const controller = new AbortController();
569
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
570
- const started = Date.now();
571
- try {
572
- const response = await fetch(target.modelsUrl, {
573
- method: 'GET',
574
- headers: { Accept: 'application/json' },
575
- signal: controller.signal,
576
- });
577
- const elapsedMs = Date.now() - started;
578
- const contentType = response.headers.get('content-type') ?? '';
579
- const text = await response.text();
580
- let payload: unknown = null;
581
- let jsonValid = false;
582
- try {
583
- payload = text ? JSON.parse(text) : null;
584
- jsonValid = true;
585
- } catch {
586
- jsonValid = false;
587
- }
588
- const modelIds = jsonValid ? extractModelIdsFromPayload(payload) : [];
589
- const status = !response.ok
590
- ? 'http-error'
591
- : !jsonValid
592
- ? 'invalid-json'
593
- : modelIds.length === 0
594
- ? 'no-models'
595
- : 'passed';
596
- return {
597
- ...target,
598
- status,
599
- liveProbe: 'confirmed',
600
- networkScope: network.scope,
601
- httpStatus: response.status,
602
- contentType,
603
- elapsedMs,
604
- jsonValid,
605
- modelCount: modelIds.length,
606
- sampleModelIds: modelIds.slice(0, 5),
607
- success: status === 'passed',
608
- nextActions: status === 'passed'
609
- ? ['Refresh the model catalog, then run a local benchmark before changing the default model.']
610
- : ['Start or fix the local server, confirm /v1/models returns model ids, then retry this smoke check.'],
611
- };
612
- } catch (error) {
613
- const elapsedMs = Date.now() - started;
614
- const aborted = controller.signal.aborted;
615
- return {
616
- ...target,
617
- status: aborted ? 'timeout' : 'unreachable',
618
- liveProbe: 'confirmed',
619
- networkScope: network.scope,
620
- elapsedMs,
621
- timeoutMs,
622
- success: false,
623
- failure: aborted ? `Timed out after ${timeoutMs}ms.` : safeSmokeError(error),
624
- nextActions: ['Start the local server, load at least one model, verify the base URL, then retry this smoke check.'],
625
- };
626
- } finally {
627
- clearTimeout(timeout);
628
- }
629
- }
630
-
631
- export async function runLocalModelServerSmoke(context: CommandContext, args: AgentHarnessModelRoutingArgs): Promise<Record<string, unknown>> {
632
- const targets = localModelSmokeTargets(context, args);
633
- if (!Array.isArray(targets)) {
634
- return {
635
- kind: 'local-model-smoke',
636
- liveProbe: 'not-run',
637
- ...targets,
638
- policy: 'No local model endpoint was probed because the requested endpoint lookup did not resolve exactly.',
639
- };
640
- }
641
- if (targets.length === 0) {
642
- return {
643
- kind: 'local-model-smoke',
644
- status: 'no-candidates',
645
- liveProbe: 'not-run',
646
- endpoints: [],
647
- nextActions: ['Use the local model cookbook to start a local server or configure a local provider endpoint.'],
648
- cookbookRoute: 'models action:"local" includeParameters:true',
649
- policy: 'No local model endpoint was probed because no candidate endpoints were available.',
650
- };
651
- }
652
- const timeoutMs = readSmokeTimeoutMs(args.timeoutMs);
653
- const checkedAt = new Date().toISOString();
654
- const results = await Promise.all(targets.map((target) => smokeOneLocalModelTarget(target, timeoutMs)));
655
- const passed = results.filter((result) => result.success === true);
656
- const blocked = results.filter((result) => result.status === 'blocked');
657
- return {
658
- kind: 'local-model-smoke',
659
- status: passed.length > 0 ? 'ready' : blocked.length === results.length ? 'blocked' : 'needs-attention',
660
- liveProbe: 'confirmed',
661
- checkedAt,
662
- timeoutMs,
663
- endpointCount: results.length,
664
- passedCount: passed.length,
665
- failedCount: results.length - passed.length,
666
- endpoints: results,
667
- nextActions: passed.length > 0
668
- ? ['Refresh the model catalog and run the local benchmark action before changing the default route.']
669
- : ['Start a local model server, load one model, and rerun this confirmed smoke check.'],
670
- cookbookRoute: 'models action:"local" includeParameters:true',
671
- policy: 'Confirmed read-only local model smoke. Agent only sends bounded GET requests to discovered or suggested local/private model-list endpoints; it does not add providers, refresh catalogs, benchmark, download models, or change routes.',
786
+ policy: 'Read-only local endpoint map. It derives candidate model-list URLs, smoke commands, confirmed route hints, and daemon-published serving diagnostics from registry/env/read-model metadata; it does not probe the network, install servers, download models, add providers, refresh models, benchmark, or change routes.',
672
787
  };
673
788
  }