letagents 0.12.9 → 0.12.11

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 (124) hide show
  1. package/README.md +18 -15
  2. package/dist/api/board-intent-payloads.js +25 -0
  3. package/dist/mcp/agent-prompt-delivery.js +20 -0
  4. package/dist/mcp/codex-session/app-server.js +89 -0
  5. package/dist/mcp/codex-session/index.js +10 -0
  6. package/dist/mcp/codex-session/rpc-client.js +85 -0
  7. package/dist/mcp/codex-session/runtime-bridge.js +271 -0
  8. package/dist/mcp/codex-session/runtime-summary.js +220 -0
  9. package/dist/mcp/codex-session/session-inspection.js +100 -0
  10. package/dist/mcp/codex-session/session-mapper.js +51 -0
  11. package/dist/mcp/codex-session/session-start.js +182 -0
  12. package/dist/mcp/codex-session/session-status.js +77 -0
  13. package/dist/mcp/codex-session/session-stop.js +43 -0
  14. package/dist/mcp/codex-session/session-supervisor.js +111 -0
  15. package/dist/mcp/codex-session/start-prompt.js +49 -0
  16. package/dist/mcp/codex-session/types.js +1 -0
  17. package/dist/mcp/codex-session.js +1 -672
  18. package/dist/mcp/config-reader.js +10 -2
  19. package/dist/mcp/git-remote.js +90 -14
  20. package/dist/mcp/local-state/agent-identity.js +26 -0
  21. package/dist/mcp/local-state/agent-sessions.js +66 -0
  22. package/dist/mcp/local-state/auth.js +57 -0
  23. package/dist/mcp/local-state/codex-live-sessions.js +59 -0
  24. package/dist/mcp/local-state/index.js +8 -0
  25. package/dist/mcp/local-state/local-chat.js +808 -0
  26. package/dist/mcp/local-state/room-sessions.js +51 -0
  27. package/dist/mcp/local-state/storage.js +95 -0
  28. package/dist/mcp/local-state/types.js +1 -0
  29. package/dist/mcp/local-state.js +1 -351
  30. package/dist/mcp/managed-agent-providers.js +65 -0
  31. package/dist/mcp/rental-tools/activity.js +37 -0
  32. package/dist/mcp/rental-tools/budget.js +36 -0
  33. package/dist/mcp/rental-tools/context.js +92 -0
  34. package/dist/mcp/rental-tools/lifecycle.js +47 -0
  35. package/dist/mcp/rental-tools/metering.js +73 -0
  36. package/dist/mcp/rental-tools/patches.js +100 -0
  37. package/dist/mcp/rental-tools/provider.js +117 -0
  38. package/dist/mcp/rental-tools/shared.js +19 -0
  39. package/dist/mcp/rental-tools/types.js +1 -0
  40. package/dist/mcp/rental-tools.js +7 -0
  41. package/dist/mcp/repo-visibility.js +8 -6
  42. package/dist/mcp/server/register-tools.js +19 -0
  43. package/dist/mcp/server/repo-context.js +28 -0
  44. package/dist/mcp/server/resources.js +77 -0
  45. package/dist/mcp/server/runtime/agent-sessions.js +173 -0
  46. package/dist/mcp/server/runtime/api.js +104 -0
  47. package/dist/mcp/server/runtime/auth-cache.js +20 -0
  48. package/dist/mcp/server/runtime/device-auth.js +48 -0
  49. package/dist/mcp/server/runtime/identity/config.js +58 -0
  50. package/dist/mcp/server/runtime/identity/directory.js +94 -0
  51. package/dist/mcp/server/runtime/identity/liveness.js +32 -0
  52. package/dist/mcp/server/runtime/identity/names.js +74 -0
  53. package/dist/mcp/server/runtime/identity/public.js +19 -0
  54. package/dist/mcp/server/runtime/identity/state.js +32 -0
  55. package/dist/mcp/server/runtime/identity.js +97 -0
  56. package/dist/mcp/server/runtime/messages.js +160 -0
  57. package/dist/mcp/server/runtime/presence.js +52 -0
  58. package/dist/mcp/server/runtime/room-api.js +42 -0
  59. package/dist/mcp/server/runtime/room-state.js +122 -0
  60. package/dist/mcp/server/runtime/rooms.js +374 -0
  61. package/dist/mcp/server/runtime/supervisor-bridge.js +94 -0
  62. package/dist/mcp/server/runtime/worker-bearer.js +67 -0
  63. package/dist/mcp/server/runtime.js +14 -0
  64. package/dist/mcp/server/tools/agent-sessions.js +415 -0
  65. package/dist/mcp/server/tools/messages/index.js +14 -0
  66. package/dist/mcp/server/tools/messages/message-lookup.js +75 -0
  67. package/dist/mcp/server/tools/messages/read-tool.js +176 -0
  68. package/dist/mcp/server/tools/messages/reasoning-tool.js +179 -0
  69. package/dist/mcp/server/tools/messages/response.js +17 -0
  70. package/dist/mcp/server/tools/messages/send-tool.js +160 -0
  71. package/dist/mcp/server/tools/messages/status-tool.js +87 -0
  72. package/dist/mcp/server/tools/messages/wait-tool.js +403 -0
  73. package/dist/mcp/server/tools/messages.js +1 -0
  74. package/dist/mcp/server/tools/onboarding/device-auth-tools.js +158 -0
  75. package/dist/mcp/server/tools/onboarding/name-tool.js +93 -0
  76. package/dist/mcp/server/tools/onboarding/responses.js +10 -0
  77. package/dist/mcp/server/tools/onboarding/status-tool.js +125 -0
  78. package/dist/mcp/server/tools/onboarding.js +8 -0
  79. package/dist/mcp/server/tools/rental/activity-tools.js +43 -0
  80. package/dist/mcp/server/tools/rental/context-tools.js +43 -0
  81. package/dist/mcp/server/tools/rental/index.js +17 -0
  82. package/dist/mcp/server/tools/rental/metering-tools.js +39 -0
  83. package/dist/mcp/server/tools/rental/patch-tools.js +53 -0
  84. package/dist/mcp/server/tools/rental/provider-tools.js +44 -0
  85. package/dist/mcp/server/tools/rental/response.js +10 -0
  86. package/dist/mcp/server/tools/rental/types.js +1 -0
  87. package/dist/mcp/server/tools/rental.js +1 -0
  88. package/dist/mcp/server/tools/rooms/index.js +5 -0
  89. package/dist/mcp/server/tools/rooms/inspection-tools.js +140 -0
  90. package/dist/mcp/server/tools/rooms/join-tools.js +37 -0
  91. package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +113 -0
  92. package/dist/mcp/server/tools/rooms/repo-visibility-tool.js +20 -0
  93. package/dist/mcp/server/tools/rooms/response.js +10 -0
  94. package/dist/mcp/server/tools/rooms/resume-tool.js +48 -0
  95. package/dist/mcp/server/tools/rooms.js +1 -0
  96. package/dist/mcp/server/tools/tasks/api.js +222 -0
  97. package/dist/mcp/server/tools/tasks/artifact-tools.js +75 -0
  98. package/dist/mcp/server/tools/tasks/board-intent-tools.js +326 -0
  99. package/dist/mcp/server/tools/tasks/board-tools.js +73 -0
  100. package/dist/mcp/server/tools/tasks/context.js +35 -0
  101. package/dist/mcp/server/tools/tasks/event-tools.js +40 -0
  102. package/dist/mcp/server/tools/tasks/index.js +16 -0
  103. package/dist/mcp/server/tools/tasks/lease-tools.js +145 -0
  104. package/dist/mcp/server/tools/tasks/mutation-tools.js +121 -0
  105. package/dist/mcp/server/tools/tasks/response.js +13 -0
  106. package/dist/mcp/server/tools/tasks/schemas.js +68 -0
  107. package/dist/mcp/server/tools/tasks/verdict-tools.js +51 -0
  108. package/dist/mcp/server/tools/tasks.js +1 -0
  109. package/dist/mcp/server.js +12 -3157
  110. package/dist/mcp/sse-client.js +9 -17
  111. package/dist/shared/activation-routing.js +272 -0
  112. package/dist/shared/agent-identity.js +21 -5
  113. package/dist/shared/agent-presence.js +5 -3
  114. package/dist/shared/agent-session-bearer.js +28 -0
  115. package/dist/shared/board-manager-failover.js +16 -0
  116. package/dist/shared/rental/lrt.js +214 -0
  117. package/dist/shared/rental/meter-types.js +14 -0
  118. package/dist/shared/room-agent-prompts.js +14 -1
  119. package/package.json +31 -16
  120. package/dist/mcp/__tests__/config-reader.test.js +0 -93
  121. package/dist/mcp/__tests__/git-remote.test.js +0 -37
  122. package/dist/mcp/__tests__/server-helpers.test.js +0 -121
  123. package/dist/shared/agent-reasoning.js +0 -49
  124. /package/dist/{mcp → shared}/codenames.js +0 -0
