@pellux/goodvibes-agent 1.1.6 → 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.
Files changed (256) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/README.md +46 -30
  3. package/dist/package/{ast-grep-napi.linux-x64-gnu-mkk8xwww.node → ast-grep-napi.linux-x64-gnu-swtppvy9.node} +0 -0
  4. package/dist/package/{ast-grep-napi.linux-x64-musl-ryqtgdv6.node → ast-grep-napi.linux-x64-musl-ttfcdtap.node} +0 -0
  5. package/dist/package/main.js +132662 -91399
  6. package/docs/README.md +14 -7
  7. package/docs/channels-remote-and-api.md +8 -5
  8. package/docs/connected-host.md +14 -12
  9. package/docs/getting-started.md +56 -30
  10. package/docs/knowledge-artifacts-and-multimodal.md +7 -5
  11. package/docs/project-planning.md +2 -2
  12. package/docs/providers-and-routing.md +11 -3
  13. package/docs/tools-and-commands.md +114 -45
  14. package/docs/voice-and-live-tts.md +3 -1
  15. package/package.json +3 -2
  16. package/release/release-notes.md +130 -5
  17. package/release/release-readiness.json +120 -90
  18. package/src/agent/assistant-cockpit.ts +247 -0
  19. package/src/agent/autonomy-schedule-format.ts +96 -0
  20. package/src/agent/autonomy-schedule.ts +514 -0
  21. package/src/agent/channel-delivery-receipts.ts +292 -0
  22. package/src/agent/competitive-feature-inventory.ts +296 -0
  23. package/src/agent/document-registry-types.ts +106 -0
  24. package/src/agent/document-registry.ts +734 -0
  25. package/src/agent/harness-control.ts +2 -2
  26. package/src/agent/memory-prompt.ts +2 -2
  27. package/src/agent/operator-actions.ts +50 -6
  28. package/src/agent/persona-registry.ts +8 -0
  29. package/src/agent/project-context-files.ts +273 -0
  30. package/src/agent/prompt-context-receipts.ts +502 -0
  31. package/src/agent/reminder-schedule-format.ts +16 -2
  32. package/src/agent/research-run-registry.ts +582 -0
  33. package/src/agent/research-source-registry.ts +441 -0
  34. package/src/agent/routine-registry.ts +19 -3
  35. package/src/agent/routine-schedule-format.ts +27 -2
  36. package/src/agent/runtime-profile-starters.ts +7 -0
  37. package/src/agent/runtime-profile.ts +106 -4
  38. package/src/agent/schedule-edit-format.ts +129 -0
  39. package/src/agent/schedule-edit.ts +496 -0
  40. package/src/agent/schedule-next-routes.ts +42 -0
  41. package/src/agent/setup-wizard-checkpoint.ts +140 -0
  42. package/src/agent/setup-wizard.ts +447 -0
  43. package/src/agent/skill-registry-types.ts +102 -0
  44. package/src/agent/skill-registry.ts +81 -107
  45. package/src/agent/vibe-confirmation-routes.ts +62 -0
  46. package/src/agent/vibe-file.ts +285 -0
  47. package/src/cli/agent-knowledge-command.ts +45 -2
  48. package/src/cli/help.ts +3 -0
  49. package/src/cli/profiles-command.ts +25 -11
  50. package/src/config/agent-settings-policy.ts +1 -18
  51. package/src/input/agent-workspace-activation.ts +53 -0
  52. package/src/input/agent-workspace-artifact-browser-editor.ts +494 -0
  53. package/src/input/agent-workspace-artifact-metadata.ts +17 -0
  54. package/src/input/agent-workspace-basic-command-editors.ts +5 -1
  55. package/src/input/agent-workspace-categories.ts +153 -155
  56. package/src/input/agent-workspace-channel-triage.ts +481 -0
  57. package/src/input/agent-workspace-channels.ts +263 -0
  58. package/src/input/agent-workspace-command-editor.ts +152 -0
  59. package/src/input/agent-workspace-context-snapshot.ts +349 -0
  60. package/src/input/agent-workspace-delegation-editor-submission.ts +8 -0
  61. package/src/input/agent-workspace-document-editor.ts +502 -0
  62. package/src/input/agent-workspace-document-ops-editor.ts +523 -0
  63. package/src/input/agent-workspace-host-category.ts +1 -1
  64. package/src/input/agent-workspace-local-library-snapshot.ts +167 -0
  65. package/src/input/agent-workspace-model-compare-editor.ts +376 -0
  66. package/src/input/agent-workspace-model-compare-prompt-submission.ts +552 -0
  67. package/src/input/agent-workspace-model-compare-types.ts +82 -0
  68. package/src/input/agent-workspace-model-compare-utils.ts +35 -0
  69. package/src/input/agent-workspace-onboarding-categories.ts +141 -0
  70. package/src/input/agent-workspace-operations-command-editor-submission.ts +53 -0
  71. package/src/input/agent-workspace-operations-command-editors.ts +22 -0
  72. package/src/input/agent-workspace-research-report-editor.ts +212 -0
  73. package/src/input/agent-workspace-research-run-editor.ts +144 -0
  74. package/src/input/agent-workspace-research-source-editor.ts +167 -0
  75. package/src/input/agent-workspace-review-packet-snapshot.ts +593 -0
  76. package/src/input/agent-workspace-review-packet-utils.ts +265 -0
  77. package/src/input/agent-workspace-settings.ts +262 -30
  78. package/src/input/agent-workspace-setup-checkpoint-action.ts +127 -0
  79. package/src/input/agent-workspace-setup-snapshot.ts +198 -0
  80. package/src/input/agent-workspace-setup.ts +34 -1
  81. package/src/input/agent-workspace-snapshot.ts +162 -147
  82. package/src/input/agent-workspace-types.ts +287 -2
  83. package/src/input/agent-workspace.ts +18 -0
  84. package/src/input/command-registry.ts +9 -2
  85. package/src/input/commands/agent-runtime-profile-runtime.ts +28 -13
  86. package/src/input/commands/channels-runtime.ts +102 -114
  87. package/src/input/commands/delegation-runtime.ts +87 -24
  88. package/src/input/commands/knowledge-browser-flags.ts +12 -0
  89. package/src/input/commands/knowledge.ts +28 -14
  90. package/src/input/commands/operator-actions-runtime.ts +6 -3
  91. package/src/input/commands/schedule-runtime.ts +71 -14
  92. package/src/input/commands/session-content.ts +2 -2
  93. package/src/input/commands/shell-core.ts +2 -0
  94. package/src/input/commands/vibe-runtime.ts +140 -0
  95. package/src/input/commands.ts +2 -0
  96. package/src/input/connected-host-routes.ts +123 -0
  97. package/src/input/settings-modal-types.ts +14 -0
  98. package/src/main.ts +15 -15
  99. package/src/panels/qr-panel.ts +2 -2
  100. package/src/renderer/agent-workspace-context-lines.ts +686 -0
  101. package/src/renderer/agent-workspace.ts +30 -419
  102. package/src/renderer/settings-modal-helpers.ts +12 -0
  103. package/src/renderer/settings-modal.ts +12 -0
  104. package/src/runtime/bootstrap-command-context.ts +11 -1
  105. package/src/runtime/bootstrap-command-parts.ts +14 -3
  106. package/src/runtime/bootstrap-core.ts +43 -1
  107. package/src/runtime/bootstrap-shell.ts +6 -0
  108. package/src/runtime/bootstrap.ts +113 -30
  109. package/src/runtime/connected-host-auth.ts +3 -2
  110. package/src/runtime/execution-ledger.ts +231 -0
  111. package/src/runtime/services.ts +88 -10
  112. package/src/runtime/tool-permission-safety.ts +287 -0
  113. package/src/shell/session-continuity-hints.ts +39 -0
  114. package/src/tools/agent-artifacts-tool.ts +606 -0
  115. package/src/tools/agent-audit-tool.ts +155 -0
  116. package/src/tools/agent-autonomy-schedule-tool.ts +238 -0
  117. package/src/tools/agent-autonomy-tool.ts +140 -0
  118. package/src/tools/agent-channel-send-tool.ts +24 -2
  119. package/src/tools/agent-channels-tool.ts +140 -0
  120. package/src/tools/agent-computer-tool.ts +168 -0
  121. package/src/tools/agent-context-policy.ts +267 -10
  122. package/src/tools/agent-context-tool.ts +143 -0
  123. package/src/tools/agent-delegation-tool.ts +128 -0
  124. package/src/tools/agent-device-tool.ts +240 -0
  125. package/src/tools/agent-documents-tool.ts +684 -0
  126. package/src/tools/agent-execution-tool.ts +230 -0
  127. package/src/tools/agent-harness-agent-orchestration.ts +703 -0
  128. package/src/tools/agent-harness-autonomy-intake.ts +785 -0
  129. package/src/tools/agent-harness-autonomy-live-records.ts +588 -0
  130. package/src/tools/agent-harness-autonomy-queue-types.ts +88 -0
  131. package/src/tools/agent-harness-autonomy-queue.ts +560 -0
  132. package/src/tools/agent-harness-background-processes-types.ts +35 -0
  133. package/src/tools/agent-harness-background-processes.ts +794 -0
  134. package/src/tools/agent-harness-browser-cockpit-route.ts +219 -0
  135. package/src/tools/agent-harness-browser-control.ts +338 -0
  136. package/src/tools/agent-harness-channel-metadata.ts +168 -10
  137. package/src/tools/agent-harness-cli-command-policy.ts +110 -0
  138. package/src/tools/agent-harness-command-catalog.ts +4 -2
  139. package/src/tools/agent-harness-command-runner.ts +42 -0
  140. package/src/tools/agent-harness-connected-host-status.ts +7 -4
  141. package/src/tools/agent-harness-delegation-posture.ts +234 -19
  142. package/src/tools/agent-harness-document-ops-reviewer-readiness.ts +234 -0
  143. package/src/tools/agent-harness-document-ops-types.ts +72 -0
  144. package/src/tools/agent-harness-document-ops.ts +671 -0
  145. package/src/tools/agent-harness-execution-history.ts +489 -0
  146. package/src/tools/agent-harness-execution-posture.ts +382 -0
  147. package/src/tools/agent-harness-file-recovery.ts +135 -0
  148. package/src/tools/agent-harness-keybinding-metadata.ts +16 -12
  149. package/src/tools/agent-harness-learning-curator-common.ts +102 -0
  150. package/src/tools/agent-harness-learning-curator-consolidation.ts +295 -0
  151. package/src/tools/agent-harness-learning-curator-proposals.ts +606 -0
  152. package/src/tools/agent-harness-learning-curator-types.ts +151 -0
  153. package/src/tools/agent-harness-learning-curator.ts +417 -0
  154. package/src/tools/agent-harness-local-model-benchmarks.ts +199 -0
  155. package/src/tools/agent-harness-local-model-cookbook.ts +238 -0
  156. package/src/tools/agent-harness-local-model-endpoints.ts +673 -0
  157. package/src/tools/agent-harness-media-posture.ts +208 -2
  158. package/src/tools/agent-harness-memory-posture.ts +556 -0
  159. package/src/tools/agent-harness-metadata.ts +100 -168
  160. package/src/tools/agent-harness-mode-catalog.ts +84 -33
  161. package/src/tools/agent-harness-model-catalog.ts +162 -0
  162. package/src/tools/agent-harness-model-provider-health.ts +186 -0
  163. package/src/tools/agent-harness-model-readiness.ts +406 -0
  164. package/src/tools/agent-harness-model-routing-types.ts +266 -0
  165. package/src/tools/agent-harness-model-routing-utils.ts +30 -0
  166. package/src/tools/agent-harness-model-routing.ts +137 -190
  167. package/src/tools/agent-harness-operator-methods.ts +115 -133
  168. package/src/tools/agent-harness-pairing-posture.ts +431 -18
  169. package/src/tools/agent-harness-personal-ops-discovery.ts +533 -0
  170. package/src/tools/agent-harness-personal-ops-intake.ts +385 -0
  171. package/src/tools/agent-harness-personal-ops-lanes.ts +204 -0
  172. package/src/tools/agent-harness-personal-ops-records.ts +732 -0
  173. package/src/tools/agent-harness-personal-ops-runner.ts +637 -0
  174. package/src/tools/agent-harness-personal-ops-types.ts +222 -0
  175. package/src/tools/agent-harness-personal-ops.ts +606 -0
  176. package/src/tools/agent-harness-project-context.ts +144 -0
  177. package/src/tools/agent-harness-prompt-context.ts +589 -0
  178. package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
  179. package/src/tools/agent-harness-release-evidence.ts +9 -7
  180. package/src/tools/agent-harness-release-readiness.ts +9 -7
  181. package/src/tools/agent-harness-research-briefing.ts +427 -0
  182. package/src/tools/agent-harness-research-queue.ts +250 -0
  183. package/src/tools/agent-harness-research-runs.ts +295 -0
  184. package/src/tools/agent-harness-research-workflow.ts +322 -0
  185. package/src/tools/agent-harness-security-posture.ts +2 -2
  186. package/src/tools/agent-harness-service-posture.ts +9 -9
  187. package/src/tools/agent-harness-session-metadata.ts +3 -3
  188. package/src/tools/agent-harness-setup-connected-host.ts +501 -0
  189. package/src/tools/agent-harness-setup-describe.ts +557 -0
  190. package/src/tools/agent-harness-setup-handoffs.ts +502 -0
  191. package/src/tools/agent-harness-setup-model-helpers.ts +101 -0
  192. package/src/tools/agent-harness-setup-plan.ts +253 -0
  193. package/src/tools/agent-harness-setup-posture-types.ts +218 -0
  194. package/src/tools/agent-harness-setup-posture-utils.ts +173 -0
  195. package/src/tools/agent-harness-setup-posture.ts +449 -221
  196. package/src/tools/agent-harness-setup-smoke.ts +508 -0
  197. package/src/tools/agent-harness-sudo-posture.ts +114 -0
  198. package/src/tools/agent-harness-tool-schema.ts +120 -7
  199. package/src/tools/agent-harness-tool-types.ts +78 -0
  200. package/src/tools/agent-harness-tool-utils.ts +43 -0
  201. package/src/tools/agent-harness-tool.ts +311 -370
  202. package/src/tools/agent-harness-ui-surface-metadata.ts +76 -69
  203. package/src/tools/agent-harness-ui-surface-types.ts +46 -0
  204. package/src/tools/agent-harness-vibe-health.ts +105 -0
  205. package/src/tools/agent-harness-workspace-action-runner.ts +149 -0
  206. package/src/tools/agent-harness-workspace-actions.ts +114 -8
  207. package/src/tools/agent-harness-workspace-editor-execution.ts +214 -0
  208. package/src/tools/agent-harness-workspace-editor-runner.ts +755 -0
  209. package/src/tools/agent-host-tool.ts +159 -0
  210. package/src/tools/agent-knowledge-ingest-tool.ts +34 -1
  211. package/src/tools/agent-learning-consolidation-core.ts +327 -0
  212. package/src/tools/agent-learning-consolidation-tool.ts +758 -0
  213. package/src/tools/agent-memory-tool.ts +214 -0
  214. package/src/tools/agent-model-compare-export.ts +315 -0
  215. package/src/tools/agent-model-compare-handoff.ts +592 -0
  216. package/src/tools/agent-model-compare-judgment.ts +633 -0
  217. package/src/tools/agent-model-compare-run.ts +722 -0
  218. package/src/tools/agent-model-compare-tool.ts +698 -0
  219. package/src/tools/agent-model-compare-types.ts +191 -0
  220. package/src/tools/agent-model-compare-utils.ts +93 -0
  221. package/src/tools/agent-models-tool.ts +208 -0
  222. package/src/tools/agent-operator-action-tool.ts +1 -1
  223. package/src/tools/agent-operator-method-tool.ts +643 -0
  224. package/src/tools/agent-personal-ops-tool.ts +197 -0
  225. package/src/tools/agent-policy-explanation.ts +415 -0
  226. package/src/tools/agent-research-report-tool.ts +608 -0
  227. package/src/tools/agent-research-runs-tool.ts +434 -0
  228. package/src/tools/agent-research-sources-tool.ts +443 -0
  229. package/src/tools/agent-research-tool.ts +687 -0
  230. package/src/tools/agent-review-packet-presets-core.ts +757 -0
  231. package/src/tools/agent-review-packet-presets-tool.ts +466 -0
  232. package/src/tools/agent-review-packet-share-tool.ts +305 -0
  233. package/src/tools/agent-route-planner-candidates-setup.ts +411 -0
  234. package/src/tools/agent-route-planner-candidates-surfaces.ts +432 -0
  235. package/src/tools/agent-route-planner-candidates-work.ts +251 -0
  236. package/src/tools/agent-route-planner-helpers.ts +667 -0
  237. package/src/tools/agent-route-planner.ts +185 -0
  238. package/src/tools/agent-route-tool.ts +105 -0
  239. package/src/tools/agent-schedule-edit-tool.ts +210 -0
  240. package/src/tools/agent-schedule-tool.ts +282 -0
  241. package/src/tools/agent-security-tool.ts +145 -0
  242. package/src/tools/agent-sessions-tool.ts +122 -0
  243. package/src/tools/agent-settings-import-tool.ts +104 -0
  244. package/src/tools/agent-settings-tool.ts +176 -0
  245. package/src/tools/agent-setup-tool.ts +226 -0
  246. package/src/tools/agent-support-tool.ts +122 -0
  247. package/src/tools/agent-terminal-process-tools.ts +167 -0
  248. package/src/tools/agent-tool-policy-guard-types.ts +77 -0
  249. package/src/tools/agent-tool-policy-guard.ts +110 -89
  250. package/src/tools/agent-vibe-tool.ts +297 -0
  251. package/src/tools/agent-work-plan-tool.ts +265 -6
  252. package/src/tools/agent-workspace-tool.ts +305 -0
  253. package/src/tools/artifact-archive.ts +169 -0
  254. package/src/tools/tool-definition-compaction.ts +36 -0
  255. package/src/tools/tool-execution-safety.ts +41 -0
  256. package/src/version.ts +1 -1
