@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.
- package/CHANGELOG.md +159 -0
- package/dist/package/main.js +19016 -12684
- package/docs/README.md +3 -3
- package/docs/connected-host.md +2 -2
- package/docs/getting-started.md +11 -9
- package/docs/tools-and-commands.md +12 -10
- package/docs/voice-and-live-tts.md +2 -2
- package/package.json +1 -1
- package/release/release-notes.md +17 -5
- package/release/release-readiness.json +36 -36
- package/src/agent/competitive-feature-inventory.ts +42 -44
- package/src/agent/setup-wizard-artifact-receipts.ts +366 -0
- package/src/agent/setup-wizard.ts +245 -9
- package/src/input/agent-workspace-categories.ts +2 -2
- package/src/input/agent-workspace-onboarding-categories.ts +4 -4
- package/src/input/agent-workspace-settings.ts +52 -1
- package/src/input/agent-workspace-setup-snapshot.ts +20 -1
- package/src/input/agent-workspace-setup.ts +32 -5
- package/src/input/agent-workspace-snapshot.ts +23 -3
- package/src/input/agent-workspace.ts +5 -1
- package/src/input/setup-wizard-live-receipts.ts +76 -0
- package/src/renderer/agent-workspace-context-lines.ts +21 -6
- package/src/renderer/agent-workspace.ts +46 -10
- package/src/runtime/tool-permission-safety.ts +1 -1
- package/src/tools/agent-harness-agent-orchestration-policy.ts +75 -0
- package/src/tools/agent-harness-agent-orchestration.ts +216 -128
- package/src/tools/agent-harness-autonomy-live-records.ts +154 -0
- package/src/tools/agent-harness-autonomy-queue-types.ts +1 -1
- package/src/tools/agent-harness-autonomy-queue.ts +19 -8
- package/src/tools/agent-harness-autonomy-watcher-read-models.ts +509 -0
- package/src/tools/agent-harness-background-processes.ts +8 -4
- package/src/tools/agent-harness-browser-cockpit-route.ts +188 -34
- package/src/tools/agent-harness-browser-control.ts +12 -4
- package/src/tools/agent-harness-browser-pwa-read-models.ts +622 -0
- package/src/tools/agent-harness-device-live-read-models.ts +366 -0
- package/src/tools/agent-harness-execution-posture.ts +3 -0
- package/src/tools/agent-harness-interactive-runtime-records.ts +421 -0
- package/src/tools/agent-harness-local-model-benchmarks.ts +71 -1
- package/src/tools/agent-harness-local-model-endpoints.ts +469 -354
- package/src/tools/agent-harness-local-model-smoke.ts +277 -0
- package/src/tools/agent-harness-local-model-url.ts +78 -0
- package/src/tools/agent-harness-media-posture.ts +27 -12
- package/src/tools/agent-harness-memory-external-providers.ts +796 -0
- package/src/tools/agent-harness-memory-posture.ts +253 -137
- package/src/tools/agent-harness-memory-provider-certification.ts +219 -0
- package/src/tools/agent-harness-memory-refinement.ts +340 -0
- package/src/tools/agent-harness-mode-catalog.ts +4 -2
- package/src/tools/agent-harness-model-provider-health.ts +139 -42
- package/src/tools/agent-harness-model-readiness.ts +31 -5
- package/src/tools/agent-harness-model-routing-types.ts +61 -0
- package/src/tools/agent-harness-model-routing.ts +31 -6
- package/src/tools/agent-harness-pairing-posture.ts +30 -9
- package/src/tools/agent-harness-personal-ops-certification.ts +116 -0
- package/src/tools/agent-harness-personal-ops-lanes.ts +81 -15
- package/src/tools/agent-harness-personal-ops-operations.ts +225 -0
- package/src/tools/agent-harness-personal-ops-provider-records.ts +358 -0
- package/src/tools/agent-harness-personal-ops-provider-task-records.ts +321 -0
- package/src/tools/agent-harness-personal-ops-records.ts +176 -224
- package/src/tools/agent-harness-personal-ops-types.ts +19 -1
- package/src/tools/agent-harness-personal-ops.ts +18 -11
- package/src/tools/agent-harness-remote-read-models.ts +541 -0
- package/src/tools/agent-harness-research-briefing.ts +26 -7
- package/src/tools/agent-harness-research-live-read-models.ts +500 -0
- package/src/tools/agent-harness-research-runs.ts +15 -3
- package/src/tools/agent-harness-research-workflow.ts +92 -19
- package/src/tools/agent-harness-setup-model-helpers.ts +1 -0
- package/src/tools/agent-harness-setup-smoke.ts +26 -1
- package/src/tools/agent-harness-tool-schema.ts +23 -1
- package/src/tools/agent-harness-tool-types.ts +5 -0
- package/src/tools/agent-harness-tool.ts +9 -1
- package/src/tools/agent-harness-workspace-actions.ts +1 -1
- package/src/tools/agent-memory-tool.ts +40 -1
- package/src/tools/agent-model-compare-run.ts +13 -0
- package/src/tools/agent-research-runner.ts +367 -0
- package/src/tools/agent-research-tool.ts +7 -179
- package/src/tools/agent-route-planner-candidates-surfaces.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -6,68 +6,133 @@ import { readBoolean, readFiniteNumber, readRecord, readString, safeIso } from '
|
|
|
6
6
|
function readPotentialProviderHealthSnapshot(source: unknown): unknown {
|
|
7
7
|
if (!source || typeof source !== 'object') return null;
|
|
8
8
|
const record = source as Record<string, unknown>;
|
|
9
|
-
const directSnapshot =
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const directSnapshot = ['getSnapshot', 'snapshot', 'list', 'listProviders', 'listRoutes', 'listHealth']
|
|
10
|
+
.map((method) => {
|
|
11
|
+
const fn = record[method];
|
|
12
|
+
if (typeof fn !== 'function') return null;
|
|
13
|
+
const result = (fn as () => unknown).call(record);
|
|
14
|
+
return result instanceof Promise ? null : result;
|
|
15
|
+
})
|
|
16
|
+
.find((result) => result !== null);
|
|
12
17
|
return directSnapshot ?? record.snapshot ?? record.data ?? record.state ?? source;
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
const PROVIDER_HEALTH_SECRET_PATTERNS: readonly [RegExp, string][] = [
|
|
21
|
+
[/("?\b(?:api[-_]?key|apikey|token|secret|password|passwd|credential|authorization)\b"?\s*:\s*)("[^"]*"|'[^']*'|[^\s,}]+)/gi, '$1"<redacted>"'],
|
|
22
|
+
[/\b([A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTHORIZATION|BEARER)[A-Z0-9_]*)=("[^"]*"|'[^']*'|[^\s]+)/gi, '$1=<redacted>'],
|
|
23
|
+
[/(\b(?:token|secret|password|passwd|api[-_]?key|apikey|authorization|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,}]+)/gi, '$1<redacted>'],
|
|
24
|
+
[/(Authorization:\s*Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, '$1<redacted>'],
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function redactProviderHealthText(value: string): string {
|
|
28
|
+
return PROVIDER_HEALTH_SECRET_PATTERNS.reduce((text, [pattern, replacement]) => text.replace(pattern, replacement), value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function entryWithKey(key: unknown, entry: unknown): Record<string, unknown> {
|
|
32
|
+
const record = readRecord(entry);
|
|
33
|
+
const textKey = readString(key);
|
|
34
|
+
if (!textKey) return record;
|
|
35
|
+
const looksLikeRoute = textKey.includes(':') || textKey.includes('/');
|
|
36
|
+
return {
|
|
37
|
+
...record,
|
|
38
|
+
...(looksLikeRoute ? { modelRouteId: readString(record.modelRouteId) || readString(record.routeId) || textKey } : {}),
|
|
39
|
+
providerId: readString(record.providerId) || readString(record.provider) || (looksLikeRoute ? readString(record.providerId) : textKey),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function looksLikeProviderHealthRecord(record: Record<string, unknown>): boolean {
|
|
44
|
+
return Boolean(
|
|
45
|
+
readString(record.providerId)
|
|
46
|
+
|| readString(record.provider)
|
|
47
|
+
|| readString(record.modelRouteId)
|
|
48
|
+
|| readString(record.routeId)
|
|
49
|
+
|| readString(record.registryKey)
|
|
50
|
+
|| readString(record.status)
|
|
51
|
+
|| readFiniteNumber(record.avgLatencyMs) !== undefined
|
|
52
|
+
|| Object.keys(readRecord(record.stats)).length > 0,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
15
56
|
function providerHealthCandidatesFromSnapshot(snapshot: unknown): readonly Record<string, unknown>[] {
|
|
16
57
|
if (!snapshot || typeof snapshot !== 'object') return [];
|
|
17
58
|
if (snapshot instanceof Map) {
|
|
18
|
-
return [...snapshot.entries()].map(([key, entry]) =>
|
|
19
|
-
const record = readRecord(entry);
|
|
20
|
-
return { ...record, providerId: readString(record.providerId) || readString(key) };
|
|
21
|
-
});
|
|
59
|
+
return [...snapshot.entries()].map(([key, entry]) => entryWithKey(key, entry));
|
|
22
60
|
}
|
|
23
61
|
const record = readRecord(snapshot);
|
|
24
|
-
const providers
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
if (Array.isArray(providers)) {
|
|
33
|
-
return providers.map((entry) => readRecord(entry));
|
|
34
|
-
}
|
|
35
|
-
if (providers && typeof providers === 'object') {
|
|
36
|
-
return Object.entries(providers as Record<string, unknown>).map(([key, entry]) => {
|
|
37
|
-
const providerRecord = readRecord(entry);
|
|
38
|
-
return { ...providerRecord, providerId: readString(providerRecord.providerId) || key };
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
if (entries instanceof Map) {
|
|
42
|
-
return [...entries.entries()].map(([key, entry]) => {
|
|
43
|
-
const entryRecord = readRecord(entry);
|
|
44
|
-
return { ...entryRecord, providerId: readString(entryRecord.providerId) || readString(key) };
|
|
45
|
-
});
|
|
62
|
+
for (const key of ['providers', 'providerHealth', 'routes', 'modelRoutes', 'routeHealth', 'providerRoutes', 'records', 'items', 'entries', 'health']) {
|
|
63
|
+
const value = record[key];
|
|
64
|
+
if (value instanceof Map) return [...value.entries()].map(([entryKey, entry]) => entryWithKey(entryKey, entry));
|
|
65
|
+
if (Array.isArray(value)) return value.map((entry) => readRecord(entry));
|
|
66
|
+
const valueRecord = readRecord(value);
|
|
67
|
+
if (Object.keys(valueRecord).length > 0) return Object.entries(valueRecord).map(([entryKey, entry]) => entryWithKey(entryKey, entry));
|
|
46
68
|
}
|
|
47
|
-
|
|
48
|
-
return entries.map((entry) => readRecord(entry));
|
|
49
|
-
}
|
|
50
|
-
return [];
|
|
69
|
+
return looksLikeProviderHealthRecord(record) ? [record] : [];
|
|
51
70
|
}
|
|
52
71
|
|
|
53
|
-
function
|
|
72
|
+
function providerHealthCandidateProviderId(candidate: Record<string, unknown>): string {
|
|
54
73
|
return readString(candidate.providerId)
|
|
55
|
-
|| readString(candidate.id)
|
|
56
74
|
|| readString(candidate.provider)
|
|
57
|
-
|| readString(candidate.
|
|
75
|
+
|| readString(candidate.providerName)
|
|
76
|
+
|| (readString(candidate.id).includes(':') ? '' : readString(candidate.id));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function providerHealthCandidateRouteId(candidate: Record<string, unknown>): string {
|
|
80
|
+
return readString(candidate.modelRouteId)
|
|
81
|
+
|| readString(candidate.registryKey)
|
|
82
|
+
|| readString(candidate.routeId)
|
|
83
|
+
|| readString(candidate.modelRoute)
|
|
84
|
+
|| (readString(candidate.id).includes(':') ? readString(candidate.id) : '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function providerHealthCandidateMatchesProvider(candidate: Record<string, unknown>, providerId: string): boolean {
|
|
88
|
+
return providerHealthCandidateProviderId(candidate) === providerId;
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
function providerHealthRecordStats(candidate: Record<string, unknown>): Record<string, unknown> {
|
|
61
92
|
return readRecord(candidate.stats);
|
|
62
93
|
}
|
|
63
94
|
|
|
95
|
+
function providerHealthRecordRateLimit(candidate: Record<string, unknown>): Record<string, unknown> {
|
|
96
|
+
return readRecord(candidate.rateLimit);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function providerHealthRecordErrors(candidate: Record<string, unknown>): Record<string, unknown> {
|
|
100
|
+
return readRecord(candidate.errors);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isoFromUnknown(value: unknown): string | null {
|
|
104
|
+
const numeric = readFiniteNumber(value);
|
|
105
|
+
if (numeric !== undefined) return safeIso(numeric);
|
|
106
|
+
const text = readString(value);
|
|
107
|
+
if (!text) return null;
|
|
108
|
+
const parsed = Date.parse(text);
|
|
109
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function firstTimestamp(values: readonly unknown[]): string | null {
|
|
113
|
+
for (const value of values) {
|
|
114
|
+
const timestamp = isoFromUnknown(value);
|
|
115
|
+
if (timestamp) return timestamp;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
64
120
|
function providerHealthTimestamp(candidate: Record<string, unknown>, stats: Record<string, unknown>, key: string): string | null {
|
|
65
|
-
return
|
|
121
|
+
return firstTimestamp([candidate[key], stats[key]]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function nestedNumber(candidate: Record<string, unknown>, stats: Record<string, unknown>, nested: Record<string, unknown>, keys: readonly string[]): number | undefined {
|
|
125
|
+
for (const key of keys) {
|
|
126
|
+
const value = readFiniteNumber(candidate[key]) ?? readFiniteNumber(stats[key]) ?? readFiniteNumber(nested[key]);
|
|
127
|
+
if (value !== undefined) return value;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
66
130
|
}
|
|
67
131
|
|
|
68
|
-
export function readProviderHealthSignal(context: CommandContext, providerId: string): ModelProviderHealthSignal {
|
|
132
|
+
export function readProviderHealthSignal(context: CommandContext, providerId: string, modelRouteId?: string): ModelProviderHealthSignal {
|
|
69
133
|
const base: Omit<ModelProviderHealthSignal, 'status' | 'daemonPublication' | 'agentConsumption' | 'missingSignals' | 'policy'> = {
|
|
70
134
|
providerId,
|
|
135
|
+
...(modelRouteId ? { modelRouteId } : {}),
|
|
71
136
|
sdkContract: {
|
|
72
137
|
providerHealthTypes: 'available',
|
|
73
138
|
importSurface: '@pellux/goodvibes-sdk/platform/runtime/ui',
|
|
@@ -77,10 +142,17 @@ export function readProviderHealthSignal(context: CommandContext, providerId: st
|
|
|
77
142
|
const policy = 'Provider health is live route-condition evidence. Local benchmark artifacts remain separate task-fit evidence and must not be treated as provider-health publication.';
|
|
78
143
|
const platform = context.platform as unknown as Record<string, unknown>;
|
|
79
144
|
const readModels = readRecord(platform.readModels);
|
|
145
|
+
const clients = readRecord(context.clients as unknown as Record<string, unknown>);
|
|
146
|
+
const operator = readRecord(clients.operator);
|
|
80
147
|
const sourceEntries: readonly { readonly path: string; readonly source: unknown }[] = [
|
|
81
148
|
{ path: 'context.platform.readModels.providerHealth', source: readModels.providerHealth },
|
|
82
149
|
{ path: 'context.platform.readModels.providersHealth', source: readModels.providersHealth },
|
|
150
|
+
{ path: 'context.platform.readModels.modelRouteHealth', source: readModels.modelRouteHealth },
|
|
151
|
+
{ path: 'context.platform.readModels.routeHealth', source: readModels.routeHealth },
|
|
152
|
+
{ path: 'context.platform.readModels.models.providerHealth', source: readRecord(readModels.models).providerHealth },
|
|
153
|
+
{ path: 'context.platform.readModels.models.routeHealth', source: readRecord(readModels.models).routeHealth },
|
|
83
154
|
{ path: 'context.platform.providerHealth', source: platform.providerHealth },
|
|
155
|
+
{ path: 'context.clients.operator.providerHealth', source: operator.providerHealth },
|
|
84
156
|
];
|
|
85
157
|
|
|
86
158
|
for (const entry of sourceEntries) {
|
|
@@ -88,7 +160,10 @@ export function readProviderHealthSignal(context: CommandContext, providerId: st
|
|
|
88
160
|
try {
|
|
89
161
|
const snapshot = readPotentialProviderHealthSnapshot(entry.source);
|
|
90
162
|
const candidates = providerHealthCandidatesFromSnapshot(snapshot);
|
|
91
|
-
const candidate =
|
|
163
|
+
const candidate = (modelRouteId
|
|
164
|
+
? candidates.find((item) => providerHealthCandidateRouteId(item) === modelRouteId)
|
|
165
|
+
: undefined)
|
|
166
|
+
?? candidates.find((item) => providerHealthCandidateMatchesProvider(item, providerId));
|
|
92
167
|
if (!candidate) {
|
|
93
168
|
return {
|
|
94
169
|
...base,
|
|
@@ -109,10 +184,24 @@ export function readProviderHealthSignal(context: CommandContext, providerId: st
|
|
|
109
184
|
}
|
|
110
185
|
|
|
111
186
|
const stats = providerHealthRecordStats(candidate);
|
|
187
|
+
const rateLimit = providerHealthRecordRateLimit(candidate);
|
|
188
|
+
const errors = providerHealthRecordErrors(candidate);
|
|
112
189
|
const avgLatencyMs = readFiniteNumber(candidate.avgLatencyMs) ?? readFiniteNumber(stats.avgLatencyMs);
|
|
113
190
|
const minLatencyMs = readFiniteNumber(candidate.minLatencyMs) ?? readFiniteNumber(stats.minLatencyMs);
|
|
114
191
|
const maxLatencyMs = readFiniteNumber(candidate.maxLatencyMs) ?? readFiniteNumber(candidate.p95LatencyMs) ?? readFiniteNumber(stats.maxLatencyMs);
|
|
115
192
|
const healthStatus = readString(candidate.status) || 'unknown';
|
|
193
|
+
const rateLimitRemaining = nestedNumber(candidate, stats, rateLimit, ['rateLimitRemaining', 'remaining', 'requestsRemaining']);
|
|
194
|
+
const rateLimitLimit = nestedNumber(candidate, stats, rateLimit, ['rateLimitLimit', 'limit', 'requestsLimit']);
|
|
195
|
+
const rateLimitResetAt = firstTimestamp([candidate.rateLimitResetAt, stats.rateLimitResetAt, rateLimit.resetAt, rateLimit.resetsAt]);
|
|
196
|
+
const lastErrorAt = firstTimestamp([candidate.lastErrorAt, stats.lastErrorAt, errors.lastErrorAt]);
|
|
197
|
+
const lastErrorMessage = readString(candidate.lastErrorMessage)
|
|
198
|
+
|| readString(stats.lastErrorMessage)
|
|
199
|
+
|| readString(errors.lastErrorMessage)
|
|
200
|
+
|| readString(errors.message);
|
|
201
|
+
const errorRate = nestedNumber(candidate, stats, errors, ['errorRate', 'recentErrorRate']);
|
|
202
|
+
const consecutiveErrors = nestedNumber(candidate, stats, errors, ['consecutiveErrors', 'failureStreak']);
|
|
203
|
+
const hasRateLimitPosture = rateLimitRemaining !== undefined || rateLimitLimit !== undefined || rateLimitResetAt !== null;
|
|
204
|
+
const hasErrorPosture = lastErrorAt !== null || lastErrorMessage || errorRate !== undefined || consecutiveErrors !== undefined;
|
|
116
205
|
return {
|
|
117
206
|
...base,
|
|
118
207
|
status: 'record-found',
|
|
@@ -126,6 +215,8 @@ export function readProviderHealthSignal(context: CommandContext, providerId: st
|
|
|
126
215
|
readModelPath: entry.path,
|
|
127
216
|
evidence: 'Agent consumed the published provider-health record for exact model-route readiness.',
|
|
128
217
|
},
|
|
218
|
+
sourceRecordId: readString(candidate.recordId) || readString(candidate.id) || providerHealthCandidateRouteId(candidate) || providerHealthCandidateProviderId(candidate),
|
|
219
|
+
...(providerHealthCandidateRouteId(candidate) ? { modelRouteId: providerHealthCandidateRouteId(candidate) } : {}),
|
|
129
220
|
healthStatus,
|
|
130
221
|
isConfigured: readBoolean(candidate.isConfigured),
|
|
131
222
|
isActive: readBoolean(candidate.isActive),
|
|
@@ -133,14 +224,20 @@ export function readProviderHealthSignal(context: CommandContext, providerId: st
|
|
|
133
224
|
minLatencyMs,
|
|
134
225
|
maxLatencyMs,
|
|
135
226
|
lastSuccessAt: providerHealthTimestamp(candidate, stats, 'lastSuccessAt'),
|
|
136
|
-
lastErrorAt
|
|
137
|
-
lastErrorMessage:
|
|
227
|
+
lastErrorAt,
|
|
228
|
+
lastErrorMessage: lastErrorMessage ? previewHarnessText(redactProviderHealthText(lastErrorMessage), 160) : undefined,
|
|
229
|
+
errorRate,
|
|
230
|
+
consecutiveErrors,
|
|
138
231
|
lastCheckedAt: providerHealthTimestamp(candidate, stats, 'lastCheckedAt'),
|
|
139
|
-
rateLimitResetAt
|
|
232
|
+
rateLimitResetAt,
|
|
233
|
+
rateLimitRemaining,
|
|
234
|
+
rateLimitLimit,
|
|
140
235
|
missingSignals: [
|
|
141
236
|
...(avgLatencyMs === undefined ? ['Provider-health record did not include average latency.'] : []),
|
|
142
237
|
...(minLatencyMs === undefined ? ['Provider-health record did not include minimum latency.'] : []),
|
|
143
238
|
...(maxLatencyMs === undefined ? ['Provider-health record did not include maximum or p95 latency.'] : []),
|
|
239
|
+
...(hasRateLimitPosture ? [] : ['Provider-health record did not include rate-limit posture.']),
|
|
240
|
+
...(hasErrorPosture ? [] : ['Provider-health record did not include error posture.']),
|
|
144
241
|
],
|
|
145
242
|
policy,
|
|
146
243
|
};
|
|
@@ -3,7 +3,7 @@ import type { CommandContext } from '../input/command-registry.ts';
|
|
|
3
3
|
import { previewHarnessText } from './agent-harness-text.ts';
|
|
4
4
|
import { readProviderHealthSignal } from './agent-harness-model-provider-health.ts';
|
|
5
5
|
import { localStackFor } from './agent-harness-local-model-endpoints.ts';
|
|
6
|
-
import type { LocalModelBenchmarkEvidence, LocalModelDetection, LocalModelHardwareProfile, LocalModelRecipe, LocalModelRecipeFit, ModelCandidate, ModelReadinessDimension, ModelReadinessScore, ModelRouteReadinessScore, ModelProviderHealthSignal } from './agent-harness-model-routing-types.ts';
|
|
6
|
+
import type { LocalModelBenchmarkEvidence, LocalModelBenchmarkRouteLatency, LocalModelDetection, LocalModelHardwareProfile, LocalModelRecipe, LocalModelRecipeFit, ModelCandidate, ModelReadinessDimension, ModelReadinessScore, ModelRouteReadinessScore, ModelProviderHealthSignal } from './agent-harness-model-routing-types.ts';
|
|
7
7
|
import { readRecord } from './agent-harness-model-routing-utils.ts';
|
|
8
8
|
|
|
9
9
|
export function localRecipeStackId(recipe: LocalModelRecipe): string {
|
|
@@ -169,6 +169,7 @@ export function latencyScore(
|
|
|
169
169
|
local: boolean,
|
|
170
170
|
benchmarkCompositeScore: number | null | undefined,
|
|
171
171
|
providerHealth: ModelProviderHealthSignal,
|
|
172
|
+
localBenchmarkLatency: LocalModelBenchmarkRouteLatency | null | undefined,
|
|
172
173
|
): ModelReadinessDimension {
|
|
173
174
|
const liveLatency = providerHealth.status === 'record-found' ? providerHealth.avgLatencyMs : undefined;
|
|
174
175
|
if (liveLatency !== undefined) {
|
|
@@ -196,6 +197,24 @@ export function latencyScore(
|
|
|
196
197
|
};
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
if (localBenchmarkLatency && localBenchmarkLatency.status !== 'failed') {
|
|
201
|
+
const latency = localBenchmarkLatency.latencyMs;
|
|
202
|
+
const score = latency <= 750
|
|
203
|
+
? 93
|
|
204
|
+
: latency <= 1500
|
|
205
|
+
? 84
|
|
206
|
+
: latency <= 3000
|
|
207
|
+
? 70
|
|
208
|
+
: 54;
|
|
209
|
+
return {
|
|
210
|
+
id: 'latency',
|
|
211
|
+
label: 'Latency',
|
|
212
|
+
score,
|
|
213
|
+
weight: 20,
|
|
214
|
+
summary: `Measured local benchmark latency is ${latency} ms from ${localBenchmarkLatency.artifactId}${localBenchmarkLatency.comparisonId ? ` (${localBenchmarkLatency.comparisonId})` : ''}.`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
199
218
|
const score = local
|
|
200
219
|
? 55
|
|
201
220
|
: benchmarkCompositeScore != null
|
|
@@ -222,9 +241,9 @@ export function weightedReadiness(dimensions: readonly ModelReadinessDimension[]
|
|
|
222
241
|
|
|
223
242
|
export function modelReadinessScore(context: CommandContext, model: ModelCandidate): ModelRouteReadinessScore {
|
|
224
243
|
const local = isLocalCandidate([model.providerId, model.registryKey, model.modelId, model.displayName]);
|
|
225
|
-
const providerHealth = readProviderHealthSignal(context, model.providerId);
|
|
244
|
+
const providerHealth = readProviderHealthSignal(context, model.providerId, model.registryKey);
|
|
226
245
|
const dimensions: readonly ModelReadinessDimension[] = [
|
|
227
|
-
latencyScore(local, model.benchmarkCompositeScore, providerHealth),
|
|
246
|
+
latencyScore(local, model.benchmarkCompositeScore, providerHealth, model.localBenchmarkLatency),
|
|
228
247
|
contextWindowScore(model.contextWindow),
|
|
229
248
|
toolSupportScore(model.capabilities),
|
|
230
249
|
visionScore(model.capabilities),
|
|
@@ -233,9 +252,10 @@ export function modelReadinessScore(context: CommandContext, model: ModelCandida
|
|
|
233
252
|
];
|
|
234
253
|
const score = weightedReadiness(dimensions);
|
|
235
254
|
const hasLiveLatency = providerHealth.status === 'record-found' && providerHealth.avgLatencyMs !== undefined;
|
|
255
|
+
const hasBenchmarkLatency = Boolean(model.localBenchmarkLatency && model.localBenchmarkLatency.status !== 'failed');
|
|
236
256
|
const missingSignals = [
|
|
237
257
|
...providerHealth.missingSignals,
|
|
238
|
-
...(hasLiveLatency ? [] : ['No live latency benchmark has been recorded for this Agent route.']),
|
|
258
|
+
...(hasLiveLatency || hasBenchmarkLatency ? [] : ['No live latency benchmark has been recorded for this Agent route.']),
|
|
239
259
|
...(model.contextWindow == null ? ['Context-window metadata is missing.'] : []),
|
|
240
260
|
...(capabilityEnabled(model.capabilities, 'toolCalling') == null ? ['Tool-calling support is unknown.'] : []),
|
|
241
261
|
...(capabilityEnabled(model.capabilities, 'multimodal') == null ? ['Vision support is unknown.'] : []),
|
|
@@ -246,6 +266,8 @@ export function modelReadinessScore(context: CommandContext, model: ModelCandida
|
|
|
246
266
|
level: modelReadinessLevel(score),
|
|
247
267
|
confidence: providerHealth.status === 'record-found' && missingSignals.length === 0
|
|
248
268
|
? 'provider-health-backed'
|
|
269
|
+
: hasBenchmarkLatency && missingSignals.length === 0
|
|
270
|
+
? 'measured'
|
|
249
271
|
: missingSignals.length === 0
|
|
250
272
|
? 'metadata-backed'
|
|
251
273
|
: 'estimated',
|
|
@@ -253,9 +275,13 @@ export function modelReadinessScore(context: CommandContext, model: ModelCandida
|
|
|
253
275
|
missingSignals,
|
|
254
276
|
providerHealth,
|
|
255
277
|
nextStep: local
|
|
256
|
-
?
|
|
278
|
+
? hasBenchmarkLatency
|
|
279
|
+
? `Review local benchmark latency evidence ${model.localBenchmarkLatency?.artifactId}, then use a separate confirmed apply/update route only if the user wants this route as default.`
|
|
280
|
+
: 'Run the local benchmark prompt before making this route the default.'
|
|
257
281
|
: providerHealth.status === 'record-found'
|
|
258
282
|
? 'Use provider-health-backed route posture for triage; run a task-specific comparison before changing the default model.'
|
|
283
|
+
: hasBenchmarkLatency
|
|
284
|
+
? `Review benchmark latency evidence ${model.localBenchmarkLatency?.artifactId}; wait for daemon provider-health publication for live status, rate-limit, and error posture before changing defaults.`
|
|
259
285
|
: 'Use this score for routing triage; wait for daemon provider-health publication or run a task-specific comparison before changing the default model.',
|
|
260
286
|
};
|
|
261
287
|
}
|
|
@@ -31,6 +31,7 @@ export interface ModelCandidate {
|
|
|
31
31
|
readonly tier?: string;
|
|
32
32
|
readonly benchmarkCompositeScore?: number | null;
|
|
33
33
|
readonly benchmarkQualityTier?: string;
|
|
34
|
+
readonly localBenchmarkLatency?: LocalModelBenchmarkRouteLatency | null;
|
|
34
35
|
readonly pinned: boolean;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -92,6 +93,8 @@ export interface ModelReadinessDimension {
|
|
|
92
93
|
export interface ModelProviderHealthSignal {
|
|
93
94
|
readonly status: 'not-reachable-in-command-context' | 'read-model-empty' | 'read-model-error' | 'record-found';
|
|
94
95
|
readonly providerId: string;
|
|
96
|
+
readonly modelRouteId?: string;
|
|
97
|
+
readonly sourceRecordId?: string;
|
|
95
98
|
readonly sdkContract: {
|
|
96
99
|
readonly providerHealthTypes: 'available';
|
|
97
100
|
readonly importSurface: string;
|
|
@@ -116,8 +119,12 @@ export interface ModelProviderHealthSignal {
|
|
|
116
119
|
readonly lastSuccessAt?: string | null;
|
|
117
120
|
readonly lastErrorAt?: string | null;
|
|
118
121
|
readonly lastErrorMessage?: string;
|
|
122
|
+
readonly errorRate?: number;
|
|
123
|
+
readonly consecutiveErrors?: number;
|
|
119
124
|
readonly lastCheckedAt?: string | null;
|
|
120
125
|
readonly rateLimitResetAt?: string | null;
|
|
126
|
+
readonly rateLimitRemaining?: number;
|
|
127
|
+
readonly rateLimitLimit?: number;
|
|
121
128
|
readonly missingSignals: readonly string[];
|
|
122
129
|
readonly policy: string;
|
|
123
130
|
}
|
|
@@ -157,6 +164,20 @@ export interface LocalModelBenchmarkWinner {
|
|
|
157
164
|
readonly applyRoute: string;
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
export interface LocalModelBenchmarkRouteLatency {
|
|
168
|
+
readonly registryKey: string;
|
|
169
|
+
readonly providerId: string;
|
|
170
|
+
readonly modelId: string;
|
|
171
|
+
readonly displayName: string;
|
|
172
|
+
readonly blindId: string;
|
|
173
|
+
readonly latencyMs: number;
|
|
174
|
+
readonly status: string;
|
|
175
|
+
readonly artifactId: string;
|
|
176
|
+
readonly comparisonId: string | null;
|
|
177
|
+
readonly createdAt: string | null;
|
|
178
|
+
readonly reviewRoute: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
160
181
|
export interface LocalModelBenchmarkEvidence {
|
|
161
182
|
readonly status: 'unavailable' | 'unmeasured' | 'comparison-saved' | 'reviewed-winner';
|
|
162
183
|
readonly comparisonCount: number;
|
|
@@ -165,6 +186,7 @@ export interface LocalModelBenchmarkEvidence {
|
|
|
165
186
|
readonly hiddenJudgmentCount: number;
|
|
166
187
|
readonly winnerStacks: readonly string[];
|
|
167
188
|
readonly winnerModels: readonly LocalModelBenchmarkWinner[];
|
|
189
|
+
readonly routeLatencies: readonly LocalModelBenchmarkRouteLatency[];
|
|
168
190
|
readonly summary: string;
|
|
169
191
|
readonly confidence: 'estimated' | 'measured';
|
|
170
192
|
}
|
|
@@ -188,6 +210,7 @@ export interface LocalModelServerEndpoint {
|
|
|
188
210
|
readonly refreshRoute: string;
|
|
189
211
|
readonly addProviderRoute: string | null;
|
|
190
212
|
readonly notes: readonly string[];
|
|
213
|
+
readonly servingDiagnostics?: LocalModelServerServingDiagnostics;
|
|
191
214
|
readonly diagnostics?: {
|
|
192
215
|
readonly liveProbe: 'not-run';
|
|
193
216
|
readonly successCriteria: readonly string[];
|
|
@@ -197,6 +220,43 @@ export interface LocalModelServerEndpoint {
|
|
|
197
220
|
};
|
|
198
221
|
}
|
|
199
222
|
|
|
223
|
+
export interface LocalModelServerServingDiagnostics {
|
|
224
|
+
readonly status: 'ready' | 'attention' | 'blocked' | 'unknown';
|
|
225
|
+
readonly source: string;
|
|
226
|
+
readonly summary: string;
|
|
227
|
+
readonly schemaStatus?: 'certified' | 'legacy';
|
|
228
|
+
readonly schemaVersion?: string;
|
|
229
|
+
readonly provenance?: readonly string[];
|
|
230
|
+
readonly publicationGuarantee?: string;
|
|
231
|
+
readonly publisher?: string;
|
|
232
|
+
readonly serverVersion?: string;
|
|
233
|
+
readonly loadedModelCount?: number;
|
|
234
|
+
readonly loadedModels: readonly string[];
|
|
235
|
+
readonly contextWindowTokens?: number;
|
|
236
|
+
readonly toolSupport?: boolean;
|
|
237
|
+
readonly resourcePressure: 'low' | 'moderate' | 'high' | 'unknown';
|
|
238
|
+
readonly resourceSummary?: string;
|
|
239
|
+
readonly lastCheckedAt?: string | null;
|
|
240
|
+
readonly startReceiptId?: string;
|
|
241
|
+
readonly repairReceiptId?: string;
|
|
242
|
+
readonly receiptStatus?: string;
|
|
243
|
+
readonly startRoute?: string;
|
|
244
|
+
readonly repairRoute?: string;
|
|
245
|
+
readonly inspectRoute: string;
|
|
246
|
+
readonly missingSignals: readonly string[];
|
|
247
|
+
readonly policy: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface LocalModelServerDiagnosticsPublication {
|
|
251
|
+
readonly status: 'not-published' | 'published-read-model' | 'read-model-empty' | 'read-model-error';
|
|
252
|
+
readonly requiredPaths: readonly string[];
|
|
253
|
+
readonly sourcePaths: readonly string[];
|
|
254
|
+
readonly recordCount: number;
|
|
255
|
+
readonly matchedEndpointCount: number;
|
|
256
|
+
readonly missingSignals: readonly string[];
|
|
257
|
+
readonly policy: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
200
260
|
export interface LocalModelServerDefaultEndpoint {
|
|
201
261
|
readonly id: string;
|
|
202
262
|
readonly label: string;
|
|
@@ -215,6 +275,7 @@ export interface LocalModelServerHealthMap {
|
|
|
215
275
|
readonly returnedEndpoints: number;
|
|
216
276
|
readonly endpoints: readonly LocalModelServerEndpoint[];
|
|
217
277
|
readonly suggestedDefaults: readonly LocalModelServerDefaultEndpoint[];
|
|
278
|
+
readonly daemonDiagnostics: LocalModelServerDiagnosticsPublication;
|
|
218
279
|
readonly nextActions: readonly string[];
|
|
219
280
|
readonly policy: string;
|
|
220
281
|
}
|
|
@@ -4,14 +4,16 @@ import type { CommandContext } from '../input/command-registry.ts';
|
|
|
4
4
|
import { requireProviderApi } from '../input/commands/runtime-services.ts';
|
|
5
5
|
import { previewHarnessText } from './agent-harness-text.ts';
|
|
6
6
|
import { localModelCookbook } from './agent-harness-local-model-cookbook.ts';
|
|
7
|
-
import {
|
|
7
|
+
import { localBenchmarkRouteLatencyMap } from './agent-harness-local-model-benchmarks.ts';
|
|
8
|
+
import { localModelDetection, localModelServerEndpoints, localModelServerHealthMap } from './agent-harness-local-model-endpoints.ts';
|
|
9
|
+
import { runLocalModelServerSmoke } from './agent-harness-local-model-smoke.ts';
|
|
8
10
|
import { localHardwareProfile, modelReadinessScore } from './agent-harness-model-readiness.ts';
|
|
9
11
|
import { contextWindowFor, loadModels, listProviderIds, modelBenchmarkCompositeScore, modelBenchmarkQualityTier, modelCapabilities, modelCurrent, modelDisplayName, modelModelId, modelProviderId, modelReasoning, modelRegistryKey, modelTier, readConfig, readProviderApi } from './agent-harness-model-catalog.ts';
|
|
10
12
|
import type { AgentHarnessModelRoutingArgs, LocalModelServerEndpoint, ModelCandidate, ModelProviderHealthSignal, ModelRouteLookupSource, ModelRouteResolution, RouteCandidate } from './agent-harness-model-routing-types.ts';
|
|
11
13
|
import { readLimit, readString } from './agent-harness-model-routing-utils.ts';
|
|
12
14
|
export type { AgentHarnessModelRoutingArgs } from './agent-harness-model-routing-types.ts';
|
|
13
15
|
export { localModelCookbook } from './agent-harness-local-model-cookbook.ts';
|
|
14
|
-
export { runLocalModelServerSmoke } from './agent-harness-local-model-
|
|
16
|
+
export { runLocalModelServerSmoke } from './agent-harness-local-model-smoke.ts';
|
|
15
17
|
|
|
16
18
|
async function readCurrentModel(context: CommandContext): Promise<unknown> {
|
|
17
19
|
try {
|
|
@@ -21,6 +23,15 @@ async function readCurrentModel(context: CommandContext): Promise<unknown> {
|
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
function attachLocalBenchmarkLatencies(context: CommandContext, models: readonly ModelCandidate[]): readonly ModelCandidate[] {
|
|
27
|
+
const latencies = localBenchmarkRouteLatencyMap(context);
|
|
28
|
+
if (latencies.size === 0) return models;
|
|
29
|
+
return models.map((model) => ({
|
|
30
|
+
...model,
|
|
31
|
+
localBenchmarkLatency: latencies.get(model.registryKey) ?? null,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
function routeCandidates(context: CommandContext): readonly RouteCandidate[] {
|
|
25
36
|
return [
|
|
26
37
|
{
|
|
@@ -238,7 +249,12 @@ function compactProviderHealthSignal(signal: ModelProviderHealthSignal): Record<
|
|
|
238
249
|
daemonPublication: signal.daemonPublication,
|
|
239
250
|
agentConsumption: signal.agentConsumption,
|
|
240
251
|
...(signal.healthStatus ? { healthStatus: signal.healthStatus } : {}),
|
|
252
|
+
...(signal.modelRouteId ? { modelRouteId: signal.modelRouteId } : {}),
|
|
253
|
+
...(signal.sourceRecordId ? { sourceRecordId: signal.sourceRecordId } : {}),
|
|
241
254
|
...(signal.avgLatencyMs !== undefined ? { avgLatencyMs: signal.avgLatencyMs } : {}),
|
|
255
|
+
...(signal.rateLimitRemaining !== undefined ? { rateLimitRemaining: signal.rateLimitRemaining } : {}),
|
|
256
|
+
...(signal.rateLimitResetAt ? { rateLimitResetAt: signal.rateLimitResetAt } : {}),
|
|
257
|
+
...(signal.lastErrorMessage ? { lastErrorMessage: signal.lastErrorMessage } : {}),
|
|
242
258
|
missingSignals: signal.missingSignals,
|
|
243
259
|
policy: signal.policy,
|
|
244
260
|
};
|
|
@@ -260,6 +276,7 @@ function describeModel(model: ModelCandidate, options: { readonly context: Comma
|
|
|
260
276
|
tier: model.tier ?? null,
|
|
261
277
|
benchmarkCompositeScore: model.benchmarkCompositeScore ?? null,
|
|
262
278
|
benchmarkQualityTier: model.benchmarkQualityTier ?? null,
|
|
279
|
+
localBenchmarkLatency: model.localBenchmarkLatency ?? null,
|
|
263
280
|
readinessScore: readiness.score,
|
|
264
281
|
readinessLevel: readiness.level,
|
|
265
282
|
readiness: options.includeParameters
|
|
@@ -330,10 +347,11 @@ function modelCandidateModelRoute(): string {
|
|
|
330
347
|
}
|
|
331
348
|
|
|
332
349
|
export async function modelRoutingCatalogStatus(context: CommandContext): Promise<Record<string, unknown>> {
|
|
333
|
-
const [
|
|
350
|
+
const [rawModels, providerIds] = await Promise.all([
|
|
334
351
|
loadModels(context),
|
|
335
352
|
Promise.resolve(listProviderIds(context)),
|
|
336
353
|
]);
|
|
354
|
+
const models = attachLocalBenchmarkLatencies(context, rawModels);
|
|
337
355
|
return {
|
|
338
356
|
modes: ['model_routing', 'model_route'],
|
|
339
357
|
status: readProviderApi(context) ? 'available' : 'degraded',
|
|
@@ -349,15 +367,20 @@ export async function modelRoutingCatalogStatus(context: CommandContext): Promis
|
|
|
349
367
|
export async function modelRoutingSummary(context: CommandContext, args: AgentHarnessModelRoutingArgs): Promise<Record<string, unknown>> {
|
|
350
368
|
const query = readString(args.query).toLowerCase();
|
|
351
369
|
const includeParameters = args.includeParameters === true;
|
|
352
|
-
const [
|
|
370
|
+
const [rawModels, providerIds, currentModel] = await Promise.all([
|
|
353
371
|
loadModels(context),
|
|
354
372
|
Promise.resolve(listProviderIds(context)),
|
|
355
373
|
readCurrentModel(context),
|
|
356
374
|
]);
|
|
375
|
+
const models = attachLocalBenchmarkLatencies(context, rawModels);
|
|
357
376
|
const routes = routeCandidates(context);
|
|
358
377
|
const filteredRoutes = routes.filter((route) => !query || routeSearchText(route).includes(query));
|
|
359
378
|
const filteredModels = models.filter((model) => !query || modelSearchText(model).includes(query));
|
|
360
379
|
const limit = readLimit(args.limit, 100);
|
|
380
|
+
const currentRegistryKey = currentModel ? modelRegistryKey(currentModel) : '';
|
|
381
|
+
const loadedCurrentModel = currentRegistryKey
|
|
382
|
+
? models.find((model) => model.registryKey === currentRegistryKey || model.modelId === modelModelId(currentModel))
|
|
383
|
+
: null;
|
|
361
384
|
return {
|
|
362
385
|
status: readProviderApi(context) ? 'available' : 'degraded',
|
|
363
386
|
current: {
|
|
@@ -378,6 +401,7 @@ export async function modelRoutingSummary(context: CommandContext, args: AgentHa
|
|
|
378
401
|
tier: modelTier(currentModel),
|
|
379
402
|
benchmarkCompositeScore: modelBenchmarkCompositeScore(currentModel),
|
|
380
403
|
benchmarkQualityTier: modelBenchmarkQualityTier(currentModel),
|
|
404
|
+
localBenchmarkLatency: loadedCurrentModel?.localBenchmarkLatency ?? null,
|
|
381
405
|
pinned: false,
|
|
382
406
|
}, { context, includeParameters }) : null,
|
|
383
407
|
},
|
|
@@ -410,9 +434,10 @@ export async function describeHarnessModelRoute(context: CommandContext, args: A
|
|
|
410
434
|
usage: 'model_route requires modelRouteId, target, or query. Prefer models action:"status" to inspect route, model, and local endpoint ids.',
|
|
411
435
|
};
|
|
412
436
|
}
|
|
413
|
-
const [
|
|
437
|
+
const [rawModels] = await Promise.all([loadModels(context)]);
|
|
438
|
+
const models = attachLocalBenchmarkLatencies(context, rawModels);
|
|
414
439
|
const routes = routeCandidates(context);
|
|
415
|
-
const endpoints =
|
|
440
|
+
const endpoints = localModelServerEndpoints(context, true);
|
|
416
441
|
const normalized = lookup.input.toLowerCase();
|
|
417
442
|
const exactRoute = routes.find((route) => route.id === lookup.input);
|
|
418
443
|
if (exactRoute) return { status: 'found', route: describeRoute(exactRoute, { context, includeParameters: true, lookup: { ...lookup, resolvedBy: 'route-id' } }) };
|
|
@@ -7,6 +7,7 @@ import { buildAgentWorkspaceChannels } from '../input/agent-workspace-channels.t
|
|
|
7
7
|
import { buildAgentWorkspaceVoiceMediaReadiness } from '../input/agent-workspace-voice-media.ts';
|
|
8
8
|
import { requirePlatform, requireShellPaths } from '../input/commands/runtime-services.ts';
|
|
9
9
|
import { browserControlPosture } from './agent-harness-browser-control.ts';
|
|
10
|
+
import { certifiedDeviceLiveRecords, deviceLiveReadModelSnapshot } from './agent-harness-device-live-read-models.ts';
|
|
10
11
|
import { previewHarnessText } from './agent-harness-text.ts';
|
|
11
12
|
|
|
12
13
|
export interface AgentHarnessPairingArgs {
|
|
@@ -370,6 +371,10 @@ function queryRequestsWholeDeviceMap(query: string): boolean {
|
|
|
370
371
|
return tokens.includes('device') && (tokens.includes('capability') || tokens.includes('capabilities') || tokens.includes('map'));
|
|
371
372
|
}
|
|
372
373
|
|
|
374
|
+
function liveDeviceEvidence(records: ReturnType<typeof certifiedDeviceLiveRecords>): Record<string, unknown> | undefined {
|
|
375
|
+
return records.length > 0 ? { certifiedLiveRecords: records.slice(0, 5) } : undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
373
378
|
function buildDeviceCapabilityMap(context: CommandContext): DeviceCapabilityMap {
|
|
374
379
|
const state = pairingState(context);
|
|
375
380
|
const hasToken = tokenPresent(state);
|
|
@@ -389,6 +394,9 @@ function buildDeviceCapabilityMap(context: CommandContext): DeviceCapabilityMap
|
|
|
389
394
|
const enabledChannels = channels.filter((channel) => channel.enabled);
|
|
390
395
|
const notificationWebhookCount = readConfigStringArray(context, 'notifications.webhookUrls').length;
|
|
391
396
|
const browserPosture = browserControlPosture(context);
|
|
397
|
+
const liveDevice = deviceLiveReadModelSnapshot(context);
|
|
398
|
+
const sensorRecords = certifiedDeviceLiveRecords(liveDevice, 'camera-location-sensors', ['camera', 'location', 'screen', 'device command', 'sensor']);
|
|
399
|
+
const sensorReady = sensorRecords.length > 0;
|
|
392
400
|
const ttsStatus: DeviceCapabilityStatus = voiceReadiness.selectedTtsProviderStatus === 'ready' && voiceReadiness.ttsVoiceConfigured
|
|
393
401
|
? 'ready'
|
|
394
402
|
: voiceReadiness.selectedTtsProviderStatus === 'ready' || voiceReadiness.selectedTtsProviderStatus === 'registered'
|
|
@@ -575,22 +583,35 @@ function buildDeviceCapabilityMap(context: CommandContext): DeviceCapabilityMap
|
|
|
575
583
|
},
|
|
576
584
|
{
|
|
577
585
|
id: 'camera-location-sensors',
|
|
578
|
-
label: 'Camera and location sensors',
|
|
586
|
+
label: 'Camera, screen, and location sensors',
|
|
579
587
|
domain: 'device',
|
|
580
|
-
status: 'not-published',
|
|
581
|
-
userOutcome: 'Use phone camera
|
|
582
|
-
summary:
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
588
|
+
status: sensorReady ? 'ready' : 'not-published',
|
|
589
|
+
userOutcome: 'Use phone camera, screen, location, or local device commands only after the daemon/companion publishes permission-scoped records.',
|
|
590
|
+
summary: sensorReady
|
|
591
|
+
? 'The SDK/daemon published certified permission-scoped camera, screen, location, or device-command capability records.'
|
|
592
|
+
: 'Camera, screen, location, and device-command adapters are not published by the current Agent-visible SDK/daemon contract.',
|
|
593
|
+
nextStep: sensorReady
|
|
594
|
+
? 'Inspect the certified device record and use only its returned confirmed permission/control routes.'
|
|
595
|
+
: 'Inspect operator methods for newly published camera/location/screen contracts before claiming device access.',
|
|
596
|
+
capabilities: [
|
|
597
|
+
'camera permission posture',
|
|
598
|
+
'screen capture permission posture',
|
|
599
|
+
'location permission posture',
|
|
600
|
+
'local device command routes',
|
|
601
|
+
...sensorRecords.flatMap((record) => record.capabilities),
|
|
602
|
+
],
|
|
603
|
+
modelRoute: sensorRecords[0]?.modelRoute ?? 'host action:"methods" query:"camera location device"',
|
|
586
604
|
setupRoutes: [
|
|
605
|
+
...sensorRecords.slice(0, 3).map((record) => record.modelRoute),
|
|
587
606
|
'host action:"methods" query:"camera location device"',
|
|
588
607
|
'host action:"capabilities" includeParameters:true',
|
|
589
608
|
],
|
|
590
609
|
evidence: {
|
|
591
|
-
publishedByCurrentAgentContract:
|
|
610
|
+
publishedByCurrentAgentContract: sensorReady,
|
|
611
|
+
liveDeviceRecords: liveDevice.capabilities.length,
|
|
612
|
+
...liveDeviceEvidence(sensorRecords),
|
|
592
613
|
},
|
|
593
|
-
policy: 'Agent reports unpublished device sensor APIs honestly
|
|
614
|
+
policy: 'Agent reports unpublished device sensor APIs honestly and counts device access only when certified SDK/daemon permission-scoped records publish exact routes without raw payloads.',
|
|
594
615
|
},
|
|
595
616
|
];
|
|
596
617
|
capabilities.sort((left, right) => deviceStatusRank(left.status) - deviceStatusRank(right.status) || left.label.localeCompare(right.label));
|