@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/agent.js ADDED
@@ -0,0 +1,329 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const Ajv = require('ajv');
6
+ const paths = require('../utils/paths');
7
+
8
+ const ajv = new Ajv({ allErrors: true });
9
+ let _agentSchema = null;
10
+
11
+ /**
12
+ * Load and cache the agent JSON schema.
13
+ * @returns {Function} Compiled ajv validator
14
+ */
15
+ function getAgentValidator() {
16
+ if (_agentSchema) return _agentSchema;
17
+ const schema = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'schemas', 'agent.json'), 'utf8'));
18
+ _agentSchema = ajv.compile(schema);
19
+ return _agentSchema;
20
+ }
21
+
22
+ /**
23
+ * Resolve $AGENT_FOLDER variable in a string.
24
+ * @param {string} text
25
+ * @param {string} agentFolderPath
26
+ * @returns {string}
27
+ */
28
+ function resolveAgentFolder(text, agentFolderPath) {
29
+ return text.replace(/\$AGENT_FOLDER/g, agentFolderPath);
30
+ }
31
+
32
+ /**
33
+ * Load a single agent from its folder.
34
+ * Validates agent.json against schema. Throws on invalid.
35
+ *
36
+ * @param {string} agentFolderPath - Absolute path to the agent folder
37
+ * @returns {Object} Loaded and validated agent definition
38
+ */
39
+ function loadAgentFromFolder(agentFolderPath) {
40
+ const configPath = path.join(agentFolderPath, 'agent.json');
41
+ if (!fs.existsSync(configPath)) {
42
+ throw new Error(`Agent folder ${agentFolderPath} is missing agent.json`);
43
+ }
44
+
45
+ let config;
46
+ try {
47
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
48
+ } catch (err) {
49
+ throw new Error(`Failed to parse agent.json at ${configPath}: ${err.message}`);
50
+ }
51
+
52
+ const validate = getAgentValidator();
53
+ if (!validate(config)) {
54
+ const errors = validate.errors.map(e => ` ${e.instancePath || '(root)'}: ${e.message}`).join('\n');
55
+ throw new Error(`Invalid agent.json at ${configPath}:\n${errors}`);
56
+ }
57
+
58
+ // Read AGENT.md
59
+ const agentMdPath = path.join(agentFolderPath, 'AGENT.md');
60
+ let agentMd = '';
61
+ if (fs.existsSync(agentMdPath)) {
62
+ agentMd = resolveAgentFolder(fs.readFileSync(agentMdPath, 'utf8'), agentFolderPath);
63
+ }
64
+
65
+ // Read SOUL.md (optional)
66
+ const soulMdPath = path.join(agentFolderPath, 'SOUL.md');
67
+ let soulMd = null;
68
+ if (fs.existsSync(soulMdPath)) {
69
+ soulMd = resolveAgentFolder(fs.readFileSync(soulMdPath, 'utf8'), agentFolderPath);
70
+ }
71
+
72
+ return {
73
+ ...config,
74
+ agentFolder: agentFolderPath,
75
+ agentMd,
76
+ soulMd,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Find and load a named agent. Checks project-level first, then global.
82
+ *
83
+ * @param {string} cwd - Project root directory
84
+ * @param {string} name - Agent name
85
+ * @returns {Object} Loaded agent definition
86
+ */
87
+ function loadAgent({ cwd, name }) {
88
+ const projectAgentDir = paths.getAgentDir(cwd, name);
89
+ if (fs.existsSync(projectAgentDir)) {
90
+ return { ...loadAgentFromFolder(projectAgentDir), source: 'project' };
91
+ }
92
+
93
+ const globalAgentDir = path.join(paths.getGlobalAgentsDir(), name);
94
+ if (fs.existsSync(globalAgentDir)) {
95
+ return { ...loadAgentFromFolder(globalAgentDir), source: 'global' };
96
+ }
97
+
98
+ const bundledAgentDir = path.join(__dirname, '..', '.veil', 'agents', name);
99
+ if (fs.existsSync(bundledAgentDir)) {
100
+ return { ...loadAgentFromFolder(bundledAgentDir), source: 'bundled' };
101
+ }
102
+
103
+ throw new Error(`Agent "${name}" not found. Looked in:\n ${projectAgentDir}\n ${globalAgentDir}\n ${bundledAgentDir}`);
104
+ }
105
+
106
+ /**
107
+ * List all available agents (project-level + global, deduped by name).
108
+ *
109
+ * @param {string} cwd - Project root directory
110
+ * @returns {Object[]} Array of { name, description, folder, source } objects
111
+ */
112
+ function listAgents({ cwd }) {
113
+ const found = new Map();
114
+
115
+ // Project-level agents first (take priority)
116
+ const projectDir = paths.getProjectAgentsDir(cwd);
117
+ if (fs.existsSync(projectDir)) {
118
+ for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {
119
+ if (!entry.isDirectory()) continue;
120
+ const agentFolder = path.join(projectDir, entry.name);
121
+ const configPath = path.join(agentFolder, 'agent.json');
122
+ if (!fs.existsSync(configPath)) continue;
123
+ try {
124
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
125
+ found.set(entry.name, {
126
+ name: entry.name,
127
+ description: config.description || '',
128
+ model: config.model || '',
129
+ modes: config.modes ? Object.keys(config.modes).filter(m => config.modes[m]?.enabled) : [],
130
+ folder: agentFolder,
131
+ source: 'project',
132
+ });
133
+ } catch {}
134
+ }
135
+ }
136
+
137
+ // Global agents (only add if not already in project)
138
+ const globalDir = paths.getGlobalAgentsDir();
139
+ if (fs.existsSync(globalDir)) {
140
+ for (const entry of fs.readdirSync(globalDir, { withFileTypes: true })) {
141
+ if (!entry.isDirectory() || found.has(entry.name)) continue;
142
+ const agentFolder = path.join(globalDir, entry.name);
143
+ const configPath = path.join(agentFolder, 'agent.json');
144
+ if (!fs.existsSync(configPath)) continue;
145
+ try {
146
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
147
+ found.set(entry.name, {
148
+ name: entry.name,
149
+ description: config.description || '',
150
+ model: config.model || '',
151
+ modes: config.modes ? Object.keys(config.modes).filter(m => config.modes[m]?.enabled) : [],
152
+ folder: agentFolder,
153
+ source: 'global',
154
+ });
155
+ } catch {}
156
+ }
157
+ }
158
+
159
+ // Bundled agents (shipped with CLI, lowest priority)
160
+ const bundledDir = path.join(__dirname, '..', '.veil', 'agents');
161
+ if (fs.existsSync(bundledDir)) {
162
+ for (const entry of fs.readdirSync(bundledDir, { withFileTypes: true })) {
163
+ if (!entry.isDirectory() || found.has(entry.name)) continue;
164
+ const agentFolder = path.join(bundledDir, entry.name);
165
+ const configPath = path.join(agentFolder, 'agent.json');
166
+ if (!fs.existsSync(configPath)) continue;
167
+ try {
168
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
169
+ found.set(entry.name, {
170
+ name: entry.name,
171
+ description: config.description || '',
172
+ model: config.model || '',
173
+ modes: config.modes ? Object.keys(config.modes).filter(m => config.modes[m]?.enabled) : [],
174
+ folder: agentFolder,
175
+ source: 'bundled',
176
+ });
177
+ } catch {}
178
+ }
179
+ }
180
+
181
+ return Array.from(found.values());
182
+ }
183
+
184
+ /**
185
+ * Get effective mode config for an agent (merges top-level defaults with mode-specific overrides).
186
+ *
187
+ * @param {Object} agent - Loaded agent definition
188
+ * @param {string} mode - 'chat'|'task'|'daemon'|'subagent'
189
+ * @param {Object} settings - Global/project settings
190
+ * @returns {Object} Effective mode config
191
+ */
192
+ function getEffectiveModeConfig(agent, mode, settings) {
193
+ const modeConfig = (agent.modes && agent.modes[mode]) || {};
194
+ return {
195
+ tools: modeConfig.tools || [],
196
+ disallowedTools: modeConfig.disallowedTools || [],
197
+ skills: modeConfig.skills || [],
198
+ autoLoadSkills: modeConfig.autoLoadSkills || [],
199
+ mcpServers: modeConfig.mcpServers || [],
200
+ allowedAgents: modeConfig.allowedAgents || [],
201
+ disallowedAgents: modeConfig.disallowedAgents || [],
202
+ permissions: modeConfig.permissions || { allow: [], deny: [] },
203
+ maxIterations: modeConfig.maxIterations || settings.maxIterations || 50,
204
+ maxDurationSeconds: modeConfig.maxDurationSeconds || settings.maxDurationSeconds || 300,
205
+ onExhausted: modeConfig.onExhausted || 'fail',
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Resolve the agent directory for a given level.
211
+ * @param {'project'|'global'} level
212
+ * @param {string} cwd
213
+ * @param {string} name
214
+ * @returns {string}
215
+ */
216
+ function resolveAgentDirForLevel(level, cwd, name) {
217
+ if (level === 'global') return path.join(paths.getGlobalAgentsDir(), name);
218
+ return paths.getAgentDir(cwd, name);
219
+ }
220
+
221
+ /**
222
+ * Check all levels for an existing agent with the given name.
223
+ * @param {string} name
224
+ * @param {string} cwd
225
+ * @returns {{ exists: boolean, level: string|null, folder: string|null }}
226
+ */
227
+ function findExistingAgent(name, cwd) {
228
+ const checks = [
229
+ { level: 'project', folder: paths.getAgentDir(cwd, name) },
230
+ { level: 'global', folder: path.join(paths.getGlobalAgentsDir(), name) },
231
+ { level: 'bundled', folder: path.join(__dirname, '..', '.veil', 'agents', name) },
232
+ ];
233
+ for (const { level, folder } of checks) {
234
+ if (fs.existsSync(path.join(folder, 'agent.json'))) {
235
+ return { exists: true, level, folder };
236
+ }
237
+ }
238
+ return { exists: false, level: null, folder: null };
239
+ }
240
+
241
+ /**
242
+ * Create a new agent at the specified level.
243
+ *
244
+ * @param {{ name: string, level?: 'project'|'global', cwd: string, config: Object, agentMd?: string }} opts
245
+ * @returns {{ name: string, level: string, folder: string }}
246
+ */
247
+ function createAgent({ name, level = 'project', cwd, config, agentMd = '' }) {
248
+ if (!name || typeof name !== 'string' || !/^[a-z0-9_-]+$/i.test(name)) {
249
+ throw Object.assign(new Error(`Invalid agent name "${name}". Use only letters, numbers, hyphens and underscores.`), { code: 'INVALID_NAME' });
250
+ }
251
+
252
+ const existing = findExistingAgent(name, cwd);
253
+ if (existing.exists) {
254
+ throw Object.assign(new Error(`Agent "${name}" already exists at ${existing.level} level (${existing.folder})`), { code: 'AGENT_EXISTS', existingLevel: existing.level });
255
+ }
256
+
257
+ // Merge name into config and validate
258
+ const fullConfig = { ...config, name };
259
+ const validate = getAgentValidator();
260
+ if (!validate(fullConfig)) {
261
+ const errors = validate.errors.map(e => ` ${e.instancePath || '(root)'}: ${e.message}`).join('\n');
262
+ throw Object.assign(new Error(`Invalid agent config:\n${errors}`), { code: 'INVALID_CONFIG' });
263
+ }
264
+
265
+ const agentDir = resolveAgentDirForLevel(level, cwd, name);
266
+ fs.mkdirSync(agentDir, { recursive: true });
267
+ fs.writeFileSync(path.join(agentDir, 'agent.json'), JSON.stringify(fullConfig, null, 2), 'utf8');
268
+ if (agentMd) {
269
+ fs.writeFileSync(path.join(agentDir, 'AGENT.md'), agentMd, 'utf8');
270
+ }
271
+
272
+ return { name, level, folder: agentDir };
273
+ }
274
+
275
+ /**
276
+ * Update an existing agent's config and/or AGENT.md.
277
+ * Finds the agent across all writable levels. Bundled agents cannot be updated.
278
+ *
279
+ * @param {{ name: string, cwd: string, config?: Object, agentMd?: string }} opts
280
+ * @returns {{ name: string, level: string, folder: string }}
281
+ */
282
+ function updateAgent({ name, cwd, config, agentMd }) {
283
+ const existing = findExistingAgent(name, cwd);
284
+ if (!existing.exists) {
285
+ throw Object.assign(new Error(`Agent "${name}" not found`), { code: 'AGENT_NOT_FOUND' });
286
+ }
287
+ if (existing.level === 'bundled') {
288
+ throw Object.assign(new Error(`Agent "${name}" is a bundled agent and cannot be modified`), { code: 'AGENT_READ_ONLY' });
289
+ }
290
+
291
+ if (config !== undefined) {
292
+ const configPath = path.join(existing.folder, 'agent.json');
293
+ const current = JSON.parse(fs.readFileSync(configPath, 'utf8'));
294
+ const merged = { ...current, ...config, name };
295
+ const validate = getAgentValidator();
296
+ if (!validate(merged)) {
297
+ const errors = validate.errors.map(e => ` ${e.instancePath || '(root)'}: ${e.message}`).join('\n');
298
+ throw Object.assign(new Error(`Invalid agent config:\n${errors}`), { code: 'INVALID_CONFIG' });
299
+ }
300
+ fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), 'utf8');
301
+ }
302
+
303
+ if (agentMd !== undefined) {
304
+ fs.writeFileSync(path.join(existing.folder, 'AGENT.md'), agentMd, 'utf8');
305
+ }
306
+
307
+ return { name, level: existing.level, folder: existing.folder };
308
+ }
309
+
310
+ /**
311
+ * Delete an agent folder. Bundled agents cannot be deleted.
312
+ *
313
+ * @param {{ name: string, cwd: string }} opts
314
+ * @returns {{ name: string, level: string, folder: string }}
315
+ */
316
+ function deleteAgent({ name, cwd }) {
317
+ const existing = findExistingAgent(name, cwd);
318
+ if (!existing.exists) {
319
+ throw Object.assign(new Error(`Agent "${name}" not found`), { code: 'AGENT_NOT_FOUND' });
320
+ }
321
+ if (existing.level === 'bundled') {
322
+ throw Object.assign(new Error(`Agent "${name}" is a bundled agent and cannot be deleted`), { code: 'AGENT_READ_ONLY' });
323
+ }
324
+
325
+ fs.rmSync(existing.folder, { recursive: true, force: true });
326
+ return { name, level: existing.level, folder: existing.folder };
327
+ }
328
+
329
+ module.exports = { loadAgent, listAgents, getEffectiveModeConfig, createAgent, updateAgent, deleteAgent };
package/core/cancel.js ADDED
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cooperative cancellation registry.
5
+ * Maps taskId → AbortController so that API routes can signal running loops to stop.
6
+ */
7
+
8
+ const _signals = new Map();
9
+
10
+ /**
11
+ * Register a cancellation signal for a task.
12
+ * @param {string} taskId
13
+ * @returns {AbortSignal}
14
+ */
15
+ function register(taskId) {
16
+ const ac = new AbortController();
17
+ _signals.set(taskId, ac);
18
+ return ac.signal;
19
+ }
20
+
21
+ /**
22
+ * Signal a running task to cancel.
23
+ * @param {string} taskId
24
+ */
25
+ function cancel(taskId) {
26
+ const ac = _signals.get(taskId);
27
+ if (ac) ac.abort();
28
+ }
29
+
30
+ /**
31
+ * Remove the signal after task completion.
32
+ * @param {string} taskId
33
+ */
34
+ function cleanup(taskId) {
35
+ _signals.delete(taskId);
36
+ }
37
+
38
+ module.exports = { register, cancel, cleanup };
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ const { callLLM, extractMessage } = require('../llm/client');
4
+ const { buildCompactionPrompt } = require('./prompt');
5
+ const { getModelConfig } = require('../utils/settings');
6
+ const { writeMemory } = require('./memory');
7
+ const F = require('../settings/fields');
8
+
9
+ /**
10
+ * Apply observation masking: replace tool outputs older than N turns with a placeholder.
11
+ * @param {Object[]} messages - Conversation messages array (mutated in place)
12
+ * @param {number} maskingTurns - How many recent turns to keep unmasked (default: 10)
13
+ * @returns {Object[]} Mutated messages array
14
+ */
15
+ function applyObservationMasking(messages, maskingTurns = F.DEFAULT_OBSERVATION_MASKING_TURNS) {
16
+ // Count assistant turns from the end
17
+ let assistantTurnCount = 0;
18
+ const masked = [];
19
+
20
+ for (let i = messages.length - 1; i >= 0; i--) {
21
+ const msg = messages[i];
22
+ if (msg.role === 'assistant') assistantTurnCount++;
23
+
24
+ if (assistantTurnCount > maskingTurns && msg.role === 'tool' && msg.content && msg.content !== '[output hidden]') {
25
+ masked.push(i);
26
+ }
27
+ }
28
+
29
+ for (const idx of masked) {
30
+ messages[idx] = { ...messages[idx], content: '[output hidden]' };
31
+ }
32
+
33
+ return messages;
34
+ }
35
+
36
+ /**
37
+ * Truncate large tool outputs in recent turns.
38
+ * @param {Object[]} messages - Conversation messages (mutated in place)
39
+ * @param {number} maxChars - Max chars before truncation (default: 500)
40
+ * @returns {Object[]}
41
+ */
42
+ function applyToolResultClearing(messages, maxChars = 500) {
43
+ for (const msg of messages) {
44
+ if (msg.role === 'tool' && msg.content && msg.content !== '[output hidden]' && msg.content.length > maxChars) {
45
+ msg.content = msg.content.slice(0, maxChars) + '\n... [truncated]';
46
+ }
47
+ }
48
+ return messages;
49
+ }
50
+
51
+ /**
52
+ * Estimate context usage as a fraction (0-1).
53
+ * Uses a rough 4 chars/token approximation.
54
+ * @param {Object[]} messages
55
+ * @param {number} contextWindow - Model's context window size in tokens (default: 100000)
56
+ * @returns {number} Usage fraction 0-1
57
+ */
58
+ function estimateContextUsage(messages, contextWindow = 100000) {
59
+ const totalChars = messages.reduce((sum, m) => {
60
+ let chars = (m.content || '').length;
61
+ if (m.tool_calls) chars += JSON.stringify(m.tool_calls).length;
62
+ return sum + chars;
63
+ }, 0);
64
+ const estimatedTokens = Math.ceil(totalChars / 4);
65
+ return estimatedTokens / contextWindow;
66
+ }
67
+
68
+ /**
69
+ * Pre-compaction memory extraction: ask the model what should be remembered long-term.
70
+ * @param {{ messages: Object[], settings: Object, cwd: string, agentName: string }} opts
71
+ */
72
+ async function extractMemoriesBeforeCompaction({ messages, settings, cwd, agentName }) {
73
+ const modelConfig = getModelConfig(settings, F.MODEL_MAIN);
74
+ if (!modelConfig[F.MODEL_API_KEY] || !modelConfig[F.MODEL_NAME]) return;
75
+
76
+ const extractionMessages = [
77
+ ...messages.slice(-20), // last 20 messages for context
78
+ {
79
+ role: 'user',
80
+ content: 'What key facts, decisions, or learnings from this conversation should be remembered long-term? Reply with a concise markdown list. If nothing important, reply with exactly: NO_MEMORY',
81
+ },
82
+ ];
83
+
84
+ try {
85
+ const response = await callLLM({
86
+ baseUrl: modelConfig[F.MODEL_BASE_URL],
87
+ apiKey: modelConfig[F.MODEL_API_KEY],
88
+ model: modelConfig[F.MODEL_NAME],
89
+ messages: extractionMessages,
90
+ tools: [],
91
+ });
92
+ const { content } = extractMessage(response);
93
+ if (content && content.trim() !== 'NO_MEMORY' && content.trim().length > 10) {
94
+ writeMemory({ cwd, agentName, scope: 'agent', content: content.trim() });
95
+ }
96
+ } catch {
97
+ // Non-fatal — compaction continues without memory extraction
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Run full context compaction: summarize conversation history.
103
+ * @param {{ messages: Object[], settings: Object, taskBrief?: string }} opts
104
+ * @returns {Promise<Object[]>} New compressed messages array
105
+ */
106
+ async function compactMessages({ messages, settings, taskBrief }) {
107
+ const modelConfig = getModelConfig(settings, F.MODEL_COMPACT);
108
+ if (!modelConfig[F.MODEL_API_KEY] || !modelConfig[F.MODEL_NAME]) {
109
+ // No compact model — just trim to last 20 messages
110
+ const systemMessages = messages.filter(m => m.role === 'system');
111
+ const recent = messages.filter(m => m.role !== 'system').slice(-20);
112
+ return [...systemMessages, ...recent];
113
+ }
114
+
115
+ const compactionPrompt = buildCompactionPrompt({ messages, taskBrief });
116
+
117
+ const compactionMessages = [
118
+ { role: 'system', content: compactionPrompt },
119
+ ...messages.filter(m => m.role !== 'system'),
120
+ ];
121
+
122
+ try {
123
+ const response = await callLLM({
124
+ baseUrl: modelConfig[F.MODEL_BASE_URL],
125
+ apiKey: modelConfig[F.MODEL_API_KEY],
126
+ model: modelConfig[F.MODEL_NAME],
127
+ messages: compactionMessages,
128
+ tools: [],
129
+ });
130
+ const { content } = extractMessage(response);
131
+
132
+ // New messages: preserve system prompt(s), replace rest with summary
133
+ const systemMessages = messages.filter(m => m.role === 'system');
134
+ return [
135
+ ...systemMessages,
136
+ { role: 'user', content: '[Previous conversation compacted]' },
137
+ { role: 'assistant', content: content || '[compaction produced no summary]' },
138
+ ];
139
+ } catch {
140
+ // Compaction failed — fall back to trimming
141
+ const systemMessages = messages.filter(m => m.role === 'system');
142
+ const recent = messages.filter(m => m.role !== 'system').slice(-20);
143
+ return [...systemMessages, ...recent];
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Full context management pipeline: masking → clearing → compaction if needed.
149
+ * @param {{ messages: Object[], settings: Object, taskBrief?: string, cwd: string, agentName: string }} opts
150
+ * @returns {Promise<{ messages: Object[], compacted: boolean }>}
151
+ */
152
+ async function manageContext({ messages, settings, taskBrief, cwd, agentName }) {
153
+ const threshold = settings.compaction?.threshold || F.DEFAULT_COMPACTION_THRESHOLD;
154
+ const maskingTurns = settings.compaction?.observationMaskingTurns || F.DEFAULT_OBSERVATION_MASKING_TURNS;
155
+
156
+ // Step 1: Observation masking
157
+ applyObservationMasking(messages, maskingTurns);
158
+
159
+ // Step 2: Tool result clearing
160
+ applyToolResultClearing(messages);
161
+
162
+ // Step 3: Check if compaction needed
163
+ const usage = estimateContextUsage(messages);
164
+ if (usage < threshold) {
165
+ return { messages, compacted: false };
166
+ }
167
+
168
+ // Step 3a: Pre-compaction memory extraction
169
+ await extractMemoriesBeforeCompaction({ messages, settings, cwd, agentName });
170
+
171
+ // Step 3b: Compact
172
+ const compacted = await compactMessages({ messages, settings, taskBrief });
173
+ return { messages: compacted, compacted: true };
174
+ }
175
+
176
+ module.exports = { manageContext };
package/core/events.js ADDED
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * In-process event bus for real-time agent events.
5
+ * WebSocket connections subscribe to this bus and receive all runtime events.
6
+ */
7
+
8
+ const { EventEmitter } = require('events');
9
+
10
+ const bus = new EventEmitter();
11
+ bus.setMaxListeners(200);
12
+
13
+ module.exports = bus;