@@ -0,0 +1,415 @@
1
+ import { z } from "zod";
2
+ import { scheduleCodexRuntimeStreamBridgeBind, } from "../../codex-session.js";
3
+ import { getManagedAgentProvider, toManagedAgentStartResponse, } from "../../managed-agent-providers.js";
4
+ import { encodeRoomIdPath, looksLikeInviteCode, normalizeInviteCode } from "../../room-id.js";
5
+ import { AGENT_INSTANCE_UUID, RepoRoomAuthRequiredError, apiCall, agentSessionCredentials, currentRoom, detectAgentIdeLabel, detectAgentRuntimeLabel, endStoredAgentSession, ensureAgentIdentity, getSessionLivenessRegistration, getAgentSessionRepoBranch, getStoredAgentSession, getTargetRoomId, ensureLocalWorkerAgentSession, isLocalRoomStorageEnabled, joinRoomIdentifier, resolveLocalRoomStorageIdentifiers, saveAgentSession, toPublicAgentSession, toPublicRoomState, toRepoRoomAuthRequiredResult, withAgentIdentity, resolveWorkerToolIdentity, } from "../runtime.js";
6
+ import { requireValidWorkerBearerRuntime, workerModeDisabledToolResult, } from "../runtime/worker-bearer.js";
7
+ import { bindSupervisedWorkerSession } from "../runtime/supervisor-bridge.js";
8
+ export function registerAgentSessionTools(server) {
9
+ // -- register_agent_session -------------------------------------------------
10
+ server.tool("register_agent_session", "Register this MCP client as an explicit room agent session. Unregistered MCP traffic is treated as controller traffic and stays out of the connected-agent roster.", {
11
+ room_id: z
12
+ .string()
13
+ .optional()
14
+ .describe("Canonical room ID. Defaults to the current room."),
15
+ session_kind: z
16
+ .enum(["worker", "controller"])
17
+ .optional()
18
+ .describe("Agent session kind. Workers appear in connected agent activity; controllers do not. Defaults to worker."),
19
+ runtime: z
20
+ .string()
21
+ .optional()
22
+ .describe("Runtime label such as codex, antigravity, or custom harness name."),
23
+ display_name: z
24
+ .string()
25
+ .optional()
26
+ .describe("Human-readable label for this worker session. Use distinct labels when one MCP process controls multiple workers."),
27
+ cwd: z
28
+ .string()
29
+ .optional()
30
+ .describe("Working directory used to detect the worker's active git branch. Defaults to the MCP server's working directory."),
31
+ }, async ({ room_id, session_kind, runtime, display_name, cwd }) => {
32
+ const targetRoomId = getTargetRoomId(room_id);
33
+ if (!targetRoomId) {
34
+ return {
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: JSON.stringify({
39
+ success: false,
40
+ error: "No room_id provided and not currently in a room.",
41
+ hint: "Join a room first, or pass room_id explicitly.",
42
+ }, null, 2),
43
+ },
44
+ ],
45
+ };
46
+ }
47
+ const requestedRuntime = runtime?.trim() || detectAgentRuntimeLabel();
48
+ const repoBranch = getAgentSessionRepoBranch(cwd);
49
+ if (await isLocalRoomStorageEnabled(targetRoomId)) {
50
+ const session = await ensureLocalWorkerAgentSession(targetRoomId, {
51
+ sessionKind: session_kind ?? "worker",
52
+ runtime: requestedRuntime,
53
+ displayName: display_name,
54
+ repoBranch,
55
+ });
56
+ return {
57
+ content: [
58
+ {
59
+ type: "text",
60
+ text: JSON.stringify({
61
+ success: true,
62
+ local: true,
63
+ agent_session: toPublicAgentSession(session),
64
+ agent_session_id: session.session_id,
65
+ use_agent_session_id: "Pass this exact local agent_session_id to wait_for_messages, send_message, send_thread_message, post_status, and task tools for this same-machine local room.",
66
+ }, null, 2),
67
+ },
68
+ ],
69
+ };
70
+ }
71
+ const { cloudRoomId } = await resolveLocalRoomStorageIdentifiers(targetRoomId);
72
+ const apiRoomId = cloudRoomId || targetRoomId;
73
+ if (requireValidWorkerBearerRuntime().mode === "worker") {
74
+ // The supplied bearer is issued for an existing server-side agent
75
+ // session. Do not call the owner-only registration endpoint or write
76
+ // any session credential to local storage.
77
+ const { agentSession } = await resolveWorkerToolIdentity({ roomId: apiRoomId });
78
+ return {
79
+ content: [
80
+ {
81
+ type: "text",
82
+ text: JSON.stringify({
83
+ success: true,
84
+ worker_bearer_mode: true,
85
+ agent_session: toPublicAgentSession(agentSession),
86
+ agent_session_id: agentSession.session_id,
87
+ use_agent_session_id: "This local worker-bearer session marker may be passed to room tools. The supplied bearer remains the only server credential.",
88
+ }, null, 2),
89
+ },
90
+ ],
91
+ };
92
+ }
93
+ const identity = await ensureAgentIdentity();
94
+ if (!identity.canonical_key) {
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: JSON.stringify({
100
+ success: false,
101
+ error: "Authenticated agent identity is required before registering an agent session.",
102
+ }, null, 2),
103
+ },
104
+ ],
105
+ };
106
+ }
107
+ const created = await apiCall(`/rooms/${encodeRoomIdPath(apiRoomId)}/agent-sessions`, {
108
+ method: "POST",
109
+ body: JSON.stringify({
110
+ actor_key: identity.canonical_key,
111
+ actor_label: identity.actor_label,
112
+ ide_label: identity.ide_label ?? detectAgentIdeLabel(),
113
+ agent_instance_id: AGENT_INSTANCE_UUID,
114
+ display_name: display_name?.trim() || identity.display_name,
115
+ session_kind: session_kind ?? "worker",
116
+ runtime: requestedRuntime,
117
+ repo_branch: repoBranch,
118
+ registration_liveness: getSessionLivenessRegistration(requestedRuntime),
119
+ }),
120
+ });
121
+ const sessionId = typeof created.session_id === "string" ? created.session_id : "";
122
+ const sessionToken = typeof created.session_token === "string" ? created.session_token : "";
123
+ if (!sessionId || !sessionToken) {
124
+ throw new Error("Agent session registration response was missing session credentials.");
125
+ }
126
+ const session = saveAgentSession({
127
+ session_id: sessionId,
128
+ session_token: sessionToken,
129
+ room_id: typeof created.room_id === "string" ? created.room_id : apiRoomId,
130
+ session_kind: created.session_kind === "controller" ? "controller" : "worker",
131
+ runtime: typeof created.runtime === "string" ? created.runtime : "unknown",
132
+ host_id: typeof created.host_id === "string" ? created.host_id : null,
133
+ host_kind: typeof created.host_kind === "string" ? created.host_kind : null,
134
+ host_label: typeof created.host_label === "string" ? created.host_label : null,
135
+ liveness_capability: typeof created.liveness_capability === "string" ? created.liveness_capability : null,
136
+ tool_bridge_id: typeof created.tool_bridge_id === "string" ? created.tool_bridge_id : null,
137
+ actor_label: typeof created.actor_label === "string" ? created.actor_label : identity.actor_label,
138
+ agent_key: typeof created.agent_key === "string" ? created.agent_key : identity.canonical_key,
139
+ agent_instance_id: typeof created.agent_instance_id === "string" ? created.agent_instance_id : AGENT_INSTANCE_UUID,
140
+ display_name: typeof created.display_name === "string" ? created.display_name : identity.display_name,
141
+ owner_label: typeof created.owner_label === "string" ? created.owner_label : identity.owner_label,
142
+ ide_label: typeof created.ide_label === "string" ? created.ide_label : identity.ide_label ?? detectAgentIdeLabel(),
143
+ repo_branch: typeof created.repo_branch === "string" ? created.repo_branch : repoBranch,
144
+ created_at: typeof created.created_at === "string" ? created.created_at : new Date().toISOString(),
145
+ updated_at: typeof created.updated_at === "string" ? created.updated_at : new Date().toISOString(),
146
+ last_seen_at: typeof created.last_seen_at === "string" ? created.last_seen_at : new Date().toISOString(),
147
+ ended_at: typeof created.ended_at === "string" ? created.ended_at : null,
148
+ });
149
+ await bindSupervisedWorkerSession(session);
150
+ scheduleCodexRuntimeStreamBridgeBind(session);
151
+ return {
152
+ content: [
153
+ {
154
+ type: "text",
155
+ text: JSON.stringify({
156
+ success: true,
157
+ agent_session: toPublicAgentSession(session),
158
+ agent_session_id: session.session_id,
159
+ use_agent_session_id: "Pass this exact agent_session_id to wait_for_messages, send_message, send_thread_message, post_status, and task tools for this specific worker. Do not rely on a shared current session.",
160
+ }, null, 2),
161
+ },
162
+ ],
163
+ };
164
+ });
165
+ // -- disconnect_agent_session ----------------------------------------------
166
+ server.tool("disconnect_agent_session", "Disconnect/end a registered worker session from a room. Pass the exact agent_session_id for the worker to disconnect.", {
167
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room or the stored session room."),
168
+ agent_session_id: z.string().optional().describe("Registered agent session to disconnect."),
169
+ }, async ({ room_id, agent_session_id }) => {
170
+ let targetRoomId = getTargetRoomId(room_id);
171
+ const localSession = agent_session_id
172
+ ? getStoredAgentSession(agent_session_id)
173
+ : null;
174
+ if (!targetRoomId && localSession) {
175
+ targetRoomId = localSession.room_id;
176
+ }
177
+ if (!targetRoomId) {
178
+ return {
179
+ content: [
180
+ {
181
+ type: "text",
182
+ text: JSON.stringify({
183
+ success: false,
184
+ error: "No room_id provided and not currently in a room.",
185
+ hint: "Join a room first, pass room_id, or pass a locally stored agent_session_id.",
186
+ }, null, 2),
187
+ },
188
+ ],
189
+ };
190
+ }
191
+ const targetSessionId = agent_session_id || localSession?.session_id;
192
+ if (!targetSessionId) {
193
+ return {
194
+ content: [
195
+ {
196
+ type: "text",
197
+ text: JSON.stringify({
198
+ success: false,
199
+ error: "agent_session_id is required to disconnect a worker session.",
200
+ hint: "Pass the exact agent_session_id returned by register_agent_session.",
201
+ }, null, 2),
202
+ },
203
+ ],
204
+ };
205
+ }
206
+ if (localSession?.ended_at) {
207
+ return {
208
+ content: [
209
+ {
210
+ type: "text",
211
+ text: JSON.stringify({
212
+ success: true,
213
+ already_ended: true,
214
+ agent_session: toPublicAgentSession(localSession),
215
+ }, null, 2),
216
+ },
217
+ ],
218
+ };
219
+ }
220
+ const requestBody = localSession?.session_id === targetSessionId && localSession.session_kind === "worker"
221
+ ? agentSessionCredentials(localSession)
222
+ : {};
223
+ if (await isLocalRoomStorageEnabled(targetRoomId)) {
224
+ const endedSession = endStoredAgentSession(targetSessionId);
225
+ return {
226
+ content: [
227
+ {
228
+ type: "text",
229
+ text: JSON.stringify({
230
+ success: true,
231
+ local: true,
232
+ agent_session: toPublicAgentSession(endedSession),
233
+ }, null, 2),
234
+ },
235
+ ],
236
+ };
237
+ }
238
+ const { cloudRoomId } = await resolveLocalRoomStorageIdentifiers(targetRoomId);
239
+ const apiRoomId = cloudRoomId || targetRoomId;
240
+ const result = await apiCall(`/rooms/${encodeRoomIdPath(apiRoomId)}/agent-sessions/${encodeURIComponent(targetSessionId)}/disconnect`, {
241
+ method: "POST",
242
+ body: JSON.stringify(requestBody),
243
+ });
244
+ const endedAt = result.agent_session && typeof result.agent_session === "object" && "ended_at" in result.agent_session
245
+ ? String(result.agent_session.ended_at ?? new Date().toISOString())
246
+ : new Date().toISOString();
247
+ const endedLocalSession = localSession?.session_id === targetSessionId
248
+ ? endStoredAgentSession(targetSessionId, endedAt)
249
+ : null;
250
+ return {
251
+ content: [
252
+ {
253
+ type: "text",
254
+ text: JSON.stringify({
255
+ success: true,
256
+ room_id: targetRoomId,
257
+ agent_session_id: targetSessionId,
258
+ agent_session: result.agent_session ?? toPublicAgentSession(endedLocalSession),
259
+ delivery_session: result.delivery_session ?? null,
260
+ local_state: endedLocalSession ? "ended" : "unchanged",
261
+ }, null, 2),
262
+ },
263
+ ],
264
+ };
265
+ });
266
+ // -- local Codex live sessions ---------------------------------------------
267
+ server.tool("start_local_codex_session", "Start or reuse a detached local Codex live session for a LetAgents room. The worker will join the room, keep polling, contribute in discussion, and do repo work from the current working directory when asked.", {
268
+ room: z
269
+ .string()
270
+ .describe("Invite code or room name to run as a detached local Codex worker."),
271
+ cwd: z
272
+ .string()
273
+ .optional()
274
+ .describe("Working directory for repo work. Defaults to the current process directory."),
275
+ stop_phrase: z
276
+ .string()
277
+ .optional()
278
+ .describe("Exact room message text that tells the worker to stop. Defaults to /stop-codex-room."),
279
+ max_minutes: z
280
+ .number()
281
+ .optional()
282
+ .describe("Optional hard stop in minutes. Defaults to 0, which means run until stopped."),
283
+ }, async ({ room, cwd, stop_phrase, max_minutes }) => {
284
+ const disabled = workerModeDisabledToolResult("Local Codex session orchestration");
285
+ if (disabled) {
286
+ return {
287
+ content: [{ type: "text", text: JSON.stringify(disabled, null, 2) }],
288
+ };
289
+ }
290
+ const joinedVia = looksLikeInviteCode(room) ? "join_code" : "join_room";
291
+ try {
292
+ const joined = await joinRoomIdentifier(room, joinedVia);
293
+ const provider = getManagedAgentProvider("codex");
294
+ const liveSession = await provider.startLocalSession({
295
+ room_id: joined.room.room_id,
296
+ room_identifier: joinedVia === "join_code" ? normalizeInviteCode(room) : room.trim(),
297
+ room_code: joined.room.code ?? null,
298
+ room_display_name: joined.room.display_name ?? null,
299
+ joined_via: joinedVia,
300
+ cwd: cwd || process.cwd(),
301
+ stop_phrase,
302
+ max_minutes,
303
+ });
304
+ return {
305
+ content: [
306
+ {
307
+ type: "text",
308
+ text: JSON.stringify(await withAgentIdentity({
309
+ success: true,
310
+ room: toPublicRoomState(joined.room),
311
+ ...toManagedAgentStartResponse(provider, liveSession),
312
+ }), null, 2),
313
+ },
314
+ ],
315
+ };
316
+ }
317
+ catch (error) {
318
+ if (error instanceof RepoRoomAuthRequiredError) {
319
+ return {
320
+ content: [
321
+ {
322
+ type: "text",
323
+ text: JSON.stringify(toRepoRoomAuthRequiredResult(error), null, 2),
324
+ },
325
+ ],
326
+ };
327
+ }
328
+ throw error;
329
+ }
330
+ });
331
+ server.tool("status_local_codex_session", "Inspect the current detached local Codex live session, or a specific one by session_id.", {
332
+ session_id: z
333
+ .string()
334
+ .optional()
335
+ .describe("Optional session id. Defaults to the current local Codex live session."),
336
+ }, async ({ session_id }) => {
337
+ const disabled = workerModeDisabledToolResult("Local Codex session orchestration");
338
+ if (disabled) {
339
+ return {
340
+ content: [{ type: "text", text: JSON.stringify(disabled, null, 2) }],
341
+ };
342
+ }
343
+ const provider = getManagedAgentProvider("codex");
344
+ const status = await provider.inspectLocalSession(session_id, currentRoom?.room_id);
345
+ if (!status) {
346
+ return {
347
+ content: [
348
+ {
349
+ type: "text",
350
+ text: JSON.stringify({ success: false, error: "No local Codex live session found." }, null, 2),
351
+ },
352
+ ],
353
+ };
354
+ }
355
+ return {
356
+ content: [
357
+ {
358
+ type: "text",
359
+ text: JSON.stringify({
360
+ success: true,
361
+ session: provider.toPublicLiveSession(status.session),
362
+ server_reachable: status.server_reachable,
363
+ thread_status: status.thread_status,
364
+ turn_status: status.turn_status,
365
+ recent_items: status.recent_items,
366
+ }, null, 2),
367
+ },
368
+ ],
369
+ };
370
+ });
371
+ server.tool("stop_local_codex_session", "Stop the current detached local Codex live session, or a specific one by session_id.", {
372
+ session_id: z
373
+ .string()
374
+ .optional()
375
+ .describe("Optional session id. Defaults to the current local Codex live session."),
376
+ shutdown_server: z
377
+ .boolean()
378
+ .optional()
379
+ .describe("If true, also terminate the spawned codex app-server process when possible."),
380
+ }, async ({ session_id, shutdown_server }) => {
381
+ const disabled = workerModeDisabledToolResult("Local Codex session orchestration");
382
+ if (disabled) {
383
+ return {
384
+ content: [{ type: "text", text: JSON.stringify(disabled, null, 2) }],
385
+ };
386
+ }
387
+ const provider = getManagedAgentProvider("codex");
388
+ const stopped = await provider.stopLocalSession({
389
+ session_id,
390
+ room_id: currentRoom?.room_id,
391
+ shutdown_server,
392
+ });
393
+ if (!stopped) {
394
+ return {
395
+ content: [
396
+ {
397
+ type: "text",
398
+ text: JSON.stringify({ success: false, error: "No local Codex live session found." }, null, 2),
399
+ },
400
+ ],
401
+ };
402
+ }
403
+ return {
404
+ content: [
405
+ {
406
+ type: "text",
407
+ text: JSON.stringify({
408
+ success: true,
409
+ session: provider.toPublicLiveSession(stopped),
410
+ }, null, 2),
411
+ },
412
+ ],
413
+ };
414
+ });
415
+ }
@@ -0,0 +1,14 @@
1
+ import { registerReadMessagesTool } from "./read-tool.js";
2
+ import { registerPostReasoningTool } from "./reasoning-tool.js";
3
+ import { registerSendMessageTool } from "./send-tool.js";
4
+ import { registerPostStatusTool } from "./status-tool.js";
5
+ import { registerWaitForMessagesTool } from "./wait-tool.js";
6
+ export function registerStatusTools(server) {
7
+ registerPostStatusTool(server);
8
+ registerPostReasoningTool(server);
9
+ }
10
+ export function registerMessageTools(server) {
11
+ registerSendMessageTool(server);
12
+ registerReadMessagesTool(server);
13
+ registerWaitForMessagesTool(server);
14
+ }
@@ -0,0 +1,75 @@
1
+ import { encodeRoomIdPath } from "../../../room-id.js";
2
+ import { ApiError, appendIncludePromptOnly, getLocalChatMessages, isMissingRouteError, roomScopedApiCall, } from "../../runtime.js";
3
+ function isRecord(value) {
4
+ return Boolean(value && typeof value === "object");
5
+ }
6
+ export async function findLocalMessageById(roomId, messageId) {
7
+ let afterCursor;
8
+ for (;;) {
9
+ const result = await getLocalChatMessages(roomId, {
10
+ after: afterCursor,
11
+ include_prompt_only: true,
12
+ });
13
+ const messages = (result.messages ?? []).filter(isRecord);
14
+ const match = messages.find((message) => message.id === messageId);
15
+ if (match)
16
+ return match;
17
+ if (!result.has_more || messages.length === 0)
18
+ return null;
19
+ const lastMessage = messages[messages.length - 1];
20
+ afterCursor = typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
21
+ if (!afterCursor)
22
+ return null;
23
+ }
24
+ }
25
+ export async function findRemoteMessageById(input) {
26
+ if (input.roomId) {
27
+ try {
28
+ const result = await roomScopedApiCall({
29
+ room_id: input.roomId,
30
+ room_path: (roomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(roomId)}/messages/${encodeURIComponent(input.messageId)}`),
31
+ project_path: () => "",
32
+ // Context lookups must not advance the room session cursor.
33
+ preserve_session_cursor: true,
34
+ });
35
+ return isRecord(result.message) ? result.message : null;
36
+ }
37
+ catch (error) {
38
+ if (isMissingRouteError(error)) {
39
+ // Older API without the by-id route: fall back to scanning history.
40
+ }
41
+ else if (error instanceof ApiError && error.status === 404) {
42
+ return null;
43
+ }
44
+ else {
45
+ throw error;
46
+ }
47
+ }
48
+ }
49
+ return scanRemoteMessageHistoryById(input);
50
+ }
51
+ async function scanRemoteMessageHistoryById(input) {
52
+ let afterCursor;
53
+ for (;;) {
54
+ const query = new URLSearchParams();
55
+ if (afterCursor)
56
+ query.set("after", afterCursor);
57
+ const qs = query.toString();
58
+ const result = await roomScopedApiCall({
59
+ room_id: input.roomId,
60
+ project_id: input.projectId,
61
+ room_path: (roomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(roomId)}/messages${qs ? `?${qs}` : ""}`),
62
+ project_path: (projectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`),
63
+ });
64
+ const messages = (result.messages ?? []).filter(isRecord);
65
+ const match = messages.find((message) => message.id === input.messageId);
66
+ if (match)
67
+ return match;
68
+ if (!result.has_more || messages.length === 0)
69
+ return null;
70
+ const lastMessage = messages[messages.length - 1];
71
+ afterCursor = typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
72
+ if (!afterCursor)
73
+ return null;
74
+ }
75
+ }
@@ -0,0 +1,176 @@
1
+ import { z } from "zod";
2
+ import { encodeRoomIdPath } from "../../../room-id.js";
3
+ import { appendIncludePromptOnly, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getLocalChatMessages, getTargetRoomId, heartbeatRoomPresence, touchRoomSession, isLocalRoomStorageEnabled, resolveLocalRoomStorageIdentifiers, roomScopedApiCall, toAgentReadableMessages, } from "../../runtime.js";
4
+ import { jsonToolResponse } from "./response.js";
5
+ export const DEFAULT_READ_MESSAGES_LIMIT = 100;
6
+ // Both the API and the local store clamp a single page to 500 messages.
7
+ const MAX_MESSAGES_PER_PAGE = 500;
8
+ export function selectRecentMessages(messages, limit) {
9
+ const total = messages.length;
10
+ const selected = limit > 0 && total > limit ? messages.slice(total - limit) : messages;
11
+ return {
12
+ messages: selected,
13
+ total_message_count: total,
14
+ omitted_message_count: total - selected.length,
15
+ };
16
+ }
17
+ function withRecencyTelemetry(output, selection) {
18
+ output.total_message_count = selection.total_message_count;
19
+ if (selection.omitted_message_count > 0) {
20
+ output.omitted_message_count = selection.omitted_message_count;
21
+ output.truncated = true;
22
+ }
23
+ return output;
24
+ }
25
+ // Fetch the most recent messages by paging BACKWARDS from the tail
26
+ // (before=latest), so a limited read never walks the full room history.
27
+ export async function fetchRecentRemoteMessages(input) {
28
+ const collected = [];
29
+ let beforeCursor = "latest";
30
+ let truncated = false;
31
+ let roomIdFromResponse;
32
+ let newestMessageId;
33
+ for (;;) {
34
+ const params = new URLSearchParams();
35
+ params.set("before", beforeCursor);
36
+ params.set("limit", String(Math.min(input.limit - collected.length, MAX_MESSAGES_PER_PAGE)));
37
+ const qs = params.toString();
38
+ const result = await roomScopedApiCall({
39
+ room_id: input.targetRoomId,
40
+ project_id: input.targetProjectId,
41
+ room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages?${qs}`),
42
+ project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages?${qs}`),
43
+ // Pages after the first end on progressively OLDER messages; letting each
44
+ // page touch the session would walk last_message_id backwards and make
45
+ // resume_room_session replay already-read history.
46
+ preserve_session_cursor: true,
47
+ });
48
+ roomIdFromResponse = roomIdFromResponse || result.room_id || result.project_id;
49
+ const msgs = result.messages ?? [];
50
+ if (newestMessageId === undefined) {
51
+ const newest = msgs[msgs.length - 1]?.id;
52
+ if (typeof newest === "string" && newest)
53
+ newestMessageId = newest;
54
+ }
55
+ collected.unshift(...msgs);
56
+ const hasOlder = Boolean(result.has_older ?? result.has_more);
57
+ if (!hasOlder || msgs.length === 0)
58
+ break;
59
+ if (collected.length >= input.limit) {
60
+ truncated = true;
61
+ break;
62
+ }
63
+ const oldestId = msgs[0]?.id;
64
+ if (typeof oldestId !== "string" || !oldestId) {
65
+ truncated = true;
66
+ break;
67
+ }
68
+ beforeCursor = oldestId;
69
+ }
70
+ if (input.targetRoomId && newestMessageId) {
71
+ touchRoomSession(input.targetRoomId, newestMessageId);
72
+ }
73
+ return { messages: collected, truncated, roomIdFromResponse };
74
+ }
75
+ export function registerReadMessagesTool(server) {
76
+ server.tool("read_messages", "Read recent messages from a Let Agents Chat room (most recent `limit`, default 100; pass limit: 0 for the full history — expensive in busy rooms). Threaded replies include thread_parent_id/thread.root_message_id; use send_thread_message with that id to continue focused side discussion without polluting the main room. For long-running work, prefer wait_for_messages with after_message_id so you only process new lines and do not treat an empty poll as the end of the mission.", {
77
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
78
+ limit: z
79
+ .number()
80
+ .int()
81
+ .min(0)
82
+ .optional()
83
+ .describe(`Return only the most recent N messages (default ${DEFAULT_READ_MESSAGES_LIMIT}). Pass 0 for the full history. When older messages are omitted, the response carries truncated=true.`),
84
+ }, async ({ room_id, limit }) => {
85
+ const effectiveLimit = limit ?? DEFAULT_READ_MESSAGES_LIMIT;
86
+ const targetRoomId = getTargetRoomId(room_id);
87
+ const targetProjectId = getFallbackProjectId();
88
+ const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
89
+ if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
90
+ const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
91
+ const effectiveLocalRoomId = sqliteRoomId || localRoomId;
92
+ if (effectiveLimit > 0 && effectiveLimit <= MAX_MESSAGES_PER_PAGE) {
93
+ const result = await getLatestLocalChatMessages(effectiveLocalRoomId, {
94
+ limit: effectiveLimit,
95
+ include_prompt_only: true,
96
+ });
97
+ const output = {
98
+ room_id: effectiveLocalRoomId,
99
+ messages: toAgentReadableMessages(result.messages ?? []),
100
+ };
101
+ if (result.has_more)
102
+ output.truncated = true;
103
+ return jsonToolResponse(output);
104
+ }
105
+ const allMessages = [];
106
+ let afterCursor;
107
+ for (;;) {
108
+ const result = await getLocalChatMessages(effectiveLocalRoomId, {
109
+ after: afterCursor,
110
+ include_prompt_only: true,
111
+ });
112
+ const msgs = result.messages ?? [];
113
+ allMessages.push(...msgs);
114
+ if (!result.has_more || msgs.length === 0)
115
+ break;
116
+ const lastMsg = msgs[msgs.length - 1];
117
+ if (!lastMsg?.id)
118
+ break;
119
+ afterCursor = lastMsg.id;
120
+ }
121
+ const selection = selectRecentMessages(allMessages, effectiveLimit);
122
+ return jsonToolResponse(withRecencyTelemetry({
123
+ room_id: effectiveLocalRoomId,
124
+ messages: toAgentReadableMessages(selection.messages),
125
+ }, selection));
126
+ }
127
+ if (effectiveLimit > 0) {
128
+ const recent = await fetchRecentRemoteMessages({
129
+ targetRoomId,
130
+ targetProjectId,
131
+ limit: effectiveLimit,
132
+ });
133
+ await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, await ensureAgentIdentity());
134
+ const output = {
135
+ messages: toAgentReadableMessages(recent.messages),
136
+ };
137
+ if (recent.truncated)
138
+ output.truncated = true;
139
+ if (recent.roomIdFromResponse) {
140
+ output[targetRoomId ? "room_id" : "project_id"] = recent.roomIdFromResponse;
141
+ }
142
+ return jsonToolResponse(output);
143
+ }
144
+ const allMessages = [];
145
+ let afterCursor;
146
+ let roomIdFromResponse;
147
+ for (;;) {
148
+ const query = new URLSearchParams();
149
+ if (afterCursor)
150
+ query.set("after", afterCursor);
151
+ const qs = query.toString();
152
+ const result = await roomScopedApiCall({
153
+ room_id: targetRoomId,
154
+ project_id: targetProjectId,
155
+ room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`),
156
+ project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages${qs ? `?${qs}` : ""}`),
157
+ });
158
+ roomIdFromResponse = roomIdFromResponse || result.room_id || result.project_id;
159
+ const msgs = result.messages ?? [];
160
+ allMessages.push(...msgs);
161
+ if (!result.has_more || msgs.length === 0)
162
+ break;
163
+ const lastMsg = msgs[msgs.length - 1];
164
+ if (!lastMsg?.id)
165
+ break;
166
+ afterCursor = lastMsg.id;
167
+ }
168
+ await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, await ensureAgentIdentity());
169
+ const selection = selectRecentMessages(allMessages, effectiveLimit);
170
+ const output = withRecencyTelemetry({ messages: toAgentReadableMessages(selection.messages) }, selection);
171
+ if (roomIdFromResponse) {
172
+ output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
173
+ }
174
+ return jsonToolResponse(output);
175
+ });
176
+ }