letagents 0.12.8 → 0.12.10

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.
Files changed (116) hide show
  1. package/README.md +16 -15
  2. package/dist/mcp/agent-prompt-delivery.js +20 -0
  3. package/dist/mcp/codex-session/app-server.js +89 -0
  4. package/dist/mcp/codex-session/index.js +10 -0
  5. package/dist/mcp/codex-session/rpc-client.js +85 -0
  6. package/dist/mcp/codex-session/runtime-bridge.js +254 -0
  7. package/dist/mcp/codex-session/runtime-summary.js +220 -0
  8. package/dist/mcp/codex-session/session-inspection.js +100 -0
  9. package/dist/mcp/codex-session/session-mapper.js +51 -0
  10. package/dist/mcp/codex-session/session-start.js +182 -0
  11. package/dist/mcp/codex-session/session-status.js +77 -0
  12. package/dist/mcp/codex-session/session-stop.js +43 -0
  13. package/dist/mcp/codex-session/session-supervisor.js +111 -0
  14. package/dist/mcp/codex-session/start-prompt.js +49 -0
  15. package/dist/mcp/codex-session/types.js +1 -0
  16. package/dist/mcp/codex-session.js +1 -500
  17. package/dist/mcp/config-reader.js +10 -2
  18. package/dist/mcp/git-remote.js +90 -14
  19. package/dist/mcp/local-state/agent-identity.js +26 -0
  20. package/dist/mcp/local-state/agent-sessions.js +66 -0
  21. package/dist/mcp/local-state/auth.js +57 -0
  22. package/dist/mcp/local-state/codex-live-sessions.js +59 -0
  23. package/dist/mcp/local-state/index.js +8 -0
  24. package/dist/mcp/local-state/local-chat.js +808 -0
  25. package/dist/mcp/local-state/room-sessions.js +51 -0
  26. package/dist/mcp/local-state/storage.js +95 -0
  27. package/dist/mcp/local-state/types.js +1 -0
  28. package/dist/mcp/local-state.js +1 -286
  29. package/dist/mcp/managed-agent-providers.js +65 -0
  30. package/dist/mcp/rental-tools/activity.js +37 -0
  31. package/dist/mcp/rental-tools/budget.js +36 -0
  32. package/dist/mcp/rental-tools/context.js +62 -0
  33. package/dist/mcp/rental-tools/lifecycle.js +47 -0
  34. package/dist/mcp/rental-tools/metering.js +73 -0
  35. package/dist/mcp/rental-tools/patches.js +100 -0
  36. package/dist/mcp/rental-tools/provider.js +117 -0
  37. package/dist/mcp/rental-tools/shared.js +19 -0
  38. package/dist/mcp/rental-tools/types.js +1 -0
  39. package/dist/mcp/rental-tools.js +7 -0
  40. package/dist/mcp/repo-visibility.js +8 -6
  41. package/dist/mcp/server/register-tools.js +19 -0
  42. package/dist/mcp/server/repo-context.js +28 -0
  43. package/dist/mcp/server/resources.js +77 -0
  44. package/dist/mcp/server/runtime/agent-sessions.js +134 -0
  45. package/dist/mcp/server/runtime/api.js +82 -0
  46. package/dist/mcp/server/runtime/auth-cache.js +20 -0
  47. package/dist/mcp/server/runtime/device-auth.js +48 -0
  48. package/dist/mcp/server/runtime/identity/config.js +58 -0
  49. package/dist/mcp/server/runtime/identity/directory.js +87 -0
  50. package/dist/mcp/server/runtime/identity/liveness.js +32 -0
  51. package/dist/mcp/server/runtime/identity/names.js +74 -0
  52. package/dist/mcp/server/runtime/identity/public.js +19 -0
  53. package/dist/mcp/server/runtime/identity/state.js +32 -0
  54. package/dist/mcp/server/runtime/identity.js +96 -0
  55. package/dist/mcp/server/runtime/messages.js +160 -0
  56. package/dist/mcp/server/runtime/presence.js +52 -0
  57. package/dist/mcp/server/runtime/room-api.js +42 -0
  58. package/dist/mcp/server/runtime/room-state.js +122 -0
  59. package/dist/mcp/server/runtime/rooms.js +318 -0
  60. package/dist/mcp/server/runtime.js +14 -0
  61. package/dist/mcp/server/tools/agent-sessions.js +374 -0
  62. package/dist/mcp/server/tools/messages/index.js +14 -0
  63. package/dist/mcp/server/tools/messages/read-tool.js +176 -0
  64. package/dist/mcp/server/tools/messages/reasoning-tool.js +179 -0
  65. package/dist/mcp/server/tools/messages/response.js +17 -0
  66. package/dist/mcp/server/tools/messages/send-tool.js +203 -0
  67. package/dist/mcp/server/tools/messages/status-tool.js +87 -0
  68. package/dist/mcp/server/tools/messages/wait-tool.js +333 -0
  69. package/dist/mcp/server/tools/messages.js +1 -0
  70. package/dist/mcp/server/tools/onboarding/device-auth-tools.js +148 -0
  71. package/dist/mcp/server/tools/onboarding/name-tool.js +89 -0
  72. package/dist/mcp/server/tools/onboarding/responses.js +10 -0
  73. package/dist/mcp/server/tools/onboarding/status-tool.js +108 -0
  74. package/dist/mcp/server/tools/onboarding.js +8 -0
  75. package/dist/mcp/server/tools/rental/activity-tools.js +43 -0
  76. package/dist/mcp/server/tools/rental/context-tools.js +35 -0
  77. package/dist/mcp/server/tools/rental/index.js +17 -0
  78. package/dist/mcp/server/tools/rental/metering-tools.js +39 -0
  79. package/dist/mcp/server/tools/rental/patch-tools.js +53 -0
  80. package/dist/mcp/server/tools/rental/provider-tools.js +44 -0
  81. package/dist/mcp/server/tools/rental/response.js +10 -0
  82. package/dist/mcp/server/tools/rental/types.js +1 -0
  83. package/dist/mcp/server/tools/rental.js +1 -0
  84. package/dist/mcp/server/tools/rooms/index.js +5 -0
  85. package/dist/mcp/server/tools/rooms/inspection-tools.js +124 -0
  86. package/dist/mcp/server/tools/rooms/join-tools.js +37 -0
  87. package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +113 -0
  88. package/dist/mcp/server/tools/rooms/repo-visibility-tool.js +20 -0
  89. package/dist/mcp/server/tools/rooms/response.js +10 -0
  90. package/dist/mcp/server/tools/rooms/resume-tool.js +48 -0
  91. package/dist/mcp/server/tools/rooms.js +1 -0
  92. package/dist/mcp/server/tools/tasks/api.js +222 -0
  93. package/dist/mcp/server/tools/tasks/artifact-tools.js +75 -0
  94. package/dist/mcp/server/tools/tasks/board-intent-tools.js +321 -0
  95. package/dist/mcp/server/tools/tasks/board-tools.js +73 -0
  96. package/dist/mcp/server/tools/tasks/context.js +35 -0
  97. package/dist/mcp/server/tools/tasks/event-tools.js +40 -0
  98. package/dist/mcp/server/tools/tasks/index.js +14 -0
  99. package/dist/mcp/server/tools/tasks/lease-tools.js +145 -0
  100. package/dist/mcp/server/tools/tasks/mutation-tools.js +121 -0
  101. package/dist/mcp/server/tools/tasks/response.js +13 -0
  102. package/dist/mcp/server/tools/tasks/schemas.js +68 -0
  103. package/dist/mcp/server/tools/tasks.js +1 -0
  104. package/dist/mcp/server.js +10 -2540
  105. package/dist/mcp/sse-client.js +22 -16
  106. package/dist/shared/activation-routing.js +263 -0
  107. package/dist/shared/agent-identity.js +21 -5
  108. package/dist/shared/agent-presence.js +24 -0
  109. package/dist/shared/handoff.js +145 -0
  110. package/dist/shared/rental/lrt.js +214 -0
  111. package/dist/shared/rental/meter-types.js +14 -0
  112. package/dist/shared/request-headers.js +3 -0
  113. package/dist/shared/room-agent-activity.js +41 -0
  114. package/dist/shared/room-agent-prompts.js +14 -1
  115. package/package.json +15 -5
  116. /package/dist/{mcp → shared}/codenames.js +0 -0
