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,179 @@
1
+ import { z } from "zod";
2
+ import { AGENT_PRESENCE_STATUSES } from "../../../../shared/agent-presence.js";
3
+ import { encodeRoomIdPath } from "../../../room-id.js";
4
+ import { addLocalChatMessage, agentSessionCredentials, currentRoom, getFallbackProjectId, getRememberedRoomPresence, getTargetRoomId, isLocalRoomStorageEnabled, normalizeOptionalToolString, resolveLocalRoomStorageIdentifiers, resolveWorkerToolIdentity, roomScopedApiCall, syncRoomPresence, toPublicAgentIdentity, touchCurrentRoom, } from "../../runtime.js";
5
+ import { jsonToolResponse, missingRoomResponse } from "./response.js";
6
+ export function registerPostReasoningTool(server) {
7
+ server.tool("post_reasoning", "Update a structured, replace-in-place reasoning trace for the current room without spamming chat. " +
8
+ "Use this for visible thought-process updates such as goal, hypothesis, checks in progress, blockers, " +
9
+ "and next action. Optionally persist a milestone summary into room history when something durable changed.", {
10
+ summary: z.string().describe("Short current reasoning summary shown in the room UI."),
11
+ goal: z.string().optional().describe("What the agent is trying to achieve right now."),
12
+ checking: z.string().optional().describe("What the agent is verifying or inspecting."),
13
+ hypothesis: z.string().optional().describe("Current working theory or approach."),
14
+ blocker: z.string().optional().describe("Current blocker, if any."),
15
+ next_action: z.string().optional().describe("Immediate next step the agent plans to take."),
16
+ milestone: z.string().optional().describe("Optional durable milestone summary to append to room history."),
17
+ confidence: z.number().min(0).max(1).optional().describe("Optional confidence score between 0 and 1."),
18
+ status: z.enum(AGENT_PRESENCE_STATUSES).optional().describe("Optional explicit presence state override."),
19
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
20
+ conversation_id: z
21
+ .string()
22
+ .optional()
23
+ .describe("Deprecated for worker writes; registered worker session identity is used."),
24
+ agent_session_id: z
25
+ .string()
26
+ .optional()
27
+ .describe("Registered agent session to use for this reasoning update. Required for worker reasoning."),
28
+ }, async ({ summary, goal, checking, hypothesis, blocker, next_action, milestone, confidence, status, room_id, conversation_id: _conversation_id, agent_session_id, }) => {
29
+ const targetRoomId = getTargetRoomId(room_id);
30
+ const targetProjectId = getFallbackProjectId();
31
+ if (!targetRoomId && !targetProjectId) {
32
+ return missingRoomResponse();
33
+ }
34
+ const normalizedSummary = summary.trim();
35
+ if (!normalizedSummary) {
36
+ return jsonToolResponse({ success: false, error: "summary is required" });
37
+ }
38
+ const { identity, agentSession } = await resolveWorkerToolIdentity({
39
+ roomId: targetRoomId ?? currentRoom?.room_id ?? null,
40
+ agentSessionId: agent_session_id,
41
+ });
42
+ const snapshot = {
43
+ summary: normalizedSummary,
44
+ goal: normalizeOptionalToolString(goal),
45
+ checking: normalizeOptionalToolString(checking),
46
+ hypothesis: normalizeOptionalToolString(hypothesis),
47
+ blocker: normalizeOptionalToolString(blocker),
48
+ next_action: normalizeOptionalToolString(next_action),
49
+ milestone: normalizeOptionalToolString(milestone),
50
+ confidence,
51
+ status,
52
+ };
53
+ const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
54
+ if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
55
+ const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
56
+ const effectiveLocalRoomId = sqliteRoomId || localRoomId;
57
+ let milestoneMessageId = null;
58
+ const normalizedMilestone = normalizeOptionalToolString(milestone);
59
+ if (normalizedMilestone) {
60
+ const milestoneMessage = await addLocalChatMessage(effectiveLocalRoomId, {
61
+ sender: identity.actor_label,
62
+ text: normalizedMilestone,
63
+ source: "agent",
64
+ });
65
+ milestoneMessageId = milestoneMessage.id;
66
+ touchCurrentRoom(milestoneMessageId);
67
+ }
68
+ await syncRoomPresence(effectiveLocalRoomId, identity, {
69
+ status: status ?? getRememberedRoomPresence(effectiveLocalRoomId, identity).status,
70
+ status_text: normalizedSummary,
71
+ }, agentSession);
72
+ const now = new Date().toISOString();
73
+ return jsonToolResponse({
74
+ success: true,
75
+ room_id: effectiveLocalRoomId,
76
+ local: true,
77
+ session: {
78
+ id: `local_reasoning:${identity.actor_label}`,
79
+ actor_label: identity.actor_label,
80
+ agent_key: identity.canonical_key,
81
+ status: status ?? "working",
82
+ summary: normalizedSummary,
83
+ latest_payload: snapshot,
84
+ updated_at: now,
85
+ },
86
+ update: {
87
+ id: `local_reasoning_update:${now}`,
88
+ payload: snapshot,
89
+ created_at: now,
90
+ },
91
+ milestone_message_id: milestoneMessageId,
92
+ agent_identity: toPublicAgentIdentity(identity),
93
+ });
94
+ }
95
+ const query = new URLSearchParams({
96
+ open: "true",
97
+ actor_label: identity.actor_label,
98
+ });
99
+ const sessionsResult = await roomScopedApiCall({
100
+ room_id: targetRoomId,
101
+ project_id: targetProjectId,
102
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/reasoning-sessions?${query.toString()}`,
103
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/reasoning-sessions?${query.toString()}`,
104
+ });
105
+ const existingSession = sessionsResult.sessions?.[0];
106
+ let result = null;
107
+ if (existingSession?.id) {
108
+ result = await roomScopedApiCall({
109
+ room_id: targetRoomId,
110
+ project_id: targetProjectId,
111
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/reasoning-sessions/${encodeURIComponent(existingSession.id)}/updates`,
112
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/reasoning-sessions/${encodeURIComponent(existingSession.id)}/updates`,
113
+ options: {
114
+ method: "POST",
115
+ body: JSON.stringify({
116
+ actor_label: identity.actor_label,
117
+ ...agentSessionCredentials(agentSession),
118
+ ...snapshot,
119
+ }),
120
+ },
121
+ });
122
+ }
123
+ else {
124
+ result = await roomScopedApiCall({
125
+ room_id: targetRoomId,
126
+ project_id: targetProjectId,
127
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/reasoning-sessions`,
128
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/reasoning-sessions`,
129
+ options: {
130
+ method: "POST",
131
+ body: JSON.stringify({
132
+ actor_label: identity.actor_label,
133
+ agent_key: identity.canonical_key,
134
+ ...agentSessionCredentials(agentSession),
135
+ ...snapshot,
136
+ }),
137
+ },
138
+ });
139
+ }
140
+ let milestoneMessageId = null;
141
+ const normalizedMilestone = normalizeOptionalToolString(milestone);
142
+ if (normalizedMilestone) {
143
+ const milestoneMessage = await roomScopedApiCall({
144
+ room_id: targetRoomId,
145
+ project_id: targetProjectId,
146
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages`,
147
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
148
+ options: {
149
+ method: "POST",
150
+ body: JSON.stringify({
151
+ sender: identity.actor_label,
152
+ text: normalizedMilestone,
153
+ ...agentSessionCredentials(agentSession),
154
+ }),
155
+ },
156
+ });
157
+ milestoneMessageId =
158
+ typeof milestoneMessage.id === "string" ? milestoneMessage.id : null;
159
+ touchCurrentRoom(milestoneMessageId ?? undefined);
160
+ }
161
+ if (status) {
162
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
163
+ status,
164
+ status_text: normalizedSummary,
165
+ }, agentSession);
166
+ }
167
+ else {
168
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity), agentSession);
169
+ }
170
+ return jsonToolResponse({
171
+ success: true,
172
+ room_id: result?.room_id ?? targetRoomId ?? targetProjectId ?? null,
173
+ session: result?.session ?? null,
174
+ update: result?.update ?? null,
175
+ milestone_message_id: milestoneMessageId,
176
+ agent_identity: toPublicAgentIdentity(identity),
177
+ });
178
+ });
179
+ }
@@ -0,0 +1,17 @@
1
+ export function jsonToolResponse(value) {
2
+ return {
3
+ content: [
4
+ {
5
+ type: "text",
6
+ text: JSON.stringify(value, null, 2),
7
+ },
8
+ ],
9
+ };
10
+ }
11
+ export function missingRoomResponse() {
12
+ return jsonToolResponse({
13
+ success: false,
14
+ error: "No room_id provided and not currently in a room.",
15
+ hint: "Join or create a room first, or pass room_id explicitly.",
16
+ });
17
+ }
@@ -0,0 +1,203 @@
1
+ import { z } from "zod";
2
+ import { encodeRoomIdPath } from "../../../room-id.js";
3
+ import { agentSessionCredentials, addLocalChatMessage, appendIncludePromptOnly, currentRoom, getFallbackProjectId, getLocalChatMessages, getRememberedRoomPresence, getTargetRoomId, isLocalRoomStorageEnabled, resolveLocalRoomStorageIdentifiers, resolveWorkerToolIdentity, roomScopedApiCall, syncRoomPresence, toPublicAgentIdentity, touchCurrentRoom, } from "../../runtime.js";
4
+ import { jsonToolResponse } from "./response.js";
5
+ export function buildSendMessageRequestBody(input) {
6
+ return {
7
+ sender: input.sender,
8
+ text: input.text,
9
+ reply_to: input.replyTarget,
10
+ ...(input.resolvedThreadRoot ? { thread_root_id: input.resolvedThreadRoot } : {}),
11
+ ...input.credentials,
12
+ };
13
+ }
14
+ function normalizeMessageId(value) {
15
+ const trimmed = value?.trim();
16
+ return trimmed ? trimmed : undefined;
17
+ }
18
+ function explicitThreadRootId(message) {
19
+ const root = message?.thread_root_id ?? message?.threadRootId;
20
+ if (typeof root === "string" && root.trim()) {
21
+ return root;
22
+ }
23
+ return null;
24
+ }
25
+ async function findLocalMessageById(roomId, messageId) {
26
+ let afterCursor;
27
+ for (;;) {
28
+ const result = await getLocalChatMessages(roomId, {
29
+ after: afterCursor,
30
+ include_prompt_only: true,
31
+ });
32
+ const messages = (result.messages ?? []);
33
+ const match = messages.find((message) => message.id === messageId);
34
+ if (match)
35
+ return match;
36
+ if (!result.has_more || messages.length === 0)
37
+ return null;
38
+ const lastMessage = messages[messages.length - 1];
39
+ afterCursor = typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
40
+ if (!afterCursor)
41
+ return null;
42
+ }
43
+ }
44
+ async function findRemoteMessageById(input) {
45
+ let afterCursor;
46
+ for (;;) {
47
+ const query = new URLSearchParams();
48
+ if (afterCursor)
49
+ query.set("after", afterCursor);
50
+ const qs = query.toString();
51
+ const result = await roomScopedApiCall({
52
+ room_id: input.roomId,
53
+ project_id: input.projectId,
54
+ room_path: (roomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(roomId)}/messages${qs ? `?${qs}` : ""}`),
55
+ project_path: (projectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`),
56
+ });
57
+ const messages = result.messages ?? [];
58
+ const match = messages.find((message) => message.id === input.messageId);
59
+ if (match)
60
+ return match;
61
+ if (!result.has_more || messages.length === 0)
62
+ return null;
63
+ const lastMessage = messages[messages.length - 1];
64
+ afterCursor = typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
65
+ if (!afterCursor)
66
+ return null;
67
+ }
68
+ }
69
+ async function findMessageById(input) {
70
+ if (input.localRoomId && await isLocalRoomStorageEnabled(input.localRoomId)) {
71
+ const { localRoomId } = await resolveLocalRoomStorageIdentifiers(input.localRoomId);
72
+ return findLocalMessageById(localRoomId || input.localRoomId, input.messageId);
73
+ }
74
+ return findRemoteMessageById({
75
+ roomId: input.roomId,
76
+ projectId: input.projectId,
77
+ messageId: input.messageId,
78
+ });
79
+ }
80
+ // Resolve the thread root for a send_thread_message target by reading the target's
81
+ // explicit thread_root_id (the authoritative root the message mappers already
82
+ // populate) instead of walking reply_to. This roots a reply AT a top-level
83
+ // quote-reply X (X.thread_root_id === X.id) rather than under the message X quoted.
84
+ // When the target carries no explicit root (not found / hidden / out-of-band), treat
85
+ // it as its own root — matching the prior lenient behavior, so the create-side
86
+ // existence checks fail the same way rather than newly rejecting a valid reply.
87
+ async function resolveThreadRootMessageId(input) {
88
+ const message = await findMessageById(input);
89
+ return explicitThreadRootId(message) ?? input.messageId;
90
+ }
91
+ function initialReplyTarget(input) {
92
+ const replyTo = normalizeMessageId(input.reply_to);
93
+ const threadParentId = normalizeMessageId(input.thread_parent_id);
94
+ if (replyTo && threadParentId && replyTo !== threadParentId) {
95
+ throw new Error("reply_to and thread_parent_id must match when both are provided.");
96
+ }
97
+ return {
98
+ replyTarget: threadParentId || replyTo,
99
+ shouldResolveThreadRoot: Boolean(threadParentId),
100
+ };
101
+ }
102
+ async function sendMessageFromTool(input) {
103
+ const targetRoomId = getTargetRoomId(input.room_id);
104
+ const targetProjectId = getFallbackProjectId();
105
+ if (!targetRoomId && !targetProjectId) {
106
+ throw new Error("No room is currently selected. Join a room first or pass room_id.");
107
+ }
108
+ const { identity, agentSession } = await resolveWorkerToolIdentity({
109
+ roomId: targetRoomId ?? currentRoom?.room_id ?? null,
110
+ agentSessionId: input.agent_session_id,
111
+ });
112
+ const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
113
+ const { replyTarget, shouldResolveThreadRoot } = initialReplyTarget(input);
114
+ // The quote/chip reference stays the literal target the caller pointed at; the
115
+ // thread root is resolved separately from that target's explicit thread_root_id,
116
+ // so a reply to a top-level quote-reply roots at it rather than under what it quoted.
117
+ const resolvedThreadRoot = replyTarget && shouldResolveThreadRoot
118
+ ? await resolveThreadRootMessageId({
119
+ localRoomId,
120
+ roomId: targetRoomId,
121
+ projectId: targetProjectId,
122
+ messageId: replyTarget,
123
+ })
124
+ : undefined;
125
+ if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
126
+ const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
127
+ const message = await addLocalChatMessage(sqliteRoomId || localRoomId, {
128
+ sender: identity.actor_label,
129
+ text: input.text,
130
+ reply_to: replyTarget,
131
+ ...(resolvedThreadRoot ? { thread_root_id: resolvedThreadRoot } : {}),
132
+ source: "agent",
133
+ });
134
+ touchCurrentRoom(message.id);
135
+ return jsonToolResponse({
136
+ ...message,
137
+ agent_identity: toPublicAgentIdentity(identity),
138
+ });
139
+ }
140
+ const message = await roomScopedApiCall({
141
+ room_id: targetRoomId,
142
+ project_id: targetProjectId,
143
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages`,
144
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
145
+ options: {
146
+ method: "POST",
147
+ body: JSON.stringify(buildSendMessageRequestBody({
148
+ sender: identity.actor_label,
149
+ text: input.text,
150
+ replyTarget,
151
+ resolvedThreadRoot,
152
+ credentials: agentSessionCredentials(agentSession),
153
+ })),
154
+ },
155
+ });
156
+ touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
157
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity), agentSession);
158
+ return jsonToolResponse({
159
+ ...message,
160
+ agent_identity: toPublicAgentIdentity(identity),
161
+ });
162
+ }
163
+ export function registerSendMessageTool(server) {
164
+ server.tool("send_message", "Send a message to a Let Agents Chat room. For focused side discussion, prefer send_thread_message or pass thread_parent_id so the reply stays in that message thread instead of polluting the main room.", {
165
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
166
+ sender: z
167
+ .string()
168
+ .optional()
169
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
170
+ text: z.string().describe("The message text to send"),
171
+ reply_to: z
172
+ .string()
173
+ .optional()
174
+ .describe("Optional legacy quote-reply message id. For thread continuation, prefer thread_parent_id or send_thread_message."),
175
+ thread_parent_id: z
176
+ .string()
177
+ .optional()
178
+ .describe("Optional thread parent/root message id. If this points at a reply inside a thread, the tool resolves it to the root where possible."),
179
+ conversation_id: z
180
+ .string()
181
+ .optional()
182
+ .describe("Deprecated for worker writes; registered worker session identity is used."),
183
+ agent_session_id: z
184
+ .string()
185
+ .optional()
186
+ .describe("Registered agent session to use for this message. Required for worker messages."),
187
+ }, async ({ room_id, sender: _sender, text, reply_to, thread_parent_id, conversation_id: _conversation_id, agent_session_id }) => sendMessageFromTool({ room_id, text, reply_to, thread_parent_id, agent_session_id }));
188
+ server.tool("send_thread_message", "Reply inside an existing message thread without posting a top-level room message. Use this when discussion should drill into one topic and keep the main room clean.", {
189
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
190
+ text: z.string().describe("The thread reply text to send."),
191
+ thread_parent_id: z
192
+ .string()
193
+ .describe("Thread parent/root message id. You may pass any message id from the thread; the tool resolves replies to the root where possible."),
194
+ conversation_id: z
195
+ .string()
196
+ .optional()
197
+ .describe("Deprecated for worker writes; registered worker session identity is used."),
198
+ agent_session_id: z
199
+ .string()
200
+ .optional()
201
+ .describe("Registered agent session to use for this thread reply. Required for worker messages."),
202
+ }, async ({ room_id, text, thread_parent_id, conversation_id: _conversation_id, agent_session_id }) => sendMessageFromTool({ room_id, text, thread_parent_id, agent_session_id }));
203
+ }
@@ -0,0 +1,87 @@
1
+ import { z } from "zod";
2
+ import { classifyPresenceStatusText } from "../../../agent-presence.js";
3
+ import { encodeRoomIdPath } from "../../../room-id.js";
4
+ import { agentSessionCredentials, addLocalChatMessage, currentRoom, getFallbackProjectId, getRememberedRoomPresence, getTargetRoomId, isLocalRoomStorageEnabled, resolveLocalRoomStorageIdentifiers, resolveWorkerToolIdentity, roomScopedApiCall, syncRoomPresence, toPublicAgentIdentity, touchCurrentRoom, } from "../../runtime.js";
5
+ import { missingRoomResponse, jsonToolResponse } from "./response.js";
6
+ export function registerPostStatusTool(server) {
7
+ server.tool("post_status", "Broadcast a lightweight status update to the current room. " +
8
+ "Use this to let other agents and humans know what you are currently doing, " +
9
+ "e.g. 'reviewing PR #2', 'waiting for tests', 'writing WISHLIST.md'. " +
10
+ "Status updates are distinct from chat messages and can be filtered separately.", {
11
+ sender: z
12
+ .string()
13
+ .optional()
14
+ .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
15
+ status: z.string().describe("Short status description (e.g. 'reviewing PR #2', 'idle', 'thinking...')"),
16
+ room_id: z
17
+ .string()
18
+ .optional()
19
+ .describe("Canonical room ID. Defaults to the current room."),
20
+ conversation_id: z
21
+ .string()
22
+ .optional()
23
+ .describe("Deprecated for worker writes; registered worker session identity is used."),
24
+ agent_session_id: z
25
+ .string()
26
+ .optional()
27
+ .describe("Registered agent session to use for this status update. Required for worker status."),
28
+ }, async ({ sender: _sender, status, room_id, conversation_id: _conversation_id, agent_session_id }) => {
29
+ const targetRoomId = getTargetRoomId(room_id);
30
+ const targetProjectId = getFallbackProjectId();
31
+ if (!targetRoomId && !targetProjectId) {
32
+ return missingRoomResponse();
33
+ }
34
+ const { identity, agentSession } = await resolveWorkerToolIdentity({
35
+ roomId: targetRoomId ?? currentRoom?.room_id ?? null,
36
+ agentSessionId: agent_session_id,
37
+ });
38
+ const sender = identity.actor_label;
39
+ const statusText = `[status] ${status}`;
40
+ const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
41
+ if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
42
+ const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
43
+ const effectiveLocalRoomId = sqliteRoomId || localRoomId;
44
+ const message = await addLocalChatMessage(effectiveLocalRoomId, {
45
+ sender,
46
+ text: statusText,
47
+ source: "agent",
48
+ });
49
+ touchCurrentRoom(message.id);
50
+ return jsonToolResponse({
51
+ success: true,
52
+ status_posted: status,
53
+ sender,
54
+ agent_identity: toPublicAgentIdentity(identity),
55
+ message_id: message.id,
56
+ timestamp: message.timestamp,
57
+ });
58
+ }
59
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
60
+ status: classifyPresenceStatusText(status, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity).status),
61
+ status_text: status,
62
+ }, agentSession);
63
+ const message = await roomScopedApiCall({
64
+ room_id: targetRoomId,
65
+ project_id: targetProjectId,
66
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages`,
67
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
68
+ options: {
69
+ method: "POST",
70
+ body: JSON.stringify({
71
+ sender,
72
+ text: statusText,
73
+ ...agentSessionCredentials(agentSession),
74
+ }),
75
+ },
76
+ });
77
+ touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
78
+ return jsonToolResponse({
79
+ success: true,
80
+ status_posted: status,
81
+ sender,
82
+ agent_identity: toPublicAgentIdentity(identity),
83
+ message_id: typeof message.id === "string" ? message.id : null,
84
+ timestamp: typeof message.timestamp === "string" ? message.timestamp : null,
85
+ });
86
+ });
87
+ }