@pellux/goodvibes-agent 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/dist/package/main.js +19016 -12684
  3. package/docs/README.md +3 -3
  4. package/docs/connected-host.md +2 -2
  5. package/docs/getting-started.md +11 -9
  6. package/docs/tools-and-commands.md +12 -10
  7. package/docs/voice-and-live-tts.md +2 -2
  8. package/package.json +1 -1
  9. package/release/release-notes.md +17 -5
  10. package/release/release-readiness.json +36 -36
  11. package/src/agent/competitive-feature-inventory.ts +42 -44
  12. package/src/agent/setup-wizard-artifact-receipts.ts +366 -0
  13. package/src/agent/setup-wizard.ts +245 -9
  14. package/src/input/agent-workspace-categories.ts +2 -2
  15. package/src/input/agent-workspace-onboarding-categories.ts +4 -4
  16. package/src/input/agent-workspace-settings.ts +52 -1
  17. package/src/input/agent-workspace-setup-snapshot.ts +20 -1
  18. package/src/input/agent-workspace-setup.ts +32 -5
  19. package/src/input/agent-workspace-snapshot.ts +23 -3
  20. package/src/input/agent-workspace.ts +5 -1
  21. package/src/input/setup-wizard-live-receipts.ts +76 -0
  22. package/src/renderer/agent-workspace-context-lines.ts +21 -6
  23. package/src/renderer/agent-workspace.ts +46 -10
  24. package/src/runtime/tool-permission-safety.ts +1 -1
  25. package/src/tools/agent-harness-agent-orchestration-policy.ts +75 -0
  26. package/src/tools/agent-harness-agent-orchestration.ts +216 -128
  27. package/src/tools/agent-harness-autonomy-live-records.ts +154 -0
  28. package/src/tools/agent-harness-autonomy-queue-types.ts +1 -1
  29. package/src/tools/agent-harness-autonomy-queue.ts +19 -8
  30. package/src/tools/agent-harness-autonomy-watcher-read-models.ts +509 -0
  31. package/src/tools/agent-harness-background-processes.ts +8 -4
  32. package/src/tools/agent-harness-browser-cockpit-route.ts +188 -34
  33. package/src/tools/agent-harness-browser-control.ts +12 -4
  34. package/src/tools/agent-harness-browser-pwa-read-models.ts +622 -0
  35. package/src/tools/agent-harness-device-live-read-models.ts +366 -0
  36. package/src/tools/agent-harness-execution-posture.ts +3 -0
  37. package/src/tools/agent-harness-interactive-runtime-records.ts +421 -0
  38. package/src/tools/agent-harness-local-model-benchmarks.ts +71 -1
  39. package/src/tools/agent-harness-local-model-endpoints.ts +469 -354
  40. package/src/tools/agent-harness-local-model-smoke.ts +277 -0
  41. package/src/tools/agent-harness-local-model-url.ts +78 -0
  42. package/src/tools/agent-harness-media-posture.ts +27 -12
  43. package/src/tools/agent-harness-memory-external-providers.ts +796 -0
  44. package/src/tools/agent-harness-memory-posture.ts +253 -137
  45. package/src/tools/agent-harness-memory-provider-certification.ts +219 -0
  46. package/src/tools/agent-harness-memory-refinement.ts +340 -0
  47. package/src/tools/agent-harness-mode-catalog.ts +4 -2
  48. package/src/tools/agent-harness-model-provider-health.ts +139 -42
  49. package/src/tools/agent-harness-model-readiness.ts +31 -5
  50. package/src/tools/agent-harness-model-routing-types.ts +61 -0
  51. package/src/tools/agent-harness-model-routing.ts +31 -6
  52. package/src/tools/agent-harness-pairing-posture.ts +30 -9
  53. package/src/tools/agent-harness-personal-ops-certification.ts +116 -0
  54. package/src/tools/agent-harness-personal-ops-lanes.ts +81 -15
  55. package/src/tools/agent-harness-personal-ops-operations.ts +225 -0
  56. package/src/tools/agent-harness-personal-ops-provider-records.ts +358 -0
  57. package/src/tools/agent-harness-personal-ops-provider-task-records.ts +321 -0
  58. package/src/tools/agent-harness-personal-ops-records.ts +176 -224
  59. package/src/tools/agent-harness-personal-ops-types.ts +19 -1
  60. package/src/tools/agent-harness-personal-ops.ts +18 -11
  61. package/src/tools/agent-harness-remote-read-models.ts +541 -0
  62. package/src/tools/agent-harness-research-briefing.ts +26 -7
  63. package/src/tools/agent-harness-research-live-read-models.ts +500 -0
  64. package/src/tools/agent-harness-research-runs.ts +15 -3
  65. package/src/tools/agent-harness-research-workflow.ts +92 -19
  66. package/src/tools/agent-harness-setup-model-helpers.ts +1 -0
  67. package/src/tools/agent-harness-setup-smoke.ts +26 -1
  68. package/src/tools/agent-harness-tool-schema.ts +23 -1
  69. package/src/tools/agent-harness-tool-types.ts +5 -0
  70. package/src/tools/agent-harness-tool.ts +9 -1
  71. package/src/tools/agent-harness-workspace-actions.ts +1 -1
  72. package/src/tools/agent-memory-tool.ts +40 -1
  73. package/src/tools/agent-model-compare-run.ts +13 -0
  74. package/src/tools/agent-research-runner.ts +367 -0
  75. package/src/tools/agent-research-tool.ts +7 -179
  76. package/src/tools/agent-route-planner-candidates-surfaces.ts +1 -1
  77. package/src/version.ts +1 -1