@@ -0,0 +1,321 @@
1
+ import { z } from "zod";
2
+ import { deleteActiveBoardManager, getCanonicalTask, getBoardIntents, getBoardSettings, patchBoardSettings, postBoardIntent, postBoardIntentDecision, postBoardManager, } from "./api.js";
3
+ import { resolveCanonicalRoomId, resolveCanonicalTaskToolIdentity, taskActorPayload, } from "./context.js";
4
+ import { jsonToolResponse, taskToolError } from "./response.js";
5
+ import { workerTaskIdentitySchema } from "./schemas.js";
6
+ import { boardIntentPayloadForLeaseAction, boardIntentPayloadForTaskCreate, boardIntentPayloadForTaskMutation, } from "../../../../api/board-intent-payloads.js";
7
+ const boardManagerModeSchema = z.enum(["off", "manager_optional", "intent_required"]);
8
+ const boardManagerRuntimeSourceSchema = z.enum(["desktop_managed", "open_model", "external", "unknown"]);
9
+ const boardIntentActionTypeSchema = z.enum([
10
+ "task_create",
11
+ "task_claim",
12
+ "task_close",
13
+ "task_override",
14
+ "task_update",
15
+ ]);
16
+ const boardIntentPayloadSchema = z.record(z.string(), z.unknown());
17
+ const boardIntentCloseStatusSchema = z.enum(["merged", "done", "cancelled"]);
18
+ async function registerTypedBoardIntent(input) {
19
+ const { identity, agentSession } = await resolveCanonicalTaskToolIdentity(input.roomId, input.agentSessionId ?? undefined);
20
+ const result = await postBoardIntent(input.roomId, {
21
+ action_type: input.actionType,
22
+ payload: input.payload,
23
+ task_id: input.taskId ?? undefined,
24
+ ...taskActorPayload(identity, agentSession),
25
+ });
26
+ return jsonToolResponse({
27
+ success: true,
28
+ ...responseObject(result),
29
+ }, 2);
30
+ }
31
+ function responseObject(value) {
32
+ return value && typeof value === "object" && !Array.isArray(value)
33
+ ? value
34
+ : {};
35
+ }
36
+ function activeWorkLeaseIdFromTask(value) {
37
+ const task = responseObject(value);
38
+ const leases = Array.isArray(task.active_leases)
39
+ ? task.active_leases
40
+ : Array.isArray(task.activeLeases)
41
+ ? task.activeLeases
42
+ : [];
43
+ const workLease = leases.find((lease) => {
44
+ if (!lease || typeof lease !== "object")
45
+ return false;
46
+ const candidate = lease;
47
+ return candidate.kind === "work"
48
+ && candidate.status === "active"
49
+ && typeof candidate.id === "string"
50
+ && candidate.id.trim().length > 0;
51
+ });
52
+ return typeof workLease?.id === "string" ? workLease.id : null;
53
+ }
54
+ export function registerBoardIntentTools(server) {
55
+ server.tool("get_board_settings", "Read Board Manager mode, the active Board Manager assignment, and pending intent count for the current room.", {
56
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
57
+ }, async ({ room_id }) => {
58
+ const targetRoomId = resolveCanonicalRoomId(room_id);
59
+ if (!targetRoomId)
60
+ return taskToolError("Not in a canonical room.");
61
+ try {
62
+ return jsonToolResponse({ success: true, ...responseObject(await getBoardSettings(targetRoomId)) }, 2);
63
+ }
64
+ catch (error) {
65
+ return taskToolError(String(error));
66
+ }
67
+ });
68
+ server.tool("set_board_manager_mode", "Set room Board Manager mode. Requires room admin authority. Use manager_optional unless the human explicitly requires all high-impact agent board actions to register intent.", {
69
+ manager_mode: boardManagerModeSchema.describe("off disables intent checks; manager_optional requires approval only while a manager is assigned; intent_required always requires approval."),
70
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
71
+ }, async ({ manager_mode, room_id }) => {
72
+ const targetRoomId = resolveCanonicalRoomId(room_id);
73
+ if (!targetRoomId)
74
+ return taskToolError("Not in a canonical room.");
75
+ try {
76
+ const result = await patchBoardSettings(targetRoomId, { manager_mode });
77
+ return jsonToolResponse({ success: true, ...responseObject(result) }, 2);
78
+ }
79
+ catch (error) {
80
+ return taskToolError(String(error));
81
+ }
82
+ });
83
+ server.tool("assign_board_manager", "Promote an active worker session to Board Manager for this room. Runtime source is inferred for Open Model workers unless explicitly provided. Requires room admin authority.", {
84
+ agent_session_id: z.string().describe("Active room agent session to promote."),
85
+ runtime_source: boardManagerRuntimeSourceSchema.optional().describe("Runtime source for audit display."),
86
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
87
+ }, async ({ agent_session_id, runtime_source, room_id }) => {
88
+ const targetRoomId = resolveCanonicalRoomId(room_id);
89
+ if (!targetRoomId)
90
+ return taskToolError("Not in a canonical room.");
91
+ try {
92
+ const result = await postBoardManager(targetRoomId, { agent_session_id, runtime_source });
93
+ return jsonToolResponse({
94
+ success: true,
95
+ ...responseObject(result),
96
+ }, 2);
97
+ }
98
+ catch (error) {
99
+ return taskToolError(String(error));
100
+ }
101
+ });
102
+ server.tool("release_board_manager", "Release the active Board Manager assignment for this room. Requires room admin authority.", {
103
+ reason: z.string().optional().describe("Why the manager role is being released."),
104
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
105
+ }, async ({ reason, room_id }) => {
106
+ const targetRoomId = resolveCanonicalRoomId(room_id);
107
+ if (!targetRoomId)
108
+ return taskToolError("Not in a canonical room.");
109
+ try {
110
+ const result = await deleteActiveBoardManager(targetRoomId, { reason });
111
+ return jsonToolResponse({ success: true, ...responseObject(result) }, 2);
112
+ }
113
+ catch (error) {
114
+ return taskToolError(String(error));
115
+ }
116
+ });
117
+ server.tool("register_board_intent", "Advanced: register intent with an exact payload for a high-impact board action. Prefer the typed register_task_*_intent tools so the later action hash matches.", {
118
+ action_type: boardIntentActionTypeSchema.describe("High-impact board action being proposed."),
119
+ payload: boardIntentPayloadSchema.describe("Exact payload for the intended action. The later mutation must match this payload."),
120
+ task_id: z.string().optional().describe("Task id for task-specific intents."),
121
+ ...workerTaskIdentitySchema,
122
+ }, async ({ action_type, payload, task_id, room_id, conversation_id: _conversation_id, agent_session_id }) => {
123
+ const targetRoomId = resolveCanonicalRoomId(room_id);
124
+ if (!targetRoomId)
125
+ return taskToolError("Not in a canonical room.");
126
+ try {
127
+ const { identity, agentSession } = await resolveCanonicalTaskToolIdentity(targetRoomId, agent_session_id);
128
+ const result = await postBoardIntent(targetRoomId, {
129
+ action_type,
130
+ payload,
131
+ task_id,
132
+ ...taskActorPayload(identity, agentSession),
133
+ });
134
+ return jsonToolResponse({
135
+ success: true,
136
+ ...responseObject(result),
137
+ }, 2);
138
+ }
139
+ catch (error) {
140
+ return taskToolError(String(error));
141
+ }
142
+ });
143
+ server.tool("register_task_create_intent", "Register intent to add a task. Use this instead of add_task when Board Manager approval is required; approval creates the task directly.", {
144
+ title: z.string().describe("Task title exactly as it will be passed to add_task."),
145
+ description: z.string().optional().describe("Task description exactly as it will be passed to add_task."),
146
+ source_message_id: z.string().optional().describe("Source message id exactly as it will be passed to add_task."),
147
+ ...workerTaskIdentitySchema,
148
+ }, async ({ title, description, source_message_id, room_id, conversation_id: _conversation_id, agent_session_id }) => {
149
+ const targetRoomId = resolveCanonicalRoomId(room_id);
150
+ if (!targetRoomId)
151
+ return taskToolError("Not in a canonical room.");
152
+ try {
153
+ return await registerTypedBoardIntent({
154
+ roomId: targetRoomId,
155
+ actionType: "task_create",
156
+ payload: boardIntentPayloadForTaskCreate({
157
+ title,
158
+ description: description ?? null,
159
+ sourceMessageId: source_message_id ?? null,
160
+ }),
161
+ agentSessionId: agent_session_id,
162
+ });
163
+ }
164
+ catch (error) {
165
+ return taskToolError(String(error));
166
+ }
167
+ });
168
+ server.tool("register_task_claim_intent", "Register intent to claim an accepted task as the current worker. Use the returned approval with claim_task.", {
169
+ task_id: z.string().describe("Task id to claim, e.g. task_12."),
170
+ ...workerTaskIdentitySchema,
171
+ }, async ({ task_id, room_id, conversation_id: _conversation_id, agent_session_id }) => {
172
+ const targetRoomId = resolveCanonicalRoomId(room_id);
173
+ if (!targetRoomId)
174
+ return taskToolError("Not in a canonical room.");
175
+ try {
176
+ const { identity, agentSession } = await resolveCanonicalTaskToolIdentity(targetRoomId, agent_session_id);
177
+ const result = await postBoardIntent(targetRoomId, {
178
+ action_type: "task_claim",
179
+ task_id,
180
+ payload: boardIntentPayloadForTaskMutation({
181
+ taskId: task_id,
182
+ status: "assigned",
183
+ assignee: identity.actor_label,
184
+ assigneeAgentKey: identity.canonical_key,
185
+ }),
186
+ ...taskActorPayload(identity, agentSession),
187
+ });
188
+ return jsonToolResponse({ success: true, ...responseObject(result) }, 2);
189
+ }
190
+ catch (error) {
191
+ return taskToolError(String(error));
192
+ }
193
+ });
194
+ server.tool("register_task_close_intent", "Register intent to close a task as merged, done, or cancelled. Use the returned approval with update_task.", {
195
+ task_id: z.string().describe("Task id to close, e.g. task_12."),
196
+ status: boardIntentCloseStatusSchema.describe("Closeout status to pass to update_task."),
197
+ pr_url: z.string().optional().describe("PR URL exactly as it will be passed to update_task, if any."),
198
+ ...workerTaskIdentitySchema,
199
+ }, async ({ task_id, status, pr_url, room_id, conversation_id: _conversation_id, agent_session_id }) => {
200
+ const targetRoomId = resolveCanonicalRoomId(room_id);
201
+ if (!targetRoomId)
202
+ return taskToolError("Not in a canonical room.");
203
+ try {
204
+ return await registerTypedBoardIntent({
205
+ roomId: targetRoomId,
206
+ actionType: "task_close",
207
+ taskId: task_id,
208
+ payload: boardIntentPayloadForTaskMutation({
209
+ taskId: task_id,
210
+ status,
211
+ prUrl: pr_url ?? null,
212
+ }),
213
+ agentSessionId: agent_session_id,
214
+ });
215
+ }
216
+ catch (error) {
217
+ return taskToolError(String(error));
218
+ }
219
+ });
220
+ server.tool("register_task_lease_action_intent", "Register intent to release or hand off a task work lease. Use the returned approval with release_task_lease or handoff_task_lease.", {
221
+ task_id: z.string().describe("Task id whose work lease will be changed."),
222
+ action: z.enum(["release", "handoff"]).describe("Lease action that will be performed."),
223
+ lease_id: z.string().optional().describe("Expected active lease id exactly as it will be passed to the lease action."),
224
+ target_agent_key: z.string().optional().describe("Target canonical agent key. Required for handoff."),
225
+ target_agent_session_id: z.string().optional().describe("Target registered agent session id exactly as it will be passed to handoff_task_lease."),
226
+ ...workerTaskIdentitySchema,
227
+ }, async ({ task_id, action, lease_id, target_agent_key, target_agent_session_id, room_id, conversation_id: _conversation_id, agent_session_id, }) => {
228
+ const targetRoomId = resolveCanonicalRoomId(room_id);
229
+ if (!targetRoomId)
230
+ return taskToolError("Not in a canonical room.");
231
+ if (action === "handoff" && !target_agent_key) {
232
+ return taskToolError("target_agent_key is required for handoff intent.");
233
+ }
234
+ try {
235
+ const expectedLeaseId = lease_id ?? activeWorkLeaseIdFromTask(await getCanonicalTask(targetRoomId, task_id));
236
+ if (!expectedLeaseId) {
237
+ return taskToolError("No active work lease exists for this task; pass lease_id if you are approving a specific lease action.");
238
+ }
239
+ return await registerTypedBoardIntent({
240
+ roomId: targetRoomId,
241
+ actionType: "task_override",
242
+ taskId: task_id,
243
+ payload: boardIntentPayloadForLeaseAction({
244
+ taskId: task_id,
245
+ action,
246
+ leaseId: expectedLeaseId,
247
+ targetActorKey: target_agent_key ?? null,
248
+ targetAgentSessionId: target_agent_session_id ?? null,
249
+ }),
250
+ agentSessionId: agent_session_id,
251
+ });
252
+ }
253
+ catch (error) {
254
+ return taskToolError(String(error));
255
+ }
256
+ });
257
+ server.tool("list_board_intents", "List Board Manager intents for this room, optionally filtered by status.", {
258
+ status: z.enum(["pending", "approved", "denied", "expired", "used"]).optional(),
259
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
260
+ }, async ({ status, room_id }) => {
261
+ const targetRoomId = resolveCanonicalRoomId(room_id);
262
+ if (!targetRoomId)
263
+ return taskToolError("Not in a canonical room.");
264
+ const params = new URLSearchParams();
265
+ if (status)
266
+ params.set("status", status);
267
+ try {
268
+ const result = await getBoardIntents(targetRoomId, params.toString());
269
+ return jsonToolResponse({ success: true, ...responseObject(result) }, 2);
270
+ }
271
+ catch (error) {
272
+ return taskToolError(String(error));
273
+ }
274
+ });
275
+ server.tool("approve_board_intent", "Approve a pending board intent. For task_create intents this creates the task immediately and returns result.kind=\"task_created\"; do not call add_task afterward. Other intent types return a scoped approval token for the follow-up board action.", {
276
+ intent_id: z.string().describe("Board intent id to approve."),
277
+ reason: z.string().optional().describe("Short approval reason."),
278
+ ...workerTaskIdentitySchema,
279
+ }, async ({ intent_id, reason, room_id, conversation_id: _conversation_id, agent_session_id }) => {
280
+ const targetRoomId = resolveCanonicalRoomId(room_id);
281
+ if (!targetRoomId)
282
+ return taskToolError("Not in a canonical room.");
283
+ try {
284
+ const { identity, agentSession } = await resolveCanonicalTaskToolIdentity(targetRoomId, agent_session_id);
285
+ const result = await postBoardIntentDecision(targetRoomId, intent_id, "approve", {
286
+ reason,
287
+ ...taskActorPayload(identity, agentSession),
288
+ });
289
+ return jsonToolResponse({
290
+ success: true,
291
+ ...responseObject(result),
292
+ }, 2);
293
+ }
294
+ catch (error) {
295
+ return taskToolError(String(error));
296
+ }
297
+ });
298
+ server.tool("deny_board_intent", "Deny a pending board intent with a short reason.", {
299
+ intent_id: z.string().describe("Board intent id to deny."),
300
+ reason: z.string().optional().describe("Short denial reason."),
301
+ ...workerTaskIdentitySchema,
302
+ }, async ({ intent_id, reason, room_id, conversation_id: _conversation_id, agent_session_id }) => {
303
+ const targetRoomId = resolveCanonicalRoomId(room_id);
304
+ if (!targetRoomId)
305
+ return taskToolError("Not in a canonical room.");
306
+ try {
307
+ const { identity, agentSession } = await resolveCanonicalTaskToolIdentity(targetRoomId, agent_session_id);
308
+ const result = await postBoardIntentDecision(targetRoomId, intent_id, "deny", {
309
+ reason,
310
+ ...taskActorPayload(identity, agentSession),
311
+ });
312
+ return jsonToolResponse({
313
+ success: true,
314
+ ...responseObject(result),
315
+ }, 2);
316
+ }
317
+ catch (error) {
318
+ return taskToolError(String(error));
319
+ }
320
+ });
321
+ }
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ import { ensureAgentIdentity, getRememberedRoomPresence, heartbeatRoomPresence, syncRoomPresence, toPublicAgentIdentity, } from "../../runtime.js";
3
+ import { createTask, listTasks } from "./api.js";
4
+ import { resolveTaskToolIdentity, resolveTaskToolTarget, taskActorPayload, } from "./context.js";
5
+ import { jsonToolResponse, taskToolError } from "./response.js";
6
+ import { boardIntentApprovalSchema, TASK_STATUSES, workerTaskIdentitySchema } from "./schemas.js";
7
+ export function registerTaskBoardTools(server) {
8
+ server.tool("add_task", "Add a new task to the room board. Tasks normally start as 'proposed' and must be " +
9
+ "accepted before an agent can claim them. Agent-created tasks require coordinator " +
10
+ "acceptance before they become claimable. Use this when a human or agent identifies " +
11
+ "work that needs to be done.", {
12
+ title: z.string().describe("Short task title, e.g. 'Wire up Jest test runner'"),
13
+ description: z.string().optional().describe("Longer description of what needs to be done"),
14
+ created_by: z
15
+ .string()
16
+ .optional()
17
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
18
+ source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
19
+ ...workerTaskIdentitySchema,
20
+ ...boardIntentApprovalSchema,
21
+ }, async ({ title, description, created_by: _createdBy, source_message_id, room_id, conversation_id: _conversation_id, agent_session_id, board_intent_id, board_approval_token }) => {
22
+ const target = resolveTaskToolTarget(room_id);
23
+ if (!target)
24
+ return taskToolError("Not in a room. Join one first.");
25
+ const { identity, agentSession } = await resolveTaskToolIdentity(target, agent_session_id);
26
+ const task = await createTask(target, {
27
+ title,
28
+ description,
29
+ created_by: identity.actor_label,
30
+ ...taskActorPayload(identity, agentSession),
31
+ source_message_id,
32
+ board_intent_id,
33
+ board_approval_token,
34
+ });
35
+ await syncRoomPresence(target.effectiveRoomId, identity, getRememberedRoomPresence(target.effectiveRoomId, identity), agentSession);
36
+ return jsonToolResponse({ success: true, task, agent_identity: toPublicAgentIdentity(identity) }, 2);
37
+ });
38
+ server.tool("get_board", "Get the current task board for the room. By default shows only actionable tasks " +
39
+ "(including merged closeout work, but not done/cancelled). Agents should check this on startup and when idle to " +
40
+ "see if there is unassigned work to claim.", {
41
+ status: z.enum(TASK_STATUSES).optional().describe("Filter by specific status"),
42
+ open_only: z.boolean().optional().describe("If true (default), only show actionable tasks, including merged closeout work but not done/cancelled"),
43
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
44
+ }, async ({ status, open_only, room_id }) => {
45
+ const target = resolveTaskToolTarget(room_id);
46
+ if (!target)
47
+ return taskToolError("Not in a room. Join one first.");
48
+ const params = new URLSearchParams();
49
+ if (status)
50
+ params.set("status", status);
51
+ if (open_only !== false)
52
+ params.set("open", "true");
53
+ const allTasks = [];
54
+ let afterCursor;
55
+ for (;;) {
56
+ const pageParams = new URLSearchParams(params);
57
+ if (afterCursor)
58
+ pageParams.set("after", afterCursor);
59
+ const qs = pageParams.toString();
60
+ const result = await listTasks(target, qs);
61
+ const tasks = result.tasks ?? [];
62
+ allTasks.push(...tasks);
63
+ if (!result.has_more || tasks.length === 0)
64
+ break;
65
+ const lastTask = tasks[tasks.length - 1];
66
+ if (!lastTask?.id)
67
+ break;
68
+ afterCursor = lastTask.id;
69
+ }
70
+ await heartbeatRoomPresence(target.effectiveRoomId, await ensureAgentIdentity());
71
+ return jsonToolResponse({ success: true, tasks: allTasks }, 2);
72
+ });
73
+ }
@@ -0,0 +1,35 @@
1
+ import { AGENT_INSTANCE_UUID, agentSessionCredentials, currentRoom, getFallbackProjectId, getTargetRoomId, resolveWorkerToolIdentity, } from "../../runtime.js";
2
+ export function resolveTaskToolTarget(roomId) {
3
+ const targetRoomId = getTargetRoomId(roomId);
4
+ const targetProjectId = getFallbackProjectId();
5
+ if (!targetRoomId && !targetProjectId)
6
+ return null;
7
+ return {
8
+ roomId: targetRoomId ?? null,
9
+ projectId: targetProjectId ?? null,
10
+ effectiveRoomId: targetRoomId ?? currentRoom?.room_id ?? null,
11
+ };
12
+ }
13
+ export function resolveCanonicalRoomId(roomId) {
14
+ return getTargetRoomId(roomId) ?? null;
15
+ }
16
+ export function resolveTaskToolIdentity(target, agentSessionId) {
17
+ return resolveWorkerToolIdentity({
18
+ roomId: target.effectiveRoomId,
19
+ agentSessionId,
20
+ });
21
+ }
22
+ export function resolveCanonicalTaskToolIdentity(roomId, agentSessionId) {
23
+ return resolveWorkerToolIdentity({
24
+ roomId,
25
+ agentSessionId,
26
+ });
27
+ }
28
+ export function taskActorPayload(identity, agentSession) {
29
+ return {
30
+ actor_label: identity.actor_label,
31
+ actor_key: identity.canonical_key,
32
+ actor_instance_id: agentSession.agent_instance_id || AGENT_INSTANCE_UUID,
33
+ ...agentSessionCredentials(agentSession),
34
+ };
35
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import { buildRoomEventsQueryString } from "../../../room-events-query.js";
3
+ import { encodeRoomIdPath } from "../../../room-id.js";
4
+ import { roomScopedApiCall } from "../../runtime.js";
5
+ import { resolveTaskToolTarget } from "./context.js";
6
+ import { jsonToolResponse, taskToolError } from "./response.js";
7
+ export function registerTaskEventTools(server) {
8
+ server.tool("get_room_events", "Get GitHub events for the room (PRs, issues, reviews, check runs, etc.). " +
9
+ "Returns a paginated list of normalized GitHub events persisted from webhooks. " +
10
+ "Use this to check what happened in the repo without parsing chat messages.", {
11
+ event_type: z.string().optional().describe("Filter by event type: pull_request, issue, issue_comment, pull_request_review, check_run, installation, installation_repositories, repository, push, create, delete"),
12
+ object_id: z.string().optional().describe("Filter by GitHub object ID (e.g. PR number, issue number)"),
13
+ actor: z.string().optional().describe("Filter by GitHub login of the actor"),
14
+ since: z.string().optional().describe("ISO timestamp — only events after this time"),
15
+ until: z.string().optional().describe("ISO timestamp — only events before this time"),
16
+ after: z.string().optional().describe("Cursor event ID for pagination (from a previous response)"),
17
+ limit: z.number().int().min(1).max(100).optional().describe("Max events to return (default 50, max 100)"),
18
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
19
+ }, async ({ event_type, object_id, actor, since, until, after, limit, room_id }) => {
20
+ const target = resolveTaskToolTarget(room_id);
21
+ if (!target)
22
+ return taskToolError("Not in a room. Join one first.");
23
+ const qs = buildRoomEventsQueryString({
24
+ event_type,
25
+ object_id,
26
+ actor,
27
+ since,
28
+ until,
29
+ after,
30
+ limit,
31
+ });
32
+ const result = await roomScopedApiCall({
33
+ room_id: target.roomId,
34
+ project_id: target.projectId,
35
+ room_path: (roomId) => `/rooms/${encodeRoomIdPath(roomId)}/events${qs ? `?${qs}` : ""}`,
36
+ project_path: (projectId) => `/rooms/${encodeURIComponent(projectId)}/events${qs ? `?${qs}` : ""}`,
37
+ });
38
+ return jsonToolResponse({ success: true, events: result.events ?? [], has_more: result.has_more ?? false }, 2);
39
+ });
40
+ }
@@ -0,0 +1,14 @@
1
+ import { registerBoardIntentTools } from "./board-intent-tools.js";
2
+ import { registerTaskArtifactTools } from "./artifact-tools.js";
3
+ import { registerTaskBoardTools } from "./board-tools.js";
4
+ import { registerTaskEventTools } from "./event-tools.js";
5
+ import { registerTaskLeaseTools } from "./lease-tools.js";
6
+ import { registerTaskMutationTools } from "./mutation-tools.js";
7
+ export function registerTaskTools(server) {
8
+ registerTaskBoardTools(server);
9
+ registerBoardIntentTools(server);
10
+ registerTaskEventTools(server);
11
+ registerTaskArtifactTools(server);
12
+ registerTaskMutationTools(server);
13
+ registerTaskLeaseTools(server);
14
+ }
@@ -0,0 +1,145 @@
1
+ import { z } from "zod";
2
+ import { syncRoomPresence, toPublicAgentIdentity } from "../../runtime.js";
3
+ import { postCanonicalTaskAction } from "./api.js";
4
+ import { resolveCanonicalRoomId, resolveCanonicalTaskToolIdentity, taskActorPayload, } from "./context.js";
5
+ import { jsonToolResponse, taskToolError } from "./response.js";
6
+ import { boardIntentApprovalSchema, taskLeaseIdentitySchema, taskReviewIdentitySchema } from "./schemas.js";
7
+ export function registerTaskLeaseTools(server) {
8
+ server.tool("claim_task_review", "Claim board review authority for a task in review. This creates a review lease " +
9
+ "for the registered worker session and is rejected if the worker holds the active work lease.", {
10
+ task_id: z.string().describe("The task to review, e.g. 'task_1'"),
11
+ reason: z.string().optional().describe("Why this worker is taking review authority"),
12
+ ...taskReviewIdentitySchema,
13
+ }, async ({ task_id, reason, room_id, conversation_id: _conversation_id, agent_session_id }) => {
14
+ const targetRoomId = resolveCanonicalRoomId(room_id);
15
+ if (!targetRoomId)
16
+ return taskToolError("Not in a canonical room.");
17
+ try {
18
+ const { identity, agentSession } = await resolveCanonicalTaskToolIdentity(targetRoomId, agent_session_id);
19
+ const result = await postCanonicalTaskAction(targetRoomId, task_id, "review-lease-action", {
20
+ action: "claim",
21
+ reason,
22
+ ...taskActorPayload(identity, agentSession),
23
+ });
24
+ await syncRoomPresence(targetRoomId, identity, {
25
+ status: "reviewing",
26
+ status_text: `reviewing ${task_id}`,
27
+ }, agentSession);
28
+ return jsonToolResponse({
29
+ success: true,
30
+ ...result,
31
+ agent_identity: toPublicAgentIdentity(identity),
32
+ }, 2);
33
+ }
34
+ catch (error) {
35
+ return taskToolError(String(error));
36
+ }
37
+ });
38
+ server.tool("release_task_review", "Release this worker's active review lease on a task. Room admins can pass a specific lease_id to clear another review lease.", {
39
+ task_id: z.string().describe("The task whose review lease should be released"),
40
+ lease_id: z.string().optional().describe("Optional review lease id. Required for admin release of another reviewer."),
41
+ reason: z.string().optional().describe("Why review authority is being released"),
42
+ ...taskReviewIdentitySchema,
43
+ }, async ({ task_id, lease_id, reason, room_id, conversation_id: _conversation_id, agent_session_id }) => {
44
+ const targetRoomId = resolveCanonicalRoomId(room_id);
45
+ if (!targetRoomId)
46
+ return taskToolError("Not in a canonical room.");
47
+ try {
48
+ const { identity, agentSession } = await resolveCanonicalTaskToolIdentity(targetRoomId, agent_session_id);
49
+ const result = await postCanonicalTaskAction(targetRoomId, task_id, "review-lease-action", {
50
+ action: "release",
51
+ lease_id: lease_id ?? undefined,
52
+ reason,
53
+ ...taskActorPayload(identity, agentSession),
54
+ });
55
+ await syncRoomPresence(targetRoomId, identity, {
56
+ status: "idle",
57
+ status_text: `released review on ${task_id}`,
58
+ }, agentSession);
59
+ return jsonToolResponse({
60
+ success: true,
61
+ ...result,
62
+ agent_identity: toPublicAgentIdentity(identity),
63
+ }, 2);
64
+ }
65
+ catch (error) {
66
+ return taskToolError(String(error));
67
+ }
68
+ });
69
+ server.tool("release_task_lease", "Release or forcibly clear the active work lease on a task so it can be claimed again. " +
70
+ "Use this when the current worker is blocked, gone, or needs to give the lane back to the room.", {
71
+ task_id: z.string().describe("The task whose active work lease should be cleared"),
72
+ lease_id: z.string().optional().describe("Optional expected active lease id for stale-checking"),
73
+ reason: z.string().optional().describe("Why the lease is being released"),
74
+ ...taskLeaseIdentitySchema,
75
+ ...boardIntentApprovalSchema,
76
+ }, async ({ task_id, lease_id, reason, room_id, conversation_id: _conversation_id, agent_session_id, board_intent_id, board_approval_token }) => {
77
+ const targetRoomId = resolveCanonicalRoomId(room_id);
78
+ if (!targetRoomId)
79
+ return taskToolError("Not in a canonical room.");
80
+ try {
81
+ const { identity, agentSession } = await resolveCanonicalTaskToolIdentity(targetRoomId, agent_session_id);
82
+ const result = await postCanonicalTaskAction(targetRoomId, task_id, "lease-action", {
83
+ action: "release",
84
+ lease_id: lease_id ?? undefined,
85
+ reason,
86
+ board_intent_id,
87
+ board_approval_token,
88
+ ...taskActorPayload(identity, agentSession),
89
+ });
90
+ await syncRoomPresence(targetRoomId, identity, {
91
+ status: "working",
92
+ status_text: `released lease on ${task_id}`,
93
+ }, agentSession);
94
+ return jsonToolResponse({
95
+ success: true,
96
+ ...result,
97
+ agent_identity: toPublicAgentIdentity(identity),
98
+ }, 2);
99
+ }
100
+ catch (error) {
101
+ return taskToolError(String(error));
102
+ }
103
+ });
104
+ server.tool("handoff_task_lease", "Transfer the active work lease on a task to another agent. " +
105
+ "This reassigns the task and mints a fresh work lease for the target worker session.", {
106
+ task_id: z.string().describe("The task whose active work lease should be transferred"),
107
+ target_agent_key: z.string().describe("Canonical agent key to receive the new work lease"),
108
+ target_actor_instance_id: z.string().optional().describe("Optional target agent instance id when selecting among active sessions for the same agent key"),
109
+ target_agent_session_id: z.string().optional().describe("Optional target registered agent session id. Required when the target agent key has multiple active worker sessions."),
110
+ lease_id: z.string().optional().describe("Optional expected active lease id for stale-checking"),
111
+ reason: z.string().optional().describe("Why the lane is being handed off"),
112
+ ...taskLeaseIdentitySchema,
113
+ ...boardIntentApprovalSchema,
114
+ }, async ({ task_id, target_agent_key, target_actor_instance_id, target_agent_session_id, lease_id, reason, room_id, conversation_id: _conversation_id, agent_session_id, board_intent_id, board_approval_token, }) => {
115
+ const targetRoomId = resolveCanonicalRoomId(room_id);
116
+ if (!targetRoomId)
117
+ return taskToolError("Not in a canonical room.");
118
+ try {
119
+ const { identity, agentSession } = await resolveCanonicalTaskToolIdentity(targetRoomId, agent_session_id);
120
+ const result = await postCanonicalTaskAction(targetRoomId, task_id, "lease-action", {
121
+ action: "handoff",
122
+ lease_id: lease_id ?? undefined,
123
+ reason,
124
+ target_actor_key: target_agent_key,
125
+ target_actor_instance_id: target_actor_instance_id ?? undefined,
126
+ target_agent_session_id: target_agent_session_id ?? undefined,
127
+ board_intent_id,
128
+ board_approval_token,
129
+ ...taskActorPayload(identity, agentSession),
130
+ });
131
+ await syncRoomPresence(targetRoomId, identity, {
132
+ status: "working",
133
+ status_text: `handed off ${task_id} to ${target_agent_key}`,
134
+ }, agentSession);
135
+ return jsonToolResponse({
136
+ success: true,
137
+ ...result,
138
+ agent_identity: toPublicAgentIdentity(identity),
139
+ }, 2);
140
+ }
141
+ catch (error) {
142
+ return taskToolError(String(error));
143
+ }
144
+ });
145
+ }