@anastops/mcp-server 0.1.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/dist/formatters.d.ts +94 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +225 -0
- package/dist/formatters.js.map +1 -0
- package/dist/handlers.d.ts +2264 -0
- package/dist/handlers.d.ts.map +1 -0
- package/dist/handlers.js +1840 -0
- package/dist/handlers.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence.d.ts +140 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +495 -0
- package/dist/persistence.js.map +1 -0
- package/package.json +70 -0
package/dist/handlers.js
ADDED
|
@@ -0,0 +1,1840 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Handlers - Simplified implementation for MVP
|
|
3
|
+
*/
|
|
4
|
+
import { AdapterRegistry } from '@anastops/adapters';
|
|
5
|
+
import { SessionManager, SessionForkService, ContextOptimizer, IntelligentRouter, } from '@anastops/core';
|
|
6
|
+
import { LRUCache } from 'lru-cache';
|
|
7
|
+
import { nanoid } from 'nanoid';
|
|
8
|
+
import { getPersistence } from './persistence.js';
|
|
9
|
+
import { formatSessionTable, formatTaskTable, formatAgentTable, getOutputFormat, } from './formatters.js';
|
|
10
|
+
/**
|
|
11
|
+
* Safe fire-and-forget wrapper for persistence operations
|
|
12
|
+
* Catches errors to prevent uncaught promise rejections from crashing the server
|
|
13
|
+
*/
|
|
14
|
+
function safePersist(operation) {
|
|
15
|
+
operation.catch(error => {
|
|
16
|
+
console.error('[Persistence Error]', error instanceof Error ? error.message : error);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
// TOON encoding constants
|
|
20
|
+
const TOON_MIN_SIZE_BYTES = 500;
|
|
21
|
+
/**
|
|
22
|
+
* Estimate token count for text
|
|
23
|
+
* Rough approximation: ~4 characters per token for English
|
|
24
|
+
*/
|
|
25
|
+
function estimateTokens(text) {
|
|
26
|
+
return Math.ceil(text.length / 4);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Convert array of uniform objects to TOON tabular format
|
|
30
|
+
* TOON format uses pipe-separated headers and values for compact representation
|
|
31
|
+
*/
|
|
32
|
+
function toTOONTabular(arr, keys) {
|
|
33
|
+
if (arr.length === 0 || keys.length === 0)
|
|
34
|
+
return '';
|
|
35
|
+
const lines = [];
|
|
36
|
+
// Header row
|
|
37
|
+
lines.push(keys.join(' | '));
|
|
38
|
+
// Data rows
|
|
39
|
+
for (const item of arr) {
|
|
40
|
+
const values = keys.map(k => {
|
|
41
|
+
const val = item[k];
|
|
42
|
+
if (val === null || val === undefined)
|
|
43
|
+
return '';
|
|
44
|
+
if (typeof val === 'string')
|
|
45
|
+
return val.slice(0, 50);
|
|
46
|
+
if (val instanceof Date)
|
|
47
|
+
return val.toISOString();
|
|
48
|
+
return String(val);
|
|
49
|
+
});
|
|
50
|
+
lines.push(values.join(' | '));
|
|
51
|
+
}
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if array contains uniform objects (all objects have same keys)
|
|
56
|
+
*/
|
|
57
|
+
function isUniformObjectArray(data) {
|
|
58
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
59
|
+
return false;
|
|
60
|
+
if (typeof data[0] !== 'object' || data[0] === null)
|
|
61
|
+
return false;
|
|
62
|
+
const firstKeys = Object.keys(data[0]).sort().join(',');
|
|
63
|
+
return data.every(item => {
|
|
64
|
+
if (typeof item !== 'object' || item === null)
|
|
65
|
+
return false;
|
|
66
|
+
return Object.keys(item).sort().join(',') === firstKeys;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Apply TOON encoding to response if beneficial
|
|
71
|
+
* Returns original data if TOON doesn't provide savings
|
|
72
|
+
*
|
|
73
|
+
* TOON (Token-Optimized Object Notation) is most effective for:
|
|
74
|
+
* - Arrays of uniform objects (converted to tabular format)
|
|
75
|
+
* - Large structured responses (> 500 bytes)
|
|
76
|
+
*/
|
|
77
|
+
export function applyTOONEncodingToResponse(data) {
|
|
78
|
+
// Only apply TOON to arrays or large objects
|
|
79
|
+
const jsonStr = JSON.stringify(data);
|
|
80
|
+
if (jsonStr.length < TOON_MIN_SIZE_BYTES) {
|
|
81
|
+
return { data }; // Too small, not worth encoding
|
|
82
|
+
}
|
|
83
|
+
const originalTokens = estimateTokens(jsonStr);
|
|
84
|
+
// Check if data structure is TOON-eligible
|
|
85
|
+
// TOON works best with arrays of uniform objects
|
|
86
|
+
if (typeof data !== 'object' || data === null) {
|
|
87
|
+
return { data };
|
|
88
|
+
}
|
|
89
|
+
const dataObj = data;
|
|
90
|
+
// Look for arrays within the response that could be TOON-encoded
|
|
91
|
+
let hasEncodableArrays = false;
|
|
92
|
+
const toonParts = [];
|
|
93
|
+
const scalarParts = [];
|
|
94
|
+
for (const [key, value] of Object.entries(dataObj)) {
|
|
95
|
+
if (isUniformObjectArray(value)) {
|
|
96
|
+
hasEncodableArrays = true;
|
|
97
|
+
const keys = Object.keys(value[0]);
|
|
98
|
+
toonParts.push(`${key}:\n${toTOONTabular(value, keys)}`);
|
|
99
|
+
}
|
|
100
|
+
else if (Array.isArray(value)) {
|
|
101
|
+
// Non-uniform array, keep as JSON
|
|
102
|
+
scalarParts.push(`${key}: ${JSON.stringify(value)}`);
|
|
103
|
+
}
|
|
104
|
+
else if (typeof value === 'object' && value !== null) {
|
|
105
|
+
// Nested object, keep as compact JSON
|
|
106
|
+
scalarParts.push(`${key}: ${JSON.stringify(value)}`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// Scalar value
|
|
110
|
+
scalarParts.push(`${key}: ${String(value)}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!hasEncodableArrays) {
|
|
114
|
+
return { data }; // No benefit from TOON encoding
|
|
115
|
+
}
|
|
116
|
+
// Build TOON-encoded representation
|
|
117
|
+
const toonEncoded = [...scalarParts, ...toonParts].join('\n');
|
|
118
|
+
const encodedTokens = estimateTokens(toonEncoded);
|
|
119
|
+
// Only use TOON if we get meaningful savings (> 20%)
|
|
120
|
+
const savingsPercent = ((originalTokens - encodedTokens) / originalTokens) * 100;
|
|
121
|
+
if (savingsPercent < 20) {
|
|
122
|
+
return { data }; // Not enough savings to justify encoding change
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
data: toonEncoded,
|
|
126
|
+
encoding: 'toon',
|
|
127
|
+
token_reduction: {
|
|
128
|
+
original: originalTokens,
|
|
129
|
+
encoded: encodedTokens,
|
|
130
|
+
savings_percent: Math.round(savingsPercent * 100) / 100,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Format a session report with all related data
|
|
136
|
+
*/
|
|
137
|
+
function formatSessionReport(report, includeOutput, includeArtifactContent) {
|
|
138
|
+
const { session, tasks: sessionTasks, agents: sessionAgents, artifacts: sessionArtifacts } = report;
|
|
139
|
+
// Calculate statistics
|
|
140
|
+
const taskStats = {
|
|
141
|
+
total: sessionTasks.length,
|
|
142
|
+
pending: sessionTasks.filter(t => t.status === 'pending').length,
|
|
143
|
+
queued: sessionTasks.filter(t => t.status === 'queued').length,
|
|
144
|
+
running: sessionTasks.filter(t => t.status === 'running').length,
|
|
145
|
+
completed: sessionTasks.filter(t => t.status === 'completed').length,
|
|
146
|
+
failed: sessionTasks.filter(t => t.status === 'failed').length,
|
|
147
|
+
cancelled: sessionTasks.filter(t => t.status === 'cancelled').length,
|
|
148
|
+
};
|
|
149
|
+
const totalTokens = sessionTasks.reduce((sum, t) => sum + (t.token_usage?.total_tokens ?? 0), 0);
|
|
150
|
+
const totalCost = sessionTasks.reduce((sum, t) => sum + (t.token_usage?.cost ?? 0), 0);
|
|
151
|
+
// Format tasks with full details
|
|
152
|
+
const formattedTasks = sessionTasks.map(t => {
|
|
153
|
+
const taskData = {
|
|
154
|
+
id: t.id,
|
|
155
|
+
type: t.type,
|
|
156
|
+
status: t.status,
|
|
157
|
+
description: t.description,
|
|
158
|
+
provider: t.provider,
|
|
159
|
+
model: t.model,
|
|
160
|
+
routing_tier: t.routing_tier,
|
|
161
|
+
complexity_score: t.complexity_score,
|
|
162
|
+
token_usage: t.token_usage,
|
|
163
|
+
created_at: t.created_at,
|
|
164
|
+
started_at: t.started_at,
|
|
165
|
+
completed_at: t.completed_at,
|
|
166
|
+
retry_count: t.retry_count,
|
|
167
|
+
input: {
|
|
168
|
+
prompt: t.input?.prompt?.slice(0, 500) ?? '',
|
|
169
|
+
context_files: t.input?.context_files ?? [],
|
|
170
|
+
agent: t.input?.agent,
|
|
171
|
+
skills: t.input?.skills,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
if (t.error) {
|
|
175
|
+
taskData.error = t.error;
|
|
176
|
+
}
|
|
177
|
+
if (includeOutput && t.output) {
|
|
178
|
+
taskData.output = {
|
|
179
|
+
content: t.output.content,
|
|
180
|
+
artifacts: t.output.artifacts,
|
|
181
|
+
files_modified: t.output.files_modified,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
else if (t.output) {
|
|
185
|
+
taskData.output = {
|
|
186
|
+
content_length: t.output.content?.length ?? 0,
|
|
187
|
+
artifacts_count: t.output.artifacts?.length ?? 0,
|
|
188
|
+
files_modified: t.output.files_modified,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return taskData;
|
|
192
|
+
});
|
|
193
|
+
// Format agents
|
|
194
|
+
const formattedAgents = sessionAgents.map(a => ({
|
|
195
|
+
id: a.id,
|
|
196
|
+
name: a.name,
|
|
197
|
+
role: a.role,
|
|
198
|
+
status: a.status,
|
|
199
|
+
provider: a.provider,
|
|
200
|
+
model: a.model,
|
|
201
|
+
tasks_completed: a.tasks_completed,
|
|
202
|
+
tasks_failed: a.tasks_failed,
|
|
203
|
+
tokens_used: a.tokens_used,
|
|
204
|
+
current_task_id: a.current_task_id,
|
|
205
|
+
created_at: a.created_at,
|
|
206
|
+
last_activity_at: a.last_activity_at,
|
|
207
|
+
}));
|
|
208
|
+
// Format artifacts
|
|
209
|
+
const formattedArtifacts = sessionArtifacts.map(a => {
|
|
210
|
+
const artifactData = {
|
|
211
|
+
id: a.id,
|
|
212
|
+
name: a.name,
|
|
213
|
+
type: a.type,
|
|
214
|
+
size_bytes: a.size_bytes,
|
|
215
|
+
token_count: a.token_count,
|
|
216
|
+
created_at: a.created_at,
|
|
217
|
+
};
|
|
218
|
+
if (includeArtifactContent) {
|
|
219
|
+
artifactData.content = a.content;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
artifactData.summary = a.summary;
|
|
223
|
+
}
|
|
224
|
+
return artifactData;
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
session: {
|
|
228
|
+
id: session.id,
|
|
229
|
+
objective: session.objective,
|
|
230
|
+
status: session.status,
|
|
231
|
+
created_at: session.created_at,
|
|
232
|
+
updated_at: session.updated_at,
|
|
233
|
+
metadata: session.metadata,
|
|
234
|
+
},
|
|
235
|
+
statistics: {
|
|
236
|
+
tasks: taskStats,
|
|
237
|
+
total_tokens: totalTokens,
|
|
238
|
+
total_cost_usd: totalCost,
|
|
239
|
+
agents_count: sessionAgents.length,
|
|
240
|
+
artifacts_count: sessionArtifacts.length,
|
|
241
|
+
},
|
|
242
|
+
tasks: formattedTasks,
|
|
243
|
+
agents: formattedAgents,
|
|
244
|
+
artifacts: formattedArtifacts,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// Shared instances
|
|
248
|
+
const sessionManager = new SessionManager();
|
|
249
|
+
const forkService = new SessionForkService(sessionManager);
|
|
250
|
+
const contextOptimizer = new ContextOptimizer();
|
|
251
|
+
const router = new IntelligentRouter();
|
|
252
|
+
const registry = AdapterRegistry.getInstance();
|
|
253
|
+
// LRU caches with size limits and TTL to prevent memory leaks
|
|
254
|
+
const agents = new LRUCache({ max: 1000, ttl: 1000 * 60 * 60 }); // 1 hour TTL
|
|
255
|
+
const tasks = new LRUCache({ max: 5000, ttl: 1000 * 60 * 60 * 24 }); // 24 hour TTL
|
|
256
|
+
const artifacts = new LRUCache({ max: 1000, ttl: 1000 * 60 * 60 * 24 }); // 24 hour TTL
|
|
257
|
+
const memoryStore = new LRUCache({ max: 10000, ttl: 1000 * 60 * 60 }); // 1 hour TTL
|
|
258
|
+
/**
|
|
259
|
+
* Get task from memory or MongoDB
|
|
260
|
+
* Loads from MongoDB and caches in memory if not found
|
|
261
|
+
*/
|
|
262
|
+
async function getTask(taskId) {
|
|
263
|
+
// Check in-memory first
|
|
264
|
+
const memTask = tasks.get(taskId);
|
|
265
|
+
if (memTask !== undefined) {
|
|
266
|
+
return memTask;
|
|
267
|
+
}
|
|
268
|
+
// Try to load from MongoDB
|
|
269
|
+
const dbTask = await getPersistence().getTask(taskId);
|
|
270
|
+
if (dbTask !== null) {
|
|
271
|
+
// Cache in memory for subsequent access
|
|
272
|
+
tasks.set(taskId, dbTask);
|
|
273
|
+
return dbTask;
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Get agent from memory or MongoDB
|
|
279
|
+
*/
|
|
280
|
+
async function getAgent(agentId) {
|
|
281
|
+
const memAgent = agents.get(agentId);
|
|
282
|
+
if (memAgent !== undefined) {
|
|
283
|
+
return memAgent;
|
|
284
|
+
}
|
|
285
|
+
const dbAgent = await getPersistence().getAgent(agentId);
|
|
286
|
+
if (dbAgent !== null) {
|
|
287
|
+
agents.set(agentId, dbAgent);
|
|
288
|
+
return dbAgent;
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Get artifact from memory or MongoDB
|
|
294
|
+
*/
|
|
295
|
+
async function getArtifact(artifactId) {
|
|
296
|
+
const memArtifact = artifacts.get(artifactId);
|
|
297
|
+
if (memArtifact !== undefined) {
|
|
298
|
+
return memArtifact;
|
|
299
|
+
}
|
|
300
|
+
const dbArtifact = await getPersistence().getArtifact(artifactId);
|
|
301
|
+
if (dbArtifact !== null) {
|
|
302
|
+
artifacts.set(artifactId, dbArtifact);
|
|
303
|
+
return dbArtifact;
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
// Tool definitions
|
|
308
|
+
export const toolDefinitions = [
|
|
309
|
+
// Session tools (5)
|
|
310
|
+
{
|
|
311
|
+
name: 'session_spawn',
|
|
312
|
+
description: 'Create a new AI orchestration session',
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
properties: {
|
|
316
|
+
objective: { type: 'string', description: 'High-level objective' },
|
|
317
|
+
context_files: { type: 'array', items: { type: 'string' } },
|
|
318
|
+
parent_session: { type: 'string' },
|
|
319
|
+
},
|
|
320
|
+
required: ['objective'],
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: 'session_status',
|
|
325
|
+
description: 'Get session status and metrics',
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: 'object',
|
|
328
|
+
properties: {
|
|
329
|
+
session_id: { type: 'string', description: 'Session ID' },
|
|
330
|
+
},
|
|
331
|
+
required: ['session_id'],
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'session_list',
|
|
336
|
+
description: 'List sessions with filters. Use format="table" for beautiful ASCII table output.',
|
|
337
|
+
inputSchema: {
|
|
338
|
+
type: 'object',
|
|
339
|
+
properties: {
|
|
340
|
+
status: { type: 'string', enum: ['active', 'completed', 'archived'] },
|
|
341
|
+
include_forks: { type: 'boolean' },
|
|
342
|
+
limit: { type: 'number' },
|
|
343
|
+
format: { type: 'string', enum: ['table', 'json'], description: 'Output format: "table" for ASCII table, "json" for raw JSON (default)' },
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: 'session_fork',
|
|
349
|
+
description: 'Fork a session to explore alternatives',
|
|
350
|
+
inputSchema: {
|
|
351
|
+
type: 'object',
|
|
352
|
+
properties: {
|
|
353
|
+
session_id: { type: 'string', description: 'Session ID to fork' },
|
|
354
|
+
fork_point: { type: 'number' },
|
|
355
|
+
reason: { type: 'string' },
|
|
356
|
+
},
|
|
357
|
+
required: ['session_id'],
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: 'session_archive',
|
|
362
|
+
description: 'Archive a completed session',
|
|
363
|
+
inputSchema: {
|
|
364
|
+
type: 'object',
|
|
365
|
+
properties: {
|
|
366
|
+
session_id: { type: 'string' },
|
|
367
|
+
},
|
|
368
|
+
required: ['session_id'],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: 'session_report',
|
|
373
|
+
description: 'Get comprehensive report of sessions with all tasks, agents, artifacts, and full details. Use for observability and debugging.',
|
|
374
|
+
inputSchema: {
|
|
375
|
+
type: 'object',
|
|
376
|
+
properties: {
|
|
377
|
+
session_id: { type: 'string', description: 'Specific session ID (omit for all sessions)' },
|
|
378
|
+
status: { type: 'string', enum: ['active', 'completed', 'archived'], description: 'Filter by status (when no session_id provided)' },
|
|
379
|
+
include_output: { type: 'boolean', description: 'Include full task output content (default: true)' },
|
|
380
|
+
include_artifacts: { type: 'boolean', description: 'Include artifact content (default: false, only metadata)' },
|
|
381
|
+
limit: { type: 'number', description: 'Max sessions to return (default: 20)' },
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: 'session_purge',
|
|
387
|
+
description: 'Permanently delete sessions and all related data (tasks, agents, artifacts). Use with caution.',
|
|
388
|
+
inputSchema: {
|
|
389
|
+
type: 'object',
|
|
390
|
+
properties: {
|
|
391
|
+
session_id: { type: 'string', description: 'Specific session ID to delete' },
|
|
392
|
+
session_ids: { type: 'array', items: { type: 'string' }, description: 'Array of session IDs to delete' },
|
|
393
|
+
status: { type: 'string', enum: ['active', 'completed', 'archived'], description: 'Delete all sessions with this status' },
|
|
394
|
+
older_than_days: { type: 'number', description: 'Delete sessions older than N days' },
|
|
395
|
+
confirm: { type: 'boolean', description: 'Must be true to execute deletion' },
|
|
396
|
+
},
|
|
397
|
+
required: ['confirm'],
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
// Agent tools (4)
|
|
401
|
+
{
|
|
402
|
+
name: 'agent_create',
|
|
403
|
+
description: 'Create a new AI agent',
|
|
404
|
+
inputSchema: {
|
|
405
|
+
type: 'object',
|
|
406
|
+
properties: {
|
|
407
|
+
session_id: { type: 'string' },
|
|
408
|
+
role: { type: 'string', enum: ['orchestrator', 'planner', 'implementer', 'reviewer', 'tester', 'debugger', 'documenter', 'specialist'] },
|
|
409
|
+
name: { type: 'string' },
|
|
410
|
+
provider: { type: 'string' },
|
|
411
|
+
model: { type: 'string' },
|
|
412
|
+
},
|
|
413
|
+
required: ['session_id', 'role'],
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: 'agent_deploy',
|
|
418
|
+
description: 'Deploy an agent to a task',
|
|
419
|
+
inputSchema: {
|
|
420
|
+
type: 'object',
|
|
421
|
+
properties: {
|
|
422
|
+
agent_id: { type: 'string' },
|
|
423
|
+
task_id: { type: 'string' },
|
|
424
|
+
},
|
|
425
|
+
required: ['agent_id', 'task_id'],
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: 'agent_status',
|
|
430
|
+
description: 'Get agent status',
|
|
431
|
+
inputSchema: {
|
|
432
|
+
type: 'object',
|
|
433
|
+
properties: { agent_id: { type: 'string' } },
|
|
434
|
+
required: ['agent_id'],
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'agent_list',
|
|
439
|
+
description: 'List agents in a session. Use format="table" for beautiful ASCII table output.',
|
|
440
|
+
inputSchema: {
|
|
441
|
+
type: 'object',
|
|
442
|
+
properties: {
|
|
443
|
+
session_id: { type: 'string' },
|
|
444
|
+
status: { type: 'string' },
|
|
445
|
+
format: { type: 'string', enum: ['table', 'json'], description: 'Output format: "table" for ASCII table, "json" for raw JSON (default)' },
|
|
446
|
+
},
|
|
447
|
+
required: ['session_id'],
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: 'agent_retire',
|
|
452
|
+
description: 'Retire an agent, marking it as completed',
|
|
453
|
+
inputSchema: {
|
|
454
|
+
type: 'object',
|
|
455
|
+
properties: {
|
|
456
|
+
agent_id: { type: 'string', description: 'ID of the agent to retire' },
|
|
457
|
+
},
|
|
458
|
+
required: ['agent_id'],
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
// Task tools (5)
|
|
462
|
+
{
|
|
463
|
+
name: 'task_create',
|
|
464
|
+
description: 'Create a task with intelligent routing. Supports agents and skills for Claude provider.',
|
|
465
|
+
inputSchema: {
|
|
466
|
+
type: 'object',
|
|
467
|
+
properties: {
|
|
468
|
+
session_id: { type: 'string' },
|
|
469
|
+
type: { type: 'string' },
|
|
470
|
+
description: { type: 'string' },
|
|
471
|
+
prompt: { type: 'string' },
|
|
472
|
+
context_files: { type: 'array', items: { type: 'string' } },
|
|
473
|
+
force_provider: { type: 'string' },
|
|
474
|
+
force_tier: { type: 'number' },
|
|
475
|
+
agent: { type: 'string', description: 'Agent name to use (e.g., orchestration-specialist). Agent defines model, tools, and skills.' },
|
|
476
|
+
skills: { type: 'array', items: { type: 'string' }, description: 'Specific skills to load (e.g., ["anastops-sessions", "anastops-tasks"])' },
|
|
477
|
+
},
|
|
478
|
+
required: ['session_id', 'type', 'description', 'prompt'],
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: 'task_queue',
|
|
483
|
+
description: 'Queue a task for execution',
|
|
484
|
+
inputSchema: {
|
|
485
|
+
type: 'object',
|
|
486
|
+
properties: { task_id: { type: 'string' } },
|
|
487
|
+
required: ['task_id'],
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: 'task_status',
|
|
492
|
+
description: 'Get task status',
|
|
493
|
+
inputSchema: {
|
|
494
|
+
type: 'object',
|
|
495
|
+
properties: { task_id: { type: 'string' } },
|
|
496
|
+
required: ['task_id'],
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: 'task_complete',
|
|
501
|
+
description: 'Mark task as completed',
|
|
502
|
+
inputSchema: {
|
|
503
|
+
type: 'object',
|
|
504
|
+
properties: {
|
|
505
|
+
task_id: { type: 'string' },
|
|
506
|
+
content: { type: 'string' },
|
|
507
|
+
artifacts: { type: 'array', items: { type: 'string' } },
|
|
508
|
+
},
|
|
509
|
+
required: ['task_id', 'content'],
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
name: 'task_list',
|
|
514
|
+
description: 'List tasks in a session. Use format="table" for beautiful ASCII table output.',
|
|
515
|
+
inputSchema: {
|
|
516
|
+
type: 'object',
|
|
517
|
+
properties: {
|
|
518
|
+
session_id: { type: 'string' },
|
|
519
|
+
status: { type: 'string' },
|
|
520
|
+
format: { type: 'string', enum: ['table', 'json'], description: 'Output format: "table" for ASCII table, "json" for raw JSON (default)' },
|
|
521
|
+
},
|
|
522
|
+
required: ['session_id'],
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: 'task_execute',
|
|
527
|
+
description: 'Execute a pending or queued task',
|
|
528
|
+
inputSchema: {
|
|
529
|
+
type: 'object',
|
|
530
|
+
properties: {
|
|
531
|
+
task_id: { type: 'string', description: 'ID of the task to execute' },
|
|
532
|
+
wait: { type: 'boolean', description: 'Wait for task completion (default: true)' },
|
|
533
|
+
},
|
|
534
|
+
required: ['task_id'],
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
name: 'task_cancel',
|
|
539
|
+
description: 'Cancel a pending, queued, or running task',
|
|
540
|
+
inputSchema: {
|
|
541
|
+
type: 'object',
|
|
542
|
+
properties: {
|
|
543
|
+
task_id: { type: 'string', description: 'ID of the task to cancel' },
|
|
544
|
+
},
|
|
545
|
+
required: ['task_id'],
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: 'task_retry',
|
|
550
|
+
description: 'Retry a failed task',
|
|
551
|
+
inputSchema: {
|
|
552
|
+
type: 'object',
|
|
553
|
+
properties: {
|
|
554
|
+
task_id: { type: 'string', description: 'ID of the failed task to retry' },
|
|
555
|
+
},
|
|
556
|
+
required: ['task_id'],
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
// Lock Management tools (3)
|
|
560
|
+
{
|
|
561
|
+
name: 'lock_acquire',
|
|
562
|
+
description: 'Acquire a file lock for a session to prevent conflicts',
|
|
563
|
+
inputSchema: {
|
|
564
|
+
type: 'object',
|
|
565
|
+
properties: {
|
|
566
|
+
session_id: { type: 'string' },
|
|
567
|
+
file_path: { type: 'string' },
|
|
568
|
+
ttl: { type: 'number', description: 'Lock TTL in milliseconds (default: 300000)' },
|
|
569
|
+
},
|
|
570
|
+
required: ['session_id', 'file_path'],
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: 'lock_release',
|
|
575
|
+
description: 'Release a file lock held by a session',
|
|
576
|
+
inputSchema: {
|
|
577
|
+
type: 'object',
|
|
578
|
+
properties: {
|
|
579
|
+
session_id: { type: 'string' },
|
|
580
|
+
file_path: { type: 'string' },
|
|
581
|
+
},
|
|
582
|
+
required: ['session_id', 'file_path'],
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
name: 'lock_status',
|
|
587
|
+
description: 'Check lock status for a file or session',
|
|
588
|
+
inputSchema: {
|
|
589
|
+
type: 'object',
|
|
590
|
+
properties: {
|
|
591
|
+
session_id: { type: 'string', description: 'Session ID to check locks for' },
|
|
592
|
+
file_path: { type: 'string', description: 'File path to check lock status' },
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
// Cost Monitoring tools (3)
|
|
597
|
+
{
|
|
598
|
+
name: 'session_cost',
|
|
599
|
+
description: 'Get cost breakdown for a session',
|
|
600
|
+
inputSchema: {
|
|
601
|
+
type: 'object',
|
|
602
|
+
properties: {
|
|
603
|
+
session_id: { type: 'string' },
|
|
604
|
+
},
|
|
605
|
+
required: ['session_id'],
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: 'session_metrics',
|
|
610
|
+
description: 'Get request and token usage metrics for a session',
|
|
611
|
+
inputSchema: {
|
|
612
|
+
type: 'object',
|
|
613
|
+
properties: {
|
|
614
|
+
session_id: { type: 'string' },
|
|
615
|
+
},
|
|
616
|
+
required: ['session_id'],
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: 'session_throttle',
|
|
621
|
+
description: 'Check rate limit and throttle status for a session',
|
|
622
|
+
inputSchema: {
|
|
623
|
+
type: 'object',
|
|
624
|
+
properties: {
|
|
625
|
+
session_id: { type: 'string' },
|
|
626
|
+
provider: { type: 'string', description: 'Specific provider to check' },
|
|
627
|
+
},
|
|
628
|
+
required: ['session_id'],
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
// Memory tools (5)
|
|
632
|
+
{
|
|
633
|
+
name: 'memory_store',
|
|
634
|
+
description: 'Store data in memory',
|
|
635
|
+
inputSchema: {
|
|
636
|
+
type: 'object',
|
|
637
|
+
properties: {
|
|
638
|
+
key: { type: 'string' },
|
|
639
|
+
value: {},
|
|
640
|
+
session_id: { type: 'string' },
|
|
641
|
+
},
|
|
642
|
+
required: ['key', 'value'],
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: 'memory_retrieve',
|
|
647
|
+
description: 'Retrieve data from memory',
|
|
648
|
+
inputSchema: {
|
|
649
|
+
type: 'object',
|
|
650
|
+
properties: { key: { type: 'string' } },
|
|
651
|
+
required: ['key'],
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
name: 'artifact_save',
|
|
656
|
+
description: 'Save an artifact',
|
|
657
|
+
inputSchema: {
|
|
658
|
+
type: 'object',
|
|
659
|
+
properties: {
|
|
660
|
+
session_id: { type: 'string' },
|
|
661
|
+
type: { type: 'string', enum: ['code', 'document', 'data', 'config', 'test', 'other'] },
|
|
662
|
+
name: { type: 'string' },
|
|
663
|
+
content: { type: 'string' },
|
|
664
|
+
},
|
|
665
|
+
required: ['session_id', 'type', 'name', 'content'],
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
name: 'artifact_get',
|
|
670
|
+
description: 'Get an artifact',
|
|
671
|
+
inputSchema: {
|
|
672
|
+
type: 'object',
|
|
673
|
+
properties: { artifact_id: { type: 'string' } },
|
|
674
|
+
required: ['artifact_id'],
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
name: 'artifact_list',
|
|
679
|
+
description: 'List all artifacts for a session',
|
|
680
|
+
inputSchema: {
|
|
681
|
+
type: 'object',
|
|
682
|
+
properties: {
|
|
683
|
+
session_id: { type: 'string', description: 'Session ID to list artifacts for' },
|
|
684
|
+
type: { type: 'string', enum: ['code', 'document', 'data', 'config', 'test', 'other'], description: 'Optional filter by artifact type' },
|
|
685
|
+
},
|
|
686
|
+
required: ['session_id'],
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
name: 'context_optimize',
|
|
691
|
+
description: 'Optimize context for token reduction',
|
|
692
|
+
inputSchema: {
|
|
693
|
+
type: 'object',
|
|
694
|
+
properties: {
|
|
695
|
+
session_id: { type: 'string' },
|
|
696
|
+
objective: { type: 'string' },
|
|
697
|
+
phase: { type: 'string' },
|
|
698
|
+
progress: { type: 'number' },
|
|
699
|
+
messages: { type: 'array' },
|
|
700
|
+
},
|
|
701
|
+
required: ['session_id', 'objective', 'phase', 'progress', 'messages'],
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
// Orchestration tools (4)
|
|
705
|
+
{
|
|
706
|
+
name: 'dispatch',
|
|
707
|
+
description: 'Dispatch prompt to AI provider with routing',
|
|
708
|
+
inputSchema: {
|
|
709
|
+
type: 'object',
|
|
710
|
+
properties: {
|
|
711
|
+
session_id: { type: 'string' },
|
|
712
|
+
prompt: { type: 'string' },
|
|
713
|
+
type: { type: 'string' },
|
|
714
|
+
force_provider: { type: 'string' },
|
|
715
|
+
force_tier: { type: 'number' },
|
|
716
|
+
},
|
|
717
|
+
required: ['session_id', 'prompt'],
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
name: 'route',
|
|
722
|
+
description: 'Get routing decision without executing',
|
|
723
|
+
inputSchema: {
|
|
724
|
+
type: 'object',
|
|
725
|
+
properties: {
|
|
726
|
+
type: { type: 'string' },
|
|
727
|
+
description: { type: 'string' },
|
|
728
|
+
},
|
|
729
|
+
required: ['type', 'description'],
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
name: 'broadcast',
|
|
734
|
+
description: 'Broadcast message to session agents',
|
|
735
|
+
inputSchema: {
|
|
736
|
+
type: 'object',
|
|
737
|
+
properties: {
|
|
738
|
+
session_id: { type: 'string' },
|
|
739
|
+
message: { type: 'string' },
|
|
740
|
+
message_type: { type: 'string', enum: ['info', 'warning', 'error', 'progress', 'complete'] },
|
|
741
|
+
},
|
|
742
|
+
required: ['session_id', 'message'],
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
name: 'aggregate',
|
|
747
|
+
description: 'Aggregate results from multiple tasks',
|
|
748
|
+
inputSchema: {
|
|
749
|
+
type: 'object',
|
|
750
|
+
properties: {
|
|
751
|
+
task_ids: { type: 'array', items: { type: 'string' } },
|
|
752
|
+
strategy: { type: 'string', enum: ['concat', 'merge', 'first', 'last', 'vote'] },
|
|
753
|
+
},
|
|
754
|
+
required: ['task_ids'],
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
// Utility tools (5)
|
|
758
|
+
{
|
|
759
|
+
name: 'health_check',
|
|
760
|
+
description: 'Check system health',
|
|
761
|
+
inputSchema: {
|
|
762
|
+
type: 'object',
|
|
763
|
+
properties: { include_providers: { type: 'boolean' } },
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
name: 'provider_list',
|
|
768
|
+
description: 'List available AI providers',
|
|
769
|
+
inputSchema: {
|
|
770
|
+
type: 'object',
|
|
771
|
+
properties: {
|
|
772
|
+
filter_healthy: { type: 'boolean' },
|
|
773
|
+
include_capabilities: { type: 'boolean' },
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
name: 'metrics_get',
|
|
779
|
+
description: 'Get system metrics',
|
|
780
|
+
inputSchema: {
|
|
781
|
+
type: 'object',
|
|
782
|
+
properties: { metric_type: { type: 'string', enum: ['routing', 'tokens', 'costs', 'all'] } },
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
name: 'config_get',
|
|
787
|
+
description: 'Get configuration value',
|
|
788
|
+
inputSchema: {
|
|
789
|
+
type: 'object',
|
|
790
|
+
properties: { key: { type: 'string' } },
|
|
791
|
+
required: ['key'],
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
name: 'config_set',
|
|
796
|
+
description: 'Set configuration value',
|
|
797
|
+
inputSchema: {
|
|
798
|
+
type: 'object',
|
|
799
|
+
properties: { key: { type: 'string' }, value: {} },
|
|
800
|
+
required: ['key', 'value'],
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
];
|
|
804
|
+
// Tool handler
|
|
805
|
+
export async function handleToolCall(name, args) {
|
|
806
|
+
try {
|
|
807
|
+
const result = await executeToolHandler(name, args);
|
|
808
|
+
const format = getOutputFormat(args);
|
|
809
|
+
// Apply table formatting for specific tools when format='table'
|
|
810
|
+
if (format === 'table') {
|
|
811
|
+
const formatted = formatResultAsTable(name, result);
|
|
812
|
+
if (formatted !== null) {
|
|
813
|
+
return {
|
|
814
|
+
content: [{ type: 'text', text: formatted }],
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
// Return result directly as JSON (no pretty-printing - MCP messages must not contain embedded newlines)
|
|
819
|
+
return {
|
|
820
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
821
|
+
isError: false,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
826
|
+
return {
|
|
827
|
+
content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
|
|
828
|
+
isError: true,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Format result as table for supported tools
|
|
834
|
+
* Returns null if tool doesn't support table formatting
|
|
835
|
+
*/
|
|
836
|
+
function formatResultAsTable(toolName, result) {
|
|
837
|
+
if (result === null || typeof result !== 'object') {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
const data = result;
|
|
841
|
+
switch (toolName) {
|
|
842
|
+
case 'session_list': {
|
|
843
|
+
const sessions = data['sessions'];
|
|
844
|
+
if (sessions !== undefined) {
|
|
845
|
+
// Add created_at if not present (use current time as fallback)
|
|
846
|
+
const sessionsWithDate = sessions.map(s => ({
|
|
847
|
+
...s,
|
|
848
|
+
created_at: s.created_at ?? new Date(),
|
|
849
|
+
}));
|
|
850
|
+
return formatSessionTable(sessionsWithDate);
|
|
851
|
+
}
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
case 'task_list': {
|
|
855
|
+
const tasks = data['tasks'];
|
|
856
|
+
if (tasks !== undefined) {
|
|
857
|
+
return formatTaskTable(tasks);
|
|
858
|
+
}
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
case 'agent_list': {
|
|
862
|
+
const agents = data['agents'];
|
|
863
|
+
if (agents !== undefined) {
|
|
864
|
+
return formatAgentTable(agents);
|
|
865
|
+
}
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
async function executeToolHandler(name, args) {
|
|
872
|
+
switch (name) {
|
|
873
|
+
// Session tools
|
|
874
|
+
case 'session_spawn': {
|
|
875
|
+
const session = sessionManager.spawn({
|
|
876
|
+
objective: args['objective'],
|
|
877
|
+
...(args['context_files'] !== undefined && { context_files: args['context_files'] }),
|
|
878
|
+
...(args['parent_session'] !== undefined && { parent_session: args['parent_session'] }),
|
|
879
|
+
});
|
|
880
|
+
// Persist to MongoDB
|
|
881
|
+
safePersist(getPersistence().saveSession(session));
|
|
882
|
+
return { session_id: session.id, objective: session.objective, status: session.status };
|
|
883
|
+
}
|
|
884
|
+
case 'session_status': {
|
|
885
|
+
const status = sessionManager.getStatus(args['session_id']);
|
|
886
|
+
// TOON encoding applied in handleToolCall
|
|
887
|
+
return status;
|
|
888
|
+
}
|
|
889
|
+
case 'session_list': {
|
|
890
|
+
// Get from in-memory first
|
|
891
|
+
const inMemorySessions = sessionManager.list({
|
|
892
|
+
...(args['status'] !== undefined && { status: args['status'] }),
|
|
893
|
+
...(args['include_forks'] !== undefined && { include_forks: args['include_forks'] }),
|
|
894
|
+
...(args['limit'] !== undefined && { limit: args['limit'] }),
|
|
895
|
+
});
|
|
896
|
+
// Also get from MongoDB for persistence
|
|
897
|
+
const persistedSessions = await getPersistence().listSessions({
|
|
898
|
+
...(args['status'] !== undefined && { status: args['status'] }),
|
|
899
|
+
...(args['limit'] !== undefined && { limit: args['limit'] }),
|
|
900
|
+
});
|
|
901
|
+
// Merge: in-memory takes precedence, then add persisted ones not in memory
|
|
902
|
+
const inMemoryIds = new Set(inMemorySessions.map(s => s.id));
|
|
903
|
+
const sessions = [
|
|
904
|
+
...inMemorySessions,
|
|
905
|
+
...persistedSessions.filter(s => !inMemoryIds.has(s.id)),
|
|
906
|
+
];
|
|
907
|
+
// Return raw result for table formatting to be applied in handleToolCall
|
|
908
|
+
const result = {
|
|
909
|
+
count: sessions.length,
|
|
910
|
+
sessions: sessions.map(s => ({
|
|
911
|
+
id: s.id,
|
|
912
|
+
objective: s.objective,
|
|
913
|
+
status: s.status,
|
|
914
|
+
created_at: s.created_at,
|
|
915
|
+
})),
|
|
916
|
+
};
|
|
917
|
+
// Don't apply TOON encoding here - let handleToolCall handle formatting
|
|
918
|
+
return result;
|
|
919
|
+
}
|
|
920
|
+
case 'session_fork': {
|
|
921
|
+
const forked = forkService.fork({
|
|
922
|
+
session_id: args['session_id'],
|
|
923
|
+
...(args['fork_point'] !== undefined && { fork_point: args['fork_point'] }),
|
|
924
|
+
...(args['reason'] !== undefined && { reason: args['reason'] }),
|
|
925
|
+
});
|
|
926
|
+
// Persist to MongoDB
|
|
927
|
+
void getPersistence().saveSession(forked);
|
|
928
|
+
return { forked_session_id: forked.id, parent_id: forked.parent_session_id, fork_point: forked.fork_point };
|
|
929
|
+
}
|
|
930
|
+
case 'session_archive': {
|
|
931
|
+
const session = sessionManager.updateStatus(args['session_id'], 'archived');
|
|
932
|
+
// Persist to MongoDB
|
|
933
|
+
safePersist(getPersistence().saveSession(session));
|
|
934
|
+
return { session_id: session.id, status: session.status };
|
|
935
|
+
}
|
|
936
|
+
case 'session_report': {
|
|
937
|
+
const sessionId = args['session_id'];
|
|
938
|
+
const statusFilter = args['status'];
|
|
939
|
+
const includeOutput = args['include_output'] ?? true;
|
|
940
|
+
const includeArtifactContent = args['include_artifacts'] ?? false;
|
|
941
|
+
const limit = args['limit'] ?? 20;
|
|
942
|
+
const persistence = getPersistence();
|
|
943
|
+
if (sessionId) {
|
|
944
|
+
// Single session report
|
|
945
|
+
const report = await persistence.getSessionReport(sessionId);
|
|
946
|
+
if (!report) {
|
|
947
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
948
|
+
}
|
|
949
|
+
return formatSessionReport(report, includeOutput, includeArtifactContent);
|
|
950
|
+
}
|
|
951
|
+
// All sessions report
|
|
952
|
+
const reportFilters = { limit };
|
|
953
|
+
if (statusFilter !== undefined) {
|
|
954
|
+
reportFilters.status = statusFilter;
|
|
955
|
+
}
|
|
956
|
+
const reports = await persistence.getAllSessionReports(reportFilters);
|
|
957
|
+
return {
|
|
958
|
+
total_sessions: reports.length,
|
|
959
|
+
sessions: reports.map(r => formatSessionReport(r, includeOutput, includeArtifactContent)),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
case 'session_purge': {
|
|
963
|
+
const confirm = args['confirm'];
|
|
964
|
+
if (!confirm) {
|
|
965
|
+
return { error: 'Must set confirm: true to execute deletion' };
|
|
966
|
+
}
|
|
967
|
+
const sessionId = args['session_id'];
|
|
968
|
+
const sessionIds = args['session_ids'];
|
|
969
|
+
const statusFilter = args['status'];
|
|
970
|
+
const olderThanDays = args['older_than_days'];
|
|
971
|
+
const persistence = getPersistence();
|
|
972
|
+
// Single session deletion
|
|
973
|
+
if (sessionId) {
|
|
974
|
+
// Remove from in-memory cache
|
|
975
|
+
sessionManager.remove(sessionId);
|
|
976
|
+
// Remove related tasks from cache
|
|
977
|
+
for (const [taskId, task] of tasks.entries()) {
|
|
978
|
+
if (task.session_id === sessionId) {
|
|
979
|
+
tasks.delete(taskId);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
// Remove related agents from cache
|
|
983
|
+
for (const [agentId, agent] of agents.entries()) {
|
|
984
|
+
if (agent.session_id === sessionId) {
|
|
985
|
+
agents.delete(agentId);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
// Remove related artifacts from cache
|
|
989
|
+
for (const [artifactId, artifact] of artifacts.entries()) {
|
|
990
|
+
if (artifact.session_id === sessionId) {
|
|
991
|
+
artifacts.delete(artifactId);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const deleted = await persistence.deleteSession(sessionId);
|
|
995
|
+
return { deleted: deleted, session_id: sessionId };
|
|
996
|
+
}
|
|
997
|
+
// Build filters for bulk deletion
|
|
998
|
+
const filters = {};
|
|
999
|
+
if (sessionIds && sessionIds.length > 0) {
|
|
1000
|
+
filters.session_ids = sessionIds;
|
|
1001
|
+
}
|
|
1002
|
+
if (statusFilter) {
|
|
1003
|
+
filters.status = statusFilter;
|
|
1004
|
+
}
|
|
1005
|
+
if (olderThanDays !== undefined) {
|
|
1006
|
+
const cutoffDate = new Date();
|
|
1007
|
+
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
|
|
1008
|
+
filters.older_than = cutoffDate;
|
|
1009
|
+
}
|
|
1010
|
+
if (Object.keys(filters).length === 0) {
|
|
1011
|
+
return { error: 'Must provide at least one filter: session_id, session_ids, status, or older_than_days' };
|
|
1012
|
+
}
|
|
1013
|
+
const result = await persistence.deleteSessions(filters);
|
|
1014
|
+
// Clean up in-memory caches for deleted sessions
|
|
1015
|
+
for (const deletedId of result.session_ids) {
|
|
1016
|
+
sessionManager.remove(deletedId);
|
|
1017
|
+
for (const [taskId, task] of tasks.entries()) {
|
|
1018
|
+
if (task.session_id === deletedId) {
|
|
1019
|
+
tasks.delete(taskId);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
for (const [agentId, agent] of agents.entries()) {
|
|
1023
|
+
if (agent.session_id === deletedId) {
|
|
1024
|
+
agents.delete(agentId);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
for (const [artifactId, artifact] of artifacts.entries()) {
|
|
1028
|
+
if (artifact.session_id === deletedId) {
|
|
1029
|
+
artifacts.delete(artifactId);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
deleted_count: result.deleted_count,
|
|
1035
|
+
session_ids: result.session_ids,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
// Agent tools
|
|
1039
|
+
case 'agent_create': {
|
|
1040
|
+
const agentId = nanoid(12);
|
|
1041
|
+
const now = new Date();
|
|
1042
|
+
const agent = {
|
|
1043
|
+
id: agentId,
|
|
1044
|
+
session_id: args['session_id'],
|
|
1045
|
+
role: args['role'] ?? 'implementer',
|
|
1046
|
+
name: args['name'] ?? `agent-${agentId.slice(0, 4)}`,
|
|
1047
|
+
status: 'idle',
|
|
1048
|
+
provider: args['provider'] ?? 'claude',
|
|
1049
|
+
model: args['model'] ?? 'claude-sonnet',
|
|
1050
|
+
current_task_id: null,
|
|
1051
|
+
tasks_completed: 0,
|
|
1052
|
+
tasks_failed: 0,
|
|
1053
|
+
tokens_used: 0,
|
|
1054
|
+
created_at: now,
|
|
1055
|
+
last_activity_at: now,
|
|
1056
|
+
config: {},
|
|
1057
|
+
};
|
|
1058
|
+
agents.set(agentId, agent);
|
|
1059
|
+
// Persist to MongoDB
|
|
1060
|
+
safePersist(getPersistence().saveAgent(agent));
|
|
1061
|
+
return { agent_id: agent.id, name: agent.name, role: agent.role, status: agent.status };
|
|
1062
|
+
}
|
|
1063
|
+
case 'agent_deploy': {
|
|
1064
|
+
const agent = await getAgent(args['agent_id']);
|
|
1065
|
+
if (agent === null)
|
|
1066
|
+
throw new Error('Agent not found');
|
|
1067
|
+
agent.current_task_id = args['task_id'];
|
|
1068
|
+
agent.status = 'working';
|
|
1069
|
+
agent.last_activity_at = new Date();
|
|
1070
|
+
agents.set(agent.id, agent);
|
|
1071
|
+
safePersist(getPersistence().saveAgent(agent));
|
|
1072
|
+
return { agent_id: agent.id, task_id: agent.current_task_id, status: agent.status };
|
|
1073
|
+
}
|
|
1074
|
+
case 'agent_status': {
|
|
1075
|
+
const agent = await getAgent(args['agent_id']);
|
|
1076
|
+
if (agent === null)
|
|
1077
|
+
throw new Error('Agent not found');
|
|
1078
|
+
return agent;
|
|
1079
|
+
}
|
|
1080
|
+
case 'agent_list': {
|
|
1081
|
+
// Get from in-memory cache first
|
|
1082
|
+
const inMemoryAgents = [];
|
|
1083
|
+
for (const [, agent] of agents.entries()) {
|
|
1084
|
+
if (!args['session_id'] || agent.session_id === args['session_id']) {
|
|
1085
|
+
inMemoryAgents.push(agent);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
// Also get from MongoDB for persistence
|
|
1089
|
+
const persistedAgents = await getPersistence().listAgents({
|
|
1090
|
+
...(args['session_id'] !== undefined && { session_id: args['session_id'] }),
|
|
1091
|
+
});
|
|
1092
|
+
// Merge: in-memory takes precedence
|
|
1093
|
+
const inMemoryIds = new Set(inMemoryAgents.map(a => a.id));
|
|
1094
|
+
const sessionAgents = [
|
|
1095
|
+
...inMemoryAgents,
|
|
1096
|
+
...persistedAgents.filter(a => !inMemoryIds.has(a.id)),
|
|
1097
|
+
];
|
|
1098
|
+
return { count: sessionAgents.length, agents: sessionAgents.map(a => ({ id: a.id, name: a.name, role: a.role, status: a.status })) };
|
|
1099
|
+
}
|
|
1100
|
+
case 'agent_retire': {
|
|
1101
|
+
const agentId = args['agent_id'];
|
|
1102
|
+
const agent = await getAgent(agentId);
|
|
1103
|
+
if (agent === null) {
|
|
1104
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
1105
|
+
}
|
|
1106
|
+
// Retire the agent
|
|
1107
|
+
agent.status = 'completed';
|
|
1108
|
+
agent.last_activity_at = new Date();
|
|
1109
|
+
agent.current_task_id = null;
|
|
1110
|
+
agents.set(agentId, agent);
|
|
1111
|
+
// Persist to MongoDB
|
|
1112
|
+
safePersist(getPersistence().saveAgent(agent));
|
|
1113
|
+
return {
|
|
1114
|
+
agent_id: agent.id,
|
|
1115
|
+
name: agent.name,
|
|
1116
|
+
role: agent.role,
|
|
1117
|
+
status: agent.status,
|
|
1118
|
+
tasks_completed: agent.tasks_completed,
|
|
1119
|
+
retired: true,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
// Task tools
|
|
1123
|
+
case 'task_create': {
|
|
1124
|
+
const taskId = nanoid(12);
|
|
1125
|
+
const now = new Date();
|
|
1126
|
+
const taskType = args['type'] ?? 'other';
|
|
1127
|
+
// Store agent and skills in task input for execution
|
|
1128
|
+
const taskInput = {
|
|
1129
|
+
prompt: args['prompt'],
|
|
1130
|
+
context_files: args['context_files'] ?? [],
|
|
1131
|
+
};
|
|
1132
|
+
if (args['agent'] !== undefined) {
|
|
1133
|
+
taskInput.agent = args['agent'];
|
|
1134
|
+
}
|
|
1135
|
+
if (args['skills'] !== undefined) {
|
|
1136
|
+
taskInput.skills = args['skills'];
|
|
1137
|
+
}
|
|
1138
|
+
const task = {
|
|
1139
|
+
id: taskId,
|
|
1140
|
+
session_id: args['session_id'],
|
|
1141
|
+
agent_id: null,
|
|
1142
|
+
type: taskType,
|
|
1143
|
+
status: 'pending',
|
|
1144
|
+
description: args['description'],
|
|
1145
|
+
input: taskInput,
|
|
1146
|
+
output: null,
|
|
1147
|
+
error: null,
|
|
1148
|
+
complexity_score: 0,
|
|
1149
|
+
routing_tier: 3,
|
|
1150
|
+
provider: 'claude',
|
|
1151
|
+
model: 'claude-sonnet',
|
|
1152
|
+
token_usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cost: 0 },
|
|
1153
|
+
created_at: now,
|
|
1154
|
+
started_at: null,
|
|
1155
|
+
completed_at: null,
|
|
1156
|
+
dependencies: [],
|
|
1157
|
+
priority: 5,
|
|
1158
|
+
retry_count: 0,
|
|
1159
|
+
max_retries: 3,
|
|
1160
|
+
};
|
|
1161
|
+
// Route the task with optional overrides
|
|
1162
|
+
const routingOverride = {};
|
|
1163
|
+
if (args['force_provider'] !== undefined) {
|
|
1164
|
+
routingOverride.force_provider = args['force_provider'];
|
|
1165
|
+
}
|
|
1166
|
+
if (args['force_tier'] !== undefined) {
|
|
1167
|
+
routingOverride.force_tier = args['force_tier'];
|
|
1168
|
+
}
|
|
1169
|
+
const routing = router.route(task, routingOverride);
|
|
1170
|
+
task.routing_tier = routing.tier;
|
|
1171
|
+
task.provider = routing.provider;
|
|
1172
|
+
task.model = routing.model;
|
|
1173
|
+
task.complexity_score = routing.complexity_score;
|
|
1174
|
+
tasks.set(taskId, task);
|
|
1175
|
+
// Persist to MongoDB
|
|
1176
|
+
safePersist(getPersistence().saveTask(task));
|
|
1177
|
+
const response = {
|
|
1178
|
+
task_id: task.id,
|
|
1179
|
+
type: task.type,
|
|
1180
|
+
routing: { tier: routing.tier, provider: routing.provider, model: routing.model },
|
|
1181
|
+
};
|
|
1182
|
+
if (taskInput.agent) {
|
|
1183
|
+
response.agent = taskInput.agent;
|
|
1184
|
+
}
|
|
1185
|
+
return response;
|
|
1186
|
+
}
|
|
1187
|
+
case 'task_queue': {
|
|
1188
|
+
const task = await getTask(args['task_id']);
|
|
1189
|
+
if (task === null)
|
|
1190
|
+
throw new Error('Task not found');
|
|
1191
|
+
task.status = 'queued';
|
|
1192
|
+
tasks.set(task.id, task);
|
|
1193
|
+
safePersist(getPersistence().saveTask(task));
|
|
1194
|
+
return { task_id: task.id, status: task.status };
|
|
1195
|
+
}
|
|
1196
|
+
case 'task_status': {
|
|
1197
|
+
const task = await getTask(args['task_id']);
|
|
1198
|
+
if (task === null)
|
|
1199
|
+
throw new Error('Task not found');
|
|
1200
|
+
return task;
|
|
1201
|
+
}
|
|
1202
|
+
case 'task_complete': {
|
|
1203
|
+
const task = await getTask(args['task_id']);
|
|
1204
|
+
if (task === null)
|
|
1205
|
+
throw new Error('Task not found');
|
|
1206
|
+
task.status = 'completed';
|
|
1207
|
+
task.completed_at = new Date();
|
|
1208
|
+
task.output = { content: args['content'], artifacts: args['artifacts'] ?? [], files_modified: [], metadata: {} };
|
|
1209
|
+
tasks.set(task.id, task);
|
|
1210
|
+
// Persist to MongoDB
|
|
1211
|
+
safePersist(getPersistence().saveTask(task));
|
|
1212
|
+
return { task_id: task.id, status: task.status };
|
|
1213
|
+
}
|
|
1214
|
+
case 'task_list': {
|
|
1215
|
+
// Get from in-memory cache first
|
|
1216
|
+
const inMemoryTasks = [];
|
|
1217
|
+
for (const [, task] of tasks.entries()) {
|
|
1218
|
+
if (!args['session_id'] || task.session_id === args['session_id']) {
|
|
1219
|
+
inMemoryTasks.push(task);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
// Also get from MongoDB for persistence
|
|
1223
|
+
const persistedTasks = await getPersistence().listTasks({
|
|
1224
|
+
...(args['session_id'] !== undefined && { session_id: args['session_id'] }),
|
|
1225
|
+
});
|
|
1226
|
+
// Merge: in-memory takes precedence
|
|
1227
|
+
const inMemoryIds = new Set(inMemoryTasks.map(t => t.id));
|
|
1228
|
+
const sessionTasks = [
|
|
1229
|
+
...inMemoryTasks,
|
|
1230
|
+
...persistedTasks.filter(t => !inMemoryIds.has(t.id)),
|
|
1231
|
+
];
|
|
1232
|
+
return { count: sessionTasks.length, tasks: sessionTasks.map(t => ({ id: t.id, type: t.type, status: t.status, description: t.description.slice(0, 50) })) };
|
|
1233
|
+
}
|
|
1234
|
+
case 'task_execute': {
|
|
1235
|
+
const taskId = args['task_id'];
|
|
1236
|
+
const wait = args['wait'] ?? true;
|
|
1237
|
+
const task = await getTask(taskId);
|
|
1238
|
+
if (task === null) {
|
|
1239
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
1240
|
+
}
|
|
1241
|
+
if (task.status !== 'pending' && task.status !== 'queued') {
|
|
1242
|
+
return {
|
|
1243
|
+
task_id: taskId,
|
|
1244
|
+
status: task.status,
|
|
1245
|
+
error: `Task cannot be executed - current status is '${task.status}'`,
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
// Update task status to running
|
|
1249
|
+
task.status = 'running';
|
|
1250
|
+
task.started_at = new Date();
|
|
1251
|
+
tasks.set(taskId, task);
|
|
1252
|
+
// Persist running status
|
|
1253
|
+
safePersist(getPersistence().saveTask(task));
|
|
1254
|
+
try {
|
|
1255
|
+
// Get adapter for the routed provider
|
|
1256
|
+
const adapter = registry.get(task.provider);
|
|
1257
|
+
if (adapter === undefined) {
|
|
1258
|
+
throw new Error(`Provider adapter not found: ${task.provider}`);
|
|
1259
|
+
}
|
|
1260
|
+
// Execute task with adapter
|
|
1261
|
+
// Use workspace root from environment or cwd
|
|
1262
|
+
const workingDir = process.env['ANASTOPS_WORKSPACE'] ?? process.cwd();
|
|
1263
|
+
// Build request with optional agent and skills
|
|
1264
|
+
const taskInput = task.input;
|
|
1265
|
+
const executeRequest = {
|
|
1266
|
+
model: task.model,
|
|
1267
|
+
prompt: taskInput?.prompt ?? task.description,
|
|
1268
|
+
working_dir: workingDir,
|
|
1269
|
+
};
|
|
1270
|
+
// Pass agent and skills if specified
|
|
1271
|
+
if (taskInput?.agent) {
|
|
1272
|
+
executeRequest.agent = taskInput.agent;
|
|
1273
|
+
}
|
|
1274
|
+
if (taskInput?.skills) {
|
|
1275
|
+
executeRequest.skills = taskInput.skills;
|
|
1276
|
+
}
|
|
1277
|
+
const response = await adapter.execute(executeRequest, { workingDir });
|
|
1278
|
+
// Update task with result
|
|
1279
|
+
task.status = 'completed';
|
|
1280
|
+
task.completed_at = new Date();
|
|
1281
|
+
task.output = {
|
|
1282
|
+
content: response.content,
|
|
1283
|
+
artifacts: [],
|
|
1284
|
+
files_modified: [],
|
|
1285
|
+
metadata: { usage: response.usage },
|
|
1286
|
+
};
|
|
1287
|
+
tasks.set(taskId, task);
|
|
1288
|
+
// Persist completed task
|
|
1289
|
+
safePersist(getPersistence().saveTask(task));
|
|
1290
|
+
// Update session metadata via sessionManager
|
|
1291
|
+
if (sessionManager.exists(task.session_id)) {
|
|
1292
|
+
const session = sessionManager.getSession(task.session_id);
|
|
1293
|
+
session.metadata = session.metadata ?? { total_tokens: 0, total_cost: 0, agents_used: [], files_affected: [], tasks_completed: 0, tasks_failed: 0 };
|
|
1294
|
+
session.metadata.tasks_completed = (session.metadata.tasks_completed ?? 0) + 1;
|
|
1295
|
+
session.metadata.total_tokens = (session.metadata.total_tokens ?? 0) + (response.usage?.total_tokens ?? 0);
|
|
1296
|
+
session.metadata.total_cost = (session.metadata.total_cost ?? 0) + (response.usage?.cost ?? 0);
|
|
1297
|
+
session.updated_at = new Date();
|
|
1298
|
+
safePersist(getPersistence().saveSession(session));
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
task_id: taskId,
|
|
1302
|
+
status: 'completed',
|
|
1303
|
+
result: response,
|
|
1304
|
+
waited: wait,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
catch (error) {
|
|
1308
|
+
task.status = 'failed';
|
|
1309
|
+
task.completed_at = new Date();
|
|
1310
|
+
task.error = error instanceof Error ? error.message : String(error);
|
|
1311
|
+
tasks.set(taskId, task);
|
|
1312
|
+
// Persist failed task
|
|
1313
|
+
safePersist(getPersistence().saveTask(task));
|
|
1314
|
+
// Update session metadata for failure via sessionManager
|
|
1315
|
+
if (sessionManager.exists(task.session_id)) {
|
|
1316
|
+
const session = sessionManager.getSession(task.session_id);
|
|
1317
|
+
session.metadata = session.metadata ?? { total_tokens: 0, total_cost: 0, agents_used: [], files_affected: [], tasks_completed: 0, tasks_failed: 0 };
|
|
1318
|
+
session.metadata.tasks_failed = (session.metadata.tasks_failed ?? 0) + 1;
|
|
1319
|
+
session.updated_at = new Date();
|
|
1320
|
+
safePersist(getPersistence().saveSession(session));
|
|
1321
|
+
}
|
|
1322
|
+
throw error;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
case 'task_cancel': {
|
|
1326
|
+
const taskId = args['task_id'];
|
|
1327
|
+
const task = await getTask(taskId);
|
|
1328
|
+
if (task === null) {
|
|
1329
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
1330
|
+
}
|
|
1331
|
+
// Check if task is cancelable (pending, queued, or running)
|
|
1332
|
+
const cancelableStatuses = ['pending', 'queued', 'running'];
|
|
1333
|
+
if (!cancelableStatuses.includes(task.status)) {
|
|
1334
|
+
return {
|
|
1335
|
+
task_id: taskId,
|
|
1336
|
+
status: task.status,
|
|
1337
|
+
cancelled: false,
|
|
1338
|
+
error: `Task cannot be cancelled - current status is '${task.status}'`,
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
// Cancel the task
|
|
1342
|
+
task.status = 'cancelled';
|
|
1343
|
+
task.completed_at = new Date();
|
|
1344
|
+
tasks.set(taskId, task);
|
|
1345
|
+
// Persist to MongoDB
|
|
1346
|
+
safePersist(getPersistence().saveTask(task));
|
|
1347
|
+
return {
|
|
1348
|
+
task_id: taskId,
|
|
1349
|
+
status: task.status,
|
|
1350
|
+
cancelled: true,
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
case 'task_retry': {
|
|
1354
|
+
const taskId = args['task_id'];
|
|
1355
|
+
const task = await getTask(taskId);
|
|
1356
|
+
if (task === null) {
|
|
1357
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
1358
|
+
}
|
|
1359
|
+
// Check if task is in failed status
|
|
1360
|
+
if (task.status !== 'failed') {
|
|
1361
|
+
return {
|
|
1362
|
+
task_id: taskId,
|
|
1363
|
+
status: task.status,
|
|
1364
|
+
retried: false,
|
|
1365
|
+
error: `Task cannot be retried - current status is '${task.status}' (must be 'failed')`,
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
// Check if max retries exceeded
|
|
1369
|
+
if (task.retry_count >= task.max_retries) {
|
|
1370
|
+
return {
|
|
1371
|
+
task_id: taskId,
|
|
1372
|
+
status: task.status,
|
|
1373
|
+
retried: false,
|
|
1374
|
+
error: `Task has exceeded max retries (${task.max_retries})`,
|
|
1375
|
+
retry_count: task.retry_count,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
// Retry the task
|
|
1379
|
+
task.status = 'pending';
|
|
1380
|
+
task.retry_count += 1;
|
|
1381
|
+
task.error = null;
|
|
1382
|
+
task.started_at = null;
|
|
1383
|
+
task.completed_at = null;
|
|
1384
|
+
tasks.set(taskId, task);
|
|
1385
|
+
// Persist to MongoDB
|
|
1386
|
+
safePersist(getPersistence().saveTask(task));
|
|
1387
|
+
return {
|
|
1388
|
+
task_id: taskId,
|
|
1389
|
+
status: task.status,
|
|
1390
|
+
retried: true,
|
|
1391
|
+
retry_count: task.retry_count,
|
|
1392
|
+
max_retries: task.max_retries,
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
// Lock Management tools
|
|
1396
|
+
case 'lock_acquire': {
|
|
1397
|
+
const sessionId = args['session_id'];
|
|
1398
|
+
const filePath = args['file_path'];
|
|
1399
|
+
const ttl = args['ttl'] ?? 300000;
|
|
1400
|
+
// In-memory lock implementation (Redis-based implementation would be used in production)
|
|
1401
|
+
const lockKey = `lock:${filePath}`;
|
|
1402
|
+
const existingLock = memoryStore.get(lockKey);
|
|
1403
|
+
if (existingLock !== undefined) {
|
|
1404
|
+
const lockData = existingLock.value;
|
|
1405
|
+
// Check if lock is expired
|
|
1406
|
+
if (lockData.expires_at > Date.now() && lockData.session_id !== sessionId) {
|
|
1407
|
+
return {
|
|
1408
|
+
session_id: sessionId,
|
|
1409
|
+
file_path: filePath,
|
|
1410
|
+
acquired: false,
|
|
1411
|
+
owner: lockData.session_id,
|
|
1412
|
+
ttl,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
// Acquire lock
|
|
1417
|
+
memoryStore.set(lockKey, {
|
|
1418
|
+
value: { session_id: sessionId, expires_at: Date.now() + ttl },
|
|
1419
|
+
stored_at: new Date(),
|
|
1420
|
+
});
|
|
1421
|
+
return {
|
|
1422
|
+
session_id: sessionId,
|
|
1423
|
+
file_path: filePath,
|
|
1424
|
+
acquired: true,
|
|
1425
|
+
ttl,
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
case 'lock_release': {
|
|
1429
|
+
const sessionId = args['session_id'];
|
|
1430
|
+
const filePath = args['file_path'];
|
|
1431
|
+
const lockKey = `lock:${filePath}`;
|
|
1432
|
+
const existingLock = memoryStore.get(lockKey);
|
|
1433
|
+
if (existingLock !== undefined) {
|
|
1434
|
+
const lockData = existingLock.value;
|
|
1435
|
+
if (lockData.session_id === sessionId) {
|
|
1436
|
+
memoryStore.delete(lockKey);
|
|
1437
|
+
return {
|
|
1438
|
+
session_id: sessionId,
|
|
1439
|
+
file_path: filePath,
|
|
1440
|
+
released: true,
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
return {
|
|
1444
|
+
session_id: sessionId,
|
|
1445
|
+
file_path: filePath,
|
|
1446
|
+
released: false,
|
|
1447
|
+
error: 'Lock is held by another session',
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
session_id: sessionId,
|
|
1452
|
+
file_path: filePath,
|
|
1453
|
+
released: true,
|
|
1454
|
+
message: 'Lock did not exist',
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
case 'lock_status': {
|
|
1458
|
+
const sessionId = args['session_id'];
|
|
1459
|
+
const filePath = args['file_path'];
|
|
1460
|
+
if (filePath !== undefined) {
|
|
1461
|
+
const lockKey = `lock:${filePath}`;
|
|
1462
|
+
const existingLock = memoryStore.get(lockKey);
|
|
1463
|
+
if (existingLock !== undefined) {
|
|
1464
|
+
const lockData = existingLock.value;
|
|
1465
|
+
const isExpired = lockData.expires_at <= Date.now();
|
|
1466
|
+
return {
|
|
1467
|
+
file_path: filePath,
|
|
1468
|
+
is_locked: !isExpired,
|
|
1469
|
+
owner: isExpired ? null : lockData.session_id,
|
|
1470
|
+
expires_at: lockData.expires_at,
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
file_path: filePath,
|
|
1475
|
+
is_locked: false,
|
|
1476
|
+
owner: null,
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
if (sessionId !== undefined) {
|
|
1480
|
+
// List all locks held by this session
|
|
1481
|
+
const sessionLocks = [];
|
|
1482
|
+
for (const [key, data] of memoryStore.entries()) {
|
|
1483
|
+
if (key.startsWith('lock:')) {
|
|
1484
|
+
const lockData = data.value;
|
|
1485
|
+
if (lockData.session_id === sessionId && lockData.expires_at > Date.now()) {
|
|
1486
|
+
sessionLocks.push({
|
|
1487
|
+
file_path: key.replace('lock:', ''),
|
|
1488
|
+
expires_at: lockData.expires_at,
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return {
|
|
1494
|
+
session_id: sessionId,
|
|
1495
|
+
lock_count: sessionLocks.length,
|
|
1496
|
+
locks: sessionLocks,
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
return { error: 'Must provide either session_id or file_path' };
|
|
1500
|
+
}
|
|
1501
|
+
// Cost Monitoring tools
|
|
1502
|
+
case 'session_cost': {
|
|
1503
|
+
const sessionId = args['session_id'];
|
|
1504
|
+
// Aggregate cost from task token_usage
|
|
1505
|
+
let totalCost = 0;
|
|
1506
|
+
for (const [, task] of tasks.entries()) {
|
|
1507
|
+
if (task.session_id === sessionId && task.token_usage !== undefined) {
|
|
1508
|
+
totalCost += task.token_usage.cost ?? 0;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return {
|
|
1512
|
+
session_id: sessionId,
|
|
1513
|
+
total_cost_usd: totalCost,
|
|
1514
|
+
currency: 'USD',
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
case 'session_metrics': {
|
|
1518
|
+
const sessionId = args['session_id'];
|
|
1519
|
+
// Aggregate metrics from tasks
|
|
1520
|
+
let requestCount = 0;
|
|
1521
|
+
let inputTokens = 0;
|
|
1522
|
+
let outputTokens = 0;
|
|
1523
|
+
let totalTokens = 0;
|
|
1524
|
+
for (const [, task] of tasks.entries()) {
|
|
1525
|
+
if (task.session_id === sessionId) {
|
|
1526
|
+
requestCount++;
|
|
1527
|
+
if (task.token_usage !== undefined) {
|
|
1528
|
+
inputTokens += task.token_usage.prompt_tokens ?? 0;
|
|
1529
|
+
outputTokens += task.token_usage.completion_tokens ?? 0;
|
|
1530
|
+
totalTokens += task.token_usage.total_tokens ?? 0;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return {
|
|
1535
|
+
session_id: sessionId,
|
|
1536
|
+
request_count: requestCount,
|
|
1537
|
+
token_usage: {
|
|
1538
|
+
input: inputTokens,
|
|
1539
|
+
output: outputTokens,
|
|
1540
|
+
total: totalTokens,
|
|
1541
|
+
},
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
case 'session_throttle': {
|
|
1545
|
+
const sessionId = args['session_id'];
|
|
1546
|
+
const provider = args['provider'] ?? 'claude';
|
|
1547
|
+
// In-memory rate limit tracking (Redis-based in production)
|
|
1548
|
+
const rateKey = `rate:${sessionId}:${provider}`;
|
|
1549
|
+
const rateData = memoryStore.get(rateKey);
|
|
1550
|
+
// Default limits per provider (requests per minute)
|
|
1551
|
+
const defaultLimit = 30;
|
|
1552
|
+
const limits = {
|
|
1553
|
+
claude: 50,
|
|
1554
|
+
openai: 60,
|
|
1555
|
+
gemini: 60,
|
|
1556
|
+
};
|
|
1557
|
+
const limit = limits[provider] ?? defaultLimit;
|
|
1558
|
+
if (rateData !== undefined) {
|
|
1559
|
+
const data = rateData.value;
|
|
1560
|
+
const windowSize = 60000; // 1 minute
|
|
1561
|
+
const now = Date.now();
|
|
1562
|
+
if (now - data.window_start > windowSize) {
|
|
1563
|
+
// Window expired, reset
|
|
1564
|
+
return {
|
|
1565
|
+
session_id: sessionId,
|
|
1566
|
+
provider,
|
|
1567
|
+
usage_percent: 0,
|
|
1568
|
+
current_count: 0,
|
|
1569
|
+
limit,
|
|
1570
|
+
throttled: false,
|
|
1571
|
+
blocked: false,
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
const usagePercent = (data.count / limit) * 100;
|
|
1575
|
+
return {
|
|
1576
|
+
session_id: sessionId,
|
|
1577
|
+
provider,
|
|
1578
|
+
usage_percent: Math.min(100, usagePercent),
|
|
1579
|
+
current_count: data.count,
|
|
1580
|
+
limit,
|
|
1581
|
+
throttled: usagePercent >= 85,
|
|
1582
|
+
blocked: usagePercent >= 95,
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
session_id: sessionId,
|
|
1587
|
+
provider,
|
|
1588
|
+
usage_percent: 0,
|
|
1589
|
+
current_count: 0,
|
|
1590
|
+
limit,
|
|
1591
|
+
throttled: false,
|
|
1592
|
+
blocked: false,
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
// Memory tools
|
|
1596
|
+
case 'memory_store': {
|
|
1597
|
+
const key = args['session_id'] !== undefined
|
|
1598
|
+
? `${String(args['session_id'])}:${String(args['key'])}`
|
|
1599
|
+
: String(args['key']);
|
|
1600
|
+
memoryStore.set(key, { value: args['value'], stored_at: new Date() });
|
|
1601
|
+
return { key, stored: true };
|
|
1602
|
+
}
|
|
1603
|
+
case 'memory_retrieve': {
|
|
1604
|
+
const data = memoryStore.get(args['key']);
|
|
1605
|
+
if (data === undefined)
|
|
1606
|
+
throw new Error('Key not found');
|
|
1607
|
+
return { key: args['key'], data };
|
|
1608
|
+
}
|
|
1609
|
+
case 'artifact_save': {
|
|
1610
|
+
const artifactId = nanoid(12);
|
|
1611
|
+
const now = new Date();
|
|
1612
|
+
const content = args['content'];
|
|
1613
|
+
const artifact = {
|
|
1614
|
+
id: artifactId,
|
|
1615
|
+
session_id: args['session_id'],
|
|
1616
|
+
task_id: null,
|
|
1617
|
+
type: args['type'] ?? 'other',
|
|
1618
|
+
name: args['name'],
|
|
1619
|
+
extension: '',
|
|
1620
|
+
content,
|
|
1621
|
+
content_hash: '',
|
|
1622
|
+
size_bytes: content.length,
|
|
1623
|
+
summary: content.slice(0, 200),
|
|
1624
|
+
token_count: Math.ceil(content.length / 4),
|
|
1625
|
+
relevance_score: 50,
|
|
1626
|
+
metadata: {},
|
|
1627
|
+
created_at: now,
|
|
1628
|
+
updated_at: now,
|
|
1629
|
+
};
|
|
1630
|
+
artifacts.set(artifactId, artifact);
|
|
1631
|
+
// Persist to MongoDB
|
|
1632
|
+
safePersist(getPersistence().saveArtifact(artifact));
|
|
1633
|
+
return { artifact_id: artifact.id, name: artifact.name, size_bytes: artifact.size_bytes };
|
|
1634
|
+
}
|
|
1635
|
+
case 'artifact_get': {
|
|
1636
|
+
const artifact = await getArtifact(args['artifact_id']);
|
|
1637
|
+
if (artifact === null)
|
|
1638
|
+
throw new Error('Artifact not found');
|
|
1639
|
+
return artifact;
|
|
1640
|
+
}
|
|
1641
|
+
case 'artifact_list': {
|
|
1642
|
+
const sessionId = args['session_id'];
|
|
1643
|
+
const typeFilter = args['type'];
|
|
1644
|
+
// Get from in-memory cache first
|
|
1645
|
+
const inMemoryArtifacts = [];
|
|
1646
|
+
for (const [, artifact] of artifacts.entries()) {
|
|
1647
|
+
if (artifact.session_id === sessionId) {
|
|
1648
|
+
if (typeFilter === undefined || artifact.type === typeFilter) {
|
|
1649
|
+
inMemoryArtifacts.push(artifact);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
// Also get from MongoDB for persistence
|
|
1654
|
+
const persistedArtifacts = await getPersistence().listArtifacts({
|
|
1655
|
+
session_id: sessionId,
|
|
1656
|
+
...(typeFilter !== undefined && { type: typeFilter }),
|
|
1657
|
+
});
|
|
1658
|
+
// Merge: in-memory takes precedence
|
|
1659
|
+
const inMemoryIds = new Set(inMemoryArtifacts.map(a => a.id));
|
|
1660
|
+
const sessionArtifacts = [
|
|
1661
|
+
...inMemoryArtifacts,
|
|
1662
|
+
...persistedArtifacts.filter(a => !inMemoryIds.has(a.id)),
|
|
1663
|
+
];
|
|
1664
|
+
return {
|
|
1665
|
+
session_id: sessionId,
|
|
1666
|
+
count: sessionArtifacts.length,
|
|
1667
|
+
...(typeFilter !== undefined && { type_filter: typeFilter }),
|
|
1668
|
+
artifacts: sessionArtifacts.map(a => ({
|
|
1669
|
+
id: a.id,
|
|
1670
|
+
name: a.name,
|
|
1671
|
+
type: a.type,
|
|
1672
|
+
size_bytes: a.size_bytes,
|
|
1673
|
+
created_at: a.created_at,
|
|
1674
|
+
})),
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
case 'context_optimize': {
|
|
1678
|
+
const sessionArtifacts = [];
|
|
1679
|
+
for (const [, artifact] of artifacts.entries()) {
|
|
1680
|
+
if (artifact.session_id === args['session_id']) {
|
|
1681
|
+
sessionArtifacts.push(artifact);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
const result = contextOptimizer.optimize({
|
|
1685
|
+
session_id: args['session_id'],
|
|
1686
|
+
objective: args['objective'],
|
|
1687
|
+
phase: args['phase'],
|
|
1688
|
+
progress: args['progress'],
|
|
1689
|
+
current_task: null,
|
|
1690
|
+
artifacts: sessionArtifacts,
|
|
1691
|
+
messages: args['messages'],
|
|
1692
|
+
decisions: [],
|
|
1693
|
+
completed_tasks: [],
|
|
1694
|
+
active_work: [],
|
|
1695
|
+
blockers: [],
|
|
1696
|
+
});
|
|
1697
|
+
return {
|
|
1698
|
+
original_tokens: result.original_tokens,
|
|
1699
|
+
optimized_tokens: result.optimized_tokens,
|
|
1700
|
+
reduction_percent: result.reduction_percent,
|
|
1701
|
+
minimal_context: result.context,
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
// Orchestration tools
|
|
1705
|
+
case 'dispatch': {
|
|
1706
|
+
const taskId = nanoid(12);
|
|
1707
|
+
const task = {
|
|
1708
|
+
id: taskId,
|
|
1709
|
+
session_id: args['session_id'],
|
|
1710
|
+
agent_id: null,
|
|
1711
|
+
type: args['type'] ?? 'other',
|
|
1712
|
+
status: 'pending',
|
|
1713
|
+
description: args['prompt'].slice(0, 200),
|
|
1714
|
+
input: { prompt: args['prompt'] },
|
|
1715
|
+
output: null,
|
|
1716
|
+
error: null,
|
|
1717
|
+
complexity_score: 0,
|
|
1718
|
+
routing_tier: 3,
|
|
1719
|
+
provider: 'claude',
|
|
1720
|
+
model: 'claude-sonnet',
|
|
1721
|
+
token_usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cost: 0 },
|
|
1722
|
+
created_at: new Date(),
|
|
1723
|
+
started_at: null,
|
|
1724
|
+
completed_at: null,
|
|
1725
|
+
dependencies: [],
|
|
1726
|
+
priority: 5,
|
|
1727
|
+
retry_count: 0,
|
|
1728
|
+
max_retries: 3,
|
|
1729
|
+
};
|
|
1730
|
+
const routingOverride = {};
|
|
1731
|
+
if (args['force_provider'] !== undefined) {
|
|
1732
|
+
routingOverride.force_provider = args['force_provider'];
|
|
1733
|
+
}
|
|
1734
|
+
if (args['force_tier'] !== undefined) {
|
|
1735
|
+
routingOverride.force_tier = args['force_tier'];
|
|
1736
|
+
}
|
|
1737
|
+
const routing = router.route(task, routingOverride);
|
|
1738
|
+
// Get adapter and execute
|
|
1739
|
+
const adapter = registry.get(routing.provider);
|
|
1740
|
+
if (adapter === undefined) {
|
|
1741
|
+
throw new Error(`Provider not available: ${routing.provider}`);
|
|
1742
|
+
}
|
|
1743
|
+
// Use workspace root for dispatch calls
|
|
1744
|
+
const workingDir = process.env['ANASTOPS_WORKSPACE'] ?? process.cwd();
|
|
1745
|
+
const response = await adapter.execute({ prompt: args['prompt'], model: routing.model, working_dir: workingDir }, { workingDir });
|
|
1746
|
+
const dispatchResult = {
|
|
1747
|
+
task_id: taskId,
|
|
1748
|
+
routing: { tier: routing.tier, provider: routing.provider, model: routing.model },
|
|
1749
|
+
response: { content: response.content, usage: response.usage },
|
|
1750
|
+
};
|
|
1751
|
+
// TOON encoding applied in handleToolCall
|
|
1752
|
+
return dispatchResult;
|
|
1753
|
+
}
|
|
1754
|
+
case 'route': {
|
|
1755
|
+
const task = {
|
|
1756
|
+
id: 'route-check',
|
|
1757
|
+
session_id: 'route-check',
|
|
1758
|
+
agent_id: null,
|
|
1759
|
+
type: args['type'] ?? 'other',
|
|
1760
|
+
status: 'pending',
|
|
1761
|
+
description: args['description'],
|
|
1762
|
+
input: { prompt: args['description'] },
|
|
1763
|
+
output: null,
|
|
1764
|
+
error: null,
|
|
1765
|
+
complexity_score: 0,
|
|
1766
|
+
routing_tier: 3,
|
|
1767
|
+
provider: 'claude',
|
|
1768
|
+
model: 'claude-sonnet',
|
|
1769
|
+
token_usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cost: 0 },
|
|
1770
|
+
created_at: new Date(),
|
|
1771
|
+
started_at: null,
|
|
1772
|
+
completed_at: null,
|
|
1773
|
+
dependencies: [],
|
|
1774
|
+
priority: 5,
|
|
1775
|
+
retry_count: 0,
|
|
1776
|
+
max_retries: 3,
|
|
1777
|
+
};
|
|
1778
|
+
const routing = router.route(task, {});
|
|
1779
|
+
return routing;
|
|
1780
|
+
}
|
|
1781
|
+
case 'broadcast': {
|
|
1782
|
+
return { session_id: args['session_id'], message: args['message'], message_type: args['message_type'] ?? 'info', broadcast_at: new Date() };
|
|
1783
|
+
}
|
|
1784
|
+
case 'aggregate': {
|
|
1785
|
+
const taskIds = args['task_ids'];
|
|
1786
|
+
const taskResults = taskIds.map(id => tasks.get(id)).filter(t => t !== undefined);
|
|
1787
|
+
return { task_count: taskResults.length, strategy: args['strategy'] ?? 'concat' };
|
|
1788
|
+
}
|
|
1789
|
+
// Utility tools
|
|
1790
|
+
case 'health_check': {
|
|
1791
|
+
const result = {
|
|
1792
|
+
status: 'healthy',
|
|
1793
|
+
timestamp: new Date(),
|
|
1794
|
+
components: { core: 'healthy', router: 'healthy', memory: 'in-memory' },
|
|
1795
|
+
};
|
|
1796
|
+
if (args['include_providers'] === true) {
|
|
1797
|
+
const providerHealth = {};
|
|
1798
|
+
for (const adapter of registry.list()) {
|
|
1799
|
+
const health = await adapter.healthCheck();
|
|
1800
|
+
providerHealth[adapter.type] = { healthy: health.healthy, error: health.error };
|
|
1801
|
+
}
|
|
1802
|
+
result['providers'] = providerHealth;
|
|
1803
|
+
}
|
|
1804
|
+
return result;
|
|
1805
|
+
}
|
|
1806
|
+
case 'provider_list': {
|
|
1807
|
+
const providers = [];
|
|
1808
|
+
for (const adapter of registry.list()) {
|
|
1809
|
+
const health = await adapter.healthCheck();
|
|
1810
|
+
if (args['filter_healthy'] === true && !health.healthy)
|
|
1811
|
+
continue;
|
|
1812
|
+
const info = { type: adapter.type, name: adapter.name, healthy: health.healthy };
|
|
1813
|
+
if (args['include_capabilities'] === true) {
|
|
1814
|
+
const caps = await adapter.getCapabilities();
|
|
1815
|
+
info['capabilities'] = caps;
|
|
1816
|
+
}
|
|
1817
|
+
providers.push(info);
|
|
1818
|
+
}
|
|
1819
|
+
return { count: providers.length, providers };
|
|
1820
|
+
}
|
|
1821
|
+
case 'metrics_get': {
|
|
1822
|
+
return router.getMetrics();
|
|
1823
|
+
}
|
|
1824
|
+
case 'config_get': {
|
|
1825
|
+
// Placeholder config
|
|
1826
|
+
const config = {
|
|
1827
|
+
default_provider: 'claude',
|
|
1828
|
+
default_timeout: 600000, // 10 minutes
|
|
1829
|
+
context_budget: 1600,
|
|
1830
|
+
};
|
|
1831
|
+
return { key: args['key'], value: config[args['key']] };
|
|
1832
|
+
}
|
|
1833
|
+
case 'config_set': {
|
|
1834
|
+
return { key: args['key'], value: args['value'], updated: true };
|
|
1835
|
+
}
|
|
1836
|
+
default:
|
|
1837
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
//# sourceMappingURL=handlers.js.map
|