agentopia 1.0.0
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/.claude/settings.local.json +28 -0
- package/dist/app.d.ts +10 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +121 -0
- package/dist/app.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -0
- package/dist/db/database.d.ts +5 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/database.js +39 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/schema.d.ts +3 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +621 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +9 -0
- package/dist/logger.js.map +1 -0
- package/dist/middleware/auth.d.ts +13 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +733 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/routes/agents.d.ts +3 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +1058 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/issues.d.ts +4 -0
- package/dist/routes/issues.d.ts.map +1 -0
- package/dist/routes/issues.js +946 -0
- package/dist/routes/issues.js.map +1 -0
- package/dist/routes/knowledge.d.ts +3 -0
- package/dist/routes/knowledge.d.ts.map +1 -0
- package/dist/routes/knowledge.js +117 -0
- package/dist/routes/knowledge.js.map +1 -0
- package/dist/routes/memories.d.ts +3 -0
- package/dist/routes/memories.d.ts.map +1 -0
- package/dist/routes/memories.js +115 -0
- package/dist/routes/memories.js.map +1 -0
- package/dist/routes/messages.d.ts +3 -0
- package/dist/routes/messages.d.ts.map +1 -0
- package/dist/routes/messages.js +130 -0
- package/dist/routes/messages.js.map +1 -0
- package/dist/routes/projects.d.ts +3 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +754 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/templates.d.ts +3 -0
- package/dist/routes/templates.d.ts.map +1 -0
- package/dist/routes/templates.js +117 -0
- package/dist/routes/templates.js.map +1 -0
- package/dist/routes/ui.d.ts +3 -0
- package/dist/routes/ui.d.ts.map +1 -0
- package/dist/routes/ui.js +38 -0
- package/dist/routes/ui.js.map +1 -0
- package/dist/services/agent-hierarchy.d.ts +14 -0
- package/dist/services/agent-hierarchy.d.ts.map +1 -0
- package/dist/services/agent-hierarchy.js +58 -0
- package/dist/services/agent-hierarchy.js.map +1 -0
- package/dist/services/agent-issue-batch.d.ts +17 -0
- package/dist/services/agent-issue-batch.d.ts.map +1 -0
- package/dist/services/agent-issue-batch.js +57 -0
- package/dist/services/agent-issue-batch.js.map +1 -0
- package/dist/services/controller.d.ts +4 -0
- package/dist/services/controller.d.ts.map +1 -0
- package/dist/services/controller.js +237 -0
- package/dist/services/controller.js.map +1 -0
- package/dist/services/langgraph-runner.d.ts +33 -0
- package/dist/services/langgraph-runner.d.ts.map +1 -0
- package/dist/services/langgraph-runner.js +478 -0
- package/dist/services/langgraph-runner.js.map +1 -0
- package/dist/services/orchestrator.d.ts +9 -0
- package/dist/services/orchestrator.d.ts.map +1 -0
- package/dist/services/orchestrator.js +116 -0
- package/dist/services/orchestrator.js.map +1 -0
- package/dist/services/pre-controller.d.ts +7 -0
- package/dist/services/pre-controller.d.ts.map +1 -0
- package/dist/services/pre-controller.js +101 -0
- package/dist/services/pre-controller.js.map +1 -0
- package/dist/services/process-manager.d.ts +67 -0
- package/dist/services/process-manager.d.ts.map +1 -0
- package/dist/services/process-manager.js +938 -0
- package/dist/services/process-manager.js.map +1 -0
- package/dist/services/project-permissions.d.ts +84 -0
- package/dist/services/project-permissions.d.ts.map +1 -0
- package/dist/services/project-permissions.js +129 -0
- package/dist/services/project-permissions.js.map +1 -0
- package/dist/services/scheduler.d.ts +6 -0
- package/dist/services/scheduler.d.ts.map +1 -0
- package/dist/services/scheduler.js +300 -0
- package/dist/services/scheduler.js.map +1 -0
- package/dist/services/system-prompt.d.ts +3 -0
- package/dist/services/system-prompt.d.ts.map +1 -0
- package/dist/services/system-prompt.js +285 -0
- package/dist/services/system-prompt.js.map +1 -0
- package/dist/services/terminal.d.ts +18 -0
- package/dist/services/terminal.d.ts.map +1 -0
- package/dist/services/terminal.js +222 -0
- package/dist/services/terminal.js.map +1 -0
- package/dist/services/websocket.d.ts +15 -0
- package/dist/services/websocket.d.ts.map +1 -0
- package/dist/services/websocket.js +204 -0
- package/dist/services/websocket.js.map +1 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/env.ini +18 -0
- package/package.json +38 -0
- package/project_id +0 -0
- package/public/admin-users.html +188 -0
- package/public/agent.html +199 -0
- package/public/css/issues.css +275 -0
- package/public/css/style.css +1299 -0
- package/public/index.html +166 -0
- package/public/issue.html +76 -0
- package/public/js/agent.js +19 -0
- package/public/js/common.js +735 -0
- package/public/js/dashboard.js +772 -0
- package/public/js/files-panel.js +703 -0
- package/public/js/interactive-terminal.js +201 -0
- package/public/js/issue-renderer.js +559 -0
- package/public/js/issue.js +57 -0
- package/public/js/project.js +2425 -0
- package/public/js/terminal.js +564 -0
- package/public/project.html +430 -0
- package/public/terminal.html +67 -0
- package/public/vendor/marked.js +74 -0
- package/public/vendor/xterm-addon-fit.js +2 -0
- package/public/vendor/xterm.css +209 -0
- package/public/vendor/xterm.js +2 -0
- package/send_message_and_update_issue.js +65 -0
- package/tsconfig.json +19 -0
- package/update_round2_and_create_round3.js +284 -0
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerAgentRoutes = registerAgentRoutes;
|
|
37
|
+
const uuid_1 = require("uuid");
|
|
38
|
+
const child_process_1 = require("child_process");
|
|
39
|
+
const fs = __importStar(require("fs/promises"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const util_1 = require("util");
|
|
43
|
+
const database_1 = require("../db/database");
|
|
44
|
+
const process_manager_1 = require("../services/process-manager");
|
|
45
|
+
const agent_issue_batch_1 = require("../services/agent-issue-batch");
|
|
46
|
+
const system_prompt_1 = require("../services/system-prompt");
|
|
47
|
+
const config_1 = require("../config");
|
|
48
|
+
const websocket_1 = require("../services/websocket");
|
|
49
|
+
const project_permissions_1 = require("../services/project-permissions");
|
|
50
|
+
const agent_hierarchy_1 = require("../services/agent-hierarchy");
|
|
51
|
+
const TOOL_CALL_REPORT_CHAR_LIMIT = 4000;
|
|
52
|
+
const MAX_AGENT_FILE_SIZE = 1024 * 1024;
|
|
53
|
+
const utf8Decoder = new util_1.TextDecoder('utf-8', { fatal: true });
|
|
54
|
+
function expandWorkingDirectory(dir) {
|
|
55
|
+
if (dir.startsWith('~/')) {
|
|
56
|
+
return path.join(os.homedir(), dir.slice(2));
|
|
57
|
+
}
|
|
58
|
+
return dir;
|
|
59
|
+
}
|
|
60
|
+
function resolveAgentFilesystemPath(agent, requestedPath) {
|
|
61
|
+
if (!agent.working_directory) {
|
|
62
|
+
throw new Error('WORKDIR_REQUIRED');
|
|
63
|
+
}
|
|
64
|
+
const rootDir = path.resolve(expandWorkingDirectory(agent.working_directory));
|
|
65
|
+
const candidate = path.resolve(rootDir, requestedPath || '.');
|
|
66
|
+
const rootPrefix = rootDir.endsWith(path.sep) ? rootDir : `${rootDir}${path.sep}`;
|
|
67
|
+
if (candidate !== rootDir && !candidate.startsWith(rootPrefix)) {
|
|
68
|
+
throw new Error('PATH_OUTSIDE_WORKDIR');
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
rootDir,
|
|
72
|
+
targetPath: candidate,
|
|
73
|
+
relativePath: candidate === rootDir ? '' : path.relative(rootDir, candidate).split(path.sep).join('/'),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function sendAgentFilePathError(reply, error) {
|
|
77
|
+
if (error instanceof Error && error.message === 'WORKDIR_REQUIRED') {
|
|
78
|
+
return reply.code(400).send({ error: 'Agent does not have a working_directory configured' });
|
|
79
|
+
}
|
|
80
|
+
if (error instanceof Error && error.message === 'PATH_OUTSIDE_WORKDIR') {
|
|
81
|
+
return reply.code(400).send({ error: 'Path is outside the working_directory' });
|
|
82
|
+
}
|
|
83
|
+
return reply.code(500).send({ error: 'Failed to resolve path' });
|
|
84
|
+
}
|
|
85
|
+
function sendAgentFileSystemError(reply, error) {
|
|
86
|
+
if (!(error instanceof Error)) {
|
|
87
|
+
return reply.code(500).send({ error: 'File operation failed' });
|
|
88
|
+
}
|
|
89
|
+
const fsError = error;
|
|
90
|
+
if (fsError.code === 'ENOENT') {
|
|
91
|
+
return reply.code(404).send({ error: 'File not found' });
|
|
92
|
+
}
|
|
93
|
+
if (fsError.code === 'EACCES' || fsError.code === 'EPERM') {
|
|
94
|
+
return reply.code(403).send({ error: 'File access denied' });
|
|
95
|
+
}
|
|
96
|
+
if (fsError.code === 'EISDIR') {
|
|
97
|
+
return reply.code(400).send({ error: 'Target is not a file' });
|
|
98
|
+
}
|
|
99
|
+
return reply.code(500).send({ error: 'File operation failed' });
|
|
100
|
+
}
|
|
101
|
+
function decodeTextFile(buffer) {
|
|
102
|
+
if (!buffer.length) {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
if (buffer.includes(0)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const sampleSize = Math.min(buffer.length, 1024);
|
|
109
|
+
let controlCharCount = 0;
|
|
110
|
+
for (let i = 0; i < sampleSize; i += 1) {
|
|
111
|
+
const byte = buffer[i];
|
|
112
|
+
if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {
|
|
113
|
+
controlCharCount += 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (controlCharCount / sampleSize > 0.2) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return utf8Decoder.decode(buffer);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function registerAgentRoutes(fastify) {
|
|
127
|
+
// List agents for a project
|
|
128
|
+
fastify.get('/api/projects/:pid/agents', async (request, reply) => {
|
|
129
|
+
const db = (0, database_1.getDatabase)();
|
|
130
|
+
const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, request.params.pid);
|
|
131
|
+
if (!access)
|
|
132
|
+
return;
|
|
133
|
+
return db.prepare('SELECT * FROM agents WHERE project_id = ? ORDER BY is_controller DESC, created_at').all(request.params.pid);
|
|
134
|
+
});
|
|
135
|
+
// Create agent
|
|
136
|
+
fastify.post('/api/projects/:pid/agents', async (request, reply) => {
|
|
137
|
+
const { name, role, is_controller, session_id, working_directory, command_template, parent_agent_id } = request.body;
|
|
138
|
+
if (!name)
|
|
139
|
+
return reply.code(400).send({ error: 'name is required' });
|
|
140
|
+
const db = (0, database_1.getDatabase)();
|
|
141
|
+
const access = (0, project_permissions_1.ensureProjectAccess)(db, request, reply, request.params.pid, true);
|
|
142
|
+
if (!access)
|
|
143
|
+
return;
|
|
144
|
+
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(request.params.pid);
|
|
145
|
+
if (!project)
|
|
146
|
+
return reply.code(404).send({ error: 'Project not found' });
|
|
147
|
+
const parentValidation = (0, agent_hierarchy_1.validateParentAgentAssignment)(db, request.params.pid, parent_agent_id);
|
|
148
|
+
if (parentValidation.error) {
|
|
149
|
+
return reply.code(400).send({ error: parentValidation.error });
|
|
150
|
+
}
|
|
151
|
+
const id = (0, uuid_1.v4)();
|
|
152
|
+
// For controller agents, default to Sonnet model if no --model flag specified
|
|
153
|
+
let finalCommandTemplate = command_template || null;
|
|
154
|
+
if (is_controller && finalCommandTemplate && !finalCommandTemplate.includes('--model')) {
|
|
155
|
+
finalCommandTemplate = finalCommandTemplate + ' --model claude-sonnet-4-6';
|
|
156
|
+
}
|
|
157
|
+
else if (is_controller && !finalCommandTemplate) {
|
|
158
|
+
finalCommandTemplate = 'cld --model claude-sonnet-4-6';
|
|
159
|
+
}
|
|
160
|
+
db.prepare(`
|
|
161
|
+
INSERT INTO agents (id, project_id, name, role, is_controller, parent_agent_id, session_id, working_directory, command_template, status)
|
|
162
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'idle')
|
|
163
|
+
`).run(id, request.params.pid, name, role || '', is_controller ? 1 : 0, parentValidation.parentAgent?.id || null, session_id || null, working_directory || null, finalCommandTemplate);
|
|
164
|
+
return reply.code(201).send(db.prepare('SELECT * FROM agents WHERE id = ?').get(id));
|
|
165
|
+
});
|
|
166
|
+
// Get agent
|
|
167
|
+
fastify.get('/api/agents/:id', async (request, reply) => {
|
|
168
|
+
const db = (0, database_1.getDatabase)();
|
|
169
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
170
|
+
if (!access)
|
|
171
|
+
return;
|
|
172
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
173
|
+
if (!agent)
|
|
174
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
175
|
+
return agent;
|
|
176
|
+
});
|
|
177
|
+
// Update agent
|
|
178
|
+
fastify.put('/api/agents/:id', async (request, reply) => {
|
|
179
|
+
const db = (0, database_1.getDatabase)();
|
|
180
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id, true);
|
|
181
|
+
if (!access)
|
|
182
|
+
return;
|
|
183
|
+
const existing = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
184
|
+
if (!existing)
|
|
185
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
186
|
+
const { name, role, session_id, working_directory, custom_instructions, session_max_runs, session_max_tokens, session_resume_timeout, command_template, parent_agent_id, paused, } = request.body;
|
|
187
|
+
let validatedParentId;
|
|
188
|
+
if (parent_agent_id !== undefined) {
|
|
189
|
+
const parentValidation = (0, agent_hierarchy_1.validateParentAgentAssignment)(db, existing.project_id, parent_agent_id, existing.id);
|
|
190
|
+
if (parentValidation.error) {
|
|
191
|
+
return reply.code(400).send({ error: parentValidation.error });
|
|
192
|
+
}
|
|
193
|
+
validatedParentId = parentValidation.parentAgent?.id || null;
|
|
194
|
+
}
|
|
195
|
+
// Build update fields dynamically — command_template and custom_instructions
|
|
196
|
+
// need special handling: COALESCE(NULL, col) preserves the old value, but we
|
|
197
|
+
// want to allow explicitly setting them to NULL when the field is in the request
|
|
198
|
+
const fields = [
|
|
199
|
+
'name = COALESCE(?, name)',
|
|
200
|
+
'role = COALESCE(?, role)',
|
|
201
|
+
'session_id = COALESCE(?, session_id)',
|
|
202
|
+
'working_directory = COALESCE(?, working_directory)',
|
|
203
|
+
'session_max_runs = COALESCE(?, session_max_runs)',
|
|
204
|
+
'session_max_tokens = COALESCE(?, session_max_tokens)',
|
|
205
|
+
'session_resume_timeout = COALESCE(?, session_resume_timeout)',
|
|
206
|
+
];
|
|
207
|
+
const params = [
|
|
208
|
+
name ?? null, role ?? null, session_id ?? null, working_directory ?? null,
|
|
209
|
+
session_max_runs !== undefined ? Math.max(1, Number.isNaN(parseInt(session_max_runs)) ? 10 : parseInt(session_max_runs)) : null,
|
|
210
|
+
session_max_tokens !== undefined ? Math.max(0, Number.isNaN(parseInt(session_max_tokens)) ? 0 : parseInt(session_max_tokens)) : null,
|
|
211
|
+
session_resume_timeout !== undefined ? Math.max(0, Number.isNaN(parseInt(session_resume_timeout)) ? 300 : parseInt(session_resume_timeout)) : null,
|
|
212
|
+
];
|
|
213
|
+
if (command_template !== undefined) {
|
|
214
|
+
fields.push('command_template = ?');
|
|
215
|
+
params.push(command_template || null);
|
|
216
|
+
}
|
|
217
|
+
if (custom_instructions !== undefined) {
|
|
218
|
+
fields.push('custom_instructions = ?');
|
|
219
|
+
params.push(custom_instructions || null);
|
|
220
|
+
}
|
|
221
|
+
if (validatedParentId !== undefined) {
|
|
222
|
+
fields.push('parent_agent_id = ?');
|
|
223
|
+
params.push(validatedParentId);
|
|
224
|
+
}
|
|
225
|
+
if (paused !== undefined) {
|
|
226
|
+
fields.push('paused = ?');
|
|
227
|
+
params.push(paused ? 1 : 0);
|
|
228
|
+
}
|
|
229
|
+
params.push(request.params.id);
|
|
230
|
+
db.prepare(`UPDATE agents SET ${fields.join(', ')} WHERE id = ?`).run(...params);
|
|
231
|
+
return db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
232
|
+
});
|
|
233
|
+
// Delete agent
|
|
234
|
+
fastify.delete('/api/agents/:id', async (request, reply) => {
|
|
235
|
+
const db = (0, database_1.getDatabase)();
|
|
236
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id, true);
|
|
237
|
+
if (!access)
|
|
238
|
+
return;
|
|
239
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
240
|
+
if (!agent)
|
|
241
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
242
|
+
if ((0, process_manager_1.isAgentRunning)(agent.id)) {
|
|
243
|
+
(0, process_manager_1.stopAgentProcess)(agent.id);
|
|
244
|
+
}
|
|
245
|
+
// Clear assigned_to on issues referencing this agent
|
|
246
|
+
db.prepare('UPDATE issues SET assigned_to = NULL WHERE assigned_to = ?').run(request.params.id);
|
|
247
|
+
db.prepare('DELETE FROM agents WHERE id = ?').run(request.params.id);
|
|
248
|
+
return { success: true };
|
|
249
|
+
});
|
|
250
|
+
// Start agent
|
|
251
|
+
fastify.post('/api/agents/:id/start', async (request, reply) => {
|
|
252
|
+
const db = (0, database_1.getDatabase)();
|
|
253
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id, true);
|
|
254
|
+
if (!access)
|
|
255
|
+
return;
|
|
256
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
257
|
+
if (!agent)
|
|
258
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
259
|
+
if (agent.paused) {
|
|
260
|
+
return reply.code(409).send({ error: 'Agent is paused. Unpause it first.' });
|
|
261
|
+
}
|
|
262
|
+
if (agent.status === 'running' || (0, process_manager_1.isAgentRunning)(agent.id)) {
|
|
263
|
+
return reply.code(409).send({ error: 'Agent is already running' });
|
|
264
|
+
}
|
|
265
|
+
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(agent.project_id);
|
|
266
|
+
if (!project)
|
|
267
|
+
return reply.code(404).send({ error: 'Project not found for this agent' });
|
|
268
|
+
// If force_new_session is requested, clear session_id to start fresh
|
|
269
|
+
if (request.body?.force_new_session) {
|
|
270
|
+
db.prepare('UPDATE agents SET session_id = NULL WHERE id = ?').run(agent.id);
|
|
271
|
+
agent.session_id = null;
|
|
272
|
+
}
|
|
273
|
+
// Build prompt: user-provided > auto-generated from role + task
|
|
274
|
+
let prompt = request.body?.prompt?.trim() || '';
|
|
275
|
+
if (!prompt) {
|
|
276
|
+
const parts = [];
|
|
277
|
+
if (agent.role)
|
|
278
|
+
parts.push(`Role: ${agent.role}`);
|
|
279
|
+
if (project.task_description)
|
|
280
|
+
parts.push(`Task: ${project.task_description}`);
|
|
281
|
+
// Include open issues assigned to this agent
|
|
282
|
+
const issues = db.prepare("SELECT * FROM issues WHERE project_id = ? AND assigned_to = ? AND status IN ('open', 'in_progress') ORDER BY priority DESC, created_at").all(project.id, agent.id);
|
|
283
|
+
if (issues.length > 0) {
|
|
284
|
+
const issueBatch = (0, agent_issue_batch_1.getAgentIssueBatch)(issues);
|
|
285
|
+
parts.push((0, agent_issue_batch_1.buildAssignedIssuesPrompt)(issueBatch));
|
|
286
|
+
(0, agent_issue_batch_1.markCurrentBatchInProgress)(db, issueBatch);
|
|
287
|
+
}
|
|
288
|
+
prompt = parts.join('\n\n');
|
|
289
|
+
}
|
|
290
|
+
if (!prompt)
|
|
291
|
+
return reply.code(400).send({ error: 'No prompt could be generated. Set agent role or project task_description.' });
|
|
292
|
+
const commandTemplate = agent.command_template || project.command_template || config_1.config.defaultCommandTemplate;
|
|
293
|
+
// Inject system prompt by default; skip for raw shell commands (bash -c / sh -c)
|
|
294
|
+
const isRawShell = /^\s*(bash|sh|zsh)\s+-c\b/.test(commandTemplate);
|
|
295
|
+
const systemPrompt = isRawShell ? undefined : (0, system_prompt_1.buildSystemPrompt)(agent, project);
|
|
296
|
+
const result = (0, process_manager_1.startAgentProcess)(agent, prompt, commandTemplate, systemPrompt);
|
|
297
|
+
return { success: true, runId: result.runId, pid: result.pid };
|
|
298
|
+
});
|
|
299
|
+
// Retry agent (re-run with the same last_prompt)
|
|
300
|
+
fastify.post('/api/agents/:id/retry', async (request, reply) => {
|
|
301
|
+
const db = (0, database_1.getDatabase)();
|
|
302
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id, true);
|
|
303
|
+
if (!access)
|
|
304
|
+
return;
|
|
305
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
306
|
+
if (!agent)
|
|
307
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
308
|
+
if (agent.paused) {
|
|
309
|
+
return reply.code(409).send({ error: 'Agent is paused. Unpause it first.' });
|
|
310
|
+
}
|
|
311
|
+
if (agent.status === 'running' || (0, process_manager_1.isAgentRunning)(agent.id)) {
|
|
312
|
+
return reply.code(409).send({ error: 'Agent is already running' });
|
|
313
|
+
}
|
|
314
|
+
if (!agent.last_prompt) {
|
|
315
|
+
return reply.code(400).send({ error: 'No previous prompt to retry. Use start instead.' });
|
|
316
|
+
}
|
|
317
|
+
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(agent.project_id);
|
|
318
|
+
if (!project)
|
|
319
|
+
return reply.code(404).send({ error: 'Project not found for this agent' });
|
|
320
|
+
// Reset session_id to ensure fresh session on retry
|
|
321
|
+
db.prepare('UPDATE agents SET session_id = NULL WHERE id = ?').run(agent.id);
|
|
322
|
+
const freshAgent = { ...agent, session_id: null };
|
|
323
|
+
const commandTemplate = agent.command_template || project.command_template || config_1.config.defaultCommandTemplate;
|
|
324
|
+
const result = (0, process_manager_1.startAgentProcess)(freshAgent, agent.last_prompt, commandTemplate);
|
|
325
|
+
return { success: true, runId: result.runId, pid: result.pid };
|
|
326
|
+
});
|
|
327
|
+
// Stop agent
|
|
328
|
+
fastify.post('/api/agents/:id/stop', async (request, reply) => {
|
|
329
|
+
const db = (0, database_1.getDatabase)();
|
|
330
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id, true);
|
|
331
|
+
if (!access)
|
|
332
|
+
return;
|
|
333
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
334
|
+
if (!agent)
|
|
335
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
336
|
+
// Set status to 'stopped' before killing so close handler preserves it
|
|
337
|
+
db.prepare("UPDATE agents SET status = 'stopped' WHERE id = ?").run(agent.id);
|
|
338
|
+
const stopped = (0, process_manager_1.stopAgentProcess)(agent.id);
|
|
339
|
+
if (!stopped) {
|
|
340
|
+
// Process not in memory map — try killing PID directly if it exists
|
|
341
|
+
if (agent.pid) {
|
|
342
|
+
// Guard: never kill our own process or parent (PID reuse after restart)
|
|
343
|
+
if (agent.pid === process.pid || agent.pid === process.ppid) {
|
|
344
|
+
fastify.log.error(`Refusing to kill PID ${agent.pid} — it is the Argus server itself (pid=${process.pid}, ppid=${process.ppid})`);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
fastify.log.warn(`Killing stale PID ${agent.pid} for agent "${agent.name}" (not in memory map)`);
|
|
348
|
+
try {
|
|
349
|
+
process.kill(agent.pid, 'SIGTERM');
|
|
350
|
+
}
|
|
351
|
+
catch { }
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
db.prepare("UPDATE agents SET pid = NULL WHERE id = ?").run(agent.id);
|
|
355
|
+
}
|
|
356
|
+
// Broadcast stopped status immediately for UI feedback
|
|
357
|
+
(0, websocket_1.broadcastToProject)(agent.project_id, {
|
|
358
|
+
type: 'agent_status', projectId: agent.project_id,
|
|
359
|
+
data: { agentId: agent.id, status: 'stopped' },
|
|
360
|
+
});
|
|
361
|
+
return { success: true };
|
|
362
|
+
});
|
|
363
|
+
// Pause agent — prevents auto-start and manual start until unpaused
|
|
364
|
+
fastify.post('/api/agents/:id/pause', async (request, reply) => {
|
|
365
|
+
const db = (0, database_1.getDatabase)();
|
|
366
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id, true);
|
|
367
|
+
if (!access)
|
|
368
|
+
return;
|
|
369
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
370
|
+
if (!agent)
|
|
371
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
372
|
+
if (agent.paused) {
|
|
373
|
+
return reply.code(409).send({ error: 'Agent is already paused' });
|
|
374
|
+
}
|
|
375
|
+
// If running, stop it first
|
|
376
|
+
if (agent.status === 'running' || (0, process_manager_1.isAgentRunning)(agent.id)) {
|
|
377
|
+
(0, process_manager_1.stopAgentProcess)(agent.id);
|
|
378
|
+
}
|
|
379
|
+
db.prepare("UPDATE agents SET paused = 1, status = 'stopped' WHERE id = ?").run(agent.id);
|
|
380
|
+
(0, websocket_1.broadcastToProject)(agent.project_id, {
|
|
381
|
+
type: 'agent_status', projectId: agent.project_id,
|
|
382
|
+
data: { agentId: agent.id, status: 'stopped', paused: true },
|
|
383
|
+
});
|
|
384
|
+
return { success: true };
|
|
385
|
+
});
|
|
386
|
+
// Unpause agent — allows it to be started again
|
|
387
|
+
fastify.post('/api/agents/:id/unpause', async (request, reply) => {
|
|
388
|
+
const db = (0, database_1.getDatabase)();
|
|
389
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id, true);
|
|
390
|
+
if (!access)
|
|
391
|
+
return;
|
|
392
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
393
|
+
if (!agent)
|
|
394
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
395
|
+
if (!agent.paused) {
|
|
396
|
+
return reply.code(409).send({ error: 'Agent is not paused' });
|
|
397
|
+
}
|
|
398
|
+
db.prepare("UPDATE agents SET paused = 0, status = 'idle' WHERE id = ?").run(agent.id);
|
|
399
|
+
(0, websocket_1.broadcastToProject)(agent.project_id, {
|
|
400
|
+
type: 'agent_status', projectId: agent.project_id,
|
|
401
|
+
data: { agentId: agent.id, status: 'idle', paused: false },
|
|
402
|
+
});
|
|
403
|
+
return { success: true };
|
|
404
|
+
});
|
|
405
|
+
// Get agent status
|
|
406
|
+
fastify.get('/api/agents/:id/status', async (request, reply) => {
|
|
407
|
+
const db = (0, database_1.getDatabase)();
|
|
408
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
409
|
+
if (!access)
|
|
410
|
+
return;
|
|
411
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
412
|
+
if (!agent)
|
|
413
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
414
|
+
// If error, prefer logs from the latest run so stale stderr from an older
|
|
415
|
+
// failure does not mask the actual reason the current run exited.
|
|
416
|
+
let lastError = null;
|
|
417
|
+
if (agent.status === 'error') {
|
|
418
|
+
const latestRun = db.prepare("SELECT run_id FROM conversation_logs WHERE agent_id = ? ORDER BY id DESC LIMIT 1").get(agent.id);
|
|
419
|
+
if (latestRun?.run_id) {
|
|
420
|
+
const errLog = db.prepare("SELECT content FROM conversation_logs WHERE agent_id = ? AND run_id = ? AND stream = 'stderr' AND trim(content) != '' ORDER BY id DESC LIMIT 1").get(agent.id, latestRun.run_id);
|
|
421
|
+
lastError = errLog?.content || null;
|
|
422
|
+
if (!lastError) {
|
|
423
|
+
const finalResult = db.prepare("SELECT content FROM conversation_logs WHERE agent_id = ? AND run_id = ? AND stream = 'stdout' AND content LIKE '%--- Final Result ---%' ORDER BY id DESC LIMIT 1").get(agent.id, latestRun.run_id);
|
|
424
|
+
lastError = finalResult?.content?.replace(/.*--- Final Result ---\n?/, '').trim() || null;
|
|
425
|
+
}
|
|
426
|
+
if (!lastError) {
|
|
427
|
+
const anyLog = db.prepare("SELECT content FROM conversation_logs WHERE agent_id = ? AND run_id = ? AND stream != 'cost' AND trim(content) != '' AND content NOT LIKE '--- [%] Cost:%' ORDER BY id DESC LIMIT 1").get(agent.id, latestRun.run_id);
|
|
428
|
+
lastError = anyLog?.content || null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (!lastError) {
|
|
432
|
+
const anyLog = db.prepare("SELECT content FROM conversation_logs WHERE agent_id = ? AND trim(content) != '' ORDER BY id DESC LIMIT 1").get(agent.id);
|
|
433
|
+
lastError = anyLog?.content || null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
id: agent.id,
|
|
438
|
+
name: agent.name,
|
|
439
|
+
status: agent.status,
|
|
440
|
+
paused: !!agent.paused,
|
|
441
|
+
pid: agent.pid,
|
|
442
|
+
is_running: (0, process_manager_1.isAgentRunning)(agent.id),
|
|
443
|
+
started_at: agent.started_at,
|
|
444
|
+
finished_at: agent.finished_at,
|
|
445
|
+
last_error: lastError,
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
// Preview system prompt for an agent
|
|
449
|
+
fastify.get('/api/agents/:id/system-prompt', async (request, reply) => {
|
|
450
|
+
const db = (0, database_1.getDatabase)();
|
|
451
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
452
|
+
if (!access)
|
|
453
|
+
return;
|
|
454
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
455
|
+
if (!agent)
|
|
456
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
457
|
+
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(agent.project_id);
|
|
458
|
+
if (!project)
|
|
459
|
+
return reply.code(404).send({ error: 'Project not found' });
|
|
460
|
+
return { prompt: (0, system_prompt_1.buildSystemPrompt)(agent, project) };
|
|
461
|
+
});
|
|
462
|
+
fastify.get('/api/agents/:id/files', async (request, reply) => {
|
|
463
|
+
const db = (0, database_1.getDatabase)();
|
|
464
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
465
|
+
if (!access)
|
|
466
|
+
return;
|
|
467
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
468
|
+
if (!agent)
|
|
469
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
470
|
+
let resolvedPath;
|
|
471
|
+
try {
|
|
472
|
+
resolvedPath = resolveAgentFilesystemPath(agent, request.query.path);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
return sendAgentFilePathError(reply, error);
|
|
476
|
+
}
|
|
477
|
+
const showHidden = request.query.showHidden === '1' || request.query.showHidden === 'true';
|
|
478
|
+
try {
|
|
479
|
+
const targetStat = await fs.stat(resolvedPath.targetPath);
|
|
480
|
+
if (!targetStat.isDirectory()) {
|
|
481
|
+
return reply.code(400).send({ error: 'Target path is not a directory' });
|
|
482
|
+
}
|
|
483
|
+
const dirents = await fs.readdir(resolvedPath.targetPath, { withFileTypes: true });
|
|
484
|
+
const visibleEntries = dirents.filter((entry) => showHidden || !entry.name.startsWith('.'));
|
|
485
|
+
const entries = (await Promise.all(visibleEntries.map(async (entry) => {
|
|
486
|
+
const entryPath = path.join(resolvedPath.targetPath, entry.name);
|
|
487
|
+
try {
|
|
488
|
+
const entryStat = await fs.stat(entryPath);
|
|
489
|
+
const relativeEntryPath = resolvedPath.relativePath
|
|
490
|
+
? path.posix.join(resolvedPath.relativePath, entry.name)
|
|
491
|
+
: entry.name;
|
|
492
|
+
return {
|
|
493
|
+
name: entry.name,
|
|
494
|
+
path: relativeEntryPath,
|
|
495
|
+
type: entry.isDirectory() ? 'dir' : 'file',
|
|
496
|
+
size: entryStat.size,
|
|
497
|
+
modified: entryStat.mtime.toISOString(),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
}))).filter(Boolean);
|
|
504
|
+
entries.sort((a, b) => {
|
|
505
|
+
if (a.type !== b.type) {
|
|
506
|
+
return a.type === 'dir' ? -1 : 1;
|
|
507
|
+
}
|
|
508
|
+
return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' });
|
|
509
|
+
});
|
|
510
|
+
return {
|
|
511
|
+
path: resolvedPath.relativePath,
|
|
512
|
+
showHidden,
|
|
513
|
+
entries,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
return sendAgentFileSystemError(reply, error);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
fastify.get('/api/agents/:id/files/content', async (request, reply) => {
|
|
521
|
+
const db = (0, database_1.getDatabase)();
|
|
522
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
523
|
+
if (!access)
|
|
524
|
+
return;
|
|
525
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
526
|
+
if (!agent)
|
|
527
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
528
|
+
if (!request.query.path)
|
|
529
|
+
return reply.code(400).send({ error: 'path is required' });
|
|
530
|
+
let resolvedPath;
|
|
531
|
+
try {
|
|
532
|
+
resolvedPath = resolveAgentFilesystemPath(agent, request.query.path);
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
return sendAgentFilePathError(reply, error);
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
const targetStat = await fs.stat(resolvedPath.targetPath);
|
|
539
|
+
if (!targetStat.isFile()) {
|
|
540
|
+
return reply.code(400).send({ error: 'Target path is not a file' });
|
|
541
|
+
}
|
|
542
|
+
if (targetStat.size > MAX_AGENT_FILE_SIZE) {
|
|
543
|
+
return reply.code(413).send({ error: 'File exceeds the 1 MB limit' });
|
|
544
|
+
}
|
|
545
|
+
const buffer = await fs.readFile(resolvedPath.targetPath);
|
|
546
|
+
if (buffer.length > MAX_AGENT_FILE_SIZE) {
|
|
547
|
+
return reply.code(413).send({ error: 'File exceeds the 1 MB limit' });
|
|
548
|
+
}
|
|
549
|
+
const content = decodeTextFile(buffer);
|
|
550
|
+
if (content === null) {
|
|
551
|
+
return reply.code(415).send({ error: 'Cannot preview binary files' });
|
|
552
|
+
}
|
|
553
|
+
return reply.type('text/plain; charset=utf-8').send(content);
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
return sendAgentFileSystemError(reply, error);
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
// Serve files directly with correct Content-Type (for PDF/HTML preview in iframe)
|
|
560
|
+
const MAX_SERVE_FILE_SIZE = 10 * 1024 * 1024; // 10 MB for binary previews
|
|
561
|
+
const SERVE_CONTENT_TYPES = {
|
|
562
|
+
'.pdf': 'application/pdf',
|
|
563
|
+
'.html': 'text/html; charset=utf-8',
|
|
564
|
+
'.htm': 'text/html; charset=utf-8',
|
|
565
|
+
};
|
|
566
|
+
fastify.get('/api/agents/:id/files/serve', async (request, reply) => {
|
|
567
|
+
const db = (0, database_1.getDatabase)();
|
|
568
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
569
|
+
if (!access)
|
|
570
|
+
return;
|
|
571
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
572
|
+
if (!agent)
|
|
573
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
574
|
+
if (!request.query.path)
|
|
575
|
+
return reply.code(400).send({ error: 'path is required' });
|
|
576
|
+
let resolvedPath;
|
|
577
|
+
try {
|
|
578
|
+
resolvedPath = resolveAgentFilesystemPath(agent, request.query.path);
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
return sendAgentFilePathError(reply, error);
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
const targetStat = await fs.stat(resolvedPath.targetPath);
|
|
585
|
+
if (!targetStat.isFile()) {
|
|
586
|
+
return reply.code(400).send({ error: 'Target path is not a file' });
|
|
587
|
+
}
|
|
588
|
+
if (targetStat.size > MAX_SERVE_FILE_SIZE) {
|
|
589
|
+
return reply.code(413).send({ error: 'File exceeds the 10 MB limit' });
|
|
590
|
+
}
|
|
591
|
+
const ext = path.extname(resolvedPath.targetPath).toLowerCase();
|
|
592
|
+
const contentType = SERVE_CONTENT_TYPES[ext];
|
|
593
|
+
if (!contentType) {
|
|
594
|
+
return reply.code(415).send({ error: 'Only PDF and HTML files can be served for preview' });
|
|
595
|
+
}
|
|
596
|
+
const buffer = await fs.readFile(resolvedPath.targetPath);
|
|
597
|
+
// For HTML files, add CSP to prevent script execution
|
|
598
|
+
if (ext === '.html' || ext === '.htm') {
|
|
599
|
+
reply.header('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; img-src data: blob:");
|
|
600
|
+
}
|
|
601
|
+
return reply.type(contentType).send(buffer);
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
return sendAgentFileSystemError(reply, error);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
fastify.put('/api/agents/:id/files/content', async (request, reply) => {
|
|
608
|
+
const db = (0, database_1.getDatabase)();
|
|
609
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id, true);
|
|
610
|
+
if (!access)
|
|
611
|
+
return;
|
|
612
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
613
|
+
if (!agent)
|
|
614
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
615
|
+
const filePath = typeof request.body?.path === 'string' ? request.body.path.trim() : '';
|
|
616
|
+
if (!filePath) {
|
|
617
|
+
return reply.code(400).send({ error: 'path is required' });
|
|
618
|
+
}
|
|
619
|
+
if (typeof request.body?.content !== 'string') {
|
|
620
|
+
return reply.code(400).send({ error: 'content must be a string' });
|
|
621
|
+
}
|
|
622
|
+
if (Buffer.byteLength(request.body.content, 'utf-8') > MAX_AGENT_FILE_SIZE) {
|
|
623
|
+
return reply.code(413).send({ error: 'File exceeds the 1 MB limit' });
|
|
624
|
+
}
|
|
625
|
+
let resolvedPath;
|
|
626
|
+
try {
|
|
627
|
+
resolvedPath = resolveAgentFilesystemPath(agent, filePath);
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
return sendAgentFilePathError(reply, error);
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
const existing = await fs.stat(resolvedPath.targetPath).catch((statError) => {
|
|
634
|
+
if (statError.code === 'ENOENT') {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
throw statError;
|
|
638
|
+
});
|
|
639
|
+
if (existing && !existing.isFile()) {
|
|
640
|
+
return reply.code(400).send({ error: 'Target path is not a file' });
|
|
641
|
+
}
|
|
642
|
+
await fs.writeFile(resolvedPath.targetPath, request.body.content, 'utf-8');
|
|
643
|
+
const savedStat = await fs.stat(resolvedPath.targetPath);
|
|
644
|
+
return {
|
|
645
|
+
success: true,
|
|
646
|
+
path: resolvedPath.relativePath,
|
|
647
|
+
size: savedStat.size,
|
|
648
|
+
modified: savedStat.mtime.toISOString(),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
return sendAgentFileSystemError(reply, error);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
// File upload (multipart/form-data)
|
|
656
|
+
fastify.post('/api/agents/:id/files/upload', async (request, reply) => {
|
|
657
|
+
const db = (0, database_1.getDatabase)();
|
|
658
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id, true);
|
|
659
|
+
if (!access)
|
|
660
|
+
return;
|
|
661
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
662
|
+
if (!agent)
|
|
663
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
664
|
+
const parts = request.parts();
|
|
665
|
+
let targetDir = '';
|
|
666
|
+
const uploaded = [];
|
|
667
|
+
for await (const part of parts) {
|
|
668
|
+
if (part.type === 'field' && part.fieldname === 'path') {
|
|
669
|
+
targetDir = String(part.value || '');
|
|
670
|
+
}
|
|
671
|
+
else if (part.type === 'file' && part.fieldname === 'file') {
|
|
672
|
+
const fileName = part.filename;
|
|
673
|
+
if (!fileName) {
|
|
674
|
+
await part.toBuffer(); // consume the stream
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
let resolvedPath;
|
|
678
|
+
try {
|
|
679
|
+
const filePath = targetDir ? path.posix.join(targetDir, fileName) : fileName;
|
|
680
|
+
resolvedPath = resolveAgentFilesystemPath(agent, filePath);
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
await part.toBuffer(); // consume the stream
|
|
684
|
+
return sendAgentFilePathError(reply, error);
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
// Ensure parent directory exists
|
|
688
|
+
await fs.mkdir(path.dirname(resolvedPath.targetPath), { recursive: true });
|
|
689
|
+
const buffer = await part.toBuffer();
|
|
690
|
+
await fs.writeFile(resolvedPath.targetPath, buffer);
|
|
691
|
+
const savedStat = await fs.stat(resolvedPath.targetPath);
|
|
692
|
+
uploaded.push({
|
|
693
|
+
success: true,
|
|
694
|
+
path: resolvedPath.relativePath,
|
|
695
|
+
name: fileName,
|
|
696
|
+
size: savedStat.size,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
return sendAgentFileSystemError(reply, error);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (uploaded.length === 0) {
|
|
705
|
+
return reply.code(400).send({ error: 'No files uploaded' });
|
|
706
|
+
}
|
|
707
|
+
if (uploaded.length === 1) {
|
|
708
|
+
return uploaded[0];
|
|
709
|
+
}
|
|
710
|
+
return { success: true, files: uploaded };
|
|
711
|
+
});
|
|
712
|
+
// File download
|
|
713
|
+
const MIME_TYPES = {
|
|
714
|
+
'.html': 'text/html', '.htm': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
|
|
715
|
+
'.json': 'application/json', '.xml': 'application/xml', '.svg': 'image/svg+xml',
|
|
716
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
|
|
717
|
+
'.webp': 'image/webp', '.ico': 'image/x-icon', '.pdf': 'application/pdf',
|
|
718
|
+
'.zip': 'application/zip', '.gz': 'application/gzip', '.tar': 'application/x-tar',
|
|
719
|
+
'.txt': 'text/plain', '.md': 'text/plain', '.csv': 'text/csv',
|
|
720
|
+
'.mp3': 'audio/mpeg', '.mp4': 'video/mp4', '.woff': 'font/woff', '.woff2': 'font/woff2',
|
|
721
|
+
};
|
|
722
|
+
fastify.get('/api/agents/:id/files/download', async (request, reply) => {
|
|
723
|
+
const db = (0, database_1.getDatabase)();
|
|
724
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
725
|
+
if (!access)
|
|
726
|
+
return;
|
|
727
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
728
|
+
if (!agent)
|
|
729
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
730
|
+
if (!request.query.path)
|
|
731
|
+
return reply.code(400).send({ error: 'path is required' });
|
|
732
|
+
let resolvedPath;
|
|
733
|
+
try {
|
|
734
|
+
resolvedPath = resolveAgentFilesystemPath(agent, request.query.path);
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
return sendAgentFilePathError(reply, error);
|
|
738
|
+
}
|
|
739
|
+
try {
|
|
740
|
+
const targetStat = await fs.stat(resolvedPath.targetPath);
|
|
741
|
+
if (!targetStat.isFile()) {
|
|
742
|
+
return reply.code(400).send({ error: 'Target path is not a file' });
|
|
743
|
+
}
|
|
744
|
+
const ext = path.extname(resolvedPath.targetPath).toLowerCase();
|
|
745
|
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
746
|
+
const fileName = path.basename(resolvedPath.targetPath);
|
|
747
|
+
const buffer = await fs.readFile(resolvedPath.targetPath);
|
|
748
|
+
return reply
|
|
749
|
+
.header('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`)
|
|
750
|
+
.type(contentType)
|
|
751
|
+
.send(buffer);
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
return sendAgentFileSystemError(reply, error);
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
// Plain text terminal output (for debugging / curl)
|
|
758
|
+
fastify.get('/api/agents/:id/terminal', async (request, reply) => {
|
|
759
|
+
const db = (0, database_1.getDatabase)();
|
|
760
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
761
|
+
if (!access)
|
|
762
|
+
return;
|
|
763
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
764
|
+
if (!agent)
|
|
765
|
+
return reply.code(404).send('Agent not found');
|
|
766
|
+
const limit = parseInt(request.query.limit || '200', 10);
|
|
767
|
+
const logs = db.prepare('SELECT * FROM conversation_logs WHERE agent_id = ? ORDER BY id DESC LIMIT ?').all(request.params.id, limit);
|
|
768
|
+
logs.reverse();
|
|
769
|
+
let text = `=== ${agent.name} [${agent.status}] ===\n\n`;
|
|
770
|
+
for (const l of logs) {
|
|
771
|
+
if (l.stream === 'stdin') {
|
|
772
|
+
text += `--- Input Prompt (${l.content.length} chars) ---\n`;
|
|
773
|
+
text += l.content.replace(/\n/g, ' ').slice(0, 100) + '...\n';
|
|
774
|
+
text += '--- Output ---\n';
|
|
775
|
+
}
|
|
776
|
+
else if (l.stream === 'stderr') {
|
|
777
|
+
text += `[ERR] ${l.content}`;
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
text += l.content;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return reply.type('text/plain').send(text);
|
|
784
|
+
});
|
|
785
|
+
// Get agent logs
|
|
786
|
+
fastify.get('/api/agents/:id/logs', async (request, reply) => {
|
|
787
|
+
const db = (0, database_1.getDatabase)();
|
|
788
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
789
|
+
if (!access)
|
|
790
|
+
return;
|
|
791
|
+
const limit = parseInt(request.query.limit || '100', 10);
|
|
792
|
+
return db.prepare('SELECT * FROM conversation_logs WHERE agent_id = ? ORDER BY id DESC LIMIT ?').all(request.params.id, limit);
|
|
793
|
+
});
|
|
794
|
+
// Get agent cost summary
|
|
795
|
+
fastify.get('/api/agents/:id/costs', async (request, reply) => {
|
|
796
|
+
const db = (0, database_1.getDatabase)();
|
|
797
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
798
|
+
if (!access)
|
|
799
|
+
return;
|
|
800
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
801
|
+
if (!agent)
|
|
802
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
803
|
+
// Cost records are cumulative per run — only take the last record per run_id
|
|
804
|
+
const costs = db.prepare(`SELECT c.content, c.run_id, c.created_at FROM conversation_logs c
|
|
805
|
+
INNER JOIN (SELECT MAX(id) as max_id FROM conversation_logs WHERE agent_id = ? AND stream = 'cost' GROUP BY run_id) latest
|
|
806
|
+
ON c.id = latest.max_id ORDER BY c.created_at`).all(request.params.id);
|
|
807
|
+
let totalCost = 0;
|
|
808
|
+
let totalInput = 0;
|
|
809
|
+
let totalOutput = 0;
|
|
810
|
+
const runs = [];
|
|
811
|
+
for (const c of costs) {
|
|
812
|
+
try {
|
|
813
|
+
const data = JSON.parse(c.content);
|
|
814
|
+
const costUsd = data.cost_usd || 0;
|
|
815
|
+
const inputTokens = data.input_tokens || 0;
|
|
816
|
+
const outputTokens = data.output_tokens || 0;
|
|
817
|
+
totalCost += costUsd;
|
|
818
|
+
totalInput += inputTokens;
|
|
819
|
+
totalOutput += outputTokens;
|
|
820
|
+
runs.push({ run_id: c.run_id, cost_usd: costUsd, input_tokens: inputTokens, output_tokens: outputTokens, timestamp: c.created_at });
|
|
821
|
+
}
|
|
822
|
+
catch { }
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
total_cost_usd: totalCost,
|
|
826
|
+
total_input_tokens: totalInput,
|
|
827
|
+
total_output_tokens: totalOutput,
|
|
828
|
+
total_runs: runs.length,
|
|
829
|
+
runs,
|
|
830
|
+
};
|
|
831
|
+
});
|
|
832
|
+
// Get logs for a specific run
|
|
833
|
+
fastify.get('/api/agents/:id/logs/:run_id', async (request, reply) => {
|
|
834
|
+
const db = (0, database_1.getDatabase)();
|
|
835
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
836
|
+
if (!access)
|
|
837
|
+
return;
|
|
838
|
+
return db.prepare('SELECT * FROM conversation_logs WHERE agent_id = ? AND run_id = ? ORDER BY id').all(request.params.id, request.params.run_id);
|
|
839
|
+
});
|
|
840
|
+
// List agent runs with summary
|
|
841
|
+
fastify.get('/api/agents/:id/runs', async (request, reply) => {
|
|
842
|
+
const db = (0, database_1.getDatabase)();
|
|
843
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
844
|
+
if (!access)
|
|
845
|
+
return;
|
|
846
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
847
|
+
if (!agent)
|
|
848
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
849
|
+
const limit = Math.min(parseInt(request.query.limit || '20', 10), 100);
|
|
850
|
+
// Get distinct run_ids with timestamps
|
|
851
|
+
const runs = db.prepare(`
|
|
852
|
+
SELECT run_id,
|
|
853
|
+
MIN(created_at) as started_at,
|
|
854
|
+
MAX(created_at) as finished_at
|
|
855
|
+
FROM conversation_logs
|
|
856
|
+
WHERE agent_id = ?
|
|
857
|
+
GROUP BY run_id
|
|
858
|
+
ORDER BY MIN(id) DESC
|
|
859
|
+
LIMIT ?
|
|
860
|
+
`).all(request.params.id, limit);
|
|
861
|
+
const result = runs.map(run => {
|
|
862
|
+
// Get cost data for this run
|
|
863
|
+
// Cost records are cumulative — take the last one (highest cumulative value)
|
|
864
|
+
const costLog = db.prepare("SELECT content FROM conversation_logs WHERE agent_id = ? AND run_id = ? AND stream = 'cost' ORDER BY id DESC LIMIT 1").get(request.params.id, run.run_id);
|
|
865
|
+
let costUsd = 0, inputTokens = 0, outputTokens = 0, durationMs = 0;
|
|
866
|
+
if (costLog) {
|
|
867
|
+
try {
|
|
868
|
+
const data = JSON.parse(costLog.content);
|
|
869
|
+
costUsd = data.cost_usd || 0;
|
|
870
|
+
inputTokens = data.input_tokens || 0;
|
|
871
|
+
outputTokens = data.output_tokens || 0;
|
|
872
|
+
durationMs = data.duration_ms || 0;
|
|
873
|
+
}
|
|
874
|
+
catch { }
|
|
875
|
+
}
|
|
876
|
+
// Count tool calls
|
|
877
|
+
const toolCount = db.prepare("SELECT COUNT(*) as c FROM conversation_logs WHERE agent_id = ? AND run_id = ? AND stream = 'stdout' AND content LIKE '[Tool:%'").get(request.params.id, run.run_id);
|
|
878
|
+
// Check if there was an error
|
|
879
|
+
const hasError = db.prepare("SELECT COUNT(*) as c FROM conversation_logs WHERE agent_id = ? AND run_id = ? AND stream = 'stderr' AND content != ''").get(request.params.id, run.run_id);
|
|
880
|
+
// Get final result snippet
|
|
881
|
+
const finalResult = db.prepare("SELECT content FROM conversation_logs WHERE agent_id = ? AND run_id = ? AND stream = 'stdout' AND content LIKE '%--- Final Result ---%' LIMIT 1").get(request.params.id, run.run_id);
|
|
882
|
+
const resultSnippet = finalResult?.content?.replace('--- Final Result ---', '').trim().slice(0, 200) || '';
|
|
883
|
+
return {
|
|
884
|
+
run_id: run.run_id,
|
|
885
|
+
started_at: run.started_at,
|
|
886
|
+
finished_at: run.finished_at,
|
|
887
|
+
status: hasError.c > 0 ? 'error' : 'success',
|
|
888
|
+
cost_usd: costUsd,
|
|
889
|
+
input_tokens: inputTokens,
|
|
890
|
+
output_tokens: outputTokens,
|
|
891
|
+
duration_ms: durationMs,
|
|
892
|
+
tool_call_count: toolCount.c,
|
|
893
|
+
result_snippet: resultSnippet,
|
|
894
|
+
};
|
|
895
|
+
});
|
|
896
|
+
return { runs: result };
|
|
897
|
+
});
|
|
898
|
+
// Get agent git status
|
|
899
|
+
fastify.get('/api/agents/:id/git-status', async (request, reply) => {
|
|
900
|
+
const db = (0, database_1.getDatabase)();
|
|
901
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
902
|
+
if (!access)
|
|
903
|
+
return;
|
|
904
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
905
|
+
if (!agent)
|
|
906
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
907
|
+
let dir = agent.working_directory;
|
|
908
|
+
if (!dir)
|
|
909
|
+
return { branch: null, recent_commits: [], has_uncommitted: false, diff_stat: '' };
|
|
910
|
+
if (dir.startsWith('~/'))
|
|
911
|
+
dir = path.join(os.homedir(), dir.slice(2));
|
|
912
|
+
try {
|
|
913
|
+
const gitExec = (cmd) => (0, child_process_1.execSync)(cmd, { cwd: dir, timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
914
|
+
const branch = gitExec('git branch --show-current') || gitExec('git rev-parse --short HEAD');
|
|
915
|
+
const logOutput = gitExec("git log --format='%H|%s|%ai' -n 5");
|
|
916
|
+
const recent_commits = logOutput ? logOutput.split('\n').map(line => {
|
|
917
|
+
const parts = line.split('|');
|
|
918
|
+
return { hash: parts[0]?.slice(0, 7) || '', message: parts[1] || '', date: parts.slice(2).join('|') };
|
|
919
|
+
}) : [];
|
|
920
|
+
const statusOutput = (0, child_process_1.execSync)('git status --porcelain', { cwd: dir, timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).toString();
|
|
921
|
+
const statusLines = statusOutput.split('\n').filter(l => l.length > 0);
|
|
922
|
+
const has_uncommitted = statusLines.length > 0;
|
|
923
|
+
let diff_stat = '';
|
|
924
|
+
if (has_uncommitted) {
|
|
925
|
+
try {
|
|
926
|
+
diff_stat = gitExec('git diff --stat');
|
|
927
|
+
}
|
|
928
|
+
catch { }
|
|
929
|
+
}
|
|
930
|
+
const uncommitted_files = statusLines.map(line => {
|
|
931
|
+
const status = line.slice(0, 2).trim();
|
|
932
|
+
const file = line.slice(3);
|
|
933
|
+
return { status, file };
|
|
934
|
+
});
|
|
935
|
+
return { branch, recent_commits, has_uncommitted, diff_stat, uncommitted_files };
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
return { branch: null, recent_commits: [], has_uncommitted: false, diff_stat: '' };
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
// Get structured run report
|
|
942
|
+
fastify.get('/api/agents/:id/runs/:run_id/report', async (request, reply) => {
|
|
943
|
+
const db = (0, database_1.getDatabase)();
|
|
944
|
+
const access = (0, project_permissions_1.ensureAgentAccess)(db, request, reply, request.params.id);
|
|
945
|
+
if (!access)
|
|
946
|
+
return;
|
|
947
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(request.params.id);
|
|
948
|
+
if (!agent)
|
|
949
|
+
return reply.code(404).send({ error: 'Agent not found' });
|
|
950
|
+
const logs = db.prepare('SELECT * FROM conversation_logs WHERE agent_id = ? AND run_id = ? ORDER BY id').all(request.params.id, request.params.run_id);
|
|
951
|
+
if (!logs.length)
|
|
952
|
+
return reply.code(404).send({ error: 'Run not found' });
|
|
953
|
+
// Parse logs into structured report
|
|
954
|
+
const toolCalls = [];
|
|
955
|
+
const textBlocks = [];
|
|
956
|
+
const filesChanged = new Set();
|
|
957
|
+
let costData = null;
|
|
958
|
+
let finalResult = '';
|
|
959
|
+
let hasError = false;
|
|
960
|
+
let errorMsg = '';
|
|
961
|
+
for (let i = 0; i < logs.length; i++) {
|
|
962
|
+
const log = logs[i];
|
|
963
|
+
if (log.stream === 'cost') {
|
|
964
|
+
try {
|
|
965
|
+
costData = JSON.parse(log.content);
|
|
966
|
+
}
|
|
967
|
+
catch { }
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (log.stream === 'stderr' && log.content.trim()) {
|
|
971
|
+
hasError = true;
|
|
972
|
+
errorMsg += log.content;
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
if (log.stream === 'stdin')
|
|
976
|
+
continue;
|
|
977
|
+
const content = log.content || '';
|
|
978
|
+
// Parse tool calls
|
|
979
|
+
const toolMatch = content.match(/^\[Tool: (\w+)\] (.*)$/s);
|
|
980
|
+
if (toolMatch) {
|
|
981
|
+
const toolName = toolMatch[1];
|
|
982
|
+
const toolInput = toolMatch[2].trim();
|
|
983
|
+
// Extract file paths from Edit/Write/Read tool calls
|
|
984
|
+
try {
|
|
985
|
+
const inputObj = JSON.parse(toolInput);
|
|
986
|
+
if (inputObj.file_path)
|
|
987
|
+
filesChanged.add(inputObj.file_path);
|
|
988
|
+
if (inputObj.path)
|
|
989
|
+
filesChanged.add(inputObj.path);
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
// Input was truncated, try to extract file paths with regex
|
|
993
|
+
const pathMatch = toolInput.match(/"(?:file_path|path)"\s*:\s*"([^"]+)"/);
|
|
994
|
+
if (pathMatch)
|
|
995
|
+
filesChanged.add(pathMatch[1]);
|
|
996
|
+
}
|
|
997
|
+
toolCalls.push({ name: toolName, input: toolInput.slice(0, TOOL_CALL_REPORT_CHAR_LIMIT), result: '', index: i });
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
// Parse results — associate with the preceding tool call
|
|
1001
|
+
const resultMatch = content.match(/^\[Result\] (.*)$/s);
|
|
1002
|
+
if (resultMatch && toolCalls.length > 0) {
|
|
1003
|
+
toolCalls[toolCalls.length - 1].result = resultMatch[1].slice(0, 500);
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
// Final result
|
|
1007
|
+
if (content.includes('--- Final Result ---')) {
|
|
1008
|
+
finalResult = content.replace(/.*--- Final Result ---\n?/, '').trim();
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
// Cost line (formatted)
|
|
1012
|
+
if (content.includes('--- Cost:'))
|
|
1013
|
+
continue;
|
|
1014
|
+
// Regular text output
|
|
1015
|
+
if (content.trim()) {
|
|
1016
|
+
textBlocks.push(content.trim());
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
// Build summary
|
|
1020
|
+
const startedAt = logs[0]?.created_at;
|
|
1021
|
+
const finishedAt = logs[logs.length - 1]?.created_at;
|
|
1022
|
+
// Tool call frequency
|
|
1023
|
+
const toolFreq = {};
|
|
1024
|
+
for (const tc of toolCalls) {
|
|
1025
|
+
toolFreq[tc.name] = (toolFreq[tc.name] || 0) + 1;
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
run_id: request.params.run_id,
|
|
1029
|
+
agent_id: request.params.id,
|
|
1030
|
+
agent_name: agent.name,
|
|
1031
|
+
started_at: startedAt,
|
|
1032
|
+
finished_at: finishedAt,
|
|
1033
|
+
status: hasError ? 'error' : 'success',
|
|
1034
|
+
error_message: errorMsg.slice(0, 1000) || null,
|
|
1035
|
+
cost: costData ? {
|
|
1036
|
+
total_usd: costData.cost_usd,
|
|
1037
|
+
input_tokens: costData.input_tokens,
|
|
1038
|
+
output_tokens: costData.output_tokens,
|
|
1039
|
+
cache_read: costData.cache_read,
|
|
1040
|
+
duration_ms: costData.duration_ms,
|
|
1041
|
+
} : null,
|
|
1042
|
+
summary: {
|
|
1043
|
+
total_tool_calls: toolCalls.length,
|
|
1044
|
+
tool_frequency: toolFreq,
|
|
1045
|
+
files_changed: Array.from(filesChanged),
|
|
1046
|
+
text_output_length: textBlocks.join('').length,
|
|
1047
|
+
},
|
|
1048
|
+
final_result: finalResult.slice(0, 2000) || null,
|
|
1049
|
+
tool_calls: toolCalls.map(tc => ({
|
|
1050
|
+
name: tc.name,
|
|
1051
|
+
input: tc.input,
|
|
1052
|
+
result: tc.result,
|
|
1053
|
+
})),
|
|
1054
|
+
key_decisions: textBlocks.slice(0, 20).map(t => t.slice(0, 300)),
|
|
1055
|
+
};
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
//# sourceMappingURL=agents.js.map
|