@pellux/goodvibes-agent 1.0.1 → 1.0.2
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 +7 -0
- package/README.md +8 -4
- package/dist/package/main.js +13750 -12196
- package/docs/README.md +4 -2
- package/docs/channels-remote-and-api.md +4 -0
- package/docs/connected-host.md +2 -0
- package/docs/getting-started.md +6 -2
- package/docs/knowledge-artifacts-and-multimodal.md +5 -3
- package/docs/project-planning.md +3 -1
- package/docs/providers-and-routing.md +3 -0
- package/docs/release-and-publishing.md +7 -6
- package/docs/tools-and-commands.md +97 -2
- package/docs/voice-and-live-tts.md +2 -0
- package/package.json +1 -1
- package/src/agent/harness-control.ts +266 -0
- package/src/config/agent-settings-policy.ts +44 -0
- package/src/input/agent-workspace-activation.ts +14 -6
- package/src/input/commands/operator-runtime.ts +139 -3
- package/src/input/settings-modal-agent-policy.ts +5 -44
- package/src/runtime/bootstrap.ts +3 -0
- package/src/tools/agent-harness-local-operations.ts +233 -0
- package/src/tools/agent-harness-metadata.ts +300 -0
- package/src/tools/agent-harness-tool.ts +774 -0
- package/src/tools/agent-local-registry-requirements.ts +18 -0
- package/src/tools/agent-local-registry-tool.ts +88 -34
- package/src/version.ts +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const AGENT_EXTERNAL_HOST_SETTING_LOCK_REASON = 'GoodVibes Agent uses a connected GoodVibes host. Change host lifecycle and bind posture from the owning host; Agent settings are read-only for those controls.';
|
|
2
|
+
|
|
3
|
+
const AGENT_HIDDEN_SETTING_PREFIXES = [
|
|
4
|
+
['cloud', 'flare.'].join(''),
|
|
5
|
+
['surfaces.', 'home', 'assistant.'].join(''),
|
|
6
|
+
'batch.',
|
|
7
|
+
'controlPlane.',
|
|
8
|
+
'danger.',
|
|
9
|
+
'httpListener.',
|
|
10
|
+
'network.',
|
|
11
|
+
'orchestration.',
|
|
12
|
+
'runtime.',
|
|
13
|
+
'service.',
|
|
14
|
+
'sandbox.',
|
|
15
|
+
'web.',
|
|
16
|
+
'watchers.',
|
|
17
|
+
'wrfc.',
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
const AGENT_HIDDEN_SETTING_KEYS = new Set<string>([
|
|
21
|
+
'ui.wrfcMessages',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const EXTERNAL_HOST_SETTING_PREFIXES = [
|
|
25
|
+
'service.',
|
|
26
|
+
'controlPlane.',
|
|
27
|
+
'httpListener.',
|
|
28
|
+
'web.',
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
const EXTERNAL_HOST_SETTING_KEYS = new Set<string>([
|
|
32
|
+
'danger.daemon',
|
|
33
|
+
'danger.httpListener',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export function isExternalHostOwnedSettingKey(key: string): boolean {
|
|
37
|
+
return EXTERNAL_HOST_SETTING_KEYS.has(key)
|
|
38
|
+
|| EXTERNAL_HOST_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isAgentHiddenSettingKey(key: string): boolean {
|
|
42
|
+
return AGENT_HIDDEN_SETTING_KEYS.has(key)
|
|
43
|
+
|| AGENT_HIDDEN_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
44
|
+
}
|
|
@@ -9,12 +9,14 @@ import type {
|
|
|
9
9
|
AgentWorkspaceActionResult,
|
|
10
10
|
AgentWorkspaceCategory,
|
|
11
11
|
AgentWorkspaceCommandDispatcher,
|
|
12
|
+
AgentWorkspaceEditorKind,
|
|
12
13
|
AgentWorkspaceFocusPane,
|
|
13
14
|
AgentWorkspaceLocalEditor,
|
|
14
15
|
AgentWorkspaceLocalEditorKind,
|
|
15
16
|
AgentWorkspaceLocalLibraryItem,
|
|
16
17
|
AgentWorkspaceLocalOperation,
|
|
17
18
|
AgentWorkspaceRuntimeSnapshot,
|
|
19
|
+
AgentWorkspaceRuntimeStarterTemplateItem,
|
|
18
20
|
} from './agent-workspace-types.ts';
|
|
19
21
|
|
|
20
22
|
interface AgentWorkspaceActivationHost {
|
|
@@ -55,7 +57,10 @@ export function activateAgentWorkspaceSelection(
|
|
|
55
57
|
const action = workspace.selectedAction;
|
|
56
58
|
if (!action) return;
|
|
57
59
|
if (action.kind === 'editor' && action.editorKind) {
|
|
58
|
-
const editor =
|
|
60
|
+
const editor = createAgentWorkspaceEditor(action.editorKind, {
|
|
61
|
+
runtimeStarterTemplates: workspace.runtimeSnapshot?.runtimeStarterTemplates ?? [],
|
|
62
|
+
selectedRoutine: workspace.selectedLocalLibraryItem('routine'),
|
|
63
|
+
});
|
|
59
64
|
if (!editor) {
|
|
60
65
|
workspace.status = `Editor unavailable: ${action.editorKind}.`;
|
|
61
66
|
workspace.lastActionResult = {
|
|
@@ -143,11 +148,14 @@ export function activateAgentWorkspaceSelection(
|
|
|
143
148
|
workspace.dispatchWorkspaceCommand(action.command);
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
export function createAgentWorkspaceEditor(
|
|
152
|
+
editorKind: AgentWorkspaceEditorKind,
|
|
153
|
+
options: {
|
|
154
|
+
readonly runtimeStarterTemplates?: readonly AgentWorkspaceRuntimeStarterTemplateItem[];
|
|
155
|
+
readonly selectedRoutine?: AgentWorkspaceLocalLibraryItem | null;
|
|
156
|
+
} = {},
|
|
149
157
|
): AgentWorkspaceLocalEditor | null {
|
|
150
|
-
if (editorKind === 'profile') return createProfileEditor(
|
|
158
|
+
if (editorKind === 'profile') return createProfileEditor(options.runtimeStarterTemplates ?? []);
|
|
151
159
|
if (editorKind === 'learned-behavior') return createLearnedBehaviorEditor();
|
|
152
160
|
if (editorKind === 'web-research') return createAgentWorkspaceWebResearchEditor('research');
|
|
153
161
|
if (editorKind === 'web-fetch') return createAgentWorkspaceWebResearchEditor('fetch');
|
|
@@ -155,7 +163,7 @@ function createWorkspaceEditor(
|
|
|
155
163
|
if (editorKind === 'knowledge-ask') return createAgentKnowledgeQueryEditor('ask');
|
|
156
164
|
if (editorKind === 'knowledge-search') return createAgentKnowledgeQueryEditor('search');
|
|
157
165
|
if (editorKind === 'reminder-schedule') return createReminderScheduleEditor();
|
|
158
|
-
if (editorKind === 'routine-schedule') return createRoutineScheduleEditor(
|
|
166
|
+
if (editorKind === 'routine-schedule') return createRoutineScheduleEditor(options.selectedRoutine ?? null);
|
|
159
167
|
if (
|
|
160
168
|
editorKind === 'memory'
|
|
161
169
|
|| editorKind === 'note'
|
|
@@ -2,14 +2,150 @@ import type { CommandRegistry } from '../command-registry.ts';
|
|
|
2
2
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
3
3
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
4
|
import { requireYesFlag, stripYesFlag } from './confirmation.ts';
|
|
5
|
+
import {
|
|
6
|
+
formatHarnessError,
|
|
7
|
+
formatHarnessMutation,
|
|
8
|
+
formatHarnessSetting,
|
|
9
|
+
formatHarnessSettingList,
|
|
10
|
+
getHarnessSetting,
|
|
11
|
+
listHarnessSettings,
|
|
12
|
+
resetHarnessSetting,
|
|
13
|
+
setHarnessSetting,
|
|
14
|
+
} from '../../agent/harness-control.ts';
|
|
15
|
+
|
|
16
|
+
function readValuedFlag(args: readonly string[], flag: string): string | undefined {
|
|
17
|
+
const assignmentPrefix = `${flag}=`;
|
|
18
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
19
|
+
const arg = args[index];
|
|
20
|
+
if (arg === flag) {
|
|
21
|
+
const value = args[index + 1];
|
|
22
|
+
return value && !value.startsWith('--') ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
if (arg?.startsWith(assignmentPrefix)) return arg.slice(assignmentPrefix.length);
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stripValuedFlag(args: readonly string[], flag: string): readonly string[] {
|
|
30
|
+
const next: string[] = [];
|
|
31
|
+
const assignmentPrefix = `${flag}=`;
|
|
32
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
33
|
+
const arg = args[index];
|
|
34
|
+
if (arg === flag) {
|
|
35
|
+
const value = args[index + 1];
|
|
36
|
+
if (value && !value.startsWith('--')) index += 1;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (arg?.startsWith(assignmentPrefix)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
next.push(arg!);
|
|
43
|
+
}
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseSettingListArgs(args: readonly string[]): {
|
|
48
|
+
readonly category?: string;
|
|
49
|
+
readonly prefix?: string;
|
|
50
|
+
readonly query?: string;
|
|
51
|
+
readonly includeHidden: boolean;
|
|
52
|
+
readonly limit?: number;
|
|
53
|
+
} {
|
|
54
|
+
const category = readValuedFlag(args, '--category');
|
|
55
|
+
const prefix = readValuedFlag(args, '--prefix');
|
|
56
|
+
const limitText = readValuedFlag(args, '--limit');
|
|
57
|
+
const remaining = stripValuedFlag(stripValuedFlag(stripValuedFlag(args, '--category'), '--prefix'), '--limit')
|
|
58
|
+
.filter((arg) => arg !== '--include-hidden');
|
|
59
|
+
const query = remaining.join(' ').trim();
|
|
60
|
+
return {
|
|
61
|
+
...(category ? { category } : {}),
|
|
62
|
+
...(prefix ? { prefix } : {}),
|
|
63
|
+
...(query ? { query } : {}),
|
|
64
|
+
includeHidden: args.includes('--include-hidden'),
|
|
65
|
+
...(limitText ? { limit: Number(limitText) } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
5
68
|
|
|
6
69
|
export function registerOperatorRuntimeCommands(registry: CommandRegistry): void {
|
|
7
70
|
registry.register({
|
|
8
71
|
name: 'settings',
|
|
9
72
|
aliases: ['cfg-ui'],
|
|
10
|
-
description: 'Open
|
|
11
|
-
|
|
12
|
-
|
|
73
|
+
description: 'Open, inspect, or update Agent settings',
|
|
74
|
+
usage: '[category|key|list|get <key>|set <key> <value> --yes|reset <key> --yes]',
|
|
75
|
+
argsHint: '[list|get|set|reset]',
|
|
76
|
+
async handler(args, ctx) {
|
|
77
|
+
const parsed = stripYesFlag(args);
|
|
78
|
+
const commandArgs = [...parsed.rest];
|
|
79
|
+
const sub = commandArgs[0];
|
|
80
|
+
|
|
81
|
+
if (!sub) {
|
|
82
|
+
if (ctx.openSettingsModal) ctx.openSettingsModal();
|
|
83
|
+
else ctx.print('Configuration workspace is not available in this runtime.');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (sub === 'list' || sub === 'schema') {
|
|
88
|
+
ctx.print(formatHarnessSettingList(listHarnessSettings(ctx.platform.configManager, parseSettingListArgs(commandArgs.slice(1)))));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (sub === 'get' || sub === 'show') {
|
|
93
|
+
const key = commandArgs[1];
|
|
94
|
+
if (!key) {
|
|
95
|
+
ctx.print('Usage: /settings get <key>');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
ctx.print(formatHarnessSetting(getHarnessSetting(ctx.platform.configManager, key)));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (sub === 'set') {
|
|
103
|
+
const key = commandArgs[1];
|
|
104
|
+
const rawValue = commandArgs.slice(2).join(' ');
|
|
105
|
+
if (!key || rawValue.length === 0) {
|
|
106
|
+
ctx.print('Usage: /settings set <key> <value> --yes');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!parsed.yes) {
|
|
110
|
+
requireYesFlag(ctx, `set setting ${key}`, '/settings set <key> <value> --yes');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const result = await setHarnessSetting(ctx.platform.configManager, ctx.platform.secretsManager, key, rawValue);
|
|
115
|
+
ctx.print(formatHarnessMutation(result));
|
|
116
|
+
ctx.renderRequest();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
ctx.print(formatHarnessError(error));
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (sub === 'reset') {
|
|
124
|
+
const key = commandArgs[1];
|
|
125
|
+
if (!key) {
|
|
126
|
+
ctx.print('Usage: /settings reset <key> --yes');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!parsed.yes) {
|
|
130
|
+
requireYesFlag(ctx, `reset setting ${key}`, '/settings reset <key> --yes');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const result = await resetHarnessSetting(ctx.platform.configManager, ctx.platform.secretsManager, key);
|
|
135
|
+
ctx.print(formatHarnessMutation(result));
|
|
136
|
+
ctx.renderRequest();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
ctx.print(formatHarnessError(error));
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (sub.includes('.')) {
|
|
144
|
+
ctx.print(formatHarnessSetting(getHarnessSetting(ctx.platform.configManager, sub)));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (ctx.openSettingsModal) ctx.openSettingsModal(sub);
|
|
13
149
|
else ctx.print('Configuration workspace is not available in this runtime.');
|
|
14
150
|
},
|
|
15
151
|
});
|
|
@@ -1,44 +1,5 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
'batch.',
|
|
7
|
-
'controlPlane.',
|
|
8
|
-
'danger.',
|
|
9
|
-
'httpListener.',
|
|
10
|
-
'network.',
|
|
11
|
-
'orchestration.',
|
|
12
|
-
'runtime.',
|
|
13
|
-
'service.',
|
|
14
|
-
'sandbox.',
|
|
15
|
-
'web.',
|
|
16
|
-
'watchers.',
|
|
17
|
-
'wrfc.',
|
|
18
|
-
] as const;
|
|
19
|
-
|
|
20
|
-
const AGENT_HIDDEN_SETTING_KEYS = new Set<string>([
|
|
21
|
-
'ui.wrfcMessages',
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
const EXTERNAL_HOST_SETTING_PREFIXES = [
|
|
25
|
-
'service.',
|
|
26
|
-
'controlPlane.',
|
|
27
|
-
'httpListener.',
|
|
28
|
-
'web.',
|
|
29
|
-
] as const;
|
|
30
|
-
|
|
31
|
-
const EXTERNAL_HOST_SETTING_KEYS = new Set<string>([
|
|
32
|
-
'danger.daemon',
|
|
33
|
-
'danger.httpListener',
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
export function isExternalHostOwnedSettingKey(key: string): boolean {
|
|
37
|
-
return EXTERNAL_HOST_SETTING_KEYS.has(key)
|
|
38
|
-
|| EXTERNAL_HOST_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function isAgentHiddenSettingKey(key: string): boolean {
|
|
42
|
-
return AGENT_HIDDEN_SETTING_KEYS.has(key)
|
|
43
|
-
|| AGENT_HIDDEN_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
44
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
AGENT_EXTERNAL_HOST_SETTING_LOCK_REASON,
|
|
3
|
+
isAgentHiddenSettingKey,
|
|
4
|
+
isExternalHostOwnedSettingKey,
|
|
5
|
+
} from '../config/agent-settings-policy.ts';
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { buildActivePersonaPrompt } from '../agent/persona-registry.ts';
|
|
|
42
42
|
import { buildEnabledSkillsPrompt } from '../agent/skill-registry.ts';
|
|
43
43
|
import { buildEnabledRoutinesPrompt } from '../agent/routine-registry.ts';
|
|
44
44
|
import { buildReviewedMemoryPrompt } from '../agent/memory-prompt.ts';
|
|
45
|
+
import { registerAgentHarnessTool } from '../tools/agent-harness-tool.ts';
|
|
45
46
|
|
|
46
47
|
const GOODVIBES_AGENT_OPERATOR_POLICY = [
|
|
47
48
|
'## GoodVibes Agent Operator Policy',
|
|
@@ -50,6 +51,7 @@ const GOODVIBES_AGENT_OPERATOR_POLICY = [
|
|
|
50
51
|
'- Use the `agent_operator_briefing` tool for read-only main-conversation summaries of connected work plan, approvals, automation, schedules, and scheduler capacity. This tool must not call mutation routes, connected-host lifecycle routes, default knowledge, non-Agent knowledge spaces, separate Agent job creation, or GoodVibes TUI delegation.',
|
|
51
52
|
'- When the user explicitly asks Agent to approve, deny, or cancel a specific approval, run/pause/resume a specific automation job, cancel/retry a specific automation run, or run a specific schedule, use the `agent_operator_action` tool with confirm:true and the original user request. It can call only its allowlisted public operator routes and must not create, edit, delete, or discover automation definitions.',
|
|
52
53
|
'- Use the `agent_work_plan` tool to keep the visible Agent-local work plan current while working in the main conversation. Create, inspect, and update local work items proactively when useful. Removing items or clearing completed items requires an explicit user request and confirm:true.',
|
|
54
|
+
'- Use the `agent_harness` tool to inspect and operate the harness surface itself: Agent workspace action catalog, slash command catalog, model tool catalog, connected-host posture, and Agent settings. Use it to change Agent settings, invoke workspace action mirrors, or invoke slash-command mirrors only when the user explicitly asks; preserve confirm:true and explicitUserRequest for mutations, keep secret-backed settings in the secret manager, and keep connected-host lifecycle/posture externally owned.',
|
|
53
55
|
'- Use the `agent_knowledge` tool for Agent Knowledge status, ask, and search from the main conversation. It must use only /api/goodvibes-agent/knowledge/* and must fail closed instead of falling back to default knowledge or non-Agent knowledge spaces.',
|
|
54
56
|
'- When the user explicitly asks Agent to add, import, remember, or ingest a URL, URL-list file, local file, bookmarks file, browser history, or connector input into Agent Knowledge, use the `agent_knowledge_ingest` tool with confirm:true and the original user request. It must write only to /api/goodvibes-agent/knowledge/* ingest routes and never to default knowledge or non-Agent knowledge spaces.',
|
|
55
57
|
'- Use the `agent_local_registry` tool when a scratchpad note, durable memory, reusable persona, skill, skill bundle, or routine would improve later work. Keep those records Agent-local, non-secret, source/provenance tagged, and reviewable. Notes are for temporary/source-triage context; promote them explicitly into memory, skills, personas, routines, or Agent Knowledge only when that is the correct durable home. Review memory with a confidence score when it should shape later turns. Starting a routine means applying its steps in this same serial conversation, not creating a background job.',
|
|
@@ -285,6 +287,7 @@ export async function bootstrapRuntime(
|
|
|
285
287
|
const commandRegistry = shell.commandRegistry;
|
|
286
288
|
const commandContext = shell.commandContext;
|
|
287
289
|
const inputHistory = shell.inputHistory;
|
|
290
|
+
registerAgentHarnessTool(toolRegistry, commandRegistry, commandContext);
|
|
288
291
|
const pluginCommandRegistry = {
|
|
289
292
|
register(command: {
|
|
290
293
|
readonly name: string;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
|
|
2
|
+
import { AgentNoteRegistry } from '../agent/note-registry.ts';
|
|
3
|
+
import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
|
|
4
|
+
import { AgentRoutineRegistry } from '../agent/routine-registry.ts';
|
|
5
|
+
import { AgentSkillRegistry } from '../agent/skill-registry.ts';
|
|
6
|
+
import type { CommandContext } from '../input/command-registry.ts';
|
|
7
|
+
import {
|
|
8
|
+
createKnowledgeUrlEditorFromNote,
|
|
9
|
+
createMemoryEditorFromNote,
|
|
10
|
+
createMemoryUpdateEditor,
|
|
11
|
+
createNoteUpdateEditor,
|
|
12
|
+
createPersonaEditorFromNote,
|
|
13
|
+
createPersonaUpdateEditor,
|
|
14
|
+
createRoutineEditorFromNote,
|
|
15
|
+
createRoutineUpdateEditor,
|
|
16
|
+
createSkillEditorFromNote,
|
|
17
|
+
createSkillUpdateEditor,
|
|
18
|
+
isAffirmative,
|
|
19
|
+
splitList,
|
|
20
|
+
} from '../input/agent-workspace-editors.ts';
|
|
21
|
+
import type { AgentWorkspaceAction, AgentWorkspaceLocalEditor, AgentWorkspaceLocalOperation } from '../input/agent-workspace-types.ts';
|
|
22
|
+
|
|
23
|
+
export interface AgentHarnessLocalOperationArgs {
|
|
24
|
+
readonly fields?: unknown;
|
|
25
|
+
readonly recordId?: unknown;
|
|
26
|
+
readonly id?: unknown;
|
|
27
|
+
readonly confirm?: unknown;
|
|
28
|
+
readonly explicitUserRequest?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AgentHarnessLocalOperationDeps {
|
|
32
|
+
readonly commandContext: CommandContext;
|
|
33
|
+
readonly toolRegistry: ToolRegistry;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type HarnessResult =
|
|
37
|
+
| { readonly success: true; readonly output: string }
|
|
38
|
+
| { readonly success: false; readonly error: string };
|
|
39
|
+
|
|
40
|
+
type RegistryDomain = 'memory' | 'note' | 'persona' | 'skill' | 'routine';
|
|
41
|
+
type RegistryAction = 'update' | 'review' | 'stale' | 'delete' | 'use' | 'clear_active' | 'enable' | 'disable' | 'start';
|
|
42
|
+
|
|
43
|
+
interface RegistryTarget {
|
|
44
|
+
readonly domain: RegistryDomain;
|
|
45
|
+
readonly action: RegistryAction;
|
|
46
|
+
readonly requiresRecordId: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function output(value: unknown): HarnessResult {
|
|
50
|
+
return { success: true, output: typeof value === 'string' ? value : JSON.stringify(value, null, 2) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function error(message: string): HarnessResult {
|
|
54
|
+
return { success: false, error: message };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readString(value: unknown): string {
|
|
58
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readFieldMap(value: unknown): Readonly<Record<string, string>> {
|
|
62
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return {};
|
|
63
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, typeof entry === 'string' ? entry : String(entry)]));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function describeEditor(editor: AgentWorkspaceLocalEditor): Record<string, unknown> {
|
|
67
|
+
return {
|
|
68
|
+
kind: editor.kind,
|
|
69
|
+
mode: editor.mode,
|
|
70
|
+
recordId: editor.recordId,
|
|
71
|
+
title: editor.title,
|
|
72
|
+
message: editor.message,
|
|
73
|
+
fields: editor.fields.map((field) => ({
|
|
74
|
+
id: field.id,
|
|
75
|
+
label: field.label,
|
|
76
|
+
required: field.required,
|
|
77
|
+
multiline: field.multiline,
|
|
78
|
+
hint: field.hint,
|
|
79
|
+
default: field.redact ? '<redacted>' : field.value,
|
|
80
|
+
})),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readRecordId(args: AgentHarnessLocalOperationArgs): string {
|
|
85
|
+
const fields = readFieldMap(args.fields);
|
|
86
|
+
return readString(args.recordId ?? args.id ?? fields.recordId ?? fields.id);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function requireConfirmed(args: AgentHarnessLocalOperationArgs, label: string): string | null {
|
|
90
|
+
if (!readString(args.explicitUserRequest)) return `${label} requires explicitUserRequest with the user's exact request or a short faithful summary.`;
|
|
91
|
+
if (args.confirm !== true) return `${label} requires confirm:true after an explicit user request.`;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function fieldReader(editor: AgentWorkspaceLocalEditor, fields: Readonly<Record<string, string>>): (id: string) => string {
|
|
96
|
+
return (id: string) => fields[id] ?? editor.fields.find((field) => field.id === id)?.value ?? '';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hasExecutionFields(args: AgentHarnessLocalOperationArgs): boolean {
|
|
100
|
+
return Object.keys(readFieldMap(args.fields)).some((key) => key !== 'id' && key !== 'recordId');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function missingRequiredFields(editor: AgentWorkspaceLocalEditor, fields: Readonly<Record<string, string>>): readonly string[] {
|
|
104
|
+
const read = fieldReader(editor, fields);
|
|
105
|
+
return editor.fields.filter((field) => field.required && !read(field.id).trim()).map((field) => field.id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function registryTarget(operation: AgentWorkspaceLocalOperation): RegistryTarget | null {
|
|
109
|
+
if (operation === 'memory-edit') return { domain: 'memory', action: 'update', requiresRecordId: true };
|
|
110
|
+
if (operation === 'memory-review') return { domain: 'memory', action: 'review', requiresRecordId: true };
|
|
111
|
+
if (operation === 'memory-stale') return { domain: 'memory', action: 'stale', requiresRecordId: true };
|
|
112
|
+
if (operation === 'memory-delete') return { domain: 'memory', action: 'delete', requiresRecordId: true };
|
|
113
|
+
if (operation === 'note-edit') return { domain: 'note', action: 'update', requiresRecordId: true };
|
|
114
|
+
if (operation === 'note-review') return { domain: 'note', action: 'review', requiresRecordId: true };
|
|
115
|
+
if (operation === 'note-stale') return { domain: 'note', action: 'stale', requiresRecordId: true };
|
|
116
|
+
if (operation === 'note-delete') return { domain: 'note', action: 'delete', requiresRecordId: true };
|
|
117
|
+
if (operation === 'persona-edit') return { domain: 'persona', action: 'update', requiresRecordId: true };
|
|
118
|
+
if (operation === 'persona-use') return { domain: 'persona', action: 'use', requiresRecordId: true };
|
|
119
|
+
if (operation === 'persona-review') return { domain: 'persona', action: 'review', requiresRecordId: true };
|
|
120
|
+
if (operation === 'persona-clear') return { domain: 'persona', action: 'clear_active', requiresRecordId: false };
|
|
121
|
+
if (operation === 'persona-delete') return { domain: 'persona', action: 'delete', requiresRecordId: true };
|
|
122
|
+
if (operation === 'skill-edit') return { domain: 'skill', action: 'update', requiresRecordId: true };
|
|
123
|
+
if (operation === 'skill-enable') return { domain: 'skill', action: 'enable', requiresRecordId: true };
|
|
124
|
+
if (operation === 'skill-disable') return { domain: 'skill', action: 'disable', requiresRecordId: true };
|
|
125
|
+
if (operation === 'skill-review') return { domain: 'skill', action: 'review', requiresRecordId: true };
|
|
126
|
+
if (operation === 'skill-delete') return { domain: 'skill', action: 'delete', requiresRecordId: true };
|
|
127
|
+
if (operation === 'routine-edit') return { domain: 'routine', action: 'update', requiresRecordId: true };
|
|
128
|
+
if (operation === 'routine-start') return { domain: 'routine', action: 'start', requiresRecordId: true };
|
|
129
|
+
if (operation === 'routine-enable') return { domain: 'routine', action: 'enable', requiresRecordId: true };
|
|
130
|
+
if (operation === 'routine-disable') return { domain: 'routine', action: 'disable', requiresRecordId: true };
|
|
131
|
+
if (operation === 'routine-review') return { domain: 'routine', action: 'review', requiresRecordId: true };
|
|
132
|
+
if (operation === 'routine-delete') return { domain: 'routine', action: 'delete', requiresRecordId: true };
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function editorForOperation(context: CommandContext, operation: AgentWorkspaceLocalOperation, recordId: string): AgentWorkspaceLocalEditor | null {
|
|
137
|
+
const shellPaths = context.workspace.shellPaths;
|
|
138
|
+
if (operation === 'memory-edit') {
|
|
139
|
+
const record = context.clients?.agentKnowledgeApi?.memory?.get(recordId);
|
|
140
|
+
return record ? createMemoryUpdateEditor(record) : null;
|
|
141
|
+
}
|
|
142
|
+
if (!shellPaths) return null;
|
|
143
|
+
if (operation === 'note-edit') {
|
|
144
|
+
const note = AgentNoteRegistry.fromShellPaths(shellPaths).get(recordId);
|
|
145
|
+
return note ? createNoteUpdateEditor(note) : null;
|
|
146
|
+
}
|
|
147
|
+
if (operation === 'note-promote-memory' || operation === 'note-promote-persona' || operation === 'note-promote-skill' || operation === 'note-promote-routine' || operation === 'note-promote-knowledge-url') {
|
|
148
|
+
const note = AgentNoteRegistry.fromShellPaths(shellPaths).get(recordId);
|
|
149
|
+
if (!note) return null;
|
|
150
|
+
if (operation === 'note-promote-memory') return createMemoryEditorFromNote(note);
|
|
151
|
+
if (operation === 'note-promote-persona') return createPersonaEditorFromNote(note);
|
|
152
|
+
if (operation === 'note-promote-skill') return createSkillEditorFromNote(note);
|
|
153
|
+
if (operation === 'note-promote-routine') return createRoutineEditorFromNote(note);
|
|
154
|
+
return note.sourceUrl ? createKnowledgeUrlEditorFromNote(note) : null;
|
|
155
|
+
}
|
|
156
|
+
if (operation === 'persona-edit') {
|
|
157
|
+
const registry = AgentPersonaRegistry.fromShellPaths(shellPaths);
|
|
158
|
+
const persona = registry.get(recordId);
|
|
159
|
+
return persona ? createPersonaUpdateEditor(persona, registry.snapshot().activePersonaId === persona.id) : null;
|
|
160
|
+
}
|
|
161
|
+
if (operation === 'skill-edit') {
|
|
162
|
+
const skill = AgentSkillRegistry.fromShellPaths(shellPaths).get(recordId);
|
|
163
|
+
return skill ? createSkillUpdateEditor(skill) : null;
|
|
164
|
+
}
|
|
165
|
+
if (operation === 'routine-edit') {
|
|
166
|
+
const routine = AgentRoutineRegistry.fromShellPaths(shellPaths).get(recordId);
|
|
167
|
+
return routine ? createRoutineUpdateEditor(routine) : null;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function localRegistryArgsFromEditor(editor: AgentWorkspaceLocalEditor, fields: Readonly<Record<string, string>>, id?: string): Record<string, unknown> {
|
|
173
|
+
const read = fieldReader(editor, fields);
|
|
174
|
+
if (editor.kind === 'memory') return { domain: 'memory', action: editor.mode === 'update' ? 'update' : 'create', id, cls: read('cls'), scope: read('scope'), summary: read('summary'), detail: read('detail'), tags: splitList(read('tags')), confidence: read('confidence'), provenance: editor.recordId ? 'agent-harness-local-operation' : 'agent-harness-note-promotion' };
|
|
175
|
+
if (editor.kind === 'note') return { domain: 'note', action: 'update', id, title: read('title'), body: read('body'), sourceUrl: read('sourceUrl'), tags: splitList(read('tags')), provenance: 'agent-harness-local-operation' };
|
|
176
|
+
if (editor.kind === 'persona') return { domain: 'persona', action: editor.mode === 'update' ? 'update' : 'create', id, name: read('name'), description: read('description'), body: read('body'), tags: splitList(read('tags')), triggers: splitList(read('triggers')), activate: read('activate'), provenance: editor.recordId ? 'agent-harness-local-operation' : 'agent-harness-note-promotion' };
|
|
177
|
+
if (editor.kind === 'skill') return { domain: 'skill', action: editor.mode === 'update' ? 'update' : 'create', id, name: read('name'), description: read('description'), procedure: read('procedure'), tags: splitList(read('tags')), triggers: splitList(read('triggers')), requiresEnv: splitList(read('requiresEnv')), requiresCommands: splitList(read('requiresCommands')), enabled: isAffirmative(read('enabled')), provenance: editor.recordId ? 'agent-harness-local-operation' : 'agent-harness-note-promotion' };
|
|
178
|
+
return { domain: 'routine', action: editor.mode === 'update' ? 'update' : 'create', id, name: read('name'), description: read('description'), steps: read('steps'), tags: splitList(read('tags')), triggers: splitList(read('triggers')), requiresEnv: splitList(read('requiresEnv')), requiresCommands: splitList(read('requiresCommands')), enabled: isAffirmative(read('enabled')), provenance: editor.recordId ? 'agent-harness-local-operation' : 'agent-harness-note-promotion' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function localRegistryArgsForTarget(target: RegistryTarget, args: AgentHarnessLocalOperationArgs, recordId: string): Record<string, unknown> {
|
|
182
|
+
const fields = readFieldMap(args.fields);
|
|
183
|
+
const base = { domain: target.domain, action: target.action, ...(recordId ? { id: recordId } : {}) };
|
|
184
|
+
const edit = target.action === 'update' ? localRegistryArgsFromEditor({ kind: target.domain, mode: 'update', recordId, title: '', selectedFieldIndex: 0, message: '', fields: [] } as AgentWorkspaceLocalEditor, fields, recordId) : {};
|
|
185
|
+
return {
|
|
186
|
+
...base,
|
|
187
|
+
...edit,
|
|
188
|
+
...(target.action === 'stale' && fields.reason ? { reason: fields.reason } : {}),
|
|
189
|
+
...(target.action === 'delete' ? { confirm: args.confirm, explicitUserRequest: readString(args.explicitUserRequest) } : {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function executeTool(toolRegistry: ToolRegistry, name: string, toolArgs: Record<string, unknown>): Promise<HarnessResult> {
|
|
194
|
+
if (!toolRegistry.has(name)) return output({ status: 'model_tool_required', modelExecution: { tool: name, args: toolArgs } });
|
|
195
|
+
const result = await toolRegistry.execute(`agent-harness-${name}-${Date.now()}`, name, toolArgs) as { readonly success: boolean; readonly output?: string; readonly error?: string };
|
|
196
|
+
if (!result.success) return error(result.error ?? `${name} failed.`);
|
|
197
|
+
return output({ status: 'executed_model_tool', tool: name, output: result.output ?? '' });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function describeLocalWorkspaceModelExecution(action: AgentWorkspaceAction): Record<string, unknown> | null {
|
|
201
|
+
if (action.kind === 'local-selection') return { tool: 'agent_local_registry', domain: action.localKind, actions: ['list', 'search', 'get'], note: 'TUI selection maps to recordId for model-run local-operation actions.' };
|
|
202
|
+
if (!action.localOperation) return null;
|
|
203
|
+
const target = registryTarget(action.localOperation);
|
|
204
|
+
if (target) return { tool: 'agent_local_registry', domain: target.domain, action: target.action, requiresRecordId: target.requiresRecordId };
|
|
205
|
+
if (action.localOperation === 'note-promote-knowledge-url') return { tool: 'agent_knowledge_ingest', sourceKind: 'url', requiresRecordId: true, selectedRecordDomain: 'note' };
|
|
206
|
+
return { tool: 'agent_local_registry', selectedRecordDomain: 'note', action: 'create', requiresRecordId: true };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function runLocalWorkspaceAction(
|
|
210
|
+
deps: AgentHarnessLocalOperationDeps,
|
|
211
|
+
action: AgentWorkspaceAction,
|
|
212
|
+
args: AgentHarnessLocalOperationArgs,
|
|
213
|
+
): Promise<HarnessResult> {
|
|
214
|
+
if (action.kind === 'local-selection') return output({ status: 'local_selection', action: action.id, modelExecution: describeLocalWorkspaceModelExecution(action) });
|
|
215
|
+
const operation = action.localOperation;
|
|
216
|
+
if (!operation) return output({ status: 'local_registry_action', action: action.id, modelExecution: describeLocalWorkspaceModelExecution(action) });
|
|
217
|
+
const target = registryTarget(operation);
|
|
218
|
+
const recordId = readRecordId(args);
|
|
219
|
+
if (target?.requiresRecordId && !recordId) return output({ status: 'needs_record_id', action: action.id, modelExecution: describeLocalWorkspaceModelExecution(action) });
|
|
220
|
+
const editor = recordId ? editorForOperation(deps.commandContext, operation, recordId) : null;
|
|
221
|
+
if (editor && !hasExecutionFields(args) && args.confirm !== true) return output({ status: 'editor', action: action.id, editor: describeEditor(editor), modelExecution: describeLocalWorkspaceModelExecution(action) });
|
|
222
|
+
const confirmationError = requireConfirmed(args, 'Workspace local registry action');
|
|
223
|
+
if (confirmationError) return error(confirmationError);
|
|
224
|
+
if (editor) {
|
|
225
|
+
const fields = readFieldMap(args.fields);
|
|
226
|
+
const missing = missingRequiredFields(editor, fields);
|
|
227
|
+
if (missing.length > 0) return output({ status: 'missing_required_fields', missing, action: action.id, editor: describeEditor(editor) });
|
|
228
|
+
if (editor.kind === 'knowledge-url') return executeTool(deps.toolRegistry, 'agent_knowledge_ingest', { sourceKind: 'url', url: fieldReader(editor, fields)('url'), tags: splitList(fieldReader(editor, fields)('tags')), folderPath: fieldReader(editor, fields)('folder'), confirm: args.confirm, explicitUserRequest: readString(args.explicitUserRequest) });
|
|
229
|
+
return executeTool(deps.toolRegistry, 'agent_local_registry', localRegistryArgsFromEditor(editor, fields, recordId));
|
|
230
|
+
}
|
|
231
|
+
if (!target) return output({ status: 'local_registry_action', action: action.id, modelExecution: describeLocalWorkspaceModelExecution(action) });
|
|
232
|
+
return executeTool(deps.toolRegistry, 'agent_local_registry', localRegistryArgsForTarget(target, args, recordId));
|
|
233
|
+
}
|