@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.
Files changed (199) hide show
  1. package/.veil/agents/analyst/AGENT.md +21 -0
  2. package/.veil/agents/analyst/agent.json +23 -0
  3. package/.veil/agents/assistant/AGENT.md +15 -0
  4. package/.veil/agents/assistant/agent.json +19 -0
  5. package/.veil/agents/coder/AGENT.md +18 -0
  6. package/.veil/agents/coder/agent.json +19 -0
  7. package/.veil/agents/hello/AGENT.md +5 -0
  8. package/.veil/agents/hello/agent.json +13 -0
  9. package/.veil/agents/writer/AGENT.md +12 -0
  10. package/.veil/agents/writer/agent.json +17 -0
  11. package/.veil/memory/MEMORY.md +343 -0
  12. package/.veil/memory/agents/analyst/MEMORY.md +55 -0
  13. package/.veil/memory/agents/hello/MEMORY.md +12 -0
  14. package/.veil/runtime.pid +1 -0
  15. package/.veil/settings.json +10 -0
  16. package/.veil-studio/studio.db +0 -0
  17. package/.veil-studio/studio.db-shm +0 -0
  18. package/.veil-studio/studio.db-wal +0 -0
  19. package/PLAN/01-vision.md +26 -0
  20. package/PLAN/02-tech-stack.md +94 -0
  21. package/PLAN/03-agents.md +232 -0
  22. package/PLAN/04-runtime.md +171 -0
  23. package/PLAN/05-tools.md +211 -0
  24. package/PLAN/06-communication.md +243 -0
  25. package/PLAN/07-storage.md +218 -0
  26. package/PLAN/08-api-cli.md +153 -0
  27. package/PLAN/09-permissions.md +108 -0
  28. package/PLAN/10-ably.md +105 -0
  29. package/PLAN/11-file-formats.md +442 -0
  30. package/PLAN/12-folder-structure.md +205 -0
  31. package/PLAN/13-operations.md +212 -0
  32. package/PLAN/README.md +23 -0
  33. package/README.md +128 -0
  34. package/REPORT.md +174 -0
  35. package/TODO.md +45 -0
  36. package/ai-tests/FRONTEND_PROMPT.md +220 -0
  37. package/ai-tests/Research & Planning.md +814 -0
  38. package/ai-tests/prompt-001-basic-api.md +230 -0
  39. package/ai-tests/prompt-002-basic-flows.md +230 -0
  40. package/ai-tests/prompt-003-agent-behaviors.md +220 -0
  41. package/api/middleware.js +60 -0
  42. package/api/routes/agents.js +193 -0
  43. package/api/routes/chat.js +93 -0
  44. package/api/routes/completions.js +122 -0
  45. package/api/routes/daemons.js +80 -0
  46. package/api/routes/memory.js +169 -0
  47. package/api/routes/models.js +40 -0
  48. package/api/routes/remote-methods.js +74 -0
  49. package/api/routes/sessions.js +208 -0
  50. package/api/routes/settings.js +108 -0
  51. package/api/routes/system.js +50 -0
  52. package/api/routes/tasks.js +270 -0
  53. package/api/server.js +120 -0
  54. package/cli/formatter.js +70 -0
  55. package/cli/index.js +443 -0
  56. package/cli/parser.js +113 -0
  57. package/config/config.json +10 -0
  58. package/config/models.json +6826 -0
  59. package/core/agent.js +329 -0
  60. package/core/cancel.js +38 -0
  61. package/core/compaction.js +176 -0
  62. package/core/events.js +13 -0
  63. package/core/loop.js +564 -0
  64. package/core/memory.js +51 -0
  65. package/core/prompt.js +185 -0
  66. package/core/queue.js +96 -0
  67. package/core/registry.js +291 -0
  68. package/core/remote-methods.js +124 -0
  69. package/core/router.js +386 -0
  70. package/core/running-sessions.js +18 -0
  71. package/docs/api/01-system.md +84 -0
  72. package/docs/api/02-agents.md +374 -0
  73. package/docs/api/03-chat.md +269 -0
  74. package/docs/api/04-tasks.md +470 -0
  75. package/docs/api/05-sessions.md +444 -0
  76. package/docs/api/06-daemons.md +142 -0
  77. package/docs/api/07-memory.md +186 -0
  78. package/docs/api/08-settings.md +133 -0
  79. package/docs/api/09-models.md +119 -0
  80. package/docs/api/09-websocket.md +350 -0
  81. package/docs/api/10-completions.md +134 -0
  82. package/docs/api/README.md +116 -0
  83. package/docs/guide/01-quickstart.md +220 -0
  84. package/docs/guide/02-folder-structure.md +185 -0
  85. package/docs/guide/03-configuration.md +252 -0
  86. package/docs/guide/04-agents.md +267 -0
  87. package/docs/guide/05-cli.md +290 -0
  88. package/docs/guide/06-tools.md +643 -0
  89. package/docs/guide/07-permissions.md +236 -0
  90. package/docs/guide/08-memory.md +139 -0
  91. package/docs/guide/09-multi-agent.md +271 -0
  92. package/docs/guide/10-daemons.md +226 -0
  93. package/docs/guide/README.md +53 -0
  94. package/docs/index.html +623 -0
  95. package/examples/README.md +151 -0
  96. package/examples/agents/assistant/AGENT.md +31 -0
  97. package/examples/agents/assistant/SOUL.md +9 -0
  98. package/examples/agents/assistant/agent.json +74 -0
  99. package/examples/agents/hello/AGENT.md +15 -0
  100. package/examples/agents/hello/agent.json +14 -0
  101. package/examples/agents/monitor/AGENT.md +51 -0
  102. package/examples/agents/monitor/agent.json +33 -0
  103. package/examples/agents/monitor/heartbeats/monitor.md +24 -0
  104. package/examples/agents/orchestrator/AGENT.md +70 -0
  105. package/examples/agents/orchestrator/agent.json +30 -0
  106. package/examples/agents/researcher/AGENT.md +52 -0
  107. package/examples/agents/researcher/agent.json +49 -0
  108. package/examples/agents/researcher/skills/web-research.md +28 -0
  109. package/examples/skills/code-review.md +72 -0
  110. package/examples/skills/summarise.md +59 -0
  111. package/examples/skills/web-research.md +42 -0
  112. package/examples/tools/word-count/index.js +27 -0
  113. package/examples/tools/word-count/tool.json +18 -0
  114. package/infrastructure/database.js +563 -0
  115. package/infrastructure/scheduler.js +122 -0
  116. package/llm/client.js +206 -0
  117. package/migrations/001-initial.sql +121 -0
  118. package/migrations/002-debuggability.sql +13 -0
  119. package/migrations/003-drop-orphaned-columns.sql +72 -0
  120. package/migrations/004-session-message-token-fields.sql +78 -0
  121. package/migrations/005-session-thinking.sql +5 -0
  122. package/package.json +30 -0
  123. package/schemas/agent.json +143 -0
  124. package/schemas/settings.json +111 -0
  125. package/scripts/fetch-models.js +93 -0
  126. package/session-debug-scenario.md +248 -0
  127. package/settings/fields.js +52 -0
  128. package/system-prompts/base-core.md +7 -0
  129. package/system-prompts/environment.md +13 -0
  130. package/system-prompts/reminders/anti-drift.md +6 -0
  131. package/system-prompts/reminders/stall-recovery.md +10 -0
  132. package/system-prompts/safety-rules.md +25 -0
  133. package/system-prompts/task-heuristics.md +27 -0
  134. package/test/client.js +71 -0
  135. package/test/integration/01-health.test.js +25 -0
  136. package/test/integration/02-agents.test.js +80 -0
  137. package/test/integration/03-chat-hello.test.js +48 -0
  138. package/test/integration/04-chat-multiturn.test.js +61 -0
  139. package/test/integration/05-chat-writer.test.js +48 -0
  140. package/test/integration/06-task-basic.test.js +68 -0
  141. package/test/integration/07-task-tools.test.js +74 -0
  142. package/test/integration/08-task-code-analysis.test.js +69 -0
  143. package/test/integration/09-memory-analyst.test.js +63 -0
  144. package/test/integration/10-task-advanced.test.js +85 -0
  145. package/test/integration/11-sessions-advanced.test.js +84 -0
  146. package/test/integration/12-assistant-chat-tools.test.js +75 -0
  147. package/test/integration/13-edge-cases.test.js +99 -0
  148. package/test/integration/14-cancel.test.js +62 -0
  149. package/test/integration/15-debug.test.js +106 -0
  150. package/test/integration/16-memory-api.test.js +83 -0
  151. package/test/integration/17-settings-api.test.js +41 -0
  152. package/test/integration/18-tool-search-activation.test.js +119 -0
  153. package/test/results/.gitkeep +0 -0
  154. package/test/runner.js +206 -0
  155. package/test/smoke.js +216 -0
  156. package/tools/agent_message.js +85 -0
  157. package/tools/agent_send.js +80 -0
  158. package/tools/agent_spawn.js +44 -0
  159. package/tools/bash.js +49 -0
  160. package/tools/edit_file.js +41 -0
  161. package/tools/glob.js +64 -0
  162. package/tools/grep.js +82 -0
  163. package/tools/list_dir.js +63 -0
  164. package/tools/log_write.js +31 -0
  165. package/tools/memory_read.js +38 -0
  166. package/tools/memory_search.js +65 -0
  167. package/tools/memory_write.js +42 -0
  168. package/tools/read_file.js +48 -0
  169. package/tools/sleep.js +22 -0
  170. package/tools/task_create.js +41 -0
  171. package/tools/task_respond.js +37 -0
  172. package/tools/task_spawn.js +64 -0
  173. package/tools/task_status.js +39 -0
  174. package/tools/task_subscribe.js +37 -0
  175. package/tools/todo_read.js +26 -0
  176. package/tools/todo_write.js +38 -0
  177. package/tools/tool_activate.js +24 -0
  178. package/tools/tool_search.js +24 -0
  179. package/tools/web_fetch.js +50 -0
  180. package/tools/web_search.js +52 -0
  181. package/tools/write_file.js +28 -0
  182. package/ui/api.js +190 -0
  183. package/ui/app.js +281 -0
  184. package/ui/index.html +382 -0
  185. package/ui/views/agents.js +377 -0
  186. package/ui/views/chat.js +610 -0
  187. package/ui/views/connection.js +96 -0
  188. package/ui/views/daemons.js +129 -0
  189. package/ui/views/feed.js +194 -0
  190. package/ui/views/memory.js +263 -0
  191. package/ui/views/models.js +146 -0
  192. package/ui/views/sessions.js +314 -0
  193. package/ui/views/settings.js +142 -0
  194. package/ui/views/tasks.js +415 -0
  195. package/utils/context.js +49 -0
  196. package/utils/id.js +16 -0
  197. package/utils/models.js +88 -0
  198. package/utils/paths.js +213 -0
  199. 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
+ ```