@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/router.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { runLoop } = require('./loop');
|
|
4
|
+
const { loadAgent, getEffectiveModeConfig } = require('./agent');
|
|
5
|
+
const { assembleSystemPrompt } = require('./prompt');
|
|
6
|
+
const { loadBuiltinTools } = require('./registry');
|
|
7
|
+
const { loadSettings, getModelConfig } = require('../utils/settings');
|
|
8
|
+
const db = require('../infrastructure/database');
|
|
9
|
+
const { notifyTaskSubscribers } = require('./queue');
|
|
10
|
+
const cancelRegistry = require('./cancel');
|
|
11
|
+
const eventBus = require('./events');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// Load built-in tools once at startup
|
|
15
|
+
const TOOLS_DIR = path.join(__dirname, '..', 'tools');
|
|
16
|
+
let _toolsLoaded = false;
|
|
17
|
+
|
|
18
|
+
function ensureToolsLoaded() {
|
|
19
|
+
if (_toolsLoaded) return;
|
|
20
|
+
loadBuiltinTools(TOOLS_DIR);
|
|
21
|
+
_toolsLoaded = true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Run a chat-mode interaction.
|
|
26
|
+
*
|
|
27
|
+
* @param {{ agentName: string, message: string, sessionId?: string, cwd: string, settings: Object, onStreamChunk?: (text: string) => void }} opts
|
|
28
|
+
* @returns {Promise<{ sessionId: string, content: string, tokenUsage: Object }>}
|
|
29
|
+
*/
|
|
30
|
+
async function runChat({ agentName, message, sessionId, cwd, settings, onStreamChunk, onInferenceToolStart, onEvent, onSessionStart }) {
|
|
31
|
+
ensureToolsLoaded();
|
|
32
|
+
|
|
33
|
+
const agent = loadAgent({ cwd, name: agentName });
|
|
34
|
+
const modeConfig = getEffectiveModeConfig(agent, 'chat', settings);
|
|
35
|
+
|
|
36
|
+
// Get or create session
|
|
37
|
+
let sid = sessionId;
|
|
38
|
+
let messages = [];
|
|
39
|
+
let thinking = agent.thinking || null;
|
|
40
|
+
|
|
41
|
+
if (sid) {
|
|
42
|
+
const session = db.getSession(sid);
|
|
43
|
+
if (!session) throw Object.assign(new Error(`Session not found: ${sid}`), { code: 'SESSION_NOT_FOUND' });
|
|
44
|
+
if (session.status === 'closed') throw Object.assign(new Error(`Session is closed: ${sid}`), { code: 'SESSION_CLOSED' });
|
|
45
|
+
// Session-level thinking overrides agent default
|
|
46
|
+
if (session.model_thinking) {
|
|
47
|
+
try { thinking = JSON.parse(session.model_thinking); } catch { /* keep agent default */ }
|
|
48
|
+
}
|
|
49
|
+
// Load existing messages
|
|
50
|
+
const dbMessages = db.getMessages(sid);
|
|
51
|
+
messages = dbMessages.map(m => {
|
|
52
|
+
const msg = { role: m.role, content: m.content };
|
|
53
|
+
if (m.tool_calls) msg.tool_calls = m.tool_calls;
|
|
54
|
+
if (m.tool_call_id) msg.tool_call_id = m.tool_call_id;
|
|
55
|
+
return msg;
|
|
56
|
+
});
|
|
57
|
+
} else {
|
|
58
|
+
const modelConfig = getModelConfig(settings, 'main');
|
|
59
|
+
sid = db.createSession({
|
|
60
|
+
agentName,
|
|
61
|
+
mode: 'chat',
|
|
62
|
+
instanceFolder: cwd,
|
|
63
|
+
model: agent.model || modelConfig.model,
|
|
64
|
+
modelThinking: thinking,
|
|
65
|
+
});
|
|
66
|
+
if (onSessionStart) onSessionStart(sid);
|
|
67
|
+
eventBus.emit('event', { type: 'session.created', sessionId: sid, agentName, event: { mode: 'chat', timestamp: Date.now() } });
|
|
68
|
+
|
|
69
|
+
// Build system prompt and add as first message
|
|
70
|
+
const systemPrompt = assembleSystemPrompt({ agent, mode: 'chat', sessionId: sid, cwd, settings });
|
|
71
|
+
messages.push({ role: 'system', content: systemPrompt });
|
|
72
|
+
db.addMessage({ sessionId: sid, role: 'system', content: systemPrompt });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Add user message
|
|
76
|
+
messages.push({ role: 'user', content: message });
|
|
77
|
+
db.addMessage({ sessionId: sid, role: 'user', content: message });
|
|
78
|
+
eventBus.emit('event', { type: 'chat.user_message', sessionId: sid, agentName, event: { content: message, timestamp: Date.now() } });
|
|
79
|
+
|
|
80
|
+
// Run loop
|
|
81
|
+
const runStartTime = Date.now();
|
|
82
|
+
let responseContent = null;
|
|
83
|
+
let tokenUsage = { input: 0, output: 0, cache: 0, cost: 0 };
|
|
84
|
+
let responseModel = null;
|
|
85
|
+
let totalIterations = 0;
|
|
86
|
+
const toolCallLog = [];
|
|
87
|
+
|
|
88
|
+
// When no SSE callbacks are provided (HTTP JSON, agent_message, agent_spawn, etc.),
|
|
89
|
+
// emit inference.tool events to the bus so WS clients see them.
|
|
90
|
+
const effectiveOnInferenceToolStart = onInferenceToolStart || ((toolName) => {
|
|
91
|
+
eventBus.emit('event', { type: 'chat.inference_tool', sessionId: sid, agentName, event: { name: toolName, timestamp: Date.now() } });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
for await (const event of runLoop({ agent, messages, settings, mode: 'chat', sessionId: sid, cwd, modeConfig, thinking, onStreamChunk, onInferenceToolStart: effectiveOnInferenceToolStart })) {
|
|
95
|
+
if (onEvent) onEvent(event);
|
|
96
|
+
// When no SSE onEvent is provided, emit message events directly to the bus
|
|
97
|
+
// so WS clients see assistant content and tool results regardless of trigger path.
|
|
98
|
+
else if (event.type === 'message') {
|
|
99
|
+
eventBus.emit('event', { type: 'chat.message', sessionId: sid, agentName, event: { ...event.message, finishReason: event.finishReason, iteration: event.iteration, tokenUsage: event.tokenUsage, timestamp: Date.now() } });
|
|
100
|
+
}
|
|
101
|
+
if (event.type === 'chat.response') {
|
|
102
|
+
responseContent = event.content;
|
|
103
|
+
tokenUsage = { input: event.totalTokens.input, output: event.totalTokens.output, cache: event.totalTokens.cache, cost: event.totalTokens.cost };
|
|
104
|
+
responseModel = event.model;
|
|
105
|
+
totalIterations = event.iteration;
|
|
106
|
+
}
|
|
107
|
+
if (event.type === 'tool.end') {
|
|
108
|
+
toolCallLog.push({ name: event.toolName, durationMs: event.durationMs, success: event.success });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
eventBus.emit('event', { type: 'chat.response', sessionId: sid, agentName, event: { content: responseContent ? responseContent.slice(0, 500) : null, toolCount: toolCallLog.length, timestamp: Date.now() } });
|
|
113
|
+
|
|
114
|
+
const session = db.getSession(sid);
|
|
115
|
+
return { sessionId: sid, content: responseContent, tokenUsage, toolCalls: toolCallLog, agentName, model: responseModel, iterations: totalIterations, durationMs: Date.now() - runStartTime, session };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Run a task. Either creates a new session or resumes an existing task.
|
|
120
|
+
*
|
|
121
|
+
* @param {{ taskId: string, cwd: string, settings?: Object }} opts
|
|
122
|
+
* @returns {Promise<{ taskId: string, status: string, output: string|null }>}
|
|
123
|
+
*/
|
|
124
|
+
async function runTask({ taskId, cwd, settings: overrideSettings }) {
|
|
125
|
+
ensureToolsLoaded();
|
|
126
|
+
|
|
127
|
+
const task = db.getTask(taskId);
|
|
128
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
129
|
+
|
|
130
|
+
const taskSettings = overrideSettings || loadSettings({ cwd });
|
|
131
|
+
const agent = loadAgent({ cwd, name: task.agent_name });
|
|
132
|
+
const modeConfig = getEffectiveModeConfig(agent, 'task', taskSettings);
|
|
133
|
+
|
|
134
|
+
const taskInput = typeof task.input === 'string' ? task.input : JSON.stringify(task.input);
|
|
135
|
+
const modelConfig = getModelConfig(taskSettings, 'main');
|
|
136
|
+
|
|
137
|
+
let sessionId;
|
|
138
|
+
let messages;
|
|
139
|
+
|
|
140
|
+
// Check if task has an existing session to continue from
|
|
141
|
+
if (task.session_id) {
|
|
142
|
+
sessionId = task.session_id;
|
|
143
|
+
// Load existing messages from the session
|
|
144
|
+
const dbMessages = db.getMessages(sessionId);
|
|
145
|
+
messages = dbMessages.map(m => {
|
|
146
|
+
const msg = { role: m.role, content: m.content };
|
|
147
|
+
if (m.tool_calls) msg.tool_calls = m.tool_calls;
|
|
148
|
+
if (m.tool_call_id) msg.tool_call_id = m.tool_call_id;
|
|
149
|
+
return msg;
|
|
150
|
+
});
|
|
151
|
+
// Add the new task input as a user message
|
|
152
|
+
messages.push({ role: 'user', content: taskInput });
|
|
153
|
+
db.addMessage({ sessionId, role: 'user', content: taskInput });
|
|
154
|
+
} else {
|
|
155
|
+
// Create new session for this task
|
|
156
|
+
sessionId = db.createSession({
|
|
157
|
+
agentName: task.agent_name,
|
|
158
|
+
mode: 'task',
|
|
159
|
+
instanceFolder: cwd,
|
|
160
|
+
model: agent.model || modelConfig.model,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const systemPrompt = assembleSystemPrompt({
|
|
164
|
+
agent,
|
|
165
|
+
mode: 'task',
|
|
166
|
+
sessionId,
|
|
167
|
+
taskId,
|
|
168
|
+
cwd,
|
|
169
|
+
settings: taskSettings,
|
|
170
|
+
taskBrief: taskInput,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
messages = [
|
|
174
|
+
{ role: 'system', content: systemPrompt },
|
|
175
|
+
{ role: 'user', content: taskInput },
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
db.addMessage({ sessionId, role: 'system', content: systemPrompt });
|
|
179
|
+
db.addMessage({ sessionId, role: 'user', content: taskInput });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
db.updateTask(taskId, { sessionId, status: 'processing', startedAt: new Date().toISOString() });
|
|
183
|
+
db.addTaskEvent({ taskId, type: 'status.change', data: { from: 'pending', to: 'processing' } });
|
|
184
|
+
eventBus.emit('event', { type: 'task.status', taskId, agentName: task.agent_name, event: { status: 'processing', sessionId, timestamp: Date.now() } });
|
|
185
|
+
|
|
186
|
+
// Register cancellation signal
|
|
187
|
+
const cancelSignal = cancelRegistry.register(taskId);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
for await (const event of runLoop({ agent, messages, settings: taskSettings, mode: 'task', taskId, sessionId, cwd, modeConfig, cancelSignal })) {
|
|
191
|
+
// Events emitted — task status/output updated inside loop
|
|
192
|
+
}
|
|
193
|
+
} finally {
|
|
194
|
+
cancelRegistry.cleanup(taskId);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Notify task subscribers on completion
|
|
198
|
+
const finalTask = db.getTask(taskId);
|
|
199
|
+
if (finalTask.status === 'finished' || finalTask.status === 'failed' || finalTask.status === 'canceled') {
|
|
200
|
+
notifyTaskSubscribers({ taskId, task: finalTask });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { taskId, sessionId, status: finalTask.status, output: finalTask.output };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resume a waiting task after /tasks/:id/respond is called.
|
|
208
|
+
* Loads existing session and continues the loop.
|
|
209
|
+
*
|
|
210
|
+
* @param {{ taskId: string, cwd: string, settings?: Object }} opts
|
|
211
|
+
* @returns {Promise<{ taskId: string, status: string, output: string|null }>}
|
|
212
|
+
*/
|
|
213
|
+
async function resumeTask({ taskId, cwd, settings: overrideSettings }) {
|
|
214
|
+
ensureToolsLoaded();
|
|
215
|
+
|
|
216
|
+
const task = db.getTask(taskId);
|
|
217
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
218
|
+
if (!task.session_id) throw new Error(`Task ${taskId} has no session to resume`);
|
|
219
|
+
|
|
220
|
+
const taskSettings = overrideSettings || loadSettings({ cwd });
|
|
221
|
+
const agent = loadAgent({ cwd, name: task.agent_name });
|
|
222
|
+
const modeConfig = getEffectiveModeConfig(agent, 'task', taskSettings);
|
|
223
|
+
|
|
224
|
+
const sessionId = task.session_id;
|
|
225
|
+
|
|
226
|
+
// Load existing messages from the session
|
|
227
|
+
const dbMessages = db.getMessages(sessionId);
|
|
228
|
+
const messages = dbMessages.map(m => {
|
|
229
|
+
const msg = { role: m.role, content: m.content };
|
|
230
|
+
if (m.tool_calls) msg.tool_calls = m.tool_calls;
|
|
231
|
+
if (m.tool_call_id) msg.tool_call_id = m.tool_call_id;
|
|
232
|
+
return msg;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
db.updateTask(taskId, { status: 'processing' });
|
|
236
|
+
db.addTaskEvent({ taskId, type: 'status.change', data: { from: 'waiting', to: 'processing', reason: 'resumed' } });
|
|
237
|
+
eventBus.emit('event', { type: 'task.status', taskId, agentName: task.agent_name, event: { status: 'processing', reason: 'resumed', timestamp: Date.now() } });
|
|
238
|
+
|
|
239
|
+
// Register cancellation signal
|
|
240
|
+
const cancelSignal = cancelRegistry.register(taskId);
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
for await (const event of runLoop({ agent, messages, settings: taskSettings, mode: 'task', taskId, sessionId, cwd, modeConfig, cancelSignal })) {
|
|
244
|
+
// Events emitted — task status/output updated inside loop
|
|
245
|
+
}
|
|
246
|
+
} finally {
|
|
247
|
+
cancelRegistry.cleanup(taskId);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Notify task subscribers on completion
|
|
251
|
+
const finalTask = db.getTask(taskId);
|
|
252
|
+
if (finalTask.status === 'finished' || finalTask.status === 'failed' || finalTask.status === 'canceled') {
|
|
253
|
+
notifyTaskSubscribers({ taskId, task: finalTask });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { taskId, status: finalTask.status, output: finalTask.output };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Run a subagent (spawned by agent_spawn). Uses modes.subagent config.
|
|
261
|
+
*
|
|
262
|
+
* @param {{ taskId: string, cwd: string, settings?: Object }} opts
|
|
263
|
+
* @returns {Promise<{ taskId: string, status: string, output: string|null }>}
|
|
264
|
+
*/
|
|
265
|
+
async function runSubagent({ taskId, cwd, settings: overrideSettings }) {
|
|
266
|
+
ensureToolsLoaded();
|
|
267
|
+
|
|
268
|
+
const task = db.getTask(taskId);
|
|
269
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
270
|
+
|
|
271
|
+
const taskSettings = overrideSettings || loadSettings({ cwd });
|
|
272
|
+
const agent = loadAgent({ cwd, name: task.agent_name });
|
|
273
|
+
const modeConfig = getEffectiveModeConfig(agent, 'subagent', taskSettings);
|
|
274
|
+
|
|
275
|
+
// Create session for this subagent run
|
|
276
|
+
const modelConfig = getModelConfig(taskSettings, 'main');
|
|
277
|
+
const sessionId = db.createSession({
|
|
278
|
+
agentName: task.agent_name,
|
|
279
|
+
mode: 'subagent',
|
|
280
|
+
instanceFolder: cwd,
|
|
281
|
+
model: agent.model || modelConfig.model,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
db.updateTask(taskId, { sessionId, status: 'processing', startedAt: new Date().toISOString() });
|
|
285
|
+
db.addTaskEvent({ taskId, type: 'status.change', data: { from: 'pending', to: 'processing' } });
|
|
286
|
+
eventBus.emit('event', { type: 'task.status', taskId, agentName: task.agent_name, event: { status: 'processing', sessionId, timestamp: Date.now() } });
|
|
287
|
+
|
|
288
|
+
// Register cancellation signal
|
|
289
|
+
const cancelSignal = cancelRegistry.register(taskId);
|
|
290
|
+
|
|
291
|
+
const taskInput = typeof task.input === 'string' ? task.input : JSON.stringify(task.input);
|
|
292
|
+
|
|
293
|
+
const systemPrompt = assembleSystemPrompt({
|
|
294
|
+
agent,
|
|
295
|
+
mode: 'subagent',
|
|
296
|
+
sessionId,
|
|
297
|
+
taskId,
|
|
298
|
+
cwd,
|
|
299
|
+
settings: taskSettings,
|
|
300
|
+
taskBrief: taskInput,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const messages = [
|
|
304
|
+
{ role: 'system', content: systemPrompt },
|
|
305
|
+
{ role: 'user', content: taskInput },
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
db.addMessage({ sessionId, role: 'system', content: systemPrompt });
|
|
309
|
+
db.addMessage({ sessionId, role: 'user', content: taskInput });
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
for await (const event of runLoop({ agent, messages, settings: taskSettings, mode: 'subagent', taskId, sessionId, cwd, modeConfig, cancelSignal })) {
|
|
313
|
+
// Events emitted — task status/output updated inside loop
|
|
314
|
+
}
|
|
315
|
+
} finally {
|
|
316
|
+
cancelRegistry.cleanup(taskId);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Notify task subscribers on completion
|
|
320
|
+
const finalTask = db.getTask(taskId);
|
|
321
|
+
if (finalTask.status === 'finished' || finalTask.status === 'failed' || finalTask.status === 'canceled') {
|
|
322
|
+
notifyTaskSubscribers({ taskId, task: finalTask });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { taskId, status: finalTask.status, output: finalTask.output };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Run a single daemon tick.
|
|
330
|
+
*
|
|
331
|
+
* @param {{ agentName: string, cwd: string, settings: Object }} opts
|
|
332
|
+
* @returns {Promise<{ status: string, content: string|null }>}
|
|
333
|
+
*/
|
|
334
|
+
async function runDaemonTick({ agentName, cwd, settings }) {
|
|
335
|
+
ensureToolsLoaded();
|
|
336
|
+
|
|
337
|
+
const agent = loadAgent({ cwd, name: agentName });
|
|
338
|
+
const modeConfig = getEffectiveModeConfig(agent, 'daemon', settings);
|
|
339
|
+
|
|
340
|
+
const modelConfig = getModelConfig(settings, 'main');
|
|
341
|
+
const sessionId = db.createSession({
|
|
342
|
+
agentName,
|
|
343
|
+
mode: 'daemon',
|
|
344
|
+
instanceFolder: cwd,
|
|
345
|
+
model: agent.model || modelConfig.model,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const systemPrompt = assembleSystemPrompt({ agent, mode: 'daemon', sessionId, cwd, settings });
|
|
349
|
+
|
|
350
|
+
// Read heartbeat file if exists
|
|
351
|
+
const { getHeartbeatsDir } = require('../utils/paths');
|
|
352
|
+
const fs = require('fs');
|
|
353
|
+
const path = require('path');
|
|
354
|
+
const heartbeatPath = (agent.modes?.daemon?.heartbeatFile)
|
|
355
|
+
? path.resolve(cwd, agent.modes.daemon.heartbeatFile)
|
|
356
|
+
: path.join(getHeartbeatsDir(cwd), `${agentName}.md`);
|
|
357
|
+
|
|
358
|
+
let daemonInstruction = `You are running a scheduled daemon tick. Check your heartbeat file and perform your scheduled duties.`;
|
|
359
|
+
if (fs.existsSync(heartbeatPath)) {
|
|
360
|
+
daemonInstruction = fs.readFileSync(heartbeatPath, 'utf8');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const messages = [
|
|
364
|
+
{ role: 'system', content: systemPrompt },
|
|
365
|
+
{ role: 'user', content: daemonInstruction },
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
eventBus.emit('event', { type: 'daemon.tick', agentName, event: { status: 'running', sessionId, timestamp: Date.now() } });
|
|
369
|
+
|
|
370
|
+
let result = null;
|
|
371
|
+
try {
|
|
372
|
+
for await (const event of runLoop({ agent, messages, settings, mode: 'daemon', sessionId, cwd, modeConfig })) {
|
|
373
|
+
if (event.type === 'daemon.tick.complete') result = event.content;
|
|
374
|
+
}
|
|
375
|
+
eventBus.emit('event', { type: 'daemon.tick', agentName, event: { status: 'done', sessionId, timestamp: Date.now() } });
|
|
376
|
+
} catch (err) {
|
|
377
|
+
eventBus.emit('event', { type: 'daemon.tick', agentName, event: { status: 'error', error: err.message, sessionId, timestamp: Date.now() } });
|
|
378
|
+
throw err;
|
|
379
|
+
} finally {
|
|
380
|
+
db.closeSession(sessionId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { status: 'done', content: result };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
module.exports = { runChat, runTask, resumeTask, runSubagent, runDaemonTick };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory set of session IDs that currently have an active runLoop running.
|
|
5
|
+
* Used by agent_message / agent_send to decide whether to queue-inject or
|
|
6
|
+
* trigger a new turn directly.
|
|
7
|
+
*
|
|
8
|
+
* Populated / cleared inside core/loop.js runLoop() via try/finally.
|
|
9
|
+
* Process-local only — does not survive restarts (that is intentional:
|
|
10
|
+
* after a restart no loop is running, so the set is correctly empty).
|
|
11
|
+
*/
|
|
12
|
+
const running = new Set();
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
add: (sessionId) => running.add(sessionId),
|
|
16
|
+
delete: (sessionId) => running.delete(sessionId),
|
|
17
|
+
has: (sessionId) => running.has(sessionId),
|
|
18
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# System Endpoints
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## GET /health
|
|
6
|
+
|
|
7
|
+
Lightweight liveness check. Returns immediately, no DB access.
|
|
8
|
+
|
|
9
|
+
**Response**
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"status": "ok",
|
|
13
|
+
"version": "0.1.0",
|
|
14
|
+
"uptime": 142
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Field | Type | Description |
|
|
19
|
+
|-------|------|-------------|
|
|
20
|
+
| `status` | string | Always `"ok"` |
|
|
21
|
+
| `version` | string | VeilCLI version |
|
|
22
|
+
| `uptime` | integer | Seconds since server start |
|
|
23
|
+
|
|
24
|
+
**Example**
|
|
25
|
+
```bash
|
|
26
|
+
curl http://localhost:5050/health
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## GET /status
|
|
32
|
+
|
|
33
|
+
Full server status snapshot including agent, session, task, and daemon counts.
|
|
34
|
+
|
|
35
|
+
**Response**
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"status": "ok",
|
|
39
|
+
"version": "0.1.0",
|
|
40
|
+
"uptime": 261,
|
|
41
|
+
"cwd": "/home/user/my-workspace",
|
|
42
|
+
"agents": 3,
|
|
43
|
+
"activeSessions": 2,
|
|
44
|
+
"pendingTasks": 1,
|
|
45
|
+
"processingTasks": 0,
|
|
46
|
+
"daemons": 1
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Field | Type | Description |
|
|
51
|
+
|-------|------|-------------|
|
|
52
|
+
| `cwd` | string | Workspace directory the server was started in |
|
|
53
|
+
| `agents` | integer | Number of agents found in `.veil/agents/` |
|
|
54
|
+
| `activeSessions` | integer | Open chat/task sessions |
|
|
55
|
+
| `pendingTasks` | integer | Tasks queued but not yet started |
|
|
56
|
+
| `processingTasks` | integer | Tasks currently running |
|
|
57
|
+
| `daemons` | integer | Daemon agents currently scheduled |
|
|
58
|
+
|
|
59
|
+
**Example**
|
|
60
|
+
```bash
|
|
61
|
+
curl http://localhost:5050/status
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## POST /shutdown
|
|
67
|
+
|
|
68
|
+
Triggers a graceful shutdown of the server. The server closes active connections, stops all daemon schedulers, closes the SQLite database, and removes its PID file before exiting.
|
|
69
|
+
|
|
70
|
+
**Request body** — none
|
|
71
|
+
|
|
72
|
+
**Response**
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"status": "shutting_down"
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The process exits approximately 500ms after responding.
|
|
80
|
+
|
|
81
|
+
**Example**
|
|
82
|
+
```bash
|
|
83
|
+
curl -X POST http://localhost:5050/shutdown
|
|
84
|
+
```
|