@newsails/veil-cli 1.0.1
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/.veil/agents/analyst/AGENT.md +21 -0
- package/.veil/agents/analyst/agent.json +23 -0
- package/.veil/agents/assistant/AGENT.md +15 -0
- package/.veil/agents/assistant/agent.json +19 -0
- package/.veil/agents/coder/AGENT.md +18 -0
- package/.veil/agents/coder/agent.json +19 -0
- package/.veil/agents/hello/AGENT.md +5 -0
- package/.veil/agents/hello/agent.json +13 -0
- package/.veil/agents/writer/AGENT.md +12 -0
- package/.veil/agents/writer/agent.json +17 -0
- package/.veil/memory/MEMORY.md +343 -0
- package/.veil/memory/agents/analyst/MEMORY.md +55 -0
- package/.veil/memory/agents/hello/MEMORY.md +12 -0
- package/.veil/runtime.pid +1 -0
- package/.veil/settings.json +10 -0
- package/.veil-studio/studio.db +0 -0
- package/.veil-studio/studio.db-shm +0 -0
- package/.veil-studio/studio.db-wal +0 -0
- package/PLAN/01-vision.md +26 -0
- package/PLAN/02-tech-stack.md +94 -0
- package/PLAN/03-agents.md +232 -0
- package/PLAN/04-runtime.md +171 -0
- package/PLAN/05-tools.md +211 -0
- package/PLAN/06-communication.md +243 -0
- package/PLAN/07-storage.md +218 -0
- package/PLAN/08-api-cli.md +153 -0
- package/PLAN/09-permissions.md +108 -0
- package/PLAN/10-ably.md +105 -0
- package/PLAN/11-file-formats.md +442 -0
- package/PLAN/12-folder-structure.md +205 -0
- package/PLAN/13-operations.md +212 -0
- package/PLAN/README.md +23 -0
- package/README.md +128 -0
- package/REPORT.md +174 -0
- package/TODO.md +45 -0
- package/ai-tests/FRONTEND_PROMPT.md +220 -0
- package/ai-tests/Research & Planning.md +814 -0
- package/ai-tests/prompt-001-basic-api.md +230 -0
- package/ai-tests/prompt-002-basic-flows.md +230 -0
- package/ai-tests/prompt-003-agent-behaviors.md +220 -0
- package/api/middleware.js +60 -0
- package/api/routes/agents.js +193 -0
- package/api/routes/chat.js +93 -0
- package/api/routes/completions.js +122 -0
- package/api/routes/daemons.js +80 -0
- package/api/routes/memory.js +169 -0
- package/api/routes/models.js +40 -0
- package/api/routes/remote-methods.js +74 -0
- package/api/routes/sessions.js +208 -0
- package/api/routes/settings.js +108 -0
- package/api/routes/system.js +50 -0
- package/api/routes/tasks.js +270 -0
- package/api/server.js +120 -0
- package/cli/formatter.js +70 -0
- package/cli/index.js +443 -0
- package/cli/parser.js +113 -0
- package/config/config.json +10 -0
- package/config/models.json +6826 -0
- package/core/agent.js +329 -0
- package/core/cancel.js +38 -0
- package/core/compaction.js +176 -0
- package/core/events.js +13 -0
- package/core/loop.js +564 -0
- package/core/memory.js +51 -0
- package/core/prompt.js +185 -0
- package/core/queue.js +96 -0
- package/core/registry.js +291 -0
- package/core/remote-methods.js +124 -0
- package/core/router.js +386 -0
- package/core/running-sessions.js +18 -0
- package/docs/api/01-system.md +84 -0
- package/docs/api/02-agents.md +374 -0
- package/docs/api/03-chat.md +269 -0
- package/docs/api/04-tasks.md +470 -0
- package/docs/api/05-sessions.md +444 -0
- package/docs/api/06-daemons.md +142 -0
- package/docs/api/07-memory.md +186 -0
- package/docs/api/08-settings.md +133 -0
- package/docs/api/09-models.md +119 -0
- package/docs/api/09-websocket.md +350 -0
- package/docs/api/10-completions.md +134 -0
- package/docs/api/README.md +116 -0
- package/docs/guide/01-quickstart.md +220 -0
- package/docs/guide/02-folder-structure.md +185 -0
- package/docs/guide/03-configuration.md +252 -0
- package/docs/guide/04-agents.md +267 -0
- package/docs/guide/05-cli.md +290 -0
- package/docs/guide/06-tools.md +643 -0
- package/docs/guide/07-permissions.md +236 -0
- package/docs/guide/08-memory.md +139 -0
- package/docs/guide/09-multi-agent.md +271 -0
- package/docs/guide/10-daemons.md +226 -0
- package/docs/guide/README.md +53 -0
- package/docs/index.html +623 -0
- package/examples/README.md +151 -0
- package/examples/agents/assistant/AGENT.md +31 -0
- package/examples/agents/assistant/SOUL.md +9 -0
- package/examples/agents/assistant/agent.json +74 -0
- package/examples/agents/hello/AGENT.md +15 -0
- package/examples/agents/hello/agent.json +14 -0
- package/examples/agents/monitor/AGENT.md +51 -0
- package/examples/agents/monitor/agent.json +33 -0
- package/examples/agents/monitor/heartbeats/monitor.md +24 -0
- package/examples/agents/orchestrator/AGENT.md +70 -0
- package/examples/agents/orchestrator/agent.json +30 -0
- package/examples/agents/researcher/AGENT.md +52 -0
- package/examples/agents/researcher/agent.json +49 -0
- package/examples/agents/researcher/skills/web-research.md +28 -0
- package/examples/skills/code-review.md +72 -0
- package/examples/skills/summarise.md +59 -0
- package/examples/skills/web-research.md +42 -0
- package/examples/tools/word-count/index.js +27 -0
- package/examples/tools/word-count/tool.json +18 -0
- package/infrastructure/database.js +563 -0
- package/infrastructure/scheduler.js +122 -0
- package/llm/client.js +206 -0
- package/migrations/001-initial.sql +121 -0
- package/migrations/002-debuggability.sql +13 -0
- package/migrations/003-drop-orphaned-columns.sql +72 -0
- package/migrations/004-session-message-token-fields.sql +78 -0
- package/migrations/005-session-thinking.sql +5 -0
- package/package.json +30 -0
- package/schemas/agent.json +143 -0
- package/schemas/settings.json +111 -0
- package/scripts/fetch-models.js +93 -0
- package/session-debug-scenario.md +248 -0
- package/settings/fields.js +52 -0
- package/system-prompts/base-core.md +7 -0
- package/system-prompts/environment.md +13 -0
- package/system-prompts/reminders/anti-drift.md +6 -0
- package/system-prompts/reminders/stall-recovery.md +10 -0
- package/system-prompts/safety-rules.md +25 -0
- package/system-prompts/task-heuristics.md +27 -0
- package/test/client.js +71 -0
- package/test/integration/01-health.test.js +25 -0
- package/test/integration/02-agents.test.js +80 -0
- package/test/integration/03-chat-hello.test.js +48 -0
- package/test/integration/04-chat-multiturn.test.js +61 -0
- package/test/integration/05-chat-writer.test.js +48 -0
- package/test/integration/06-task-basic.test.js +68 -0
- package/test/integration/07-task-tools.test.js +74 -0
- package/test/integration/08-task-code-analysis.test.js +69 -0
- package/test/integration/09-memory-analyst.test.js +63 -0
- package/test/integration/10-task-advanced.test.js +85 -0
- package/test/integration/11-sessions-advanced.test.js +84 -0
- package/test/integration/12-assistant-chat-tools.test.js +75 -0
- package/test/integration/13-edge-cases.test.js +99 -0
- package/test/integration/14-cancel.test.js +62 -0
- package/test/integration/15-debug.test.js +106 -0
- package/test/integration/16-memory-api.test.js +83 -0
- package/test/integration/17-settings-api.test.js +41 -0
- package/test/integration/18-tool-search-activation.test.js +119 -0
- package/test/results/.gitkeep +0 -0
- package/test/runner.js +206 -0
- package/test/smoke.js +216 -0
- package/tools/agent_message.js +85 -0
- package/tools/agent_send.js +80 -0
- package/tools/agent_spawn.js +44 -0
- package/tools/bash.js +49 -0
- package/tools/edit_file.js +41 -0
- package/tools/glob.js +64 -0
- package/tools/grep.js +82 -0
- package/tools/list_dir.js +63 -0
- package/tools/log_write.js +31 -0
- package/tools/memory_read.js +38 -0
- package/tools/memory_search.js +65 -0
- package/tools/memory_write.js +42 -0
- package/tools/read_file.js +48 -0
- package/tools/sleep.js +22 -0
- package/tools/task_create.js +41 -0
- package/tools/task_respond.js +37 -0
- package/tools/task_spawn.js +64 -0
- package/tools/task_status.js +39 -0
- package/tools/task_subscribe.js +37 -0
- package/tools/todo_read.js +26 -0
- package/tools/todo_write.js +38 -0
- package/tools/tool_activate.js +24 -0
- package/tools/tool_search.js +24 -0
- package/tools/web_fetch.js +50 -0
- package/tools/web_search.js +52 -0
- package/tools/write_file.js +28 -0
- package/ui/api.js +190 -0
- package/ui/app.js +281 -0
- package/ui/index.html +382 -0
- package/ui/views/agents.js +377 -0
- package/ui/views/chat.js +610 -0
- package/ui/views/connection.js +96 -0
- package/ui/views/daemons.js +129 -0
- package/ui/views/feed.js +194 -0
- package/ui/views/memory.js +263 -0
- package/ui/views/models.js +146 -0
- package/ui/views/sessions.js +314 -0
- package/ui/views/settings.js +142 -0
- package/ui/views/tasks.js +415 -0
- package/utils/context.js +49 -0
- package/utils/id.js +16 -0
- package/utils/models.js +88 -0
- package/utils/paths.js +213 -0
- package/utils/settings.js +172 -0
package/core/prompt.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const paths = require('../utils/paths');
|
|
7
|
+
const context = require('../utils/context');
|
|
8
|
+
|
|
9
|
+
const PROMPTS_DIR = path.join(__dirname, '..', 'system-prompts');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read a system prompt template file.
|
|
13
|
+
* @param {string} filename - Relative to system-prompts/
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function readPromptFile(filename) {
|
|
17
|
+
try {
|
|
18
|
+
return fs.readFileSync(path.join(PROMPTS_DIR, filename), 'utf8');
|
|
19
|
+
} catch {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Replace template variables in a prompt string.
|
|
26
|
+
* @param {string} text
|
|
27
|
+
* @param {Object} vars - Key/value map of variables
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
function interpolate(text, vars) {
|
|
31
|
+
return text.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
32
|
+
return vars[key] !== undefined ? String(vars[key]) : `{{${key}}}`;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read a markdown file safely.
|
|
38
|
+
* @param {string} filePath
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function readMd(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
return fs.readFileSync(filePath, 'utf8').trim();
|
|
44
|
+
} catch {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get git status for the project directory.
|
|
51
|
+
* @param {string} cwd
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function getGitStatus(cwd) {
|
|
55
|
+
try {
|
|
56
|
+
const { execSync } = require('child_process');
|
|
57
|
+
const branch = execSync('git branch --show-current 2>/dev/null', { cwd, timeout: 3000 }).toString().trim();
|
|
58
|
+
const status = execSync('git status --short 2>/dev/null | head -20', { cwd, timeout: 3000 }).toString().trim();
|
|
59
|
+
if (!branch) return 'Not a git repository';
|
|
60
|
+
return `Branch: ${branch}\n${status || '(clean)'}`;
|
|
61
|
+
} catch {
|
|
62
|
+
return 'Not a git repository';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Assemble the 7-layer system prompt for an agent session.
|
|
68
|
+
*
|
|
69
|
+
* @param {{ agent: Object, mode: string, sessionId: string, taskId?: string, cwd: string, settings: Object, memory?: string, todos?: Object[], taskBrief?: string }} opts
|
|
70
|
+
* @returns {string} Full system prompt text
|
|
71
|
+
*/
|
|
72
|
+
function assembleSystemPrompt({ agent, mode, sessionId, taskId, cwd, settings, memory, todos, taskBrief }) {
|
|
73
|
+
const layers = [];
|
|
74
|
+
const version = context.getVersion();
|
|
75
|
+
|
|
76
|
+
// ── Layer 1: Base Core (static) ───────────────────────────────────────────
|
|
77
|
+
const baseCoreVars = {
|
|
78
|
+
VERSION: version,
|
|
79
|
+
AGENT_NAME: agent.name,
|
|
80
|
+
MODE: mode,
|
|
81
|
+
SESSION_ID: sessionId || '',
|
|
82
|
+
};
|
|
83
|
+
layers.push(interpolate(readPromptFile('base-core.md'), baseCoreVars));
|
|
84
|
+
|
|
85
|
+
// ── Layer 2: Safety & Rules (static) ─────────────────────────────────────
|
|
86
|
+
// layers.push(readPromptFile('safety-rules.md'));
|
|
87
|
+
|
|
88
|
+
// ── Layer 3: Environment (dynamic) ───────────────────────────────────────
|
|
89
|
+
const mcpServersList = settings.mcpServers && Object.keys(settings.mcpServers).length > 0
|
|
90
|
+
? Object.keys(settings.mcpServers).map(s => `- ${s}`).join('\n')
|
|
91
|
+
: 'None configured';
|
|
92
|
+
|
|
93
|
+
const envVars = {
|
|
94
|
+
OS: `${os.type()} ${os.release()}`,
|
|
95
|
+
SHELL: process.env.SHELL || '/bin/sh',
|
|
96
|
+
CWD: cwd,
|
|
97
|
+
DATETIME: new Date().toISOString(),
|
|
98
|
+
DATEONLY: new Date().toISOString().split('T')[0],
|
|
99
|
+
NODE_VERSION: process.version,
|
|
100
|
+
GIT_STATUS: getGitStatus(cwd),
|
|
101
|
+
MCP_SERVERS: mcpServersList,
|
|
102
|
+
};
|
|
103
|
+
layers.push(interpolate(readPromptFile('environment.md'), envVars));
|
|
104
|
+
|
|
105
|
+
// ── Layer 4: Agent Instructions ───────────────────────────────────────────
|
|
106
|
+
const agentInstructions = [];
|
|
107
|
+
|
|
108
|
+
// Global project AGENT.md
|
|
109
|
+
const projectAgentMd = readMd(paths.getProjectAgentMdPath(cwd));
|
|
110
|
+
if (projectAgentMd) agentInstructions.push(`## Project Instructions\n\n${projectAgentMd}`);
|
|
111
|
+
|
|
112
|
+
// Agent-specific AGENT.md
|
|
113
|
+
if (agent.agentMd) agentInstructions.push(`## Agent Instructions\n\n${agent.agentMd}`);
|
|
114
|
+
|
|
115
|
+
// SOUL.md
|
|
116
|
+
if (agent.soulMd) agentInstructions.push(`## Persona\n\n${agent.soulMd}`);
|
|
117
|
+
|
|
118
|
+
// Memory
|
|
119
|
+
const agentMemoryPath = paths.getAgentMemoryDir(cwd, agent.name);
|
|
120
|
+
const agentMemoryMd = readMd(path.join(agentMemoryPath, 'MEMORY.md'));
|
|
121
|
+
const projectMemoryMd = readMd(path.join(paths.getProjectMemoryDir(cwd), 'MEMORY.md'));
|
|
122
|
+
if (agentMemoryMd) agentInstructions.push(`## Agent Memory\n\n${agentMemoryMd}`);
|
|
123
|
+
if (projectMemoryMd) agentInstructions.push(`## Project Memory\n\n${projectMemoryMd}`);
|
|
124
|
+
if (memory) agentInstructions.push(`## Injected Memory\n\n${memory}`);
|
|
125
|
+
|
|
126
|
+
if (agentInstructions.length > 0) {
|
|
127
|
+
layers.push(agentInstructions.join('\n\n---\n\n'));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Layer 5: Tools & Capabilities ────────────────────────────────────────
|
|
131
|
+
// Tool descriptions are injected at the API level (tools parameter), not in system prompt.
|
|
132
|
+
// Only listing custom tools summary here as Tier 2.
|
|
133
|
+
// (Registry handles Tier 1 full schemas via buildToolsForLLM)
|
|
134
|
+
|
|
135
|
+
// ── Layer 6: Task Heuristics ──────────────────────────────────────────────
|
|
136
|
+
if (mode === 'task' || mode === 'daemon' || mode === 'subagent') {
|
|
137
|
+
layers.push(readPromptFile('task-heuristics.md'));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Layer 7: Session Context ──────────────────────────────────────────────
|
|
141
|
+
const sessionCtx = [];
|
|
142
|
+
if (taskBrief) sessionCtx.push(`## Current Task\n\n${taskBrief}`);
|
|
143
|
+
if (todos && todos.length > 0) {
|
|
144
|
+
const todoList = todos.map(t => `- [${t.status === 'completed' ? 'x' : ' '}] ${t.content}`).join('\n');
|
|
145
|
+
sessionCtx.push(`## Active TODOs\n\n${todoList}`);
|
|
146
|
+
}
|
|
147
|
+
if (sessionCtx.length > 0) {
|
|
148
|
+
layers.push(sessionCtx.join('\n\n'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return layers.filter(Boolean).join('\n\n---\n\n').trim();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build a compaction prompt — summarizes conversation for context compression.
|
|
156
|
+
* @param {{ messages: Object[], taskBrief?: string }} opts
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
function buildCompactionPrompt({ messages, taskBrief }) {
|
|
160
|
+
return `You are summarizing a conversation to reduce context length.
|
|
161
|
+
|
|
162
|
+
${taskBrief ? `The task was: ${taskBrief}\n\n` : ''}Summarize the conversation below, preserving:
|
|
163
|
+
- Key decisions made
|
|
164
|
+
- Important findings and facts discovered
|
|
165
|
+
- Files read or modified (with their paths)
|
|
166
|
+
- Active problems or unresolved issues
|
|
167
|
+
- Any errors encountered and how they were handled
|
|
168
|
+
- Current state and what step comes next
|
|
169
|
+
|
|
170
|
+
Be concise but complete. The summary will replace the full conversation history.
|
|
171
|
+
|
|
172
|
+
Format as structured markdown with clear sections.`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build a reminder injection for mid-conversation.
|
|
177
|
+
* @param {{ type: 'anti-drift'|'stall-recovery', vars?: Object }} opts
|
|
178
|
+
* @returns {string}
|
|
179
|
+
*/
|
|
180
|
+
function buildReminder({ type, vars = {} }) {
|
|
181
|
+
const filename = `reminders/${type}.md`;
|
|
182
|
+
return interpolate(readPromptFile(filename), vars);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { assembleSystemPrompt, buildCompactionPrompt, buildReminder };
|
package/core/queue.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require('../infrastructure/database');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In-memory agent message queue helper.
|
|
7
|
+
* The actual queue is in SQLite (agent_messages table).
|
|
8
|
+
* This module provides higher-level queue operations.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get and consume all non-followup pending messages for an agent.
|
|
13
|
+
* Returns them as user-role messages to inject into conversation.
|
|
14
|
+
* Messages with a target_session_id are only drained by that specific session.
|
|
15
|
+
* @param {string} agentName
|
|
16
|
+
* @param {string|null} sessionId - The current session ID (used to match target_session_id)
|
|
17
|
+
* @returns {{ messages: Object[], correlationIds: string[] }}
|
|
18
|
+
*/
|
|
19
|
+
function drainNonFollowup(agentName, sessionId = null) {
|
|
20
|
+
const pending = db.getPendingAgentMessages(agentName);
|
|
21
|
+
const messages = [];
|
|
22
|
+
const correlationIds = [];
|
|
23
|
+
|
|
24
|
+
for (const msg of pending) {
|
|
25
|
+
if (msg.followup) continue;
|
|
26
|
+
if (msg.target_session_id && msg.target_session_id !== sessionId) continue;
|
|
27
|
+
messages.push({
|
|
28
|
+
role: 'user',
|
|
29
|
+
content: `[Message from ${msg.from_agent}]: ${msg.content}`,
|
|
30
|
+
_correlationId: msg.correlation_id || null,
|
|
31
|
+
_messageId: msg.id,
|
|
32
|
+
});
|
|
33
|
+
if (msg.correlation_id) correlationIds.push(msg.correlation_id);
|
|
34
|
+
db.markAgentMessageDelivered(msg.id);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { messages, correlationIds };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get and consume all followup pending messages for an agent.
|
|
42
|
+
* Messages with a target_session_id are only drained by that specific session.
|
|
43
|
+
* @param {string} agentName
|
|
44
|
+
* @param {string|null} sessionId - The current session ID (used to match target_session_id)
|
|
45
|
+
* @returns {Object[]}
|
|
46
|
+
*/
|
|
47
|
+
function drainFollowup(agentName, sessionId = null) {
|
|
48
|
+
const pending = db.getPendingAgentMessages(agentName);
|
|
49
|
+
const messages = [];
|
|
50
|
+
|
|
51
|
+
for (const msg of pending) {
|
|
52
|
+
if (!msg.followup) continue;
|
|
53
|
+
if (msg.target_session_id && msg.target_session_id !== sessionId) continue;
|
|
54
|
+
messages.push({
|
|
55
|
+
role: 'user',
|
|
56
|
+
content: `[Message from ${msg.from_agent}]: ${msg.content}`,
|
|
57
|
+
});
|
|
58
|
+
db.markAgentMessageDelivered(msg.id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return messages;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Post a response to a correlated agent_message (sync messaging).
|
|
66
|
+
* @param {string} correlationId
|
|
67
|
+
* @param {string} response
|
|
68
|
+
*/
|
|
69
|
+
function postCorrelatedResponse(correlationId, response) {
|
|
70
|
+
if (!correlationId) return;
|
|
71
|
+
db.getDb().prepare(
|
|
72
|
+
"UPDATE agent_messages SET response = ?, delivered_at = ? WHERE correlation_id = ? AND status = 'delivered'"
|
|
73
|
+
).run(response, new Date().toISOString(), correlationId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Notify all durable subscribers of a completed task.
|
|
78
|
+
* Injects a message into each subscriber's session.
|
|
79
|
+
* @param {{ taskId: string, task: Object }} opts
|
|
80
|
+
*/
|
|
81
|
+
function notifyTaskSubscribers({ taskId, task }) {
|
|
82
|
+
const subs = db.getPendingSubscriptions(taskId);
|
|
83
|
+
for (const sub of subs) {
|
|
84
|
+
const content = `[Task ${taskId} completed] Agent: ${task.agent_name}, Status: ${task.status}${task.output ? `\nOutput: ${String(task.output).slice(0, 500)}` : ''}`;
|
|
85
|
+
db.enqueueAgentMessage({
|
|
86
|
+
targetAgent: sub.subscriber_agent,
|
|
87
|
+
targetSessionId: sub.subscriber_session_id,
|
|
88
|
+
fromAgent: 'system',
|
|
89
|
+
content,
|
|
90
|
+
followup: false,
|
|
91
|
+
});
|
|
92
|
+
db.markSubscriptionDelivered(sub.id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { drainNonFollowup, drainFollowup, postCorrelatedResponse, notifyTaskSubscribers };
|
package/core/registry.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const Ajv = require('ajv');
|
|
6
|
+
const addFormats = require('ajv-formats');
|
|
7
|
+
const paths = require('../utils/paths');
|
|
8
|
+
|
|
9
|
+
const ajv = new Ajv({ allErrors: true });
|
|
10
|
+
addFormats(ajv);
|
|
11
|
+
|
|
12
|
+
// Cache of compiled validators per tool name
|
|
13
|
+
const _validators = new Map();
|
|
14
|
+
|
|
15
|
+
// Registry of all built-in tools: name → { schema, execute }
|
|
16
|
+
const _builtins = new Map();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a built-in tool.
|
|
20
|
+
* @param {string} name
|
|
21
|
+
* @param {{ schema: Object, execute: Function }} tool
|
|
22
|
+
*/
|
|
23
|
+
function registerBuiltin(name, tool) {
|
|
24
|
+
if (!tool.schema || !tool.execute) {
|
|
25
|
+
throw new Error(`Tool "${name}" must have schema and execute`);
|
|
26
|
+
}
|
|
27
|
+
_builtins.set(name, tool);
|
|
28
|
+
if (tool.schema.input_schema) {
|
|
29
|
+
_validators.set(name, ajv.compile(tool.schema.input_schema));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load and register all built-in tools from the tools/ directory.
|
|
35
|
+
* @param {string} toolsDir - Absolute path to the tools/ directory
|
|
36
|
+
*/
|
|
37
|
+
function loadBuiltinTools(toolsDir) {
|
|
38
|
+
const files = fs.readdirSync(toolsDir).filter(f => f.endsWith('.js'));
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
try {
|
|
41
|
+
const tool = require(path.join(toolsDir, file));
|
|
42
|
+
if (tool.schema && tool.execute) {
|
|
43
|
+
registerBuiltin(tool.schema.name, tool);
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(`Failed to load built-in tool ${file}: ${err.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load a custom JS tool from a folder.
|
|
53
|
+
* @param {string} toolFolder - Absolute path to the tool folder
|
|
54
|
+
* @returns {{ schema: Object, execute: Function }|null}
|
|
55
|
+
*/
|
|
56
|
+
function loadCustomTool(toolFolder) {
|
|
57
|
+
const manifestPath = path.join(toolFolder, 'tool.json');
|
|
58
|
+
const indexPath = path.join(toolFolder, 'index.js');
|
|
59
|
+
if (!fs.existsSync(manifestPath) || !fs.existsSync(indexPath)) return null;
|
|
60
|
+
try {
|
|
61
|
+
const schema = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
62
|
+
// Hot reload: delete require cache before loading
|
|
63
|
+
delete require.cache[require.resolve(indexPath)];
|
|
64
|
+
const execute = require(indexPath);
|
|
65
|
+
return { schema, execute };
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(`Failed to load custom tool at ${toolFolder}: ${err.message}`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Discover custom tools from a directory (each subdirectory is a tool).
|
|
74
|
+
* @param {string} dir
|
|
75
|
+
* @returns {Map<string, { schema: Object, execute: Function }>}
|
|
76
|
+
*/
|
|
77
|
+
function discoverCustomTools(dir) {
|
|
78
|
+
const result = new Map();
|
|
79
|
+
if (!fs.existsSync(dir)) return result;
|
|
80
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
81
|
+
if (!entry.isDirectory()) continue;
|
|
82
|
+
const tool = loadCustomTool(path.join(dir, entry.name));
|
|
83
|
+
if (tool && tool.schema && tool.schema.name) {
|
|
84
|
+
result.set(tool.schema.name, tool);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a tool by name, following the resolution order:
|
|
92
|
+
* built-in → agent-level → project-level → global → not found
|
|
93
|
+
*
|
|
94
|
+
* @param {{ name: string, agentConfig: Object, cwd: string }} opts
|
|
95
|
+
* @returns {{ schema: Object, execute: Function }|null}
|
|
96
|
+
*/
|
|
97
|
+
function resolveTool({ name, agentConfig, cwd }) {
|
|
98
|
+
// 1. Built-in
|
|
99
|
+
if (_builtins.has(name)) return _builtins.get(name);
|
|
100
|
+
|
|
101
|
+
// 2. Agent-level
|
|
102
|
+
if (agentConfig && agentConfig.agentFolder) {
|
|
103
|
+
const agentToolDir = path.join(agentConfig.agentFolder, 'tools', name);
|
|
104
|
+
const agentTool = loadCustomTool(agentToolDir);
|
|
105
|
+
if (agentTool) return agentTool;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. Project-level
|
|
109
|
+
const projectTool = loadCustomTool(path.join(paths.getProjectToolsDir(cwd), name));
|
|
110
|
+
if (projectTool) return projectTool;
|
|
111
|
+
|
|
112
|
+
// 4. Global
|
|
113
|
+
const globalTool = loadCustomTool(path.join(paths.getGlobalToolsDir(), name));
|
|
114
|
+
if (globalTool) return globalTool;
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build the tools list to send to the LLM.
|
|
121
|
+
* Tier 1 (built-in): full schemas
|
|
122
|
+
* Tier 2 (custom): names + one-line descriptions only (full schema loaded via tool_search)
|
|
123
|
+
*
|
|
124
|
+
* @param {{ agentConfig: Object, cwd: string, modeConfig?: Object }} opts
|
|
125
|
+
* @returns {Object[]} OpenAI-format tool definitions
|
|
126
|
+
*/
|
|
127
|
+
function buildToolsForLLM({ agentConfig, cwd, modeConfig = {} }) {
|
|
128
|
+
const allowedTools = modeConfig.tools || [];
|
|
129
|
+
const disallowedTools = new Set(modeConfig.disallowedTools || []);
|
|
130
|
+
|
|
131
|
+
// Auto-disable memory tools if memory is disabled for this agent
|
|
132
|
+
const memoryEnabled = agentConfig?.memory?.enabled !== false;
|
|
133
|
+
if (!memoryEnabled) {
|
|
134
|
+
disallowedTools.add('memory_write');
|
|
135
|
+
disallowedTools.add('memory_read');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = [];
|
|
139
|
+
|
|
140
|
+
// Tier 1: built-in tools — full schemas
|
|
141
|
+
for (const [name, tool] of _builtins) {
|
|
142
|
+
if (disallowedTools.has(name)) continue;
|
|
143
|
+
if (allowedTools.length > 0 && !allowedTools.includes(name)) continue;
|
|
144
|
+
result.push(toolToOpenAIFormat(tool.schema));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get names + descriptions of all custom tools (Tier 2 listing).
|
|
152
|
+
* @param {{ agentConfig: Object, cwd: string }} opts
|
|
153
|
+
* @returns {Array<{ name: string, description: string }>}
|
|
154
|
+
*/
|
|
155
|
+
function listCustomToolSummaries({ agentConfig, cwd }) {
|
|
156
|
+
const summaries = [];
|
|
157
|
+
|
|
158
|
+
// Agent-level
|
|
159
|
+
if (agentConfig && agentConfig.agentFolder) {
|
|
160
|
+
const agentToolsDir = path.join(agentConfig.agentFolder, 'tools');
|
|
161
|
+
for (const [name, tool] of discoverCustomTools(agentToolsDir)) {
|
|
162
|
+
summaries.push({ name, description: tool.schema.description || '' });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Project-level
|
|
167
|
+
for (const [name, tool] of discoverCustomTools(paths.getProjectToolsDir(cwd))) {
|
|
168
|
+
summaries.push({ name, description: tool.schema.description || '' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Global
|
|
172
|
+
for (const [name, tool] of discoverCustomTools(paths.getGlobalToolsDir())) {
|
|
173
|
+
summaries.push({ name, description: tool.schema.description || '' });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return summaries;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Search tools by query string (name or description match).
|
|
181
|
+
* Returns full schemas for matching tools.
|
|
182
|
+
* @param {{ query: string, agentConfig: Object, cwd: string }} opts
|
|
183
|
+
* @returns {Object[]} Full tool schemas
|
|
184
|
+
*/
|
|
185
|
+
function searchTools({ query, agentConfig, cwd }) {
|
|
186
|
+
const lq = query.toLowerCase();
|
|
187
|
+
const results = [];
|
|
188
|
+
|
|
189
|
+
for (const [name, tool] of _builtins) {
|
|
190
|
+
if (name.includes(lq) || (tool.schema.description || '').toLowerCase().includes(lq)) {
|
|
191
|
+
results.push(tool.schema);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (agentConfig && agentConfig.agentFolder) {
|
|
196
|
+
for (const [, tool] of discoverCustomTools(path.join(agentConfig.agentFolder, 'tools'))) {
|
|
197
|
+
const n = tool.schema.name || '';
|
|
198
|
+
if (n.includes(lq) || (tool.schema.description || '').toLowerCase().includes(lq)) {
|
|
199
|
+
results.push(tool.schema);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const [, tool] of discoverCustomTools(paths.getProjectToolsDir(cwd))) {
|
|
205
|
+
const n = tool.schema.name || '';
|
|
206
|
+
if (n.includes(lq) || (tool.schema.description || '').toLowerCase().includes(lq)) {
|
|
207
|
+
results.push(tool.schema);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Validate tool input arguments against the tool's JSON schema.
|
|
216
|
+
* @param {string} toolName
|
|
217
|
+
* @param {Object} input
|
|
218
|
+
* @returns {{ valid: boolean, errors?: string[] }}
|
|
219
|
+
*/
|
|
220
|
+
function validateToolInput(toolName, input) {
|
|
221
|
+
const validator = _validators.get(toolName);
|
|
222
|
+
if (!validator) return { valid: true };
|
|
223
|
+
const valid = validator(input);
|
|
224
|
+
if (!valid) {
|
|
225
|
+
return { valid: false, errors: validator.errors.map(e => `${e.instancePath || '(root)'}: ${e.message}`) };
|
|
226
|
+
}
|
|
227
|
+
return { valid: true };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Convert our tool schema format to OpenAI function-calling format.
|
|
232
|
+
* @param {Object} schema
|
|
233
|
+
* @returns {Object}
|
|
234
|
+
*/
|
|
235
|
+
function toolToOpenAIFormat(schema) {
|
|
236
|
+
return {
|
|
237
|
+
type: 'function',
|
|
238
|
+
function: {
|
|
239
|
+
name: schema.name,
|
|
240
|
+
description: schema.description || '',
|
|
241
|
+
parameters: schema.input_schema || { type: 'object', properties: {} },
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Resolve a custom tool by name and return its OpenAI-format schema, applying
|
|
248
|
+
* the same whitelist/blocklist rules as buildToolsForLLM.
|
|
249
|
+
* Also registers an AJV validator for the tool if not already registered.
|
|
250
|
+
* Returns null if the tool is blocked, not in the whitelist, or not found.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} name - Tool name to activate
|
|
253
|
+
* @param {Object} agentConfig
|
|
254
|
+
* @param {string} cwd
|
|
255
|
+
* @param {Object} modeConfig
|
|
256
|
+
* @returns {Object|null} OpenAI-format tool definition, or null
|
|
257
|
+
*/
|
|
258
|
+
function activateToolByName(name, agentConfig, cwd, modeConfig = {}) {
|
|
259
|
+
const allowedTools = modeConfig.tools || [];
|
|
260
|
+
const disallowedTools = new Set(modeConfig.disallowedTools || []);
|
|
261
|
+
|
|
262
|
+
if (disallowedTools.has(name)) return null;
|
|
263
|
+
if (allowedTools.length > 0 && !allowedTools.includes(name)) return null;
|
|
264
|
+
|
|
265
|
+
const tool = resolveTool({ name, agentConfig, cwd });
|
|
266
|
+
if (!tool) return null;
|
|
267
|
+
|
|
268
|
+
if (tool.schema.input_schema && !_validators.has(name)) {
|
|
269
|
+
try { _validators.set(name, ajv.compile(tool.schema.input_schema)); } catch {}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return toolToOpenAIFormat(tool.schema);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get all registered built-in tool names.
|
|
277
|
+
* @returns {string[]}
|
|
278
|
+
*/
|
|
279
|
+
function getBuiltinNames() {
|
|
280
|
+
return Array.from(_builtins.keys());
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
loadBuiltinTools,
|
|
285
|
+
resolveTool,
|
|
286
|
+
buildToolsForLLM,
|
|
287
|
+
listCustomToolSummaries,
|
|
288
|
+
searchTools,
|
|
289
|
+
validateToolInput,
|
|
290
|
+
activateToolByName,
|
|
291
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require('crypto');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In-memory Remote Method Execution queue.
|
|
7
|
+
*
|
|
8
|
+
* Allows custom tools to call remoteMethodExecution({ method, data, timeoutMs })
|
|
9
|
+
* which blocks until a connected UI client posts a result via
|
|
10
|
+
* POST /remote-methods/:id/result.
|
|
11
|
+
*
|
|
12
|
+
* Methods do NOT survive a server restart (by design).
|
|
13
|
+
* SSE clients are notified of new pending methods and of completed ones
|
|
14
|
+
* (so secondary clients know to discard them).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @type {Map<string, { id: string, method: string, data: any, createdAt: string, resolve: Function, reject: Function, timer: NodeJS.Timeout }>} */
|
|
18
|
+
const _pending = new Map();
|
|
19
|
+
|
|
20
|
+
/** @type {Set<import('http').ServerResponse>} */
|
|
21
|
+
const _clients = new Set();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Send an SSE event to all connected clients.
|
|
25
|
+
* @param {{ type: string, [key: string]: any }} payload
|
|
26
|
+
*/
|
|
27
|
+
function _broadcast(payload) {
|
|
28
|
+
const line = `data: ${JSON.stringify(payload)}\n\n`;
|
|
29
|
+
for (const res of _clients) {
|
|
30
|
+
try {
|
|
31
|
+
res.write(line);
|
|
32
|
+
} catch {
|
|
33
|
+
_clients.delete(res);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register a new SSE client. Immediately flushes all currently-pending
|
|
40
|
+
* methods so the client is fully caught up.
|
|
41
|
+
* @param {import('http').ServerResponse} res
|
|
42
|
+
*/
|
|
43
|
+
function addClient(res) {
|
|
44
|
+
_clients.add(res);
|
|
45
|
+
// Flush current pending methods to the new client
|
|
46
|
+
for (const entry of _pending.values()) {
|
|
47
|
+
try {
|
|
48
|
+
res.write(`data: ${JSON.stringify({
|
|
49
|
+
type: 'method.pending',
|
|
50
|
+
id: entry.id,
|
|
51
|
+
method: entry.method,
|
|
52
|
+
data: entry.data,
|
|
53
|
+
createdAt: entry.createdAt,
|
|
54
|
+
})}\n\n`);
|
|
55
|
+
} catch { /* client may have already gone */ }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Remove an SSE client (on disconnect).
|
|
61
|
+
* @param {import('http').ServerResponse} res
|
|
62
|
+
*/
|
|
63
|
+
function removeClient(res) {
|
|
64
|
+
_clients.delete(res);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Enqueue a remote method call. Returns a Promise that resolves when the UI
|
|
69
|
+
* posts a result, or rejects on timeout.
|
|
70
|
+
*
|
|
71
|
+
* @param {{ method: string, data?: any, timeoutMs?: number }} opts
|
|
72
|
+
* @returns {Promise<any>} Resolves with whatever the UI sends as `result`
|
|
73
|
+
*/
|
|
74
|
+
function enqueue({ method, data = null, timeoutMs = 120_000 }) {
|
|
75
|
+
const id = randomUUID();
|
|
76
|
+
const createdAt = new Date().toISOString();
|
|
77
|
+
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
if (_pending.has(id)) {
|
|
81
|
+
_pending.delete(id);
|
|
82
|
+
_broadcast({ type: 'method.done', id, timedOut: true });
|
|
83
|
+
reject(new Error(`remoteMethodExecution timed out after ${timeoutMs}ms (id=${id})`));
|
|
84
|
+
}
|
|
85
|
+
}, timeoutMs);
|
|
86
|
+
|
|
87
|
+
_pending.set(id, { id, method, data, createdAt, resolve, reject, timer });
|
|
88
|
+
|
|
89
|
+
// Notify all connected UIs
|
|
90
|
+
_broadcast({ type: 'method.pending', id, method, data, createdAt });
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve a pending remote method call with a result from the UI.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} id
|
|
98
|
+
* @param {any} result
|
|
99
|
+
* @returns {'ok'|'not_found'|'already_resolved'}
|
|
100
|
+
*/
|
|
101
|
+
function resolve(id, result) {
|
|
102
|
+
const entry = _pending.get(id);
|
|
103
|
+
if (!entry) return 'not_found';
|
|
104
|
+
|
|
105
|
+
clearTimeout(entry.timer);
|
|
106
|
+
_pending.delete(id);
|
|
107
|
+
|
|
108
|
+
// Notify ALL clients (including the one that just responded) so multi-client
|
|
109
|
+
// setups can discard the pending method immediately.
|
|
110
|
+
_broadcast({ type: 'method.done', id, timedOut: false });
|
|
111
|
+
|
|
112
|
+
entry.resolve(result);
|
|
113
|
+
return 'ok';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Return a snapshot of all currently-pending method IDs (for diagnostics).
|
|
118
|
+
* @returns {string[]}
|
|
119
|
+
*/
|
|
120
|
+
function pendingIds() {
|
|
121
|
+
return Array.from(_pending.keys());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { addClient, removeClient, enqueue, resolve, pendingIds };
|