@pellux/goodvibes-agent 1.1.7 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/CHANGELOG.md +329 -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 +138903 -91477
  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 +58 -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 +116 -45
  14. package/docs/voice-and-live-tts.md +3 -1
  15. package/package.json +3 -2
  16. package/release/release-notes.md +142 -5
  17. package/release/release-readiness.json +124 -94
  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 +294 -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-artifact-receipts.ts +366 -0
  42. package/src/agent/setup-wizard-checkpoint.ts +140 -0
  43. package/src/agent/setup-wizard.ts +683 -0
  44. package/src/agent/skill-registry-types.ts +102 -0
  45. package/src/agent/skill-registry.ts +81 -107
  46. package/src/agent/vibe-confirmation-routes.ts +62 -0
  47. package/src/agent/vibe-file.ts +285 -0
  48. package/src/cli/agent-knowledge-command.ts +45 -2
  49. package/src/cli/help.ts +3 -0
  50. package/src/cli/profiles-command.ts +25 -11
  51. package/src/config/agent-settings-policy.ts +1 -18
  52. package/src/input/agent-workspace-activation.ts +53 -0
  53. package/src/input/agent-workspace-artifact-browser-editor.ts +494 -0
  54. package/src/input/agent-workspace-artifact-metadata.ts +17 -0
  55. package/src/input/agent-workspace-basic-command-editors.ts +5 -1
  56. package/src/input/agent-workspace-categories.ts +153 -155
  57. package/src/input/agent-workspace-channel-triage.ts +481 -0
  58. package/src/input/agent-workspace-channels.ts +263 -0
  59. package/src/input/agent-workspace-command-editor.ts +152 -0
  60. package/src/input/agent-workspace-context-snapshot.ts +349 -0
  61. package/src/input/agent-workspace-delegation-editor-submission.ts +8 -0
  62. package/src/input/agent-workspace-document-editor.ts +502 -0
  63. package/src/input/agent-workspace-document-ops-editor.ts +523 -0
  64. package/src/input/agent-workspace-host-category.ts +1 -1
  65. package/src/input/agent-workspace-local-library-snapshot.ts +167 -0
  66. package/src/input/agent-workspace-model-compare-editor.ts +376 -0
  67. package/src/input/agent-workspace-model-compare-prompt-submission.ts +552 -0
  68. package/src/input/agent-workspace-model-compare-types.ts +82 -0
  69. package/src/input/agent-workspace-model-compare-utils.ts +35 -0
  70. package/src/input/agent-workspace-onboarding-categories.ts +141 -0
  71. package/src/input/agent-workspace-operations-command-editor-submission.ts +53 -0
  72. package/src/input/agent-workspace-operations-command-editors.ts +22 -0
  73. package/src/input/agent-workspace-research-report-editor.ts +212 -0
  74. package/src/input/agent-workspace-research-run-editor.ts +144 -0
  75. package/src/input/agent-workspace-research-source-editor.ts +167 -0
  76. package/src/input/agent-workspace-review-packet-snapshot.ts +593 -0
  77. package/src/input/agent-workspace-review-packet-utils.ts +265 -0
  78. package/src/input/agent-workspace-settings.ts +313 -30
  79. package/src/input/agent-workspace-setup-checkpoint-action.ts +127 -0
  80. package/src/input/agent-workspace-setup-snapshot.ts +217 -0
  81. package/src/input/agent-workspace-setup.ts +61 -1
  82. package/src/input/agent-workspace-snapshot.ts +184 -149
  83. package/src/input/agent-workspace-types.ts +287 -2
  84. package/src/input/agent-workspace.ts +23 -1
  85. package/src/input/command-registry.ts +9 -2
  86. package/src/input/commands/agent-runtime-profile-runtime.ts +28 -13
  87. package/src/input/commands/channels-runtime.ts +102 -114
  88. package/src/input/commands/delegation-runtime.ts +87 -24
  89. package/src/input/commands/knowledge-browser-flags.ts +12 -0
  90. package/src/input/commands/knowledge.ts +28 -14
  91. package/src/input/commands/operator-actions-runtime.ts +6 -3
  92. package/src/input/commands/schedule-runtime.ts +71 -14
  93. package/src/input/commands/session-content.ts +2 -2
  94. package/src/input/commands/shell-core.ts +2 -0
  95. package/src/input/commands/vibe-runtime.ts +140 -0
  96. package/src/input/commands.ts +2 -0
  97. package/src/input/connected-host-routes.ts +123 -0
  98. package/src/input/settings-modal-types.ts +14 -0
  99. package/src/input/setup-wizard-live-receipts.ts +76 -0
  100. package/src/main.ts +15 -15
  101. package/src/panels/qr-panel.ts +2 -2
  102. package/src/renderer/agent-workspace-context-lines.ts +701 -0
  103. package/src/renderer/agent-workspace.ts +68 -421
  104. package/src/renderer/settings-modal-helpers.ts +12 -0
  105. package/src/renderer/settings-modal.ts +12 -0
  106. package/src/runtime/bootstrap-command-context.ts +11 -1
  107. package/src/runtime/bootstrap-command-parts.ts +14 -3
  108. package/src/runtime/bootstrap-core.ts +39 -1
  109. package/src/runtime/bootstrap-shell.ts +6 -0
  110. package/src/runtime/bootstrap.ts +113 -30
  111. package/src/runtime/connected-host-auth.ts +3 -2
  112. package/src/runtime/execution-ledger.ts +231 -0
  113. package/src/runtime/services.ts +88 -10
  114. package/src/runtime/tool-permission-safety.ts +199 -5
  115. package/src/shell/session-continuity-hints.ts +39 -0
  116. package/src/tools/agent-artifacts-tool.ts +606 -0
  117. package/src/tools/agent-audit-tool.ts +155 -0
  118. package/src/tools/agent-autonomy-schedule-tool.ts +238 -0
  119. package/src/tools/agent-autonomy-tool.ts +140 -0
  120. package/src/tools/agent-channel-send-tool.ts +24 -2
  121. package/src/tools/agent-channels-tool.ts +140 -0
  122. package/src/tools/agent-computer-tool.ts +168 -0
  123. package/src/tools/agent-context-policy.ts +137 -7
  124. package/src/tools/agent-context-tool.ts +143 -0
  125. package/src/tools/agent-delegation-tool.ts +128 -0
  126. package/src/tools/agent-device-tool.ts +240 -0
  127. package/src/tools/agent-documents-tool.ts +684 -0
  128. package/src/tools/agent-execution-tool.ts +230 -0
  129. package/src/tools/agent-harness-agent-orchestration-policy.ts +75 -0
  130. package/src/tools/agent-harness-agent-orchestration.ts +791 -0
  131. package/src/tools/agent-harness-autonomy-intake.ts +785 -0
  132. package/src/tools/agent-harness-autonomy-live-records.ts +742 -0
  133. package/src/tools/agent-harness-autonomy-queue-types.ts +88 -0
  134. package/src/tools/agent-harness-autonomy-queue.ts +571 -0
  135. package/src/tools/agent-harness-autonomy-watcher-read-models.ts +509 -0
  136. package/src/tools/agent-harness-background-processes-types.ts +35 -0
  137. package/src/tools/agent-harness-background-processes.ts +798 -0
  138. package/src/tools/agent-harness-browser-cockpit-route.ts +373 -0
  139. package/src/tools/agent-harness-browser-control.ts +346 -0
  140. package/src/tools/agent-harness-browser-pwa-read-models.ts +622 -0
  141. package/src/tools/agent-harness-channel-metadata.ts +168 -10
  142. package/src/tools/agent-harness-cli-command-policy.ts +110 -0
  143. package/src/tools/agent-harness-command-catalog.ts +4 -2
  144. package/src/tools/agent-harness-command-runner.ts +42 -0
  145. package/src/tools/agent-harness-connected-host-status.ts +7 -4
  146. package/src/tools/agent-harness-delegation-posture.ts +234 -19
  147. package/src/tools/agent-harness-device-live-read-models.ts +366 -0
  148. package/src/tools/agent-harness-document-ops-reviewer-readiness.ts +234 -0
  149. package/src/tools/agent-harness-document-ops-types.ts +72 -0
  150. package/src/tools/agent-harness-document-ops.ts +671 -0
  151. package/src/tools/agent-harness-execution-history.ts +489 -0
  152. package/src/tools/agent-harness-execution-posture.ts +385 -0
  153. package/src/tools/agent-harness-file-recovery.ts +135 -0
  154. package/src/tools/agent-harness-interactive-runtime-records.ts +421 -0
  155. package/src/tools/agent-harness-keybinding-metadata.ts +16 -12
  156. package/src/tools/agent-harness-learning-curator-common.ts +102 -0
  157. package/src/tools/agent-harness-learning-curator-consolidation.ts +295 -0
  158. package/src/tools/agent-harness-learning-curator-proposals.ts +606 -0
  159. package/src/tools/agent-harness-learning-curator-types.ts +151 -0
  160. package/src/tools/agent-harness-learning-curator.ts +417 -0
  161. package/src/tools/agent-harness-local-model-benchmarks.ts +269 -0
  162. package/src/tools/agent-harness-local-model-cookbook.ts +238 -0
  163. package/src/tools/agent-harness-local-model-endpoints.ts +788 -0
  164. package/src/tools/agent-harness-local-model-smoke.ts +277 -0
  165. package/src/tools/agent-harness-local-model-url.ts +78 -0
  166. package/src/tools/agent-harness-media-posture.ts +223 -2
  167. package/src/tools/agent-harness-memory-external-providers.ts +796 -0
  168. package/src/tools/agent-harness-memory-posture.ts +672 -0
  169. package/src/tools/agent-harness-memory-provider-certification.ts +219 -0
  170. package/src/tools/agent-harness-memory-refinement.ts +340 -0
  171. package/src/tools/agent-harness-metadata.ts +100 -168
  172. package/src/tools/agent-harness-mode-catalog.ts +86 -33
  173. package/src/tools/agent-harness-model-catalog.ts +162 -0
  174. package/src/tools/agent-harness-model-provider-health.ts +283 -0
  175. package/src/tools/agent-harness-model-readiness.ts +432 -0
  176. package/src/tools/agent-harness-model-routing-types.ts +327 -0
  177. package/src/tools/agent-harness-model-routing-utils.ts +30 -0
  178. package/src/tools/agent-harness-model-routing.ts +165 -193
  179. package/src/tools/agent-harness-operator-methods.ts +115 -133
  180. package/src/tools/agent-harness-pairing-posture.ts +452 -18
  181. package/src/tools/agent-harness-personal-ops-certification.ts +116 -0
  182. package/src/tools/agent-harness-personal-ops-discovery.ts +533 -0
  183. package/src/tools/agent-harness-personal-ops-intake.ts +385 -0
  184. package/src/tools/agent-harness-personal-ops-lanes.ts +270 -0
  185. package/src/tools/agent-harness-personal-ops-operations.ts +225 -0
  186. package/src/tools/agent-harness-personal-ops-provider-records.ts +358 -0
  187. package/src/tools/agent-harness-personal-ops-provider-task-records.ts +321 -0
  188. package/src/tools/agent-harness-personal-ops-records.ts +684 -0
  189. package/src/tools/agent-harness-personal-ops-runner.ts +637 -0
  190. package/src/tools/agent-harness-personal-ops-types.ts +240 -0
  191. package/src/tools/agent-harness-personal-ops.ts +613 -0
  192. package/src/tools/agent-harness-project-context.ts +144 -0
  193. package/src/tools/agent-harness-prompt-context.ts +589 -0
  194. package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
  195. package/src/tools/agent-harness-release-evidence.ts +9 -7
  196. package/src/tools/agent-harness-release-readiness.ts +9 -7
  197. package/src/tools/agent-harness-remote-read-models.ts +541 -0
  198. package/src/tools/agent-harness-research-briefing.ts +446 -0
  199. package/src/tools/agent-harness-research-live-read-models.ts +500 -0
  200. package/src/tools/agent-harness-research-queue.ts +250 -0
  201. package/src/tools/agent-harness-research-runs.ts +307 -0
  202. package/src/tools/agent-harness-research-workflow.ts +395 -0
  203. package/src/tools/agent-harness-security-posture.ts +2 -2
  204. package/src/tools/agent-harness-service-posture.ts +9 -9
  205. package/src/tools/agent-harness-session-metadata.ts +3 -3
  206. package/src/tools/agent-harness-setup-connected-host.ts +501 -0
  207. package/src/tools/agent-harness-setup-describe.ts +557 -0
  208. package/src/tools/agent-harness-setup-handoffs.ts +502 -0
  209. package/src/tools/agent-harness-setup-model-helpers.ts +102 -0
  210. package/src/tools/agent-harness-setup-plan.ts +253 -0
  211. package/src/tools/agent-harness-setup-posture-types.ts +218 -0
  212. package/src/tools/agent-harness-setup-posture-utils.ts +173 -0
  213. package/src/tools/agent-harness-setup-posture.ts +449 -221
  214. package/src/tools/agent-harness-setup-smoke.ts +533 -0
  215. package/src/tools/agent-harness-sudo-posture.ts +114 -0
  216. package/src/tools/agent-harness-tool-schema.ts +142 -7
  217. package/src/tools/agent-harness-tool-types.ts +83 -0
  218. package/src/tools/agent-harness-tool-utils.ts +43 -0
  219. package/src/tools/agent-harness-tool.ts +319 -370
  220. package/src/tools/agent-harness-ui-surface-metadata.ts +76 -69
  221. package/src/tools/agent-harness-ui-surface-types.ts +46 -0
  222. package/src/tools/agent-harness-vibe-health.ts +105 -0
  223. package/src/tools/agent-harness-workspace-action-runner.ts +149 -0
  224. package/src/tools/agent-harness-workspace-actions.ts +114 -8
  225. package/src/tools/agent-harness-workspace-editor-execution.ts +214 -0
  226. package/src/tools/agent-harness-workspace-editor-runner.ts +755 -0
  227. package/src/tools/agent-host-tool.ts +159 -0
  228. package/src/tools/agent-knowledge-ingest-tool.ts +34 -1
  229. package/src/tools/agent-learning-consolidation-core.ts +327 -0
  230. package/src/tools/agent-learning-consolidation-tool.ts +758 -0
  231. package/src/tools/agent-memory-tool.ts +253 -0
  232. package/src/tools/agent-model-compare-export.ts +315 -0
  233. package/src/tools/agent-model-compare-handoff.ts +592 -0
  234. package/src/tools/agent-model-compare-judgment.ts +633 -0
  235. package/src/tools/agent-model-compare-run.ts +735 -0
  236. package/src/tools/agent-model-compare-tool.ts +698 -0
  237. package/src/tools/agent-model-compare-types.ts +191 -0
  238. package/src/tools/agent-model-compare-utils.ts +93 -0
  239. package/src/tools/agent-models-tool.ts +208 -0
  240. package/src/tools/agent-operator-action-tool.ts +1 -1
  241. package/src/tools/agent-operator-method-tool.ts +643 -0
  242. package/src/tools/agent-personal-ops-tool.ts +197 -0
  243. package/src/tools/agent-policy-explanation.ts +415 -0
  244. package/src/tools/agent-research-report-tool.ts +608 -0
  245. package/src/tools/agent-research-runner.ts +367 -0
  246. package/src/tools/agent-research-runs-tool.ts +434 -0
  247. package/src/tools/agent-research-sources-tool.ts +443 -0
  248. package/src/tools/agent-research-tool.ts +515 -0
  249. package/src/tools/agent-review-packet-presets-core.ts +757 -0
  250. package/src/tools/agent-review-packet-presets-tool.ts +466 -0
  251. package/src/tools/agent-review-packet-share-tool.ts +305 -0
  252. package/src/tools/agent-route-planner-candidates-setup.ts +411 -0
  253. package/src/tools/agent-route-planner-candidates-surfaces.ts +432 -0
  254. package/src/tools/agent-route-planner-candidates-work.ts +251 -0
  255. package/src/tools/agent-route-planner-helpers.ts +667 -0
  256. package/src/tools/agent-route-planner.ts +185 -0
  257. package/src/tools/agent-route-tool.ts +105 -0
  258. package/src/tools/agent-schedule-edit-tool.ts +210 -0
  259. package/src/tools/agent-schedule-tool.ts +282 -0
  260. package/src/tools/agent-security-tool.ts +145 -0
  261. package/src/tools/agent-sessions-tool.ts +122 -0
  262. package/src/tools/agent-settings-import-tool.ts +104 -0
  263. package/src/tools/agent-settings-tool.ts +176 -0
  264. package/src/tools/agent-setup-tool.ts +226 -0
  265. package/src/tools/agent-support-tool.ts +122 -0
  266. package/src/tools/agent-terminal-process-tools.ts +167 -0
  267. package/src/tools/agent-tool-policy-guard-types.ts +77 -0
  268. package/src/tools/agent-tool-policy-guard.ts +106 -85
  269. package/src/tools/agent-vibe-tool.ts +297 -0
  270. package/src/tools/agent-work-plan-tool.ts +265 -6
  271. package/src/tools/agent-workspace-tool.ts +305 -0
  272. package/src/tools/artifact-archive.ts +169 -0
  273. package/src/tools/tool-definition-compaction.ts +36 -0
  274. package/src/version.ts +1 -1
