agent-relay-server 0.42.0 → 0.44.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/docs/openapi.json +85 -1
- package/package.json +2 -2
- package/public/assets/{activity-G6l1nOLj.js → activity-Dkzmef0Z.js} +2 -2
- package/public/assets/{activity-G6l1nOLj.js.map → activity-Dkzmef0Z.js.map} +1 -1
- package/public/assets/{agent-profiles-BWKpVoNw.js → agent-profiles-DQYHMAyu.js} +2 -2
- package/public/assets/{agent-profiles-BWKpVoNw.js.map → agent-profiles-DQYHMAyu.js.map} +1 -1
- package/public/assets/{agents-DBuCRS5A.js → agents-atr6HZ1e.js} +2 -2
- package/public/assets/{agents-DBuCRS5A.js.map → agents-atr6HZ1e.js.map} +1 -1
- package/public/assets/{analytics-BhwMtd0_.js → analytics-azFfAy4R.js} +2 -2
- package/public/assets/{analytics-BhwMtd0_.js.map → analytics-azFfAy4R.js.map} +1 -1
- package/public/assets/{automation-B7jnsYhH.js → automation-Bk4umHIm.js} +2 -2
- package/public/assets/{automation-B7jnsYhH.js.map → automation-Bk4umHIm.js.map} +1 -1
- package/public/assets/{branch-state-badge-dN2mQ2gs.js → branch-state-badge-DymObp8d.js} +2 -2
- package/public/assets/{branch-state-badge-dN2mQ2gs.js.map → branch-state-badge-DymObp8d.js.map} +1 -1
- package/public/assets/channels-B_0wDy0s.js +2 -0
- package/public/assets/{channels-CQy-JL7o.js.map → channels-B_0wDy0s.js.map} +1 -1
- package/public/assets/chat-DxV4MyYF.js +2 -0
- package/public/assets/chat-DxV4MyYF.js.map +1 -0
- package/public/assets/{connectors-ByNxzq2k.js → connectors-DLqYF2_3.js} +2 -2
- package/public/assets/{connectors-ByNxzq2k.js.map → connectors-DLqYF2_3.js.map} +1 -1
- package/public/assets/display-ConJ9cJB.js.map +1 -1
- package/public/assets/{formatted-body-impl-BXuR2gfL.js → formatted-body-impl-LK0QcFLC.js} +2 -2
- package/public/assets/{formatted-body-impl-BXuR2gfL.js.map → formatted-body-impl-LK0QcFLC.js.map} +1 -1
- package/public/assets/{index-CAnJGSBl.js → index-CT10XH5Y.js} +8 -8
- package/public/assets/{index-CAnJGSBl.js.map → index-CT10XH5Y.js.map} +1 -1
- package/public/assets/index-Ryvn1efN.css +2 -0
- package/public/assets/{integrations-BBmL-PnQ.js → integrations-CdeQuRSQ.js} +2 -2
- package/public/assets/{integrations-BBmL-PnQ.js.map → integrations-CdeQuRSQ.js.map} +1 -1
- package/public/assets/{maintenance-B1bX9ety.js → maintenance-DbqSE_ps.js} +2 -2
- package/public/assets/{maintenance-B1bX9ety.js.map → maintenance-DbqSE_ps.js.map} +1 -1
- package/public/assets/{managed-agents-D12-nGKd.js → managed-agents-BxhRiBVn.js} +2 -2
- package/public/assets/{managed-agents-D12-nGKd.js.map → managed-agents-BxhRiBVn.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BWd3swt8.js → markdown-preview-impl-Dz2Spmat.js} +2 -2
- package/public/assets/{markdown-preview-impl-BWd3swt8.js.map → markdown-preview-impl-Dz2Spmat.js.map} +1 -1
- package/public/assets/{memory-4ERBFJBZ.js → memory-BTjUKTyA.js} +2 -2
- package/public/assets/{memory-4ERBFJBZ.js.map → memory-BTjUKTyA.js.map} +1 -1
- package/public/assets/{messages-B3ZES72c.js → messages-C8z05Y3O.js} +2 -2
- package/public/assets/{messages-B3ZES72c.js.map → messages-C8z05Y3O.js.map} +1 -1
- package/public/assets/{orchestrators-qKPntM-a.js → orchestrators-DQqtYryc.js} +2 -2
- package/public/assets/{orchestrators-qKPntM-a.js.map → orchestrators-DQqtYryc.js.map} +1 -1
- package/public/assets/{overview-ekmeyPaO.js → overview-CGYu6qQr.js} +2 -2
- package/public/assets/{overview-ekmeyPaO.js.map → overview-CGYu6qQr.js.map} +1 -1
- package/public/assets/pairs-BgqXzCto.js +2 -0
- package/public/assets/{pairs-B2QdTF0M.js.map → pairs-BgqXzCto.js.map} +1 -1
- package/public/assets/{security-WkMkEhb7.js → security-BqfRVw-p.js} +2 -2
- package/public/assets/{security-WkMkEhb7.js.map → security-BqfRVw-p.js.map} +1 -1
- package/public/assets/{settings-C5aXX3od.js → settings-63whklIq.js} +3 -3
- package/public/assets/{settings-C5aXX3od.js.map → settings-63whklIq.js.map} +1 -1
- package/public/assets/store-B6qT5_de.js +9 -0
- package/public/assets/store-B6qT5_de.js.map +1 -0
- package/public/assets/tasks-DiZ_4TYW.js +2 -0
- package/public/assets/{tasks-ppuVeXkO.js.map → tasks-DiZ_4TYW.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DXycIJKe.js → terminal-viewer-impl-D3T1F77z.js} +2 -2
- package/public/assets/{terminal-viewer-impl-DXycIJKe.js.map → terminal-viewer-impl-D3T1F77z.js.map} +1 -1
- package/public/assets/work-queue-C1p2Hybt.js +2 -0
- package/public/assets/{work-queue-C8UzeekT.js.map → work-queue-C1p2Hybt.js.map} +1 -1
- package/public/assets/{workspaces-BD1P6J21.js → workspaces-DqVr0uN7.js} +2 -2
- package/public/assets/{workspaces-BD1P6J21.js.map → workspaces-DqVr0uN7.js.map} +1 -1
- package/public/index.html +3 -3
- package/src/agent-ref.ts +7 -4
- package/src/channel-target.ts +1 -0
- package/src/db/agent-search.ts +6 -0
- package/src/db/agents.ts +11 -2
- package/src/db/channels.ts +2 -1
- package/src/db/delivery.ts +4 -1
- package/src/db/index.ts +1 -0
- package/src/db/mappers.ts +1 -1
- package/src/db/message-reads.ts +10 -1
- package/src/db/migrations.ts +54 -2
- package/src/db/plans.ts +17 -2
- package/src/db/schema.ts +24 -3
- package/src/db/teams.ts +151 -0
- package/src/mcp-plan-tools.ts +7 -3
- package/src/mcp-team-tools.ts +87 -0
- package/src/mcp.ts +6 -1
- package/src/routes/index.ts +4 -0
- package/src/routes/integrations.ts +2 -1
- package/src/routes/messages.ts +1 -1
- package/src/routes/plans.ts +9 -3
- package/src/routes/teams.ts +60 -0
- package/src/runtime-tokens.ts +9 -3
- package/src/security.ts +7 -1
- package/src/services/plan-graph.ts +17 -3
- package/src/services/register-agent.ts +3 -1
- package/src/services/spawn-agent.ts +4 -1
- package/src/services/team.ts +69 -0
- package/src/token-db.ts +3 -3
- package/src/validation.ts +2 -0
- package/public/assets/channels-CQy-JL7o.js +0 -2
- package/public/assets/chat-KaK5_x65.js +0 -2
- package/public/assets/chat-KaK5_x65.js.map +0 -1
- package/public/assets/index-DzRKGPXS.css +0 -2
- package/public/assets/pairs-B2QdTF0M.js +0 -2
- package/public/assets/store-CtKmqMfx.js +0 -9
- package/public/assets/store-CtKmqMfx.js.map +0 -1
- package/public/assets/tasks-ppuVeXkO.js +0 -2
- package/public/assets/work-queue-C8UzeekT.js +0 -2
package/src/db/delivery.ts
CHANGED
|
@@ -174,6 +174,10 @@ export function matchingDeliveryAgents(target: string): AgentCard[] {
|
|
|
174
174
|
const label = target.slice(6);
|
|
175
175
|
return candidates.filter((agent) => agent.label === label);
|
|
176
176
|
}
|
|
177
|
+
if (target.startsWith("team:")) {
|
|
178
|
+
const teamId = target.slice(5);
|
|
179
|
+
return candidates.filter((agent) => agent.teamId === teamId);
|
|
180
|
+
}
|
|
177
181
|
return [];
|
|
178
182
|
}
|
|
179
183
|
|
|
@@ -392,4 +396,3 @@ export function expireQueuedMessages(now: number = Date.now()): Message[] {
|
|
|
392
396
|
})();
|
|
393
397
|
return ids.map((id) => getMessage(id)).filter((message): message is Message => Boolean(message));
|
|
394
398
|
}
|
|
395
|
-
|
package/src/db/index.ts
CHANGED
package/src/db/mappers.ts
CHANGED
|
@@ -130,6 +130,7 @@ export function rowToAgent(row: any): AgentCard {
|
|
|
130
130
|
context: parseJson<ContextState | undefined>(row.context_state, undefined),
|
|
131
131
|
meta: parseJson(row.meta, {}),
|
|
132
132
|
spawnedBy: row.spawned_by ?? undefined,
|
|
133
|
+
teamId: row.team_id ?? undefined,
|
|
133
134
|
lastSeen: row.last_seen,
|
|
134
135
|
createdAt: row.created_at,
|
|
135
136
|
};
|
|
@@ -394,4 +395,3 @@ export const MSG_SELECT = `SELECT m.*, (
|
|
|
394
395
|
) AS reactions_json FROM messages m`;
|
|
395
396
|
|
|
396
397
|
// --- Agents ---
|
|
397
|
-
|
package/src/db/message-reads.ts
CHANGED
|
@@ -162,6 +162,7 @@ export function pollMessages(query: PollQuery): Message[] {
|
|
|
162
162
|
const agentTags = agent?.tags ?? [];
|
|
163
163
|
const agentCaps = agent?.capabilities ?? [];
|
|
164
164
|
const agentLabel = agent?.label;
|
|
165
|
+
const agentTeamId = agent?.teamId;
|
|
165
166
|
const agentLegacyChannelTargets = legacyChannelTargets(agent);
|
|
166
167
|
|
|
167
168
|
const conditions: string[] = [];
|
|
@@ -186,11 +187,19 @@ export function pollMessages(query: PollQuery): Message[] {
|
|
|
186
187
|
targetClauses.push("to_target = ?");
|
|
187
188
|
params.push(`label:${agentLabel}`);
|
|
188
189
|
}
|
|
190
|
+
if (agentTeamId) {
|
|
191
|
+
targetClauses.push("to_target = ?");
|
|
192
|
+
params.push(`team:${agentTeamId}`);
|
|
193
|
+
}
|
|
189
194
|
for (const target of agentLegacyChannelTargets) {
|
|
190
195
|
targetClauses.push("to_target = ?");
|
|
191
196
|
params.push(target);
|
|
192
197
|
}
|
|
193
198
|
conditions.push(`(${targetClauses.join(" OR ")})`);
|
|
199
|
+
if (agentTeamId) {
|
|
200
|
+
conditions.push("(from_agent != ? OR to_target != ?)");
|
|
201
|
+
params.push(query.for, `team:${agentTeamId}`);
|
|
202
|
+
}
|
|
194
203
|
conditions.push("delivery_status != 'queued' AND delivery_status NOT IN ('failed', 'dead')");
|
|
195
204
|
|
|
196
205
|
// Hide active claims held by someone else, but let expired claims surface so
|
|
@@ -323,7 +332,7 @@ export function markRead(messageId: number, agentId: string): boolean {
|
|
|
323
332
|
delivery_next_retry_at = NULL,
|
|
324
333
|
delivery_poison_reason = NULL,
|
|
325
334
|
delivery_updated_at = ?
|
|
326
|
-
WHERE id = ? AND (to_target = ? OR resolved_to_agent = ? OR to_target = 'broadcast' OR to_target LIKE 'tag:%' OR to_target LIKE 'cap:%' OR to_target LIKE 'label:%')
|
|
335
|
+
WHERE id = ? AND (to_target = ? OR resolved_to_agent = ? OR to_target = 'broadcast' OR to_target LIKE 'tag:%' OR to_target LIKE 'cap:%' OR to_target LIKE 'label:%' OR to_target LIKE 'team:%')
|
|
327
336
|
`).run(Date.now(), messageId, agentId, agentId);
|
|
328
337
|
return true;
|
|
329
338
|
}
|
package/src/db/migrations.ts
CHANGED
|
@@ -279,22 +279,70 @@ export function applyMigrations(): void {
|
|
|
279
279
|
// which runs per persisted session step. Added here (not the base schema) because the column
|
|
280
280
|
// is migration-added for pre-hardening DBs. Without it that lookup full-scans messages.
|
|
281
281
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_idempotency ON messages(from_agent, idempotency_key)");
|
|
282
|
+
getDb().run(`
|
|
283
|
+
CREATE TABLE IF NOT EXISTS teams (
|
|
284
|
+
id TEXT PRIMARY KEY,
|
|
285
|
+
coordinator_id TEXT,
|
|
286
|
+
root_coordinator_id TEXT,
|
|
287
|
+
title TEXT,
|
|
288
|
+
brief TEXT,
|
|
289
|
+
definition_of_done TEXT,
|
|
290
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
291
|
+
outcome_status TEXT,
|
|
292
|
+
outcome_summary TEXT,
|
|
293
|
+
composition TEXT,
|
|
294
|
+
cost_tokens INTEGER,
|
|
295
|
+
cost_usd REAL,
|
|
296
|
+
wall_clock_ms INTEGER,
|
|
297
|
+
lands_clean INTEGER,
|
|
298
|
+
created_at INTEGER NOT NULL,
|
|
299
|
+
updated_at INTEGER NOT NULL,
|
|
300
|
+
dissolved_at INTEGER
|
|
301
|
+
)
|
|
302
|
+
`);
|
|
303
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_teams_coordinator ON teams(coordinator_id)");
|
|
304
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_teams_status ON teams(status)");
|
|
282
305
|
getDb().run(`
|
|
283
306
|
CREATE TABLE IF NOT EXISTS plans (
|
|
284
|
-
id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
|
|
307
|
+
id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, team_id TEXT, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
|
|
285
308
|
title TEXT, objective TEXT, nodes TEXT NOT NULL DEFAULT '[]', edges TEXT NOT NULL DEFAULT '[]',
|
|
286
309
|
created_by TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
287
310
|
)
|
|
288
311
|
`);
|
|
312
|
+
const planCols = getDb().query("PRAGMA table_info(plans)").all() as any[];
|
|
313
|
+
const planColNames = planCols.map((c: any) => c.name);
|
|
314
|
+
if (!planColNames.includes("team_id")) {
|
|
315
|
+
getDb().run("ALTER TABLE plans ADD COLUMN team_id TEXT");
|
|
316
|
+
}
|
|
289
317
|
getDb().run("CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_thread_channel ON plans(thread_id, coalesce(channel, ''))");
|
|
290
318
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_plans_updated ON plans(updated_at)");
|
|
319
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_plans_team ON plans(team_id)");
|
|
291
320
|
getDb().run(`
|
|
292
321
|
CREATE TABLE IF NOT EXISTS plan_versions (
|
|
293
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id TEXT NOT NULL
|
|
322
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id TEXT NOT NULL,
|
|
294
323
|
version INTEGER NOT NULL, title TEXT, objective TEXT, nodes TEXT NOT NULL, edges TEXT NOT NULL,
|
|
295
324
|
changed_by TEXT, changed_at INTEGER NOT NULL
|
|
296
325
|
)
|
|
297
326
|
`);
|
|
327
|
+
const planVersionFks = getDb().query("PRAGMA foreign_key_list(plan_versions)").all() as any[];
|
|
328
|
+
if (planVersionFks.some((fk) => fk.table === "plans" && String(fk.on_delete).toUpperCase() === "CASCADE")) {
|
|
329
|
+
getDb().transaction(() => {
|
|
330
|
+
getDb().run("ALTER TABLE plan_versions RENAME TO plan_versions_old");
|
|
331
|
+
getDb().run(`
|
|
332
|
+
CREATE TABLE plan_versions (
|
|
333
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id TEXT NOT NULL,
|
|
334
|
+
version INTEGER NOT NULL, title TEXT, objective TEXT, nodes TEXT NOT NULL, edges TEXT NOT NULL,
|
|
335
|
+
changed_by TEXT, changed_at INTEGER NOT NULL
|
|
336
|
+
)
|
|
337
|
+
`);
|
|
338
|
+
getDb().run(`
|
|
339
|
+
INSERT INTO plan_versions (id, plan_id, version, title, objective, nodes, edges, changed_by, changed_at)
|
|
340
|
+
SELECT id, plan_id, version, title, objective, nodes, edges, changed_by, changed_at
|
|
341
|
+
FROM plan_versions_old
|
|
342
|
+
`);
|
|
343
|
+
getDb().run("DROP TABLE plan_versions_old");
|
|
344
|
+
})();
|
|
345
|
+
}
|
|
298
346
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_plan_versions_plan ON plan_versions(plan_id, version DESC)");
|
|
299
347
|
|
|
300
348
|
// Backfill thread_id for pre-migration rows (self-threaded).
|
|
@@ -372,6 +420,10 @@ export function applyMigrations(): void {
|
|
|
372
420
|
if (!agentColNames.includes("spawned_by")) {
|
|
373
421
|
getDb().run("ALTER TABLE agents ADD COLUMN spawned_by TEXT");
|
|
374
422
|
}
|
|
423
|
+
if (!agentColNames.includes("team_id")) {
|
|
424
|
+
getDb().run("ALTER TABLE agents ADD COLUMN team_id TEXT");
|
|
425
|
+
}
|
|
426
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_agents_team ON agents(team_id)");
|
|
375
427
|
getDb().run(`
|
|
376
428
|
CREATE TABLE IF NOT EXISTS context_snapshots (
|
|
377
429
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
package/src/db/plans.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Plan, PlanEdge, PlanNode } from "../types";
|
|
|
6
6
|
interface PlanRow {
|
|
7
7
|
id: string;
|
|
8
8
|
thread_id: number;
|
|
9
|
+
team_id: string | null;
|
|
9
10
|
channel: string | null;
|
|
10
11
|
version: number;
|
|
11
12
|
title: string | null;
|
|
@@ -49,6 +50,7 @@ function rowToPlan(row: PlanRow): Plan {
|
|
|
49
50
|
return {
|
|
50
51
|
id: row.id,
|
|
51
52
|
threadId: String(row.thread_id),
|
|
53
|
+
...(row.team_id ? { teamId: row.team_id } : {}),
|
|
52
54
|
...(row.channel ? { channel: row.channel } : {}),
|
|
53
55
|
version: row.version,
|
|
54
56
|
...(row.title ? { title: row.title } : {}),
|
|
@@ -82,6 +84,7 @@ export function planThreadExists(threadId: number): boolean {
|
|
|
82
84
|
export function createPlanRow(input: {
|
|
83
85
|
id?: string;
|
|
84
86
|
threadId: number;
|
|
87
|
+
teamId?: string;
|
|
85
88
|
channel?: string;
|
|
86
89
|
title?: string;
|
|
87
90
|
objective?: string;
|
|
@@ -93,11 +96,12 @@ export function createPlanRow(input: {
|
|
|
93
96
|
const now = Date.now();
|
|
94
97
|
try {
|
|
95
98
|
getDb().query(`
|
|
96
|
-
INSERT INTO plans (id, thread_id, channel, version, title, objective, nodes, edges, created_by, created_at, updated_at)
|
|
97
|
-
VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)
|
|
99
|
+
INSERT INTO plans (id, thread_id, team_id, channel, version, title, objective, nodes, edges, created_by, created_at, updated_at)
|
|
100
|
+
VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)
|
|
98
101
|
`).run(
|
|
99
102
|
id,
|
|
100
103
|
input.threadId,
|
|
104
|
+
input.teamId ?? null,
|
|
101
105
|
input.channel ?? null,
|
|
102
106
|
input.title ?? null,
|
|
103
107
|
input.objective ?? null,
|
|
@@ -128,6 +132,17 @@ export function getPlanByThread(threadId: number, channel?: string): Plan | null
|
|
|
128
132
|
return row ? rowToPlan(row) : null;
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
export function getPlanByTeam(teamId: string, channel?: string): Plan | null {
|
|
136
|
+
const row = channel === undefined
|
|
137
|
+
? getDb()
|
|
138
|
+
.query("SELECT * FROM plans WHERE team_id = ? ORDER BY updated_at DESC LIMIT 1")
|
|
139
|
+
.get(teamId) as PlanRow | undefined
|
|
140
|
+
: getDb()
|
|
141
|
+
.query("SELECT * FROM plans WHERE team_id = ? AND coalesce(channel, '') = ? ORDER BY updated_at DESC LIMIT 1")
|
|
142
|
+
.get(teamId, channel) as PlanRow | undefined;
|
|
143
|
+
return row ? rowToPlan(row) : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
131
146
|
export function replacePlan(input: Plan, changedBy?: string): Plan {
|
|
132
147
|
const existing = getPlan(input.id);
|
|
133
148
|
if (!existing) throw new ValidationError(`plan ${input.id} not found`);
|
package/src/db/schema.ts
CHANGED
|
@@ -106,10 +106,10 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
106
106
|
context_state TEXT,
|
|
107
107
|
meta TEXT NOT NULL DEFAULT '{}',
|
|
108
108
|
spawned_by TEXT,
|
|
109
|
+
team_id TEXT,
|
|
109
110
|
last_seen INTEGER NOT NULL,
|
|
110
111
|
created_at INTEGER NOT NULL
|
|
111
112
|
);
|
|
112
|
-
|
|
113
113
|
CREATE TABLE IF NOT EXISTS messages (
|
|
114
114
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
115
115
|
from_agent TEXT NOT NULL,
|
|
@@ -150,15 +150,36 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
150
150
|
-- per candidate row (O(n^2)): ~8.6s at 4k rows, which blew the 5s Stop-hook
|
|
151
151
|
-- timeout and wedged turns in "busy" (#199).
|
|
152
152
|
CREATE INDEX IF NOT EXISTS idx_msg_reply_to ON messages(reply_to, from_agent);
|
|
153
|
+
CREATE TABLE IF NOT EXISTS teams (
|
|
154
|
+
id TEXT PRIMARY KEY,
|
|
155
|
+
coordinator_id TEXT,
|
|
156
|
+
root_coordinator_id TEXT,
|
|
157
|
+
title TEXT,
|
|
158
|
+
brief TEXT,
|
|
159
|
+
definition_of_done TEXT,
|
|
160
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
161
|
+
outcome_status TEXT,
|
|
162
|
+
outcome_summary TEXT,
|
|
163
|
+
composition TEXT,
|
|
164
|
+
cost_tokens INTEGER,
|
|
165
|
+
cost_usd REAL,
|
|
166
|
+
wall_clock_ms INTEGER,
|
|
167
|
+
lands_clean INTEGER,
|
|
168
|
+
created_at INTEGER NOT NULL,
|
|
169
|
+
updated_at INTEGER NOT NULL,
|
|
170
|
+
dissolved_at INTEGER
|
|
171
|
+
);
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_teams_coordinator ON teams(coordinator_id);
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_teams_status ON teams(status);
|
|
153
174
|
CREATE TABLE IF NOT EXISTS plans (
|
|
154
|
-
id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
|
|
175
|
+
id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, team_id TEXT, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
|
|
155
176
|
title TEXT, objective TEXT, nodes TEXT NOT NULL DEFAULT '[]', edges TEXT NOT NULL DEFAULT '[]',
|
|
156
177
|
created_by TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
157
178
|
);
|
|
158
179
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_thread_channel ON plans(thread_id, coalesce(channel, ''));
|
|
159
180
|
CREATE INDEX IF NOT EXISTS idx_plans_updated ON plans(updated_at);
|
|
160
181
|
CREATE TABLE IF NOT EXISTS plan_versions (
|
|
161
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id TEXT NOT NULL
|
|
182
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id TEXT NOT NULL,
|
|
162
183
|
version INTEGER NOT NULL, title TEXT, objective TEXT, nodes TEXT NOT NULL, edges TEXT NOT NULL,
|
|
163
184
|
changed_by TEXT, changed_at INTEGER NOT NULL
|
|
164
185
|
);
|
package/src/db/teams.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getAgent, setAgentTeam } from "./agents.ts";
|
|
3
|
+
import { getDb, ValidationError } from "./connection.ts";
|
|
4
|
+
import { rowToAgent } from "./mappers.ts";
|
|
5
|
+
import type { AgentCard, Team } from "../types";
|
|
6
|
+
|
|
7
|
+
interface TeamRow {
|
|
8
|
+
id: string;
|
|
9
|
+
coordinator_id: string | null;
|
|
10
|
+
root_coordinator_id: string | null;
|
|
11
|
+
title: string | null;
|
|
12
|
+
brief: string | null;
|
|
13
|
+
definition_of_done: string | null;
|
|
14
|
+
status: Team["status"];
|
|
15
|
+
outcome_status: Team["outcomeStatus"] | null;
|
|
16
|
+
outcome_summary: string | null;
|
|
17
|
+
composition: string | null;
|
|
18
|
+
cost_tokens: number | null;
|
|
19
|
+
cost_usd: number | null;
|
|
20
|
+
wall_clock_ms: number | null;
|
|
21
|
+
lands_clean: number | null;
|
|
22
|
+
created_at: number;
|
|
23
|
+
updated_at: number;
|
|
24
|
+
dissolved_at: number | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TeamDissolveInput {
|
|
28
|
+
outcomeStatus?: Team["outcomeStatus"];
|
|
29
|
+
outcomeSummary?: string;
|
|
30
|
+
landsClean?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function teamId(): string {
|
|
34
|
+
return `team_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rowToTeam(row: TeamRow): Team {
|
|
38
|
+
return {
|
|
39
|
+
id: row.id,
|
|
40
|
+
...(row.coordinator_id ? { coordinatorId: row.coordinator_id } : {}),
|
|
41
|
+
...(row.root_coordinator_id ? { rootCoordinatorId: row.root_coordinator_id } : {}),
|
|
42
|
+
...(row.title ? { title: row.title } : {}),
|
|
43
|
+
...(row.brief ? { brief: row.brief } : {}),
|
|
44
|
+
...(row.definition_of_done ? { definitionOfDone: row.definition_of_done } : {}),
|
|
45
|
+
status: row.status,
|
|
46
|
+
...(row.outcome_status ? { outcomeStatus: row.outcome_status } : {}),
|
|
47
|
+
...(row.outcome_summary ? { outcomeSummary: row.outcome_summary } : {}),
|
|
48
|
+
...(row.composition ? { composition: row.composition } : {}),
|
|
49
|
+
...(row.cost_tokens !== null ? { costTokens: row.cost_tokens } : {}),
|
|
50
|
+
...(row.cost_usd !== null ? { costUsd: row.cost_usd } : {}),
|
|
51
|
+
...(row.wall_clock_ms !== null ? { wallClockMs: row.wall_clock_ms } : {}),
|
|
52
|
+
...(row.lands_clean !== null ? { landsClean: row.lands_clean === 1 } : {}),
|
|
53
|
+
createdAt: row.created_at,
|
|
54
|
+
updatedAt: row.updated_at,
|
|
55
|
+
...(row.dissolved_at !== null ? { dissolvedAt: row.dissolved_at } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getTeam(id: string): Team | null {
|
|
60
|
+
const row = getDb().query("SELECT * FROM teams WHERE id = ?").get(id) as TeamRow | undefined;
|
|
61
|
+
return row ? rowToTeam(row) : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getTeamRoster(teamId: string): AgentCard[] {
|
|
65
|
+
const rows = getDb()
|
|
66
|
+
.query("SELECT * FROM agents WHERE team_id = ? ORDER BY created_at ASC, id ASC")
|
|
67
|
+
.all(teamId) as any[];
|
|
68
|
+
return rows.map(rowToAgent);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function ensureTeam(coordinatorId: string): Team {
|
|
72
|
+
const coordinator = getAgent(coordinatorId);
|
|
73
|
+
if (!coordinator) throw new ValidationError(`coordinator agent ${coordinatorId} not found`);
|
|
74
|
+
if (coordinator.teamId) {
|
|
75
|
+
const existing = getTeam(coordinator.teamId);
|
|
76
|
+
if (existing) return existing;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const id = teamId();
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
getDb().transaction(() => {
|
|
82
|
+
getDb().query(`
|
|
83
|
+
INSERT INTO teams (id, coordinator_id, root_coordinator_id, status, created_at, updated_at)
|
|
84
|
+
VALUES (?, ?, ?, 'active', ?, ?)
|
|
85
|
+
`).run(id, coordinatorId, coordinatorId, now, now);
|
|
86
|
+
setAgentTeam(coordinatorId, id);
|
|
87
|
+
})();
|
|
88
|
+
return getTeam(id)!;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function setTeamBrief(input: {
|
|
92
|
+
teamId: string;
|
|
93
|
+
title?: string;
|
|
94
|
+
brief?: string;
|
|
95
|
+
definitionOfDone?: string;
|
|
96
|
+
}): Team {
|
|
97
|
+
const existing = getTeam(input.teamId);
|
|
98
|
+
if (!existing) throw new ValidationError(`team ${input.teamId} not found`);
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
getDb().query(`
|
|
101
|
+
UPDATE teams
|
|
102
|
+
SET title = ?, brief = ?, definition_of_done = ?, updated_at = ?
|
|
103
|
+
WHERE id = ?
|
|
104
|
+
`).run(
|
|
105
|
+
input.title ?? null,
|
|
106
|
+
input.brief ?? null,
|
|
107
|
+
input.definitionOfDone ?? null,
|
|
108
|
+
now,
|
|
109
|
+
input.teamId,
|
|
110
|
+
);
|
|
111
|
+
return getTeam(input.teamId)!;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function compositionForMembers(members: AgentCard[]): string {
|
|
115
|
+
return JSON.stringify(members.map((agent) => ({
|
|
116
|
+
id: agent.id,
|
|
117
|
+
label: agent.label,
|
|
118
|
+
role: typeof agent.meta?.role === "string" ? agent.meta.role : undefined,
|
|
119
|
+
provider: agent.providerCapabilities?.model?.provider,
|
|
120
|
+
model: agent.providerCapabilities?.model?.id ?? agent.providerCapabilities?.model?.alias,
|
|
121
|
+
})));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function dissolveTeam(teamId: string, input: TeamDissolveInput = {}): Team {
|
|
125
|
+
const existing = getTeam(teamId);
|
|
126
|
+
if (!existing) throw new ValidationError(`team ${teamId} not found`);
|
|
127
|
+
const members = getTeamRoster(teamId);
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
getDb().query(`
|
|
130
|
+
UPDATE teams
|
|
131
|
+
SET status = 'dissolved',
|
|
132
|
+
outcome_status = ?,
|
|
133
|
+
outcome_summary = ?,
|
|
134
|
+
composition = ?,
|
|
135
|
+
wall_clock_ms = ?,
|
|
136
|
+
lands_clean = ?,
|
|
137
|
+
dissolved_at = ?,
|
|
138
|
+
updated_at = ?
|
|
139
|
+
WHERE id = ?
|
|
140
|
+
`).run(
|
|
141
|
+
input.outcomeStatus ?? null,
|
|
142
|
+
input.outcomeSummary ?? null,
|
|
143
|
+
compositionForMembers(members),
|
|
144
|
+
Math.max(0, now - existing.createdAt),
|
|
145
|
+
input.landsClean === undefined ? null : input.landsClean ? 1 : 0,
|
|
146
|
+
now,
|
|
147
|
+
now,
|
|
148
|
+
teamId,
|
|
149
|
+
);
|
|
150
|
+
return getTeam(teamId)!;
|
|
151
|
+
}
|
package/src/mcp-plan-tools.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
addPlanEdge,
|
|
6
6
|
addPlanNode,
|
|
7
7
|
createPlan,
|
|
8
|
+
deletePlanGraph,
|
|
8
9
|
getPlanGraph,
|
|
9
10
|
removePlanEdge,
|
|
10
11
|
removePlanNode,
|
|
@@ -47,6 +48,7 @@ export const PLAN_TOOLS = [
|
|
|
47
48
|
type: "object",
|
|
48
49
|
properties: {
|
|
49
50
|
threadId: { oneOf: [{ type: "string" }, { type: "integer", minimum: 1 }] },
|
|
51
|
+
teamId: { type: "string" },
|
|
50
52
|
channel: { type: "string" },
|
|
51
53
|
title: { type: "string" },
|
|
52
54
|
objective: { type: "string" },
|
|
@@ -81,13 +83,14 @@ export const PLAN_TOOLS = [
|
|
|
81
83
|
{ name: "relay_plan_add_edge", description: "Add a dependency edge to a plan DAG; rejects cycles.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" }, from: { type: "string" }, to: { type: "string" } }, required: ["planId", "from", "to"], additionalProperties: false } },
|
|
82
84
|
{ name: "relay_plan_remove_node", description: "Remove a node and its connected edges from a plan DAG.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" }, nodeId: { type: "string" } }, required: ["planId", "nodeId"], additionalProperties: false } },
|
|
83
85
|
{ name: "relay_plan_remove_edge", description: "Remove a dependency edge from a plan DAG.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" }, from: { type: "string" }, to: { type: "string" } }, required: ["planId", "from", "to"], additionalProperties: false } },
|
|
86
|
+
{ name: "relay_plan_delete", description: "Delete a persisted plan DAG and archive its latest version.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" } }, required: ["planId"], additionalProperties: false } },
|
|
84
87
|
{
|
|
85
88
|
name: "relay_plan_get",
|
|
86
|
-
description: "Fetch a plan by planId or by threadId/channel.",
|
|
89
|
+
description: "Fetch a plan by planId, teamId, or by threadId/channel.",
|
|
87
90
|
requiredScopes: ["plans:read"],
|
|
88
91
|
inputSchema: {
|
|
89
92
|
type: "object",
|
|
90
|
-
properties: { planId: { type: "string" }, threadId: { oneOf: [{ type: "string" }, { type: "integer", minimum: 1 }] }, channel: { type: "string" } },
|
|
93
|
+
properties: { planId: { type: "string" }, teamId: { type: "string" }, threadId: { oneOf: [{ type: "string" }, { type: "integer", minimum: 1 }] }, channel: { type: "string" } },
|
|
91
94
|
additionalProperties: false,
|
|
92
95
|
},
|
|
93
96
|
},
|
|
@@ -111,8 +114,9 @@ export function relayPlanTool(auth: McpAuthLike, name: string, args: Record<stri
|
|
|
111
114
|
if (name === "relay_plan_add_edge") return addPlanEdge(stringArg(args.planId, "planId"), { from: args.from, to: args.to }, ctx) as unknown as Record<string, unknown>;
|
|
112
115
|
if (name === "relay_plan_remove_node") return removePlanNode(stringArg(args.planId, "planId"), stringArg(args.nodeId, "nodeId"), ctx) as unknown as Record<string, unknown>;
|
|
113
116
|
if (name === "relay_plan_remove_edge") return removePlanEdge(stringArg(args.planId, "planId"), { from: args.from, to: args.to }, ctx) as unknown as Record<string, unknown>;
|
|
117
|
+
if (name === "relay_plan_delete") return deletePlanGraph(stringArg(args.planId, "planId"), ctx);
|
|
114
118
|
if (name === "relay_plan_get") {
|
|
115
|
-
const plan = getPlanGraph({ planId: args.planId, threadId: args.threadId, channel: args.channel });
|
|
119
|
+
const plan = getPlanGraph({ planId: args.planId, teamId: args.teamId, threadId: args.threadId, channel: args.channel });
|
|
116
120
|
if (!plan) throw new McpNotFoundError("plan not found");
|
|
117
121
|
return plan as unknown as Record<string, unknown>;
|
|
118
122
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { McpAuthError, McpNotFoundError } from "./mcp-errors";
|
|
2
|
+
import { authContextFromMcp } from "./services/auth-context";
|
|
3
|
+
import { ValidationError } from "./db";
|
|
4
|
+
import { dissolveCallerTeam, getCallerTeam, setCallerTeamBrief, TeamAuthError, TeamNotFoundError } from "./services/team";
|
|
5
|
+
import { cleanString, optionalEnum } from "./validation";
|
|
6
|
+
import type { Team } from "./types";
|
|
7
|
+
|
|
8
|
+
const OUTCOME_STATUSES = ["success", "partial", "failed"] as const;
|
|
9
|
+
|
|
10
|
+
export const TEAM_TOOLS = [
|
|
11
|
+
{
|
|
12
|
+
name: "relay_team_get",
|
|
13
|
+
description: "Return the caller's team row, live roster, shared brief/definition of done, and any bound plan.",
|
|
14
|
+
requiredScopes: ["teams:read"],
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: { channel: { type: "string" } },
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "relay_team_set_brief",
|
|
23
|
+
description: "Create/update the caller's team brief, title, and definition of done. Lazily mints a team when needed.",
|
|
24
|
+
requiredScopes: ["teams:write"],
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
title: { type: "string" },
|
|
29
|
+
brief: { type: "string" },
|
|
30
|
+
definitionOfDone: { type: "string" },
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "relay_team_dissolve",
|
|
37
|
+
description: "Dissolve the caller's team and freeze the composition/outcome snapshot. Does not kill members.",
|
|
38
|
+
requiredScopes: ["teams:write"],
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
outcomeStatus: { type: "string", enum: OUTCOME_STATUSES },
|
|
43
|
+
outcomeSummary: { type: "string" },
|
|
44
|
+
landsClean: { type: "boolean" },
|
|
45
|
+
},
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
] satisfies Array<{ name: string; description: string; requiredScopes: string[]; inputSchema: Record<string, unknown> }>;
|
|
50
|
+
|
|
51
|
+
type McpAuthLike = Parameters<typeof authContextFromMcp>[0];
|
|
52
|
+
|
|
53
|
+
function mapTeamError(error: unknown): never {
|
|
54
|
+
if (error instanceof TeamAuthError) throw new McpAuthError(error.message);
|
|
55
|
+
if (error instanceof TeamNotFoundError) throw new McpNotFoundError(error.message);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function relayTeamTool(auth: McpAuthLike, name: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
60
|
+
const ctx = authContextFromMcp(auth);
|
|
61
|
+
try {
|
|
62
|
+
if (name === "relay_team_get") {
|
|
63
|
+
return getCallerTeam(ctx, cleanString(args.channel, "channel", { max: 120 })) as unknown as Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
if (name === "relay_team_set_brief") {
|
|
66
|
+
return setCallerTeamBrief(ctx, {
|
|
67
|
+
title: cleanString(args.title, "title", { max: 240 }),
|
|
68
|
+
brief: cleanString(args.brief, "brief", { max: 16_000 }),
|
|
69
|
+
definitionOfDone: cleanString(args.definitionOfDone, "definitionOfDone", { max: 4000 }),
|
|
70
|
+
}) as unknown as Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
if (name === "relay_team_dissolve") {
|
|
73
|
+
return dissolveCallerTeam(ctx, {
|
|
74
|
+
outcomeStatus: optionalEnum(args.outcomeStatus, "outcomeStatus", OUTCOME_STATUSES) as Team["outcomeStatus"] | undefined,
|
|
75
|
+
outcomeSummary: cleanString(args.outcomeSummary, "outcomeSummary", { max: 4000 }),
|
|
76
|
+
landsClean: args.landsClean === undefined || args.landsClean === null
|
|
77
|
+
? undefined
|
|
78
|
+
: typeof args.landsClean === "boolean"
|
|
79
|
+
? args.landsClean
|
|
80
|
+
: (() => { throw new ValidationError("landsClean must be a boolean"); })(),
|
|
81
|
+
}) as unknown as Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
mapTeamError(error);
|
|
85
|
+
}
|
|
86
|
+
throw new ValidationError(`unknown tool: ${name}`);
|
|
87
|
+
}
|
package/src/mcp.ts
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
import { sendMessageService } from "./services/send-message";
|
|
48
48
|
import { ServiceAuthError } from "./services/errors";
|
|
49
49
|
import { PLAN_TOOLS, relayPlanTool } from "./mcp-plan-tools";
|
|
50
|
+
import { TEAM_TOOLS, relayTeamTool } from "./mcp-team-tools";
|
|
50
51
|
import type { ActivityKind, ArtifactKind, ArtifactSensitivity, AttachmentRef, Command, SendMessageInput, Message, SpawnApprovalMode, SpawnProvider, WorkspaceAutoMergePolicy, WorkspaceMergeStrategy, WorkspaceMode, WorkspaceRecord } from "./types";
|
|
51
52
|
import { LAND_STRATEGIES, applyWorkspaceAction, waitForWorkspaceStatus, type WorkspaceAction } from "./workspace-actions";
|
|
52
53
|
import { AUTO_MERGE_POLICIES } from "./workspace-merge";
|
|
@@ -101,7 +102,7 @@ const TOOLS: ToolDefinition[] = [
|
|
|
101
102
|
type: "object",
|
|
102
103
|
properties: {
|
|
103
104
|
from: { type: "string", description: "Deprecated/optional. Your identity is taken from your auth token; ignored for managed agents. You never need to set this." },
|
|
104
|
-
to: { type: "string", description: "Target: an agent id, label, name, or a unique id segment (e.g. the number part) — resolved automatically. Or a fan-out selector: tag:, cap:, label:, policy:, or broadcast." },
|
|
105
|
+
to: { type: "string", description: "Target: an agent id, label, name, or a unique id segment (e.g. the number part) — resolved automatically. Or a fan-out selector: tag:, cap:, label:, team:, policy:, or broadcast." },
|
|
105
106
|
body: { type: "string" },
|
|
106
107
|
subject: { type: "string" },
|
|
107
108
|
channel: { type: "string" },
|
|
@@ -232,6 +233,7 @@ const TOOLS: ToolDefinition[] = [
|
|
|
232
233
|
kind: { type: ["string", "array"], items: { type: "string" } },
|
|
233
234
|
status: { type: ["string", "array"], items: { type: "string" }, description: "online/idle/busy/offline/stale, the 'running' shorthand, or 'all'. Default 'running'." },
|
|
234
235
|
spawnedBy: { type: "string", description: "Parent agent id, or 'me' for your own spawned children (#221)." },
|
|
236
|
+
teamId: { type: "string", description: "Durable team id for roster filtering." },
|
|
235
237
|
sortBy: { type: "string", enum: ["lastActive", "created", "label"] },
|
|
236
238
|
order: { type: "string", enum: ["asc", "desc"] },
|
|
237
239
|
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
@@ -456,6 +458,7 @@ const TOOLS: ToolDefinition[] = [
|
|
|
456
458
|
additionalProperties: false,
|
|
457
459
|
},
|
|
458
460
|
},
|
|
461
|
+
...TEAM_TOOLS,
|
|
459
462
|
...PLAN_TOOLS,
|
|
460
463
|
];
|
|
461
464
|
|
|
@@ -568,6 +571,7 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
|
|
|
568
571
|
else if (name === "relay_workspace_claim") result = relayWorkspaceMutation(auth, "claim", args);
|
|
569
572
|
else if (name === "relay_workspace_release") result = relayWorkspaceMutation(auth, "release-claim", args);
|
|
570
573
|
else if (name === "relay_workspace_land") result = relayWorkspaceMutation(auth, "merge", args);
|
|
574
|
+
else if (name.startsWith("relay_team_")) result = relayTeamTool(auth, name, args);
|
|
571
575
|
else if (name.startsWith("relay_plan_")) result = relayPlanTool(auth, name, args);
|
|
572
576
|
else throw new ValidationError(`unknown tool: ${name}`);
|
|
573
577
|
|
|
@@ -831,6 +835,7 @@ function relayFindAgents(auth: McpAuthContext, args: Record<string, unknown>): R
|
|
|
831
835
|
machine: optionalStringOrArray(args.machine, "machine"),
|
|
832
836
|
kind: optionalStringOrArray(args.kind, "kind"),
|
|
833
837
|
spawnedBy,
|
|
838
|
+
teamId: optionalString(args.teamId, "teamId", 120),
|
|
834
839
|
// Default to running — you can only act on a live agent (consistent with #218).
|
|
835
840
|
status: optionalStringOrArray(args.status, "status") ?? "running",
|
|
836
841
|
};
|
package/src/routes/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { getPlanByIdRoute, getPlansRoute } from "./plans";
|
|
|
30
30
|
import { getRecipeByName, getRecipeInstanceArtifacts, getRecipeInstanceById, getRecipeInstances, getRecipes, postRecipeInstanceArtifacts, postRecipeStart, postRecipeStop } from "./recipes";
|
|
31
31
|
import { getStewardConfigRoute, getWorkspaceConfigRoute, putStewardConfigRoute, putWorkspaceConfigRoute } from "./steward";
|
|
32
32
|
import { getTaskById, getTaskEvents, getTasks, patchTaskStatus, postClaimTask, postRenewTaskClaim } from "./tasks";
|
|
33
|
+
import { getMyTeamRoute, postMyTeamDissolveRoute, putMyTeamBriefRoute } from "./teams";
|
|
33
34
|
import { postMcp } from "../mcp";
|
|
34
35
|
import { postOrchestratorBootstrap, postOrchestratorBootstrapExchange } from "./orchestrator-bootstrap";
|
|
35
36
|
|
|
@@ -135,6 +136,9 @@ const routes: Route[] = [
|
|
|
135
136
|
route("DELETE", "/api/workspaces/:id", deleteWorkspaceById),
|
|
136
137
|
|
|
137
138
|
route("POST", "/api/system/broadcast", postSystemBroadcast),
|
|
139
|
+
route("GET", "/api/teams/me", getMyTeamRoute),
|
|
140
|
+
route("PUT", "/api/teams/me/brief", putMyTeamBriefRoute),
|
|
141
|
+
route("POST", "/api/teams/me/dissolve", postMyTeamDissolveRoute),
|
|
138
142
|
route("GET", "/api/recipes", getRecipes),
|
|
139
143
|
route("POST", "/api/recipes/start", postRecipeStart),
|
|
140
144
|
route("GET", "/api/recipes/instances", getRecipeInstances),
|
|
@@ -9,7 +9,7 @@ import { isRecord } from "agent-relay-sdk";
|
|
|
9
9
|
import { parseChannelRouteTarget } from "../channel-target.ts";
|
|
10
10
|
import { type ChannelBinding, type ChannelBindingMode, type ChannelRouteTarget, type ChannelSummary, type IntegrationEventInput, type IntegrationSummary } from "../types";
|
|
11
11
|
|
|
12
|
-
const VALID_CHANNEL_BINDING_TARGET_TYPES = ["agent", "label", "tag", "capability", "broadcast", "orchestrator", "pool", "policy"] as const;
|
|
12
|
+
const VALID_CHANNEL_BINDING_TARGET_TYPES = ["agent", "label", "team", "tag", "capability", "broadcast", "orchestrator", "pool", "policy"] as const;
|
|
13
13
|
|
|
14
14
|
const VALID_CHANNEL_BINDING_MODES = ["exclusive", "broadcast"] as const;
|
|
15
15
|
|
|
@@ -59,6 +59,7 @@ function messageTargetForChannelTarget(target: ChannelRouteTarget, binding?: Cha
|
|
|
59
59
|
return binding.poolAgentId;
|
|
60
60
|
}
|
|
61
61
|
if (target.type === "label") return `label:${target.id}`;
|
|
62
|
+
if (target.type === "team") return `team:${target.id}`;
|
|
62
63
|
if (target.type === "tag") return `tag:${target.id}`;
|
|
63
64
|
if (target.type === "capability") return `cap:${target.id}`;
|
|
64
65
|
if (target.type === "broadcast") return "broadcast";
|
package/src/routes/messages.ts
CHANGED
|
@@ -66,7 +66,7 @@ function normalizeMessageInput(body: unknown): SendMessageInput {
|
|
|
66
66
|
return input;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const FANOUT_PREFIXES = ["tag:", "cap:", "label:"];
|
|
69
|
+
const FANOUT_PREFIXES = ["tag:", "cap:", "label:", "team:"];
|
|
70
70
|
|
|
71
71
|
function isDirectTarget(to: string): boolean {
|
|
72
72
|
return to !== "broadcast" && !FANOUT_PREFIXES.some((p) => to.startsWith(p));
|
package/src/routes/plans.ts
CHANGED
|
@@ -5,10 +5,16 @@ import { error, json, parseQueryInt, type Handler } from "./_shared";
|
|
|
5
5
|
|
|
6
6
|
export const getPlansRoute: Handler = (req) => {
|
|
7
7
|
const url = new URL(req.url);
|
|
8
|
-
const threadId = parseQueryInt(url.searchParams.get("threadId"), { min: 1, max: Number.MAX_SAFE_INTEGER });
|
|
9
|
-
if (threadId === null) return error("threadId required");
|
|
10
|
-
if (Number.isNaN(threadId)) return error("threadId must be a positive integer");
|
|
11
8
|
const channel = cleanString(url.searchParams.get("channel") ?? undefined, "channel", { max: 120 });
|
|
9
|
+
const teamId = cleanString(url.searchParams.get("teamId") ?? undefined, "teamId", { max: 120 });
|
|
10
|
+
if (teamId) {
|
|
11
|
+
const plan = getPlanGraph({ teamId, channel });
|
|
12
|
+
return plan ? json(plan) : error("plan not found", 404);
|
|
13
|
+
}
|
|
14
|
+
const rawThreadId = url.searchParams.get("threadId");
|
|
15
|
+
if (rawThreadId === null) return error("threadId or teamId required");
|
|
16
|
+
const threadId = parseQueryInt(rawThreadId, { min: 1, max: Number.MAX_SAFE_INTEGER });
|
|
17
|
+
if (Number.isNaN(threadId)) return error("threadId must be a positive integer");
|
|
12
18
|
const plan = getPlanGraph({ threadId, channel });
|
|
13
19
|
return plan ? json(plan) : error("plan not found", 404);
|
|
14
20
|
};
|