@@ -0,0 +1,698 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { ArtifactDescriptor, ArtifactRecord, ArtifactStore } from '@pellux/goodvibes-sdk/platform/artifacts';
3
+ import type { ChatRequest, ChatResponse, LLMProvider } from '@pellux/goodvibes-sdk/platform/providers';
4
+ import type { Tool } from '@pellux/goodvibes-sdk/platform/types';
5
+ import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
6
+ import { createZipArchive } from './artifact-archive.ts';
7
+ import type { AgentModelCompareToolArgs, AgentModelCompareToolDeps, SavedComparisonArtifact, StoredComparison } from './agent-model-compare-types.ts';
8
+ import { BLIND_LABELS, DEFAULT_CANDIDATE_COUNT, DEFAULT_MAX_TOKENS, DEFAULT_SIDE_BY_SIDE_PREVIEW_BYTES, MAX_CANDIDATES, MAX_COMPLETION_TOKENS, MAX_PROMPT_CHARS, MAX_SIDE_BY_SIDE_PREVIEW_BYTES, MIN_CANDIDATES, MODE_ANALYTICS, MODE_APPLY, MODE_EXPORT, MODE_HANDOFF, MODE_HANDOFF_ARCHIVE, MODE_HANDOFF_DIFF, MODE_JUDGE, MODE_REVEAL, MODE_REVIEW, MODE_ROUTE_DECISION, MODE_RUN, MODE_SIDE_BY_SIDE, MODE_SYNTHESIS } from './agent-model-compare-types.ts';
9
+ import { buildRunPromptFromArtifact, formatPreview, formatReveal, formatReview, formatRunResult, formatSavedComparisonArtifacts, formatSavedHandoffArtifacts, loadComparisonFromArtifact, parseMode, rememberComparison, resolveComparisonForRead, resolveCurrentModel, runCandidate, saveComparisonArtifact, selectComparisonModels } from './agent-model-compare-run.ts';
10
+ import { ensureSelectableWinnerModel, findCandidate, formatApplyPreview, formatApplyResult, formatComparisonAnalytics, formatComparisonSynthesis, formatJudgePreview, formatJudgmentResult, formatRouteDecisionPreview, formatRouteDecisionResult, loadJudgmentFromArtifact, loadSavedJudgments, parseRouteDecision, readComparisonAnalyticsFilters, saveComparisonJudgmentArtifact, saveRouteDecisionArtifact } from './agent-model-compare-judgment.ts';
11
+ import { comparisonExportMarkdown, comparisonHandoffMarkdown, formatSideBySideReview, judgmentExportMarkdown, loadHandoffRelatedArtifacts, saveComparisonExportArtifact, saveComparisonHandoffArtifact } from './agent-model-compare-export.ts';
12
+ import { buildComparisonHandoffArchivePayload, findRouteDecisionArtifactIdsForHandoff, formatExportPreview, formatExportResult, formatHandoffArchivePreview, formatHandoffArchiveResult, formatHandoffDiff, formatHandoffPreview, formatHandoffResult, loadHandoffArchiveArtifacts, loadHandoffDiffArtifact, loadHandoffFromArtifact, saveComparisonHandoffArchiveArtifact } from './agent-model-compare-handoff.ts';
13
+ import { clamp, failure, output, previewText, readBenchmarkKind, readBoolean, readComparisonTag, readModelRefs, readNumber, readOptionalBoolean, readString, readStringList } from './agent-model-compare-utils.ts';
14
+ export type { AgentModelCompareCatalogModel, AgentModelCompareModelCatalog, AgentModelCompareProviderRegistry, AgentModelCompareRouteUpdateResult, AgentModelCompareToolArgs, AgentModelCompareToolDeps } from './agent-model-compare-types.ts';
15
+
16
+ export function createAgentModelCompareTool(deps: AgentModelCompareToolDeps): Tool {
17
+ const comparisons = new Map<string, StoredComparison>();
18
+ return {
19
+ definition: {
20
+ name: 'agent_model_compare',
21
+ description: 'Blind compare prompts/artifacts, review, route decisions, handoff, diff.',
22
+ parameters: {
23
+ type: 'object',
24
+ properties: {
25
+ mode: {
26
+ type: 'string',
27
+ enum: [MODE_RUN, MODE_REVEAL, MODE_REVIEW, MODE_SIDE_BY_SIDE, MODE_JUDGE, MODE_APPLY, MODE_ROUTE_DECISION, MODE_EXPORT, MODE_HANDOFF, MODE_HANDOFF_ARCHIVE, MODE_HANDOFF_DIFF, MODE_ANALYTICS, MODE_SYNTHESIS],
28
+ description: 'Select compare workflow mode.',
29
+ },
30
+ prompt: {
31
+ type: 'string',
32
+ description: 'Exact prompt sent identically to every candidate model.',
33
+ },
34
+ modelRefs: {
35
+ type: 'array',
36
+ items: { type: 'string' },
37
+ description: 'Optional registry keys or model ids. Omit to auto-select candidates.',
38
+ },
39
+ candidateCount: {
40
+ type: 'number',
41
+ description: 'Number of candidates to auto-select when modelRefs is omitted. 2 to 4.',
42
+ },
43
+ rubric: {
44
+ type: 'string',
45
+ description: 'Optional judging rubric shown with the blinded results.',
46
+ },
47
+ systemPrompt: {
48
+ type: 'string',
49
+ description: 'Optional system prompt sent identically to every candidate model.',
50
+ },
51
+ maxTokens: {
52
+ type: 'number',
53
+ description: 'Optional per-candidate output cap. Defaults to 2048, max 8192.',
54
+ },
55
+ reveal: {
56
+ type: 'boolean',
57
+ description: 'If true, include model identities immediately after the blinded outputs.',
58
+ },
59
+ saveArtifact: {
60
+ type: 'boolean',
61
+ description: 'Defaults true; save the local JSON review artifact.',
62
+ },
63
+ benchmarkKind: {
64
+ type: 'string',
65
+ description: 'Optional benchmark tag for saved comparisons or analytics filters.',
66
+ },
67
+ taskType: {
68
+ type: 'string',
69
+ description: 'Optional task type tag for saved comparisons or analytics filters.',
70
+ },
71
+ documentId: {
72
+ type: 'string',
73
+ description: 'Optional document id tag for saved comparisons or analytics filters.',
74
+ },
75
+ comparisonId: {
76
+ type: 'string',
77
+ description: 'Stored comparison id for mode reveal.',
78
+ },
79
+ artifactId: {
80
+ type: 'string',
81
+ description: 'Run source artifact, saved comparison, or judgment artifact id.',
82
+ },
83
+ leftArtifactId: {
84
+ type: 'string',
85
+ description: 'Left saved reviewer handoff artifact id for handoffDiff.',
86
+ },
87
+ rightArtifactId: {
88
+ type: 'string',
89
+ description: 'Right saved reviewer handoff artifact id for handoffDiff.',
90
+ },
91
+ sectionId: {
92
+ type: 'string',
93
+ description: 'Optional handoffDiff jump: all, metadata, policy, related, comparison.',
94
+ },
95
+ winnerBlindId: {
96
+ type: 'string',
97
+ description: 'Candidate label to save as winner, such as A or Candidate B.',
98
+ },
99
+ winner: {
100
+ type: 'string',
101
+ description: 'Alias for winnerBlindId.',
102
+ },
103
+ reasons: {
104
+ type: 'string',
105
+ description: 'User-visible reasons for the saved judgment.',
106
+ },
107
+ notes: {
108
+ type: 'string',
109
+ description: 'Optional extra judgment notes.',
110
+ },
111
+ decision: {
112
+ type: 'string',
113
+ enum: ['left-unchanged', 'leave-unchanged', 'keep-current', 'no-change', 'applied-winner'],
114
+ description: 'Route-decision receipt choice for routeDecision mode.',
115
+ },
116
+ limit: {
117
+ type: 'number',
118
+ description: 'Max saved judgments to inspect.',
119
+ },
120
+ includeReasons: {
121
+ type: 'boolean',
122
+ description: 'If true, include short reason excerpts.',
123
+ },
124
+ relatedArtifactIds: {
125
+ type: 'array',
126
+ items: { type: 'string' },
127
+ description: 'Related artifacts for reviewer view or handoff.',
128
+ },
129
+ previewBytes: {
130
+ type: 'number',
131
+ description: 'Max bytes per related artifact preview.',
132
+ },
133
+ confirm: {
134
+ type: 'boolean',
135
+ description: 'Required true for provider calls and mutating modes.',
136
+ },
137
+ explicitUserRequest: {
138
+ type: 'string',
139
+ description: 'User request authorizing this comparison.',
140
+ },
141
+ },
142
+ required: ['mode'],
143
+ additionalProperties: false,
144
+ },
145
+ sideEffects: ['network', 'state'],
146
+ concurrency: 'serial',
147
+ },
148
+ execute: async (rawArgs: Record<string, unknown>) => {
149
+ try {
150
+ const args = rawArgs as AgentModelCompareToolArgs;
151
+ const mode = parseMode(args.mode);
152
+ if (mode === MODE_REVEAL || mode === MODE_REVIEW) {
153
+ const comparisonId = readString(args.comparisonId);
154
+ const artifactId = readString(args.artifactId);
155
+ if (mode === MODE_REVIEW && !comparisonId && !artifactId) {
156
+ return output(formatSavedComparisonArtifacts(deps.artifactStore));
157
+ }
158
+ if (!comparisonId && !artifactId) {
159
+ return failure(`${mode} mode requires comparisonId or artifactId.`);
160
+ }
161
+ const comparison = await resolveComparisonForRead({
162
+ artifactStore: deps.artifactStore,
163
+ comparisons,
164
+ comparisonId,
165
+ artifactId,
166
+ });
167
+ if (!comparison) {
168
+ return failure(`Unknown comparison. Run a new comparison or pass a saved comparison artifactId.`);
169
+ }
170
+ rememberComparison(comparisons, comparison);
171
+ return output(mode === MODE_REVEAL ? formatReveal(comparison) : formatReview(comparison, readBoolean(args.reveal)));
172
+ }
173
+
174
+ if (mode === MODE_SIDE_BY_SIDE) {
175
+ const artifactId = readString(args.artifactId);
176
+ if (!artifactId) {
177
+ return output([
178
+ formatSavedComparisonArtifacts(deps.artifactStore),
179
+ '',
180
+ 'Choose a saved comparison or judgment artifactId to render a side-by-side reviewer view.',
181
+ ].join('\n'));
182
+ }
183
+ if (!deps.artifactStore?.readContent) {
184
+ return failure('Side-by-side reviewer view is unavailable because this runtime cannot read artifact content.');
185
+ }
186
+ const relatedArtifactIds = readStringList(args.relatedArtifactIds).filter((relatedId) => relatedId !== artifactId);
187
+ const previewBytes = clamp(
188
+ readNumber(args.previewBytes, DEFAULT_SIDE_BY_SIDE_PREVIEW_BYTES),
189
+ 200,
190
+ MAX_SIDE_BY_SIDE_PREVIEW_BYTES,
191
+ );
192
+ const comparison = await loadComparisonFromArtifact(deps.artifactStore, artifactId);
193
+ if (comparison) {
194
+ const relatedArtifacts = await loadHandoffRelatedArtifacts(deps.artifactStore, relatedArtifactIds, previewBytes);
195
+ rememberComparison(comparisons, comparison);
196
+ return output(formatSideBySideReview({
197
+ sourceKind: 'comparison',
198
+ sourceArtifactId: artifactId,
199
+ comparisonId: comparison.comparisonId,
200
+ comparison,
201
+ reveal: readBoolean(args.reveal),
202
+ relatedArtifacts,
203
+ previewBytes,
204
+ }));
205
+ }
206
+ const judgment = await loadJudgmentFromArtifact(deps.artifactStore, artifactId);
207
+ if (!judgment) return failure('Unknown comparison or judgment artifact. Pass a saved blind model comparison artifactId.');
208
+ const relatedArtifacts = await loadHandoffRelatedArtifacts(deps.artifactStore, relatedArtifactIds, previewBytes);
209
+ return output(formatSideBySideReview({
210
+ sourceKind: 'judgment',
211
+ sourceArtifactId: artifactId,
212
+ comparisonId: judgment.comparisonId,
213
+ judgment,
214
+ reveal: judgment.revealIncludedInJudgment,
215
+ relatedArtifacts,
216
+ previewBytes,
217
+ }));
218
+ }
219
+
220
+ if (mode === MODE_HANDOFF_DIFF) {
221
+ const leftArtifactId = readString(args.leftArtifactId || args.artifactId);
222
+ const rightArtifactId = readString(args.rightArtifactId);
223
+ if (!leftArtifactId && !rightArtifactId) {
224
+ return output([
225
+ formatSavedHandoffArtifacts(deps.artifactStore),
226
+ '',
227
+ 'Choose two saved reviewer handoff artifact ids with leftArtifactId and rightArtifactId to render a visual diff.',
228
+ ].join('\n'));
229
+ }
230
+ if (!leftArtifactId || !rightArtifactId) {
231
+ return failure('handoffDiff mode requires leftArtifactId and rightArtifactId.');
232
+ }
233
+ if (leftArtifactId === rightArtifactId) {
234
+ return failure('handoffDiff mode requires two different reviewer handoff artifact ids.');
235
+ }
236
+ if (!deps.artifactStore?.readContent) {
237
+ return failure('Reviewer handoff diff is unavailable because this runtime cannot read artifact content.');
238
+ }
239
+ const left = await loadHandoffDiffArtifact(deps.artifactStore, leftArtifactId);
240
+ const right = await loadHandoffDiffArtifact(deps.artifactStore, rightArtifactId);
241
+ if (!left || !right) {
242
+ return failure('Unknown reviewer handoff artifact. Pass two saved blind model comparison handoff artifact ids.');
243
+ }
244
+ return output(formatHandoffDiff({ left, right, sectionId: readString(args.sectionId) }));
245
+ }
246
+
247
+ if (mode === MODE_ANALYTICS) {
248
+ const limit = clamp(readNumber(args.limit, 20), 1, 100);
249
+ const filters = readComparisonAnalyticsFilters(args);
250
+ const judgments = await loadSavedJudgments(deps.artifactStore, limit, filters);
251
+ return output(formatComparisonAnalytics({
252
+ judgments,
253
+ limit,
254
+ includeReasons: readOptionalBoolean(args.includeReasons, true),
255
+ filters,
256
+ storeAvailable: Boolean(deps.artifactStore?.list && deps.artifactStore.readContent),
257
+ }));
258
+ }
259
+
260
+ if (mode === MODE_SYNTHESIS) {
261
+ const limit = clamp(readNumber(args.limit, 20), 1, 100);
262
+ const filters = readComparisonAnalyticsFilters(args);
263
+ const judgments = await loadSavedJudgments(deps.artifactStore, limit, filters);
264
+ return output(formatComparisonSynthesis({
265
+ judgments,
266
+ limit,
267
+ includeReasons: readOptionalBoolean(args.includeReasons, true),
268
+ filters,
269
+ storeAvailable: Boolean(deps.artifactStore?.list && deps.artifactStore.readContent),
270
+ }));
271
+ }
272
+
273
+ if (mode === MODE_JUDGE) {
274
+ const comparisonId = readString(args.comparisonId);
275
+ const artifactId = readString(args.artifactId);
276
+ const winnerBlindId = readString(args.winnerBlindId ?? args.winner);
277
+ const reasons = readString(args.reasons);
278
+ const explicitUserRequest = readString(args.explicitUserRequest);
279
+ if (!comparisonId && !artifactId) return failure('judge mode requires comparisonId or artifactId.');
280
+ if (!winnerBlindId) return failure('winnerBlindId is required for judge mode.');
281
+ if (!reasons) return failure('reasons are required for judge mode.');
282
+ if (!explicitUserRequest) {
283
+ return failure('explicitUserRequest is required so saved judgments stay tied to a direct user request.');
284
+ }
285
+ if (!readBoolean(args.confirm)) {
286
+ return failure([
287
+ formatJudgePreview(args),
288
+ '',
289
+ 'Judgment confirmation required. Call this tool with confirm:true only when the user explicitly asked GoodVibes Agent to save this judgment.',
290
+ ].join('\n'));
291
+ }
292
+ const comparison = await resolveComparisonForRead({
293
+ artifactStore: deps.artifactStore,
294
+ comparisons,
295
+ comparisonId,
296
+ artifactId,
297
+ });
298
+ if (!comparison) return failure('Unknown comparison. Run a new comparison or pass a saved comparison artifactId.');
299
+ const winner = findCandidate(comparison, winnerBlindId);
300
+ if (!winner) {
301
+ return failure(`Unknown winnerBlindId ${winnerBlindId}. Available candidates: ${comparison.candidates.map((candidate) => candidate.blindId).join(', ')}.`);
302
+ }
303
+ const artifact = await saveComparisonJudgmentArtifact({
304
+ artifactStore: deps.artifactStore,
305
+ comparison,
306
+ winner,
307
+ reasons,
308
+ notes: readString(args.notes),
309
+ reveal: readBoolean(args.reveal),
310
+ });
311
+ rememberComparison(comparisons, comparison);
312
+ return output(formatJudgmentResult({
313
+ comparison,
314
+ winner,
315
+ artifact,
316
+ reasons,
317
+ reveal: readBoolean(args.reveal),
318
+ }));
319
+ }
320
+
321
+ if (mode === MODE_APPLY) {
322
+ const artifactId = readString(args.artifactId);
323
+ const explicitUserRequest = readString(args.explicitUserRequest);
324
+ if (!artifactId) return failure('apply mode requires a saved judgment artifactId.');
325
+ if (!explicitUserRequest) {
326
+ return failure('explicitUserRequest is required so route updates stay tied to a direct user request.');
327
+ }
328
+ if (!deps.applyModelRoute) {
329
+ return failure('Model route updates are unavailable in this runtime. Use settings action:"set" for provider.model if available.');
330
+ }
331
+ if (!deps.artifactStore?.readContent) {
332
+ return failure('Saved judgment artifacts are unavailable because this runtime cannot read artifact content.');
333
+ }
334
+ const judgment = await loadJudgmentFromArtifact(deps.artifactStore, artifactId);
335
+ if (!judgment) return failure('Unknown judgment artifact. Pass a saved model comparison judgment artifactId.');
336
+ if (!judgment.revealIncludedInJudgment || !judgment.winnerModel?.registryKey) {
337
+ return failure('Judgment artifact does not include a revealed winner model. Save or reveal the judgment before applying a route update.');
338
+ }
339
+ if (!readBoolean(args.confirm)) {
340
+ return failure([
341
+ formatApplyPreview(judgment),
342
+ '',
343
+ 'Route update confirmation required. Call this tool with confirm:true only when the user explicitly asked GoodVibes Agent to apply this winning model.',
344
+ ].join('\n'));
345
+ }
346
+ await ensureSelectableWinnerModel(deps.modelCatalog, judgment.winnerModel.registryKey);
347
+ const result = await deps.applyModelRoute(judgment.winnerModel.registryKey);
348
+ await deps.modelCatalog.recordModelUsage?.(judgment.winnerModel.registryKey);
349
+ let receipt: SavedComparisonArtifact | null = null;
350
+ let receiptError: string | null = null;
351
+ try {
352
+ receipt = await saveRouteDecisionArtifact({
353
+ artifactStore: deps.artifactStore,
354
+ decision: 'applied-winner',
355
+ judgment,
356
+ route: result,
357
+ explicitUserRequest,
358
+ });
359
+ } catch (error) {
360
+ receiptError = error instanceof Error ? error.message : String(error);
361
+ }
362
+ return output(formatApplyResult({ judgment, result, receipt, receiptError }));
363
+ }
364
+
365
+ if (mode === MODE_ROUTE_DECISION) {
366
+ const artifactId = readString(args.artifactId);
367
+ const explicitUserRequest = readString(args.explicitUserRequest);
368
+ const decision = parseRouteDecision(args.decision);
369
+ if (!artifactId) return failure('routeDecision mode requires a saved judgment artifactId.');
370
+ if (!explicitUserRequest) {
371
+ return failure('explicitUserRequest is required so route-decision receipts stay tied to a direct user request.');
372
+ }
373
+ if (decision !== 'left-unchanged') {
374
+ return failure('routeDecision mode records leave-unchanged decisions. Use mode:"apply" to apply a revealed winner.');
375
+ }
376
+ if (!deps.artifactStore?.readContent || !deps.artifactStore.create) {
377
+ return failure('Route-decision receipts are unavailable because this runtime cannot read and create artifact content.');
378
+ }
379
+ const judgment = await loadJudgmentFromArtifact(deps.artifactStore, artifactId);
380
+ if (!judgment) return failure('Unknown judgment artifact. Pass a saved model comparison judgment artifactId.');
381
+ if (!judgment.revealIncludedInJudgment || !judgment.winnerModel?.registryKey) {
382
+ return failure('Judgment artifact does not include a revealed winner model. Save or reveal the judgment before recording a route decision.');
383
+ }
384
+ const currentModel = (await resolveCurrentModel(deps.modelCatalog))?.registryKey ?? null;
385
+ if (!readBoolean(args.confirm)) {
386
+ return failure([
387
+ formatRouteDecisionPreview({ judgment, decision, currentModel }),
388
+ '',
389
+ 'Route-decision confirmation required. Call this tool with confirm:true only when the user explicitly asked GoodVibes Agent to leave the current model route unchanged.',
390
+ ].join('\n'));
391
+ }
392
+ const receipt = await saveRouteDecisionArtifact({
393
+ artifactStore: deps.artifactStore,
394
+ decision,
395
+ judgment,
396
+ currentModel,
397
+ explicitUserRequest,
398
+ });
399
+ return output(formatRouteDecisionResult({ judgment, decision, receipt, currentModel }));
400
+ }
401
+
402
+ if (mode === MODE_EXPORT) {
403
+ const artifactId = readString(args.artifactId);
404
+ const explicitUserRequest = readString(args.explicitUserRequest);
405
+ if (!artifactId) return failure('export mode requires a saved comparison or judgment artifactId.');
406
+ if (!explicitUserRequest) {
407
+ return failure('explicitUserRequest is required so exports stay tied to a direct user request.');
408
+ }
409
+ if (!deps.artifactStore?.readContent || !deps.artifactStore.create) {
410
+ return failure('Saved comparison export is unavailable because this runtime cannot read and create artifact content.');
411
+ }
412
+
413
+ const comparison = await loadComparisonFromArtifact(deps.artifactStore, artifactId);
414
+ if (comparison) {
415
+ const reveal = readBoolean(args.reveal);
416
+ if (!readBoolean(args.confirm)) {
417
+ return failure([
418
+ formatExportPreview({
419
+ sourceKind: 'comparison',
420
+ sourceArtifactId: artifactId,
421
+ comparisonId: comparison.comparisonId,
422
+ reveal,
423
+ }),
424
+ '',
425
+ 'Export confirmation required. Call this tool with confirm:true only when the user explicitly asked GoodVibes Agent to create this markdown report.',
426
+ ].join('\n'));
427
+ }
428
+ const artifact = await saveComparisonExportArtifact({
429
+ artifactStore: deps.artifactStore,
430
+ sourceArtifactId: artifactId,
431
+ sourceKind: 'comparison',
432
+ comparisonId: comparison.comparisonId,
433
+ markdown: comparisonExportMarkdown(comparison, reveal),
434
+ reveal,
435
+ });
436
+ rememberComparison(comparisons, comparison);
437
+ return output(formatExportResult({
438
+ sourceKind: 'comparison',
439
+ sourceArtifactId: artifactId,
440
+ comparisonId: comparison.comparisonId,
441
+ artifact,
442
+ }));
443
+ }
444
+
445
+ const judgment = await loadJudgmentFromArtifact(deps.artifactStore, artifactId);
446
+ if (!judgment) return failure('Unknown comparison or judgment artifact. Pass a saved blind model comparison artifactId.');
447
+ const reveal = judgment.revealIncludedInJudgment;
448
+ if (!readBoolean(args.confirm)) {
449
+ return failure([
450
+ formatExportPreview({
451
+ sourceKind: 'judgment',
452
+ sourceArtifactId: artifactId,
453
+ comparisonId: judgment.comparisonId,
454
+ reveal,
455
+ }),
456
+ '',
457
+ 'Export confirmation required. Call this tool with confirm:true only when the user explicitly asked GoodVibes Agent to create this markdown report.',
458
+ ].join('\n'));
459
+ }
460
+ const artifact = await saveComparisonExportArtifact({
461
+ artifactStore: deps.artifactStore,
462
+ sourceArtifactId: artifactId,
463
+ sourceKind: 'judgment',
464
+ comparisonId: judgment.comparisonId,
465
+ markdown: judgmentExportMarkdown(judgment),
466
+ reveal,
467
+ });
468
+ return output(formatExportResult({
469
+ sourceKind: 'judgment',
470
+ sourceArtifactId: artifactId,
471
+ comparisonId: judgment.comparisonId,
472
+ artifact,
473
+ }));
474
+ }
475
+
476
+ if (mode === MODE_HANDOFF) {
477
+ const artifactId = readString(args.artifactId);
478
+ const explicitUserRequest = readString(args.explicitUserRequest);
479
+ const relatedArtifactIds = readStringList(args.relatedArtifactIds).filter((relatedId) => relatedId !== artifactId);
480
+ if (!artifactId) return failure('handoff mode requires a saved comparison or judgment artifactId.');
481
+ if (relatedArtifactIds.length === 0) return failure('handoff mode requires at least one relatedArtifactIds entry.');
482
+ if (!explicitUserRequest) {
483
+ return failure('explicitUserRequest is required so reviewer handoffs stay tied to a direct user request.');
484
+ }
485
+ if (!deps.artifactStore?.readContent || !deps.artifactStore.create) {
486
+ return failure('Reviewer handoff is unavailable because this runtime cannot read and create artifact content.');
487
+ }
488
+
489
+ const comparison = await loadComparisonFromArtifact(deps.artifactStore, artifactId);
490
+ if (comparison) {
491
+ const reveal = readBoolean(args.reveal);
492
+ if (!readBoolean(args.confirm)) {
493
+ return failure([
494
+ formatHandoffPreview({
495
+ sourceKind: 'comparison',
496
+ sourceArtifactId: artifactId,
497
+ comparisonId: comparison.comparisonId,
498
+ reveal,
499
+ relatedArtifactIds,
500
+ }),
501
+ '',
502
+ 'Handoff confirmation required. Call this tool with confirm:true only when the user explicitly asked GoodVibes Agent to create this reviewer handoff.',
503
+ ].join('\n'));
504
+ }
505
+ const relatedArtifacts = await loadHandoffRelatedArtifacts(deps.artifactStore, relatedArtifactIds);
506
+ const artifact = await saveComparisonHandoffArtifact({
507
+ artifactStore: deps.artifactStore,
508
+ sourceArtifactId: artifactId,
509
+ sourceKind: 'comparison',
510
+ comparisonId: comparison.comparisonId,
511
+ relatedArtifactIds,
512
+ markdown: comparisonHandoffMarkdown({
513
+ sourceKind: 'comparison',
514
+ sourceArtifactId: artifactId,
515
+ comparisonId: comparison.comparisonId,
516
+ comparisonMarkdown: comparisonExportMarkdown(comparison, reveal),
517
+ reveal,
518
+ relatedArtifacts,
519
+ }),
520
+ reveal,
521
+ });
522
+ rememberComparison(comparisons, comparison);
523
+ return output(formatHandoffResult({
524
+ sourceKind: 'comparison',
525
+ sourceArtifactId: artifactId,
526
+ comparisonId: comparison.comparisonId,
527
+ relatedArtifactCount: relatedArtifacts.length,
528
+ artifact,
529
+ }));
530
+ }
531
+
532
+ const judgment = await loadJudgmentFromArtifact(deps.artifactStore, artifactId);
533
+ if (!judgment) return failure('Unknown comparison or judgment artifact. Pass a saved blind model comparison artifactId.');
534
+ const reveal = judgment.revealIncludedInJudgment;
535
+ if (!readBoolean(args.confirm)) {
536
+ return failure([
537
+ formatHandoffPreview({
538
+ sourceKind: 'judgment',
539
+ sourceArtifactId: artifactId,
540
+ comparisonId: judgment.comparisonId,
541
+ reveal,
542
+ relatedArtifactIds,
543
+ }),
544
+ '',
545
+ 'Handoff confirmation required. Call this tool with confirm:true only when the user explicitly asked GoodVibes Agent to create this reviewer handoff.',
546
+ ].join('\n'));
547
+ }
548
+ const relatedArtifacts = await loadHandoffRelatedArtifacts(deps.artifactStore, relatedArtifactIds);
549
+ const artifact = await saveComparisonHandoffArtifact({
550
+ artifactStore: deps.artifactStore,
551
+ sourceArtifactId: artifactId,
552
+ sourceKind: 'judgment',
553
+ comparisonId: judgment.comparisonId,
554
+ relatedArtifactIds,
555
+ markdown: comparisonHandoffMarkdown({
556
+ sourceKind: 'judgment',
557
+ sourceArtifactId: artifactId,
558
+ comparisonId: judgment.comparisonId,
559
+ comparisonMarkdown: judgmentExportMarkdown(judgment),
560
+ reveal,
561
+ relatedArtifacts,
562
+ }),
563
+ reveal,
564
+ });
565
+ return output(formatHandoffResult({
566
+ sourceKind: 'judgment',
567
+ sourceArtifactId: artifactId,
568
+ comparisonId: judgment.comparisonId,
569
+ relatedArtifactCount: relatedArtifacts.length,
570
+ artifact,
571
+ }));
572
+ }
573
+
574
+ if (mode === MODE_HANDOFF_ARCHIVE) {
575
+ const artifactId = readString(args.artifactId);
576
+ const explicitUserRequest = readString(args.explicitUserRequest);
577
+ if (!artifactId) return output(formatSavedHandoffArtifacts(deps.artifactStore));
578
+ if (!explicitUserRequest) {
579
+ return failure('explicitUserRequest is required so reviewer handoff archives stay tied to a direct user request.');
580
+ }
581
+ if (!deps.artifactStore?.readContent || !deps.artifactStore.create) {
582
+ return failure('Reviewer handoff archive is unavailable because this runtime cannot read and create artifact content.');
583
+ }
584
+
585
+ const handoff = await loadHandoffFromArtifact(deps.artifactStore, artifactId);
586
+ if (!handoff) return failure('Unknown reviewer handoff artifact. Pass a saved blind model comparison handoff artifactId.');
587
+ const routeDecisionArtifactIds = findRouteDecisionArtifactIdsForHandoff(deps.artifactStore, handoff);
588
+ if (!readBoolean(args.confirm)) {
589
+ return failure([
590
+ formatHandoffArchivePreview({ handoff, routeDecisionArtifactIds }),
591
+ '',
592
+ 'Handoff archive confirmation required. Call this tool with confirm:true only when the user explicitly asked GoodVibes Agent to create this reviewer handoff ZIP.',
593
+ ].join('\n'));
594
+ }
595
+
596
+ const artifacts = await loadHandoffArchiveArtifacts(deps.artifactStore, handoff, routeDecisionArtifactIds);
597
+ const payload = buildComparisonHandoffArchivePayload({ handoff, artifacts });
598
+ const archive = createZipArchive(payload.entries);
599
+ const artifact = await saveComparisonHandoffArchiveArtifact({
600
+ artifactStore: deps.artifactStore,
601
+ handoff,
602
+ payload,
603
+ archive,
604
+ });
605
+ return output(formatHandoffArchiveResult({
606
+ handoff,
607
+ artifact,
608
+ artifactCount: payload.artifactCount,
609
+ routeDecisionArtifactCount: payload.routeDecisionArtifactIds.length,
610
+ sourceBytes: payload.sourceBytes,
611
+ archiveBytes: archive.byteLength,
612
+ }));
613
+ }
614
+
615
+ const promptInput = readString(args.prompt);
616
+ const explicitUserRequest = readString(args.explicitUserRequest);
617
+ const sourceArtifactId = readString(args.artifactId);
618
+ const refs = readModelRefs(args.modelRefs);
619
+ const benchmarkKind = readBenchmarkKind(args.benchmarkKind);
620
+ const taskType = readComparisonTag(args.taskType);
621
+ const requestedDocumentId = readComparisonTag(args.documentId);
622
+ const candidateCount = clamp(readNumber(args.candidateCount, DEFAULT_CANDIDATE_COUNT), MIN_CANDIDATES, MAX_CANDIDATES);
623
+ if (!promptInput && !sourceArtifactId) return failure('prompt or artifactId is required.');
624
+ if (!explicitUserRequest) {
625
+ return failure('explicitUserRequest is required so model comparison stays tied to a direct user request.');
626
+ }
627
+ if (!readBoolean(args.confirm)) {
628
+ return failure([
629
+ formatPreview(args, refs, candidateCount),
630
+ '',
631
+ 'Model tool confirmation required. Call this tool with confirm:true only when the user explicitly asked GoodVibes Agent to run this comparison.',
632
+ ].join('\n'));
633
+ }
634
+ const runPrompt = await buildRunPromptFromArtifact({
635
+ artifactStore: deps.artifactStore,
636
+ prompt: promptInput,
637
+ artifactId: sourceArtifactId,
638
+ });
639
+ const prompt = runPrompt.prompt;
640
+ if (prompt.length > MAX_PROMPT_CHARS) return failure(`prompt exceeds ${MAX_PROMPT_CHARS} characters.`);
641
+
642
+ const maxTokens = clamp(readNumber(args.maxTokens, DEFAULT_MAX_TOKENS), 1, MAX_COMPLETION_TOKENS);
643
+ const systemPrompt = readString(args.systemPrompt);
644
+ const models = await selectComparisonModels(deps.modelCatalog, refs, candidateCount);
645
+ const results = await Promise.all(models.map((model, index) => runCandidate(
646
+ deps,
647
+ model,
648
+ BLIND_LABELS[index] ?? String(index + 1),
649
+ prompt,
650
+ systemPrompt,
651
+ maxTokens,
652
+ )));
653
+ const reveal = readBoolean(args.reveal);
654
+ const baseComparison: StoredComparison = {
655
+ comparisonId: `cmp_${randomUUID()}`,
656
+ createdAt: new Date().toISOString(),
657
+ promptPreview: previewText(prompt, 160),
658
+ rubric: readString(args.rubric),
659
+ ...(runPrompt.sourceArtifact ? { sourceArtifact: runPrompt.sourceArtifact } : {}),
660
+ ...(benchmarkKind ? { benchmarkKind } : {}),
661
+ ...(taskType ? { taskType } : {}),
662
+ ...(requestedDocumentId || runPrompt.sourceArtifact?.documentId ? { documentId: requestedDocumentId || runPrompt.sourceArtifact?.documentId } : {}),
663
+ candidates: results,
664
+ };
665
+ const saved = await saveComparisonArtifact({
666
+ artifactStore: deps.artifactStore,
667
+ comparison: baseComparison,
668
+ prompt,
669
+ systemPrompt,
670
+ maxTokens,
671
+ revealIncludedInTranscript: reveal,
672
+ enabled: readOptionalBoolean(args.saveArtifact, true),
673
+ ...(benchmarkKind ? { benchmarkKind } : {}),
674
+ });
675
+ const comparison: StoredComparison = {
676
+ ...baseComparison,
677
+ ...(saved.artifact ? { artifact: saved.artifact } : {}),
678
+ artifactStatus: saved.status,
679
+ };
680
+ rememberComparison(comparisons, comparison);
681
+ await Promise.allSettled(models.map((model) => deps.modelCatalog.recordModelUsage?.(model.registryKey)));
682
+ const rendered = formatRunResult(comparison, reveal);
683
+ return results.some((candidate) => candidate.status === 'completed')
684
+ ? output(rendered)
685
+ : failure(rendered);
686
+ } catch (error) {
687
+ return failure(error instanceof Error ? error.message : String(error));
688
+ }
689
+ },
690
+ };
691
+ }
692
+
693
+ export function registerAgentModelCompareTool(
694
+ registry: ToolRegistry,
695
+ deps: AgentModelCompareToolDeps,
696
+ ): void {
697
+ registry.register(createAgentModelCompareTool(deps));
698
+ }