@@ -0,0 +1,277 @@
1
+ import type { CommandContext } from '../input/command-registry.ts';
2
+ import type { AgentHarnessModelRoutingArgs, LocalModelServerDefaultEndpoint, LocalModelServerEndpoint, LocalModelSmokeTarget } from './agent-harness-model-routing-types.ts';
3
+ import { previewHarnessText } from './agent-harness-text.ts';
4
+ import { readLimit, readRecord, readString } from './agent-harness-model-routing-utils.ts';
5
+ import { collectLocalServerEndpointCandidates, describeLocalServerEndpoint, isPrivateOrLocalUrl, localEndpointSmokeRoute, localModelServerDefaults, parseUrlCandidate } from './agent-harness-local-model-endpoints.ts';
6
+
7
+ function localModelSmokeTargetFromEndpoint(endpoint: LocalModelServerEndpoint): LocalModelSmokeTarget {
8
+ return {
9
+ kind: endpoint.kind,
10
+ id: endpoint.id,
11
+ label: `Local model server ${endpoint.baseUrl}`,
12
+ providerId: endpoint.providerId,
13
+ stack: endpoint.stack,
14
+ baseUrl: endpoint.baseUrl,
15
+ modelsUrl: endpoint.modelsUrl,
16
+ smokeCommand: endpoint.smokeCommand,
17
+ smokeRoute: endpoint.smokeRoute,
18
+ refreshRoute: endpoint.refreshRoute,
19
+ addProviderRoute: endpoint.addProviderRoute,
20
+ source: endpoint.sources.join(', ') || 'local-endpoint',
21
+ notes: endpoint.notes,
22
+ };
23
+ }
24
+
25
+ function localModelSmokeTargetFromDefault(endpoint: LocalModelServerDefaultEndpoint): LocalModelSmokeTarget {
26
+ return {
27
+ kind: 'suggested-local-server',
28
+ id: endpoint.id,
29
+ label: endpoint.label,
30
+ providerId: null,
31
+ stack: endpoint.stack,
32
+ baseUrl: endpoint.baseUrl,
33
+ modelsUrl: endpoint.modelsUrl,
34
+ smokeCommand: endpoint.smokeCommand,
35
+ smokeRoute: localEndpointSmokeRoute(endpoint.id),
36
+ refreshRoute: 'agent_harness mode:"run_command" command:"/refresh-models" confirm:true explicitUserRequest:"Refresh models after verifying the local server."',
37
+ addProviderRoute: endpoint.addProviderRoute,
38
+ source: 'suggested-default',
39
+ notes: [endpoint.startHint],
40
+ };
41
+ }
42
+
43
+ function localSmokeTargetSearchText(target: LocalModelSmokeTarget): string {
44
+ return [
45
+ target.kind,
46
+ target.id,
47
+ target.label,
48
+ target.providerId ?? '',
49
+ target.stack ?? '',
50
+ target.baseUrl,
51
+ target.modelsUrl,
52
+ target.source,
53
+ ...target.notes,
54
+ ].join('\n').toLowerCase();
55
+ }
56
+
57
+ function localModelSmokeLookup(args: AgentHarnessModelRoutingArgs): string {
58
+ const fields = readRecord(args.fields);
59
+ return readString(args.modelRouteId)
60
+ || readString(args.target)
61
+ || readString(args.query)
62
+ || readString(fields.endpointId)
63
+ || readString(fields.modelRouteId)
64
+ || readString(fields.baseUrl)
65
+ || readString(fields.modelsUrl);
66
+ }
67
+
68
+ function localModelSmokeTargets(context: CommandContext, args: AgentHarnessModelRoutingArgs): Record<string, unknown> | readonly LocalModelSmokeTarget[] {
69
+ const endpoints = collectLocalServerEndpointCandidates(context)
70
+ .map((endpoint) => localModelSmokeTargetFromEndpoint(describeLocalServerEndpoint(endpoint, true)));
71
+ const defaults = localModelServerDefaults().map(localModelSmokeTargetFromDefault);
72
+ const lookup = localModelSmokeLookup(args);
73
+ const allTargets = [...endpoints, ...defaults];
74
+ if (lookup) {
75
+ const normalized = lookup.toLowerCase();
76
+ const exact = allTargets.filter((target) => target.id === lookup || target.baseUrl === lookup || target.modelsUrl === lookup);
77
+ if (exact.length === 1) return exact;
78
+ if (exact.length > 1) {
79
+ return {
80
+ status: 'ambiguous',
81
+ input: lookup,
82
+ candidates: exact.slice(0, 8).map((target) => ({
83
+ kind: target.kind,
84
+ id: target.id,
85
+ label: target.label,
86
+ baseUrl: target.baseUrl,
87
+ modelsUrl: target.modelsUrl,
88
+ })),
89
+ };
90
+ }
91
+ const searched = allTargets.filter((target) => localSmokeTargetSearchText(target).includes(normalized));
92
+ if (searched.length === 1) return searched;
93
+ if (searched.length > 1) {
94
+ return {
95
+ status: 'ambiguous',
96
+ input: lookup,
97
+ candidates: searched.slice(0, 8).map((target) => ({
98
+ kind: target.kind,
99
+ id: target.id,
100
+ label: target.label,
101
+ baseUrl: target.baseUrl,
102
+ modelsUrl: target.modelsUrl,
103
+ })),
104
+ };
105
+ }
106
+ return {
107
+ status: 'missing_lookup',
108
+ input: lookup,
109
+ usage: 'Unknown local model endpoint. Use models action:"local" includeParameters:true to inspect local endpoint ids, or omit the lookup to check detected/default local servers.',
110
+ };
111
+ }
112
+ const pool = endpoints.length ? endpoints : defaults;
113
+ return pool.slice(0, readLimit(args.limit, 4));
114
+ }
115
+
116
+ function readSmokeTimeoutMs(value: unknown): number {
117
+ const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
118
+ if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return 1500;
119
+ return Math.max(250, Math.min(10000, Math.trunc(parsed)));
120
+ }
121
+
122
+ function localSmokeNetworkScope(modelsUrl: string): { readonly allowed: boolean; readonly scope: string; readonly reason?: string } {
123
+ const url = parseUrlCandidate(modelsUrl);
124
+ if (!url || !/^https?:$/.test(url.protocol)) return { allowed: false, scope: 'invalid-url', reason: 'The model-list URL is not a valid HTTP(S) URL.' };
125
+ const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
126
+ if (host === '0.0.0.0') {
127
+ return { allowed: false, scope: 'bind-all-host', reason: '0.0.0.0 is a bind address, not a client URL. Use 127.0.0.1 or the intended LAN host.' };
128
+ }
129
+ if (!isPrivateOrLocalUrl(url.href)) {
130
+ return { allowed: false, scope: 'non-local-host', reason: 'Local model smoke only probes loopback, local-name, or private LAN endpoints.' };
131
+ }
132
+ if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return { allowed: true, scope: 'loopback' };
133
+ if (host.endsWith('.local') || !host.includes('.')) return { allowed: true, scope: 'local-name' };
134
+ return { allowed: true, scope: 'private-lan' };
135
+ }
136
+
137
+ function extractModelIdsFromPayload(payload: unknown): readonly string[] {
138
+ const record = readRecord(payload);
139
+ const candidates = Array.isArray(record.data)
140
+ ? record.data
141
+ : Array.isArray(record.models)
142
+ ? record.models
143
+ : Array.isArray(payload)
144
+ ? payload
145
+ : [];
146
+ const ids = candidates.map((entry) => {
147
+ if (typeof entry === 'string') return entry;
148
+ const item = readRecord(entry);
149
+ return readString(item.id) || readString(item.name) || readString(item.model);
150
+ }).filter(Boolean);
151
+ return [...new Set(ids)].slice(0, 12);
152
+ }
153
+
154
+ function safeSmokeError(error: unknown): string {
155
+ const message = error instanceof Error ? error.message : String(error);
156
+ return previewHarnessText(message.replace(/https?:\/\/\S+/g, '[redacted-url]'), 180);
157
+ }
158
+
159
+ async function smokeOneLocalModelTarget(target: LocalModelSmokeTarget, timeoutMs: number): Promise<Record<string, unknown>> {
160
+ const network = localSmokeNetworkScope(target.modelsUrl);
161
+ if (!network.allowed) {
162
+ return {
163
+ ...target,
164
+ status: 'blocked',
165
+ liveProbe: 'confirmed',
166
+ networkScope: network.scope,
167
+ failure: network.reason,
168
+ nextActions: ['Inspect the endpoint route and correct the base URL before running smoke again.'],
169
+ };
170
+ }
171
+
172
+ const controller = new AbortController();
173
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
174
+ const started = Date.now();
175
+ try {
176
+ const response = await fetch(target.modelsUrl, {
177
+ method: 'GET',
178
+ headers: { Accept: 'application/json' },
179
+ signal: controller.signal,
180
+ });
181
+ const elapsedMs = Date.now() - started;
182
+ const contentType = response.headers.get('content-type') ?? '';
183
+ const text = await response.text();
184
+ let payload: unknown = null;
185
+ let jsonValid = false;
186
+ try {
187
+ payload = text ? JSON.parse(text) : null;
188
+ jsonValid = true;
189
+ } catch {
190
+ jsonValid = false;
191
+ }
192
+ const modelIds = jsonValid ? extractModelIdsFromPayload(payload) : [];
193
+ const status = !response.ok
194
+ ? 'http-error'
195
+ : !jsonValid
196
+ ? 'invalid-json'
197
+ : modelIds.length === 0
198
+ ? 'no-models'
199
+ : 'passed';
200
+ return {
201
+ ...target,
202
+ status,
203
+ liveProbe: 'confirmed',
204
+ networkScope: network.scope,
205
+ httpStatus: response.status,
206
+ contentType,
207
+ elapsedMs,
208
+ jsonValid,
209
+ modelCount: modelIds.length,
210
+ sampleModelIds: modelIds.slice(0, 5),
211
+ success: status === 'passed',
212
+ nextActions: status === 'passed'
213
+ ? ['Refresh the model catalog, then run a local benchmark before changing the default model.']
214
+ : ['Start or fix the local server, confirm /v1/models returns model ids, then retry this smoke check.'],
215
+ };
216
+ } catch (error) {
217
+ const elapsedMs = Date.now() - started;
218
+ const aborted = controller.signal.aborted;
219
+ return {
220
+ ...target,
221
+ status: aborted ? 'timeout' : 'unreachable',
222
+ liveProbe: 'confirmed',
223
+ networkScope: network.scope,
224
+ elapsedMs,
225
+ timeoutMs,
226
+ success: false,
227
+ failure: aborted ? `Timed out after ${timeoutMs}ms.` : safeSmokeError(error),
228
+ nextActions: ['Start the local server, load at least one model, verify the base URL, then retry this smoke check.'],
229
+ };
230
+ } finally {
231
+ clearTimeout(timeout);
232
+ }
233
+ }
234
+
235
+ export async function runLocalModelServerSmoke(context: CommandContext, args: AgentHarnessModelRoutingArgs): Promise<Record<string, unknown>> {
236
+ const targets = localModelSmokeTargets(context, args);
237
+ if (!Array.isArray(targets)) {
238
+ return {
239
+ kind: 'local-model-smoke',
240
+ liveProbe: 'not-run',
241
+ ...targets,
242
+ policy: 'No local model endpoint was probed because the requested endpoint lookup did not resolve exactly.',
243
+ };
244
+ }
245
+ if (targets.length === 0) {
246
+ return {
247
+ kind: 'local-model-smoke',
248
+ status: 'no-candidates',
249
+ liveProbe: 'not-run',
250
+ endpoints: [],
251
+ nextActions: ['Use the local model cookbook to start a local server or configure a local provider endpoint.'],
252
+ cookbookRoute: 'models action:"local" includeParameters:true',
253
+ policy: 'No local model endpoint was probed because no candidate endpoints were available.',
254
+ };
255
+ }
256
+ const timeoutMs = readSmokeTimeoutMs(args.timeoutMs);
257
+ const checkedAt = new Date().toISOString();
258
+ const results = await Promise.all(targets.map((target) => smokeOneLocalModelTarget(target, timeoutMs)));
259
+ const passed = results.filter((result) => result.success === true);
260
+ const blocked = results.filter((result) => result.status === 'blocked');
261
+ return {
262
+ kind: 'local-model-smoke',
263
+ status: passed.length > 0 ? 'ready' : blocked.length === results.length ? 'blocked' : 'needs-attention',
264
+ liveProbe: 'confirmed',
265
+ checkedAt,
266
+ timeoutMs,
267
+ endpointCount: results.length,
268
+ passedCount: passed.length,
269
+ failedCount: results.length - passed.length,
270
+ endpoints: results,
271
+ nextActions: passed.length > 0
272
+ ? ['Refresh the model catalog and run the local benchmark action before changing the default route.']
273
+ : ['Start a local model server, load one model, and rerun this confirmed smoke check.'],
274
+ cookbookRoute: 'models action:"local" includeParameters:true',
275
+ policy: 'Confirmed read-only local model smoke. Agent only sends bounded GET requests to discovered or suggested local/private model-list endpoints; it does not add providers, refresh catalogs, benchmark, download models, or change routes.',
276
+ };
277
+ }
@@ -0,0 +1,78 @@
1
+ export function localStackFor(value: string): string | null {
2
+ const normalized = value.toLowerCase();
3
+ if (/ollama[-_\s]?cloud/.test(normalized)) return null;
4
+ if (/\bollama\b/.test(normalized)) return 'ollama';
5
+ if (/llama[.-]?cpp|llamacpp/.test(normalized)) return 'llama.cpp';
6
+ if (/\bvllm\b/.test(normalized)) return 'vllm';
7
+ if (/lm[-_\s]?studio/.test(normalized)) return 'openai-compatible';
8
+ if (/localai|text-generation-inference|\btgi\b/.test(normalized)) return 'openai-compatible';
9
+ if (/localhost|127\.0\.0\.1|\[?::1\]?/.test(normalized)) return 'openai-compatible';
10
+ if (/openai-compatible|openai compatible|custom-provider|custom provider/.test(normalized)) return 'openai-compatible';
11
+ return null;
12
+ }
13
+
14
+ export function cleanUrlCandidate(value: string): string {
15
+ return value.trim().replace(/[),.;]+$/g, '');
16
+ }
17
+
18
+ export function extractUrls(value: string): readonly string[] {
19
+ const matches = value.match(/https?:\/\/[^\s"'`<>]+/gi) ?? [];
20
+ return [...new Set(matches.map(cleanUrlCandidate).filter(Boolean))];
21
+ }
22
+
23
+ export function parseUrlCandidate(raw: string): URL | null {
24
+ const trimmed = cleanUrlCandidate(raw);
25
+ if (!trimmed) return null;
26
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
27
+ try {
28
+ return new URL(withScheme);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export function isPrivateOrLocalHost(hostname: string): boolean {
35
+ const host = hostname.replace(/^\[|\]$/g, '').toLowerCase();
36
+ if (!host) return false;
37
+ if (host === 'localhost' || host === '0.0.0.0' || host === '::1') return true;
38
+ if (host.endsWith('.local')) return true;
39
+ if (host.includes(':')) return host.startsWith('fc') || host.startsWith('fd') || host.startsWith('fe80:');
40
+ const octets = host.split('.').map((entry) => Number(entry));
41
+ if (octets.length === 4 && octets.every((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 255)) {
42
+ const [first, second] = octets as [number, number, number, number];
43
+ return first === 10
44
+ || first === 127
45
+ || (first === 172 && second >= 16 && second <= 31)
46
+ || (first === 192 && second === 168)
47
+ || (first === 169 && second === 254)
48
+ || first === 0;
49
+ }
50
+ return !host.includes('.');
51
+ }
52
+
53
+ export function isPrivateOrLocalUrl(raw: string): boolean {
54
+ const url = parseUrlCandidate(raw);
55
+ if (!url || !/^https?:$/.test(url.protocol)) return false;
56
+ return isPrivateOrLocalHost(url.hostname);
57
+ }
58
+
59
+ export function normalizeLocalBaseUrl(raw: string, stackHint?: string | null): string | null {
60
+ const url = parseUrlCandidate(raw);
61
+ if (!url || !/^https?:$/.test(url.protocol)) return null;
62
+ const stack = stackHint ?? localStackFor(raw) ?? (isPrivateOrLocalHost(url.hostname) ? 'openai-compatible' : null);
63
+ if (!isPrivateOrLocalUrl(url.href)) return null;
64
+
65
+ let pathname = url.pathname.replace(/\/+$/g, '');
66
+ if (pathname.endsWith('/models')) pathname = pathname.slice(0, -'/models'.length);
67
+ if (pathname.endsWith('/api/tags')) pathname = pathname.slice(0, -'/api/tags'.length);
68
+ const needsOpenAiPath = stack === 'ollama' || stack === 'llama.cpp' || stack === 'vllm' || stack === 'openai-compatible';
69
+ if (needsOpenAiPath && (!pathname || pathname === '/')) pathname = '/v1';
70
+ url.pathname = pathname || '';
71
+ url.search = '';
72
+ url.hash = '';
73
+ return url.toString().replace(/\/+$/g, '');
74
+ }
75
+
76
+ export function modelsUrlFor(baseUrl: string): string {
77
+ return `${baseUrl.replace(/\/+$/g, '')}/models`;
78
+ }
@@ -1,6 +1,7 @@
1
1
  import { buildAgentWorkspaceVoiceMediaReadiness } from '../input/agent-workspace-voice-media.ts';
2
2
  import type { AgentWorkspaceVoiceMediaProviderStatus } from '../input/agent-workspace-voice-media.ts';
3
3
  import type { CommandContext } from '../input/command-registry.ts';
4
+ import { certifiedDeviceLiveRecords, deviceLiveReadModelSnapshot } from './agent-harness-device-live-read-models.ts';
4
5
  import { previewHarnessText } from './agent-harness-text.ts';
5
6
 
6
7
  export interface AgentHarnessMediaArgs {
@@ -16,6 +17,8 @@ type MediaProviderResolution =
16
17
  | { readonly status: 'ambiguous'; readonly input: string; readonly candidates: readonly Record<string, unknown>[] }
17
18
  | { readonly status: 'missing_lookup'; readonly usage: string };
18
19
 
20
+ type VoiceWorkflowStatus = 'ready' | 'attention' | 'setup-needed' | 'not-published';
21
+
19
22
  interface RuntimeProviderStatus {
20
23
  readonly id: string;
21
24
  readonly state: string;
@@ -23,6 +26,21 @@ interface RuntimeProviderStatus {
23
26
  readonly detail?: string;
24
27
  }
25
28
 
29
+ interface VoiceInteractionWorkflow {
30
+ readonly id: string;
31
+ readonly label: string;
32
+ readonly status: VoiceWorkflowStatus;
33
+ readonly userOutcome: string;
34
+ readonly summary: string;
35
+ readonly nextStep: string;
36
+ readonly capabilities: readonly string[];
37
+ readonly modelRoute: string;
38
+ readonly userRoute?: string;
39
+ readonly setupRoutes: readonly string[];
40
+ readonly evidence: Record<string, unknown>;
41
+ readonly policy: string;
42
+ }
43
+
26
44
  function readString(value: unknown): string {
27
45
  return typeof value === 'string' ? value.trim() : '';
28
46
  }
@@ -70,6 +88,198 @@ function providerSearchText(provider: AgentWorkspaceVoiceMediaProviderStatus, ru
70
88
  ].join('\n').toLowerCase();
71
89
  }
72
90
 
91
+ function voiceWorkflowSearchText(workflow: VoiceInteractionWorkflow): string {
92
+ return [
93
+ workflow.id,
94
+ workflow.label,
95
+ workflow.status,
96
+ workflow.userOutcome,
97
+ workflow.summary,
98
+ workflow.nextStep,
99
+ workflow.modelRoute,
100
+ workflow.userRoute ?? '',
101
+ ...workflow.capabilities,
102
+ ].join('\n').toLowerCase();
103
+ }
104
+
105
+ function voiceWorkflowMatches(workflow: VoiceInteractionWorkflow, query: string): boolean {
106
+ if (!query) return true;
107
+ const text = voiceWorkflowSearchText(workflow);
108
+ if (text.includes(query)) return true;
109
+ const tokens = query.split(/\s+/).map((token) => token.trim()).filter(Boolean);
110
+ return tokens.length > 0 && tokens.every((token) => text.includes(token));
111
+ }
112
+
113
+ function voiceWorkflowSummary(workflows: readonly VoiceInteractionWorkflow[]): Record<string, unknown> {
114
+ return {
115
+ total: workflows.length,
116
+ ready: workflows.filter((workflow) => workflow.status === 'ready').length,
117
+ attention: workflows.filter((workflow) => workflow.status === 'attention').length,
118
+ setupNeeded: workflows.filter((workflow) => workflow.status === 'setup-needed').length,
119
+ notPublished: workflows.filter((workflow) => workflow.status === 'not-published').length,
120
+ primaryNextStep: workflows.find((workflow) => workflow.status !== 'ready')?.nextStep
121
+ ?? 'Voice workflows are ready; keep effects on explicit user-visible routes.',
122
+ };
123
+ }
124
+
125
+ function hasReadyVoiceFeature(readiness: ReturnType<typeof buildReadiness>, featureIds: readonly string[]): boolean {
126
+ return readiness.voiceProviders.some((provider) => (
127
+ provider.setupState === 'ready'
128
+ && featureIds.some((feature) => provider.features.includes(feature))
129
+ ));
130
+ }
131
+
132
+ function hasRegisteredVoiceFeature(readiness: ReturnType<typeof buildReadiness>, featureIds: readonly string[]): boolean {
133
+ return readiness.voiceProviders.some((provider) => featureIds.some((feature) => provider.features.includes(feature)));
134
+ }
135
+
136
+ function buildVoiceInteractionWorkflows(
137
+ context: CommandContext,
138
+ readiness: ReturnType<typeof buildReadiness>,
139
+ ): readonly VoiceInteractionWorkflow[] {
140
+ const liveDevice = deviceLiveReadModelSnapshot(context);
141
+ const pushToTalkRecords = certifiedDeviceLiveRecords(liveDevice, 'push-to-talk', ['push to talk', 'speech input', 'microphone']);
142
+ const transcriptionRecords = certifiedDeviceLiveRecords(liveDevice, 'voice-memo-transcription', ['voice memo', 'speech-to-text', 'audio transcription']);
143
+ const spokenResponseRecords = certifiedDeviceLiveRecords(liveDevice, 'spoken-responses', ['tts', 'spoken response', 'speaker']);
144
+ const wakeRecords = certifiedDeviceLiveRecords(liveDevice, 'wake-and-speak', ['wake word', 'always listening']);
145
+ const voiceEnabled = readConfigBoolean(context, 'ui.voiceEnabled', false);
146
+ const spokenTurnRuntime = typeof context.submitSpokenInput === 'function';
147
+ const stopSpokenOutputRuntime = typeof context.stopSpokenOutput === 'function';
148
+ const transcribeRuntime = typeof context.platform.voiceService?.transcribe === 'function';
149
+ const speechInputReady = hasReadyVoiceFeature(readiness, ['stt', 'realtime']);
150
+ const speechInputRegistered = hasRegisteredVoiceFeature(readiness, ['stt', 'realtime']);
151
+ const selectedTtsReady = readiness.selectedTtsProviderStatus === 'ready';
152
+ const spokenReady = spokenResponseRecords.length > 0 || (spokenTurnRuntime && selectedTtsReady && readiness.ttsVoiceConfigured);
153
+ const spokenAttention = spokenTurnRuntime && selectedTtsReady && !readiness.ttsVoiceConfigured;
154
+ const pushToTalkReady = pushToTalkRecords.length > 0 || (voiceEnabled && spokenTurnRuntime && speechInputReady);
155
+ const pushToTalkAttention = voiceEnabled && (spokenTurnRuntime || speechInputRegistered);
156
+ const transcriptionReady = transcriptionRecords.length > 0 || (transcribeRuntime && speechInputReady);
157
+ const transcriptionAttention = transcribeRuntime || speechInputRegistered;
158
+
159
+ return [
160
+ {
161
+ id: 'push-to-talk',
162
+ label: 'Push-to-talk input',
163
+ status: pushToTalkReady ? 'ready' : pushToTalkAttention ? 'attention' : 'setup-needed',
164
+ userOutcome: 'Speak a short prompt when the local voice surface and speech-input provider are ready.',
165
+ summary: pushToTalkReady
166
+ ? pushToTalkRecords.length > 0 ? 'The SDK/daemon published a certified push-to-talk route with permission-scoped microphone evidence.' : 'Voice surface, spoken-turn runtime, and a ready STT/realtime provider are available.'
167
+ : pushToTalkAttention
168
+ ? 'Some voice input pieces are present, but the full push-to-talk route is not ready.'
169
+ : 'Voice input needs the voice surface plus a ready STT or realtime provider.',
170
+ nextStep: pushToTalkReady
171
+ ? 'Use the visible voice surface when the user asks to speak to the assistant.'
172
+ : 'Review /voice and media provider posture before presenting push-to-talk as available.',
173
+ capabilities: ['push-to-talk', 'speech input', 'stt', 'realtime voice'],
174
+ modelRoute: 'agent_harness mode:"media_posture" query:"push to talk" includeParameters:true',
175
+ userRoute: '/voice review',
176
+ setupRoutes: [
177
+ 'settings action:"get" query:"ui.voiceEnabled" includeParameters:true',
178
+ 'agent_harness mode:"media_posture" query:"stt realtime" includeParameters:true',
179
+ ],
180
+ evidence: {
181
+ voiceSurfaceEnabled: voiceEnabled,
182
+ spokenTurnRuntime,
183
+ speechInputReady,
184
+ speechInputRegistered,
185
+ ...(pushToTalkRecords.length > 0 ? { certifiedLiveRecords: pushToTalkRecords.slice(0, 5) } : {}),
186
+ },
187
+ policy: 'Voice input stays a visible local operator surface; provider setup and transcript submission are separate explicit routes.',
188
+ },
189
+ {
190
+ id: 'voice-memo-transcription',
191
+ label: 'Voice memo transcription',
192
+ status: transcriptionReady ? 'ready' : transcriptionAttention ? 'attention' : 'setup-needed',
193
+ userOutcome: 'Transcribe an audio note only when a speech-to-text provider and runtime route are both present.',
194
+ summary: transcriptionReady
195
+ ? transcriptionRecords.length > 0 ? 'The SDK/daemon published a certified voice memo transcription route.' : 'Speech-to-text provider and voiceService.transcribe are available.'
196
+ : transcriptionAttention
197
+ ? 'A speech-to-text provider or runtime route is present, but transcription is not fully ready.'
198
+ : 'No ready speech-to-text transcription route is available.',
199
+ nextStep: transcriptionReady
200
+ ? 'Route audio transcription through reviewed media or connected-host voice routes when the user supplies audio.'
201
+ : 'Configure a provider with STT capability and confirm the runtime exposes voiceService.transcribe.',
202
+ capabilities: ['voice memo', 'speech-to-text', 'audio transcription'],
203
+ modelRoute: 'agent_harness mode:"media_posture" query:"voice memo transcription" includeParameters:true',
204
+ setupRoutes: [
205
+ 'agent_harness mode:"media_posture" query:"stt" includeParameters:true',
206
+ 'host action:"methods" query:"voice.stt"',
207
+ ],
208
+ evidence: {
209
+ transcribeRuntime,
210
+ speechInputReady,
211
+ speechInputRegistered,
212
+ ...(transcriptionRecords.length > 0 ? { certifiedLiveRecords: transcriptionRecords.slice(0, 5) } : {}),
213
+ },
214
+ policy: 'Audio bytes are not printed into chat; transcription must use reviewed media or connected-host voice routes.',
215
+ },
216
+ {
217
+ id: 'spoken-responses',
218
+ label: 'Spoken responses',
219
+ status: spokenReady ? 'ready' : spokenAttention ? 'attention' : 'setup-needed',
220
+ userOutcome: 'Play an assistant answer aloud with a predictable TTS provider and voice.',
221
+ summary: spokenReady
222
+ ? spokenResponseRecords.length > 0 ? 'The SDK/daemon published a certified spoken-response route.' : 'Spoken-turn runtime, selected TTS provider, and voice setting are ready.'
223
+ : spokenAttention
224
+ ? 'Spoken-turn runtime and provider are ready, but the exact voice is not configured.'
225
+ : 'Spoken responses need a ready selected TTS provider and runtime spoken-turn route.',
226
+ nextStep: spokenReady
227
+ ? 'Use /tts only when the user asks for spoken output.'
228
+ : 'Choose a ready TTS provider and voice, then verify the runtime exposes submitSpokenInput.',
229
+ capabilities: ['tts', 'spoken answer', 'stop spoken output'],
230
+ modelRoute: 'agent_harness mode:"media_posture" query:"spoken responses" includeParameters:true',
231
+ userRoute: '/tts <prompt>',
232
+ setupRoutes: [
233
+ 'agent_harness mode:"open_ui_surface" surfaceId:"tts-provider-picker" confirm:true explicitUserRequest:"..."',
234
+ 'agent_harness mode:"open_ui_surface" surfaceId:"tts-voice-picker" confirm:true explicitUserRequest:"..."',
235
+ ],
236
+ evidence: {
237
+ spokenTurnRuntime,
238
+ stopSpokenOutputRuntime,
239
+ selectedTtsProviderStatus: readiness.selectedTtsProviderStatus,
240
+ ttsVoiceConfigured: readiness.ttsVoiceConfigured,
241
+ ...(spokenResponseRecords.length > 0 ? { certifiedLiveRecords: spokenResponseRecords.slice(0, 5) } : {}),
242
+ },
243
+ policy: 'Spoken turns submit normal assistant prompts and may call model/speech providers; playback stop is local runtime control.',
244
+ },
245
+ {
246
+ id: 'wake-and-speak',
247
+ label: 'Wake and speak',
248
+ status: wakeRecords.length > 0 ? 'ready' : 'not-published',
249
+ userOutcome: 'Use wake-word or always-listening input only after a permission-scoped runtime contract exists.',
250
+ summary: wakeRecords.length > 0
251
+ ? 'The SDK/daemon published certified wake-word evidence with permission scope, receipt metadata, and exact control routes.'
252
+ : 'Wake-word or always-listening voice capture is not published by the current Agent runtime contract.',
253
+ nextStep: wakeRecords.length > 0
254
+ ? 'Inspect the certified wake-word route and keep microphone capture on visible permission controls.'
255
+ : 'Use explicit voice input or /tts until a wake-word route is published with visible permission controls.',
256
+ capabilities: ['wake word', 'always listening', 'permission repair'],
257
+ modelRoute: wakeRecords[0]?.modelRoute ?? 'agent_harness mode:"media_posture" query:"wake word" includeParameters:true',
258
+ setupRoutes: [
259
+ ...wakeRecords.slice(0, 3).map((record) => record.modelRoute),
260
+ 'agent_harness mode:"media_posture" query:"push to talk" includeParameters:true',
261
+ 'agent_harness mode:"pairing_posture" query:"device" includeParameters:true',
262
+ ],
263
+ evidence: {
264
+ publishedByCurrentAgentContract: wakeRecords.length > 0,
265
+ ...(wakeRecords.length > 0 ? { certifiedLiveRecords: wakeRecords.slice(0, 5) } : {}),
266
+ },
267
+ policy: 'Agent does not claim always-listening behavior without an explicit permission-scoped runtime contract and certified SDK/daemon receipt evidence.',
268
+ },
269
+ ];
270
+ }
271
+
272
+ function describeVoiceWorkflow(workflow: VoiceInteractionWorkflow, includeParameters: boolean): Record<string, unknown> {
273
+ if (includeParameters) return { ...workflow };
274
+ return {
275
+ workflowId: workflow.id,
276
+ label: workflow.label,
277
+ status: workflow.status,
278
+ summary: previewHarnessText(workflow.summary),
279
+ modelRoute: workflow.modelRoute,
280
+ };
281
+ }
282
+
73
283
  function describeProviderCandidate(provider: AgentWorkspaceVoiceMediaProviderStatus): Record<string, unknown> {
74
284
  return {
75
285
  mediaProviderId: `${provider.domain}:${provider.id}`,
@@ -124,7 +334,7 @@ function describeProvider(
124
334
  inspectPosture: 'agent_harness mode:"media_posture"',
125
335
  inspectProvider: 'agent_harness mode:"media_provider"',
126
336
  generateMedia: 'agent_media_generate with confirm:true and explicitUserRequest',
127
- ttsSettings: 'agent_harness mode:"settings", mode:"get_setting", mode:"set_setting" for tts.provider, tts.voice, tts.llmProvider, and tts.llmModel',
337
+ ttsSettings: 'settings action:"list|get|set" for tts.provider, tts.voice, tts.llmProvider, and tts.llmModel',
128
338
  },
129
339
  policy: {
130
340
  effect: 'read-only',
@@ -179,12 +389,14 @@ function buildReadiness(context: CommandContext) {
179
389
 
180
390
  export function mediaPostureCatalogStatus(context: CommandContext): Record<string, unknown> {
181
391
  const readiness = buildReadiness(context);
392
+ const voiceWorkflows = buildVoiceInteractionWorkflows(context, readiness);
182
393
  return {
183
394
  modes: ['media_posture', 'media_provider'],
184
395
  voiceProviders: readiness.voiceProviders.length,
185
396
  mediaProviders: readiness.mediaProviders.length,
186
397
  readyVoiceProviders: readiness.readyVoiceProviderCount,
187
398
  readyMediaProviders: readiness.readyMediaProviderCount,
399
+ voiceWorkflowSummary: voiceWorkflowSummary(voiceWorkflows),
188
400
  readOnly: true,
189
401
  };
190
402
  }
@@ -199,9 +411,14 @@ export async function mediaPostureSummary(context: CommandContext, args: AgentHa
199
411
  ...readiness.mediaProviders.map((provider) => ({ provider, runtimeStatus: mediaStatuses.get(provider.id) })),
200
412
  ];
201
413
  const query = readString(args.query).toLowerCase();
414
+ const limit = readLimit(args.limit, 100);
415
+ const voiceWorkflows = buildVoiceInteractionWorkflows(context, readiness);
202
416
  const filtered = providers
203
417
  .filter(({ provider, runtimeStatus }) => !query || providerSearchText(provider, runtimeStatus).includes(query))
204
- .slice(0, readLimit(args.limit, 100));
418
+ .slice(0, limit);
419
+ const filteredVoiceWorkflows = voiceWorkflows
420
+ .filter((workflow) => voiceWorkflowMatches(workflow, query))
421
+ .slice(0, limit);
205
422
  return {
206
423
  status: 'available',
207
424
  summary: {
@@ -215,6 +432,8 @@ export async function mediaPostureSummary(context: CommandContext, args: AgentHa
215
432
  ttsResponseRouteConfigured: readiness.ttsResponseRouteConfigured,
216
433
  voiceSurfaceEnabled: readConfigBoolean(context, 'ui.voiceEnabled', false),
217
434
  browserToolState: readiness.browserToolState,
435
+ voiceWorkflows: voiceWorkflowSummary(voiceWorkflows),
436
+ returnedVoiceWorkflows: filteredVoiceWorkflows.length,
218
437
  artifactStoreAvailable: Boolean(context.platform.artifactStore),
219
438
  ttsProviderSetting: readConfigString(context, 'tts.provider') || null,
220
439
  ttsVoiceSettingConfigured: readConfigString(context, 'tts.voice').length > 0,
@@ -223,6 +442,7 @@ export async function mediaPostureSummary(context: CommandContext, args: AgentHa
223
442
  providers: filtered.map(({ provider, runtimeStatus }) => describeProvider(provider, runtimeStatus, {
224
443
  includeParameters: args.includeParameters === true,
225
444
  })),
445
+ voiceWorkflows: filteredVoiceWorkflows.map((workflow) => describeVoiceWorkflow(workflow, args.includeParameters === true)),
226
446
  returned: filtered.length,
227
447
  total: providers.length,
228
448
  policy: 'Read-only voice/media posture. Media generation, voice enable/disable, TTS setting changes, and bundle export stay confirmation-gated through first-class tools, settings modes, workspace actions, or slash-command mirrors.',
@@ -232,6 +452,7 @@ export async function mediaPostureSummary(context: CommandContext, args: AgentHa
232
452
  singleProviderMode: 'media_provider',
233
453
  ttsProviderPicker: 'agent_harness mode:"open_ui_surface" surfaceId:"tts-provider-picker" confirm:true explicitUserRequest:"..."',
234
454
  ttsVoicePicker: 'agent_harness mode:"open_ui_surface" surfaceId:"tts-voice-picker" confirm:true explicitUserRequest:"..."',
455
+ voiceWorkflowPosture: 'agent_harness mode:"media_posture" query:"push to talk" includeParameters:true',
235
456
  commands: ['/media providers', '/voice review', '/tts <prompt>', '/image <path>'],
236
457
  } } : {}),
237
458
  };