letagents 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/codenames.js +1 -1
- package/dist/mcp/local-state.js +6 -0
- package/dist/mcp/server.js +90 -28
- package/package.json +1 -1
package/dist/mcp/codenames.js
CHANGED
|
@@ -135,7 +135,7 @@ export function normalizeAgentBaseName(input) {
|
|
|
135
135
|
// ---------------------------------------------------------------------------
|
|
136
136
|
export function hashStringToIndex(value, modulo) {
|
|
137
137
|
const digest = createHash("sha256").update(value).digest();
|
|
138
|
-
return digest.
|
|
138
|
+
return digest.readUInt32BE(0) % modulo;
|
|
139
139
|
}
|
|
140
140
|
export function codenameFromIndex(index) {
|
|
141
141
|
const normalizedIndex = ((index % AGENT_CODENAME_SPACE) + AGENT_CODENAME_SPACE) % AGENT_CODENAME_SPACE;
|
package/dist/mcp/local-state.js
CHANGED
|
@@ -157,6 +157,12 @@ export function getStoredAgentIdentity(identityKey) {
|
|
|
157
157
|
if (scoped) {
|
|
158
158
|
return scoped;
|
|
159
159
|
}
|
|
160
|
+
// For UUID-based identity keys, do NOT fall back to the shared global
|
|
161
|
+
// agent_identity — that would cause different processes to inherit each
|
|
162
|
+
// other's identity. Only legacy (non-UUID) keys use the global fallback.
|
|
163
|
+
if (identityKey.startsWith("instance:")) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
160
166
|
}
|
|
161
167
|
return state.agent_identity ?? null;
|
|
162
168
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -18,6 +18,30 @@ let currentRoom = null;
|
|
|
18
18
|
let currentAgentIdentityKey = "";
|
|
19
19
|
let currentAgentIdentity = null;
|
|
20
20
|
let currentAuthenticatedAccount = undefined;
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Conversation-scoped identity (Option C: per-conversation hints)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const MAX_CONVERSATION_IDENTITIES = 20;
|
|
25
|
+
const conversationIdentities = new Map();
|
|
26
|
+
/**
|
|
27
|
+
* Get or set a conversation-scoped identity override.
|
|
28
|
+
* Falls back to the global `currentAgentIdentity` when conversationId is absent.
|
|
29
|
+
*/
|
|
30
|
+
function getConversationIdentity(conversationId) {
|
|
31
|
+
if (!conversationId)
|
|
32
|
+
return currentAgentIdentity;
|
|
33
|
+
return conversationIdentities.get(conversationId) ?? currentAgentIdentity;
|
|
34
|
+
}
|
|
35
|
+
function setConversationIdentity(conversationId, identity) {
|
|
36
|
+
// LRU-style eviction: if at cap, remove oldest entry
|
|
37
|
+
if (!conversationIdentities.has(conversationId) &&
|
|
38
|
+
conversationIdentities.size >= MAX_CONVERSATION_IDENTITIES) {
|
|
39
|
+
const oldestKey = conversationIdentities.keys().next().value;
|
|
40
|
+
if (oldestKey !== undefined)
|
|
41
|
+
conversationIdentities.delete(oldestKey);
|
|
42
|
+
}
|
|
43
|
+
conversationIdentities.set(conversationId, identity);
|
|
44
|
+
}
|
|
21
45
|
let currentAuthenticatedAccountSource = null;
|
|
22
46
|
let currentAuthenticatedEnvToken = null;
|
|
23
47
|
// ---------------------------------------------------------------------------
|
|
@@ -996,7 +1020,12 @@ server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room
|
|
|
996
1020
|
}
|
|
997
1021
|
});
|
|
998
1022
|
// -- get_current_room -------------------------------------------------------
|
|
999
|
-
server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {
|
|
1023
|
+
server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {
|
|
1024
|
+
conversation_id: z
|
|
1025
|
+
.string()
|
|
1026
|
+
.optional()
|
|
1027
|
+
.describe("Optional conversation ID to report the conversation-scoped identity instead of the global one."),
|
|
1028
|
+
}, async ({ conversation_id }) => {
|
|
1000
1029
|
return {
|
|
1001
1030
|
content: [
|
|
1002
1031
|
{
|
|
@@ -1005,7 +1034,9 @@ server.tool("get_current_room", "Get information about the currently joined room
|
|
|
1005
1034
|
? {
|
|
1006
1035
|
connected: true,
|
|
1007
1036
|
...toPublicRoomState(currentRoom),
|
|
1008
|
-
agent_identity: toPublicAgentIdentity(
|
|
1037
|
+
agent_identity: toPublicAgentIdentity(getConversationIdentity(conversation_id)
|
|
1038
|
+
?? currentAgentIdentity
|
|
1039
|
+
?? getStoredAgentIdentity(currentAgentIdentityKey)),
|
|
1009
1040
|
auth: getStoredAuth()
|
|
1010
1041
|
? {
|
|
1011
1042
|
source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
|
|
@@ -1078,7 +1109,11 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
1078
1109
|
.string()
|
|
1079
1110
|
.optional()
|
|
1080
1111
|
.describe("Canonical room ID. Defaults to the current room."),
|
|
1081
|
-
|
|
1112
|
+
conversation_id: z
|
|
1113
|
+
.string()
|
|
1114
|
+
.optional()
|
|
1115
|
+
.describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1116
|
+
}, async ({ sender: _sender, status, room_id, conversation_id }) => {
|
|
1082
1117
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1083
1118
|
const targetProjectId = getFallbackProjectId();
|
|
1084
1119
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1097,7 +1132,7 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
1097
1132
|
}
|
|
1098
1133
|
// Status messages use a reserved prefix so the UI (and agents) can distinguish
|
|
1099
1134
|
// them from normal chat messages without changing the data model.
|
|
1100
|
-
const identity = await ensureAgentIdentity();
|
|
1135
|
+
const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
|
|
1101
1136
|
const sender = identity.actor_label;
|
|
1102
1137
|
const statusText = `[status] ${status}`;
|
|
1103
1138
|
const message = await roomScopedApiCall({
|
|
@@ -1144,7 +1179,8 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
|
|
|
1144
1179
|
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
1145
1180
|
source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
|
|
1146
1181
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1147
|
-
|
|
1182
|
+
conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1183
|
+
}, async ({ title, description, created_by: _createdBy, source_message_id, room_id, conversation_id }) => {
|
|
1148
1184
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1149
1185
|
const targetProjectId = getFallbackProjectId();
|
|
1150
1186
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1152,7 +1188,7 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
|
|
|
1152
1188
|
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
|
|
1153
1189
|
};
|
|
1154
1190
|
}
|
|
1155
|
-
const identity = await ensureAgentIdentity();
|
|
1191
|
+
const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
|
|
1156
1192
|
const task = await roomScopedApiCall({
|
|
1157
1193
|
room_id: targetRoomId,
|
|
1158
1194
|
project_id: targetProjectId,
|
|
@@ -1232,7 +1268,8 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
1232
1268
|
.optional()
|
|
1233
1269
|
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
1234
1270
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1235
|
-
|
|
1271
|
+
conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1272
|
+
}, async ({ task_id, assignee: _assignee, room_id, conversation_id }) => {
|
|
1236
1273
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1237
1274
|
const targetProjectId = getFallbackProjectId();
|
|
1238
1275
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1241,7 +1278,7 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
1241
1278
|
};
|
|
1242
1279
|
}
|
|
1243
1280
|
try {
|
|
1244
|
-
const identity = await ensureAgentIdentity();
|
|
1281
|
+
const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
|
|
1245
1282
|
const updated = await roomScopedApiCall({
|
|
1246
1283
|
room_id: targetRoomId,
|
|
1247
1284
|
project_id: targetProjectId,
|
|
@@ -1278,7 +1315,8 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1278
1315
|
.describe("New assignee for the task. Defaults to the current agent when status=assigned."),
|
|
1279
1316
|
pr_url: z.string().optional().describe("PR URL to link to the task"),
|
|
1280
1317
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1281
|
-
|
|
1318
|
+
conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1319
|
+
}, async ({ task_id, status, assignee, pr_url, room_id, conversation_id }) => {
|
|
1282
1320
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1283
1321
|
const targetProjectId = getFallbackProjectId();
|
|
1284
1322
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1288,7 +1326,7 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1288
1326
|
}
|
|
1289
1327
|
try {
|
|
1290
1328
|
const identity = status === "assigned" && !assignee
|
|
1291
|
-
? await ensureAgentIdentity()
|
|
1329
|
+
? (getConversationIdentity(conversation_id) ?? await ensureAgentIdentity())
|
|
1292
1330
|
: null;
|
|
1293
1331
|
const updated = await roomScopedApiCall({
|
|
1294
1332
|
room_id: targetRoomId,
|
|
@@ -1329,7 +1367,8 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
|
|
|
1329
1367
|
task_id: z.string().describe("The task ID to submit for review"),
|
|
1330
1368
|
pr_url: z.string().optional().describe("GitHub PR URL for the work"),
|
|
1331
1369
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1332
|
-
|
|
1370
|
+
conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1371
|
+
}, async ({ task_id, pr_url, room_id, conversation_id: _conversationId }) => {
|
|
1333
1372
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1334
1373
|
const targetProjectId = getFallbackProjectId();
|
|
1335
1374
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1510,13 +1549,17 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
|
|
|
1510
1549
|
.optional()
|
|
1511
1550
|
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
1512
1551
|
text: z.string().describe("The message text to send"),
|
|
1513
|
-
|
|
1552
|
+
conversation_id: z
|
|
1553
|
+
.string()
|
|
1554
|
+
.optional()
|
|
1555
|
+
.describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1556
|
+
}, async ({ room_id, sender: _sender, text, conversation_id }) => {
|
|
1514
1557
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1515
1558
|
const targetProjectId = getFallbackProjectId();
|
|
1516
1559
|
if (!targetRoomId && !targetProjectId) {
|
|
1517
1560
|
throw new Error("No room is currently selected. Join a room first or pass room_id.");
|
|
1518
1561
|
}
|
|
1519
|
-
const identity = await ensureAgentIdentity();
|
|
1562
|
+
const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
|
|
1520
1563
|
const message = await roomScopedApiCall({
|
|
1521
1564
|
room_id: targetRoomId,
|
|
1522
1565
|
project_id: targetProjectId,
|
|
@@ -1898,7 +1941,11 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
|
|
|
1898
1941
|
.min(2)
|
|
1899
1942
|
.max(64)
|
|
1900
1943
|
.describe("The desired display name for this agent (2-64 characters)."),
|
|
1901
|
-
|
|
1944
|
+
conversation_id: z
|
|
1945
|
+
.string()
|
|
1946
|
+
.optional()
|
|
1947
|
+
.describe("Optional conversation ID to scope this name change. When provided, only this conversation uses the new name; other conversations keep their own identity."),
|
|
1948
|
+
}, async ({ name: desiredName, conversation_id }) => {
|
|
1902
1949
|
const trimmedName = desiredName.trim();
|
|
1903
1950
|
if (trimmedName.length < 2 || trimmedName.length > 64) {
|
|
1904
1951
|
return {
|
|
@@ -1924,14 +1971,6 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
|
|
|
1924
1971
|
const slugName = normalizeAgentBaseName(trimmedName);
|
|
1925
1972
|
try {
|
|
1926
1973
|
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
1974
|
const ideLabel = detectAgentIdeLabel();
|
|
1936
1975
|
const ownerAttribution = formatOwnerAttribution(owner.label);
|
|
1937
1976
|
const actorLabel = buildAgentActorLabel({
|
|
@@ -1939,6 +1978,22 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
|
|
|
1939
1978
|
owner_label: owner.label,
|
|
1940
1979
|
ide_label: ideLabel,
|
|
1941
1980
|
});
|
|
1981
|
+
// When conversation_id is provided, skip server-side /agents registration
|
|
1982
|
+
// to avoid creating stranded durable agent records from ephemeral renames.
|
|
1983
|
+
let canonicalKey = owner.login ? `${owner.login}/${slugName}` : null;
|
|
1984
|
+
if (!conversation_id) {
|
|
1985
|
+
const registered = await apiCall("/agents", {
|
|
1986
|
+
method: "POST",
|
|
1987
|
+
body: JSON.stringify({
|
|
1988
|
+
name: slugName,
|
|
1989
|
+
display_name: trimmedName,
|
|
1990
|
+
owner_label: owner.label,
|
|
1991
|
+
}),
|
|
1992
|
+
});
|
|
1993
|
+
if (typeof registered.canonical_key === "string") {
|
|
1994
|
+
canonicalKey = registered.canonical_key;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1942
1997
|
const updatedIdentity = {
|
|
1943
1998
|
name: slugName,
|
|
1944
1999
|
display_name: trimmedName,
|
|
@@ -1946,14 +2001,19 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
|
|
|
1946
2001
|
owner_attribution: ownerAttribution,
|
|
1947
2002
|
ide_label: ideLabel,
|
|
1948
2003
|
actor_label: actorLabel,
|
|
1949
|
-
canonical_key:
|
|
1950
|
-
? registered.canonical_key
|
|
1951
|
-
: owner.login ? `${owner.login}/${slugName}` : null,
|
|
2004
|
+
canonical_key: canonicalKey,
|
|
1952
2005
|
runtime_key: currentAgentIdentityKey,
|
|
1953
|
-
source: "api",
|
|
2006
|
+
source: conversation_id ? "local" : "api",
|
|
1954
2007
|
resolved_at: new Date().toISOString(),
|
|
1955
2008
|
};
|
|
1956
|
-
|
|
2009
|
+
if (conversation_id) {
|
|
2010
|
+
// Scope to this conversation only
|
|
2011
|
+
setConversationIdentity(conversation_id, updatedIdentity);
|
|
2012
|
+
}
|
|
2013
|
+
else {
|
|
2014
|
+
// Global rename (backward compatible)
|
|
2015
|
+
currentAgentIdentity = setStoredAgentIdentity(updatedIdentity, currentAgentIdentityKey);
|
|
2016
|
+
}
|
|
1957
2017
|
return {
|
|
1958
2018
|
content: [
|
|
1959
2019
|
{
|
|
@@ -1961,7 +2021,9 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
|
|
|
1961
2021
|
text: JSON.stringify({
|
|
1962
2022
|
success: true,
|
|
1963
2023
|
message: `Agent name changed to "${trimmedName}".`,
|
|
1964
|
-
agent_identity: toPublicAgentIdentity(
|
|
2024
|
+
agent_identity: toPublicAgentIdentity(conversation_id
|
|
2025
|
+
? getConversationIdentity(conversation_id)
|
|
2026
|
+
: currentAgentIdentity),
|
|
1965
2027
|
}, null, 2),
|
|
1966
2028
|
},
|
|
1967
2029
|
],
|