agent-relay-server 0.106.0 → 0.108.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 +503 -1
- package/package.json +6 -5
- package/public/assets/{MessageBubble-CPYQQLDI.js → MessageBubble-DYJezSjU.js} +2 -2
- package/public/assets/{MessageBubble-CPYQQLDI.js.map → MessageBubble-DYJezSjU.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-B7xZt5DI.js → PlanGraphPanel-MWcVw38W.js} +2 -2
- package/public/assets/{PlanGraphPanel-B7xZt5DI.js.map → PlanGraphPanel-MWcVw38W.js.map} +1 -1
- package/public/assets/{activity-UKQrQ8_0.js → activity-Db3X3dg8.js} +2 -2
- package/public/assets/{activity-UKQrQ8_0.js.map → activity-Db3X3dg8.js.map} +1 -1
- package/public/assets/{agent-profiles-Zr_5nNEJ.js → agent-profiles-Di2w5U1o.js} +2 -2
- package/public/assets/{agent-profiles-Zr_5nNEJ.js.map → agent-profiles-Di2w5U1o.js.map} +1 -1
- package/public/assets/{agents-FdrfmJsj.js → agents-GSJXvmU1.js} +2 -2
- package/public/assets/{agents-FdrfmJsj.js.map → agents-GSJXvmU1.js.map} +1 -1
- package/public/assets/{analytics-VximqXQ9.js → analytics-B9USQLl5.js} +18 -18
- package/public/assets/{analytics-VximqXQ9.js.map → analytics-B9USQLl5.js.map} +1 -1
- package/public/assets/{automation-DlEcJvgu.js → automation-XRePs1dT.js} +2 -2
- package/public/assets/{automation-DlEcJvgu.js.map → automation-XRePs1dT.js.map} +1 -1
- package/public/assets/{branch-state-badge-CcLZ_u86.js → branch-state-badge-C_VNyER3.js} +2 -2
- package/public/assets/{branch-state-badge-CcLZ_u86.js.map → branch-state-badge-C_VNyER3.js.map} +1 -1
- package/public/assets/{channels-CcSnLzCG.js → channels-DACvNoC2.js} +2 -2
- package/public/assets/{channels-CcSnLzCG.js.map → channels-DACvNoC2.js.map} +1 -1
- package/public/assets/{chat-D1bunuI-.js → chat-D_0uHgB6.js} +2 -2
- package/public/assets/{chat-D1bunuI-.js.map → chat-D_0uHgB6.js.map} +1 -1
- package/public/assets/{connectors-DbPOlQhL.js → connectors-C0B38LUK.js} +2 -2
- package/public/assets/{connectors-DbPOlQhL.js.map → connectors-C0B38LUK.js.map} +1 -1
- package/public/assets/display-1J__Ti1K.js +3 -0
- package/public/assets/display-1J__Ti1K.js.map +1 -0
- package/public/assets/{formatted-body-impl-4v9SkUD1.js → formatted-body-impl-BKWCAjBd.js} +2 -2
- package/public/assets/{formatted-body-impl-4v9SkUD1.js.map → formatted-body-impl-BKWCAjBd.js.map} +1 -1
- package/public/assets/index-DlBSuzd2.js +23 -0
- package/public/assets/index-DlBSuzd2.js.map +1 -0
- package/public/assets/{insights-CntgHcTp.js → insights-BzNoYvDe.js} +2 -2
- package/public/assets/{insights-CntgHcTp.js.map → insights-BzNoYvDe.js.map} +1 -1
- package/public/assets/{integrations-hkSW8CzM.js → integrations-DLzJpt9G.js} +2 -2
- package/public/assets/{integrations-hkSW8CzM.js.map → integrations-DLzJpt9G.js.map} +1 -1
- package/public/assets/{maintenance-Dw7frQtu.js → maintenance-B6_0-ECm.js} +2 -2
- package/public/assets/{maintenance-Dw7frQtu.js.map → maintenance-B6_0-ECm.js.map} +1 -1
- package/public/assets/{managed-agents-Bj--bFmZ.js → managed-agents-CpzKXDtV.js} +2 -2
- package/public/assets/{managed-agents-Bj--bFmZ.js.map → managed-agents-CpzKXDtV.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-DGLCocU8.js → markdown-preview-impl-DxBnlJ2k.js} +2 -2
- package/public/assets/{markdown-preview-impl-DGLCocU8.js.map → markdown-preview-impl-DxBnlJ2k.js.map} +1 -1
- package/public/assets/{memory-yj2MIxUA.js → memory-Bnm7ILWL.js} +2 -2
- package/public/assets/{memory-yj2MIxUA.js.map → memory-Bnm7ILWL.js.map} +1 -1
- package/public/assets/{messages-BT5mL7iz.js → messages-D9LkocTc.js} +2 -2
- package/public/assets/{messages-BT5mL7iz.js.map → messages-D9LkocTc.js.map} +1 -1
- package/public/assets/{orchestrators-BVDAhrPk.js → orchestrators-C__JfrNr.js} +2 -2
- package/public/assets/{orchestrators-BVDAhrPk.js.map → orchestrators-C__JfrNr.js.map} +1 -1
- package/public/assets/{overview-DlqJIqYK.js → overview-BQ0b0V7Q.js} +2 -2
- package/public/assets/{overview-DlqJIqYK.js.map → overview-BQ0b0V7Q.js.map} +1 -1
- package/public/assets/{pairs-DQ4tNCo3.js → pairs-ChyeBs0Z.js} +2 -2
- package/public/assets/{pairs-DQ4tNCo3.js.map → pairs-ChyeBs0Z.js.map} +1 -1
- package/public/assets/{projects-vt63bwHN.js → projects-v5Kp1Oaq.js} +2 -2
- package/public/assets/{projects-vt63bwHN.js.map → projects-v5Kp1Oaq.js.map} +1 -1
- package/public/assets/{prompt-settings-DlsqOP0C.js → prompt-settings-YhAOlYc6.js} +2 -2
- package/public/assets/{prompt-settings-DlsqOP0C.js.map → prompt-settings-YhAOlYc6.js.map} +1 -1
- package/public/assets/{registry-Cw2fdWrn.js → registry-DZoAfrzx.js} +2 -2
- package/public/assets/{registry-Cw2fdWrn.js.map → registry-DZoAfrzx.js.map} +1 -1
- package/public/assets/{security-ucr7IT1V.js → security-DRCj00eA.js} +2 -2
- package/public/assets/{security-ucr7IT1V.js.map → security-DRCj00eA.js.map} +1 -1
- package/public/assets/{select-D8UWCtYZ.js → select-ZOpLw2Fd.js} +2 -2
- package/public/assets/{select-D8UWCtYZ.js.map → select-ZOpLw2Fd.js.map} +1 -1
- package/public/assets/settings-DGgwr9WH.js +6 -0
- package/public/assets/settings-DGgwr9WH.js.map +1 -0
- package/public/assets/{store-DUfrGvOE.js → store-Bnag1kY-.js} +2 -2
- package/public/assets/{store-DUfrGvOE.js.map → store-Bnag1kY-.js.map} +1 -1
- package/public/assets/{tasks-jRUvxUd5.js → tasks-DUTu8ZhA.js} +2 -2
- package/public/assets/{tasks-jRUvxUd5.js.map → tasks-DUTu8ZhA.js.map} +1 -1
- package/public/assets/{teams-Dn0yggD-.js → teams-BJpaoG2m.js} +2 -2
- package/public/assets/{teams-Dn0yggD-.js.map → teams-BJpaoG2m.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-fLly20IP.js → terminal-viewer-impl-CQhh6oXa.js} +2 -2
- package/public/assets/{terminal-viewer-impl-fLly20IP.js.map → terminal-viewer-impl-CQhh6oXa.js.map} +1 -1
- package/public/assets/types-BiJFuTiL.js +2 -0
- package/public/assets/types-BiJFuTiL.js.map +1 -0
- package/public/assets/{user-avatar-BWLUHt2e.js → user-avatar-BYGrUx1U.js} +2 -2
- package/public/assets/{user-avatar-BWLUHt2e.js.map → user-avatar-BYGrUx1U.js.map} +1 -1
- package/public/assets/{work-queue-BAnNY0xE.js → work-queue-BH2-mARc.js} +2 -2
- package/public/assets/{work-queue-BAnNY0xE.js.map → work-queue-BH2-mARc.js.map} +1 -1
- package/public/assets/{workspaces-CIsQqQby.js → workspaces-DldYyXZR.js} +2 -2
- package/public/assets/{workspaces-CIsQqQby.js.map → workspaces-DldYyXZR.js.map} +1 -1
- package/public/index.html +4 -4
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +6 -0
- package/src/branch-landed.ts +93 -4
- package/src/config-store.ts +12 -1
- package/src/db/agents.ts +3 -2
- package/src/db/continuations.ts +13 -3
- package/src/db/index.ts +3 -0
- package/src/db/issue-lifecycle.ts +12 -0
- package/src/db/mappers.ts +17 -0
- package/src/db/messages.ts +9 -4
- package/src/db/migrations.ts +91 -1
- package/src/db/notification-subscriptions.ts +1 -1
- package/src/db/prompt-templates.ts +19 -0
- package/src/db/provider-quotas.ts +6 -0
- package/src/db/schema.ts +67 -1
- package/src/db/stats.ts +3 -2
- package/src/db/task-comment-through.ts +112 -0
- package/src/db/task-core.ts +14 -1
- package/src/db/task-entity.ts +629 -0
- package/src/db/task-maintenance.ts +4 -3
- package/src/db/task-memory.ts +180 -0
- package/src/db/tasks.ts +24 -10
- package/src/issue-tracker/adapter.ts +15 -0
- package/src/issue-tracker/github.ts +40 -2
- package/src/issue-tracker/index.ts +1 -0
- package/src/mcp/index.ts +12 -1
- package/src/mcp/tools-spawn.ts +4 -0
- package/src/mcp-dispatch-tools.ts +164 -0
- package/src/mcp-notification-subscription-tools.ts +62 -4
- package/src/mcp-plan-tools.ts +30 -3
- package/src/mcp-release-tools.ts +33 -0
- package/src/mcp-task-memory-tools.ts +132 -0
- package/src/mcp-task-tools.ts +232 -0
- package/src/mcp-task-weave-tools.ts +66 -0
- package/src/notification-router.ts +24 -2
- package/src/notification-rules.ts +4 -0
- package/src/notification-types.ts +25 -0
- package/src/prompt-resolver.ts +66 -3
- package/src/routes/agents-spawn.ts +10 -0
- package/src/routes/commands.ts +17 -1
- package/src/routes/index.ts +16 -2
- package/src/routes/plans.ts +10 -2
- package/src/routes/provider-quotas.ts +13 -1
- package/src/routes/tasks.ts +276 -2
- package/src/routing-policy-store.ts +147 -2
- package/src/security.ts +2 -0
- package/src/self-resume.ts +13 -2
- package/src/services/issue-lifecycle.ts +9 -1
- package/src/services/plan-graph.ts +107 -9
- package/src/services/release-readiness.ts +142 -0
- package/src/services/send-message.ts +31 -0
- package/src/services/spawn-agent.ts +81 -4
- package/src/services/task-context-hydration.ts +361 -0
- package/src/services/task-entity.ts +149 -0
- package/src/services/task-issue-weave.ts +558 -0
- package/src/services/task-lifecycle.ts +465 -0
- package/src/services/task-plan-view.ts +178 -0
- package/src/services/task-semantic-events.ts +137 -0
- package/src/services/transient-agent-reaper.ts +71 -23
- package/src/spawn-targets.ts +27 -2
- package/src/task-dispatcher.ts +461 -0
- package/src/upgrade-install-verify.ts +20 -1
- package/public/assets/display-BWi8DQCp.js +0 -3
- package/public/assets/display-BWi8DQCp.js.map +0 -1
- package/public/assets/index-Vmhw442d.js +0 -23
- package/public/assets/index-Vmhw442d.js.map +0 -1
- package/public/assets/settings-Dvf_ps1B.js +0 -6
- package/public/assets/settings-Dvf_ps1B.js.map +0 -1
- package/public/assets/types-DorDDvEb.js +0 -2
- package/public/assets/types-DorDDvEb.js.map +0 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// #839 — MCP surface for the declarative task dispatcher.
|
|
2
|
+
//
|
|
3
|
+
// `relay_task_dispatch` is the EXPLICIT dispatch entrypoint (the seam the issue asks for):
|
|
4
|
+
// the coordinator declares a Task's work-shape and relay returns a concrete spawn
|
|
5
|
+
// (provider/model/effort) plus the rationale behind it. It does NOT spawn — that stays
|
|
6
|
+
// with the coordinator / #838 (spawn-with-task) — so wiring it can never perturb the live
|
|
7
|
+
// spawn path. When the policy's auto-review threshold is met it also creates the dependent
|
|
8
|
+
// review Task via the #834 dependency API (opt out with createReview:false).
|
|
9
|
+
import { McpNotFoundError } from "./mcp-errors";
|
|
10
|
+
import { ValidationError } from "./db";
|
|
11
|
+
import { authContextFromMcp } from "./services/auth-context";
|
|
12
|
+
import { addTaskEntityDependency, createTaskEntity, getTaskEntity } from "./services/task-entity";
|
|
13
|
+
import { dispatchTask, type DispatchPin } from "./task-dispatcher";
|
|
14
|
+
import { cleanString } from "./validation";
|
|
15
|
+
import {
|
|
16
|
+
isRecord,
|
|
17
|
+
isSpawnProvider,
|
|
18
|
+
SPAWN_PROVIDERS,
|
|
19
|
+
TASK_DISPATCH_COMPLEXITIES,
|
|
20
|
+
TASK_DISPATCH_CRITICALITIES,
|
|
21
|
+
TASK_DISPATCH_FOOTPRINTS,
|
|
22
|
+
TASK_DISPATCH_TYPES,
|
|
23
|
+
type TaskDispatch,
|
|
24
|
+
} from "./types";
|
|
25
|
+
|
|
26
|
+
const DISPATCH_SHAPE_SCHEMA = {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
complexity: { type: "string", enum: TASK_DISPATCH_COMPLEXITIES },
|
|
30
|
+
type: { type: "string", enum: TASK_DISPATCH_TYPES },
|
|
31
|
+
contextFootprint: { type: "string", enum: TASK_DISPATCH_FOOTPRINTS },
|
|
32
|
+
criticality: { type: "string", enum: TASK_DISPATCH_CRITICALITIES },
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
const PIN_SCHEMA = {
|
|
38
|
+
type: "object",
|
|
39
|
+
description: "Explicit per-case override; any field set wins over the policy-derived target.",
|
|
40
|
+
properties: {
|
|
41
|
+
provider: { type: "string", enum: SPAWN_PROVIDERS },
|
|
42
|
+
model: { type: "string" },
|
|
43
|
+
effort: { type: "string" },
|
|
44
|
+
},
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
const TASK_ID_PROP = { oneOf: [{ type: "integer", minimum: 1 }, { type: "string" }] } as const;
|
|
49
|
+
|
|
50
|
+
export const DISPATCH_TOOLS = [
|
|
51
|
+
{
|
|
52
|
+
name: "relay_task_dispatch",
|
|
53
|
+
description:
|
|
54
|
+
"Map a Task's work-shape ({complexity,type,contextFootprint,criticality}) + live quota/bands onto a concrete spawn (provider/model/effort) with a rationale. Reads a Task's dispatch field (taskId) and/or an inline dispatch override; overridable by an explicit model/provider pin. Returns the decision — it does NOT spawn (that's the coordinator / #838). When the policy's auto-review threshold is met it creates a dependent review Task (opt out with createReview:false).",
|
|
55
|
+
// Plans a dispatch (read) and may create a dependent review Task (write).
|
|
56
|
+
requiredScopes: ["task:read", "task:write"],
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
taskId: TASK_ID_PROP,
|
|
61
|
+
dispatch: DISPATCH_SHAPE_SCHEMA,
|
|
62
|
+
pin: PIN_SCHEMA,
|
|
63
|
+
createReview: { type: "boolean", description: "When a taskId is given and the auto-review threshold is met, create the dependent review Task. Default true." },
|
|
64
|
+
},
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
] satisfies Array<{ name: string; description: string; requiredScopes: string[]; inputSchema: Record<string, unknown> }>;
|
|
69
|
+
|
|
70
|
+
type McpAuthLike = Parameters<typeof authContextFromMcp>[0];
|
|
71
|
+
|
|
72
|
+
function taskIdArg(value: unknown): number {
|
|
73
|
+
const n = typeof value === "string" ? Number(value) : value;
|
|
74
|
+
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) throw new ValidationError("taskId must be a positive integer");
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pinArg(value: unknown): DispatchPin {
|
|
79
|
+
if (value === undefined || value === null) return {};
|
|
80
|
+
if (!isRecord(value)) throw new ValidationError("pin must be an object");
|
|
81
|
+
const pin: DispatchPin = {};
|
|
82
|
+
if (value.provider !== undefined) {
|
|
83
|
+
const provider = cleanString(value.provider, "pin.provider", { max: 80 });
|
|
84
|
+
if (provider) {
|
|
85
|
+
if (!isSpawnProvider(provider)) throw new ValidationError(`pin.provider must be one of: ${SPAWN_PROVIDERS.join(", ")}`);
|
|
86
|
+
pin.provider = provider;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const model = cleanString(value.model, "pin.model", { max: 200 });
|
|
90
|
+
if (model) pin.model = model;
|
|
91
|
+
const effort = cleanString(value.effort, "pin.effort", { max: 80 });
|
|
92
|
+
if (effort) pin.effort = effort;
|
|
93
|
+
return pin;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function dispatchShapeArg(value: unknown): TaskDispatch | undefined {
|
|
97
|
+
if (value === undefined || value === null) return undefined;
|
|
98
|
+
if (!isRecord(value)) throw new ValidationError("dispatch must be an object");
|
|
99
|
+
return value as TaskDispatch;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The first dependent Task of the build that is itself a review (dispatch.type='review'), if any.
|
|
103
|
+
// Makes auto-review creation idempotent across repeated dispatches of the same Task.
|
|
104
|
+
function findExistingReviewDependent(dependentIds: number[]): number | undefined {
|
|
105
|
+
for (const id of dependentIds) {
|
|
106
|
+
if (getTaskEntity(id)?.dispatch?.type === "review") return id;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function relayDispatchTool(auth: McpAuthLike, name: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
112
|
+
const actor = authContextFromMcp(auth).actor.id;
|
|
113
|
+
if (!isRecord(args)) throw new ValidationError("arguments must be an object");
|
|
114
|
+
if (name !== "relay_task_dispatch") throw new ValidationError(`unknown tool: ${name}`);
|
|
115
|
+
|
|
116
|
+
const hasTaskId = args.taskId !== undefined && args.taskId !== null;
|
|
117
|
+
const taskId = hasTaskId ? taskIdArg(args.taskId) : undefined;
|
|
118
|
+
const inlineShape = dispatchShapeArg(args.dispatch);
|
|
119
|
+
|
|
120
|
+
let parentTitle: string | undefined;
|
|
121
|
+
let parentTeamId: string | undefined;
|
|
122
|
+
let parentDependents: number[] = [];
|
|
123
|
+
let shape = inlineShape;
|
|
124
|
+
if (taskId !== undefined) {
|
|
125
|
+
const task = getTaskEntity(taskId);
|
|
126
|
+
if (!task) throw new McpNotFoundError("task not found");
|
|
127
|
+
parentTitle = task.title;
|
|
128
|
+
parentTeamId = task.teamId;
|
|
129
|
+
parentDependents = task.dependents;
|
|
130
|
+
// Inline dispatch (if supplied) overrides the stored work-shape for this call.
|
|
131
|
+
shape = inlineShape ?? task.dispatch;
|
|
132
|
+
}
|
|
133
|
+
if (!shape && !inlineShape) shape = {};
|
|
134
|
+
|
|
135
|
+
const decision = dispatchTask(shape, pinArg(args.pin));
|
|
136
|
+
|
|
137
|
+
let reviewTaskId: number | undefined;
|
|
138
|
+
const createReview = args.createReview !== false; // default true
|
|
139
|
+
if (decision.review && createReview && taskId !== undefined) {
|
|
140
|
+
// Idempotent: if this build Task already has a review dependent, reuse it rather than
|
|
141
|
+
// spawning a duplicate on every re-dispatch.
|
|
142
|
+
const existing = findExistingReviewDependent(parentDependents);
|
|
143
|
+
if (existing !== undefined) {
|
|
144
|
+
reviewTaskId = existing;
|
|
145
|
+
} else {
|
|
146
|
+
const review = createTaskEntity({
|
|
147
|
+
title: `Review: ${parentTitle ?? `task #${taskId}`}`,
|
|
148
|
+
intent: `Adversarial review of task #${taskId}${parentTitle ? ` — ${parentTitle}` : ""}. Triggered: ${decision.review.reason}.`,
|
|
149
|
+
dispatch: decision.review.dispatch,
|
|
150
|
+
createdBy: actor,
|
|
151
|
+
...(parentTeamId ? { teamId: parentTeamId } : {}),
|
|
152
|
+
});
|
|
153
|
+
// The review Task depends on the build Task (build must complete first).
|
|
154
|
+
addTaskEntityDependency(review.id, taskId, actor);
|
|
155
|
+
reviewTaskId = review.id;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...(taskId !== undefined ? { taskId } : {}),
|
|
161
|
+
decision: decision as unknown as Record<string, unknown>,
|
|
162
|
+
...(reviewTaskId !== undefined ? { reviewTaskId } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -16,11 +16,14 @@ import {
|
|
|
16
16
|
} from "./db/notification-subscriptions";
|
|
17
17
|
import { ValidationError } from "./db";
|
|
18
18
|
import { McpAuthError, McpNotFoundError } from "./mcp-errors";
|
|
19
|
-
import { authContextFromMcp } from "./services/auth-context";
|
|
19
|
+
import { authContextFromMcp, type AuthContext } from "./services/auth-context";
|
|
20
|
+
import { getTaskEntity } from "./services/task-entity";
|
|
20
21
|
import { cleanString } from "./validation";
|
|
21
22
|
import { NOTIFICATION_TYPES, type NotificationScope, type NotificationType } from "./notification-types";
|
|
22
23
|
import { previewNotificationRouting, whyNotified } from "./notification-router";
|
|
23
24
|
|
|
25
|
+
const TASK_NOTIFICATION_TYPES = ["task.created", "task.started", "task.blocked", "task.needs_decision", "task.landed", "task.closed"] as const satisfies readonly NotificationType[];
|
|
26
|
+
|
|
24
27
|
export const NOTIFICATION_SUBSCRIPTION_TOOLS = [
|
|
25
28
|
{
|
|
26
29
|
name: "relay_subscribe",
|
|
@@ -32,6 +35,7 @@ export const NOTIFICATION_SUBSCRIPTION_TOOLS = [
|
|
|
32
35
|
type: { type: "string", enum: NOTIFICATION_SUBSCRIPTION_TYPES },
|
|
33
36
|
mode: { type: "string", enum: NOTIFICATION_SUBSCRIPTION_MODES, description: "Default opt_in. mute and opt_out suppress matching default deliveries." },
|
|
34
37
|
scope: { type: "object", description: "Optional partial notification scope match, such as { teamId } or { repoRoot }." },
|
|
38
|
+
taskId: { type: "integer", minimum: 1, description: "Subscribe to semantic transitions for one durable Task." },
|
|
35
39
|
streams: { type: "array", items: { type: "string", enum: CHILD_STREAMS }, description: "Child streams to opt into. responses is always on and is not persisted." },
|
|
36
40
|
childAgentId: { type: "string", description: "Limit child stream subscriptions to one spawned child." },
|
|
37
41
|
teamId: { type: "string", description: "Limit child stream subscriptions to children on a team." },
|
|
@@ -51,6 +55,7 @@ export const NOTIFICATION_SUBSCRIPTION_TOOLS = [
|
|
|
51
55
|
type: { type: "string", enum: NOTIFICATION_SUBSCRIPTION_TYPES },
|
|
52
56
|
mode: { type: "string", enum: NOTIFICATION_SUBSCRIPTION_MODES },
|
|
53
57
|
scope: { type: "object" },
|
|
58
|
+
taskId: { type: "integer", minimum: 1 },
|
|
54
59
|
streams: { type: "array", items: { type: "string", enum: CHILD_STREAMS } },
|
|
55
60
|
childAgentId: { type: "string" },
|
|
56
61
|
teamId: { type: "string" },
|
|
@@ -105,7 +110,8 @@ export function relayNotificationSubscriptionTool(
|
|
|
105
110
|
name: string,
|
|
106
111
|
args: Record<string, unknown>,
|
|
107
112
|
): Record<string, unknown> {
|
|
108
|
-
const
|
|
113
|
+
const ctx = authContextFromMcp(auth);
|
|
114
|
+
const agentId = callerAgentId(ctx);
|
|
109
115
|
if (name === "relay_subscribe") {
|
|
110
116
|
if (Object.prototype.hasOwnProperty.call(args, "streams")) {
|
|
111
117
|
const scope = cleanChildStreamScope(args, agentId);
|
|
@@ -127,6 +133,17 @@ export function relayNotificationSubscriptionTool(
|
|
|
127
133
|
},
|
|
128
134
|
};
|
|
129
135
|
}
|
|
136
|
+
if (Object.prototype.hasOwnProperty.call(args, "taskId")) {
|
|
137
|
+
const scope = taskScope(ctx, args.taskId, args.scope);
|
|
138
|
+
const mode = cleanModeOrDefault(args.mode);
|
|
139
|
+
const subscriptions = TASK_NOTIFICATION_TYPES.map((type) => addSubscription({
|
|
140
|
+
agentId,
|
|
141
|
+
type,
|
|
142
|
+
mode,
|
|
143
|
+
scopeMatch: scope,
|
|
144
|
+
}));
|
|
145
|
+
return { subscriptions, taskFilter: scope };
|
|
146
|
+
}
|
|
130
147
|
if (args.type === undefined || args.type === null) throw new ValidationError("type or streams required");
|
|
131
148
|
const subscription = addSubscription({
|
|
132
149
|
agentId,
|
|
@@ -153,6 +170,22 @@ export function relayNotificationSubscriptionTool(
|
|
|
153
170
|
}
|
|
154
171
|
return { removed, responsesAlwaysOn: true };
|
|
155
172
|
}
|
|
173
|
+
if (Object.prototype.hasOwnProperty.call(args, "taskId")) {
|
|
174
|
+
const scope = taskScope(ctx, args.taskId, args.scope);
|
|
175
|
+
const types = args.type === undefined || args.type === null
|
|
176
|
+
? TASK_NOTIFICATION_TYPES
|
|
177
|
+
: [cleanSubscriptionType(args.type)];
|
|
178
|
+
let removed = 0;
|
|
179
|
+
for (const type of types) {
|
|
180
|
+
removed += removeSubscription({
|
|
181
|
+
agentId,
|
|
182
|
+
type,
|
|
183
|
+
mode: args.mode === undefined || args.mode === null ? undefined : cleanSubscriptionMode(args.mode),
|
|
184
|
+
scopeMatch: scope,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return { removed };
|
|
188
|
+
}
|
|
156
189
|
const type = args.type === undefined || args.type === null ? undefined : cleanSubscriptionType(args.type);
|
|
157
190
|
if (!id && !type) throw new ValidationError("id or type required");
|
|
158
191
|
const removed = removeSubscription({
|
|
@@ -191,8 +224,7 @@ export function relayNotificationSubscriptionTool(
|
|
|
191
224
|
throw new ValidationError(`unknown tool: ${name}`);
|
|
192
225
|
}
|
|
193
226
|
|
|
194
|
-
function callerAgentId(
|
|
195
|
-
const ctx = authContextFromMcp(auth);
|
|
227
|
+
function callerAgentId(ctx: AuthContext): string {
|
|
196
228
|
if (!ctx.callerAgentId) throw new McpAuthError("notification subscriptions require a single caller agent identity");
|
|
197
229
|
return ctx.callerAgentId;
|
|
198
230
|
}
|
|
@@ -222,6 +254,32 @@ function cleanMessageId(value: unknown): number {
|
|
|
222
254
|
return value;
|
|
223
255
|
}
|
|
224
256
|
|
|
257
|
+
function cleanPositiveInteger(value: unknown, field: string): number {
|
|
258
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
259
|
+
throw new ValidationError(`${field} must be a positive integer`);
|
|
260
|
+
}
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function taskScope(ctx: AuthContext, taskIdValue: unknown, explicitScope: unknown): Partial<NotificationScope> {
|
|
265
|
+
const taskId = cleanPositiveInteger(taskIdValue, "taskId");
|
|
266
|
+
assertTaskReadable(ctx, taskId);
|
|
267
|
+
const scope = cleanScope(explicitScope) ?? {};
|
|
268
|
+
if (scope.taskId && scope.taskId !== String(taskId)) throw new ValidationError("scope.taskId must match taskId");
|
|
269
|
+
return { ...scope, taskId: String(taskId) };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function assertTaskReadable(ctx: AuthContext, taskId: number): void {
|
|
273
|
+
if (!hasTaskReadScope(ctx)) throw new McpAuthError("task subscription requires task:read");
|
|
274
|
+
if (!getTaskEntity(taskId)) throw new McpNotFoundError("task not found");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function hasTaskReadScope(ctx: AuthContext): boolean {
|
|
278
|
+
const scopes = ctx.scopes ?? [];
|
|
279
|
+
if (scopes.includes("*") || scopes.includes("admin:*")) return true;
|
|
280
|
+
return scopes.includes("task:read") || scopes.includes("task:write") || scopes.includes("task:*");
|
|
281
|
+
}
|
|
282
|
+
|
|
225
283
|
function cleanChildStreams(value: unknown): ChildStream[] {
|
|
226
284
|
if (!Array.isArray(value) || value.length === 0) throw new ValidationError("streams must be a non-empty array");
|
|
227
285
|
return [...new Set(value.map(cleanChildStream))];
|
package/src/mcp-plan-tools.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { authContextFromMcp } from "./services/auth-context";
|
|
|
4
4
|
import {
|
|
5
5
|
addPlanEdge,
|
|
6
6
|
addPlanNode,
|
|
7
|
+
bindPlanNodeToTask,
|
|
7
8
|
createPlan,
|
|
8
9
|
deletePlanGraph,
|
|
9
10
|
getPlanGraph,
|
|
@@ -14,6 +15,16 @@ import {
|
|
|
14
15
|
import { isRecord } from "agent-relay-sdk";
|
|
15
16
|
|
|
16
17
|
const PLAN_STATUSES = ["pending", "in-progress", "landed", "blocked"] as const;
|
|
18
|
+
// #836: refs.taskId binds a plan node to a durable Task (the node then projects its
|
|
19
|
+
// label/scopeBlurb/status from that Task — no double-bookkeeping); refs.issueId links a GitHub issue.
|
|
20
|
+
const PLAN_NODE_REFS_SCHEMA = {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
taskId: { oneOf: [{ type: "string" }, { type: "integer", minimum: 1 }] },
|
|
24
|
+
issueId: { type: "string" },
|
|
25
|
+
},
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
} as const;
|
|
17
28
|
const PLAN_NODE_SCHEMA = {
|
|
18
29
|
type: "object",
|
|
19
30
|
properties: {
|
|
@@ -28,6 +39,7 @@ const PLAN_NODE_SCHEMA = {
|
|
|
28
39
|
lane: { type: "string" },
|
|
29
40
|
status: { type: "string", enum: PLAN_STATUSES },
|
|
30
41
|
statusSource: { type: "string", enum: ["manual", "bound"] },
|
|
42
|
+
refs: PLAN_NODE_REFS_SCHEMA,
|
|
31
43
|
blackboardRefs: { type: "array", items: { type: "string" } },
|
|
32
44
|
},
|
|
33
45
|
required: ["id", "label"],
|
|
@@ -76,6 +88,7 @@ export const PLAN_TOOLS = [
|
|
|
76
88
|
lane: { oneOf: [{ type: "string" }, { type: "null" }] },
|
|
77
89
|
status: { type: "string", enum: PLAN_STATUSES },
|
|
78
90
|
statusSource: { type: "string", enum: ["manual", "bound"] },
|
|
91
|
+
refs: { oneOf: [PLAN_NODE_REFS_SCHEMA, { type: "null" }] },
|
|
79
92
|
blackboardRefs: { oneOf: [{ type: "array", items: { type: "string" } }, { type: "null" }] },
|
|
80
93
|
},
|
|
81
94
|
required: ["planId", "nodeId"],
|
|
@@ -86,13 +99,19 @@ export const PLAN_TOOLS = [
|
|
|
86
99
|
{ 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 } },
|
|
87
100
|
{ 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 } },
|
|
88
101
|
{ 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 } },
|
|
102
|
+
{
|
|
103
|
+
name: "relay_plan_bind_node",
|
|
104
|
+
description: "Bind a plan node to a durable Task (#836). Pass taskId to link an existing deliverable Task, or omit it to mint one from the node's label/scopeBlurb/status. The bound node then projects its label/scopeBlurb/status from the Task — one object, no double-bookkeeping — and edges between bound nodes become task→task dependencies.",
|
|
105
|
+
requiredScopes: ["plans:write"],
|
|
106
|
+
inputSchema: { type: "object", properties: { planId: { type: "string" }, nodeId: { type: "string" }, taskId: { type: "integer", minimum: 1 } }, required: ["planId", "nodeId"], additionalProperties: false },
|
|
107
|
+
},
|
|
89
108
|
{
|
|
90
109
|
name: "relay_plan_get",
|
|
91
|
-
description: "Fetch a plan by planId, teamId, or by threadId/channel.",
|
|
110
|
+
description: "Fetch a plan by planId, teamId, or by threadId/channel. Bound nodes project from their Task. Pass view:'tasks' with a teamId to get the pure task-backed DAG synthesized from deliverable Tasks + task_dependencies (no plan row required).",
|
|
92
111
|
requiredScopes: ["plans:read"],
|
|
93
112
|
inputSchema: {
|
|
94
113
|
type: "object",
|
|
95
|
-
properties: { planId: { type: "string" }, teamId: { type: "string" }, threadId: { oneOf: [{ type: "string" }, { type: "integer", minimum: 1 }] }, channel: { type: "string" } },
|
|
114
|
+
properties: { planId: { type: "string" }, teamId: { type: "string" }, threadId: { oneOf: [{ type: "string" }, { type: "integer", minimum: 1 }] }, channel: { type: "string" }, view: { type: "string", enum: ["plan", "tasks"] } },
|
|
96
115
|
additionalProperties: false,
|
|
97
116
|
},
|
|
98
117
|
},
|
|
@@ -117,8 +136,16 @@ export function relayPlanTool(auth: McpAuthLike, name: string, args: Record<stri
|
|
|
117
136
|
if (name === "relay_plan_remove_node") return removePlanNode(stringArg(args.planId, "planId"), stringArg(args.nodeId, "nodeId"), ctx) as unknown as Record<string, unknown>;
|
|
118
137
|
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>;
|
|
119
138
|
if (name === "relay_plan_delete") return deletePlanGraph(stringArg(args.planId, "planId"), ctx);
|
|
139
|
+
if (name === "relay_plan_bind_node") {
|
|
140
|
+
return bindPlanNodeToTask(
|
|
141
|
+
stringArg(args.planId, "planId"),
|
|
142
|
+
stringArg(args.nodeId, "nodeId"),
|
|
143
|
+
{ ...(typeof args.taskId === "number" ? { taskId: args.taskId } : {}) },
|
|
144
|
+
ctx,
|
|
145
|
+
) as unknown as Record<string, unknown>;
|
|
146
|
+
}
|
|
120
147
|
if (name === "relay_plan_get") {
|
|
121
|
-
const plan = getPlanGraph({ planId: args.planId, teamId: args.teamId, threadId: args.threadId, channel: args.channel });
|
|
148
|
+
const plan = getPlanGraph({ planId: args.planId, teamId: args.teamId, threadId: args.threadId, channel: args.channel, view: args.view });
|
|
122
149
|
if (!plan) throw new McpNotFoundError("plan not found");
|
|
123
150
|
return plan as unknown as Record<string, unknown>;
|
|
124
151
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ValidationError } from "./db";
|
|
2
|
+
import { releaseReadinessReport } from "./services/release-readiness";
|
|
3
|
+
import { cleanString } from "./validation";
|
|
4
|
+
import { isRecord } from "./types";
|
|
5
|
+
|
|
6
|
+
export const RELEASE_TOOLS = [
|
|
7
|
+
{
|
|
8
|
+
name: "relay_release_readiness",
|
|
9
|
+
description: "Report landed-on-main deliverable Tasks that have not yet deployed/released, with linked issues, commit refs, and a conservative suggested release bump.",
|
|
10
|
+
requiredScopes: ["task:read"],
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
teamId: { type: "string" },
|
|
15
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
16
|
+
},
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
] satisfies Array<{ name: string; description: string; requiredScopes: string[]; inputSchema: Record<string, unknown> }>;
|
|
21
|
+
|
|
22
|
+
export function relayReleaseTool(name: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
23
|
+
if (name !== "relay_release_readiness") throw new ValidationError(`unknown tool: ${name}`);
|
|
24
|
+
if (!isRecord(args)) throw new ValidationError("arguments must be an object");
|
|
25
|
+
const limit = args.limit === undefined ? undefined : Number(args.limit);
|
|
26
|
+
if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)) {
|
|
27
|
+
throw new ValidationError("limit must be between 1 and 500");
|
|
28
|
+
}
|
|
29
|
+
return releaseReadinessReport({
|
|
30
|
+
teamId: cleanString(args.teamId, "teamId", { max: 200 }),
|
|
31
|
+
limit,
|
|
32
|
+
}) as unknown as Record<string, unknown>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { stringValue } from "agent-relay-sdk";
|
|
2
|
+
import {
|
|
3
|
+
appendTaskMemoryDecision,
|
|
4
|
+
appendTaskMemoryRuledOut,
|
|
5
|
+
deliverableTaskIdsByOwnerAgent,
|
|
6
|
+
deliverableTaskIdsBySpawnRequestId,
|
|
7
|
+
getAgent,
|
|
8
|
+
readTaskMemoryEnvelope,
|
|
9
|
+
replaceTaskMemoryWorkingState,
|
|
10
|
+
ValidationError,
|
|
11
|
+
} from "./db";
|
|
12
|
+
import { McpAuthError } from "./mcp-errors";
|
|
13
|
+
import { callerAgentId } from "./mcp/identity";
|
|
14
|
+
import { optionalPositiveInt, stringField } from "./mcp/validation";
|
|
15
|
+
import { renderContinuation } from "./self-resume";
|
|
16
|
+
import type { McpAuthContext, ToolDefinition } from "./mcp/types";
|
|
17
|
+
|
|
18
|
+
const TASK_ID_PROP = { type: "integer", minimum: 1, description: "Task id. Optional when the caller is bound to exactly one Task." } as const;
|
|
19
|
+
|
|
20
|
+
export const TASK_MEMORY_TOOLS = [
|
|
21
|
+
{
|
|
22
|
+
name: "relay_task_memory_read",
|
|
23
|
+
description: "Read the task-bound continuation envelope for a durable Task. If taskId is omitted, resolves the caller's bound Task; if provided, the Task must still be bound to the caller unless the caller has an admin scope.",
|
|
24
|
+
requiredScopes: ["task:read"],
|
|
25
|
+
inputSchema: { type: "object", properties: { taskId: TASK_ID_PROP }, additionalProperties: false },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "relay_task_memory_replace_working_state",
|
|
29
|
+
description: "Replace the caller-bound Task's current working-state memory. This is replaceable; later writes overwrite it.",
|
|
30
|
+
requiredScopes: ["task:write"],
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: { taskId: TASK_ID_PROP, workingState: { type: "string" } },
|
|
34
|
+
required: ["workingState"],
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "relay_task_memory_append_ruled_out",
|
|
40
|
+
description: "Append dead ends to the caller-bound Task's ruled-out ledger. Append-only: existing entries cannot be edited through this tool.",
|
|
41
|
+
requiredScopes: ["task:write"],
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
taskId: TASK_ID_PROP,
|
|
46
|
+
ruledOut: {
|
|
47
|
+
type: "array",
|
|
48
|
+
items: {
|
|
49
|
+
oneOf: [
|
|
50
|
+
{ type: "string" },
|
|
51
|
+
{ type: "object", properties: { ruledOut: { type: "string" }, why: { type: "string" } }, required: ["ruledOut"], additionalProperties: false },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
required: ["ruledOut"],
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "relay_task_memory_append_decision",
|
|
62
|
+
description: "Append a decision to the caller-bound Task's decision log. Append-only: use this for durable choices a retry agent must inherit.",
|
|
63
|
+
requiredScopes: ["task:write"],
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: { taskId: TASK_ID_PROP, decision: { type: "string" }, rationale: { type: "string" } },
|
|
67
|
+
required: ["decision"],
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
] satisfies ToolDefinition[];
|
|
72
|
+
|
|
73
|
+
export function relayTaskMemoryTool(auth: McpAuthContext, name: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
74
|
+
const taskId = resolveTaskId(auth, args);
|
|
75
|
+
if (name === "relay_task_memory_read") return response(readTaskMemoryEnvelope(taskId));
|
|
76
|
+
if (name === "relay_task_memory_replace_working_state") {
|
|
77
|
+
return response(replaceTaskMemoryWorkingState({
|
|
78
|
+
taskId,
|
|
79
|
+
workingState: stringField(args.workingState, "workingState", { required: true, maxBytes: 1_000_000 }),
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
if (name === "relay_task_memory_append_ruled_out") {
|
|
83
|
+
if (!Array.isArray(args.ruledOut)) throw new ValidationError("ruledOut must be an array");
|
|
84
|
+
return response(appendTaskMemoryRuledOut({ taskId, entries: args.ruledOut as Array<string | { ruledOut: string; why?: string }> }));
|
|
85
|
+
}
|
|
86
|
+
if (name === "relay_task_memory_append_decision") {
|
|
87
|
+
return response(appendTaskMemoryDecision({
|
|
88
|
+
taskId,
|
|
89
|
+
decision: stringField(args.decision, "decision", { required: true, maxBytes: 64_000 }),
|
|
90
|
+
rationale: stringField(args.rationale, "rationale", { maxBytes: 64_000 }),
|
|
91
|
+
by: callerAgentId(auth) ?? auth.actor,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
throw new ValidationError(`unknown tool: ${name}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveTaskId(auth: McpAuthContext, args: Record<string, unknown>): number {
|
|
98
|
+
const explicit = optionalPositiveInt(args.taskId, "taskId");
|
|
99
|
+
const ids = boundTaskIds(auth);
|
|
100
|
+
if (explicit !== undefined) {
|
|
101
|
+
// Deliberate privileged escape hatch: admin/server tooling can inspect or repair any
|
|
102
|
+
// task memory row, but ordinary provider tokens must be bound to the explicit task id.
|
|
103
|
+
if (hasTaskMemoryAdminScope(auth) || ids.has(explicit)) return explicit;
|
|
104
|
+
throw new McpAuthError("taskId is not bound to the caller");
|
|
105
|
+
}
|
|
106
|
+
if (ids.size === 1) return [...ids][0]!;
|
|
107
|
+
if (ids.size === 0) throw new McpAuthError("caller is not bound to a Task");
|
|
108
|
+
throw new McpAuthError("caller is bound to multiple Tasks; pass taskId explicitly");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function boundTaskIds(auth: McpAuthContext): Set<number> {
|
|
112
|
+
const agentId = callerAgentId(auth);
|
|
113
|
+
if (!agentId) return new Set();
|
|
114
|
+
const ids = new Set<number>(deliverableTaskIdsByOwnerAgent(agentId));
|
|
115
|
+
const spawnRequestId = stringValue(getAgent(agentId)?.meta?.spawnRequestId);
|
|
116
|
+
if (spawnRequestId) {
|
|
117
|
+
for (const id of deliverableTaskIdsBySpawnRequestId(spawnRequestId)) ids.add(id);
|
|
118
|
+
}
|
|
119
|
+
return ids;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hasTaskMemoryAdminScope(auth: McpAuthContext): boolean {
|
|
123
|
+
return auth.scopes.includes("*") || auth.scopes.includes("admin:*") || auth.scopes.includes("system:admin") || auth.scopes.includes("task:admin");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function response(envelope: ReturnType<typeof readTaskMemoryEnvelope>): Record<string, unknown> {
|
|
127
|
+
return {
|
|
128
|
+
taskId: envelope.taskId,
|
|
129
|
+
memory: envelope,
|
|
130
|
+
rendered: renderContinuation(envelope, { intro: `Task-bound memory — generation ${envelope.generation}.` }),
|
|
131
|
+
};
|
|
132
|
+
}
|