letagents 0.11.1 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +65 -0
- package/README.md +7 -4
- package/dist/mcp/codenames.js +5 -3
- package/dist/mcp/codex-session.js +500 -0
- package/dist/mcp/config-reader.js +1 -1
- package/dist/mcp/git-remote.js +3 -3
- package/dist/mcp/local-state.js +58 -0
- package/dist/mcp/server.js +337 -59
- package/dist/mcp/sse-client.js +19 -3
- package/dist/shared/room-agent-prompts.js +25 -0
- package/package.json +5 -3
package/dist/mcp/server.js
CHANGED
|
@@ -11,13 +11,39 @@ import { SseClient } from "./sse-client.js";
|
|
|
11
11
|
import { getRoomFromConfig } from "./config-reader.js";
|
|
12
12
|
import { getGitRemoteIdentity } from "./git-remote.js";
|
|
13
13
|
import { AGENT_CODENAME_SPACE, normalizeSlugSegment, normalizeAgentBaseName, pickLocalCodename, } from "./codenames.js";
|
|
14
|
-
import { clearPendingDeviceAuth, clearStoredAuth, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
|
|
14
|
+
import { clearPendingDeviceAuth, clearStoredAuth, getCurrentCodexLiveSession, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, listStoredCodexLiveSessions, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
|
|
15
15
|
import { encodeRoomIdPath, getCanonicalRoomWebPath, looksLikeInviteCode, normalizeInviteCode, } from "./room-id.js";
|
|
16
16
|
import { buildAgentActorLabel, formatOwnerAttribution, inferAgentIdeLabel, toTitleCaseCodename, } from "../shared/agent-identity.js";
|
|
17
|
+
import { buildRoomAgentPrompt, normalizeAgentPromptKind, } from "../shared/room-agent-prompts.js";
|
|
18
|
+
import { inspectLocalCodexSession, startLocalCodexSession, stopLocalCodexSession, toPublicCodexLiveSession, } from "./codex-session.js";
|
|
17
19
|
let currentRoom = null;
|
|
18
20
|
let currentAgentIdentityKey = "";
|
|
19
21
|
let currentAgentIdentity = null;
|
|
20
22
|
let currentAuthenticatedAccount = undefined;
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Conversation-scoped identity (Option C: per-conversation hints)
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
const MAX_CONVERSATION_IDENTITIES = 20;
|
|
27
|
+
const conversationIdentities = new Map();
|
|
28
|
+
/**
|
|
29
|
+
* Get or set a conversation-scoped identity override.
|
|
30
|
+
* Falls back to the global `currentAgentIdentity` when conversationId is absent.
|
|
31
|
+
*/
|
|
32
|
+
function getConversationIdentity(conversationId) {
|
|
33
|
+
if (!conversationId)
|
|
34
|
+
return currentAgentIdentity;
|
|
35
|
+
return conversationIdentities.get(conversationId) ?? currentAgentIdentity;
|
|
36
|
+
}
|
|
37
|
+
function setConversationIdentity(conversationId, identity) {
|
|
38
|
+
// LRU-style eviction: if at cap, remove oldest entry
|
|
39
|
+
if (!conversationIdentities.has(conversationId) &&
|
|
40
|
+
conversationIdentities.size >= MAX_CONVERSATION_IDENTITIES) {
|
|
41
|
+
const oldestKey = conversationIdentities.keys().next().value;
|
|
42
|
+
if (oldestKey !== undefined)
|
|
43
|
+
conversationIdentities.delete(oldestKey);
|
|
44
|
+
}
|
|
45
|
+
conversationIdentities.set(conversationId, identity);
|
|
46
|
+
}
|
|
21
47
|
let currentAuthenticatedAccountSource = null;
|
|
22
48
|
let currentAuthenticatedEnvToken = null;
|
|
23
49
|
// ---------------------------------------------------------------------------
|
|
@@ -722,6 +748,43 @@ function getLastMessageId(payload) {
|
|
|
722
748
|
const lastMessage = messages?.at(-1);
|
|
723
749
|
return typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
|
|
724
750
|
}
|
|
751
|
+
function withJoinRoomAgentPrompt(payload) {
|
|
752
|
+
return {
|
|
753
|
+
...payload,
|
|
754
|
+
agent_prompt_kind: "join",
|
|
755
|
+
agent_prompt: buildRoomAgentPrompt("join"),
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function normalizeJoinSessionMode(value) {
|
|
759
|
+
return String(value || "").trim().toLowerCase() === "live" ? "live" : "current";
|
|
760
|
+
}
|
|
761
|
+
function getCurrentLiveSessionPayload(roomId) {
|
|
762
|
+
const session = getCurrentCodexLiveSession(roomId);
|
|
763
|
+
return session ? toPublicCodexLiveSession(session) : null;
|
|
764
|
+
}
|
|
765
|
+
function toAgentReadableMessage(message) {
|
|
766
|
+
if (!message || typeof message !== "object") {
|
|
767
|
+
return message;
|
|
768
|
+
}
|
|
769
|
+
const record = message;
|
|
770
|
+
const kind = normalizeAgentPromptKind(record.agent_prompt_kind);
|
|
771
|
+
const text = typeof record.text === "string" ? record.text : null;
|
|
772
|
+
if (!kind || text === null) {
|
|
773
|
+
return record;
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
...record,
|
|
777
|
+
visible_text: text,
|
|
778
|
+
agent_prompt: buildRoomAgentPrompt(kind),
|
|
779
|
+
prompt_injected: kind === "inline",
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function toAgentReadableMessages(messages) {
|
|
783
|
+
return (messages ?? []).map((message) => toAgentReadableMessage(message));
|
|
784
|
+
}
|
|
785
|
+
function appendIncludePromptOnly(path) {
|
|
786
|
+
return `${path}${path.includes("?") ? "&" : "?"}include_prompt_only=1`;
|
|
787
|
+
}
|
|
725
788
|
async function roomScopedApiCall(input) {
|
|
726
789
|
if (input.room_id) {
|
|
727
790
|
try {
|
|
@@ -853,18 +916,46 @@ async function createInviteRoom() {
|
|
|
853
916
|
},
|
|
854
917
|
};
|
|
855
918
|
}
|
|
856
|
-
async function
|
|
919
|
+
async function buildJoinResponse(input) {
|
|
920
|
+
const basePayload = await withAgentIdentity({
|
|
921
|
+
...toPublicRoomResponse(input.joined.response, input.joined.room.room_id),
|
|
922
|
+
joined_via: input.joined_via,
|
|
923
|
+
session_mode: input.session_mode,
|
|
924
|
+
});
|
|
925
|
+
if (input.session_mode === "current") {
|
|
926
|
+
return withJoinRoomAgentPrompt(basePayload);
|
|
927
|
+
}
|
|
928
|
+
const liveSession = await startLocalCodexSession({
|
|
929
|
+
room_id: input.joined.room.room_id,
|
|
930
|
+
room_identifier: input.room_identifier,
|
|
931
|
+
room_code: input.joined.room.code ?? null,
|
|
932
|
+
room_display_name: input.joined.room.display_name ?? null,
|
|
933
|
+
joined_via: input.joined_via,
|
|
934
|
+
cwd: process.cwd(),
|
|
935
|
+
});
|
|
936
|
+
return withJoinRoomAgentPrompt({
|
|
937
|
+
...basePayload,
|
|
938
|
+
local_codex_session: toPublicCodexLiveSession(liveSession.session),
|
|
939
|
+
local_codex_session_started: !liveSession.reused,
|
|
940
|
+
local_codex_session_reused: liveSession.reused,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
async function joinInviteCode(code, sessionMode) {
|
|
857
944
|
const joined = await joinRoomIdentifier(code, "join_code");
|
|
858
|
-
return
|
|
859
|
-
|
|
945
|
+
return buildJoinResponse({
|
|
946
|
+
joined,
|
|
947
|
+
room_identifier: normalizeInviteCode(code),
|
|
860
948
|
joined_via: "join_code",
|
|
949
|
+
session_mode: sessionMode,
|
|
861
950
|
});
|
|
862
951
|
}
|
|
863
|
-
async function joinNamedRoom(name) {
|
|
952
|
+
async function joinNamedRoom(name, sessionMode) {
|
|
864
953
|
const joined = await joinRoomIdentifier(name, "join_room");
|
|
865
|
-
return
|
|
866
|
-
|
|
954
|
+
return buildJoinResponse({
|
|
955
|
+
joined,
|
|
956
|
+
room_identifier: name.trim(),
|
|
867
957
|
joined_via: "join_room",
|
|
958
|
+
session_mode: sessionMode,
|
|
868
959
|
});
|
|
869
960
|
}
|
|
870
961
|
// ---------------------------------------------------------------------------
|
|
@@ -895,8 +986,8 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
|
|
|
895
986
|
const result = await roomScopedApiCall({
|
|
896
987
|
room_id: normalizedRoomId,
|
|
897
988
|
project_id: storedSession?.project_id ?? null,
|
|
898
|
-
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}
|
|
899
|
-
project_path: (projectId) => `/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}
|
|
989
|
+
room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`),
|
|
990
|
+
project_path: (projectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`),
|
|
900
991
|
});
|
|
901
992
|
const msgs = result.messages ?? [];
|
|
902
993
|
allMessages.push(...msgs);
|
|
@@ -912,7 +1003,7 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
|
|
|
912
1003
|
{
|
|
913
1004
|
uri: uri.href,
|
|
914
1005
|
mimeType: "application/json",
|
|
915
|
-
text: JSON.stringify({ messages: allMessages }, null, 2),
|
|
1006
|
+
text: JSON.stringify({ messages: toAgentReadableMessages(allMessages) }, null, 2),
|
|
916
1007
|
},
|
|
917
1008
|
],
|
|
918
1009
|
};
|
|
@@ -924,7 +1015,7 @@ server.tool("create_room", "Create a new invite room on Let Agents Chat. Returns
|
|
|
924
1015
|
content: [
|
|
925
1016
|
{
|
|
926
1017
|
type: "text",
|
|
927
|
-
text: JSON.stringify(created.response, null, 2),
|
|
1018
|
+
text: JSON.stringify(withJoinRoomAgentPrompt(created.response), null, 2),
|
|
928
1019
|
},
|
|
929
1020
|
],
|
|
930
1021
|
};
|
|
@@ -936,7 +1027,7 @@ server.tool("create_project", "Legacy alias for create_room. Creates a new invit
|
|
|
936
1027
|
content: [
|
|
937
1028
|
{
|
|
938
1029
|
type: "text",
|
|
939
|
-
text: JSON.stringify(created.response, null, 2),
|
|
1030
|
+
text: JSON.stringify(withJoinRoomAgentPrompt(created.response), null, 2),
|
|
940
1031
|
},
|
|
941
1032
|
],
|
|
942
1033
|
};
|
|
@@ -944,12 +1035,16 @@ server.tool("create_project", "Legacy alias for create_room. Creates a new invit
|
|
|
944
1035
|
// -- join_code --------------------------------------------------------------
|
|
945
1036
|
server.tool("join_code", "Join an existing room using an invite code.", {
|
|
946
1037
|
code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
|
|
947
|
-
|
|
1038
|
+
session_mode: z
|
|
1039
|
+
.enum(["live", "current"])
|
|
1040
|
+
.optional()
|
|
1041
|
+
.describe("Use 'current' (default) for a normal inline join. Use 'live' to start/reuse a detached local Codex room worker."),
|
|
1042
|
+
}, async ({ code, session_mode }) => {
|
|
948
1043
|
return {
|
|
949
1044
|
content: [
|
|
950
1045
|
{
|
|
951
1046
|
type: "text",
|
|
952
|
-
text: JSON.stringify(await joinInviteCode(code), null, 2),
|
|
1047
|
+
text: JSON.stringify(await joinInviteCode(code, normalizeJoinSessionMode(session_mode)), null, 2),
|
|
953
1048
|
},
|
|
954
1049
|
],
|
|
955
1050
|
};
|
|
@@ -957,12 +1052,16 @@ server.tool("join_code", "Join an existing room using an invite code.", {
|
|
|
957
1052
|
// -- join_project -----------------------------------------------------------
|
|
958
1053
|
server.tool("join_project", "Legacy alias for join_code. Join an existing room using an invite code.", {
|
|
959
1054
|
code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
|
|
960
|
-
|
|
1055
|
+
session_mode: z
|
|
1056
|
+
.enum(["live", "current"])
|
|
1057
|
+
.optional()
|
|
1058
|
+
.describe("Use 'current' (default) for a normal inline join. Use 'live' to start/reuse a detached local Codex room worker."),
|
|
1059
|
+
}, async ({ code, session_mode }) => {
|
|
961
1060
|
return {
|
|
962
1061
|
content: [
|
|
963
1062
|
{
|
|
964
1063
|
type: "text",
|
|
965
|
-
text: JSON.stringify(await joinInviteCode(code), null, 2),
|
|
1064
|
+
text: JSON.stringify(await joinInviteCode(code, normalizeJoinSessionMode(session_mode)), null, 2),
|
|
966
1065
|
},
|
|
967
1066
|
],
|
|
968
1067
|
};
|
|
@@ -970,13 +1069,17 @@ server.tool("join_project", "Legacy alias for join_code. Join an existing room u
|
|
|
970
1069
|
// -- join_room --------------------------------------------------------------
|
|
971
1070
|
server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room if it doesn't exist. Use this for repo-based room joining.", {
|
|
972
1071
|
name: z.string().describe("The room name to join (e.g. 'github.com/owner/repo')"),
|
|
973
|
-
|
|
1072
|
+
session_mode: z
|
|
1073
|
+
.enum(["live", "current"])
|
|
1074
|
+
.optional()
|
|
1075
|
+
.describe("Use 'current' (default) for a normal inline join. Use 'live' to start/reuse a detached local Codex room worker."),
|
|
1076
|
+
}, async ({ name, session_mode }) => {
|
|
974
1077
|
try {
|
|
975
1078
|
return {
|
|
976
1079
|
content: [
|
|
977
1080
|
{
|
|
978
1081
|
type: "text",
|
|
979
|
-
text: JSON.stringify(await joinNamedRoom(name), null, 2),
|
|
1082
|
+
text: JSON.stringify(await joinNamedRoom(name, normalizeJoinSessionMode(session_mode)), null, 2),
|
|
980
1083
|
},
|
|
981
1084
|
],
|
|
982
1085
|
};
|
|
@@ -995,17 +1098,156 @@ server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room
|
|
|
995
1098
|
throw error;
|
|
996
1099
|
}
|
|
997
1100
|
});
|
|
1101
|
+
// -- local Codex live sessions ---------------------------------------------
|
|
1102
|
+
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.", {
|
|
1103
|
+
room: z
|
|
1104
|
+
.string()
|
|
1105
|
+
.describe("Invite code or room name to run as a detached local Codex worker."),
|
|
1106
|
+
cwd: z
|
|
1107
|
+
.string()
|
|
1108
|
+
.optional()
|
|
1109
|
+
.describe("Working directory for repo work. Defaults to the current process directory."),
|
|
1110
|
+
stop_phrase: z
|
|
1111
|
+
.string()
|
|
1112
|
+
.optional()
|
|
1113
|
+
.describe("Exact room message text that tells the worker to stop. Defaults to /stop-codex-room."),
|
|
1114
|
+
max_minutes: z
|
|
1115
|
+
.number()
|
|
1116
|
+
.optional()
|
|
1117
|
+
.describe("Optional hard stop in minutes. Defaults to 0, which means run until stopped."),
|
|
1118
|
+
}, async ({ room, cwd, stop_phrase, max_minutes }) => {
|
|
1119
|
+
const joinedVia = looksLikeInviteCode(room) ? "join_code" : "join_room";
|
|
1120
|
+
try {
|
|
1121
|
+
const joined = await joinRoomIdentifier(room, joinedVia);
|
|
1122
|
+
const liveSession = await startLocalCodexSession({
|
|
1123
|
+
room_id: joined.room.room_id,
|
|
1124
|
+
room_identifier: joinedVia === "join_code" ? normalizeInviteCode(room) : room.trim(),
|
|
1125
|
+
room_code: joined.room.code ?? null,
|
|
1126
|
+
room_display_name: joined.room.display_name ?? null,
|
|
1127
|
+
joined_via: joinedVia,
|
|
1128
|
+
cwd: cwd || process.cwd(),
|
|
1129
|
+
stop_phrase,
|
|
1130
|
+
max_minutes,
|
|
1131
|
+
});
|
|
1132
|
+
return {
|
|
1133
|
+
content: [
|
|
1134
|
+
{
|
|
1135
|
+
type: "text",
|
|
1136
|
+
text: JSON.stringify(await withAgentIdentity({
|
|
1137
|
+
success: true,
|
|
1138
|
+
room: toPublicRoomState(joined.room),
|
|
1139
|
+
local_codex_session: toPublicCodexLiveSession(liveSession.session),
|
|
1140
|
+
local_codex_session_started: !liveSession.reused,
|
|
1141
|
+
local_codex_session_reused: liveSession.reused,
|
|
1142
|
+
}), null, 2),
|
|
1143
|
+
},
|
|
1144
|
+
],
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
if (error instanceof RepoRoomAuthRequiredError) {
|
|
1149
|
+
return {
|
|
1150
|
+
content: [
|
|
1151
|
+
{
|
|
1152
|
+
type: "text",
|
|
1153
|
+
text: JSON.stringify(toRepoRoomAuthRequiredResult(error), null, 2),
|
|
1154
|
+
},
|
|
1155
|
+
],
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
throw error;
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
server.tool("status_local_codex_session", "Inspect the current detached local Codex live session, or a specific one by session_id.", {
|
|
1162
|
+
session_id: z
|
|
1163
|
+
.string()
|
|
1164
|
+
.optional()
|
|
1165
|
+
.describe("Optional session id. Defaults to the current local Codex live session."),
|
|
1166
|
+
}, async ({ session_id }) => {
|
|
1167
|
+
const status = await inspectLocalCodexSession(session_id, currentRoom?.room_id);
|
|
1168
|
+
if (!status) {
|
|
1169
|
+
return {
|
|
1170
|
+
content: [
|
|
1171
|
+
{
|
|
1172
|
+
type: "text",
|
|
1173
|
+
text: JSON.stringify({ success: false, error: "No local Codex live session found." }, null, 2),
|
|
1174
|
+
},
|
|
1175
|
+
],
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
content: [
|
|
1180
|
+
{
|
|
1181
|
+
type: "text",
|
|
1182
|
+
text: JSON.stringify({
|
|
1183
|
+
success: true,
|
|
1184
|
+
session: toPublicCodexLiveSession(status.session),
|
|
1185
|
+
server_reachable: status.server_reachable,
|
|
1186
|
+
thread_status: status.thread_status,
|
|
1187
|
+
turn_status: status.turn_status,
|
|
1188
|
+
recent_items: status.recent_items,
|
|
1189
|
+
}, null, 2),
|
|
1190
|
+
},
|
|
1191
|
+
],
|
|
1192
|
+
};
|
|
1193
|
+
});
|
|
1194
|
+
server.tool("stop_local_codex_session", "Stop the current detached local Codex live session, or a specific one by session_id.", {
|
|
1195
|
+
session_id: z
|
|
1196
|
+
.string()
|
|
1197
|
+
.optional()
|
|
1198
|
+
.describe("Optional session id. Defaults to the current local Codex live session."),
|
|
1199
|
+
shutdown_server: z
|
|
1200
|
+
.boolean()
|
|
1201
|
+
.optional()
|
|
1202
|
+
.describe("If true, also terminate the spawned codex app-server process when possible."),
|
|
1203
|
+
}, async ({ session_id, shutdown_server }) => {
|
|
1204
|
+
const stopped = await stopLocalCodexSession({
|
|
1205
|
+
session_id,
|
|
1206
|
+
room_id: currentRoom?.room_id,
|
|
1207
|
+
shutdown_server,
|
|
1208
|
+
});
|
|
1209
|
+
if (!stopped) {
|
|
1210
|
+
return {
|
|
1211
|
+
content: [
|
|
1212
|
+
{
|
|
1213
|
+
type: "text",
|
|
1214
|
+
text: JSON.stringify({ success: false, error: "No local Codex live session found." }, null, 2),
|
|
1215
|
+
},
|
|
1216
|
+
],
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
return {
|
|
1220
|
+
content: [
|
|
1221
|
+
{
|
|
1222
|
+
type: "text",
|
|
1223
|
+
text: JSON.stringify({
|
|
1224
|
+
success: true,
|
|
1225
|
+
session: toPublicCodexLiveSession(stopped),
|
|
1226
|
+
}, null, 2),
|
|
1227
|
+
},
|
|
1228
|
+
],
|
|
1229
|
+
};
|
|
1230
|
+
});
|
|
998
1231
|
// -- get_current_room -------------------------------------------------------
|
|
999
|
-
server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {
|
|
1232
|
+
server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {
|
|
1233
|
+
conversation_id: z
|
|
1234
|
+
.string()
|
|
1235
|
+
.optional()
|
|
1236
|
+
.describe("Optional conversation ID to report the conversation-scoped identity instead of the global one."),
|
|
1237
|
+
}, async ({ conversation_id }) => {
|
|
1000
1238
|
return {
|
|
1001
1239
|
content: [
|
|
1002
1240
|
{
|
|
1003
1241
|
type: "text",
|
|
1004
1242
|
text: JSON.stringify(currentRoom
|
|
1005
|
-
? {
|
|
1243
|
+
? withJoinRoomAgentPrompt({
|
|
1006
1244
|
connected: true,
|
|
1007
1245
|
...toPublicRoomState(currentRoom),
|
|
1008
|
-
|
|
1246
|
+
current_local_codex_session: getCurrentLiveSessionPayload(currentRoom.room_id),
|
|
1247
|
+
local_codex_session_count: listStoredCodexLiveSessions().length,
|
|
1248
|
+
agent_identity: toPublicAgentIdentity(getConversationIdentity(conversation_id)
|
|
1249
|
+
?? currentAgentIdentity
|
|
1250
|
+
?? getStoredAgentIdentity(currentAgentIdentityKey)),
|
|
1009
1251
|
auth: getStoredAuth()
|
|
1010
1252
|
? {
|
|
1011
1253
|
source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
|
|
@@ -1013,8 +1255,13 @@ server.tool("get_current_room", "Get information about the currently joined room
|
|
|
1013
1255
|
account: getStoredAuth()?.account ?? null,
|
|
1014
1256
|
}
|
|
1015
1257
|
: null,
|
|
1016
|
-
}
|
|
1017
|
-
: {
|
|
1258
|
+
})
|
|
1259
|
+
: {
|
|
1260
|
+
connected: false,
|
|
1261
|
+
message: "Not currently in any room",
|
|
1262
|
+
current_local_codex_session: getCurrentLiveSessionPayload(), // no room context
|
|
1263
|
+
local_codex_session_count: listStoredCodexLiveSessions().length,
|
|
1264
|
+
}, null, 2),
|
|
1018
1265
|
},
|
|
1019
1266
|
],
|
|
1020
1267
|
};
|
|
@@ -1078,7 +1325,11 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
1078
1325
|
.string()
|
|
1079
1326
|
.optional()
|
|
1080
1327
|
.describe("Canonical room ID. Defaults to the current room."),
|
|
1081
|
-
|
|
1328
|
+
conversation_id: z
|
|
1329
|
+
.string()
|
|
1330
|
+
.optional()
|
|
1331
|
+
.describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1332
|
+
}, async ({ sender: _sender, status, room_id, conversation_id }) => {
|
|
1082
1333
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1083
1334
|
const targetProjectId = getFallbackProjectId();
|
|
1084
1335
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1097,7 +1348,7 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
1097
1348
|
}
|
|
1098
1349
|
// Status messages use a reserved prefix so the UI (and agents) can distinguish
|
|
1099
1350
|
// them from normal chat messages without changing the data model.
|
|
1100
|
-
const identity = await ensureAgentIdentity();
|
|
1351
|
+
const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
|
|
1101
1352
|
const sender = identity.actor_label;
|
|
1102
1353
|
const statusText = `[status] ${status}`;
|
|
1103
1354
|
const message = await roomScopedApiCall({
|
|
@@ -1144,7 +1395,8 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
|
|
|
1144
1395
|
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
1145
1396
|
source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
|
|
1146
1397
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1147
|
-
|
|
1398
|
+
conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1399
|
+
}, async ({ title, description, created_by: _createdBy, source_message_id, room_id, conversation_id }) => {
|
|
1148
1400
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1149
1401
|
const targetProjectId = getFallbackProjectId();
|
|
1150
1402
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1152,7 +1404,7 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
|
|
|
1152
1404
|
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
|
|
1153
1405
|
};
|
|
1154
1406
|
}
|
|
1155
|
-
const identity = await ensureAgentIdentity();
|
|
1407
|
+
const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
|
|
1156
1408
|
const task = await roomScopedApiCall({
|
|
1157
1409
|
room_id: targetRoomId,
|
|
1158
1410
|
project_id: targetProjectId,
|
|
@@ -1232,7 +1484,8 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
1232
1484
|
.optional()
|
|
1233
1485
|
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
1234
1486
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1235
|
-
|
|
1487
|
+
conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1488
|
+
}, async ({ task_id, assignee: _assignee, room_id, conversation_id }) => {
|
|
1236
1489
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1237
1490
|
const targetProjectId = getFallbackProjectId();
|
|
1238
1491
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1241,7 +1494,7 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
1241
1494
|
};
|
|
1242
1495
|
}
|
|
1243
1496
|
try {
|
|
1244
|
-
const identity = await ensureAgentIdentity();
|
|
1497
|
+
const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
|
|
1245
1498
|
const updated = await roomScopedApiCall({
|
|
1246
1499
|
room_id: targetRoomId,
|
|
1247
1500
|
project_id: targetProjectId,
|
|
@@ -1278,7 +1531,8 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1278
1531
|
.describe("New assignee for the task. Defaults to the current agent when status=assigned."),
|
|
1279
1532
|
pr_url: z.string().optional().describe("PR URL to link to the task"),
|
|
1280
1533
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1281
|
-
|
|
1534
|
+
conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1535
|
+
}, async ({ task_id, status, assignee, pr_url, room_id, conversation_id }) => {
|
|
1282
1536
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1283
1537
|
const targetProjectId = getFallbackProjectId();
|
|
1284
1538
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1288,7 +1542,7 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1288
1542
|
}
|
|
1289
1543
|
try {
|
|
1290
1544
|
const identity = status === "assigned" && !assignee
|
|
1291
|
-
? await ensureAgentIdentity()
|
|
1545
|
+
? (getConversationIdentity(conversation_id) ?? await ensureAgentIdentity())
|
|
1292
1546
|
: null;
|
|
1293
1547
|
const updated = await roomScopedApiCall({
|
|
1294
1548
|
room_id: targetRoomId,
|
|
@@ -1329,7 +1583,8 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
|
|
|
1329
1583
|
task_id: z.string().describe("The task ID to submit for review"),
|
|
1330
1584
|
pr_url: z.string().optional().describe("GitHub PR URL for the work"),
|
|
1331
1585
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1332
|
-
|
|
1586
|
+
conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1587
|
+
}, async ({ task_id, pr_url, room_id, conversation_id: _conversationId }) => {
|
|
1333
1588
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1334
1589
|
const targetProjectId = getFallbackProjectId();
|
|
1335
1590
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1510,13 +1765,17 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
|
|
|
1510
1765
|
.optional()
|
|
1511
1766
|
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
1512
1767
|
text: z.string().describe("The message text to send"),
|
|
1513
|
-
|
|
1768
|
+
conversation_id: z
|
|
1769
|
+
.string()
|
|
1770
|
+
.optional()
|
|
1771
|
+
.describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1772
|
+
}, async ({ room_id, sender: _sender, text, conversation_id }) => {
|
|
1514
1773
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1515
1774
|
const targetProjectId = getFallbackProjectId();
|
|
1516
1775
|
if (!targetRoomId && !targetProjectId) {
|
|
1517
1776
|
throw new Error("No room is currently selected. Join a room first or pass room_id.");
|
|
1518
1777
|
}
|
|
1519
|
-
const identity = await ensureAgentIdentity();
|
|
1778
|
+
const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
|
|
1520
1779
|
const message = await roomScopedApiCall({
|
|
1521
1780
|
room_id: targetRoomId,
|
|
1522
1781
|
project_id: targetProjectId,
|
|
@@ -1558,8 +1817,8 @@ server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
|
|
|
1558
1817
|
const result = await roomScopedApiCall({
|
|
1559
1818
|
room_id: targetRoomId,
|
|
1560
1819
|
project_id: targetProjectId,
|
|
1561
|
-
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}
|
|
1562
|
-
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages${qs ? `?${qs}` : ""}
|
|
1820
|
+
room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`),
|
|
1821
|
+
project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages${qs ? `?${qs}` : ""}`),
|
|
1563
1822
|
});
|
|
1564
1823
|
roomIdFromResponse = roomIdFromResponse || result.room_id || result.project_id;
|
|
1565
1824
|
const msgs = result.messages ?? [];
|
|
@@ -1572,7 +1831,7 @@ server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
|
|
|
1572
1831
|
break;
|
|
1573
1832
|
afterCursor = lastMsg.id;
|
|
1574
1833
|
}
|
|
1575
|
-
const output = { messages: allMessages };
|
|
1834
|
+
const output = { messages: toAgentReadableMessages(allMessages) };
|
|
1576
1835
|
if (roomIdFromResponse) {
|
|
1577
1836
|
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
1578
1837
|
}
|
|
@@ -1611,8 +1870,8 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
1611
1870
|
const firstResult = await roomScopedApiCall({
|
|
1612
1871
|
room_id: targetRoomId,
|
|
1613
1872
|
project_id: targetProjectId,
|
|
1614
|
-
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}
|
|
1615
|
-
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}
|
|
1873
|
+
room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`),
|
|
1874
|
+
project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`),
|
|
1616
1875
|
options: { signal: AbortSignal.timeout(clientTimeout) },
|
|
1617
1876
|
});
|
|
1618
1877
|
const allMessages = [...(firstResult.messages ?? [])];
|
|
@@ -1627,8 +1886,8 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
1627
1886
|
const page = await roomScopedApiCall({
|
|
1628
1887
|
room_id: targetRoomId,
|
|
1629
1888
|
project_id: targetProjectId,
|
|
1630
|
-
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages?${qs}
|
|
1631
|
-
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages?${qs}
|
|
1889
|
+
room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages?${qs}`),
|
|
1890
|
+
project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages?${qs}`),
|
|
1632
1891
|
});
|
|
1633
1892
|
const msgs = page.messages ?? [];
|
|
1634
1893
|
allMessages.push(...msgs);
|
|
@@ -1639,7 +1898,7 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
1639
1898
|
break;
|
|
1640
1899
|
}
|
|
1641
1900
|
}
|
|
1642
|
-
const output = { messages: allMessages };
|
|
1901
|
+
const output = { messages: toAgentReadableMessages(allMessages) };
|
|
1643
1902
|
if (roomIdFromResponse) {
|
|
1644
1903
|
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
1645
1904
|
}
|
|
@@ -1898,7 +2157,11 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
|
|
|
1898
2157
|
.min(2)
|
|
1899
2158
|
.max(64)
|
|
1900
2159
|
.describe("The desired display name for this agent (2-64 characters)."),
|
|
1901
|
-
|
|
2160
|
+
conversation_id: z
|
|
2161
|
+
.string()
|
|
2162
|
+
.optional()
|
|
2163
|
+
.describe("Optional conversation ID to scope this name change. When provided, only this conversation uses the new name; other conversations keep their own identity."),
|
|
2164
|
+
}, async ({ name: desiredName, conversation_id }) => {
|
|
1902
2165
|
const trimmedName = desiredName.trim();
|
|
1903
2166
|
if (trimmedName.length < 2 || trimmedName.length > 64) {
|
|
1904
2167
|
return {
|
|
@@ -1924,14 +2187,6 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
|
|
|
1924
2187
|
const slugName = normalizeAgentBaseName(trimmedName);
|
|
1925
2188
|
try {
|
|
1926
2189
|
const owner = await resolveOwnerContext();
|
|
1927
|
-
const registered = await apiCall("/agents", {
|
|
1928
|
-
method: "POST",
|
|
1929
|
-
body: JSON.stringify({
|
|
1930
|
-
name: slugName,
|
|
1931
|
-
display_name: trimmedName,
|
|
1932
|
-
owner_label: owner.label,
|
|
1933
|
-
}),
|
|
1934
|
-
});
|
|
1935
2190
|
const ideLabel = detectAgentIdeLabel();
|
|
1936
2191
|
const ownerAttribution = formatOwnerAttribution(owner.label);
|
|
1937
2192
|
const actorLabel = buildAgentActorLabel({
|
|
@@ -1939,6 +2194,22 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
|
|
|
1939
2194
|
owner_label: owner.label,
|
|
1940
2195
|
ide_label: ideLabel,
|
|
1941
2196
|
});
|
|
2197
|
+
// When conversation_id is provided, skip server-side /agents registration
|
|
2198
|
+
// to avoid creating stranded durable agent records from ephemeral renames.
|
|
2199
|
+
let canonicalKey = owner.login ? `${owner.login}/${slugName}` : null;
|
|
2200
|
+
if (!conversation_id) {
|
|
2201
|
+
const registered = await apiCall("/agents", {
|
|
2202
|
+
method: "POST",
|
|
2203
|
+
body: JSON.stringify({
|
|
2204
|
+
name: slugName,
|
|
2205
|
+
display_name: trimmedName,
|
|
2206
|
+
owner_label: owner.label,
|
|
2207
|
+
}),
|
|
2208
|
+
});
|
|
2209
|
+
if (typeof registered.canonical_key === "string") {
|
|
2210
|
+
canonicalKey = registered.canonical_key;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
1942
2213
|
const updatedIdentity = {
|
|
1943
2214
|
name: slugName,
|
|
1944
2215
|
display_name: trimmedName,
|
|
@@ -1946,14 +2217,19 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
|
|
|
1946
2217
|
owner_attribution: ownerAttribution,
|
|
1947
2218
|
ide_label: ideLabel,
|
|
1948
2219
|
actor_label: actorLabel,
|
|
1949
|
-
canonical_key:
|
|
1950
|
-
? registered.canonical_key
|
|
1951
|
-
: owner.login ? `${owner.login}/${slugName}` : null,
|
|
2220
|
+
canonical_key: canonicalKey,
|
|
1952
2221
|
runtime_key: currentAgentIdentityKey,
|
|
1953
|
-
source: "api",
|
|
2222
|
+
source: conversation_id ? "local" : "api",
|
|
1954
2223
|
resolved_at: new Date().toISOString(),
|
|
1955
2224
|
};
|
|
1956
|
-
|
|
2225
|
+
if (conversation_id) {
|
|
2226
|
+
// Scope to this conversation only
|
|
2227
|
+
setConversationIdentity(conversation_id, updatedIdentity);
|
|
2228
|
+
}
|
|
2229
|
+
else {
|
|
2230
|
+
// Global rename (backward compatible)
|
|
2231
|
+
currentAgentIdentity = setStoredAgentIdentity(updatedIdentity, currentAgentIdentityKey);
|
|
2232
|
+
}
|
|
1957
2233
|
return {
|
|
1958
2234
|
content: [
|
|
1959
2235
|
{
|
|
@@ -1961,7 +2237,9 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
|
|
|
1961
2237
|
text: JSON.stringify({
|
|
1962
2238
|
success: true,
|
|
1963
2239
|
message: `Agent name changed to "${trimmedName}".`,
|
|
1964
|
-
agent_identity: toPublicAgentIdentity(
|
|
2240
|
+
agent_identity: toPublicAgentIdentity(conversation_id
|
|
2241
|
+
? getConversationIdentity(conversation_id)
|
|
2242
|
+
: currentAgentIdentity),
|
|
1965
2243
|
}, null, 2),
|
|
1966
2244
|
},
|
|
1967
2245
|
],
|
|
@@ -2006,14 +2284,14 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
|
|
|
2006
2284
|
content: [
|
|
2007
2285
|
{
|
|
2008
2286
|
type: "text",
|
|
2009
|
-
text: JSON.stringify({
|
|
2287
|
+
text: JSON.stringify(withJoinRoomAgentPrompt({
|
|
2010
2288
|
success: true,
|
|
2011
2289
|
rejoined_from_local_state: true,
|
|
2012
2290
|
server_session_resumed: false,
|
|
2013
2291
|
last_message_id_before_restart: savedRoom.last_message_id ?? null,
|
|
2014
2292
|
room: toPublicRoomState(joined.room),
|
|
2015
2293
|
agent_identity: toPublicAgentIdentity(agentIdentity),
|
|
2016
|
-
}, null, 2),
|
|
2294
|
+
}), null, 2),
|
|
2017
2295
|
},
|
|
2018
2296
|
],
|
|
2019
2297
|
};
|