@@ -0,0 +1,367 @@
1
+ import type { Tool } from '@pellux/goodvibes-sdk/platform/types';
2
+ import type { CommandContext } from '../input/command-registry.ts';
3
+ import { AgentResearchRunRegistry, type AgentResearchRunRecord } from '../agent/research-run-registry.ts';
4
+ import { AgentResearchSourceRegistry, researchSourceReportLine, type AgentResearchSourceRecord } from '../agent/research-source-registry.ts';
5
+ import type { AgentResearchToolArgs } from './agent-research-tool.ts';
6
+
7
+ type ToolResult = { readonly success: true; readonly output: string } | { readonly success: false; readonly error: string };
8
+
9
+ function error(message: string): { readonly success: false; readonly error: string } {
10
+ return { success: false, error: message };
11
+ }
12
+
13
+ function readString(value: unknown): string {
14
+ return typeof value === 'string' ? value.trim() : '';
15
+ }
16
+
17
+ function readNumber(value: unknown, fallback: number, max: number): number {
18
+ const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
19
+ if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
20
+ return Math.max(1, Math.min(max, Math.trunc(parsed)));
21
+ }
22
+
23
+ function compactArgs(entries: Record<string, unknown>): Record<string, unknown> {
24
+ return Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== undefined && value !== ''));
25
+ }
26
+
27
+ function searchArgs(args: AgentResearchToolArgs): Record<string, unknown> {
28
+ const safeSearch = readString(args.safeSearch).toLowerCase() === 'strict' ? 'strict' : 'moderate';
29
+ return compactArgs({
30
+ query: args.query ?? args.target,
31
+ providerId: args.providerId,
32
+ maxResults: readNumber(args.maxResults, readNumber(args.limit, 5, 10), 10),
33
+ verbosity: 'evidence',
34
+ region: args.region,
35
+ safeSearch,
36
+ timeRange: args.timeRange,
37
+ includeEvidence: true,
38
+ evidenceTopN: readNumber(args.evidenceTopN, 3, 3),
39
+ evidenceExtract: args.evidenceExtract ?? 'readable',
40
+ });
41
+ }
42
+
43
+ function previewText(value: unknown, limit: number): string {
44
+ const text = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
45
+ if (text.length <= limit) return text;
46
+ return `${text.slice(0, Math.max(0, limit - 3)).trimEnd()}...`;
47
+ }
48
+
49
+ function quote(value: string): string {
50
+ return JSON.stringify(value);
51
+ }
52
+
53
+ function fallbackTitle(question: string): string {
54
+ const cleaned = question.trim().replace(/\s+/g, ' ');
55
+ if (!cleaned) return 'Deep research run';
56
+ return cleaned.length <= 72 ? cleaned : `${cleaned.slice(0, 71).trimEnd()}...`;
57
+ }
58
+
59
+ function readRecord(value: unknown): Record<string, unknown> | null {
60
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
61
+ }
62
+
63
+ function readRecordArray(value: unknown): readonly Record<string, unknown>[] {
64
+ return Array.isArray(value) ? value.map(readRecord).filter((entry): entry is Record<string, unknown> => Boolean(entry)) : [];
65
+ }
66
+
67
+ function hostname(value: string): string {
68
+ try {
69
+ return new URL(value).hostname.replace(/^www\./i, '');
70
+ } catch {
71
+ return '';
72
+ }
73
+ }
74
+
75
+ function sanitizeUrlForRoute(value: string): string {
76
+ try {
77
+ const url = new URL(value);
78
+ for (const key of [...url.searchParams.keys()]) {
79
+ if (/token|secret|password|authorization|credential|api[-_]?key/i.test(key)) url.searchParams.set(key, '<redacted>');
80
+ }
81
+ return url.toString();
82
+ } catch {
83
+ return value.replace(/([?&\s](?:token|secret|password|authorization|credential|api[-_]?key)=)[^\s&]+/gi, '$1<redacted>');
84
+ }
85
+ }
86
+
87
+ function candidateFromSearchResult(question: string, result: Record<string, unknown>, index: number): Record<string, unknown> {
88
+ const rawUrl = readString(result.url);
89
+ const url = sanitizeUrlForRoute(rawUrl);
90
+ const title = previewText(result.title, 120) || hostname(rawUrl) || `Search result ${index + 1}`;
91
+ const publisher = previewText(result.domain, 80) || hostname(rawUrl);
92
+ const evidenceRecords = readRecordArray(result.evidence);
93
+ const evidenceText = previewText(
94
+ evidenceRecords.map((entry) => readString(entry.content)).filter(Boolean).join('\n\n') || result.snippet,
95
+ 420,
96
+ );
97
+ const summary = previewText(result.snippet, 260) || previewText(evidenceText, 260) || `Public web result for ${question}.`;
98
+ const captureArgs = compactArgs({
99
+ action: 'add_source',
100
+ question,
101
+ title,
102
+ url,
103
+ publisher,
104
+ summary,
105
+ evidence: evidenceText,
106
+ credibility: 'unreviewed',
107
+ tags: ['research', 'web-search'],
108
+ confirm: true,
109
+ explicitUserRequest: '...',
110
+ });
111
+ return {
112
+ rank: typeof result.rank === 'number' ? result.rank : index + 1,
113
+ title,
114
+ url,
115
+ ...(publisher ? { publisher } : {}),
116
+ summary,
117
+ ...(evidenceText ? { evidencePreview: evidenceText } : {}),
118
+ captureArgs,
119
+ captureRoute: [
120
+ 'research action:"add_source"',
121
+ `question:${quote(question)}`,
122
+ `title:${quote(title)}`,
123
+ ...(url ? [`url:${quote(url)}`] : []),
124
+ ...(publisher ? [`publisher:${quote(publisher)}`] : []),
125
+ `summary:${quote(summary)}`,
126
+ ...(evidenceText ? [`evidence:${quote(evidenceText)}`] : []),
127
+ 'credibility:"unreviewed"',
128
+ 'tags:["research","web-search"]',
129
+ 'confirm:true',
130
+ 'explicitUserRequest:"..."',
131
+ ].join(' '),
132
+ };
133
+ }
134
+
135
+ function parseSearchPayload(outputValue: unknown): Record<string, unknown> | null {
136
+ if (typeof outputValue !== 'string') return readRecord(outputValue);
137
+ try {
138
+ return readRecord(JSON.parse(outputValue));
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+
144
+ function resolveSearchRun(args: AgentResearchToolArgs, shellPaths: CommandContext['workspace']['shellPaths'] | undefined): AgentResearchRunRecord | null {
145
+ if (!shellPaths) return null;
146
+ const lookup = readString(args.runId) || readString(args.id);
147
+ if (!lookup) return null;
148
+ return AgentResearchRunRegistry.fromShellPaths(shellPaths).get(lookup);
149
+ }
150
+
151
+ export async function runPublicSearch(
152
+ webSearchTool: Tool | undefined,
153
+ args: AgentResearchToolArgs,
154
+ shellPaths: CommandContext['workspace']['shellPaths'] | undefined,
155
+ ): Promise<ToolResult> {
156
+ const run = resolveSearchRun(args, shellPaths);
157
+ const query = readString(args.query) || readString(args.question) || run?.question || readString(args.target);
158
+ if ((readString(args.runId) || readString(args.id)) && !run && !readString(args.query) && !readString(args.question)) {
159
+ return error(`Unknown research run ${readString(args.runId) || readString(args.id)}. Use research action:"runs" to inspect run ids.`);
160
+ }
161
+ if (!query) return error('research action:"search" requires query or target.');
162
+ if (!webSearchTool) {
163
+ return error('Bounded public web search is unavailable because web_search is not registered. Use research action:"briefing" for the offline next-action queue or action:"plan" for the route plan.');
164
+ }
165
+ const webArgs = searchArgs({ ...args, query });
166
+ const webResult = await webSearchTool.execute(webArgs);
167
+ if (!webResult.success) return { success: false, error: readString(webResult.error) || 'web_search failed.' };
168
+ const payload = parseSearchPayload(webResult.output);
169
+ const results = readRecordArray(payload?.results);
170
+ const sourceCandidates = results
171
+ .filter((result) => readString(result.url))
172
+ .map((result, index) => candidateFromSearchResult(query, result, index));
173
+ return {
174
+ success: true,
175
+ output: JSON.stringify({
176
+ status: sourceCandidates.length > 0 ? 'source-candidates-ready' : 'no-source-candidates',
177
+ query,
178
+ providerId: readString(payload?.providerId),
179
+ providerLabel: readString(payload?.providerLabel),
180
+ resultCount: results.length,
181
+ sourceCandidateCount: sourceCandidates.length,
182
+ sourceCandidates,
183
+ nextRoutes: {
184
+ ...(run ? {
185
+ run: `research action:"run" runId:${quote(run.id)}`,
186
+ briefing: `research action:"briefing" target:${quote(run.id)}`,
187
+ startRun: `research action:"start_run" id:${quote(run.id)} confirm:true explicitUserRequest:"..."`,
188
+ checkpointAfterCapture: `research action:"checkpoint" id:${quote(run.id)} phase:"reading" progress:${Math.max(run.progress, 25)} note:"Captured candidate sources for review." sourceIds:["..."] confirm:true explicitUserRequest:"..."`,
189
+ } : {
190
+ createRun: `research action:"create_run" question:${quote(query)} plan:["Search bounded public web sources","Capture candidate sources","Review credibility","Save sourced report"] confirm:true explicitUserRequest:"..."`,
191
+ }),
192
+ sourceQueue: `research action:"sources" query:${quote(query)}`,
193
+ reviewedBundle: `research action:"bundle" query:${quote(query)} includeReportLines:true`,
194
+ saveReport: `research action:"report" question:${quote(query)} sources:[...] visualReport:true requireCitationCoverage:true confirm:true explicitUserRequest:"..."`,
195
+ },
196
+ ...(run ? { run: { id: run.id, title: run.title, status: run.status, phase: run.phase, progress: run.progress } } : {}),
197
+ searchArgs: webArgs,
198
+ policy: 'This route performs bounded read-only public web search only. It does not create or start a run, save sources, checkpoint progress, write a report, ingest Knowledge, or send messages. Use each returned route only after explicit user confirmation.',
199
+ }, null, 2),
200
+ };
201
+ }
202
+
203
+ function requireConfirmedRunner(args: AgentResearchToolArgs): string {
204
+ const explicitUserRequest = readString(args.explicitUserRequest);
205
+ if (!explicitUserRequest) throw new Error('research action:"runner" requires explicitUserRequest before it can create or checkpoint a visible research run.');
206
+ if (args.confirm !== true) throw new Error('research action:"runner" requires confirm:true before it can create or checkpoint a visible research run.');
207
+ return explicitUserRequest;
208
+ }
209
+
210
+ function researchSourceRoutes(source: AgentResearchSourceRecord): Record<string, unknown> {
211
+ return {
212
+ sourceId: source.id,
213
+ title: source.title,
214
+ status: source.status,
215
+ credibility: source.credibility,
216
+ score: source.score,
217
+ inspectRoute: `research action:"source" sourceId:${quote(source.id)}`,
218
+ reviewRoute: `research action:"review_source" id:${quote(source.id)} credibility:"high|medium|mixed|low" score:80 note:"..." confirm:true explicitUserRequest:"..."`,
219
+ reportLine: researchSourceReportLine(source),
220
+ };
221
+ }
222
+
223
+ function sourceFromCandidate(
224
+ registry: AgentResearchSourceRegistry,
225
+ question: string,
226
+ candidate: Record<string, unknown>,
227
+ ): { readonly source: AgentResearchSourceRecord; readonly created: boolean } | null {
228
+ const title = readString(candidate.title);
229
+ const summary = readString(candidate.summary);
230
+ const url = readString(candidate.url);
231
+ if (!title || !summary) return null;
232
+ const publisher = readString(candidate.publisher);
233
+ const evidence = readString(candidate.evidencePreview);
234
+ try {
235
+ return {
236
+ created: true,
237
+ source: registry.create({
238
+ question,
239
+ title,
240
+ ...(url ? { url } : {}),
241
+ ...(publisher ? { publisher } : {}),
242
+ summary,
243
+ ...(evidence ? { evidence } : {}),
244
+ credibility: 'unreviewed',
245
+ tags: ['research', 'web-search', 'runner'],
246
+ note: 'Captured by confirmed GoodVibes research runner.',
247
+ provenance: 'agent-research-runner',
248
+ }),
249
+ };
250
+ } catch (err) {
251
+ const existing = registry.get(url || title);
252
+ if (existing) return { created: false, source: existing };
253
+ throw err;
254
+ }
255
+ }
256
+
257
+ export async function runConfirmedResearchRunner(
258
+ webSearchTool: Tool | undefined,
259
+ args: AgentResearchToolArgs,
260
+ shellPaths: CommandContext['workspace']['shellPaths'] | undefined,
261
+ ): Promise<ToolResult> {
262
+ try {
263
+ const explicitUserRequest = requireConfirmedRunner(args);
264
+ if (!shellPaths) return error('Confirmed research runner is unavailable because this runtime did not provide shell paths.');
265
+ if (!webSearchTool) return error('Confirmed research runner requires the web_search tool for bounded source collection. Use research action:"plan" for offline routing.');
266
+ const runRegistry = AgentResearchRunRegistry.fromShellPaths(shellPaths);
267
+ const sourceRegistry = AgentResearchSourceRegistry.fromShellPaths(shellPaths);
268
+ const lookup = readString(args.runId) || readString(args.id);
269
+ const existingRun = lookup ? runRegistry.get(lookup) : null;
270
+ if (lookup && !existingRun) return error(`Unknown research run ${lookup}. Use research action:"runs" to inspect run ids.`);
271
+ const question = readString(args.query) || readString(args.question) || readString(args.target) || existingRun?.question || explicitUserRequest;
272
+ if (!question) return error('Confirmed research runner requires query, question, target, or an existing run with a question.');
273
+ let run = existingRun ?? runRegistry.create({
274
+ title: readString(args.title) || fallbackTitle(question),
275
+ question,
276
+ goal: readString(args.goal) || `Collect reviewed source candidates for: ${question}`,
277
+ plan: [
278
+ 'Run bounded public web source collection.',
279
+ 'Save candidate sources into the local research source queue.',
280
+ 'Checkpoint the visible research run with source ids and next review steps.',
281
+ 'Save a citation-covered report only after source review.',
282
+ ],
283
+ nextSteps: ['Run bounded source collection.', 'Review candidate sources.', 'Save a sourced report artifact.'],
284
+ note: `Confirmed runner authorized by user request: ${previewText(explicitUserRequest, 160)}`,
285
+ provenance: 'agent-research-runner',
286
+ });
287
+ if (run.status === 'planned') {
288
+ run = runRegistry.start(run.id, 'Confirmed research runner started bounded source collection.');
289
+ } else if (run.status === 'paused' || run.status === 'blocked') {
290
+ run = runRegistry.resume(run.id, 'Confirmed research runner resumed bounded source collection.');
291
+ } else if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {
292
+ return error(`Cannot run confirmed research runner for ${run.status} research run ${run.id}. Create a follow-up run instead.`);
293
+ }
294
+
295
+ const webResult = await webSearchTool.execute(searchArgs({ ...args, query: run.question }));
296
+ if (!webResult.success) {
297
+ run = runRegistry.checkpoint(run.id, {
298
+ status: 'blocked',
299
+ phase: 'searching',
300
+ progress: Math.max(run.progress, 10),
301
+ note: `Confirmed research runner blocked during source collection: ${readString(webResult.error) || 'web_search failed.'}`,
302
+ nextSteps: ['Inspect runner readiness.', 'Retry bounded source collection after search is available.'],
303
+ });
304
+ return {
305
+ success: false,
306
+ error: `Confirmed research runner blocked for ${run.id}: ${readString(webResult.error) || 'web_search failed.'}`,
307
+ };
308
+ }
309
+
310
+ const payload = parseSearchPayload(webResult.output);
311
+ const results = readRecordArray(payload?.results);
312
+ const candidates = results
313
+ .filter((result) => readString(result.url))
314
+ .map((result, index) => candidateFromSearchResult(run.question, result, index));
315
+ const saved = candidates
316
+ .map((candidate) => sourceFromCandidate(sourceRegistry, run.question, candidate))
317
+ .filter((entry): entry is { readonly source: AgentResearchSourceRecord; readonly created: boolean } => entry !== null);
318
+ const sourceIds = saved.map((entry) => entry.source.id);
319
+ const createdCount = saved.filter((entry) => entry.created).length;
320
+ const duplicateCount = saved.length - createdCount;
321
+ run = runRegistry.checkpoint(run.id, {
322
+ status: 'running',
323
+ phase: 'reading',
324
+ progress: Math.max(run.progress, sourceIds.length > 0 ? 35 : 20),
325
+ note: `Confirmed research runner captured ${createdCount} new source candidate(s)${duplicateCount > 0 ? ` and reused ${duplicateCount} existing source(s)` : ''}.`,
326
+ nextSteps: sourceIds.length > 0
327
+ ? ['Review candidate source credibility.', 'Save a citation-covered report artifact after source review.']
328
+ : ['Try a more specific query or provider.', 'Capture sources manually if public search found none.'],
329
+ sourceIds,
330
+ });
331
+
332
+ return {
333
+ success: true,
334
+ output: JSON.stringify({
335
+ status: sourceIds.length > 0 ? 'source-collection-checkpointed' : 'no-source-candidates-checkpointed',
336
+ run: {
337
+ id: run.id,
338
+ title: run.title,
339
+ status: run.status,
340
+ phase: run.phase,
341
+ progress: run.progress,
342
+ sourceIds: run.sourceIds,
343
+ inspectRoute: `research action:"run" runId:${quote(run.id)}`,
344
+ checkpointRoute: `research action:"checkpoint" id:${quote(run.id)} phase:"reading" progress:${Math.max(run.progress, 35)} note:"..." sourceIds:["..."] confirm:true explicitUserRequest:"..."`,
345
+ cancelRoute: `research action:"cancel" id:${quote(run.id)} note:"..." confirm:true explicitUserRequest:"..."`,
346
+ },
347
+ query: run.question,
348
+ providerId: readString(payload?.providerId),
349
+ resultCount: results.length,
350
+ sourceCandidateCount: candidates.length,
351
+ savedSourceCount: createdCount,
352
+ reusedSourceCount: duplicateCount,
353
+ sources: saved.map((entry) => researchSourceRoutes(entry.source)),
354
+ nextRoutes: {
355
+ run: `research action:"run" runId:${quote(run.id)}`,
356
+ sources: `research action:"sources" query:${quote(run.question)} includeReportLines:true`,
357
+ reviewSource: 'research action:"review_source" id:"..." credibility:"high|medium|mixed|low" score:80 note:"..." confirm:true explicitUserRequest:"..."',
358
+ bundle: `research action:"bundle" query:${quote(run.question)} includeReportLines:true`,
359
+ saveReport: `research action:"report" question:${quote(run.question)} sources:[...] visualReport:true requireCitationCoverage:true confirm:true explicitUserRequest:"..."`,
360
+ },
361
+ policy: 'Confirmed research runner only creates/updates local visible research run state and local source queue records. It does not save a report, ingest Knowledge, send messages, or control a browser/PWA surface.',
362
+ }, null, 2),
363
+ };
364
+ } catch (err) {
365
+ return error(err instanceof Error ? err.message : String(err));
366
+ }
367
+ }
@@ -1,11 +1,11 @@
1
1
  import type { Tool } from '@pellux/goodvibes-sdk/platform/types';
2
2
  import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
3
3
  import type { CommandContext, CommandRegistry } from '../input/command-registry.ts';
4
- import { AgentResearchRunRegistry, type AgentResearchRunRecord } from '../agent/research-run-registry.ts';
5
4
  import { createAgentArtifactsTool } from './agent-artifacts-tool.ts';
6
5
  import { createAgentHarnessTool } from './agent-harness-tool.ts';
7
6
  import { createAgentResearchReportTool } from './agent-research-report-tool.ts';
8
7
  import { createAgentResearchRunsTool } from './agent-research-runs-tool.ts';
8
+ import { runConfirmedResearchRunner, runPublicSearch } from './agent-research-runner.ts';
9
9
  import { createAgentResearchSourcesTool } from './agent-research-sources-tool.ts';
10
10
 
11
11
  type AgentResearchAction =
@@ -36,7 +36,7 @@ type AgentResearchAction =
36
36
  | 'delete_source'
37
37
  | 'report';
38
38
 
39
- interface AgentResearchToolArgs {
39
+ export interface AgentResearchToolArgs {
40
40
  readonly action?: unknown;
41
41
  readonly mode?: unknown;
42
42
  readonly id?: unknown;
@@ -111,12 +111,6 @@ function readString(value: unknown): string {
111
111
  return typeof value === 'string' ? value.trim() : '';
112
112
  }
113
113
 
114
- function readNumber(value: unknown, fallback: number, max: number): number {
115
- const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
116
- if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
117
- return Math.max(1, Math.min(max, Math.trunc(parsed)));
118
- }
119
-
120
114
  function normalizeResearchAction(value: unknown): AgentResearchAction | null {
121
115
  const action = readString(value).toLowerCase().replace(/-/g, '_');
122
116
  if (!action) return null;
@@ -279,22 +273,6 @@ function bundleArgs(args: AgentResearchToolArgs): Record<string, unknown> {
279
273
  });
280
274
  }
281
275
 
282
- function searchArgs(args: AgentResearchToolArgs): Record<string, unknown> {
283
- const safeSearch = readString(args.safeSearch).toLowerCase() === 'strict' ? 'strict' : 'moderate';
284
- return compactArgs({
285
- query: args.query ?? args.target,
286
- providerId: args.providerId,
287
- maxResults: readNumber(args.maxResults, readNumber(args.limit, 5, 10), 10),
288
- verbosity: 'evidence',
289
- region: args.region,
290
- safeSearch,
291
- timeRange: args.timeRange,
292
- includeEvidence: true,
293
- evidenceTopN: readNumber(args.evidenceTopN, 3, 3),
294
- evidenceExtract: args.evidenceExtract ?? 'readable',
295
- });
296
- }
297
-
298
276
  function reportsArgs(args: AgentResearchToolArgs): Record<string, unknown> {
299
277
  return compactArgs({
300
278
  mode: 'list',
@@ -313,160 +291,6 @@ function reportArtifactArgs(args: AgentResearchToolArgs): Record<string, unknown
313
291
  });
314
292
  }
315
293
 
316
- function previewText(value: unknown, limit: number): string {
317
- const text = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
318
- if (text.length <= limit) return text;
319
- return `${text.slice(0, Math.max(0, limit - 3)).trimEnd()}...`;
320
- }
321
-
322
- function quote(value: string): string {
323
- return JSON.stringify(value);
324
- }
325
-
326
- function readRecord(value: unknown): Record<string, unknown> | null {
327
- return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
328
- }
329
-
330
- function readRecordArray(value: unknown): readonly Record<string, unknown>[] {
331
- return Array.isArray(value) ? value.map(readRecord).filter((entry): entry is Record<string, unknown> => Boolean(entry)) : [];
332
- }
333
-
334
- function hostname(value: string): string {
335
- try {
336
- return new URL(value).hostname.replace(/^www\./i, '');
337
- } catch {
338
- return '';
339
- }
340
- }
341
-
342
- function sanitizeUrlForRoute(value: string): string {
343
- try {
344
- const url = new URL(value);
345
- for (const key of [...url.searchParams.keys()]) {
346
- if (/token|secret|password|authorization|credential|api[-_]?key/i.test(key)) url.searchParams.set(key, '<redacted>');
347
- }
348
- return url.toString();
349
- } catch {
350
- return value.replace(/([?&\s](?:token|secret|password|authorization|credential|api[-_]?key)=)[^\s&]+/gi, '$1<redacted>');
351
- }
352
- }
353
-
354
- function candidateFromSearchResult(question: string, result: Record<string, unknown>, index: number): Record<string, unknown> {
355
- const rawUrl = readString(result.url);
356
- const url = sanitizeUrlForRoute(rawUrl);
357
- const title = previewText(result.title, 120) || hostname(rawUrl) || `Search result ${index + 1}`;
358
- const publisher = previewText(result.domain, 80) || hostname(rawUrl);
359
- const evidenceRecords = readRecordArray(result.evidence);
360
- const evidenceText = previewText(
361
- evidenceRecords.map((entry) => readString(entry.content)).filter(Boolean).join('\n\n') || result.snippet,
362
- 420,
363
- );
364
- const summary = previewText(result.snippet, 260) || previewText(evidenceText, 260) || `Public web result for ${question}.`;
365
- const captureArgs = compactArgs({
366
- action: 'add_source',
367
- question,
368
- title,
369
- url,
370
- publisher,
371
- summary,
372
- evidence: evidenceText,
373
- credibility: 'unreviewed',
374
- tags: ['research', 'web-search'],
375
- confirm: true,
376
- explicitUserRequest: '...',
377
- });
378
- return {
379
- rank: typeof result.rank === 'number' ? result.rank : index + 1,
380
- title,
381
- url,
382
- ...(publisher ? { publisher } : {}),
383
- summary,
384
- ...(evidenceText ? { evidencePreview: evidenceText } : {}),
385
- captureArgs,
386
- captureRoute: [
387
- 'research action:"add_source"',
388
- `question:${quote(question)}`,
389
- `title:${quote(title)}`,
390
- ...(url ? [`url:${quote(url)}`] : []),
391
- ...(publisher ? [`publisher:${quote(publisher)}`] : []),
392
- `summary:${quote(summary)}`,
393
- ...(evidenceText ? [`evidence:${quote(evidenceText)}`] : []),
394
- 'credibility:"unreviewed"',
395
- 'tags:["research","web-search"]',
396
- 'confirm:true',
397
- 'explicitUserRequest:"..."',
398
- ].join(' '),
399
- };
400
- }
401
-
402
- function parseSearchPayload(outputValue: unknown): Record<string, unknown> | null {
403
- if (typeof outputValue !== 'string') return readRecord(outputValue);
404
- try {
405
- return readRecord(JSON.parse(outputValue));
406
- } catch {
407
- return null;
408
- }
409
- }
410
-
411
- function resolveSearchRun(args: AgentResearchToolArgs, shellPaths: CommandContext['workspace']['shellPaths'] | undefined): AgentResearchRunRecord | null {
412
- if (!shellPaths) return null;
413
- const lookup = readString(args.runId) || readString(args.id);
414
- if (!lookup) return null;
415
- return AgentResearchRunRegistry.fromShellPaths(shellPaths).get(lookup);
416
- }
417
-
418
- async function runPublicSearch(
419
- webSearchTool: Tool | undefined,
420
- args: AgentResearchToolArgs,
421
- shellPaths: CommandContext['workspace']['shellPaths'] | undefined,
422
- ): Promise<{ readonly success: true; readonly output: string } | { readonly success: false; readonly error: string }> {
423
- const run = resolveSearchRun(args, shellPaths);
424
- const query = readString(args.query) || readString(args.question) || run?.question || readString(args.target);
425
- if ((readString(args.runId) || readString(args.id)) && !run && !readString(args.query) && !readString(args.question)) {
426
- return error(`Unknown research run ${readString(args.runId) || readString(args.id)}. Use research action:"runs" to inspect run ids.`);
427
- }
428
- if (!query) return error('research action:"search" requires query or target.');
429
- if (!webSearchTool) {
430
- return error('Bounded public web search is unavailable because web_search is not registered. Use research action:"briefing" for the offline next-action queue or action:"plan" for the route plan.');
431
- }
432
- const webArgs = searchArgs({ ...args, query });
433
- const webResult = await webSearchTool.execute(webArgs);
434
- if (!webResult.success) return { success: false, error: readString(webResult.error) || 'web_search failed.' };
435
- const payload = parseSearchPayload(webResult.output);
436
- const results = readRecordArray(payload?.results);
437
- const sourceCandidates = results
438
- .filter((result) => readString(result.url))
439
- .map((result, index) => candidateFromSearchResult(query, result, index));
440
- return {
441
- success: true,
442
- output: JSON.stringify({
443
- status: sourceCandidates.length > 0 ? 'source-candidates-ready' : 'no-source-candidates',
444
- query,
445
- providerId: readString(payload?.providerId),
446
- providerLabel: readString(payload?.providerLabel),
447
- resultCount: results.length,
448
- sourceCandidateCount: sourceCandidates.length,
449
- sourceCandidates,
450
- nextRoutes: {
451
- ...(run ? {
452
- run: `research action:"run" runId:${quote(run.id)}`,
453
- briefing: `research action:"briefing" target:${quote(run.id)}`,
454
- startRun: `research action:"start_run" id:${quote(run.id)} confirm:true explicitUserRequest:"..."`,
455
- checkpointAfterCapture: `research action:"checkpoint" id:${quote(run.id)} phase:"reading" progress:${Math.max(run.progress, 25)} note:"Captured candidate sources for review." sourceIds:["..."] confirm:true explicitUserRequest:"..."`,
456
- } : {
457
- createRun: `research action:"create_run" question:${quote(query)} plan:["Search bounded public web sources","Capture candidate sources","Review credibility","Save sourced report"] confirm:true explicitUserRequest:"..."`,
458
- }),
459
- sourceQueue: `research action:"sources" query:${quote(query)}`,
460
- reviewedBundle: `research action:"bundle" query:${quote(query)} includeReportLines:true`,
461
- saveReport: `research action:"report" question:${quote(query)} sources:[...] visualReport:true requireCitationCoverage:true confirm:true explicitUserRequest:"..."`,
462
- },
463
- ...(run ? { run: { id: run.id, title: run.title, status: run.status, phase: run.phase, progress: run.progress } } : {}),
464
- searchArgs: webArgs,
465
- policy: 'This route performs bounded read-only public web search only. It does not create or start a run, save sources, checkpoint progress, write a report, ingest Knowledge, or send messages. Use each returned route only after explicit user confirmation.',
466
- }, null, 2),
467
- };
468
- }
469
-
470
294
  function addSourceArgs(args: AgentResearchToolArgs): Record<string, unknown> {
471
295
  return compactArgs({
472
296
  mode: 'add',
@@ -648,7 +472,11 @@ export function createAgentResearchTool(deps: AgentResearchToolDeps): Tool {
648
472
 
649
473
  if (action === 'briefing') return harnessTool.execute(briefingArgs(args));
650
474
  if (action === 'plan') return harnessTool.execute(planArgs(args));
651
- if (action === 'runner') return harnessTool.execute(runnerArgs(args));
475
+ if (action === 'runner') {
476
+ return args.confirm === true
477
+ ? runConfirmedResearchRunner(webSearchTool, args, shellPaths)
478
+ : harnessTool.execute(runnerArgs(args));
479
+ }
652
480
  if (action === 'runs') return harnessTool.execute(runsArgs(args));
653
481
  if (action === 'run') return harnessTool.execute(runArgs(args));
654
482
  if (action === 'sources') return harnessTool.execute(sourcesArgs(args));
@@ -119,7 +119,7 @@ if (browserControlLike(lower)) {
119
119
  'workspace action:"open" surfaceId:"connected-browser-cockpit" confirm:true explicitUserRequest:"..."',
120
120
  'host action:"services" query:"web" includeParameters:true',
121
121
  ],
122
- policy: 'Browser/PWA readiness is read-only. Opening the connected browser cockpit is a visible confirmed handoff; browser-native Agent workspace receipts remain unpublished until the connected host provides them.',
122
+ policy: 'Browser/PWA readiness is read-only. Opening the connected browser cockpit is a visible confirmed handoff; browser-native Agent workspace routes count as ready only from certified SDK/daemon browser/PWA route and first-run receipt read models.',
123
123
  });
124
124
  }
125
125
 
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '1.2.0';
9
+ let _version = '1.3.0';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {
12
12
  readonly version?: unknown;