@pellux/goodvibes-agent 0.1.48 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -1
- package/package.json +1 -1
- package/src/input/agent-workspace.ts +171 -16
- package/src/panels/builtin/knowledge.ts +3 -3
- package/src/panels/builtin/shared.ts +3 -0
- package/src/panels/knowledge-panel.ts +80 -9
- package/src/renderer/agent-workspace.ts +59 -0
- package/src/runtime/bootstrap-core.ts +2 -0
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/bootstrap.ts +1 -0
- package/src/tools/agent-local-registry-tool.ts +341 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GoodVibes Agent will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.1.50 - 2026-05-31
|
|
6
|
+
|
|
7
|
+
- bdb654a Improve local library workspaces
|
|
8
|
+
|
|
9
|
+
## 0.1.49 - 2026-05-31
|
|
10
|
+
|
|
11
|
+
- 445e694 Show isolated Agent Knowledge in TUI panel
|
|
12
|
+
- 632d951 Add agent-local registry tool
|
|
13
|
+
- 4832355 Make agent setup workspace actionable
|
|
14
|
+
|
|
5
15
|
## 0.1.48 - 2026-05-31
|
|
6
16
|
|
|
7
17
|
- 67f8ce0 Remove audit remnants and surface setup checklist
|
|
8
|
-
- 34c3d0b Remove internal
|
|
18
|
+
- 34c3d0b Remove internal development-only surfaces
|
|
9
19
|
|
|
10
20
|
## 0.1.47 - 2026-05-31
|
|
11
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.50",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Near-fork GoodVibes operator assistant with the GoodVibes TUI shell, renderer, input, fullscreen workspace, and daemon-connected Agent product brain.",
|
|
6
6
|
"type": "module",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
|
|
2
2
|
import { basename, sep } from 'node:path';
|
|
3
3
|
import type { CommandContext } from './command-registry.ts';
|
|
4
|
-
import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
|
|
5
|
-
import { AgentRoutineRegistry } from '../agent/routine-registry.ts';
|
|
6
|
-
import { AgentSkillRegistry } from '../agent/skill-registry.ts';
|
|
4
|
+
import { AgentPersonaRegistry, type AgentPersonaRecord } from '../agent/persona-registry.ts';
|
|
5
|
+
import { AgentRoutineRegistry, type AgentRoutineRecord } from '../agent/routine-registry.ts';
|
|
6
|
+
import { AgentSkillRegistry, type AgentSkillRecord } from '../agent/skill-registry.ts';
|
|
7
7
|
import { getAgentRuntimeProfilesRoot, listAgentRuntimeProfiles, listAgentRuntimeProfileTemplates } from '../agent/runtime-profile.ts';
|
|
8
8
|
import {
|
|
9
9
|
buildAgentWorkspaceChannels,
|
|
@@ -20,13 +20,14 @@ export const AGENT_WORKSPACE_MODAL_NAME = 'agentWorkspace';
|
|
|
20
20
|
|
|
21
21
|
export type AgentWorkspaceFocusPane = 'categories' | 'actions';
|
|
22
22
|
|
|
23
|
-
export type AgentWorkspaceActionKind = 'command' | 'guidance';
|
|
23
|
+
export type AgentWorkspaceActionKind = 'command' | 'guidance' | 'workspace';
|
|
24
24
|
|
|
25
25
|
export interface AgentWorkspaceAction {
|
|
26
26
|
readonly id: string;
|
|
27
27
|
readonly label: string;
|
|
28
28
|
readonly detail: string;
|
|
29
29
|
readonly command?: string;
|
|
30
|
+
readonly targetCategoryId?: string;
|
|
30
31
|
readonly kind: AgentWorkspaceActionKind;
|
|
31
32
|
readonly safety: 'safe' | 'read-only' | 'delegates' | 'blocked';
|
|
32
33
|
}
|
|
@@ -52,6 +53,19 @@ export interface AgentWorkspaceActionResult {
|
|
|
52
53
|
readonly safety?: AgentWorkspaceAction['safety'];
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
export interface AgentWorkspaceLocalLibraryItem {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly description: string;
|
|
60
|
+
readonly reviewState: string;
|
|
61
|
+
readonly source: string;
|
|
62
|
+
readonly tags: readonly string[];
|
|
63
|
+
readonly triggers: readonly string[];
|
|
64
|
+
readonly active?: boolean;
|
|
65
|
+
readonly enabled?: boolean;
|
|
66
|
+
readonly startCount?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
type AgentWorkspaceConfigReader = {
|
|
56
70
|
get(key: string): unknown;
|
|
57
71
|
};
|
|
@@ -68,10 +82,13 @@ export interface AgentWorkspaceRuntimeSnapshot {
|
|
|
68
82
|
readonly sessionMemoryCount: number;
|
|
69
83
|
readonly localRoutineCount: number;
|
|
70
84
|
readonly enabledRoutineCount: number;
|
|
85
|
+
readonly localRoutines: readonly AgentWorkspaceLocalLibraryItem[];
|
|
71
86
|
readonly localSkillCount: number;
|
|
72
87
|
readonly enabledSkillCount: number;
|
|
88
|
+
readonly localSkills: readonly AgentWorkspaceLocalLibraryItem[];
|
|
73
89
|
readonly localPersonaCount: number;
|
|
74
90
|
readonly activePersonaName: string;
|
|
91
|
+
readonly localPersonas: readonly AgentWorkspaceLocalLibraryItem[];
|
|
75
92
|
readonly knowledgeRoute: '/api/goodvibes-agent/knowledge';
|
|
76
93
|
readonly knowledgeIsolation: 'agent-only';
|
|
77
94
|
readonly executionPolicy: 'serial-proactive';
|
|
@@ -142,6 +159,46 @@ function inferActiveRuntimeProfile(homeDirectory: string): string {
|
|
|
142
159
|
return homeDirectory.includes(marker) ? basename(homeDirectory) : '(default home)';
|
|
143
160
|
}
|
|
144
161
|
|
|
162
|
+
function summarizePersonaItem(persona: AgentPersonaRecord, activePersonaId: string | null): AgentWorkspaceLocalLibraryItem {
|
|
163
|
+
return {
|
|
164
|
+
id: persona.id,
|
|
165
|
+
name: persona.name,
|
|
166
|
+
description: persona.description,
|
|
167
|
+
reviewState: persona.reviewState,
|
|
168
|
+
source: persona.source,
|
|
169
|
+
tags: persona.tags,
|
|
170
|
+
triggers: persona.triggers,
|
|
171
|
+
active: persona.id === activePersonaId,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function summarizeSkillItem(skill: AgentSkillRecord): AgentWorkspaceLocalLibraryItem {
|
|
176
|
+
return {
|
|
177
|
+
id: skill.id,
|
|
178
|
+
name: skill.name,
|
|
179
|
+
description: skill.description,
|
|
180
|
+
reviewState: skill.reviewState,
|
|
181
|
+
source: skill.source,
|
|
182
|
+
tags: skill.tags,
|
|
183
|
+
triggers: skill.triggers,
|
|
184
|
+
enabled: skill.enabled,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function summarizeRoutineItem(routine: AgentRoutineRecord): AgentWorkspaceLocalLibraryItem {
|
|
189
|
+
return {
|
|
190
|
+
id: routine.id,
|
|
191
|
+
name: routine.name,
|
|
192
|
+
description: routine.description,
|
|
193
|
+
reviewState: routine.reviewState,
|
|
194
|
+
source: routine.source,
|
|
195
|
+
tags: routine.tags,
|
|
196
|
+
triggers: routine.triggers,
|
|
197
|
+
enabled: routine.enabled,
|
|
198
|
+
startCount: routine.startCount,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
145
202
|
export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): AgentWorkspaceRuntimeSnapshot {
|
|
146
203
|
const host = readConfigString(context, 'controlPlane.host', '127.0.0.1');
|
|
147
204
|
const port = readConfigNumber(context, 'controlPlane.port', 3421);
|
|
@@ -164,31 +221,43 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
|
|
|
164
221
|
const personaSnapshot = (() => {
|
|
165
222
|
try {
|
|
166
223
|
const shellPaths = context.workspace?.shellPaths;
|
|
167
|
-
if (!shellPaths) return { count: 0, activeName: '(none)' };
|
|
224
|
+
if (!shellPaths) return { count: 0, activeName: '(none)', items: [] };
|
|
168
225
|
const snapshot = AgentPersonaRegistry.fromShellPaths(shellPaths).snapshot();
|
|
169
|
-
return {
|
|
226
|
+
return {
|
|
227
|
+
count: snapshot.personas.length,
|
|
228
|
+
activeName: snapshot.activePersona?.name ?? '(none)',
|
|
229
|
+
items: snapshot.personas.map((persona) => summarizePersonaItem(persona, snapshot.activePersonaId)),
|
|
230
|
+
};
|
|
170
231
|
} catch {
|
|
171
|
-
return { count: 0, activeName: '(unavailable)' };
|
|
232
|
+
return { count: 0, activeName: '(unavailable)', items: [] };
|
|
172
233
|
}
|
|
173
234
|
})();
|
|
174
235
|
const skillSnapshot = (() => {
|
|
175
236
|
try {
|
|
176
237
|
const shellPaths = context.workspace?.shellPaths;
|
|
177
|
-
if (!shellPaths) return { count: 0, enabled: 0 };
|
|
238
|
+
if (!shellPaths) return { count: 0, enabled: 0, items: [] };
|
|
178
239
|
const snapshot = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot();
|
|
179
|
-
return {
|
|
240
|
+
return {
|
|
241
|
+
count: snapshot.skills.length,
|
|
242
|
+
enabled: snapshot.enabledSkills.length,
|
|
243
|
+
items: snapshot.skills.map(summarizeSkillItem),
|
|
244
|
+
};
|
|
180
245
|
} catch {
|
|
181
|
-
return { count: 0, enabled: 0 };
|
|
246
|
+
return { count: 0, enabled: 0, items: [] };
|
|
182
247
|
}
|
|
183
248
|
})();
|
|
184
249
|
const routineSnapshot = (() => {
|
|
185
250
|
try {
|
|
186
251
|
const shellPaths = context.workspace?.shellPaths;
|
|
187
|
-
if (!shellPaths) return { count: 0, enabled: 0 };
|
|
252
|
+
if (!shellPaths) return { count: 0, enabled: 0, items: [] };
|
|
188
253
|
const snapshot = AgentRoutineRegistry.fromShellPaths(shellPaths).snapshot();
|
|
189
|
-
return {
|
|
254
|
+
return {
|
|
255
|
+
count: snapshot.routines.length,
|
|
256
|
+
enabled: snapshot.enabledRoutines.length,
|
|
257
|
+
items: snapshot.routines.map(summarizeRoutineItem),
|
|
258
|
+
};
|
|
190
259
|
} catch {
|
|
191
|
-
return { count: 0, enabled: 0 };
|
|
260
|
+
return { count: 0, enabled: 0, items: [] };
|
|
192
261
|
}
|
|
193
262
|
})();
|
|
194
263
|
const runtimeProfiles = (() => {
|
|
@@ -264,10 +333,13 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
|
|
|
264
333
|
sessionMemoryCount,
|
|
265
334
|
localRoutineCount: routineSnapshot.count,
|
|
266
335
|
enabledRoutineCount: routineSnapshot.enabled,
|
|
336
|
+
localRoutines: routineSnapshot.items,
|
|
267
337
|
localSkillCount: skillSnapshot.count,
|
|
268
338
|
enabledSkillCount: skillSnapshot.enabled,
|
|
339
|
+
localSkills: skillSnapshot.items,
|
|
269
340
|
localPersonaCount: personaSnapshot.count,
|
|
270
341
|
activePersonaName: personaSnapshot.activeName,
|
|
342
|
+
localPersonas: personaSnapshot.items,
|
|
271
343
|
knowledgeRoute: '/api/goodvibes-agent/knowledge',
|
|
272
344
|
knowledgeIsolation: 'agent-only',
|
|
273
345
|
executionPolicy: 'serial-proactive',
|
|
@@ -307,6 +379,11 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
|
|
|
307
379
|
actions: [
|
|
308
380
|
{ id: 'chat', label: 'Continue assistant chat', detail: 'Close this workspace and type a normal message. Agent work stays serial in the main conversation.', kind: 'guidance', safety: 'safe' },
|
|
309
381
|
{ id: 'model', label: 'Choose model', detail: 'Open the model/provider workspace for the Agent chat route.', command: '/model', kind: 'command', safety: 'safe' },
|
|
382
|
+
{ id: 'setup-home', label: 'Setup checklist', detail: 'Jump to the first-run checklist for provider, knowledge, personas, skills, routines, memory, channels, and voice/media.', targetCategoryId: 'setup', kind: 'workspace', safety: 'safe' },
|
|
383
|
+
{ id: 'knowledge-home', label: 'Agent Knowledge', detail: 'Jump to isolated Agent Knowledge status, ingest, search, and review flows.', targetCategoryId: 'knowledge', kind: 'workspace', safety: 'read-only' },
|
|
384
|
+
{ id: 'memory-home', label: 'Memory, skills, routines', detail: 'Jump to local memory, persona, skill, and routine setup. These are core Agent product features.', targetCategoryId: 'memory', kind: 'workspace', safety: 'safe' },
|
|
385
|
+
{ id: 'channels-home', label: 'Channels', detail: 'Jump to companion pairing and channel readiness without changing daemon lifecycle.', targetCategoryId: 'channels', kind: 'workspace', safety: 'read-only' },
|
|
386
|
+
{ id: 'voice-home', label: 'Voice and media', detail: 'Jump to voice, TTS, image input, browser, and node posture setup.', targetCategoryId: 'voice-media', kind: 'workspace', safety: 'safe' },
|
|
310
387
|
{ id: 'help', label: 'Browse commands', detail: 'Open registry-driven command help.', command: '/help', kind: 'command', safety: 'safe' },
|
|
311
388
|
{ id: 'health', label: 'Review health', detail: 'Show the local health review surface without starting or mutating daemon services.', command: '/health review', kind: 'command', safety: 'read-only' },
|
|
312
389
|
],
|
|
@@ -320,6 +397,15 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
|
|
|
320
397
|
actions: [
|
|
321
398
|
{ id: 'config', label: 'Open config workspace', detail: 'Use the TUI-derived fullscreen settings workspace.', command: '/config', kind: 'command', safety: 'safe' },
|
|
322
399
|
{ id: 'onboarding', label: 'Open setup wizard', detail: 'Review Agent runtime settings in the fullscreen setup flow.', command: '/onboarding', kind: 'command', safety: 'safe' },
|
|
400
|
+
{ id: 'setup-provider-model', label: 'Provider and model', detail: 'Choose the provider/model route for normal assistant chat.', command: '/model', kind: 'command', safety: 'safe' },
|
|
401
|
+
{ id: 'setup-agent-knowledge', label: 'Agent Knowledge', detail: 'Inspect the isolated Agent Knowledge store before ingesting source-backed material.', command: '/knowledge status', kind: 'command', safety: 'read-only' },
|
|
402
|
+
{ id: 'setup-runtime-profiles', label: 'Runtime profiles', detail: 'Browse starter templates for isolated Agent homes and operator identities.', command: '/agent-profile templates', kind: 'command', safety: 'read-only' },
|
|
403
|
+
{ id: 'setup-personas', label: 'Personas', detail: 'Create or select the active local Agent persona.', targetCategoryId: 'personas', kind: 'workspace', safety: 'safe' },
|
|
404
|
+
{ id: 'setup-skills', label: 'Skills', detail: 'Create, review, and enable reusable local Agent skills.', targetCategoryId: 'skills', kind: 'workspace', safety: 'safe' },
|
|
405
|
+
{ id: 'setup-routines', label: 'Routines', detail: 'Create, review, and enable local Agent routines before any explicit schedule promotion.', targetCategoryId: 'routines', kind: 'workspace', safety: 'safe' },
|
|
406
|
+
{ id: 'setup-memory', label: 'Local memory', detail: 'Inspect local/session memory; secrets stay rejected or redacted.', command: '/memory', kind: 'command', safety: 'read-only' },
|
|
407
|
+
{ id: 'setup-channels', label: 'Channels', detail: 'Open companion pairing and channel readiness setup.', command: '/pair', kind: 'command', safety: 'safe' },
|
|
408
|
+
{ id: 'setup-voice-media', label: 'Voice and media', detail: 'Open TTS/media settings for voice and image-capable Agent flows.', command: '/config tts', kind: 'command', safety: 'safe' },
|
|
323
409
|
{ id: 'provider', label: 'Provider status', detail: 'Review provider/model posture.', command: '/provider', kind: 'command', safety: 'read-only' },
|
|
324
410
|
{ id: 'auth', label: 'Auth review', detail: 'Review authentication posture without printing token values.', command: '/auth review', kind: 'command', safety: 'read-only' },
|
|
325
411
|
],
|
|
@@ -395,9 +481,53 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
|
|
|
395
481
|
detail: 'Memory, routines, skills, and personas stay Agent-local until stable shared daemon registry contracts exist. Secrets must not be stored as memory.',
|
|
396
482
|
actions: [
|
|
397
483
|
{ id: 'memory', label: 'Open memory', detail: 'Inspect local/session memory commands and surfaces.', command: '/memory', kind: 'command', safety: 'read-only' },
|
|
398
|
-
{ id: '
|
|
399
|
-
{ id: 'skills', label: 'Local skill library', detail: '
|
|
400
|
-
{ id: '
|
|
484
|
+
{ id: 'personas', label: 'Persona library', detail: 'Open the local persona workspace for active role selection and review.', targetCategoryId: 'personas', kind: 'workspace', safety: 'safe' },
|
|
485
|
+
{ id: 'skills', label: 'Local skill library', detail: 'Open the local skill workspace for reusable procedures and review.', targetCategoryId: 'skills', kind: 'workspace', safety: 'safe' },
|
|
486
|
+
{ id: 'routines', label: 'Routine library', detail: 'Open the local routine workspace for repeatable workflows and schedule promotion review.', targetCategoryId: 'routines', kind: 'workspace', safety: 'safe' },
|
|
487
|
+
],
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
id: 'personas',
|
|
491
|
+
group: 'LEARN',
|
|
492
|
+
label: 'Personas',
|
|
493
|
+
summary: 'Local behavior profiles for the main assistant.',
|
|
494
|
+
detail: 'Personas shape the serial Agent in the main conversation. They are not background agents and they never spawn specialist roots.',
|
|
495
|
+
actions: [
|
|
496
|
+
{ id: 'personas-list', label: 'List personas', detail: 'Print the full local persona library.', command: '/personas list', kind: 'command', safety: 'read-only' },
|
|
497
|
+
{ id: 'personas-active', label: 'Show active persona', detail: 'Inspect the active local persona applied to new turns.', command: '/personas active', kind: 'command', safety: 'read-only' },
|
|
498
|
+
{ id: 'personas-create', label: 'Create persona', detail: 'Create a local persona with real name, summary, and instructions. Placeholder commands are never dispatched.', command: '/personas create --name <name> --description <summary> --body <instructions>', kind: 'command', safety: 'safe' },
|
|
499
|
+
{ id: 'personas-use', label: 'Use persona', detail: 'Activate a local persona by id or name.', command: '/personas use <id>', kind: 'command', safety: 'safe' },
|
|
500
|
+
{ id: 'personas-review', label: 'Review persona', detail: 'Mark a local persona reviewed after inspecting it.', command: '/personas review <id>', kind: 'command', safety: 'safe' },
|
|
501
|
+
{ id: 'personas-clear', label: 'Clear active persona', detail: 'Return to the default Agent policy without deleting any persona.', command: '/personas clear', kind: 'command', safety: 'safe' },
|
|
502
|
+
],
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
id: 'skills',
|
|
506
|
+
group: 'LEARN',
|
|
507
|
+
label: 'Skills',
|
|
508
|
+
summary: 'Reusable local procedures the assistant can apply on demand.',
|
|
509
|
+
detail: 'Skills are local, reviewable procedures. Enabled skills inform the main conversation; secret-looking content is rejected.',
|
|
510
|
+
actions: [
|
|
511
|
+
{ id: 'skills-list', label: 'List skills', detail: 'Print the full local Agent skill library.', command: '/agent-skills list', kind: 'command', safety: 'read-only' },
|
|
512
|
+
{ id: 'skills-enabled', label: 'Enabled skills', detail: 'Show only skills currently injected into Agent guidance.', command: '/agent-skills enabled', kind: 'command', safety: 'read-only' },
|
|
513
|
+
{ id: 'skills-create', label: 'Create skill', detail: 'Create a reusable local procedure with real details. Placeholder commands are never dispatched.', command: '/agent-skills create --name <name> --description <summary> --procedure <steps>', kind: 'command', safety: 'safe' },
|
|
514
|
+
{ id: 'skills-enable', label: 'Enable skill', detail: 'Enable a local Agent skill by id or name.', command: '/agent-skills enable <id>', kind: 'command', safety: 'safe' },
|
|
515
|
+
{ id: 'skills-review', label: 'Review skill', detail: 'Mark a local skill reviewed after inspecting it.', command: '/agent-skills review <id>', kind: 'command', safety: 'safe' },
|
|
516
|
+
],
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
id: 'routines',
|
|
520
|
+
group: 'LEARN',
|
|
521
|
+
label: 'Routines',
|
|
522
|
+
summary: 'Repeatable workflows for the main conversation.',
|
|
523
|
+
detail: 'Routines run in the main conversation by default. Promotion to an external daemon schedule requires a real schedule command and --yes.',
|
|
524
|
+
actions: [
|
|
525
|
+
{ id: 'routines-list', label: 'List routines', detail: 'Print the full local Agent routine library.', command: '/routines list', kind: 'command', safety: 'read-only' },
|
|
526
|
+
{ id: 'routines-enabled', label: 'Enabled routines', detail: 'Show routines available for direct use.', command: '/routines enabled', kind: 'command', safety: 'read-only' },
|
|
527
|
+
{ id: 'routines-create', label: 'Create routine', detail: 'Create a repeatable workflow with real steps. Placeholder commands are never dispatched.', command: '/routines create --name <name> --description <summary> --steps <steps>', kind: 'command', safety: 'safe' },
|
|
528
|
+
{ id: 'routines-start', label: 'Start routine', detail: 'Start a local routine in the main conversation without creating a hidden job.', command: '/routines start <id>', kind: 'command', safety: 'safe' },
|
|
529
|
+
{ id: 'routines-promote', label: 'Promote to schedule', detail: 'Create an external daemon schedule from a reviewed routine only with real timing and --yes.', command: '/routines promote <id> --cron <expr> --yes', kind: 'command', safety: 'safe' },
|
|
530
|
+
{ id: 'routines-receipts', label: 'Promotion receipts', detail: 'Inspect local redacted routine schedule promotion receipts.', command: '/routines receipts', kind: 'command', safety: 'read-only' },
|
|
401
531
|
],
|
|
402
532
|
},
|
|
403
533
|
{
|
|
@@ -566,6 +696,31 @@ export class AgentWorkspace {
|
|
|
566
696
|
const action = this.selectedAction;
|
|
567
697
|
if (!action) return;
|
|
568
698
|
if (action.kind === 'guidance' || !action.command) {
|
|
699
|
+
if (action.kind === 'workspace' && action.targetCategoryId) {
|
|
700
|
+
const targetIndex = this.categories.findIndex((category) => category.id === action.targetCategoryId);
|
|
701
|
+
if (targetIndex >= 0) {
|
|
702
|
+
this.selectedCategoryIndex = targetIndex;
|
|
703
|
+
this.selectedActionIndex = 0;
|
|
704
|
+
this.focusActions();
|
|
705
|
+
this.status = `Opened ${this.selectedCategory.label}.`;
|
|
706
|
+
this.lastActionResult = {
|
|
707
|
+
kind: 'refreshed',
|
|
708
|
+
title: `Opened ${this.selectedCategory.label}`,
|
|
709
|
+
detail: action.detail,
|
|
710
|
+
safety: action.safety,
|
|
711
|
+
};
|
|
712
|
+
this.clampSelection();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
this.status = `Workspace area unavailable: ${action.targetCategoryId}.`;
|
|
716
|
+
this.lastActionResult = {
|
|
717
|
+
kind: 'error',
|
|
718
|
+
title: 'Workspace area unavailable',
|
|
719
|
+
detail: `No Agent workspace category exists for ${action.targetCategoryId}.`,
|
|
720
|
+
safety: action.safety,
|
|
721
|
+
};
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
569
724
|
this.status = action.detail;
|
|
570
725
|
this.lastActionResult = {
|
|
571
726
|
kind: 'guidance',
|
|
@@ -6,14 +6,14 @@ import type { ResolvedBuiltinPanelDeps } from './shared.ts';
|
|
|
6
6
|
export function registerKnowledgePanels(manager: PanelManager, deps: ResolvedBuiltinPanelDeps): void {
|
|
7
7
|
if (!deps.memoryRegistry) return;
|
|
8
8
|
|
|
9
|
-
const { memoryRegistry } = deps;
|
|
9
|
+
const { agentKnowledgeService, memoryRegistry } = deps;
|
|
10
10
|
manager.registerType({
|
|
11
11
|
id: 'knowledge',
|
|
12
12
|
name: 'Knowledge',
|
|
13
13
|
icon: 'K',
|
|
14
14
|
category: 'agent',
|
|
15
|
-
description: '
|
|
16
|
-
factory: () => new KnowledgePanel(memoryRegistry),
|
|
15
|
+
description: 'Isolated Agent Knowledge plus local non-secret memory review',
|
|
16
|
+
factory: () => new KnowledgePanel(memoryRegistry, agentKnowledgeService ?? null),
|
|
17
17
|
});
|
|
18
18
|
manager.registerType({
|
|
19
19
|
id: 'memory',
|
|
@@ -21,6 +21,7 @@ import type { SessionMemoryStore } from '@pellux/goodvibes-sdk/platform/core';
|
|
|
21
21
|
import type { ExecutionPlanManager } from '@pellux/goodvibes-sdk/platform/core';
|
|
22
22
|
import type { AdaptivePlanner } from '@pellux/goodvibes-sdk/platform/core';
|
|
23
23
|
import type { ProjectPlanningService } from '@pellux/goodvibes-sdk/platform/knowledge';
|
|
24
|
+
import type { KnowledgeService } from '@pellux/goodvibes-sdk/platform/knowledge';
|
|
24
25
|
import type { ApiTokenAuditor } from '@pellux/goodvibes-sdk/platform/security';
|
|
25
26
|
import type { ComponentHealthMonitor } from '../../runtime/perf/panel-health-monitor.ts';
|
|
26
27
|
import type { WorktreeRegistry } from '@/runtime/index.ts';
|
|
@@ -65,6 +66,8 @@ export interface BuiltinPanelDeps {
|
|
|
65
66
|
evalRegistry?: import('../eval-panel.ts').EvalRegistry;
|
|
66
67
|
/** MemoryRegistry for the Memory panel. */
|
|
67
68
|
memoryRegistry?: MemoryRegistry;
|
|
69
|
+
/** Isolated Agent Knowledge service for the Agent Knowledge panel. */
|
|
70
|
+
agentKnowledgeService?: Pick<KnowledgeService, 'getStatus'>;
|
|
68
71
|
/** Shared policy runtime state for governance/policy diagnostics. */
|
|
69
72
|
policyRuntimeState?: import('@/runtime/index.ts').PolicyRuntimeState;
|
|
70
73
|
/** Approval broker for control-plane/operator panels. */
|
|
@@ -2,9 +2,9 @@ import type { Line } from '../types/grid.ts';
|
|
|
2
2
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
3
3
|
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
4
4
|
import type { MemoryClass, MemoryRecord, MemoryRegistry, MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state';
|
|
5
|
+
import type { KnowledgeStatus } from '@pellux/goodvibes-sdk/platform/knowledge';
|
|
5
6
|
import {
|
|
6
7
|
buildBodyText,
|
|
7
|
-
buildEmptyState,
|
|
8
8
|
buildGuidanceLine,
|
|
9
9
|
buildKeyValueLine,
|
|
10
10
|
buildPanelLine,
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
DEFAULT_PANEL_PALETTE,
|
|
13
13
|
} from './polish.ts';
|
|
14
14
|
|
|
15
|
+
export interface AgentKnowledgePanelService {
|
|
16
|
+
readonly getStatus: () => Promise<KnowledgeStatus & { readonly note?: string }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
function summarize(records: MemoryRecord[], cls: MemoryClass): MemoryRecord[] {
|
|
16
20
|
return records.filter((record) => record.cls === cls).slice(0, 3);
|
|
17
21
|
}
|
|
@@ -42,19 +46,25 @@ function formatConfidence(confidence: number): string {
|
|
|
42
46
|
|
|
43
47
|
export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
|
|
44
48
|
private readonly registry: MemoryRegistry;
|
|
49
|
+
private readonly agentKnowledgeService: AgentKnowledgePanelService | null;
|
|
45
50
|
private unsubscribe?: () => void;
|
|
46
51
|
private records: MemoryRecord[] = [];
|
|
52
|
+
private agentKnowledgeStatus: (KnowledgeStatus & { readonly note?: string }) | null = null;
|
|
53
|
+
private agentKnowledgeError: string | null = null;
|
|
54
|
+
private agentKnowledgeLoading = false;
|
|
47
55
|
// I1: confirm for destructive review-state mutations
|
|
48
56
|
private confirm: ConfirmState<{ id: string; action: 'stale' | 'contradicted' }> | null = null;
|
|
49
57
|
|
|
50
|
-
public constructor(registry: MemoryRegistry) {
|
|
58
|
+
public constructor(registry: MemoryRegistry, agentKnowledgeService: AgentKnowledgePanelService | null = null) {
|
|
51
59
|
super('knowledge', 'Knowledge', 'K', 'agent');
|
|
52
60
|
this.registry = registry;
|
|
61
|
+
this.agentKnowledgeService = agentKnowledgeService;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
public override onActivate(): void {
|
|
56
65
|
super.onActivate();
|
|
57
66
|
this.refresh();
|
|
67
|
+
this.refreshAgentKnowledgeStatus();
|
|
58
68
|
this.unsubscribe = this.registry.subscribe(() => {
|
|
59
69
|
this.refresh();
|
|
60
70
|
this.markDirty();
|
|
@@ -89,12 +99,13 @@ export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
|
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
protected override getPalette() { return C; }
|
|
92
|
-
protected override getEmptyStateMessage() { return 'No
|
|
102
|
+
protected override getEmptyStateMessage() { return 'No Agent Knowledge sources or local memory review records'; }
|
|
93
103
|
protected override getEmptyStateActions() {
|
|
94
104
|
return [
|
|
95
|
-
{ command: '/
|
|
96
|
-
{ command: '/
|
|
97
|
-
{ command: '/
|
|
105
|
+
{ command: '/knowledge status', summary: 'inspect the isolated Agent Knowledge store' },
|
|
106
|
+
{ command: '/knowledge ingest-url <url> --yes', summary: 'ingest source-backed material into Agent Knowledge only' },
|
|
107
|
+
{ command: '/knowledge queue', summary: 'review Agent Knowledge issues' },
|
|
108
|
+
{ command: '/recall add fact <summary>', summary: 'capture a local non-secret memory record when appropriate' },
|
|
98
109
|
];
|
|
99
110
|
}
|
|
100
111
|
|
|
@@ -198,6 +209,64 @@ export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
|
|
|
198
209
|
this.clampSelection();
|
|
199
210
|
}
|
|
200
211
|
|
|
212
|
+
private refreshAgentKnowledgeStatus(): void {
|
|
213
|
+
if (!this.agentKnowledgeService || this.agentKnowledgeLoading) return;
|
|
214
|
+
this.agentKnowledgeLoading = true;
|
|
215
|
+
this.agentKnowledgeError = null;
|
|
216
|
+
this.agentKnowledgeService.getStatus()
|
|
217
|
+
.then((status) => {
|
|
218
|
+
this.agentKnowledgeStatus = status;
|
|
219
|
+
this.agentKnowledgeError = null;
|
|
220
|
+
})
|
|
221
|
+
.catch((error: unknown) => {
|
|
222
|
+
this.agentKnowledgeError = error instanceof Error ? error.message : String(error);
|
|
223
|
+
})
|
|
224
|
+
.finally(() => {
|
|
225
|
+
this.agentKnowledgeLoading = false;
|
|
226
|
+
this.markDirty();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private buildAgentKnowledgeHeader(width: number): Line[] {
|
|
231
|
+
const lines: Line[] = [
|
|
232
|
+
buildPanelLine(width, [[' Agent Knowledge Segment', C.label]]),
|
|
233
|
+
buildPanelLine(width, [
|
|
234
|
+
[' route ', C.label],
|
|
235
|
+
['/api/goodvibes-agent/knowledge/*', C.info],
|
|
236
|
+
[' isolated: no default Knowledge/Wiki or HomeGraph fallback', C.dim],
|
|
237
|
+
]),
|
|
238
|
+
];
|
|
239
|
+
if (this.agentKnowledgeLoading && !this.agentKnowledgeStatus) {
|
|
240
|
+
lines.push(buildPanelLine(width, [[' loading isolated Agent Knowledge status...', C.dim]]));
|
|
241
|
+
} else if (this.agentKnowledgeStatus) {
|
|
242
|
+
const status = this.agentKnowledgeStatus;
|
|
243
|
+
lines.push(buildKeyValueLine(width, [
|
|
244
|
+
{ label: 'Ready', value: status.ready ? 'yes' : 'no', valueColor: status.ready ? C.good : C.warn },
|
|
245
|
+
{ label: 'Sources', value: String(status.sourceCount), valueColor: status.sourceCount > 0 ? C.info : C.dim },
|
|
246
|
+
{ label: 'Nodes', value: String(status.nodeCount), valueColor: status.nodeCount > 0 ? C.info : C.dim },
|
|
247
|
+
{ label: 'Issues', value: String(status.issueCount), valueColor: status.issueCount > 0 ? C.warn : C.good },
|
|
248
|
+
], C));
|
|
249
|
+
const note = status.note ? ` note: ${status.note}` : '';
|
|
250
|
+
lines.push(buildPanelLine(width, [[' storage: ', C.label], [status.storagePath, C.dim], [note, C.dim]]));
|
|
251
|
+
} else {
|
|
252
|
+
lines.push(buildPanelLine(width, [[' status: not loaded; /knowledge status uses the same isolated route.', C.dim]]));
|
|
253
|
+
}
|
|
254
|
+
if (this.agentKnowledgeError) {
|
|
255
|
+
lines.push(...buildBodyText(width, `Agent Knowledge status warning: ${this.agentKnowledgeError}`, C, C.warn));
|
|
256
|
+
}
|
|
257
|
+
lines.push(buildPanelLine(width, [
|
|
258
|
+
[' actions ', C.label],
|
|
259
|
+
['/knowledge status', C.value],
|
|
260
|
+
[' | ', C.dim],
|
|
261
|
+
['/knowledge ingest-url <url> --yes', C.value],
|
|
262
|
+
[' | ', C.dim],
|
|
263
|
+
['/knowledge search <query>', C.value],
|
|
264
|
+
[' | ', C.dim],
|
|
265
|
+
['/knowledge queue', C.value],
|
|
266
|
+
]));
|
|
267
|
+
return lines;
|
|
268
|
+
}
|
|
269
|
+
|
|
201
270
|
public render(width: number, height: number): Line[] {
|
|
202
271
|
this.clampSelection();
|
|
203
272
|
|
|
@@ -214,12 +283,14 @@ export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
|
|
|
214
283
|
|
|
215
284
|
if (this.records.length === 0) this.refresh();
|
|
216
285
|
|
|
217
|
-
const intro = '
|
|
286
|
+
const intro = 'Isolated Agent Knowledge plus local non-secret memory review. This surface never falls back to default Knowledge/Wiki or HomeGraph.';
|
|
218
287
|
const records = this.registry.search({ limit: 200 });
|
|
288
|
+
const agentKnowledgeHeader = this.buildAgentKnowledgeHeader(width);
|
|
219
289
|
|
|
220
290
|
if (records.length === 0) {
|
|
221
291
|
return this.renderList(width, height, {
|
|
222
292
|
title: 'Knowledge Control Room',
|
|
293
|
+
header: agentKnowledgeHeader,
|
|
223
294
|
footer: [buildPanelLine(width, [[' Review keys: Up/Down move r/Enter review s stale c contradicted f fresh', C.dim]])],
|
|
224
295
|
});
|
|
225
296
|
}
|
|
@@ -334,11 +405,11 @@ export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
|
|
|
334
405
|
|
|
335
406
|
return this.renderList(width, height, {
|
|
336
407
|
title: 'Knowledge Control Room',
|
|
337
|
-
header: [...classLines, ...reviewLines],
|
|
408
|
+
header: [...agentKnowledgeHeader, ...classLines, ...reviewLines],
|
|
338
409
|
footer: [
|
|
410
|
+
buildPanelLine(width, [[' Up/Down move r/Enter reviewed s stale c contradicted f fresh', C.dim]]),
|
|
339
411
|
...(selectedLines.length > 0 ? selectedLines : []),
|
|
340
412
|
...recentSummaryLines,
|
|
341
|
-
buildPanelLine(width, [[' Up/Down move r/Enter reviewed s stale c contradicted f fresh', C.dim]]),
|
|
342
413
|
],
|
|
343
414
|
});
|
|
344
415
|
}
|
|
@@ -67,6 +67,7 @@ function buildLeftRows(workspace: AgentWorkspace, height: number): WorkspaceRow[
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function actionCommand(action: AgentWorkspaceAction): string {
|
|
70
|
+
if (action.kind === 'workspace') return action.targetCategoryId ? `open ${action.targetCategoryId}` : '(workspace)';
|
|
70
71
|
return action.command ?? '(guidance)';
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -99,6 +100,40 @@ function setupChecklistLines(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLi
|
|
|
99
100
|
return lines;
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
function localLibraryLines(
|
|
104
|
+
title: string,
|
|
105
|
+
items: readonly AgentWorkspaceRuntimeSnapshot['localPersonas'][number][],
|
|
106
|
+
emptyText: string,
|
|
107
|
+
): ContextLine[] {
|
|
108
|
+
const lines: ContextLine[] = [
|
|
109
|
+
{ text: title, fg: PALETTE.title, bold: true },
|
|
110
|
+
];
|
|
111
|
+
if (items.length === 0) {
|
|
112
|
+
lines.push({ text: emptyText, fg: PALETTE.warn });
|
|
113
|
+
return lines;
|
|
114
|
+
}
|
|
115
|
+
for (const item of items.slice(0, 8)) {
|
|
116
|
+
const status = [
|
|
117
|
+
item.active ? 'active' : '',
|
|
118
|
+
item.enabled === true ? 'enabled' : item.enabled === false ? 'disabled' : '',
|
|
119
|
+
item.reviewState,
|
|
120
|
+
item.startCount !== undefined ? `starts ${item.startCount}` : '',
|
|
121
|
+
].filter(Boolean).join(' / ');
|
|
122
|
+
const tags = item.tags.length > 0 ? ` tags=${item.tags.join(',')}` : '';
|
|
123
|
+
const triggers = item.triggers.length > 0 ? ` triggers=${item.triggers.join(',')}` : '';
|
|
124
|
+
lines.push({
|
|
125
|
+
text: `${item.id}: ${item.name} (${status})`,
|
|
126
|
+
fg: item.reviewState === 'stale' ? PALETTE.warn : PALETTE.info,
|
|
127
|
+
bold: item.active === true,
|
|
128
|
+
});
|
|
129
|
+
lines.push({ text: ` ${item.description}${tags}${triggers}`, fg: PALETTE.muted });
|
|
130
|
+
}
|
|
131
|
+
if (items.length > 8) {
|
|
132
|
+
lines.push({ text: `${items.length - 8} more item(s). Open the library command for the full list.`, fg: PALETTE.dim });
|
|
133
|
+
}
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
|
|
102
137
|
function snapshotLines(category: AgentWorkspaceCategory, snapshot: AgentWorkspaceRuntimeSnapshot | null): ContextLine[] {
|
|
103
138
|
if (!snapshot) return [{ text: 'Runtime context is not loaded yet.', fg: PALETTE.warn }];
|
|
104
139
|
const base: ContextLine[] = [{ text: 'Live Agent Context', fg: PALETTE.title, bold: true }];
|
|
@@ -177,6 +212,30 @@ function snapshotLines(category: AgentWorkspaceCategory, snapshot: AgentWorkspac
|
|
|
177
212
|
{ text: 'Durable memory, routines, skills, and personas remain Agent-local until shared registry contracts exist.', fg: PALETTE.good },
|
|
178
213
|
{ text: 'Secrets are rejected/redacted; store secret references instead of secret values.', fg: PALETTE.warn },
|
|
179
214
|
);
|
|
215
|
+
} else if (category.id === 'personas') {
|
|
216
|
+
base.push(
|
|
217
|
+
{ text: `Personas: ${snapshot.localPersonaCount}; active: ${snapshot.activePersonaName}`, fg: PALETTE.info },
|
|
218
|
+
{ text: 'Personas are local behavior profiles for the serial main-conversation assistant, not spawned agents.', fg: PALETTE.good },
|
|
219
|
+
{ text: 'Use them for tone, role, domain constraints, tool posture, and repeatable operating preferences.', fg: PALETTE.muted },
|
|
220
|
+
{ text: '' },
|
|
221
|
+
...localLibraryLines('Persona Library', snapshot.localPersonas, 'No local personas yet. Create one with /personas create ...'),
|
|
222
|
+
);
|
|
223
|
+
} else if (category.id === 'skills') {
|
|
224
|
+
base.push(
|
|
225
|
+
{ text: `Skills: ${snapshot.localSkillCount}; enabled: ${snapshot.enabledSkillCount}`, fg: PALETTE.info },
|
|
226
|
+
{ text: 'Skills are reusable local procedures the assistant can apply from the main conversation.', fg: PALETTE.good },
|
|
227
|
+
{ text: 'Enabled skills are injected as operating guidance; secret-looking content is rejected.', fg: PALETTE.warn },
|
|
228
|
+
{ text: '' },
|
|
229
|
+
...localLibraryLines('Skill Library', snapshot.localSkills, 'No local skills yet. Create one with /agent-skills create ...'),
|
|
230
|
+
);
|
|
231
|
+
} else if (category.id === 'routines') {
|
|
232
|
+
base.push(
|
|
233
|
+
{ text: `Routines: ${snapshot.localRoutineCount}; enabled: ${snapshot.enabledRoutineCount}`, fg: PALETTE.info },
|
|
234
|
+
{ text: 'Routines are repeatable main-conversation workflows. Starting one does not create hidden jobs.', fg: PALETTE.good },
|
|
235
|
+
{ text: 'Scheduling a reviewed routine is explicit and writes to the externally owned daemon only with --yes.', fg: PALETTE.warn },
|
|
236
|
+
{ text: '' },
|
|
237
|
+
...localLibraryLines('Routine Library', snapshot.localRoutines, 'No local routines yet. Create one with /routines create ...'),
|
|
238
|
+
);
|
|
180
239
|
} else if (category.id === 'work') {
|
|
181
240
|
base.push(
|
|
182
241
|
{ text: 'Work plan and approvals are read or explicitly confirmed through public operator routes.', fg: PALETTE.info },
|
|
@@ -30,6 +30,7 @@ import { registerBootstrapRuntimeEvents } from '@/runtime/index.ts';
|
|
|
30
30
|
import { createRuntimeServices, type RuntimeServices } from './services.ts';
|
|
31
31
|
import { createUiRuntimeServices, type UiRuntimeServices } from './ui-services.ts';
|
|
32
32
|
import { installAgentToolPolicyGuard } from '../tools/wrfc-agent-guard.ts';
|
|
33
|
+
import { registerAgentLocalRegistryTool } from '../tools/agent-local-registry-tool.ts';
|
|
33
34
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
34
35
|
|
|
35
36
|
export interface BootstrapCoreState {
|
|
@@ -227,6 +228,7 @@ export async function initializeBootstrapCore(
|
|
|
227
228
|
overflowHandler: services.overflowHandler,
|
|
228
229
|
changeTracker: services.sessionChangeTracker,
|
|
229
230
|
});
|
|
231
|
+
registerAgentLocalRegistryTool(toolRegistry, services.shellPaths);
|
|
230
232
|
installAgentToolPolicyGuard(toolRegistry, {
|
|
231
233
|
getLastUserMessage: () => conversation.getLastUserMessage(),
|
|
232
234
|
});
|
|
@@ -135,6 +135,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
135
135
|
sandboxSessionRegistry: services.sandboxSessionRegistry,
|
|
136
136
|
systemMessagesPanel,
|
|
137
137
|
memoryRegistry: services.memoryRegistry,
|
|
138
|
+
agentKnowledgeService: services.agentKnowledgeService,
|
|
138
139
|
uiServices,
|
|
139
140
|
pluginManager: services.pluginManager,
|
|
140
141
|
hookDispatcher: services.hookDispatcher,
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -48,6 +48,7 @@ const GOODVIBES_AGENT_OPERATOR_POLICY = [
|
|
|
48
48
|
'## GoodVibes Agent Operator Policy',
|
|
49
49
|
'- Default to serial, proactive assistant work in the main conversation. Answer, inspect, summarize, remember useful non-secret facts, configure local Agent state, use read-only daemon/operator routes, and take safe non-destructive actions without spawning local agents or WRFC.',
|
|
50
50
|
'- GoodVibes Agent connects to an externally managed GoodVibes daemon. Do not start, stop, restart, install, expose, or mutate daemon/listener/control-plane surface posture from Agent runtime.',
|
|
51
|
+
'- Use the `agent_local_registry` tool when a reusable persona, skill, or routine would improve future work. Keep those records local, non-secret, source/provenance tagged, and reviewable. Starting a routine means applying its steps in this same serial conversation, not creating a background job.',
|
|
51
52
|
'- WRFC is never the default Agent reasoning path. Do not create local WRFC chains for planning, research, operations, knowledge, memory, configuration, approvals, automation observability, or ordinary assistant work.',
|
|
52
53
|
'- GoodVibes Agent is not the coding TUI. Do not use the `agent` tool to spawn local Engineer, Reviewer, Tester, Verifier, or batch-spawn roots from Agent.',
|
|
53
54
|
'- When the user explicitly asks to build, implement, fix, patch, or review code, preserve the full original user ask and delegate one build request to GoodVibes TUI through the public shared-session/build-delegation contract. Include clear executionIntent and request WRFC only for explicit build/fix/review work or when the user explicitly asks for WRFC/agent review.',
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import type { Tool } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
|
+
import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
|
|
3
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
|
+
import { AgentPersonaRegistry, type AgentPersonaRecord } from '../agent/persona-registry.ts';
|
|
5
|
+
import { AgentRoutineRegistry, type AgentRoutineRecord } from '../agent/routine-registry.ts';
|
|
6
|
+
import { AgentSkillRegistry, type AgentSkillRecord } from '../agent/skill-registry.ts';
|
|
7
|
+
|
|
8
|
+
export type AgentLocalRegistryDomain = 'persona' | 'skill' | 'routine';
|
|
9
|
+
export type AgentLocalRegistryAction =
|
|
10
|
+
| 'list'
|
|
11
|
+
| 'search'
|
|
12
|
+
| 'get'
|
|
13
|
+
| 'create'
|
|
14
|
+
| 'update'
|
|
15
|
+
| 'enable'
|
|
16
|
+
| 'disable'
|
|
17
|
+
| 'review'
|
|
18
|
+
| 'stale'
|
|
19
|
+
| 'use'
|
|
20
|
+
| 'clear_active'
|
|
21
|
+
| 'start';
|
|
22
|
+
|
|
23
|
+
export interface AgentLocalRegistryToolArgs {
|
|
24
|
+
readonly domain?: unknown;
|
|
25
|
+
readonly action?: unknown;
|
|
26
|
+
readonly id?: unknown;
|
|
27
|
+
readonly query?: unknown;
|
|
28
|
+
readonly name?: unknown;
|
|
29
|
+
readonly description?: unknown;
|
|
30
|
+
readonly body?: unknown;
|
|
31
|
+
readonly procedure?: unknown;
|
|
32
|
+
readonly steps?: unknown;
|
|
33
|
+
readonly triggers?: unknown;
|
|
34
|
+
readonly tags?: unknown;
|
|
35
|
+
readonly reason?: unknown;
|
|
36
|
+
readonly enabled?: unknown;
|
|
37
|
+
readonly provenance?: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DOMAINS: readonly AgentLocalRegistryDomain[] = ['persona', 'skill', 'routine'];
|
|
41
|
+
const ACTIONS: readonly AgentLocalRegistryAction[] = [
|
|
42
|
+
'list',
|
|
43
|
+
'search',
|
|
44
|
+
'get',
|
|
45
|
+
'create',
|
|
46
|
+
'update',
|
|
47
|
+
'enable',
|
|
48
|
+
'disable',
|
|
49
|
+
'review',
|
|
50
|
+
'stale',
|
|
51
|
+
'use',
|
|
52
|
+
'clear_active',
|
|
53
|
+
'start',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function isDomain(value: unknown): value is AgentLocalRegistryDomain {
|
|
57
|
+
return typeof value === 'string' && DOMAINS.includes(value as AgentLocalRegistryDomain);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isAction(value: unknown): value is AgentLocalRegistryAction {
|
|
61
|
+
return typeof value === 'string' && ACTIONS.includes(value as AgentLocalRegistryAction);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readString(value: unknown): string {
|
|
65
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readStringList(value: unknown): readonly string[] {
|
|
69
|
+
if (typeof value === 'string') {
|
|
70
|
+
return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
if (!Array.isArray(value)) return [];
|
|
73
|
+
return value.filter((entry): entry is string => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function registryError(message: string): { readonly success: false; readonly error: string } {
|
|
77
|
+
return { success: false, error: message };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function registryOutput(output: string): { readonly success: true; readonly output: string } {
|
|
81
|
+
return { success: true, output };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function requireId(args: AgentLocalRegistryToolArgs): string {
|
|
85
|
+
const id = readString(args.id);
|
|
86
|
+
if (!id) throw new Error('id is required.');
|
|
87
|
+
return id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function requireName(args: AgentLocalRegistryToolArgs): string {
|
|
91
|
+
const name = readString(args.name);
|
|
92
|
+
if (!name) throw new Error('name is required.');
|
|
93
|
+
return name;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function requireDescription(args: AgentLocalRegistryToolArgs): string {
|
|
97
|
+
const description = readString(args.description);
|
|
98
|
+
if (!description) throw new Error('description is required.');
|
|
99
|
+
return description;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatPersona(persona: AgentPersonaRecord, activeId: string | null): string {
|
|
103
|
+
const active = persona.id === activeId ? 'active' : 'inactive';
|
|
104
|
+
return `${persona.id} ${active} ${persona.reviewState} ${persona.name} - ${persona.description}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatSkill(skill: AgentSkillRecord): string {
|
|
108
|
+
const enabled = skill.enabled ? 'enabled' : 'disabled';
|
|
109
|
+
return `${skill.id} ${enabled} ${skill.reviewState} ${skill.name} - ${skill.description}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatRoutine(routine: AgentRoutineRecord): string {
|
|
113
|
+
const enabled = routine.enabled ? 'enabled' : 'disabled';
|
|
114
|
+
return `${routine.id} ${enabled} ${routine.reviewState} starts=${routine.startCount} ${routine.name} - ${routine.description}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function listPersonas(registry: AgentPersonaRegistry, records: readonly AgentPersonaRecord[], title: string): string {
|
|
118
|
+
const snapshot = registry.snapshot();
|
|
119
|
+
return records.length === 0
|
|
120
|
+
? `${title}\nNo Agent-local personas.`
|
|
121
|
+
: [title, ...records.map((persona) => formatPersona(persona, snapshot.activePersonaId))].join('\n');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function listSkills(records: readonly AgentSkillRecord[], title: string): string {
|
|
125
|
+
return records.length === 0
|
|
126
|
+
? `${title}\nNo Agent-local skills.`
|
|
127
|
+
: [title, ...records.map(formatSkill)].join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function listRoutines(records: readonly AgentRoutineRecord[], title: string): string {
|
|
131
|
+
return records.length === 0
|
|
132
|
+
? `${title}\nNo Agent-local routines.`
|
|
133
|
+
: [title, ...records.map(formatRoutine)].join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function handlePersona(shellPaths: ShellPathService, action: AgentLocalRegistryAction, args: AgentLocalRegistryToolArgs): string {
|
|
137
|
+
const registry = AgentPersonaRegistry.fromShellPaths(shellPaths);
|
|
138
|
+
if (action === 'list') return listPersonas(registry, registry.list(), 'Agent-local personas');
|
|
139
|
+
if (action === 'search') return listPersonas(registry, registry.search(readString(args.query)), 'Agent-local personas search');
|
|
140
|
+
if (action === 'get') {
|
|
141
|
+
const persona = registry.get(requireId(args));
|
|
142
|
+
if (!persona) return `Unknown Agent-local persona: ${readString(args.id)}`;
|
|
143
|
+
return [
|
|
144
|
+
formatPersona(persona, registry.snapshot().activePersonaId),
|
|
145
|
+
`triggers: ${persona.triggers.join(', ') || '(manual)'}`,
|
|
146
|
+
`tags: ${persona.tags.join(', ') || '(none)'}`,
|
|
147
|
+
'',
|
|
148
|
+
persona.body,
|
|
149
|
+
].join('\n');
|
|
150
|
+
}
|
|
151
|
+
if (action === 'create') {
|
|
152
|
+
const persona = registry.create({
|
|
153
|
+
name: requireName(args),
|
|
154
|
+
description: requireDescription(args),
|
|
155
|
+
body: readString(args.body),
|
|
156
|
+
tags: readStringList(args.tags),
|
|
157
|
+
triggers: readStringList(args.triggers),
|
|
158
|
+
source: 'agent',
|
|
159
|
+
provenance: readString(args.provenance) || 'agent-local-registry-tool',
|
|
160
|
+
});
|
|
161
|
+
return `Created Agent-local persona ${persona.id}: ${persona.name}`;
|
|
162
|
+
}
|
|
163
|
+
if (action === 'update') {
|
|
164
|
+
const persona = registry.update(requireId(args), {
|
|
165
|
+
name: readString(args.name) || undefined,
|
|
166
|
+
description: readString(args.description) || undefined,
|
|
167
|
+
body: readString(args.body) || undefined,
|
|
168
|
+
tags: args.tags === undefined ? undefined : readStringList(args.tags),
|
|
169
|
+
triggers: args.triggers === undefined ? undefined : readStringList(args.triggers),
|
|
170
|
+
provenance: readString(args.provenance) || 'agent-local-registry-tool',
|
|
171
|
+
});
|
|
172
|
+
return `Updated Agent-local persona ${persona.id}: ${persona.name}`;
|
|
173
|
+
}
|
|
174
|
+
if (action === 'use') {
|
|
175
|
+
const persona = registry.setActive(requireId(args));
|
|
176
|
+
return `Active Agent-local persona set to ${persona.id}: ${persona.name}`;
|
|
177
|
+
}
|
|
178
|
+
if (action === 'clear_active') {
|
|
179
|
+
registry.clearActive();
|
|
180
|
+
return 'Cleared active Agent-local persona.';
|
|
181
|
+
}
|
|
182
|
+
if (action === 'review') return `Reviewed Agent-local persona ${registry.markReviewed(requireId(args)).id}.`;
|
|
183
|
+
if (action === 'stale') return `Marked Agent-local persona ${registry.markStale(requireId(args), readString(args.reason)).id} stale.`;
|
|
184
|
+
throw new Error(`Action ${action} is not valid for personas.`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function handleSkill(shellPaths: ShellPathService, action: AgentLocalRegistryAction, args: AgentLocalRegistryToolArgs): string {
|
|
188
|
+
const registry = AgentSkillRegistry.fromShellPaths(shellPaths);
|
|
189
|
+
if (action === 'list') return listSkills(registry.list(), 'Agent-local skills');
|
|
190
|
+
if (action === 'search') return listSkills(registry.search(readString(args.query)), 'Agent-local skills search');
|
|
191
|
+
if (action === 'get') {
|
|
192
|
+
const skill = registry.get(requireId(args));
|
|
193
|
+
if (!skill) return `Unknown Agent-local skill: ${readString(args.id)}`;
|
|
194
|
+
return [
|
|
195
|
+
formatSkill(skill),
|
|
196
|
+
`triggers: ${skill.triggers.join(', ') || '(manual)'}`,
|
|
197
|
+
`tags: ${skill.tags.join(', ') || '(none)'}`,
|
|
198
|
+
'',
|
|
199
|
+
skill.procedure,
|
|
200
|
+
].join('\n');
|
|
201
|
+
}
|
|
202
|
+
if (action === 'create') {
|
|
203
|
+
const skill = registry.create({
|
|
204
|
+
name: requireName(args),
|
|
205
|
+
description: requireDescription(args),
|
|
206
|
+
procedure: readString(args.procedure),
|
|
207
|
+
triggers: readStringList(args.triggers),
|
|
208
|
+
tags: readStringList(args.tags),
|
|
209
|
+
enabled: args.enabled === true,
|
|
210
|
+
source: 'agent',
|
|
211
|
+
provenance: readString(args.provenance) || 'agent-local-registry-tool',
|
|
212
|
+
});
|
|
213
|
+
return `Created Agent-local skill ${skill.id}: ${skill.name}`;
|
|
214
|
+
}
|
|
215
|
+
if (action === 'update') {
|
|
216
|
+
const skill = registry.update(requireId(args), {
|
|
217
|
+
name: readString(args.name) || undefined,
|
|
218
|
+
description: readString(args.description) || undefined,
|
|
219
|
+
procedure: readString(args.procedure) || undefined,
|
|
220
|
+
triggers: args.triggers === undefined ? undefined : readStringList(args.triggers),
|
|
221
|
+
tags: args.tags === undefined ? undefined : readStringList(args.tags),
|
|
222
|
+
provenance: readString(args.provenance) || 'agent-local-registry-tool',
|
|
223
|
+
});
|
|
224
|
+
return `Updated Agent-local skill ${skill.id}: ${skill.name}`;
|
|
225
|
+
}
|
|
226
|
+
if (action === 'enable' || action === 'disable') {
|
|
227
|
+
const skill = registry.setEnabled(requireId(args), action === 'enable');
|
|
228
|
+
return `${action === 'enable' ? 'Enabled' : 'Disabled'} Agent-local skill ${skill.id}: ${skill.name}`;
|
|
229
|
+
}
|
|
230
|
+
if (action === 'review') return `Reviewed Agent-local skill ${registry.markReviewed(requireId(args)).id}.`;
|
|
231
|
+
if (action === 'stale') return `Marked Agent-local skill ${registry.markStale(requireId(args), readString(args.reason)).id} stale.`;
|
|
232
|
+
throw new Error(`Action ${action} is not valid for skills.`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function handleRoutine(shellPaths: ShellPathService, action: AgentLocalRegistryAction, args: AgentLocalRegistryToolArgs): string {
|
|
236
|
+
const registry = AgentRoutineRegistry.fromShellPaths(shellPaths);
|
|
237
|
+
if (action === 'list') return listRoutines(registry.list(), 'Agent-local routines');
|
|
238
|
+
if (action === 'search') return listRoutines(registry.search(readString(args.query)), 'Agent-local routines search');
|
|
239
|
+
if (action === 'get') {
|
|
240
|
+
const routine = registry.get(requireId(args));
|
|
241
|
+
if (!routine) return `Unknown Agent-local routine: ${readString(args.id)}`;
|
|
242
|
+
return [
|
|
243
|
+
formatRoutine(routine),
|
|
244
|
+
`triggers: ${routine.triggers.join(', ') || '(manual)'}`,
|
|
245
|
+
`tags: ${routine.tags.join(', ') || '(none)'}`,
|
|
246
|
+
'',
|
|
247
|
+
routine.steps,
|
|
248
|
+
].join('\n');
|
|
249
|
+
}
|
|
250
|
+
if (action === 'create') {
|
|
251
|
+
const routine = registry.create({
|
|
252
|
+
name: requireName(args),
|
|
253
|
+
description: requireDescription(args),
|
|
254
|
+
steps: readString(args.steps),
|
|
255
|
+
triggers: readStringList(args.triggers),
|
|
256
|
+
tags: readStringList(args.tags),
|
|
257
|
+
enabled: args.enabled === true,
|
|
258
|
+
source: 'agent',
|
|
259
|
+
provenance: readString(args.provenance) || 'agent-local-registry-tool',
|
|
260
|
+
});
|
|
261
|
+
return `Created Agent-local routine ${routine.id}: ${routine.name}`;
|
|
262
|
+
}
|
|
263
|
+
if (action === 'update') {
|
|
264
|
+
const routine = registry.update(requireId(args), {
|
|
265
|
+
name: readString(args.name) || undefined,
|
|
266
|
+
description: readString(args.description) || undefined,
|
|
267
|
+
steps: readString(args.steps) || undefined,
|
|
268
|
+
triggers: args.triggers === undefined ? undefined : readStringList(args.triggers),
|
|
269
|
+
tags: args.tags === undefined ? undefined : readStringList(args.tags),
|
|
270
|
+
provenance: readString(args.provenance) || 'agent-local-registry-tool',
|
|
271
|
+
});
|
|
272
|
+
return `Updated Agent-local routine ${routine.id}: ${routine.name}`;
|
|
273
|
+
}
|
|
274
|
+
if (action === 'enable' || action === 'disable') {
|
|
275
|
+
const routine = registry.setEnabled(requireId(args), action === 'enable');
|
|
276
|
+
return `${action === 'enable' ? 'Enabled' : 'Disabled'} Agent-local routine ${routine.id}: ${routine.name}`;
|
|
277
|
+
}
|
|
278
|
+
if (action === 'start') {
|
|
279
|
+
const routine = registry.markStarted(requireId(args));
|
|
280
|
+
return [
|
|
281
|
+
`Started Agent-local routine ${routine.id}: ${routine.name}`,
|
|
282
|
+
'Policy: same main conversation; no hidden background job, daemon mutation, or external side effect was started.',
|
|
283
|
+
'',
|
|
284
|
+
routine.steps,
|
|
285
|
+
].join('\n');
|
|
286
|
+
}
|
|
287
|
+
if (action === 'review') return `Reviewed Agent-local routine ${registry.markReviewed(requireId(args)).id}.`;
|
|
288
|
+
if (action === 'stale') return `Marked Agent-local routine ${registry.markStale(requireId(args), readString(args.reason)).id} stale.`;
|
|
289
|
+
throw new Error(`Action ${action} is not valid for routines.`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function createAgentLocalRegistryTool(shellPaths: ShellPathService): Tool {
|
|
293
|
+
return {
|
|
294
|
+
definition: {
|
|
295
|
+
name: 'agent_local_registry',
|
|
296
|
+
description: [
|
|
297
|
+
'Inspect and maintain GoodVibes Agent-local personas, skills, and routines from the main conversation.',
|
|
298
|
+
'Use this for safe self-improvement: create or refine reusable behavior, enable skills/routines, choose personas, review/stale records, and start routines in the same serial conversation.',
|
|
299
|
+
'This tool cannot delete records, create schedules, mutate the daemon, send messages, run background jobs, or delegate build work.',
|
|
300
|
+
].join(' '),
|
|
301
|
+
parameters: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
properties: {
|
|
304
|
+
domain: { type: 'string', enum: [...DOMAINS] },
|
|
305
|
+
action: { type: 'string', enum: [...ACTIONS] },
|
|
306
|
+
id: { type: 'string' },
|
|
307
|
+
query: { type: 'string' },
|
|
308
|
+
name: { type: 'string' },
|
|
309
|
+
description: { type: 'string' },
|
|
310
|
+
body: { type: 'string', description: 'Persona body/instructions.' },
|
|
311
|
+
procedure: { type: 'string', description: 'Skill procedure.' },
|
|
312
|
+
steps: { type: 'string', description: 'Routine steps.' },
|
|
313
|
+
triggers: { type: 'array', items: { type: 'string' } },
|
|
314
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
315
|
+
reason: { type: 'string' },
|
|
316
|
+
enabled: { type: 'boolean' },
|
|
317
|
+
provenance: { type: 'string' },
|
|
318
|
+
},
|
|
319
|
+
required: ['domain', 'action'],
|
|
320
|
+
additionalProperties: false,
|
|
321
|
+
},
|
|
322
|
+
sideEffects: ['state'],
|
|
323
|
+
},
|
|
324
|
+
execute: async (rawArgs: unknown) => {
|
|
325
|
+
const args = rawArgs as AgentLocalRegistryToolArgs;
|
|
326
|
+
if (!isDomain(args.domain)) return registryError(`Unknown domain. Valid: ${DOMAINS.join(', ')}.`);
|
|
327
|
+
if (!isAction(args.action)) return registryError(`Unknown action. Valid: ${ACTIONS.join(', ')}.`);
|
|
328
|
+
try {
|
|
329
|
+
if (args.domain === 'persona') return registryOutput(handlePersona(shellPaths, args.action, args));
|
|
330
|
+
if (args.domain === 'skill') return registryOutput(handleSkill(shellPaths, args.action, args));
|
|
331
|
+
return registryOutput(handleRoutine(shellPaths, args.action, args));
|
|
332
|
+
} catch (error) {
|
|
333
|
+
return registryError(error instanceof Error ? error.message : String(error));
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function registerAgentLocalRegistryTool(registry: ToolRegistry, shellPaths: ShellPathService): void {
|
|
340
|
+
registry.register(createAgentLocalRegistryTool(shellPaths));
|
|
341
|
+
}
|
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 = '0.1.
|
|
9
|
+
let _version = '0.1.50';
|
|
10
10
|
let _sdkVersion = '0.33.35';
|
|
11
11
|
try {
|
|
12
12
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {
|