@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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const context = require('../utils/context');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Auth middleware — validates X-Veil-Secret header if a secret is configured.
|
|
7
|
+
*/
|
|
8
|
+
function authMiddleware(req, res, next) {
|
|
9
|
+
// /health is always unprotected (liveness probe for load balancers / orchestrators)
|
|
10
|
+
if (req.path === '/health') return next();
|
|
11
|
+
|
|
12
|
+
// Secret is read from settings at request time via context
|
|
13
|
+
const secret = req.app.locals.settings?.secret;
|
|
14
|
+
if (!secret) return next(); // No secret configured — open access
|
|
15
|
+
|
|
16
|
+
const provided = req.headers['x-veil-secret'];
|
|
17
|
+
if (!provided || provided !== secret) {
|
|
18
|
+
return res.status(401).json({ error: { code: 'UNAUTHORIZED', message: 'Invalid or missing X-Veil-Secret header' } });
|
|
19
|
+
}
|
|
20
|
+
next();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Standard error response formatter.
|
|
25
|
+
* @param {Response} res
|
|
26
|
+
* @param {number} status
|
|
27
|
+
* @param {string} code
|
|
28
|
+
* @param {string} message
|
|
29
|
+
*/
|
|
30
|
+
function sendError(res, status, code, message) {
|
|
31
|
+
return res.status(status).json({ error: { code, message } });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Global error handler middleware.
|
|
36
|
+
*/
|
|
37
|
+
function errorHandler(err, req, res, next) {
|
|
38
|
+
const codeMap = {
|
|
39
|
+
'AGENT_NOT_FOUND': 404,
|
|
40
|
+
'MODE_NOT_SUPPORTED': 400,
|
|
41
|
+
'TASK_NOT_FOUND': 404,
|
|
42
|
+
'SESSION_NOT_FOUND': 404,
|
|
43
|
+
'SESSION_CLOSED': 400,
|
|
44
|
+
'TASK_NOT_WAITING': 400,
|
|
45
|
+
'PERMISSION_DENIED': 403,
|
|
46
|
+
'VALIDATION_ERROR': 400,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const code = err.code || 'INTERNAL_ERROR';
|
|
50
|
+
const status = codeMap[code] || 500;
|
|
51
|
+
const message = err.message || 'An unexpected error occurred';
|
|
52
|
+
|
|
53
|
+
if (status === 500) {
|
|
54
|
+
console.error('[ERROR]', err);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return sendError(res, status, code, message);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { authMiddleware, sendError, errorHandler };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
const { listAgents, loadAgent, getEffectiveModeConfig, createAgent, updateAgent, deleteAgent } = require('../../core/agent');
|
|
8
|
+
const { listCustomToolSummaries } = require('../../core/registry');
|
|
9
|
+
const { sendError } = require('../middleware');
|
|
10
|
+
const context = require('../../utils/context');
|
|
11
|
+
const db = require('../../infrastructure/database');
|
|
12
|
+
const paths = require('../../utils/paths');
|
|
13
|
+
|
|
14
|
+
// POST /agents — create a new agent
|
|
15
|
+
router.post('/', (req, res, next) => {
|
|
16
|
+
try {
|
|
17
|
+
const cwd = context.getCwd();
|
|
18
|
+
const { name, level = 'project', config, agentMd } = req.body;
|
|
19
|
+
|
|
20
|
+
if (!name) return sendError(res, 400, 'VALIDATION_ERROR', '"name" is required');
|
|
21
|
+
if (!config || typeof config !== 'object') return sendError(res, 400, 'VALIDATION_ERROR', '"config" object is required');
|
|
22
|
+
if (level !== 'project' && level !== 'global') return sendError(res, 400, 'VALIDATION_ERROR', '"level" must be "project" or "global"');
|
|
23
|
+
|
|
24
|
+
const result = createAgent({ name, level, cwd, config, agentMd });
|
|
25
|
+
res.status(201).json({ agent: result });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err.code === 'AGENT_EXISTS') return sendError(res, 409, 'AGENT_EXISTS', err.message);
|
|
28
|
+
if (err.code === 'INVALID_NAME') return sendError(res, 400, 'INVALID_NAME', err.message);
|
|
29
|
+
if (err.code === 'INVALID_CONFIG') return sendError(res, 400, 'INVALID_CONFIG', err.message);
|
|
30
|
+
next(err);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// GET /agents
|
|
35
|
+
router.get('/', (req, res, next) => {
|
|
36
|
+
try {
|
|
37
|
+
const agents = listAgents({ cwd: context.getCwd() });
|
|
38
|
+
res.json({ agents });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
next(err);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// GET /agents/:name
|
|
45
|
+
router.get('/:name', (req, res, next) => {
|
|
46
|
+
try {
|
|
47
|
+
const agent = loadAgent({ cwd: context.getCwd(), name: req.params.name });
|
|
48
|
+
res.json({ agent: {
|
|
49
|
+
name: agent.name,
|
|
50
|
+
description: agent.description || '',
|
|
51
|
+
model: agent.model,
|
|
52
|
+
temperature: agent.temperature,
|
|
53
|
+
reasoning: agent.reasoning,
|
|
54
|
+
modes: agent.modes,
|
|
55
|
+
skillDiscovery: agent.skillDiscovery,
|
|
56
|
+
memory: agent.memory,
|
|
57
|
+
agentFolder: agent.agentFolder,
|
|
58
|
+
source: agent.source,
|
|
59
|
+
agentMd: agent.agentMd || '',
|
|
60
|
+
}});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err.message && err.message.startsWith('Agent "')) {
|
|
63
|
+
err.code = 'AGENT_NOT_FOUND';
|
|
64
|
+
}
|
|
65
|
+
next(err);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// POST /agents/:name/reload — hot-reload agent config from disk
|
|
70
|
+
router.post('/:name/reload', (req, res, next) => {
|
|
71
|
+
try {
|
|
72
|
+
const cwd = context.getCwd();
|
|
73
|
+
const agent = loadAgent({ cwd, name: req.params.name });
|
|
74
|
+
res.json({ reloaded: true, agent });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err.message && err.message.includes('not found')) {
|
|
77
|
+
return sendError(res, 404, 'AGENT_NOT_FOUND', err.message);
|
|
78
|
+
}
|
|
79
|
+
next(err);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// PUT /agents/:name — update agent config and/or AGENT.md
|
|
84
|
+
router.put('/:name', (req, res, next) => {
|
|
85
|
+
try {
|
|
86
|
+
const cwd = context.getCwd();
|
|
87
|
+
const { config, agentMd } = req.body;
|
|
88
|
+
|
|
89
|
+
if (config === undefined && agentMd === undefined) {
|
|
90
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'At least one of "config" or "agentMd" is required');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = updateAgent({ name: req.params.name, cwd, config, agentMd });
|
|
94
|
+
res.json({ agent: result });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err.code === 'AGENT_NOT_FOUND') return sendError(res, 404, 'AGENT_NOT_FOUND', err.message);
|
|
97
|
+
if (err.code === 'AGENT_READ_ONLY') return sendError(res, 403, 'AGENT_READ_ONLY', err.message);
|
|
98
|
+
if (err.code === 'INVALID_CONFIG') return sendError(res, 400, 'INVALID_CONFIG', err.message);
|
|
99
|
+
next(err);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// DELETE /agents/:name — delete an agent folder
|
|
104
|
+
router.delete('/:name', (req, res, next) => {
|
|
105
|
+
try {
|
|
106
|
+
const cwd = context.getCwd();
|
|
107
|
+
const result = deleteAgent({ name: req.params.name, cwd });
|
|
108
|
+
res.json({ deleted: true, agent: result });
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err.code === 'AGENT_NOT_FOUND') return sendError(res, 404, 'AGENT_NOT_FOUND', err.message);
|
|
111
|
+
if (err.code === 'AGENT_READ_ONLY') return sendError(res, 403, 'AGENT_READ_ONLY', err.message);
|
|
112
|
+
next(err);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// GET /agents/:name/sessions
|
|
117
|
+
router.get('/:name/sessions', (req, res, next) => {
|
|
118
|
+
try {
|
|
119
|
+
const cwd = context.getCwd();
|
|
120
|
+
const { limit, cursor, status } = req.query;
|
|
121
|
+
const sessions = db.listSessions({
|
|
122
|
+
instanceFolder: cwd,
|
|
123
|
+
agentName: req.params.name,
|
|
124
|
+
status,
|
|
125
|
+
limit: limit ? parseInt(limit, 10) : 20,
|
|
126
|
+
cursor,
|
|
127
|
+
});
|
|
128
|
+
res.json({ agentName: req.params.name, sessions });
|
|
129
|
+
} catch (err) {
|
|
130
|
+
next(err);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// GET /agents/:name/tasks
|
|
135
|
+
router.get('/:name/tasks', (req, res, next) => {
|
|
136
|
+
try {
|
|
137
|
+
const cwd = context.getCwd();
|
|
138
|
+
const { limit, cursor, status, priority } = req.query;
|
|
139
|
+
const tasks = db.listTasks({
|
|
140
|
+
instanceFolder: cwd,
|
|
141
|
+
agentName: req.params.name,
|
|
142
|
+
status,
|
|
143
|
+
priority,
|
|
144
|
+
limit: limit ? parseInt(limit, 10) : 20,
|
|
145
|
+
cursor,
|
|
146
|
+
});
|
|
147
|
+
res.json({ agentName: req.params.name, tasks });
|
|
148
|
+
} catch (err) {
|
|
149
|
+
next(err);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// GET /agents/:name/skills
|
|
154
|
+
router.get('/:name/skills', (req, res, next) => {
|
|
155
|
+
try {
|
|
156
|
+
const cwd = context.getCwd();
|
|
157
|
+
let agent;
|
|
158
|
+
try {
|
|
159
|
+
agent = loadAgent({ cwd, name: req.params.name });
|
|
160
|
+
} catch {
|
|
161
|
+
return sendError(res, 404, 'AGENT_NOT_FOUND', `Agent "${req.params.name}" not found`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const modeConfig = agent.modes?.task || agent.modes?.subagent || {};
|
|
165
|
+
const skillNames = modeConfig.skills || [];
|
|
166
|
+
const projectSkillsDir = paths.getProjectSkillsDir(cwd);
|
|
167
|
+
|
|
168
|
+
const skills = skillNames.map(skillName => {
|
|
169
|
+
const projectSkillPath = path.join(projectSkillsDir, skillName + '.md');
|
|
170
|
+
const agentSkillPath = agent.agentFolder
|
|
171
|
+
? path.join(agent.agentFolder, 'skills', skillName + '.md')
|
|
172
|
+
: null;
|
|
173
|
+
const agentLoaded = agentSkillPath && fs.existsSync(agentSkillPath);
|
|
174
|
+
const projectLoaded = fs.existsSync(projectSkillPath);
|
|
175
|
+
const loaded = agentLoaded || projectLoaded;
|
|
176
|
+
return {
|
|
177
|
+
name: skillName,
|
|
178
|
+
loaded,
|
|
179
|
+
source: agentLoaded ? 'agent' : projectLoaded ? 'project' : null,
|
|
180
|
+
path: agentLoaded ? agentSkillPath : projectLoaded ? projectSkillPath : null,
|
|
181
|
+
error: loaded ? null : 'File not found',
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const customTools = listCustomToolSummaries({ agentConfig: agent, cwd });
|
|
186
|
+
|
|
187
|
+
res.json({ agentName: req.params.name, skills, customTools });
|
|
188
|
+
} catch (err) {
|
|
189
|
+
next(err);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
module.exports = router;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const router = express.Router({ mergeParams: true });
|
|
5
|
+
const { runChat } = require('../../core/router');
|
|
6
|
+
const { loadAgent } = require('../../core/agent');
|
|
7
|
+
const { sendError } = require('../middleware');
|
|
8
|
+
const context = require('../../utils/context');
|
|
9
|
+
const eventBus = require('../../core/events');
|
|
10
|
+
|
|
11
|
+
// POST /agents/:name/chat
|
|
12
|
+
router.post('/', async (req, res, next) => {
|
|
13
|
+
try {
|
|
14
|
+
const { name } = req.params;
|
|
15
|
+
const { message, sessionId, sse = false } = req.body;
|
|
16
|
+
const cwd = context.getCwd();
|
|
17
|
+
const settings = req.app.locals.settings;
|
|
18
|
+
|
|
19
|
+
if (!message || typeof message !== 'string') {
|
|
20
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'message is required and must be a string');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Verify agent exists and supports chat mode
|
|
24
|
+
let agent;
|
|
25
|
+
try {
|
|
26
|
+
agent = loadAgent({ cwd, name });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return sendError(res, 404, 'AGENT_NOT_FOUND', err.message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!agent.modes?.chat?.enabled) {
|
|
32
|
+
return sendError(res, 400, 'MODE_NOT_SUPPORTED', `Agent "${name}" does not support chat mode`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── SSE streaming mode ────────────────────────────────────────────────────
|
|
36
|
+
if (sse) {
|
|
37
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
38
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
39
|
+
res.setHeader('Connection', 'keep-alive');
|
|
40
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
41
|
+
res.flushHeaders();
|
|
42
|
+
|
|
43
|
+
let currentSessionId = sessionId || null;
|
|
44
|
+
|
|
45
|
+
function sendEvent(event, data) {
|
|
46
|
+
try { res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); } catch {}
|
|
47
|
+
eventBus.emit('event', { type: 'session.stream', sessionId: currentSessionId, agentName: name, eventType: event, data });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const result = await runChat({
|
|
52
|
+
agentName: name, message, sessionId, cwd, settings,
|
|
53
|
+
onSessionStart: (sid) => { currentSessionId = sid; },
|
|
54
|
+
onStreamChunk: (text) => sendEvent('inference.chunk', { content: text }),
|
|
55
|
+
onInferenceToolStart: (toolName) => sendEvent('inference.tool', { name: toolName }),
|
|
56
|
+
onEvent: (event) => {
|
|
57
|
+
if (event.type === 'message')
|
|
58
|
+
sendEvent('message', {
|
|
59
|
+
...event.message,
|
|
60
|
+
...(event.finishReason !== undefined && { finishReason: event.finishReason }),
|
|
61
|
+
...(event.iteration !== undefined && { iteration: event.iteration }),
|
|
62
|
+
...(event.tokenUsage !== undefined && { tokenUsage: event.tokenUsage }),
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
sendEvent('done', {
|
|
67
|
+
session: result.session,
|
|
68
|
+
agentName: result.agentName, model: result.model,
|
|
69
|
+
iterations: result.iterations, durationMs: result.durationMs,
|
|
70
|
+
tokenUsage: result.tokenUsage, toolCalls: result.toolCalls || [],
|
|
71
|
+
});
|
|
72
|
+
} catch (err) {
|
|
73
|
+
sendEvent('error', { error: err.message, code: err.code || 'INTERNAL_ERROR' });
|
|
74
|
+
}
|
|
75
|
+
res.end();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Standard JSON mode ────────────────────────────────────────────────────
|
|
80
|
+
const result = await runChat({ agentName: name, message, sessionId, cwd, settings });
|
|
81
|
+
|
|
82
|
+
res.json({
|
|
83
|
+
sessionId: result.sessionId,
|
|
84
|
+
message: { role: 'assistant', content: result.content },
|
|
85
|
+
tokenUsage: result.tokenUsage,
|
|
86
|
+
toolCalls: result.toolCalls || [],
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
next(err);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
module.exports = router;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const { callLLM, extractMessage, extractUsage } = require('../../llm/client');
|
|
6
|
+
const { getModelConfig } = require('../../utils/settings');
|
|
7
|
+
const { calculateCost } = require('../../utils/models');
|
|
8
|
+
const { sendError } = require('../middleware');
|
|
9
|
+
const F = require('../../settings/fields');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* POST /completions
|
|
13
|
+
*
|
|
14
|
+
* Standalone chat completion endpoint — no agent, no session, no tool loop.
|
|
15
|
+
* Useful for independent LLM calls from apps built on Veil.
|
|
16
|
+
*
|
|
17
|
+
* Body:
|
|
18
|
+
* model? {string} Override the default model from settings
|
|
19
|
+
* messages {array} OpenAI-compatible messages array (required)
|
|
20
|
+
* temperature? {number}
|
|
21
|
+
* max_tokens? {number}
|
|
22
|
+
* tools? {array} OpenAI-compatible tool definitions
|
|
23
|
+
* reasoning? {string} Reasoning effort (e.g. "high")
|
|
24
|
+
* thinking? {object} Extended thinking config (e.g. { type: "enabled", budget_tokens: 10000 })
|
|
25
|
+
* sse? {boolean} Stream response via Server-Sent Events
|
|
26
|
+
*/
|
|
27
|
+
router.post('/', async (req, res, next) => {
|
|
28
|
+
try {
|
|
29
|
+
const settings = req.app.locals.settings;
|
|
30
|
+
const {
|
|
31
|
+
model,
|
|
32
|
+
messages,
|
|
33
|
+
temperature,
|
|
34
|
+
max_tokens,
|
|
35
|
+
tools,
|
|
36
|
+
reasoning,
|
|
37
|
+
thinking,
|
|
38
|
+
modalities,
|
|
39
|
+
audio,
|
|
40
|
+
sse = false,
|
|
41
|
+
} = req.body;
|
|
42
|
+
|
|
43
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
44
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'messages is required and must be a non-empty array');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const modelConfig = getModelConfig(settings, F.MODEL_MAIN);
|
|
48
|
+
const resolvedModel = model || modelConfig[F.MODEL_NAME];
|
|
49
|
+
|
|
50
|
+
if (!resolvedModel) {
|
|
51
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'model is required (or configure a default model in settings)');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const llmParams = {
|
|
55
|
+
baseUrl: modelConfig[F.MODEL_BASE_URL],
|
|
56
|
+
apiKey: modelConfig[F.MODEL_API_KEY],
|
|
57
|
+
model: resolvedModel,
|
|
58
|
+
messages,
|
|
59
|
+
tools: Array.isArray(tools) ? tools : [],
|
|
60
|
+
temperature: temperature !== undefined ? temperature : undefined,
|
|
61
|
+
maxTokens: max_tokens !== undefined ? max_tokens : undefined,
|
|
62
|
+
reasoning: reasoning !== undefined ? reasoning : undefined,
|
|
63
|
+
thinking: thinking !== undefined ? thinking : undefined,
|
|
64
|
+
modalities: modalities !== undefined ? modalities : undefined,
|
|
65
|
+
audio: audio !== undefined ? audio : undefined,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ── SSE streaming mode ──────────────────────────────────────────────────
|
|
69
|
+
if (sse) {
|
|
70
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
71
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
72
|
+
res.setHeader('Connection', 'keep-alive');
|
|
73
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
74
|
+
res.flushHeaders();
|
|
75
|
+
|
|
76
|
+
function sendEvent(event, data) {
|
|
77
|
+
try { res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); } catch {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const response = await callLLM({
|
|
82
|
+
...llmParams,
|
|
83
|
+
onChunk: (text) => sendEvent('chunk', { content: text }),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const { content, audio: audioOut, toolCalls, finishReason } = extractMessage(response);
|
|
87
|
+
const usage = extractUsage(response);
|
|
88
|
+
const cost = usage.cost || calculateCost(resolvedModel, { input: usage.input, output: usage.output, cache: usage.cache });
|
|
89
|
+
|
|
90
|
+
sendEvent('done', {
|
|
91
|
+
message: { role: 'assistant', content, audio: audioOut, tool_calls: toolCalls || [] },
|
|
92
|
+
usage: { input: usage.input, output: usage.output, cache: usage.cache },
|
|
93
|
+
cost,
|
|
94
|
+
finish_reason: finishReason,
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
sendEvent('error', { error: err.message, code: err.code || 'INTERNAL_ERROR' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
res.end();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Standard JSON mode ──────────────────────────────────────────────────
|
|
105
|
+
const response = await callLLM(llmParams);
|
|
106
|
+
|
|
107
|
+
const { content, audio: audioOut, toolCalls, finishReason } = extractMessage(response);
|
|
108
|
+
const usage = extractUsage(response);
|
|
109
|
+
const cost = usage.cost || calculateCost(resolvedModel, { input: usage.input, output: usage.output, cache: usage.cache });
|
|
110
|
+
|
|
111
|
+
res.json({
|
|
112
|
+
message: { role: 'assistant', content, audio: audioOut, tool_calls: toolCalls || [] },
|
|
113
|
+
usage: { input: usage.input, output: usage.output, cache: usage.cache },
|
|
114
|
+
cost,
|
|
115
|
+
finish_reason: finishReason,
|
|
116
|
+
});
|
|
117
|
+
} catch (err) {
|
|
118
|
+
next(err);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
module.exports = router;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const { loadAgent } = require('../../core/agent');
|
|
6
|
+
const { sendError } = require('../middleware');
|
|
7
|
+
const context = require('../../utils/context');
|
|
8
|
+
|
|
9
|
+
// GET /daemons
|
|
10
|
+
router.get('/', (req, res, next) => {
|
|
11
|
+
try {
|
|
12
|
+
const scheduler = req.app.locals.scheduler;
|
|
13
|
+
if (!scheduler) return res.json({ daemons: [] });
|
|
14
|
+
res.json({ daemons: scheduler.listDaemons() });
|
|
15
|
+
} catch (err) {
|
|
16
|
+
next(err);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// POST /agents/:name/daemon/start
|
|
21
|
+
router.post('/:name/daemon/start', (req, res, next) => {
|
|
22
|
+
try {
|
|
23
|
+
const { name } = req.params;
|
|
24
|
+
const cwd = context.getCwd();
|
|
25
|
+
let agent;
|
|
26
|
+
try {
|
|
27
|
+
agent = loadAgent({ cwd, name });
|
|
28
|
+
} catch {
|
|
29
|
+
return sendError(res, 404, 'AGENT_NOT_FOUND', `Agent "${name}" not found`);
|
|
30
|
+
}
|
|
31
|
+
if (!agent.modes?.daemon?.enabled) {
|
|
32
|
+
return sendError(res, 400, 'MODE_NOT_SUPPORTED', `Agent "${name}" does not support daemon mode`);
|
|
33
|
+
}
|
|
34
|
+
const scheduler = req.app.locals.scheduler;
|
|
35
|
+
scheduler.startDaemon({ agentName: name, agent, cwd, settings: req.app.locals.settings });
|
|
36
|
+
res.json({ agentName: name, status: 'started' });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
next(err);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// POST /agents/:name/daemon/stop
|
|
43
|
+
router.post('/:name/daemon/stop', (req, res, next) => {
|
|
44
|
+
try {
|
|
45
|
+
const { name } = req.params;
|
|
46
|
+
const scheduler = req.app.locals.scheduler;
|
|
47
|
+
scheduler.stopDaemon(name);
|
|
48
|
+
res.json({ agentName: name, status: 'stopped' });
|
|
49
|
+
} catch (err) {
|
|
50
|
+
next(err);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// POST /agents/:name/daemon/trigger
|
|
55
|
+
router.post('/:name/daemon/trigger', async (req, res, next) => {
|
|
56
|
+
try {
|
|
57
|
+
const { name } = req.params;
|
|
58
|
+
const cwd = context.getCwd();
|
|
59
|
+
const settings = req.app.locals.settings;
|
|
60
|
+
let agent;
|
|
61
|
+
try {
|
|
62
|
+
agent = loadAgent({ cwd, name });
|
|
63
|
+
} catch {
|
|
64
|
+
return sendError(res, 404, 'AGENT_NOT_FOUND', `Agent "${name}" not found`);
|
|
65
|
+
}
|
|
66
|
+
if (!agent.modes?.daemon?.enabled) {
|
|
67
|
+
return sendError(res, 400, 'MODE_NOT_SUPPORTED', `Agent "${name}" does not support daemon mode`);
|
|
68
|
+
}
|
|
69
|
+
const { runDaemonTick } = require('../../core/router');
|
|
70
|
+
// Fire async
|
|
71
|
+
setImmediate(() => runDaemonTick({ agentName: name, cwd, settings }).catch(err => {
|
|
72
|
+
console.error(`Daemon tick error for ${name}:`, err.message);
|
|
73
|
+
}));
|
|
74
|
+
res.json({ agentName: name, status: 'triggered' });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
next(err);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
module.exports = router;
|