agent-orchestration 0.5.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/.cursor/rules/orchestrator-auto.mdc +107 -0
- package/.cursor/rules/orchestrator-main.mdc +86 -0
- package/.cursor/rules/orchestrator-sub.mdc +114 -0
- package/LICENSE +21 -0
- package/README.md +329 -0
- package/activeContext.md +37 -0
- package/dist/bin/cli.d.ts +11 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +410 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/database.d.ts +91 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +522 -0
- package/dist/database.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +132 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +124 -0
- package/dist/models.js.map +1 -0
- package/dist/tools/agent.d.ts +10 -0
- package/dist/tools/agent.d.ts.map +1 -0
- package/dist/tools/agent.js +185 -0
- package/dist/tools/agent.js.map +1 -0
- package/dist/tools/coordination.d.ts +6 -0
- package/dist/tools/coordination.d.ts.map +1 -0
- package/dist/tools/coordination.js +114 -0
- package/dist/tools/coordination.js.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +108 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/task.d.ts +6 -0
- package/dist/tools/task.d.ts.map +1 -0
- package/dist/tools/task.js +267 -0
- package/dist/tools/task.js.map +1 -0
- package/dist/tools/utility.d.ts +6 -0
- package/dist/tools/utility.d.ts.map +1 -0
- package/dist/tools/utility.js +162 -0
- package/dist/tools/utility.js.map +1 -0
- package/dist/utils/contextSync.d.ts +12 -0
- package/dist/utils/contextSync.d.ts.map +1 -0
- package/dist/utils/contextSync.js +124 -0
- package/dist/utils/contextSync.js.map +1 -0
- package/package.json +54 -0
- package/src/bin/cli.ts +430 -0
- package/src/database.ts +764 -0
- package/src/index.ts +71 -0
- package/src/models.ts +226 -0
- package/src/tools/agent.ts +241 -0
- package/src/tools/coordination.ts +152 -0
- package/src/tools/index.ts +9 -0
- package/src/tools/memory.ts +150 -0
- package/src/tools/task.ts +334 -0
- package/src/tools/utility.ts +202 -0
- package/src/utils/contextSync.ts +144 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared memory tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { getDatabase } from '../database.js';
|
|
8
|
+
import { getCurrentAgentId } from './agent.js';
|
|
9
|
+
|
|
10
|
+
export function registerMemoryTools(server: McpServer): void {
|
|
11
|
+
// memory_set
|
|
12
|
+
server.tool(
|
|
13
|
+
'memory_set',
|
|
14
|
+
'Store a value in shared memory. Use namespaces to organize: context, decisions, findings, blockers.',
|
|
15
|
+
{
|
|
16
|
+
key: z.string().describe('The key to store the value under'),
|
|
17
|
+
value: z.string().describe('The value to store (will be stored as-is)'),
|
|
18
|
+
namespace: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.default('default')
|
|
22
|
+
.describe('Namespace for organization (context, decisions, findings, blockers)'),
|
|
23
|
+
ttl_seconds: z
|
|
24
|
+
.number()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Time-to-live in seconds. Entry auto-deletes after this time.'),
|
|
27
|
+
},
|
|
28
|
+
async ({ key, value, namespace, ttl_seconds }) => {
|
|
29
|
+
const agentId = getCurrentAgentId();
|
|
30
|
+
|
|
31
|
+
const entry = getDatabase().setMemory({
|
|
32
|
+
key,
|
|
33
|
+
value,
|
|
34
|
+
namespace,
|
|
35
|
+
createdBy: agentId,
|
|
36
|
+
ttlSeconds: ttl_seconds ?? null,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const ttlInfo = entry.ttlSeconds ? ` (expires in ${entry.ttlSeconds}s)` : '';
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: `Stored '${key}' in namespace '${namespace}'${ttlInfo}`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// memory_get
|
|
53
|
+
server.tool(
|
|
54
|
+
'memory_get',
|
|
55
|
+
'Retrieve a value from shared memory.',
|
|
56
|
+
{
|
|
57
|
+
key: z.string().describe('The key to retrieve'),
|
|
58
|
+
namespace: z.string().optional().default('default').describe('The namespace to search in'),
|
|
59
|
+
},
|
|
60
|
+
async ({ key, namespace }) => {
|
|
61
|
+
const entry = getDatabase().getMemory(key, namespace);
|
|
62
|
+
|
|
63
|
+
if (!entry) {
|
|
64
|
+
return {
|
|
65
|
+
content: [
|
|
66
|
+
{
|
|
67
|
+
type: 'text',
|
|
68
|
+
text: `Key '${key}' not found in namespace '${namespace}'.`,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const valueStr =
|
|
75
|
+
typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value, null, 2);
|
|
76
|
+
|
|
77
|
+
const lines = [
|
|
78
|
+
`**${namespace}:${key}**`,
|
|
79
|
+
'',
|
|
80
|
+
valueStr,
|
|
81
|
+
'',
|
|
82
|
+
`_Updated: ${entry.updatedAt.toISOString()}_`,
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
if (entry.expiresAt) {
|
|
86
|
+
lines.push(`_Expires: ${entry.expiresAt.toISOString()}_`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// memory_list
|
|
96
|
+
server.tool(
|
|
97
|
+
'memory_list',
|
|
98
|
+
'List all keys in a namespace.',
|
|
99
|
+
{
|
|
100
|
+
namespace: z.string().optional().default('default').describe('The namespace to list'),
|
|
101
|
+
},
|
|
102
|
+
async ({ namespace }) => {
|
|
103
|
+
const entries = getDatabase().listMemory(namespace);
|
|
104
|
+
|
|
105
|
+
if (entries.length === 0) {
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: 'text', text: `No entries in namespace '${namespace}'.` }],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const lines = [`# Memory: ${namespace}\n`];
|
|
112
|
+
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const valuePreview =
|
|
115
|
+
typeof entry.value === 'string'
|
|
116
|
+
? entry.value.slice(0, 100) + (entry.value.length > 100 ? '...' : '')
|
|
117
|
+
: JSON.stringify(entry.value).slice(0, 100);
|
|
118
|
+
|
|
119
|
+
lines.push(`- **${entry.key}**: ${valuePreview}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// memory_delete
|
|
129
|
+
server.tool(
|
|
130
|
+
'memory_delete',
|
|
131
|
+
'Delete a value from shared memory.',
|
|
132
|
+
{
|
|
133
|
+
key: z.string().describe('The key to delete'),
|
|
134
|
+
namespace: z.string().optional().default('default').describe('The namespace'),
|
|
135
|
+
},
|
|
136
|
+
async ({ key, namespace }) => {
|
|
137
|
+
const deleted = getDatabase().deleteMemory(key, namespace);
|
|
138
|
+
|
|
139
|
+
if (deleted) {
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: 'text', text: `Deleted '${key}' from namespace '${namespace}'.` }],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
content: [{ type: 'text', text: `Key '${key}' not found in namespace '${namespace}'.` }],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task management tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { getDatabase } from '../database.js';
|
|
8
|
+
import { TaskPriority, TaskStatus } from '../models.js';
|
|
9
|
+
import { getCurrentAgentId } from './agent.js';
|
|
10
|
+
|
|
11
|
+
export function registerTaskTools(server: McpServer): void {
|
|
12
|
+
// task_create
|
|
13
|
+
server.tool(
|
|
14
|
+
'task_create',
|
|
15
|
+
'Create a new task in the task queue.',
|
|
16
|
+
{
|
|
17
|
+
title: z.string().describe('Short title for the task'),
|
|
18
|
+
description: z.string().optional().default('').describe('Detailed description'),
|
|
19
|
+
priority: z
|
|
20
|
+
.enum(['low', 'normal', 'high', 'urgent'])
|
|
21
|
+
.optional()
|
|
22
|
+
.default('normal')
|
|
23
|
+
.describe('Priority level'),
|
|
24
|
+
assigned_to: z.string().optional().describe('Agent ID to assign to'),
|
|
25
|
+
dependencies: z
|
|
26
|
+
.array(z.string())
|
|
27
|
+
.optional()
|
|
28
|
+
.default([])
|
|
29
|
+
.describe('List of task IDs that must complete first'),
|
|
30
|
+
},
|
|
31
|
+
async ({ title, description, priority, assigned_to, dependencies }) => {
|
|
32
|
+
const agentId = getCurrentAgentId();
|
|
33
|
+
|
|
34
|
+
const task = getDatabase().createTask({
|
|
35
|
+
title,
|
|
36
|
+
description,
|
|
37
|
+
priority: priority as TaskPriority,
|
|
38
|
+
createdBy: agentId,
|
|
39
|
+
assignedTo: assigned_to ?? null,
|
|
40
|
+
dependencies,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: `Task created: '${task.title}' (\`${task.id}\`)`,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// task_claim
|
|
55
|
+
server.tool(
|
|
56
|
+
'task_claim',
|
|
57
|
+
'Claim a task to work on it. Sets status to in_progress.',
|
|
58
|
+
{
|
|
59
|
+
task_id: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('Task ID to claim. If omitted, claims the next available task.'),
|
|
63
|
+
},
|
|
64
|
+
async ({ task_id }) => {
|
|
65
|
+
const agentId = getCurrentAgentId();
|
|
66
|
+
if (!agentId) {
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: 'text', text: 'Error: Not registered.' }],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const db = getDatabase();
|
|
73
|
+
let task;
|
|
74
|
+
|
|
75
|
+
if (task_id) {
|
|
76
|
+
task = db.getTask(task_id);
|
|
77
|
+
if (!task) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: 'text', text: `Task ${task_id} not found.` }],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
task = db.getNextAvailableTask(agentId);
|
|
84
|
+
if (!task) {
|
|
85
|
+
return {
|
|
86
|
+
content: [{ type: 'text', text: 'No available tasks to claim.' }],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check dependencies
|
|
92
|
+
if (!db.checkDependenciesMet(task.id)) {
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: 'text', text: `Cannot claim: dependencies not met.` }],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Claim it
|
|
99
|
+
const updated = db.updateTask(task.id, {
|
|
100
|
+
status: TaskStatus.IN_PROGRESS,
|
|
101
|
+
assignedTo: agentId,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (updated) {
|
|
105
|
+
return {
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: `Claimed task: '${updated.title}' (\`${updated.id}\`)`,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: 'text', text: 'Failed to claim task.' }],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// task_update
|
|
122
|
+
server.tool(
|
|
123
|
+
'task_update',
|
|
124
|
+
'Update a task status or progress.',
|
|
125
|
+
{
|
|
126
|
+
task_id: z.string().describe('The task ID to update'),
|
|
127
|
+
status: z
|
|
128
|
+
.enum(['pending', 'assigned', 'in_progress', 'completed', 'failed', 'cancelled'])
|
|
129
|
+
.optional()
|
|
130
|
+
.describe('New status'),
|
|
131
|
+
progress: z.number().min(0).max(100).optional().describe('Progress percentage (0-100)'),
|
|
132
|
+
output: z.string().optional().describe('Output or notes'),
|
|
133
|
+
},
|
|
134
|
+
async ({ task_id, status, progress, output }) => {
|
|
135
|
+
const db = getDatabase();
|
|
136
|
+
const task = db.getTask(task_id);
|
|
137
|
+
|
|
138
|
+
if (!task) {
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: 'text', text: `Task ${task_id} not found.` }],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const metadata = progress !== undefined ? { progress } : undefined;
|
|
145
|
+
|
|
146
|
+
const updated = db.updateTask(task_id, {
|
|
147
|
+
status: status as TaskStatus | undefined,
|
|
148
|
+
output,
|
|
149
|
+
metadata,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (updated) {
|
|
153
|
+
const statusInfo = status ? ` → ${status}` : '';
|
|
154
|
+
const progressInfo = progress !== undefined ? ` (${progress}%)` : '';
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: 'text',
|
|
160
|
+
text: `Updated task '${updated.title}'${statusInfo}${progressInfo}`,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: 'text', text: 'Failed to update task.' }],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// task_complete
|
|
173
|
+
server.tool(
|
|
174
|
+
'task_complete',
|
|
175
|
+
'Mark a task as completed with optional output.',
|
|
176
|
+
{
|
|
177
|
+
task_id: z.string().describe('The task ID to complete'),
|
|
178
|
+
output: z.string().optional().describe('Summary of what was done'),
|
|
179
|
+
},
|
|
180
|
+
async ({ task_id, output }) => {
|
|
181
|
+
const db = getDatabase();
|
|
182
|
+
const task = db.getTask(task_id);
|
|
183
|
+
|
|
184
|
+
if (!task) {
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: 'text', text: `Task ${task_id} not found.` }],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const updated = db.updateTask(task_id, {
|
|
191
|
+
status: TaskStatus.COMPLETED,
|
|
192
|
+
output,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (updated) {
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: 'text',
|
|
200
|
+
text: `✅ Task completed: '${updated.title}'`,
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: 'text', text: 'Failed to complete task.' }],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// task_list
|
|
213
|
+
server.tool(
|
|
214
|
+
'task_list',
|
|
215
|
+
'List tasks with optional filters.',
|
|
216
|
+
{
|
|
217
|
+
status: z
|
|
218
|
+
.enum(['pending', 'assigned', 'in_progress', 'completed', 'failed', 'cancelled'])
|
|
219
|
+
.optional()
|
|
220
|
+
.describe('Filter by status'),
|
|
221
|
+
assigned_to: z.string().optional().describe('Filter by assigned agent ID'),
|
|
222
|
+
mine: z.boolean().optional().describe('Show only my tasks'),
|
|
223
|
+
},
|
|
224
|
+
async ({ status, assigned_to, mine }) => {
|
|
225
|
+
const agentId = getCurrentAgentId();
|
|
226
|
+
const db = getDatabase();
|
|
227
|
+
|
|
228
|
+
let assignee = assigned_to;
|
|
229
|
+
if (mine && agentId) {
|
|
230
|
+
assignee = agentId;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const tasks = db.listTasks({
|
|
234
|
+
status: status as TaskStatus | undefined,
|
|
235
|
+
assignedTo: assignee,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (tasks.length === 0) {
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: 'text', text: 'No tasks found.' }],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const lines = ['# Tasks\n'];
|
|
245
|
+
|
|
246
|
+
for (const task of tasks) {
|
|
247
|
+
const emoji = {
|
|
248
|
+
pending: '⏳',
|
|
249
|
+
assigned: '📋',
|
|
250
|
+
in_progress: '🔄',
|
|
251
|
+
completed: '✅',
|
|
252
|
+
failed: '❌',
|
|
253
|
+
cancelled: '🚫',
|
|
254
|
+
}[task.status];
|
|
255
|
+
|
|
256
|
+
lines.push(`${emoji} **${task.title}** (\`${task.id.slice(0, 8)}...\`)`);
|
|
257
|
+
lines.push(` Status: ${task.status} | Assigned: ${task.assignedTo || 'none'}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// is_my_turn
|
|
267
|
+
server.tool(
|
|
268
|
+
'is_my_turn',
|
|
269
|
+
"Check if it's your turn to work on a task or if work is available.",
|
|
270
|
+
{
|
|
271
|
+
task_id: z
|
|
272
|
+
.string()
|
|
273
|
+
.optional()
|
|
274
|
+
.describe('Specific task ID, or leave empty to check for any available task'),
|
|
275
|
+
},
|
|
276
|
+
async ({ task_id }) => {
|
|
277
|
+
const agentId = getCurrentAgentId();
|
|
278
|
+
if (!agentId) {
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: 'text', text: 'Error: Not registered.' }],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const db = getDatabase();
|
|
285
|
+
|
|
286
|
+
if (task_id) {
|
|
287
|
+
const task = db.getTask(task_id);
|
|
288
|
+
if (!task) {
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: 'text', text: `Task ${task_id} not found.` }],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (task.assignedTo !== agentId) {
|
|
295
|
+
return {
|
|
296
|
+
content: [
|
|
297
|
+
{
|
|
298
|
+
type: 'text',
|
|
299
|
+
text: `No - assigned to ${task.assignedTo || 'no one'}.`,
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!db.checkDependenciesMet(task_id)) {
|
|
306
|
+
return {
|
|
307
|
+
content: [{ type: 'text', text: 'No - waiting for dependencies.' }],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (task.status === TaskStatus.COMPLETED) {
|
|
312
|
+
return {
|
|
313
|
+
content: [{ type: 'text', text: 'No - already completed.' }],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
content: [{ type: 'text', text: 'Yes - task is ready for you. Use task_claim to start.' }],
|
|
319
|
+
};
|
|
320
|
+
} else {
|
|
321
|
+
const available = db.getNextAvailableTask(agentId);
|
|
322
|
+
if (available) {
|
|
323
|
+
return {
|
|
324
|
+
content: [{ type: 'text', text: `Yes - '${available.title}' is available.` }],
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
content: [{ type: 'text', text: 'No - no tasks available.' }],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility tools (bootstrap, claim_todo)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { getDatabase } from '../database.js';
|
|
8
|
+
import { AgentRole, AgentStatus, TaskPriority, TaskStatus } from '../models.js';
|
|
9
|
+
import {
|
|
10
|
+
getCurrentAgentId,
|
|
11
|
+
setCurrentAgent,
|
|
12
|
+
} from './agent.js';
|
|
13
|
+
import { syncToActiveContext } from '../utils/contextSync.js';
|
|
14
|
+
|
|
15
|
+
export function registerUtilityTools(server: McpServer): void {
|
|
16
|
+
// bootstrap
|
|
17
|
+
server.tool(
|
|
18
|
+
'bootstrap',
|
|
19
|
+
'Initialize agent session: register (if needed), get current focus, pending tasks, and recent decisions. Call this once at the start of your session.',
|
|
20
|
+
{
|
|
21
|
+
name: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Agent name. If not provided, uses env MCP_ORCH_AGENT_NAME or generates one.'),
|
|
25
|
+
role: z
|
|
26
|
+
.enum(['main', 'sub'])
|
|
27
|
+
.optional()
|
|
28
|
+
.default('sub')
|
|
29
|
+
.describe("Agent role. Defaults to env MCP_ORCH_AGENT_ROLE or 'sub'."),
|
|
30
|
+
},
|
|
31
|
+
async ({ name, role }) => {
|
|
32
|
+
const db = getDatabase();
|
|
33
|
+
|
|
34
|
+
// Get or generate agent name
|
|
35
|
+
let agentName = name ?? process.env.MCP_ORCH_AGENT_NAME;
|
|
36
|
+
if (!agentName) {
|
|
37
|
+
agentName = `agent-${Date.now()}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const agentRole =
|
|
41
|
+
role === 'main'
|
|
42
|
+
? AgentRole.MAIN
|
|
43
|
+
: role === 'sub'
|
|
44
|
+
? AgentRole.SUB
|
|
45
|
+
: process.env.MCP_ORCH_AGENT_ROLE === 'main'
|
|
46
|
+
? AgentRole.MAIN
|
|
47
|
+
: AgentRole.SUB;
|
|
48
|
+
|
|
49
|
+
const capabilities = (process.env.MCP_ORCH_CAPABILITIES ?? 'code').split(',');
|
|
50
|
+
|
|
51
|
+
// Check if agent already exists
|
|
52
|
+
let agent = db.getAgentByName(agentName);
|
|
53
|
+
|
|
54
|
+
if (agent) {
|
|
55
|
+
// Reconnect
|
|
56
|
+
setCurrentAgent(agent.id, agent.name);
|
|
57
|
+
db.updateAgentHeartbeat(agent.id, AgentStatus.ACTIVE);
|
|
58
|
+
} else {
|
|
59
|
+
// Register new
|
|
60
|
+
agent = db.createAgent({
|
|
61
|
+
name: agentName,
|
|
62
|
+
role: agentRole,
|
|
63
|
+
capabilities,
|
|
64
|
+
status: AgentStatus.ACTIVE,
|
|
65
|
+
});
|
|
66
|
+
setCurrentAgent(agent.id, agent.name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Get current context
|
|
70
|
+
const focusEntry = db.getMemory('current_focus', 'context');
|
|
71
|
+
const focusText = focusEntry ? String(focusEntry.value) : 'Not set';
|
|
72
|
+
|
|
73
|
+
// Get pending tasks for this agent
|
|
74
|
+
const myTasks = db.listTasks({ assignedTo: agent.id });
|
|
75
|
+
const pendingTasks = myTasks.filter((t) =>
|
|
76
|
+
['pending', 'assigned'].includes(t.status)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Get recent decisions
|
|
80
|
+
const decisions = db.listMemory('decisions');
|
|
81
|
+
|
|
82
|
+
// Sync context
|
|
83
|
+
syncToActiveContext();
|
|
84
|
+
|
|
85
|
+
const lines: string[] = [
|
|
86
|
+
'# Session Initialized',
|
|
87
|
+
'',
|
|
88
|
+
`**Agent**: ${agent.name} (\`${agent.id}\`)`,
|
|
89
|
+
`**Role**: ${agent.role}`,
|
|
90
|
+
'',
|
|
91
|
+
'## Current Focus',
|
|
92
|
+
focusText,
|
|
93
|
+
'',
|
|
94
|
+
'## Your Pending Tasks',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
if (pendingTasks.length > 0) {
|
|
98
|
+
for (const t of pendingTasks.slice(0, 5)) {
|
|
99
|
+
lines.push(`- ${t.title} (\`${t.id.slice(0, 8)}...\`)`);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
lines.push('_No tasks assigned to you._');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
lines.push('', '## Recent Decisions');
|
|
106
|
+
|
|
107
|
+
if (decisions.length > 0) {
|
|
108
|
+
for (const d of decisions.slice(0, 5)) {
|
|
109
|
+
lines.push(`- **${d.key}**: ${String(d.value).slice(0, 80)}`);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
lines.push('_No decisions recorded._');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
lines.push('', '---', 'Use `is_my_turn` to check for available work.');
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// claim_todo
|
|
124
|
+
server.tool(
|
|
125
|
+
'claim_todo',
|
|
126
|
+
'FOR SUB-AGENTS: Register yourself AND claim a specific task in one call. Use this when you were spawned to work on a specific todo. This creates the task if it doesn\'t exist, then claims it for you.',
|
|
127
|
+
{
|
|
128
|
+
title: z.string().describe('The title of the todo/task you were spawned to work on'),
|
|
129
|
+
description: z.string().optional().default('').describe('Additional details about the task'),
|
|
130
|
+
priority: z
|
|
131
|
+
.enum(['low', 'normal', 'high', 'urgent'])
|
|
132
|
+
.optional()
|
|
133
|
+
.default('normal')
|
|
134
|
+
.describe('Priority level'),
|
|
135
|
+
},
|
|
136
|
+
async ({ title, description, priority }) => {
|
|
137
|
+
const db = getDatabase();
|
|
138
|
+
|
|
139
|
+
// Generate agent name
|
|
140
|
+
const agentName = `sub-${Date.now()}`;
|
|
141
|
+
|
|
142
|
+
// Register as sub-agent
|
|
143
|
+
const agent = db.createAgent({
|
|
144
|
+
name: agentName,
|
|
145
|
+
role: AgentRole.SUB,
|
|
146
|
+
capabilities: ['code'],
|
|
147
|
+
status: AgentStatus.BUSY, // Already working
|
|
148
|
+
});
|
|
149
|
+
setCurrentAgent(agent.id, agent.name);
|
|
150
|
+
|
|
151
|
+
// Check if a task with this title already exists and is pending
|
|
152
|
+
const allTasks = db.listTasks();
|
|
153
|
+
let task = allTasks.find(
|
|
154
|
+
(t) =>
|
|
155
|
+
t.title.toLowerCase().trim() === title.toLowerCase().trim() &&
|
|
156
|
+
['pending', 'assigned'].includes(t.status)
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (task) {
|
|
160
|
+
// Claim the existing task
|
|
161
|
+
task = db.updateTask(task.id, {
|
|
162
|
+
assignedTo: agent.id,
|
|
163
|
+
status: TaskStatus.IN_PROGRESS,
|
|
164
|
+
})!;
|
|
165
|
+
} else {
|
|
166
|
+
// Create a new task and claim it
|
|
167
|
+
task = db.createTask({
|
|
168
|
+
title,
|
|
169
|
+
description,
|
|
170
|
+
priority: priority as TaskPriority,
|
|
171
|
+
status: TaskStatus.IN_PROGRESS,
|
|
172
|
+
assignedTo: agent.id,
|
|
173
|
+
createdBy: agent.id,
|
|
174
|
+
startedAt: new Date(),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Sync context
|
|
179
|
+
syncToActiveContext();
|
|
180
|
+
|
|
181
|
+
const lines: string[] = [
|
|
182
|
+
'# Task Claimed',
|
|
183
|
+
'',
|
|
184
|
+
`**You are**: ${agent.name} (\`${agent.id}\`)`,
|
|
185
|
+
`**Working on**: ${task.title}`,
|
|
186
|
+
`**Task ID**: \`${task.id}\``,
|
|
187
|
+
'',
|
|
188
|
+
'---',
|
|
189
|
+
'',
|
|
190
|
+
'Now you can start working. Remember to:',
|
|
191
|
+
'1. `lock_acquire` on any files you edit',
|
|
192
|
+
'2. `task_update` to report progress',
|
|
193
|
+
'3. `task_complete` when done',
|
|
194
|
+
'4. `agent_unregister` when finished',
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
}
|