@pellux/goodvibes-agent 1.1.7 → 1.2.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 +170 -0
- package/README.md +46 -30
- package/dist/package/{ast-grep-napi.linux-x64-gnu-mkk8xwww.node → ast-grep-napi.linux-x64-gnu-swtppvy9.node} +0 -0
- package/dist/package/{ast-grep-napi.linux-x64-musl-ryqtgdv6.node → ast-grep-napi.linux-x64-musl-ttfcdtap.node} +0 -0
- package/dist/package/main.js +132577 -91483
- package/docs/README.md +14 -7
- package/docs/channels-remote-and-api.md +8 -5
- package/docs/connected-host.md +14 -12
- package/docs/getting-started.md +56 -30
- package/docs/knowledge-artifacts-and-multimodal.md +7 -5
- package/docs/project-planning.md +2 -2
- package/docs/providers-and-routing.md +11 -3
- package/docs/tools-and-commands.md +114 -45
- package/docs/voice-and-live-tts.md +3 -1
- package/package.json +3 -2
- package/release/release-notes.md +130 -5
- package/release/release-readiness.json +120 -90
- package/src/agent/assistant-cockpit.ts +247 -0
- package/src/agent/autonomy-schedule-format.ts +96 -0
- package/src/agent/autonomy-schedule.ts +514 -0
- package/src/agent/channel-delivery-receipts.ts +292 -0
- package/src/agent/competitive-feature-inventory.ts +296 -0
- package/src/agent/document-registry-types.ts +106 -0
- package/src/agent/document-registry.ts +734 -0
- package/src/agent/harness-control.ts +2 -2
- package/src/agent/memory-prompt.ts +2 -2
- package/src/agent/operator-actions.ts +50 -6
- package/src/agent/persona-registry.ts +8 -0
- package/src/agent/project-context-files.ts +273 -0
- package/src/agent/prompt-context-receipts.ts +502 -0
- package/src/agent/reminder-schedule-format.ts +16 -2
- package/src/agent/research-run-registry.ts +582 -0
- package/src/agent/research-source-registry.ts +441 -0
- package/src/agent/routine-registry.ts +19 -3
- package/src/agent/routine-schedule-format.ts +27 -2
- package/src/agent/runtime-profile-starters.ts +7 -0
- package/src/agent/runtime-profile.ts +106 -4
- package/src/agent/schedule-edit-format.ts +129 -0
- package/src/agent/schedule-edit.ts +496 -0
- package/src/agent/schedule-next-routes.ts +42 -0
- package/src/agent/setup-wizard-checkpoint.ts +140 -0
- package/src/agent/setup-wizard.ts +447 -0
- package/src/agent/skill-registry-types.ts +102 -0
- package/src/agent/skill-registry.ts +81 -107
- package/src/agent/vibe-confirmation-routes.ts +62 -0
- package/src/agent/vibe-file.ts +285 -0
- package/src/cli/agent-knowledge-command.ts +45 -2
- package/src/cli/help.ts +3 -0
- package/src/cli/profiles-command.ts +25 -11
- package/src/config/agent-settings-policy.ts +1 -18
- package/src/input/agent-workspace-activation.ts +53 -0
- package/src/input/agent-workspace-artifact-browser-editor.ts +494 -0
- package/src/input/agent-workspace-artifact-metadata.ts +17 -0
- package/src/input/agent-workspace-basic-command-editors.ts +5 -1
- package/src/input/agent-workspace-categories.ts +153 -155
- package/src/input/agent-workspace-channel-triage.ts +481 -0
- package/src/input/agent-workspace-channels.ts +263 -0
- package/src/input/agent-workspace-command-editor.ts +152 -0
- package/src/input/agent-workspace-context-snapshot.ts +349 -0
- package/src/input/agent-workspace-delegation-editor-submission.ts +8 -0
- package/src/input/agent-workspace-document-editor.ts +502 -0
- package/src/input/agent-workspace-document-ops-editor.ts +523 -0
- package/src/input/agent-workspace-host-category.ts +1 -1
- package/src/input/agent-workspace-local-library-snapshot.ts +167 -0
- package/src/input/agent-workspace-model-compare-editor.ts +376 -0
- package/src/input/agent-workspace-model-compare-prompt-submission.ts +552 -0
- package/src/input/agent-workspace-model-compare-types.ts +82 -0
- package/src/input/agent-workspace-model-compare-utils.ts +35 -0
- package/src/input/agent-workspace-onboarding-categories.ts +141 -0
- package/src/input/agent-workspace-operations-command-editor-submission.ts +53 -0
- package/src/input/agent-workspace-operations-command-editors.ts +22 -0
- package/src/input/agent-workspace-research-report-editor.ts +212 -0
- package/src/input/agent-workspace-research-run-editor.ts +144 -0
- package/src/input/agent-workspace-research-source-editor.ts +167 -0
- package/src/input/agent-workspace-review-packet-snapshot.ts +593 -0
- package/src/input/agent-workspace-review-packet-utils.ts +265 -0
- package/src/input/agent-workspace-settings.ts +262 -30
- package/src/input/agent-workspace-setup-checkpoint-action.ts +127 -0
- package/src/input/agent-workspace-setup-snapshot.ts +198 -0
- package/src/input/agent-workspace-setup.ts +34 -1
- package/src/input/agent-workspace-snapshot.ts +162 -147
- package/src/input/agent-workspace-types.ts +287 -2
- package/src/input/agent-workspace.ts +18 -0
- package/src/input/command-registry.ts +9 -2
- package/src/input/commands/agent-runtime-profile-runtime.ts +28 -13
- package/src/input/commands/channels-runtime.ts +102 -114
- package/src/input/commands/delegation-runtime.ts +87 -24
- package/src/input/commands/knowledge-browser-flags.ts +12 -0
- package/src/input/commands/knowledge.ts +28 -14
- package/src/input/commands/operator-actions-runtime.ts +6 -3
- package/src/input/commands/schedule-runtime.ts +71 -14
- package/src/input/commands/session-content.ts +2 -2
- package/src/input/commands/shell-core.ts +2 -0
- package/src/input/commands/vibe-runtime.ts +140 -0
- package/src/input/commands.ts +2 -0
- package/src/input/connected-host-routes.ts +123 -0
- package/src/input/settings-modal-types.ts +14 -0
- package/src/main.ts +15 -15
- package/src/panels/qr-panel.ts +2 -2
- package/src/renderer/agent-workspace-context-lines.ts +686 -0
- package/src/renderer/agent-workspace.ts +30 -419
- package/src/renderer/settings-modal-helpers.ts +12 -0
- package/src/renderer/settings-modal.ts +12 -0
- package/src/runtime/bootstrap-command-context.ts +11 -1
- package/src/runtime/bootstrap-command-parts.ts +14 -3
- package/src/runtime/bootstrap-core.ts +39 -1
- package/src/runtime/bootstrap-shell.ts +6 -0
- package/src/runtime/bootstrap.ts +113 -30
- package/src/runtime/connected-host-auth.ts +3 -2
- package/src/runtime/execution-ledger.ts +231 -0
- package/src/runtime/services.ts +88 -10
- package/src/runtime/tool-permission-safety.ts +199 -5
- package/src/shell/session-continuity-hints.ts +39 -0
- package/src/tools/agent-artifacts-tool.ts +606 -0
- package/src/tools/agent-audit-tool.ts +155 -0
- package/src/tools/agent-autonomy-schedule-tool.ts +238 -0
- package/src/tools/agent-autonomy-tool.ts +140 -0
- package/src/tools/agent-channel-send-tool.ts +24 -2
- package/src/tools/agent-channels-tool.ts +140 -0
- package/src/tools/agent-computer-tool.ts +168 -0
- package/src/tools/agent-context-policy.ts +137 -7
- package/src/tools/agent-context-tool.ts +143 -0
- package/src/tools/agent-delegation-tool.ts +128 -0
- package/src/tools/agent-device-tool.ts +240 -0
- package/src/tools/agent-documents-tool.ts +684 -0
- package/src/tools/agent-execution-tool.ts +230 -0
- package/src/tools/agent-harness-agent-orchestration.ts +703 -0
- package/src/tools/agent-harness-autonomy-intake.ts +785 -0
- package/src/tools/agent-harness-autonomy-live-records.ts +588 -0
- package/src/tools/agent-harness-autonomy-queue-types.ts +88 -0
- package/src/tools/agent-harness-autonomy-queue.ts +560 -0
- package/src/tools/agent-harness-background-processes-types.ts +35 -0
- package/src/tools/agent-harness-background-processes.ts +794 -0
- package/src/tools/agent-harness-browser-cockpit-route.ts +219 -0
- package/src/tools/agent-harness-browser-control.ts +338 -0
- package/src/tools/agent-harness-channel-metadata.ts +168 -10
- package/src/tools/agent-harness-cli-command-policy.ts +110 -0
- package/src/tools/agent-harness-command-catalog.ts +4 -2
- package/src/tools/agent-harness-command-runner.ts +42 -0
- package/src/tools/agent-harness-connected-host-status.ts +7 -4
- package/src/tools/agent-harness-delegation-posture.ts +234 -19
- package/src/tools/agent-harness-document-ops-reviewer-readiness.ts +234 -0
- package/src/tools/agent-harness-document-ops-types.ts +72 -0
- package/src/tools/agent-harness-document-ops.ts +671 -0
- package/src/tools/agent-harness-execution-history.ts +489 -0
- package/src/tools/agent-harness-execution-posture.ts +382 -0
- package/src/tools/agent-harness-file-recovery.ts +135 -0
- package/src/tools/agent-harness-keybinding-metadata.ts +16 -12
- package/src/tools/agent-harness-learning-curator-common.ts +102 -0
- package/src/tools/agent-harness-learning-curator-consolidation.ts +295 -0
- package/src/tools/agent-harness-learning-curator-proposals.ts +606 -0
- package/src/tools/agent-harness-learning-curator-types.ts +151 -0
- package/src/tools/agent-harness-learning-curator.ts +417 -0
- package/src/tools/agent-harness-local-model-benchmarks.ts +199 -0
- package/src/tools/agent-harness-local-model-cookbook.ts +238 -0
- package/src/tools/agent-harness-local-model-endpoints.ts +673 -0
- package/src/tools/agent-harness-media-posture.ts +208 -2
- package/src/tools/agent-harness-memory-posture.ts +556 -0
- package/src/tools/agent-harness-metadata.ts +100 -168
- package/src/tools/agent-harness-mode-catalog.ts +84 -33
- package/src/tools/agent-harness-model-catalog.ts +162 -0
- package/src/tools/agent-harness-model-provider-health.ts +186 -0
- package/src/tools/agent-harness-model-readiness.ts +406 -0
- package/src/tools/agent-harness-model-routing-types.ts +266 -0
- package/src/tools/agent-harness-model-routing-utils.ts +30 -0
- package/src/tools/agent-harness-model-routing.ts +137 -190
- package/src/tools/agent-harness-operator-methods.ts +115 -133
- package/src/tools/agent-harness-pairing-posture.ts +431 -18
- package/src/tools/agent-harness-personal-ops-discovery.ts +533 -0
- package/src/tools/agent-harness-personal-ops-intake.ts +385 -0
- package/src/tools/agent-harness-personal-ops-lanes.ts +204 -0
- package/src/tools/agent-harness-personal-ops-records.ts +732 -0
- package/src/tools/agent-harness-personal-ops-runner.ts +637 -0
- package/src/tools/agent-harness-personal-ops-types.ts +222 -0
- package/src/tools/agent-harness-personal-ops.ts +606 -0
- package/src/tools/agent-harness-project-context.ts +144 -0
- package/src/tools/agent-harness-prompt-context.ts +589 -0
- package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
- package/src/tools/agent-harness-release-evidence.ts +9 -7
- package/src/tools/agent-harness-release-readiness.ts +9 -7
- package/src/tools/agent-harness-research-briefing.ts +427 -0
- package/src/tools/agent-harness-research-queue.ts +250 -0
- package/src/tools/agent-harness-research-runs.ts +295 -0
- package/src/tools/agent-harness-research-workflow.ts +322 -0
- package/src/tools/agent-harness-security-posture.ts +2 -2
- package/src/tools/agent-harness-service-posture.ts +9 -9
- package/src/tools/agent-harness-session-metadata.ts +3 -3
- package/src/tools/agent-harness-setup-connected-host.ts +501 -0
- package/src/tools/agent-harness-setup-describe.ts +557 -0
- package/src/tools/agent-harness-setup-handoffs.ts +502 -0
- package/src/tools/agent-harness-setup-model-helpers.ts +101 -0
- package/src/tools/agent-harness-setup-plan.ts +253 -0
- package/src/tools/agent-harness-setup-posture-types.ts +218 -0
- package/src/tools/agent-harness-setup-posture-utils.ts +173 -0
- package/src/tools/agent-harness-setup-posture.ts +449 -221
- package/src/tools/agent-harness-setup-smoke.ts +508 -0
- package/src/tools/agent-harness-sudo-posture.ts +114 -0
- package/src/tools/agent-harness-tool-schema.ts +120 -7
- package/src/tools/agent-harness-tool-types.ts +78 -0
- package/src/tools/agent-harness-tool-utils.ts +43 -0
- package/src/tools/agent-harness-tool.ts +311 -370
- package/src/tools/agent-harness-ui-surface-metadata.ts +76 -69
- package/src/tools/agent-harness-ui-surface-types.ts +46 -0
- package/src/tools/agent-harness-vibe-health.ts +105 -0
- package/src/tools/agent-harness-workspace-action-runner.ts +149 -0
- package/src/tools/agent-harness-workspace-actions.ts +114 -8
- package/src/tools/agent-harness-workspace-editor-execution.ts +214 -0
- package/src/tools/agent-harness-workspace-editor-runner.ts +755 -0
- package/src/tools/agent-host-tool.ts +159 -0
- package/src/tools/agent-knowledge-ingest-tool.ts +34 -1
- package/src/tools/agent-learning-consolidation-core.ts +327 -0
- package/src/tools/agent-learning-consolidation-tool.ts +758 -0
- package/src/tools/agent-memory-tool.ts +214 -0
- package/src/tools/agent-model-compare-export.ts +315 -0
- package/src/tools/agent-model-compare-handoff.ts +592 -0
- package/src/tools/agent-model-compare-judgment.ts +633 -0
- package/src/tools/agent-model-compare-run.ts +722 -0
- package/src/tools/agent-model-compare-tool.ts +698 -0
- package/src/tools/agent-model-compare-types.ts +191 -0
- package/src/tools/agent-model-compare-utils.ts +93 -0
- package/src/tools/agent-models-tool.ts +208 -0
- package/src/tools/agent-operator-action-tool.ts +1 -1
- package/src/tools/agent-operator-method-tool.ts +643 -0
- package/src/tools/agent-personal-ops-tool.ts +197 -0
- package/src/tools/agent-policy-explanation.ts +415 -0
- package/src/tools/agent-research-report-tool.ts +608 -0
- package/src/tools/agent-research-runs-tool.ts +434 -0
- package/src/tools/agent-research-sources-tool.ts +443 -0
- package/src/tools/agent-research-tool.ts +687 -0
- package/src/tools/agent-review-packet-presets-core.ts +757 -0
- package/src/tools/agent-review-packet-presets-tool.ts +466 -0
- package/src/tools/agent-review-packet-share-tool.ts +305 -0
- package/src/tools/agent-route-planner-candidates-setup.ts +411 -0
- package/src/tools/agent-route-planner-candidates-surfaces.ts +432 -0
- package/src/tools/agent-route-planner-candidates-work.ts +251 -0
- package/src/tools/agent-route-planner-helpers.ts +667 -0
- package/src/tools/agent-route-planner.ts +185 -0
- package/src/tools/agent-route-tool.ts +105 -0
- package/src/tools/agent-schedule-edit-tool.ts +210 -0
- package/src/tools/agent-schedule-tool.ts +282 -0
- package/src/tools/agent-security-tool.ts +145 -0
- package/src/tools/agent-sessions-tool.ts +122 -0
- package/src/tools/agent-settings-import-tool.ts +104 -0
- package/src/tools/agent-settings-tool.ts +176 -0
- package/src/tools/agent-setup-tool.ts +226 -0
- package/src/tools/agent-support-tool.ts +122 -0
- package/src/tools/agent-terminal-process-tools.ts +167 -0
- package/src/tools/agent-tool-policy-guard-types.ts +77 -0
- package/src/tools/agent-tool-policy-guard.ts +106 -85
- package/src/tools/agent-vibe-tool.ts +297 -0
- package/src/tools/agent-work-plan-tool.ts +265 -6
- package/src/tools/agent-workspace-tool.ts +305 -0
- package/src/tools/artifact-archive.ts +169 -0
- package/src/tools/tool-definition-compaction.ts +36 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { AgentModelCompareArtifactStore, AgentModelCompareModelCatalog, AgentModelCompareRouteUpdateResult, AgentModelCompareToolArgs, CompareCandidateResult, LoadedComparisonJudgment, SavedComparisonArtifact, StoredComparison } from './agent-model-compare-types.ts';
|
|
3
|
+
import { SYNTHESIS_THEMES } from './agent-model-compare-types.ts';
|
|
4
|
+
import { isModelCompareJudgmentArtifact, listSelectableModels, toSavedComparisonArtifact } from './agent-model-compare-run.ts';
|
|
5
|
+
import { readBoolean, readComparisonTag, readRecord, readString, previewText } from './agent-model-compare-utils.ts';
|
|
6
|
+
|
|
7
|
+
export function normalizeBlindId(value: string): string {
|
|
8
|
+
return value
|
|
9
|
+
.replace(/^candidate\s+/i, '')
|
|
10
|
+
.trim()
|
|
11
|
+
.toUpperCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function findCandidate(comparison: StoredComparison, blindId: string): CompareCandidateResult | null {
|
|
15
|
+
const normalized = normalizeBlindId(blindId);
|
|
16
|
+
return comparison.candidates.find((candidate) => candidate.blindId.toUpperCase() === normalized) ?? null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function modelRouteHandoff(candidate: CompareCandidateResult, reveal: boolean): Record<string, unknown> {
|
|
20
|
+
return {
|
|
21
|
+
...(reveal ? {
|
|
22
|
+
routeInspection: `agent_harness mode:"model_route" target:"${candidate.model.registryKey}"`,
|
|
23
|
+
confirmedMainRouteUpdate: `settings action:"set" key:"provider.model" value:"${candidate.model.registryKey}" confirm:true explicitUserRequest:"..."`,
|
|
24
|
+
} : {
|
|
25
|
+
routeInspection: 'reveal the winning model before model-route inspection',
|
|
26
|
+
}),
|
|
27
|
+
policy: 'This judgment does not change the selected model. Route updates require a separate confirmed action.',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function judgmentArtifactText(input: {
|
|
32
|
+
readonly judgmentId: string;
|
|
33
|
+
readonly comparison: StoredComparison;
|
|
34
|
+
readonly winner: CompareCandidateResult;
|
|
35
|
+
readonly reasons: string;
|
|
36
|
+
readonly notes: string;
|
|
37
|
+
readonly reveal: boolean;
|
|
38
|
+
}): string {
|
|
39
|
+
return `${JSON.stringify({
|
|
40
|
+
schema: 'goodvibes.agent.model_compare_judgment.v1',
|
|
41
|
+
judgmentId: input.judgmentId,
|
|
42
|
+
comparisonId: input.comparison.comparisonId,
|
|
43
|
+
sourceArtifactId: input.comparison.artifact?.artifactId ?? null,
|
|
44
|
+
sourceDocumentId: input.comparison.documentId ?? null,
|
|
45
|
+
documentId: input.comparison.documentId ?? null,
|
|
46
|
+
benchmarkKind: input.comparison.benchmarkKind ?? null,
|
|
47
|
+
taskType: input.comparison.taskType ?? null,
|
|
48
|
+
createdAt: new Date().toISOString(),
|
|
49
|
+
promptPreview: input.comparison.promptPreview,
|
|
50
|
+
rubric: input.comparison.rubric,
|
|
51
|
+
winnerBlindId: input.winner.blindId,
|
|
52
|
+
...(input.reveal ? {
|
|
53
|
+
winnerModel: {
|
|
54
|
+
registryKey: input.winner.model.registryKey,
|
|
55
|
+
providerId: input.winner.model.providerId,
|
|
56
|
+
modelId: input.winner.model.modelId,
|
|
57
|
+
displayName: input.winner.model.displayName,
|
|
58
|
+
},
|
|
59
|
+
} : {}),
|
|
60
|
+
reasons: input.reasons,
|
|
61
|
+
notes: input.notes,
|
|
62
|
+
revealIncludedInJudgment: input.reveal,
|
|
63
|
+
routeHandoff: modelRouteHandoff(input.winner, input.reveal),
|
|
64
|
+
candidates: input.comparison.candidates.map((candidate) => ({
|
|
65
|
+
blindId: candidate.blindId,
|
|
66
|
+
status: candidate.status,
|
|
67
|
+
...(input.reveal ? {
|
|
68
|
+
model: {
|
|
69
|
+
registryKey: candidate.model.registryKey,
|
|
70
|
+
providerId: candidate.model.providerId,
|
|
71
|
+
modelId: candidate.model.modelId,
|
|
72
|
+
displayName: candidate.model.displayName,
|
|
73
|
+
},
|
|
74
|
+
} : {}),
|
|
75
|
+
})),
|
|
76
|
+
}, null, 2)}\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function saveComparisonJudgmentArtifact(input: {
|
|
80
|
+
readonly artifactStore?: AgentModelCompareArtifactStore;
|
|
81
|
+
readonly comparison: StoredComparison;
|
|
82
|
+
readonly winner: CompareCandidateResult;
|
|
83
|
+
readonly reasons: string;
|
|
84
|
+
readonly notes: string;
|
|
85
|
+
readonly reveal: boolean;
|
|
86
|
+
}): Promise<SavedComparisonArtifact> {
|
|
87
|
+
if (!input.artifactStore) throw new Error('Cannot save judgment because the artifact store is unavailable.');
|
|
88
|
+
const judgmentId = `jdg_${randomUUID()}`;
|
|
89
|
+
const descriptor = await input.artifactStore.create({
|
|
90
|
+
kind: 'data',
|
|
91
|
+
mimeType: 'application/json',
|
|
92
|
+
filename: `blind-model-comparison-judgment-${judgmentId}.json`,
|
|
93
|
+
text: judgmentArtifactText({
|
|
94
|
+
judgmentId,
|
|
95
|
+
comparison: input.comparison,
|
|
96
|
+
winner: input.winner,
|
|
97
|
+
reasons: input.reasons,
|
|
98
|
+
notes: input.notes,
|
|
99
|
+
reveal: input.reveal,
|
|
100
|
+
}),
|
|
101
|
+
metadata: {
|
|
102
|
+
purpose: 'agent-model-compare-judgment',
|
|
103
|
+
judgmentId,
|
|
104
|
+
comparisonId: input.comparison.comparisonId,
|
|
105
|
+
sourceArtifactId: input.comparison.artifact?.artifactId ?? null,
|
|
106
|
+
sourceDocumentId: input.comparison.documentId ?? null,
|
|
107
|
+
documentId: input.comparison.documentId ?? null,
|
|
108
|
+
benchmarkKind: input.comparison.benchmarkKind ?? null,
|
|
109
|
+
taskType: input.comparison.taskType ?? null,
|
|
110
|
+
winnerBlindId: input.winner.blindId,
|
|
111
|
+
promptPreview: input.comparison.promptPreview,
|
|
112
|
+
revealIncludedInJudgment: input.reveal,
|
|
113
|
+
...(input.reveal ? { winnerModel: input.winner.model.registryKey } : {}),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
return toSavedComparisonArtifact(descriptor);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function formatJudgePreview(args: AgentModelCompareToolArgs): string {
|
|
120
|
+
return [
|
|
121
|
+
'Agent blind model comparison judgment preview',
|
|
122
|
+
` comparison ${readString(args.comparisonId) || readString(args.artifactId) || '(missing)'}`,
|
|
123
|
+
` winner ${readString(args.winnerBlindId ?? args.winner) || '(missing)'}`,
|
|
124
|
+
` reasons ${previewText(readString(args.reasons) || '(missing)')}`,
|
|
125
|
+
` reveal ${readBoolean(args.reveal) ? 'include model identity in judgment' : 'keep judgment blind'}`,
|
|
126
|
+
' policy saves a local judgment artifact and never changes the selected model',
|
|
127
|
+
].join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function formatJudgmentResult(input: {
|
|
131
|
+
readonly comparison: StoredComparison;
|
|
132
|
+
readonly winner: CompareCandidateResult;
|
|
133
|
+
readonly artifact: SavedComparisonArtifact;
|
|
134
|
+
readonly reasons: string;
|
|
135
|
+
readonly reveal: boolean;
|
|
136
|
+
}): string {
|
|
137
|
+
const routeHandoff = modelRouteHandoff(input.winner, input.reveal);
|
|
138
|
+
const lines = [
|
|
139
|
+
`Blind model comparison judgment saved for ${input.comparison.comparisonId}`,
|
|
140
|
+
`winner Candidate ${input.winner.blindId}`,
|
|
141
|
+
...(input.reveal ? [`winner model ${input.winner.model.registryKey} (${input.winner.model.displayName})`] : []),
|
|
142
|
+
`artifact ${input.artifact.artifactId}${input.artifact.filename ? ` ${input.artifact.filename}` : ''} (${input.artifact.mimeType}, ${input.artifact.sizeBytes} bytes)`,
|
|
143
|
+
`reasons ${input.reasons || '(none)'}`,
|
|
144
|
+
'',
|
|
145
|
+
'Route handoff',
|
|
146
|
+
` inspect ${routeHandoff.routeInspection}`,
|
|
147
|
+
];
|
|
148
|
+
if (input.reveal) {
|
|
149
|
+
lines.push(` update ${routeHandoff.confirmedMainRouteUpdate}`);
|
|
150
|
+
} else {
|
|
151
|
+
lines.push(' update reveal the winner first, then use a separate confirmed model-routing action');
|
|
152
|
+
}
|
|
153
|
+
lines.push('No selected model was changed.');
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function parseJudgmentArtifactPayload(value: unknown, artifact: SavedComparisonArtifact, metadata: Record<string, unknown> = {}): LoadedComparisonJudgment | null {
|
|
158
|
+
const payload = readRecord(value);
|
|
159
|
+
if (!payload || readString(payload.schema) !== 'goodvibes.agent.model_compare_judgment.v1') return null;
|
|
160
|
+
const winnerModel = readRecord(payload.winnerModel);
|
|
161
|
+
const registryKey = readString(winnerModel?.registryKey);
|
|
162
|
+
const sourceArtifactId = readString(payload.sourceArtifactId) || readString(metadata.sourceArtifactId);
|
|
163
|
+
const benchmarkKind = readComparisonTag(payload.benchmarkKind) || readComparisonTag(metadata.benchmarkKind);
|
|
164
|
+
const taskType = readComparisonTag(payload.taskType) || readComparisonTag(metadata.taskType);
|
|
165
|
+
const documentId = readString(payload.documentId)
|
|
166
|
+
|| readString(payload.sourceDocumentId)
|
|
167
|
+
|| readString(metadata.documentId)
|
|
168
|
+
|| readString(metadata.sourceDocumentId);
|
|
169
|
+
return {
|
|
170
|
+
artifact,
|
|
171
|
+
judgmentId: readString(payload.judgmentId) || `judgment_from_${artifact.artifactId}`,
|
|
172
|
+
comparisonId: readString(payload.comparisonId) || 'unknown-comparison',
|
|
173
|
+
winnerBlindId: readString(payload.winnerBlindId) || '?',
|
|
174
|
+
reasons: readString(payload.reasons),
|
|
175
|
+
notes: readString(payload.notes),
|
|
176
|
+
revealIncludedInJudgment: payload.revealIncludedInJudgment === true,
|
|
177
|
+
...(sourceArtifactId ? { sourceArtifactId } : {}),
|
|
178
|
+
...(benchmarkKind ? { benchmarkKind } : {}),
|
|
179
|
+
...(taskType ? { taskType } : {}),
|
|
180
|
+
...(documentId ? { documentId } : {}),
|
|
181
|
+
...(registryKey ? {
|
|
182
|
+
winnerModel: {
|
|
183
|
+
registryKey,
|
|
184
|
+
providerId: readString(winnerModel?.providerId),
|
|
185
|
+
modelId: readString(winnerModel?.modelId),
|
|
186
|
+
displayName: readString(winnerModel?.displayName) || registryKey,
|
|
187
|
+
},
|
|
188
|
+
} : {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function loadJudgmentFromArtifact(
|
|
193
|
+
artifactStore: AgentModelCompareArtifactStore | undefined,
|
|
194
|
+
artifactId: string,
|
|
195
|
+
): Promise<LoadedComparisonJudgment | null> {
|
|
196
|
+
if (!artifactStore?.readContent) return null;
|
|
197
|
+
const { record, buffer } = await artifactStore.readContent(artifactId);
|
|
198
|
+
let payload: unknown;
|
|
199
|
+
try {
|
|
200
|
+
payload = JSON.parse(buffer.toString('utf-8')) as unknown;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return parseJudgmentArtifactPayload(payload, toSavedComparisonArtifact(record), record.metadata);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function loadSavedJudgments(
|
|
208
|
+
artifactStore: AgentModelCompareArtifactStore | undefined,
|
|
209
|
+
limit: number,
|
|
210
|
+
filters: ComparisonAnalyticsFilters = {},
|
|
211
|
+
): Promise<readonly LoadedComparisonJudgment[]> {
|
|
212
|
+
if (!artifactStore?.list || !artifactStore.readContent) return [];
|
|
213
|
+
const listLimit = hasComparisonFilters(filters) ? Math.max(limit * 10, 100) : Math.max(limit * 3, limit);
|
|
214
|
+
const artifacts = artifactStore.list(listLimit)
|
|
215
|
+
.filter(isModelCompareJudgmentArtifact)
|
|
216
|
+
.slice(0, listLimit);
|
|
217
|
+
const loaded = await Promise.all(artifacts.map(async (artifact) => {
|
|
218
|
+
try {
|
|
219
|
+
return await loadJudgmentFromArtifact(artifactStore, artifact.id);
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}));
|
|
224
|
+
return loaded
|
|
225
|
+
.filter((judgment): judgment is LoadedComparisonJudgment => judgment !== null)
|
|
226
|
+
.filter((judgment) => judgmentMatchesFilters(judgment, filters))
|
|
227
|
+
.slice(0, limit);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function ensureSelectableWinnerModel(
|
|
231
|
+
catalog: AgentModelCompareModelCatalog,
|
|
232
|
+
registryKey: string,
|
|
233
|
+
): Promise<void> {
|
|
234
|
+
const selectableModels = await listSelectableModels(catalog);
|
|
235
|
+
if (!selectableModels.some((model) => model.registryKey === registryKey)) {
|
|
236
|
+
throw new Error(`Winner model ${registryKey} is not currently selectable. Refresh model routing before applying this judgment.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function formatApplyPreview(judgment: LoadedComparisonJudgment): string {
|
|
241
|
+
return [
|
|
242
|
+
'Agent blind model comparison route update preview',
|
|
243
|
+
` judgment ${judgment.judgmentId}`,
|
|
244
|
+
` comparison ${judgment.comparisonId}`,
|
|
245
|
+
` winner Candidate ${judgment.winnerBlindId}`,
|
|
246
|
+
` model ${judgment.winnerModel?.registryKey ?? '(not revealed in judgment)'}`,
|
|
247
|
+
` reasons ${previewText(judgment.reasons || '(none)')}`,
|
|
248
|
+
' policy changes provider.model only after explicit confirmation',
|
|
249
|
+
].join('\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function formatApplyResult(input: {
|
|
253
|
+
readonly judgment: LoadedComparisonJudgment;
|
|
254
|
+
readonly result: AgentModelCompareRouteUpdateResult;
|
|
255
|
+
readonly receipt?: SavedComparisonArtifact | null;
|
|
256
|
+
readonly receiptError?: string | null;
|
|
257
|
+
}): string {
|
|
258
|
+
const lines = [
|
|
259
|
+
`Applied blind model comparison winner ${input.judgment.judgmentId}`,
|
|
260
|
+
`comparison ${input.judgment.comparisonId}`,
|
|
261
|
+
`winner Candidate ${input.judgment.winnerBlindId}`,
|
|
262
|
+
`selected model ${input.result.selectedModel}`,
|
|
263
|
+
];
|
|
264
|
+
if (input.result.previousModel) lines.push(`previous model ${input.result.previousModel}`);
|
|
265
|
+
if (input.receipt) lines.push(`route decision receipt ${input.receipt.artifactId}${input.receipt.filename ? ` ${input.receipt.filename}` : ''}`);
|
|
266
|
+
if (input.receiptError) lines.push(`route decision receipt unavailable: ${input.receiptError}`);
|
|
267
|
+
lines.push('Judgment and comparison artifacts were not changed.');
|
|
268
|
+
return lines.join('\n');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type ComparisonRouteDecision = 'applied-winner' | 'left-unchanged';
|
|
272
|
+
|
|
273
|
+
export function parseRouteDecision(value: unknown): ComparisonRouteDecision | null {
|
|
274
|
+
const normalized = readString(value).toLowerCase().replace(/[\s_-]+/g, '');
|
|
275
|
+
if (normalized === 'leftunchanged' || normalized === 'leaveunchanged' || normalized === 'keepcurrent' || normalized === 'nochange' || normalized === 'unchanged') {
|
|
276
|
+
return 'left-unchanged';
|
|
277
|
+
}
|
|
278
|
+
if (normalized === 'appliedwinner' || normalized === 'applywinner' || normalized === 'apply') return 'applied-winner';
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function routeDecisionArtifactText(input: {
|
|
283
|
+
readonly decisionId: string;
|
|
284
|
+
readonly decision: ComparisonRouteDecision;
|
|
285
|
+
readonly judgment: LoadedComparisonJudgment;
|
|
286
|
+
readonly route?: AgentModelCompareRouteUpdateResult | null;
|
|
287
|
+
readonly currentModel?: string | null;
|
|
288
|
+
readonly explicitUserRequest: string;
|
|
289
|
+
}): string {
|
|
290
|
+
return `${JSON.stringify({
|
|
291
|
+
schema: 'goodvibes.agent.model_compare.route_decision.v1',
|
|
292
|
+
decisionId: input.decisionId,
|
|
293
|
+
decision: input.decision,
|
|
294
|
+
createdAt: new Date().toISOString(),
|
|
295
|
+
comparisonId: input.judgment.comparisonId,
|
|
296
|
+
judgmentId: input.judgment.judgmentId,
|
|
297
|
+
judgmentArtifactId: input.judgment.artifact.artifactId,
|
|
298
|
+
winnerBlindId: input.judgment.winnerBlindId,
|
|
299
|
+
winnerModel: input.judgment.winnerModel?.registryKey ?? null,
|
|
300
|
+
currentModel: input.currentModel ?? input.route?.selectedModel ?? null,
|
|
301
|
+
previousModel: input.route?.previousModel ?? null,
|
|
302
|
+
selectedModel: input.route?.selectedModel ?? input.currentModel ?? null,
|
|
303
|
+
explicitUserRequest: input.explicitUserRequest,
|
|
304
|
+
policy: {
|
|
305
|
+
mutation: input.decision === 'applied-winner' ? 'provider.model updated through confirmed apply' : 'provider.model intentionally left unchanged',
|
|
306
|
+
transcript: 'Route-decision evidence is saved as an artifact; comparison and judgment artifacts are not modified.',
|
|
307
|
+
},
|
|
308
|
+
}, null, 2)}\n`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function saveRouteDecisionArtifact(input: {
|
|
312
|
+
readonly artifactStore?: AgentModelCompareArtifactStore;
|
|
313
|
+
readonly decision: ComparisonRouteDecision;
|
|
314
|
+
readonly judgment: LoadedComparisonJudgment;
|
|
315
|
+
readonly route?: AgentModelCompareRouteUpdateResult | null;
|
|
316
|
+
readonly currentModel?: string | null;
|
|
317
|
+
readonly explicitUserRequest: string;
|
|
318
|
+
}): Promise<SavedComparisonArtifact> {
|
|
319
|
+
if (!input.artifactStore) throw new Error('Cannot save route decision because the artifact store is unavailable.');
|
|
320
|
+
const decisionId = `rdec_${randomUUID()}`;
|
|
321
|
+
const descriptor = await input.artifactStore.create({
|
|
322
|
+
kind: 'data',
|
|
323
|
+
mimeType: 'application/json',
|
|
324
|
+
filename: `blind-model-comparison-route-decision-${decisionId}.json`,
|
|
325
|
+
text: routeDecisionArtifactText({
|
|
326
|
+
decisionId,
|
|
327
|
+
decision: input.decision,
|
|
328
|
+
judgment: input.judgment,
|
|
329
|
+
route: input.route,
|
|
330
|
+
currentModel: input.currentModel,
|
|
331
|
+
explicitUserRequest: input.explicitUserRequest,
|
|
332
|
+
}),
|
|
333
|
+
metadata: {
|
|
334
|
+
purpose: 'agent-model-compare-route-decision',
|
|
335
|
+
decisionId,
|
|
336
|
+
decision: input.decision,
|
|
337
|
+
comparisonId: input.judgment.comparisonId,
|
|
338
|
+
judgmentId: input.judgment.judgmentId,
|
|
339
|
+
judgmentArtifactId: input.judgment.artifact.artifactId,
|
|
340
|
+
winnerBlindId: input.judgment.winnerBlindId,
|
|
341
|
+
...(input.judgment.winnerModel?.registryKey ? { winnerModel: input.judgment.winnerModel.registryKey } : {}),
|
|
342
|
+
...(input.currentModel ? { currentModel: input.currentModel } : {}),
|
|
343
|
+
...(input.route?.previousModel ? { previousModel: input.route.previousModel } : {}),
|
|
344
|
+
selectedModel: input.route?.selectedModel ?? input.currentModel ?? null,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
return toSavedComparisonArtifact(descriptor);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function formatRouteDecisionPreview(input: {
|
|
351
|
+
readonly judgment: LoadedComparisonJudgment;
|
|
352
|
+
readonly decision: ComparisonRouteDecision;
|
|
353
|
+
readonly currentModel?: string | null;
|
|
354
|
+
}): string {
|
|
355
|
+
return [
|
|
356
|
+
'Agent blind model comparison route-decision preview',
|
|
357
|
+
` decision ${input.decision}`,
|
|
358
|
+
` judgment ${input.judgment.judgmentId}`,
|
|
359
|
+
` comparison ${input.judgment.comparisonId}`,
|
|
360
|
+
` winner Candidate ${input.judgment.winnerBlindId}`,
|
|
361
|
+
` winner model ${input.judgment.winnerModel?.registryKey ?? '(not revealed in judgment)'}`,
|
|
362
|
+
` current model ${input.currentModel ?? '(unknown)'}`,
|
|
363
|
+
' policy creates one local route-decision receipt and does not change model routing',
|
|
364
|
+
].join('\n');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function formatRouteDecisionResult(input: {
|
|
368
|
+
readonly judgment: LoadedComparisonJudgment;
|
|
369
|
+
readonly decision: ComparisonRouteDecision;
|
|
370
|
+
readonly receipt: SavedComparisonArtifact;
|
|
371
|
+
readonly currentModel?: string | null;
|
|
372
|
+
}): string {
|
|
373
|
+
return [
|
|
374
|
+
`Recorded blind model comparison route decision ${input.decision}`,
|
|
375
|
+
`comparison ${input.judgment.comparisonId}`,
|
|
376
|
+
`judgment ${input.judgment.judgmentId}`,
|
|
377
|
+
`winner Candidate ${input.judgment.winnerBlindId}`,
|
|
378
|
+
`current model ${input.currentModel ?? '(unknown)'}`,
|
|
379
|
+
`route decision receipt ${input.receipt.artifactId}${input.receipt.filename ? ` ${input.receipt.filename}` : ''}`,
|
|
380
|
+
'No selected model was changed.',
|
|
381
|
+
].join('\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function incrementCount(map: Map<string, number>, key: string): void {
|
|
385
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function sortedCounts(map: Map<string, number>): readonly [string, number][] {
|
|
389
|
+
return [...map.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export interface ComparisonAnalyticsFilters {
|
|
393
|
+
readonly benchmarkKind?: string;
|
|
394
|
+
readonly taskType?: string;
|
|
395
|
+
readonly documentId?: string;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function readComparisonAnalyticsFilters(args: AgentModelCompareToolArgs): ComparisonAnalyticsFilters {
|
|
399
|
+
const benchmarkKind = readComparisonTag(args.benchmarkKind);
|
|
400
|
+
const taskType = readComparisonTag(args.taskType);
|
|
401
|
+
const documentId = readComparisonTag(args.documentId);
|
|
402
|
+
return {
|
|
403
|
+
...(benchmarkKind ? { benchmarkKind } : {}),
|
|
404
|
+
...(taskType ? { taskType } : {}),
|
|
405
|
+
...(documentId ? { documentId } : {}),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function hasComparisonFilters(filters: ComparisonAnalyticsFilters): boolean {
|
|
410
|
+
return Boolean(filters.benchmarkKind || filters.taskType || filters.documentId);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function matchesFilter(actual: string | undefined, expected: string | undefined): boolean {
|
|
414
|
+
if (!expected) return true;
|
|
415
|
+
return (actual ?? '').toLowerCase() === expected.toLowerCase();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function judgmentMatchesFilters(judgment: LoadedComparisonJudgment, filters: ComparisonAnalyticsFilters): boolean {
|
|
419
|
+
return matchesFilter(judgment.benchmarkKind, filters.benchmarkKind)
|
|
420
|
+
&& matchesFilter(judgment.taskType, filters.taskType)
|
|
421
|
+
&& matchesFilter(judgment.documentId, filters.documentId);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function formatComparisonFilters(filters: ComparisonAnalyticsFilters): string {
|
|
425
|
+
if (!hasComparisonFilters(filters)) return 'filters none';
|
|
426
|
+
const parts = [
|
|
427
|
+
filters.benchmarkKind ? `benchmarkKind ${filters.benchmarkKind}` : null,
|
|
428
|
+
filters.taskType ? `taskType ${filters.taskType}` : null,
|
|
429
|
+
filters.documentId ? `documentId ${filters.documentId}` : null,
|
|
430
|
+
].filter((entry): entry is string => Boolean(entry));
|
|
431
|
+
return `filters ${parts.join('; ')}`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function incrementOptionalDimension(map: Map<string, number>, value: string | undefined): void {
|
|
435
|
+
incrementCount(map, value || '(untagged)');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function formatComparisonAnalytics(input: {
|
|
439
|
+
readonly judgments: readonly LoadedComparisonJudgment[];
|
|
440
|
+
readonly limit: number;
|
|
441
|
+
readonly includeReasons: boolean;
|
|
442
|
+
readonly filters: ComparisonAnalyticsFilters;
|
|
443
|
+
readonly storeAvailable: boolean;
|
|
444
|
+
}): string {
|
|
445
|
+
if (!input.storeAvailable) {
|
|
446
|
+
return 'Saved comparison analytics are unavailable because the artifact store does not expose listing and content reads in this runtime.';
|
|
447
|
+
}
|
|
448
|
+
if (input.judgments.length === 0) {
|
|
449
|
+
return hasComparisonFilters(input.filters)
|
|
450
|
+
? `No saved comparison judgments matched ${formatComparisonFilters(input.filters)}. Clear a filter or save a matching judgment with agent_model_compare mode:"judge".`
|
|
451
|
+
: 'No saved comparison judgments found. Save a judgment with agent_model_compare mode:"judge" first.';
|
|
452
|
+
}
|
|
453
|
+
const modelWinners = new Map<string, number>();
|
|
454
|
+
const blindWinners = new Map<string, number>();
|
|
455
|
+
const benchmarkCounts = new Map<string, number>();
|
|
456
|
+
const taskCounts = new Map<string, number>();
|
|
457
|
+
const documentCounts = new Map<string, number>();
|
|
458
|
+
let revealed = 0;
|
|
459
|
+
for (const judgment of input.judgments) {
|
|
460
|
+
incrementCount(blindWinners, normalizeBlindId(judgment.winnerBlindId) || judgment.winnerBlindId || '?');
|
|
461
|
+
incrementOptionalDimension(benchmarkCounts, judgment.benchmarkKind);
|
|
462
|
+
incrementOptionalDimension(taskCounts, judgment.taskType);
|
|
463
|
+
incrementOptionalDimension(documentCounts, judgment.documentId);
|
|
464
|
+
if (judgment.winnerModel?.registryKey) {
|
|
465
|
+
revealed += 1;
|
|
466
|
+
incrementCount(modelWinners, `${judgment.winnerModel.registryKey} (${judgment.winnerModel.displayName})`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const hidden = input.judgments.length - revealed;
|
|
470
|
+
const lines = [
|
|
471
|
+
'Blind model comparison analytics',
|
|
472
|
+
`judgments ${input.judgments.length}; revealed ${revealed}; hidden ${hidden}; limit ${input.limit}`,
|
|
473
|
+
formatComparisonFilters(input.filters),
|
|
474
|
+
'',
|
|
475
|
+
'Winning models',
|
|
476
|
+
];
|
|
477
|
+
if (modelWinners.size === 0) {
|
|
478
|
+
lines.push(' No revealed winner models yet. Save a judgment with reveal:true before model-level analytics are available.');
|
|
479
|
+
} else {
|
|
480
|
+
lines.push(...sortedCounts(modelWinners).map(([model, count]) => ` ${model}: ${count}`));
|
|
481
|
+
}
|
|
482
|
+
lines.push('');
|
|
483
|
+
lines.push('Winning blind slots');
|
|
484
|
+
lines.push(...sortedCounts(blindWinners).map(([blindId, count]) => ` Candidate ${blindId}: ${count}`));
|
|
485
|
+
lines.push('');
|
|
486
|
+
lines.push('Trend dimensions');
|
|
487
|
+
lines.push(' benchmark tags');
|
|
488
|
+
lines.push(...sortedCounts(benchmarkCounts).map(([tag, count]) => ` ${tag}: ${count}`));
|
|
489
|
+
lines.push(' task types');
|
|
490
|
+
lines.push(...sortedCounts(taskCounts).map(([task, count]) => ` ${task}: ${count}`));
|
|
491
|
+
lines.push(' documents');
|
|
492
|
+
lines.push(...sortedCounts(documentCounts).map(([document, count]) => ` ${document}: ${count}`));
|
|
493
|
+
lines.push('');
|
|
494
|
+
lines.push('Recent judgments');
|
|
495
|
+
for (const judgment of input.judgments.slice(0, 10)) {
|
|
496
|
+
const model = judgment.winnerModel?.registryKey
|
|
497
|
+
? ` model ${judgment.winnerModel.registryKey}`
|
|
498
|
+
: ' model hidden';
|
|
499
|
+
lines.push(` ${judgment.artifact.artifactId} ${judgment.comparisonId} winner Candidate ${judgment.winnerBlindId}${model}`);
|
|
500
|
+
const dimensions = [
|
|
501
|
+
judgment.benchmarkKind ? `benchmark ${judgment.benchmarkKind}` : null,
|
|
502
|
+
judgment.taskType ? `task ${judgment.taskType}` : null,
|
|
503
|
+
judgment.documentId ? `document ${judgment.documentId}` : null,
|
|
504
|
+
].filter(Boolean).join('; ');
|
|
505
|
+
if (dimensions) lines.push(` ${dimensions}`);
|
|
506
|
+
if (input.includeReasons && judgment.reasons) {
|
|
507
|
+
lines.push(` reasons ${previewText(judgment.reasons, 120)}`);
|
|
508
|
+
}
|
|
509
|
+
if (input.includeReasons && judgment.notes) {
|
|
510
|
+
lines.push(` notes ${previewText(judgment.notes, 120)}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
lines.push('No selected model was changed.');
|
|
514
|
+
return lines.join('\n');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function countPhrase(count: number, singular: string, plural = `${singular}s`): string {
|
|
518
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export function pushSynthesisTheme(
|
|
522
|
+
map: Map<string, LoadedComparisonJudgment[]>,
|
|
523
|
+
label: string,
|
|
524
|
+
judgment: LoadedComparisonJudgment,
|
|
525
|
+
): void {
|
|
526
|
+
const judgments = map.get(label) ?? [];
|
|
527
|
+
judgments.push(judgment);
|
|
528
|
+
map.set(label, judgments);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function formatComparisonSynthesis(input: {
|
|
532
|
+
readonly judgments: readonly LoadedComparisonJudgment[];
|
|
533
|
+
readonly limit: number;
|
|
534
|
+
readonly includeReasons: boolean;
|
|
535
|
+
readonly filters: ComparisonAnalyticsFilters;
|
|
536
|
+
readonly storeAvailable: boolean;
|
|
537
|
+
}): string {
|
|
538
|
+
if (!input.storeAvailable) {
|
|
539
|
+
return 'Saved comparison synthesis is unavailable because the artifact store does not expose listing and content reads in this runtime.';
|
|
540
|
+
}
|
|
541
|
+
if (input.judgments.length === 0) {
|
|
542
|
+
return hasComparisonFilters(input.filters)
|
|
543
|
+
? `No saved comparison judgments matched ${formatComparisonFilters(input.filters)}. Clear a filter or save a matching judgment with agent_model_compare mode:"judge".`
|
|
544
|
+
: 'No saved comparison judgments found. Save a judgment with agent_model_compare mode:"judge" first.';
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const modelWinners = new Map<string, number>();
|
|
548
|
+
const comparisonIds = new Set<string>();
|
|
549
|
+
const benchmarkCounts = new Map<string, number>();
|
|
550
|
+
const taskCounts = new Map<string, number>();
|
|
551
|
+
const documentCounts = new Map<string, number>();
|
|
552
|
+
const themes = new Map<string, LoadedComparisonJudgment[]>();
|
|
553
|
+
let revealed = 0;
|
|
554
|
+
for (const judgment of input.judgments) {
|
|
555
|
+
comparisonIds.add(judgment.comparisonId);
|
|
556
|
+
incrementOptionalDimension(benchmarkCounts, judgment.benchmarkKind);
|
|
557
|
+
incrementOptionalDimension(taskCounts, judgment.taskType);
|
|
558
|
+
incrementOptionalDimension(documentCounts, judgment.documentId);
|
|
559
|
+
if (judgment.winnerModel?.registryKey) {
|
|
560
|
+
revealed += 1;
|
|
561
|
+
incrementCount(modelWinners, `${judgment.winnerModel.registryKey} (${judgment.winnerModel.displayName})`);
|
|
562
|
+
}
|
|
563
|
+
const preferenceText = `${judgment.reasons}\n${judgment.notes}`.trim();
|
|
564
|
+
const matched = SYNTHESIS_THEMES.filter((theme) => theme.pattern.test(preferenceText));
|
|
565
|
+
if (matched.length === 0) {
|
|
566
|
+
pushSynthesisTheme(themes, 'Uncategorized preference signal', judgment);
|
|
567
|
+
} else {
|
|
568
|
+
for (const theme of matched) pushSynthesisTheme(themes, theme.label, judgment);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const hidden = input.judgments.length - revealed;
|
|
573
|
+
const themeCounts = [...themes.entries()]
|
|
574
|
+
.sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]));
|
|
575
|
+
const lines = [
|
|
576
|
+
'Blind model comparison synthesis',
|
|
577
|
+
`judgments ${input.judgments.length}; revealed ${revealed}; hidden ${hidden}; compared comparisons ${comparisonIds.size}; limit ${input.limit}`,
|
|
578
|
+
formatComparisonFilters(input.filters),
|
|
579
|
+
'',
|
|
580
|
+
'Winning model direction',
|
|
581
|
+
];
|
|
582
|
+
if (modelWinners.size === 0) {
|
|
583
|
+
lines.push(' No revealed winner models yet. Reveal judgment winners before model-level synthesis is available.');
|
|
584
|
+
} else {
|
|
585
|
+
lines.push(...sortedCounts(modelWinners).map(([model, count]) => ` ${model}: ${count}`));
|
|
586
|
+
}
|
|
587
|
+
if (hidden > 0) {
|
|
588
|
+
lines.push(` Hidden winners: ${countPhrase(hidden, 'judgment')} ${hidden === 1 ? 'needs' : 'need'} reveal before model-level synthesis.`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
lines.push('');
|
|
592
|
+
lines.push('Cross-session reason themes');
|
|
593
|
+
for (const [label, judgments] of themeCounts) {
|
|
594
|
+
lines.push(` ${label}: ${countPhrase(judgments.length, 'judgment')}`);
|
|
595
|
+
if (!input.includeReasons) continue;
|
|
596
|
+
const example = judgments.find((judgment) => judgment.reasons || judgment.notes);
|
|
597
|
+
if (!example) continue;
|
|
598
|
+
const source = example.reasons || example.notes;
|
|
599
|
+
lines.push(` example ${example.artifact.artifactId}: ${previewText(source, 120)}`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
lines.push('');
|
|
603
|
+
lines.push('Trend dimensions');
|
|
604
|
+
lines.push(' benchmark tags');
|
|
605
|
+
lines.push(...sortedCounts(benchmarkCounts).map(([tag, count]) => ` ${tag}: ${count}`));
|
|
606
|
+
lines.push(' task types');
|
|
607
|
+
lines.push(...sortedCounts(taskCounts).map(([task, count]) => ` ${task}: ${count}`));
|
|
608
|
+
lines.push(' documents');
|
|
609
|
+
lines.push(...sortedCounts(documentCounts).map(([document, count]) => ` ${document}: ${count}`));
|
|
610
|
+
|
|
611
|
+
lines.push('');
|
|
612
|
+
lines.push('Recent synthesis inputs');
|
|
613
|
+
for (const judgment of input.judgments.slice(0, 10)) {
|
|
614
|
+
const model = judgment.winnerModel?.registryKey
|
|
615
|
+
? ` model ${judgment.winnerModel.registryKey}`
|
|
616
|
+
: ' model hidden';
|
|
617
|
+
lines.push(` ${judgment.artifact.artifactId} ${judgment.comparisonId} winner Candidate ${judgment.winnerBlindId}${model}`);
|
|
618
|
+
const dimensions = [
|
|
619
|
+
judgment.benchmarkKind ? `benchmark ${judgment.benchmarkKind}` : null,
|
|
620
|
+
judgment.taskType ? `task ${judgment.taskType}` : null,
|
|
621
|
+
judgment.documentId ? `document ${judgment.documentId}` : null,
|
|
622
|
+
].filter(Boolean).join('; ');
|
|
623
|
+
if (dimensions) lines.push(` ${dimensions}`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
lines.push('');
|
|
627
|
+
lines.push('Recommended next actions');
|
|
628
|
+
if (hidden > 0) lines.push(' - Reveal hidden judgments before applying model-level route changes.');
|
|
629
|
+
lines.push(' - Export comparison or judgment reports before sharing evidence externally.');
|
|
630
|
+
lines.push(' - Apply a winner only through confirmed mode:"apply".');
|
|
631
|
+
lines.push('No selected model was changed.');
|
|
632
|
+
return lines.join('\n');
|
|
633
|
+
}
|