letagents 0.12.8 → 0.12.9
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/__tests__/config-reader.test.js +93 -0
- package/dist/mcp/__tests__/git-remote.test.js +37 -0
- package/dist/mcp/__tests__/server-helpers.test.js +121 -0
- package/dist/mcp/codex-session.js +216 -44
- package/dist/mcp/local-state.js +65 -0
- package/dist/mcp/server.js +667 -50
- package/dist/mcp/sse-client.js +16 -2
- package/dist/shared/agent-presence.js +23 -0
- package/dist/shared/agent-reasoning.js +49 -0
- package/dist/shared/handoff.js +145 -0
- package/dist/shared/request-headers.js +3 -0
- package/dist/shared/room-agent-activity.js +41 -0
- package/dist/shared/room-agent-prompts.js +1 -1
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -11,13 +11,15 @@ 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, getCurrentCodexLiveSession, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, listStoredCodexLiveSessions, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
|
|
14
|
+
import { clearPendingDeviceAuth, clearStoredAuth, endStoredAgentSession, getCurrentCodexLiveSession, getLocalStatePath, getPendingDeviceAuth, getCurrentAgentSession, getStoredAgentSession, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, listStoredCodexLiveSessions, saveAgentSession, 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
17
|
import { buildRoomAgentPrompt, normalizeAgentPromptKind, } from "../shared/room-agent-prompts.js";
|
|
18
|
+
import { LETAGENTS_AGENT_SESSION_ID_HEADER, LETAGENTS_AGENT_SESSION_TOKEN_HEADER, LETAGENTS_ORIGIN_ROOM_ID_HEADER, } from "../shared/request-headers.js";
|
|
18
19
|
import { inspectLocalCodexSession, startLocalCodexSession, stopLocalCodexSession, toPublicCodexLiveSession, } from "./codex-session.js";
|
|
19
20
|
import { classifyPresenceStatusText, deriveTaskPresenceStatus, getRoomIdentityPresenceCacheKey, } from "./agent-presence.js";
|
|
20
21
|
import { buildRoomEventsQueryString } from "./room-events-query.js";
|
|
22
|
+
import { AGENT_PRESENCE_STATUSES, } from "../shared/agent-presence.js";
|
|
21
23
|
import { getPollTimeoutCapMs } from "../shared/poll-timeout-cap.js";
|
|
22
24
|
let currentRoom = null;
|
|
23
25
|
let currentAgentIdentityKey = "";
|
|
@@ -712,6 +714,17 @@ async function withAgentIdentity(payload) {
|
|
|
712
714
|
agent_identity: toPublicAgentIdentity(await ensureAgentIdentity()),
|
|
713
715
|
};
|
|
714
716
|
}
|
|
717
|
+
function getCurrentStreamAgentIdentity() {
|
|
718
|
+
const identity = currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey);
|
|
719
|
+
if (!identity?.actor_label || !identity.canonical_key) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
actorLabel: identity.actor_label,
|
|
724
|
+
actorKey: identity.canonical_key,
|
|
725
|
+
actorInstanceId: AGENT_INSTANCE_UUID,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
715
728
|
function rememberRoom(state, lastMessageId) {
|
|
716
729
|
currentRoom = state;
|
|
717
730
|
saveRoomSession({
|
|
@@ -726,6 +739,7 @@ function rememberRoom(state, lastMessageId) {
|
|
|
726
739
|
sseClient.subscribe({
|
|
727
740
|
roomId: state.room_id,
|
|
728
741
|
projectId: state.project_id ?? null,
|
|
742
|
+
agentIdentity: getCurrentStreamAgentIdentity(),
|
|
729
743
|
}, (_message) => {
|
|
730
744
|
touchRoomSession(state.room_id);
|
|
731
745
|
server.server.sendResourceListChanged();
|
|
@@ -744,22 +758,24 @@ function getRememberedRoomPresence(roomId, identity) {
|
|
|
744
758
|
}
|
|
745
759
|
return (roomPresenceByIdentity.get(getRoomIdentityPresenceCacheKey(roomId, identity.actor_label)) ?? { status: "idle", status_text: null });
|
|
746
760
|
}
|
|
747
|
-
async function syncRoomPresence(roomId, identity, presence) {
|
|
748
|
-
|
|
761
|
+
async function syncRoomPresence(roomId, identity, presence, agentSession) {
|
|
762
|
+
const resolvedIdentity = agentSession ? identityFromAgentSession(agentSession) : identity;
|
|
763
|
+
if (!roomId || !resolvedIdentity || !agentSession) {
|
|
749
764
|
return;
|
|
750
765
|
}
|
|
751
|
-
roomPresenceByIdentity.set(getRoomIdentityPresenceCacheKey(roomId,
|
|
766
|
+
roomPresenceByIdentity.set(getRoomIdentityPresenceCacheKey(roomId, resolvedIdentity.actor_label), presence);
|
|
752
767
|
try {
|
|
753
768
|
await apiCall(`/rooms/${encodeRoomIdPath(roomId)}/presence`, {
|
|
754
769
|
method: "POST",
|
|
755
770
|
body: JSON.stringify({
|
|
756
|
-
actor_label:
|
|
757
|
-
agent_key:
|
|
758
|
-
display_name:
|
|
759
|
-
owner_label:
|
|
760
|
-
ide_label:
|
|
771
|
+
actor_label: resolvedIdentity.actor_label,
|
|
772
|
+
agent_key: resolvedIdentity.canonical_key,
|
|
773
|
+
display_name: resolvedIdentity.display_name,
|
|
774
|
+
owner_label: resolvedIdentity.owner_label,
|
|
775
|
+
ide_label: resolvedIdentity.ide_label,
|
|
761
776
|
status: presence.status,
|
|
762
777
|
status_text: presence.status_text,
|
|
778
|
+
...agentSessionCredentials(agentSession),
|
|
763
779
|
}),
|
|
764
780
|
});
|
|
765
781
|
touchRoomSession(roomId);
|
|
@@ -774,6 +790,15 @@ async function syncRoomPresence(roomId, identity, presence) {
|
|
|
774
790
|
async function heartbeatRoomPresence(roomId, identity) {
|
|
775
791
|
await syncRoomPresence(roomId, identity, getRememberedRoomPresence(roomId, identity));
|
|
776
792
|
}
|
|
793
|
+
function buildAgentDeliveryHeaders(agentSession) {
|
|
794
|
+
if (!agentSession) {
|
|
795
|
+
return {};
|
|
796
|
+
}
|
|
797
|
+
return {
|
|
798
|
+
[LETAGENTS_AGENT_SESSION_ID_HEADER]: agentSession.session_id,
|
|
799
|
+
[LETAGENTS_AGENT_SESSION_TOKEN_HEADER]: agentSession.session_token,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
777
802
|
function getTargetRoomId(roomId) {
|
|
778
803
|
return roomId || currentRoom?.room_id || null;
|
|
779
804
|
}
|
|
@@ -802,6 +827,81 @@ function getCurrentLiveSessionPayload(roomId) {
|
|
|
802
827
|
const session = getCurrentCodexLiveSession(roomId);
|
|
803
828
|
return session ? toPublicCodexLiveSession(session) : null;
|
|
804
829
|
}
|
|
830
|
+
function toPublicAgentSession(session) {
|
|
831
|
+
if (!session) {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
session_id: session.session_id,
|
|
836
|
+
room_id: session.room_id,
|
|
837
|
+
session_kind: session.session_kind,
|
|
838
|
+
runtime: session.runtime,
|
|
839
|
+
actor_label: session.actor_label,
|
|
840
|
+
agent_key: session.agent_key,
|
|
841
|
+
agent_instance_id: session.agent_instance_id ?? null,
|
|
842
|
+
display_name: session.display_name,
|
|
843
|
+
owner_label: session.owner_label,
|
|
844
|
+
ide_label: session.ide_label,
|
|
845
|
+
created_at: session.created_at,
|
|
846
|
+
updated_at: session.updated_at,
|
|
847
|
+
last_seen_at: session.last_seen_at,
|
|
848
|
+
ended_at: session.ended_at ?? null,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function resolveAgentSession(roomId, sessionId) {
|
|
852
|
+
if (!sessionId) {
|
|
853
|
+
return getCurrentAgentSession(roomId);
|
|
854
|
+
}
|
|
855
|
+
const session = getStoredAgentSession(sessionId);
|
|
856
|
+
if (!session) {
|
|
857
|
+
throw new Error(`Unknown agent_session_id: ${sessionId}`);
|
|
858
|
+
}
|
|
859
|
+
if (session.ended_at) {
|
|
860
|
+
throw new Error(`agent_session_id ${sessionId} ended at ${session.ended_at}`);
|
|
861
|
+
}
|
|
862
|
+
if (roomId && session.room_id !== roomId) {
|
|
863
|
+
throw new Error(`agent_session_id ${sessionId} is registered for ${session.room_id}, not ${roomId}`);
|
|
864
|
+
}
|
|
865
|
+
return session;
|
|
866
|
+
}
|
|
867
|
+
function identityFromAgentSession(session) {
|
|
868
|
+
return {
|
|
869
|
+
name: normalizeAgentBaseName(session.display_name),
|
|
870
|
+
display_name: session.display_name,
|
|
871
|
+
owner_label: session.owner_label,
|
|
872
|
+
owner_attribution: formatOwnerAttribution(session.owner_label),
|
|
873
|
+
ide_label: session.ide_label,
|
|
874
|
+
actor_label: session.actor_label,
|
|
875
|
+
canonical_key: session.agent_key,
|
|
876
|
+
runtime_key: `agent_session:${session.session_id}`,
|
|
877
|
+
source: "api",
|
|
878
|
+
resolved_at: session.updated_at,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
function requireWorkerAgentSession(roomId, sessionId) {
|
|
882
|
+
const session = resolveAgentSession(roomId, sessionId);
|
|
883
|
+
if (!session) {
|
|
884
|
+
throw new Error("Registered worker agent_session_id is required for this write action. " +
|
|
885
|
+
"Call register_agent_session for this room first, then pass the returned agent_session_id.");
|
|
886
|
+
}
|
|
887
|
+
if (session.session_kind !== "worker") {
|
|
888
|
+
throw new Error("Worker agent_session_id is required for this write action.");
|
|
889
|
+
}
|
|
890
|
+
return session;
|
|
891
|
+
}
|
|
892
|
+
async function resolveWorkerToolIdentity(input) {
|
|
893
|
+
const agentSession = requireWorkerAgentSession(input.roomId, input.agentSessionId);
|
|
894
|
+
return {
|
|
895
|
+
identity: identityFromAgentSession(agentSession),
|
|
896
|
+
agentSession,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
function agentSessionCredentials(agentSession) {
|
|
900
|
+
return {
|
|
901
|
+
agent_session_id: agentSession.session_id,
|
|
902
|
+
agent_session_token: agentSession.session_token,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
805
905
|
function toAgentReadableMessage(message) {
|
|
806
906
|
if (!message || typeof message !== "object") {
|
|
807
907
|
return message;
|
|
@@ -826,9 +926,20 @@ function appendIncludePromptOnly(path) {
|
|
|
826
926
|
return `${path}${path.includes("?") ? "&" : "?"}include_prompt_only=1`;
|
|
827
927
|
}
|
|
828
928
|
async function roomScopedApiCall(input) {
|
|
929
|
+
const headers = {
|
|
930
|
+
...input.options?.headers,
|
|
931
|
+
};
|
|
932
|
+
if (currentRoom?.room_id &&
|
|
933
|
+
!Object.keys(headers).some((key) => key.toLowerCase() === LETAGENTS_ORIGIN_ROOM_ID_HEADER.toLowerCase())) {
|
|
934
|
+
headers[LETAGENTS_ORIGIN_ROOM_ID_HEADER] = currentRoom.room_id;
|
|
935
|
+
}
|
|
936
|
+
const options = {
|
|
937
|
+
...input.options,
|
|
938
|
+
headers,
|
|
939
|
+
};
|
|
829
940
|
if (input.room_id) {
|
|
830
941
|
try {
|
|
831
|
-
const result = await apiCall(input.room_path(input.room_id),
|
|
942
|
+
const result = await apiCall(input.room_path(input.room_id), options);
|
|
832
943
|
touchRoomSession(input.room_id, getLastMessageId(result));
|
|
833
944
|
return result;
|
|
834
945
|
}
|
|
@@ -842,12 +953,16 @@ async function roomScopedApiCall(input) {
|
|
|
842
953
|
if (!input.project_id) {
|
|
843
954
|
throw new Error("No room is available for this request.");
|
|
844
955
|
}
|
|
845
|
-
const result = await apiCall(input.project_path(input.project_id),
|
|
956
|
+
const result = await apiCall(input.project_path(input.project_id), options);
|
|
846
957
|
if (input.room_id) {
|
|
847
958
|
touchRoomSession(input.room_id, getLastMessageId(result));
|
|
848
959
|
}
|
|
849
960
|
return result;
|
|
850
961
|
}
|
|
962
|
+
function normalizeOptionalToolString(value) {
|
|
963
|
+
const trimmed = value?.trim();
|
|
964
|
+
return trimmed ? trimmed : undefined;
|
|
965
|
+
}
|
|
851
966
|
async function joinRoomIdentifier(identifier, joinedVia) {
|
|
852
967
|
const roomId = joinedVia === "join_code" ? normalizeInviteCode(identifier) : identifier.trim();
|
|
853
968
|
try {
|
|
@@ -855,6 +970,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
855
970
|
const joinedRoomId = typeof response.room_id === "string"
|
|
856
971
|
? response.room_id
|
|
857
972
|
: roomId;
|
|
973
|
+
const agentIdentity = await ensureAgentIdentity();
|
|
858
974
|
const room = rememberRoom(toRoomState({
|
|
859
975
|
room_id: joinedRoomId,
|
|
860
976
|
project_id: typeof response.project_id === "string" ? response.project_id : null,
|
|
@@ -866,10 +982,9 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
866
982
|
display_name: typeof response.display_name === "string" ? response.display_name : null,
|
|
867
983
|
joined_via: joinedVia,
|
|
868
984
|
}));
|
|
869
|
-
const agentIdentity = await ensureAgentIdentity();
|
|
870
985
|
await syncRoomPresence(room.room_id, agentIdentity, {
|
|
871
986
|
status: "idle",
|
|
872
|
-
status_text: "
|
|
987
|
+
status_text: "available in room",
|
|
873
988
|
});
|
|
874
989
|
return {
|
|
875
990
|
room,
|
|
@@ -891,6 +1006,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
891
1006
|
const legacyRoomId = typeof project.code === "string"
|
|
892
1007
|
? project.code
|
|
893
1008
|
: roomId;
|
|
1009
|
+
const agentIdentity = await ensureAgentIdentity();
|
|
894
1010
|
const room = rememberRoom(toRoomState({
|
|
895
1011
|
room_id: legacyRoomId,
|
|
896
1012
|
project_id: typeof project.id === "string" ? project.id : null,
|
|
@@ -898,10 +1014,9 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
898
1014
|
display_name: typeof project.display_name === "string" ? project.display_name : null,
|
|
899
1015
|
joined_via: joinedVia,
|
|
900
1016
|
}));
|
|
901
|
-
const agentIdentity = await ensureAgentIdentity();
|
|
902
1017
|
await syncRoomPresence(room.room_id, agentIdentity, {
|
|
903
1018
|
status: "idle",
|
|
904
|
-
status_text: "
|
|
1019
|
+
status_text: "available in room",
|
|
905
1020
|
});
|
|
906
1021
|
return {
|
|
907
1022
|
room,
|
|
@@ -919,6 +1034,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
919
1034
|
: typeof project.code === "string" && project.code.trim()
|
|
920
1035
|
? project.code
|
|
921
1036
|
: roomId;
|
|
1037
|
+
const agentIdentity = await ensureAgentIdentity();
|
|
922
1038
|
const room = rememberRoom(toRoomState({
|
|
923
1039
|
room_id: legacyRoomId,
|
|
924
1040
|
project_id: typeof project.id === "string" ? project.id : null,
|
|
@@ -930,10 +1046,9 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
930
1046
|
display_name: typeof project.display_name === "string" ? project.display_name : null,
|
|
931
1047
|
joined_via: joinedVia,
|
|
932
1048
|
}));
|
|
933
|
-
const agentIdentity = await ensureAgentIdentity();
|
|
934
1049
|
await syncRoomPresence(room.room_id, agentIdentity, {
|
|
935
1050
|
status: "idle",
|
|
936
|
-
status_text: "
|
|
1051
|
+
status_text: "available in room",
|
|
937
1052
|
});
|
|
938
1053
|
return {
|
|
939
1054
|
room,
|
|
@@ -962,7 +1077,7 @@ async function createInviteRoom() {
|
|
|
962
1077
|
const agentIdentity = await ensureAgentIdentity();
|
|
963
1078
|
await syncRoomPresence(room.room_id, agentIdentity, {
|
|
964
1079
|
status: "idle",
|
|
965
|
-
status_text: "
|
|
1080
|
+
status_text: "available in room",
|
|
966
1081
|
});
|
|
967
1082
|
return {
|
|
968
1083
|
room,
|
|
@@ -1154,6 +1269,186 @@ server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room
|
|
|
1154
1269
|
throw error;
|
|
1155
1270
|
}
|
|
1156
1271
|
});
|
|
1272
|
+
// -- register_agent_session -------------------------------------------------
|
|
1273
|
+
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.", {
|
|
1274
|
+
room_id: z
|
|
1275
|
+
.string()
|
|
1276
|
+
.optional()
|
|
1277
|
+
.describe("Canonical room ID. Defaults to the current room."),
|
|
1278
|
+
session_kind: z
|
|
1279
|
+
.enum(["worker", "controller"])
|
|
1280
|
+
.optional()
|
|
1281
|
+
.describe("Agent session kind. Workers appear in connected agent activity; controllers do not. Defaults to worker."),
|
|
1282
|
+
runtime: z
|
|
1283
|
+
.string()
|
|
1284
|
+
.optional()
|
|
1285
|
+
.describe("Runtime label such as codex, antigravity, or custom harness name."),
|
|
1286
|
+
display_name: z
|
|
1287
|
+
.string()
|
|
1288
|
+
.optional()
|
|
1289
|
+
.describe("Human-readable label for this worker session. Use distinct labels when one MCP process controls multiple workers."),
|
|
1290
|
+
}, async ({ room_id, session_kind, runtime, display_name }) => {
|
|
1291
|
+
const targetRoomId = getTargetRoomId(room_id);
|
|
1292
|
+
if (!targetRoomId) {
|
|
1293
|
+
return {
|
|
1294
|
+
content: [
|
|
1295
|
+
{
|
|
1296
|
+
type: "text",
|
|
1297
|
+
text: JSON.stringify({
|
|
1298
|
+
success: false,
|
|
1299
|
+
error: "No room_id provided and not currently in a room.",
|
|
1300
|
+
hint: "Join a room first, or pass room_id explicitly.",
|
|
1301
|
+
}, null, 2),
|
|
1302
|
+
},
|
|
1303
|
+
],
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
const identity = await ensureAgentIdentity();
|
|
1307
|
+
if (!identity.canonical_key) {
|
|
1308
|
+
return {
|
|
1309
|
+
content: [
|
|
1310
|
+
{
|
|
1311
|
+
type: "text",
|
|
1312
|
+
text: JSON.stringify({
|
|
1313
|
+
success: false,
|
|
1314
|
+
error: "Authenticated agent identity is required before registering an agent session.",
|
|
1315
|
+
}, null, 2),
|
|
1316
|
+
},
|
|
1317
|
+
],
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
const created = await apiCall(`/rooms/${encodeRoomIdPath(targetRoomId)}/agent-sessions`, {
|
|
1321
|
+
method: "POST",
|
|
1322
|
+
body: JSON.stringify({
|
|
1323
|
+
actor_key: identity.canonical_key,
|
|
1324
|
+
actor_label: identity.actor_label,
|
|
1325
|
+
ide_label: identity.ide_label ?? detectAgentIdeLabel(),
|
|
1326
|
+
agent_instance_id: AGENT_INSTANCE_UUID,
|
|
1327
|
+
display_name: display_name?.trim() || identity.display_name,
|
|
1328
|
+
session_kind: session_kind ?? "worker",
|
|
1329
|
+
runtime: runtime?.trim() || detectAgentIdeLabel().toLowerCase(),
|
|
1330
|
+
}),
|
|
1331
|
+
});
|
|
1332
|
+
const sessionId = typeof created.session_id === "string" ? created.session_id : "";
|
|
1333
|
+
const sessionToken = typeof created.session_token === "string" ? created.session_token : "";
|
|
1334
|
+
if (!sessionId || !sessionToken) {
|
|
1335
|
+
throw new Error("Agent session registration response was missing session credentials.");
|
|
1336
|
+
}
|
|
1337
|
+
const session = saveAgentSession({
|
|
1338
|
+
session_id: sessionId,
|
|
1339
|
+
session_token: sessionToken,
|
|
1340
|
+
room_id: typeof created.room_id === "string" ? created.room_id : targetRoomId,
|
|
1341
|
+
session_kind: created.session_kind === "controller" ? "controller" : "worker",
|
|
1342
|
+
runtime: typeof created.runtime === "string" ? created.runtime : "unknown",
|
|
1343
|
+
actor_label: typeof created.actor_label === "string" ? created.actor_label : identity.actor_label,
|
|
1344
|
+
agent_key: typeof created.agent_key === "string" ? created.agent_key : identity.canonical_key,
|
|
1345
|
+
agent_instance_id: typeof created.agent_instance_id === "string" ? created.agent_instance_id : AGENT_INSTANCE_UUID,
|
|
1346
|
+
display_name: typeof created.display_name === "string" ? created.display_name : identity.display_name,
|
|
1347
|
+
owner_label: typeof created.owner_label === "string" ? created.owner_label : identity.owner_label,
|
|
1348
|
+
ide_label: typeof created.ide_label === "string" ? created.ide_label : identity.ide_label ?? detectAgentIdeLabel(),
|
|
1349
|
+
created_at: typeof created.created_at === "string" ? created.created_at : new Date().toISOString(),
|
|
1350
|
+
updated_at: typeof created.updated_at === "string" ? created.updated_at : new Date().toISOString(),
|
|
1351
|
+
last_seen_at: typeof created.last_seen_at === "string" ? created.last_seen_at : new Date().toISOString(),
|
|
1352
|
+
ended_at: typeof created.ended_at === "string" ? created.ended_at : null,
|
|
1353
|
+
});
|
|
1354
|
+
return {
|
|
1355
|
+
content: [
|
|
1356
|
+
{
|
|
1357
|
+
type: "text",
|
|
1358
|
+
text: JSON.stringify({
|
|
1359
|
+
success: true,
|
|
1360
|
+
agent_session: toPublicAgentSession(session),
|
|
1361
|
+
agent_session_id: session.session_id,
|
|
1362
|
+
use_agent_session_id: "Pass this agent_session_id to wait_for_messages, send_message, post_status, and task tools for this specific worker.",
|
|
1363
|
+
}, null, 2),
|
|
1364
|
+
},
|
|
1365
|
+
],
|
|
1366
|
+
};
|
|
1367
|
+
});
|
|
1368
|
+
// -- disconnect_agent_session ----------------------------------------------
|
|
1369
|
+
server.tool("disconnect_agent_session", "Disconnect/end a registered worker session from a room. Defaults to the current worker session; admins may pass another agent_session_id.", {
|
|
1370
|
+
room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room or the stored session room."),
|
|
1371
|
+
agent_session_id: z.string().optional().describe("Registered agent session to disconnect. Defaults to the current session for the room."),
|
|
1372
|
+
}, async ({ room_id, agent_session_id }) => {
|
|
1373
|
+
let targetRoomId = getTargetRoomId(room_id);
|
|
1374
|
+
const localSession = agent_session_id
|
|
1375
|
+
? getStoredAgentSession(agent_session_id)
|
|
1376
|
+
: getCurrentAgentSession(targetRoomId ?? currentRoom?.room_id ?? null);
|
|
1377
|
+
if (!targetRoomId && localSession) {
|
|
1378
|
+
targetRoomId = localSession.room_id;
|
|
1379
|
+
}
|
|
1380
|
+
if (!targetRoomId) {
|
|
1381
|
+
return {
|
|
1382
|
+
content: [
|
|
1383
|
+
{
|
|
1384
|
+
type: "text",
|
|
1385
|
+
text: JSON.stringify({
|
|
1386
|
+
success: false,
|
|
1387
|
+
error: "No room_id provided and not currently in a room.",
|
|
1388
|
+
hint: "Join a room first, pass room_id, or pass a locally stored agent_session_id.",
|
|
1389
|
+
}, null, 2),
|
|
1390
|
+
},
|
|
1391
|
+
],
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
const targetSessionId = agent_session_id || localSession?.session_id;
|
|
1395
|
+
if (!targetSessionId) {
|
|
1396
|
+
return {
|
|
1397
|
+
content: [
|
|
1398
|
+
{
|
|
1399
|
+
type: "text",
|
|
1400
|
+
text: JSON.stringify({
|
|
1401
|
+
success: false,
|
|
1402
|
+
error: "No registered agent session was found for this room.",
|
|
1403
|
+
hint: "Pass agent_session_id or call register_agent_session first.",
|
|
1404
|
+
}, null, 2),
|
|
1405
|
+
},
|
|
1406
|
+
],
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
if (localSession?.ended_at) {
|
|
1410
|
+
return {
|
|
1411
|
+
content: [
|
|
1412
|
+
{
|
|
1413
|
+
type: "text",
|
|
1414
|
+
text: JSON.stringify({
|
|
1415
|
+
success: true,
|
|
1416
|
+
already_ended: true,
|
|
1417
|
+
agent_session: toPublicAgentSession(localSession),
|
|
1418
|
+
}, null, 2),
|
|
1419
|
+
},
|
|
1420
|
+
],
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
const requestBody = localSession?.session_id === targetSessionId && localSession.session_kind === "worker"
|
|
1424
|
+
? agentSessionCredentials(localSession)
|
|
1425
|
+
: {};
|
|
1426
|
+
const result = await apiCall(`/rooms/${encodeRoomIdPath(targetRoomId)}/agent-sessions/${encodeURIComponent(targetSessionId)}/disconnect`, {
|
|
1427
|
+
method: "POST",
|
|
1428
|
+
body: JSON.stringify(requestBody),
|
|
1429
|
+
});
|
|
1430
|
+
const endedAt = result.agent_session && typeof result.agent_session === "object" && "ended_at" in result.agent_session
|
|
1431
|
+
? String(result.agent_session.ended_at ?? new Date().toISOString())
|
|
1432
|
+
: new Date().toISOString();
|
|
1433
|
+
const endedLocalSession = localSession?.session_id === targetSessionId
|
|
1434
|
+
? endStoredAgentSession(targetSessionId, endedAt)
|
|
1435
|
+
: null;
|
|
1436
|
+
return {
|
|
1437
|
+
content: [
|
|
1438
|
+
{
|
|
1439
|
+
type: "text",
|
|
1440
|
+
text: JSON.stringify({
|
|
1441
|
+
success: true,
|
|
1442
|
+
room_id: targetRoomId,
|
|
1443
|
+
agent_session_id: targetSessionId,
|
|
1444
|
+
agent_session: result.agent_session ?? toPublicAgentSession(endedLocalSession),
|
|
1445
|
+
delivery_session: result.delivery_session ?? null,
|
|
1446
|
+
local_state: endedLocalSession ? "ended" : "unchanged",
|
|
1447
|
+
}, null, 2),
|
|
1448
|
+
},
|
|
1449
|
+
],
|
|
1450
|
+
};
|
|
1451
|
+
});
|
|
1157
1452
|
// -- local Codex live sessions ---------------------------------------------
|
|
1158
1453
|
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.", {
|
|
1159
1454
|
room: z
|
|
@@ -1384,8 +1679,12 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
1384
1679
|
conversation_id: z
|
|
1385
1680
|
.string()
|
|
1386
1681
|
.optional()
|
|
1387
|
-
.describe("
|
|
1388
|
-
|
|
1682
|
+
.describe("Deprecated for worker writes; registered worker session identity is used."),
|
|
1683
|
+
agent_session_id: z
|
|
1684
|
+
.string()
|
|
1685
|
+
.optional()
|
|
1686
|
+
.describe("Registered agent session to use for this status update."),
|
|
1687
|
+
}, async ({ sender: _sender, status, room_id, conversation_id: _conversation_id, agent_session_id }) => {
|
|
1389
1688
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1390
1689
|
const targetProjectId = getFallbackProjectId();
|
|
1391
1690
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1404,7 +1703,10 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
1404
1703
|
}
|
|
1405
1704
|
// Status messages use a reserved prefix so the UI (and agents) can distinguish
|
|
1406
1705
|
// them from normal chat messages without changing the data model.
|
|
1407
|
-
const identity
|
|
1706
|
+
const { identity, agentSession } = await resolveWorkerToolIdentity({
|
|
1707
|
+
roomId: targetRoomId ?? currentRoom?.room_id ?? null,
|
|
1708
|
+
agentSessionId: agent_session_id,
|
|
1709
|
+
});
|
|
1408
1710
|
const sender = identity.actor_label;
|
|
1409
1711
|
const statusText = `[status] ${status}`;
|
|
1410
1712
|
const message = await roomScopedApiCall({
|
|
@@ -1414,14 +1716,18 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
1414
1716
|
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
|
|
1415
1717
|
options: {
|
|
1416
1718
|
method: "POST",
|
|
1417
|
-
body: JSON.stringify({
|
|
1719
|
+
body: JSON.stringify({
|
|
1720
|
+
sender,
|
|
1721
|
+
text: statusText,
|
|
1722
|
+
...agentSessionCredentials(agentSession),
|
|
1723
|
+
}),
|
|
1418
1724
|
},
|
|
1419
1725
|
});
|
|
1420
1726
|
touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
|
|
1421
1727
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
|
|
1422
1728
|
status: classifyPresenceStatusText(status, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity).status),
|
|
1423
1729
|
status_text: status,
|
|
1424
|
-
});
|
|
1730
|
+
}, agentSession);
|
|
1425
1731
|
return {
|
|
1426
1732
|
content: [
|
|
1427
1733
|
{
|
|
@@ -1438,14 +1744,170 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
1438
1744
|
],
|
|
1439
1745
|
};
|
|
1440
1746
|
});
|
|
1747
|
+
// -- post_reasoning ---------------------------------------------------------
|
|
1748
|
+
server.tool("post_reasoning", "Update a structured, replace-in-place reasoning trace for the current room without spamming chat. " +
|
|
1749
|
+
"Use this for visible thought-process updates such as goal, hypothesis, checks in progress, blockers, " +
|
|
1750
|
+
"and next action. Optionally persist a milestone summary into room history when something durable changed.", {
|
|
1751
|
+
summary: z.string().describe("Short current reasoning summary shown in the room UI."),
|
|
1752
|
+
goal: z.string().optional().describe("What the agent is trying to achieve right now."),
|
|
1753
|
+
checking: z.string().optional().describe("What the agent is verifying or inspecting."),
|
|
1754
|
+
hypothesis: z.string().optional().describe("Current working theory or approach."),
|
|
1755
|
+
blocker: z.string().optional().describe("Current blocker, if any."),
|
|
1756
|
+
next_action: z.string().optional().describe("Immediate next step the agent plans to take."),
|
|
1757
|
+
milestone: z.string().optional().describe("Optional durable milestone summary to append to room history."),
|
|
1758
|
+
confidence: z.number().min(0).max(1).optional().describe("Optional confidence score between 0 and 1."),
|
|
1759
|
+
status: z.enum(AGENT_PRESENCE_STATUSES).optional().describe("Optional explicit presence state override."),
|
|
1760
|
+
room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
|
|
1761
|
+
conversation_id: z
|
|
1762
|
+
.string()
|
|
1763
|
+
.optional()
|
|
1764
|
+
.describe("Deprecated for worker writes; registered worker session identity is used."),
|
|
1765
|
+
agent_session_id: z
|
|
1766
|
+
.string()
|
|
1767
|
+
.optional()
|
|
1768
|
+
.describe("Registered agent session to use for this reasoning update."),
|
|
1769
|
+
}, async ({ summary, goal, checking, hypothesis, blocker, next_action, milestone, confidence, status, room_id, conversation_id: _conversation_id, agent_session_id, }) => {
|
|
1770
|
+
const targetRoomId = getTargetRoomId(room_id);
|
|
1771
|
+
const targetProjectId = getFallbackProjectId();
|
|
1772
|
+
if (!targetRoomId && !targetProjectId) {
|
|
1773
|
+
return {
|
|
1774
|
+
content: [
|
|
1775
|
+
{
|
|
1776
|
+
type: "text",
|
|
1777
|
+
text: JSON.stringify({
|
|
1778
|
+
success: false,
|
|
1779
|
+
error: "No room_id provided and not currently in a room.",
|
|
1780
|
+
hint: "Join or create a room first, or pass room_id explicitly.",
|
|
1781
|
+
}, null, 2),
|
|
1782
|
+
},
|
|
1783
|
+
],
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
const normalizedSummary = summary.trim();
|
|
1787
|
+
if (!normalizedSummary) {
|
|
1788
|
+
return {
|
|
1789
|
+
content: [
|
|
1790
|
+
{
|
|
1791
|
+
type: "text",
|
|
1792
|
+
text: JSON.stringify({ success: false, error: "summary is required" }, null, 2),
|
|
1793
|
+
},
|
|
1794
|
+
],
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
const { identity, agentSession } = await resolveWorkerToolIdentity({
|
|
1798
|
+
roomId: targetRoomId ?? currentRoom?.room_id ?? null,
|
|
1799
|
+
agentSessionId: agent_session_id,
|
|
1800
|
+
});
|
|
1801
|
+
const snapshot = {
|
|
1802
|
+
summary: normalizedSummary,
|
|
1803
|
+
goal: normalizeOptionalToolString(goal),
|
|
1804
|
+
checking: normalizeOptionalToolString(checking),
|
|
1805
|
+
hypothesis: normalizeOptionalToolString(hypothesis),
|
|
1806
|
+
blocker: normalizeOptionalToolString(blocker),
|
|
1807
|
+
next_action: normalizeOptionalToolString(next_action),
|
|
1808
|
+
milestone: normalizeOptionalToolString(milestone),
|
|
1809
|
+
confidence,
|
|
1810
|
+
status,
|
|
1811
|
+
};
|
|
1812
|
+
const query = new URLSearchParams({
|
|
1813
|
+
open: "true",
|
|
1814
|
+
actor_label: identity.actor_label,
|
|
1815
|
+
});
|
|
1816
|
+
const sessionsResult = await roomScopedApiCall({
|
|
1817
|
+
room_id: targetRoomId,
|
|
1818
|
+
project_id: targetProjectId,
|
|
1819
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/reasoning-sessions?${query.toString()}`,
|
|
1820
|
+
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/reasoning-sessions?${query.toString()}`,
|
|
1821
|
+
});
|
|
1822
|
+
const existingSession = sessionsResult.sessions?.[0];
|
|
1823
|
+
let result = null;
|
|
1824
|
+
if (existingSession?.id) {
|
|
1825
|
+
result = await roomScopedApiCall({
|
|
1826
|
+
room_id: targetRoomId,
|
|
1827
|
+
project_id: targetProjectId,
|
|
1828
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/reasoning-sessions/${encodeURIComponent(existingSession.id)}/updates`,
|
|
1829
|
+
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/reasoning-sessions/${encodeURIComponent(existingSession.id)}/updates`,
|
|
1830
|
+
options: {
|
|
1831
|
+
method: "POST",
|
|
1832
|
+
body: JSON.stringify({
|
|
1833
|
+
actor_label: identity.actor_label,
|
|
1834
|
+
...agentSessionCredentials(agentSession),
|
|
1835
|
+
...snapshot,
|
|
1836
|
+
}),
|
|
1837
|
+
},
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
else {
|
|
1841
|
+
result = await roomScopedApiCall({
|
|
1842
|
+
room_id: targetRoomId,
|
|
1843
|
+
project_id: targetProjectId,
|
|
1844
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/reasoning-sessions`,
|
|
1845
|
+
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/reasoning-sessions`,
|
|
1846
|
+
options: {
|
|
1847
|
+
method: "POST",
|
|
1848
|
+
body: JSON.stringify({
|
|
1849
|
+
actor_label: identity.actor_label,
|
|
1850
|
+
agent_key: identity.canonical_key,
|
|
1851
|
+
...agentSessionCredentials(agentSession),
|
|
1852
|
+
...snapshot,
|
|
1853
|
+
}),
|
|
1854
|
+
},
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
let milestoneMessageId = null;
|
|
1858
|
+
const normalizedMilestone = normalizeOptionalToolString(milestone);
|
|
1859
|
+
if (normalizedMilestone) {
|
|
1860
|
+
const milestoneMessage = await roomScopedApiCall({
|
|
1861
|
+
room_id: targetRoomId,
|
|
1862
|
+
project_id: targetProjectId,
|
|
1863
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages`,
|
|
1864
|
+
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
|
|
1865
|
+
options: {
|
|
1866
|
+
method: "POST",
|
|
1867
|
+
body: JSON.stringify({
|
|
1868
|
+
sender: identity.actor_label,
|
|
1869
|
+
text: normalizedMilestone,
|
|
1870
|
+
...agentSessionCredentials(agentSession),
|
|
1871
|
+
}),
|
|
1872
|
+
},
|
|
1873
|
+
});
|
|
1874
|
+
milestoneMessageId =
|
|
1875
|
+
typeof milestoneMessage.id === "string" ? milestoneMessage.id : null;
|
|
1876
|
+
touchCurrentRoom(milestoneMessageId ?? undefined);
|
|
1877
|
+
}
|
|
1878
|
+
if (status) {
|
|
1879
|
+
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
|
|
1880
|
+
status,
|
|
1881
|
+
status_text: normalizedSummary,
|
|
1882
|
+
}, agentSession);
|
|
1883
|
+
}
|
|
1884
|
+
else {
|
|
1885
|
+
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity), agentSession);
|
|
1886
|
+
}
|
|
1887
|
+
return {
|
|
1888
|
+
content: [
|
|
1889
|
+
{
|
|
1890
|
+
type: "text",
|
|
1891
|
+
text: JSON.stringify({
|
|
1892
|
+
success: true,
|
|
1893
|
+
room_id: result?.room_id ?? targetRoomId ?? targetProjectId ?? null,
|
|
1894
|
+
session: result?.session ?? null,
|
|
1895
|
+
update: result?.update ?? null,
|
|
1896
|
+
milestone_message_id: milestoneMessageId,
|
|
1897
|
+
agent_identity: toPublicAgentIdentity(identity),
|
|
1898
|
+
}, null, 2),
|
|
1899
|
+
},
|
|
1900
|
+
],
|
|
1901
|
+
};
|
|
1902
|
+
});
|
|
1441
1903
|
// -- Task Board Tools -------------------------------------------------------
|
|
1442
1904
|
const TASK_STATUSES = [
|
|
1443
1905
|
"proposed", "accepted", "assigned", "in_progress",
|
|
1444
1906
|
"blocked", "in_review", "merged", "done", "cancelled",
|
|
1445
1907
|
];
|
|
1446
1908
|
server.tool("add_task", "Add a new task to the room board. Tasks normally start as 'proposed' and must be " +
|
|
1447
|
-
"accepted before an agent can claim them
|
|
1448
|
-
"
|
|
1909
|
+
"accepted before an agent can claim them. Agent-created tasks require coordinator " +
|
|
1910
|
+
"acceptance before they become claimable. Use this when a human or agent identifies " +
|
|
1449
1911
|
"work that needs to be done.", {
|
|
1450
1912
|
title: z.string().describe("Short task title, e.g. 'Wire up Jest test runner'"),
|
|
1451
1913
|
description: z.string().optional().describe("Longer description of what needs to be done"),
|
|
@@ -1455,8 +1917,9 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
|
|
|
1455
1917
|
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
1456
1918
|
source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
|
|
1457
1919
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1458
|
-
conversation_id: z.string().optional().describe("
|
|
1459
|
-
|
|
1920
|
+
conversation_id: z.string().optional().describe("Deprecated for worker writes; registered worker session identity is used."),
|
|
1921
|
+
agent_session_id: z.string().optional().describe("Registered agent session to use for this task action."),
|
|
1922
|
+
}, async ({ title, description, created_by: _createdBy, source_message_id, room_id, conversation_id: _conversation_id, agent_session_id }) => {
|
|
1460
1923
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1461
1924
|
const targetProjectId = getFallbackProjectId();
|
|
1462
1925
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1464,7 +1927,10 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
|
|
|
1464
1927
|
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
|
|
1465
1928
|
};
|
|
1466
1929
|
}
|
|
1467
|
-
const identity
|
|
1930
|
+
const { identity, agentSession } = await resolveWorkerToolIdentity({
|
|
1931
|
+
roomId: targetRoomId ?? currentRoom?.room_id ?? null,
|
|
1932
|
+
agentSessionId: agent_session_id,
|
|
1933
|
+
});
|
|
1468
1934
|
const task = await roomScopedApiCall({
|
|
1469
1935
|
room_id: targetRoomId,
|
|
1470
1936
|
project_id: targetProjectId,
|
|
@@ -1476,11 +1942,15 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
|
|
|
1476
1942
|
title,
|
|
1477
1943
|
description,
|
|
1478
1944
|
created_by: identity.actor_label,
|
|
1945
|
+
actor_label: identity.actor_label,
|
|
1946
|
+
actor_key: identity.canonical_key,
|
|
1947
|
+
actor_instance_id: agentSession?.agent_instance_id || AGENT_INSTANCE_UUID,
|
|
1479
1948
|
source_message_id,
|
|
1949
|
+
...agentSessionCredentials(agentSession),
|
|
1480
1950
|
}),
|
|
1481
1951
|
},
|
|
1482
1952
|
});
|
|
1483
|
-
await
|
|
1953
|
+
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity), agentSession);
|
|
1484
1954
|
return {
|
|
1485
1955
|
content: [
|
|
1486
1956
|
{
|
|
@@ -1584,8 +2054,9 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
1584
2054
|
.optional()
|
|
1585
2055
|
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
1586
2056
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1587
|
-
conversation_id: z.string().optional().describe("
|
|
1588
|
-
|
|
2057
|
+
conversation_id: z.string().optional().describe("Deprecated for worker writes; registered worker session identity is used."),
|
|
2058
|
+
agent_session_id: z.string().optional().describe("Registered agent session to use for this task action."),
|
|
2059
|
+
}, async ({ task_id, assignee: _assignee, room_id, conversation_id: _conversation_id, agent_session_id }) => {
|
|
1589
2060
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1590
2061
|
const targetProjectId = getFallbackProjectId();
|
|
1591
2062
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1594,7 +2065,10 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
1594
2065
|
};
|
|
1595
2066
|
}
|
|
1596
2067
|
try {
|
|
1597
|
-
const identity
|
|
2068
|
+
const { identity, agentSession } = await resolveWorkerToolIdentity({
|
|
2069
|
+
roomId: targetRoomId ?? currentRoom?.room_id ?? null,
|
|
2070
|
+
agentSessionId: agent_session_id,
|
|
2071
|
+
});
|
|
1598
2072
|
const updated = await roomScopedApiCall({
|
|
1599
2073
|
room_id: targetRoomId,
|
|
1600
2074
|
project_id: targetProjectId,
|
|
@@ -1607,14 +2081,16 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
1607
2081
|
assignee: identity.actor_label,
|
|
1608
2082
|
actor_label: identity.actor_label,
|
|
1609
2083
|
actor_key: identity.canonical_key,
|
|
2084
|
+
actor_instance_id: agentSession?.agent_instance_id || AGENT_INSTANCE_UUID,
|
|
1610
2085
|
assignee_agent_key: identity.canonical_key,
|
|
2086
|
+
...agentSessionCredentials(agentSession),
|
|
1611
2087
|
}),
|
|
1612
2088
|
},
|
|
1613
2089
|
});
|
|
1614
2090
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
|
|
1615
2091
|
status: "working",
|
|
1616
2092
|
status_text: `claimed ${task_id}`,
|
|
1617
|
-
});
|
|
2093
|
+
}, agentSession);
|
|
1618
2094
|
return {
|
|
1619
2095
|
content: [
|
|
1620
2096
|
{
|
|
@@ -1655,8 +2131,9 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1655
2131
|
.optional()
|
|
1656
2132
|
.describe("Persisted provider-neutral task workflow artifacts to attach to the task"),
|
|
1657
2133
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1658
|
-
conversation_id: z.string().optional().describe("
|
|
1659
|
-
|
|
2134
|
+
conversation_id: z.string().optional().describe("Deprecated for worker writes; registered worker session identity is used."),
|
|
2135
|
+
agent_session_id: z.string().optional().describe("Registered agent session to use for this task action."),
|
|
2136
|
+
}, async ({ task_id, status, assignee, pr_url, workflow_artifacts, room_id, conversation_id: _conversation_id, agent_session_id }) => {
|
|
1660
2137
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1661
2138
|
const targetProjectId = getFallbackProjectId();
|
|
1662
2139
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1665,7 +2142,10 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1665
2142
|
};
|
|
1666
2143
|
}
|
|
1667
2144
|
try {
|
|
1668
|
-
const identity
|
|
2145
|
+
const { identity, agentSession } = await resolveWorkerToolIdentity({
|
|
2146
|
+
roomId: targetRoomId ?? currentRoom?.room_id ?? null,
|
|
2147
|
+
agentSessionId: agent_session_id,
|
|
2148
|
+
});
|
|
1669
2149
|
const nextAssignee = status === "assigned" && !assignee ? identity.actor_label : assignee;
|
|
1670
2150
|
const nextAssigneeAgentKey = nextAssignee === identity.actor_label ? identity.canonical_key : undefined;
|
|
1671
2151
|
const updated = await roomScopedApiCall({
|
|
@@ -1683,6 +2163,8 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1683
2163
|
workflow_artifacts,
|
|
1684
2164
|
actor_label: identity.actor_label,
|
|
1685
2165
|
actor_key: identity.canonical_key,
|
|
2166
|
+
actor_instance_id: agentSession?.agent_instance_id || AGENT_INSTANCE_UUID,
|
|
2167
|
+
...agentSessionCredentials(agentSession),
|
|
1686
2168
|
}),
|
|
1687
2169
|
},
|
|
1688
2170
|
});
|
|
@@ -1691,7 +2173,7 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1691
2173
|
status_text: status
|
|
1692
2174
|
? `${task_id} -> ${status}`
|
|
1693
2175
|
: getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity).status_text,
|
|
1694
|
-
});
|
|
2176
|
+
}, agentSession);
|
|
1695
2177
|
return {
|
|
1696
2178
|
content: [
|
|
1697
2179
|
{
|
|
@@ -1717,8 +2199,9 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
|
|
|
1717
2199
|
task_id: z.string().describe("The task ID to submit for review"),
|
|
1718
2200
|
pr_url: z.string().optional().describe("GitHub PR URL for the work"),
|
|
1719
2201
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1720
|
-
conversation_id: z.string().optional().describe("
|
|
1721
|
-
|
|
2202
|
+
conversation_id: z.string().optional().describe("Deprecated for worker writes; registered worker session identity is used."),
|
|
2203
|
+
agent_session_id: z.string().optional().describe("Registered agent session to use for this task action."),
|
|
2204
|
+
}, async ({ task_id, pr_url, room_id, conversation_id: _conversation_id, agent_session_id }) => {
|
|
1722
2205
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1723
2206
|
const targetProjectId = getFallbackProjectId();
|
|
1724
2207
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1727,7 +2210,10 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
|
|
|
1727
2210
|
};
|
|
1728
2211
|
}
|
|
1729
2212
|
try {
|
|
1730
|
-
const identity
|
|
2213
|
+
const { identity, agentSession } = await resolveWorkerToolIdentity({
|
|
2214
|
+
roomId: targetRoomId ?? currentRoom?.room_id ?? null,
|
|
2215
|
+
agentSessionId: agent_session_id,
|
|
2216
|
+
});
|
|
1731
2217
|
const updated = await roomScopedApiCall({
|
|
1732
2218
|
room_id: targetRoomId,
|
|
1733
2219
|
project_id: targetProjectId,
|
|
@@ -1740,13 +2226,15 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
|
|
|
1740
2226
|
pr_url,
|
|
1741
2227
|
actor_label: identity.actor_label,
|
|
1742
2228
|
actor_key: identity.canonical_key,
|
|
2229
|
+
actor_instance_id: agentSession?.agent_instance_id || AGENT_INSTANCE_UUID,
|
|
2230
|
+
...agentSessionCredentials(agentSession),
|
|
1743
2231
|
}),
|
|
1744
2232
|
},
|
|
1745
2233
|
});
|
|
1746
2234
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
|
|
1747
2235
|
status: "reviewing",
|
|
1748
2236
|
status_text: `${task_id} -> in_review`,
|
|
1749
|
-
});
|
|
2237
|
+
}, agentSession);
|
|
1750
2238
|
return {
|
|
1751
2239
|
content: [{
|
|
1752
2240
|
type: "text",
|
|
@@ -1764,6 +2252,114 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
|
|
|
1764
2252
|
};
|
|
1765
2253
|
}
|
|
1766
2254
|
});
|
|
2255
|
+
server.tool("release_task_lease", "Release or forcibly clear the active work lease on a task so it can be claimed again. " +
|
|
2256
|
+
"Use this when the current worker is blocked, gone, or needs to give the lane back to the room.", {
|
|
2257
|
+
task_id: z.string().describe("The task whose active work lease should be cleared"),
|
|
2258
|
+
lease_id: z.string().optional().describe("Optional expected active lease id for stale-checking"),
|
|
2259
|
+
reason: z.string().optional().describe("Why the lease is being released"),
|
|
2260
|
+
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
2261
|
+
conversation_id: z.string().optional().describe("Deprecated for worker writes; registered worker session identity is used."),
|
|
2262
|
+
agent_session_id: z.string().optional().describe("Registered agent session to use for this lease action."),
|
|
2263
|
+
}, async ({ task_id, lease_id, reason, room_id, conversation_id: _conversation_id, agent_session_id }) => {
|
|
2264
|
+
const targetRoomId = getTargetRoomId(room_id);
|
|
2265
|
+
if (!targetRoomId) {
|
|
2266
|
+
return {
|
|
2267
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a canonical room." }) }],
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
try {
|
|
2271
|
+
const { identity, agentSession } = await resolveWorkerToolIdentity({
|
|
2272
|
+
roomId: targetRoomId,
|
|
2273
|
+
agentSessionId: agent_session_id,
|
|
2274
|
+
});
|
|
2275
|
+
const result = await apiCall(`/rooms/${encodeRoomIdPath(targetRoomId)}/tasks/${encodeURIComponent(task_id)}/lease-action`, {
|
|
2276
|
+
method: "POST",
|
|
2277
|
+
body: JSON.stringify({
|
|
2278
|
+
action: "release",
|
|
2279
|
+
lease_id: lease_id ?? undefined,
|
|
2280
|
+
reason,
|
|
2281
|
+
actor_label: identity.actor_label,
|
|
2282
|
+
actor_key: identity.canonical_key,
|
|
2283
|
+
actor_instance_id: agentSession?.agent_instance_id || AGENT_INSTANCE_UUID,
|
|
2284
|
+
...agentSessionCredentials(agentSession),
|
|
2285
|
+
}),
|
|
2286
|
+
});
|
|
2287
|
+
await syncRoomPresence(targetRoomId, identity, {
|
|
2288
|
+
status: "working",
|
|
2289
|
+
status_text: `released lease on ${task_id}`,
|
|
2290
|
+
}, agentSession);
|
|
2291
|
+
return {
|
|
2292
|
+
content: [{
|
|
2293
|
+
type: "text",
|
|
2294
|
+
text: JSON.stringify({
|
|
2295
|
+
success: true,
|
|
2296
|
+
...result,
|
|
2297
|
+
agent_identity: toPublicAgentIdentity(identity),
|
|
2298
|
+
}, null, 2),
|
|
2299
|
+
}],
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
catch (error) {
|
|
2303
|
+
return {
|
|
2304
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }) }],
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
server.tool("handoff_task_lease", "Transfer the active work lease on a task to another agent. " +
|
|
2309
|
+
"This reassigns the task and mints a fresh work lease for the target agent key.", {
|
|
2310
|
+
task_id: z.string().describe("The task whose active work lease should be transferred"),
|
|
2311
|
+
target_agent_key: z.string().describe("Canonical agent key to receive the new work lease"),
|
|
2312
|
+
lease_id: z.string().optional().describe("Optional expected active lease id for stale-checking"),
|
|
2313
|
+
reason: z.string().optional().describe("Why the lane is being handed off"),
|
|
2314
|
+
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
2315
|
+
conversation_id: z.string().optional().describe("Deprecated for worker writes; registered worker session identity is used."),
|
|
2316
|
+
agent_session_id: z.string().optional().describe("Registered agent session to use for this lease action."),
|
|
2317
|
+
}, async ({ task_id, target_agent_key, lease_id, reason, room_id, conversation_id: _conversation_id, agent_session_id }) => {
|
|
2318
|
+
const targetRoomId = getTargetRoomId(room_id);
|
|
2319
|
+
if (!targetRoomId) {
|
|
2320
|
+
return {
|
|
2321
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a canonical room." }) }],
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
try {
|
|
2325
|
+
const { identity, agentSession } = await resolveWorkerToolIdentity({
|
|
2326
|
+
roomId: targetRoomId,
|
|
2327
|
+
agentSessionId: agent_session_id,
|
|
2328
|
+
});
|
|
2329
|
+
const result = await apiCall(`/rooms/${encodeRoomIdPath(targetRoomId)}/tasks/${encodeURIComponent(task_id)}/lease-action`, {
|
|
2330
|
+
method: "POST",
|
|
2331
|
+
body: JSON.stringify({
|
|
2332
|
+
action: "handoff",
|
|
2333
|
+
lease_id: lease_id ?? undefined,
|
|
2334
|
+
reason,
|
|
2335
|
+
target_actor_key: target_agent_key,
|
|
2336
|
+
actor_label: identity.actor_label,
|
|
2337
|
+
actor_key: identity.canonical_key,
|
|
2338
|
+
actor_instance_id: agentSession?.agent_instance_id || AGENT_INSTANCE_UUID,
|
|
2339
|
+
...agentSessionCredentials(agentSession),
|
|
2340
|
+
}),
|
|
2341
|
+
});
|
|
2342
|
+
await syncRoomPresence(targetRoomId, identity, {
|
|
2343
|
+
status: "working",
|
|
2344
|
+
status_text: `handed off ${task_id} to ${target_agent_key}`,
|
|
2345
|
+
}, agentSession);
|
|
2346
|
+
return {
|
|
2347
|
+
content: [{
|
|
2348
|
+
type: "text",
|
|
2349
|
+
text: JSON.stringify({
|
|
2350
|
+
success: true,
|
|
2351
|
+
...result,
|
|
2352
|
+
agent_identity: toPublicAgentIdentity(identity),
|
|
2353
|
+
}, null, 2),
|
|
2354
|
+
}],
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
catch (error) {
|
|
2358
|
+
return {
|
|
2359
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }) }],
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
1767
2363
|
server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat by creating a .letagents.json config file. " +
|
|
1768
2364
|
"This explicitly sets up repo-based room auto-join. Reads git remote to derive the room name, " +
|
|
1769
2365
|
"or accepts a custom room name. Will NOT overwrite an existing .letagents.json. " +
|
|
@@ -1923,14 +2519,21 @@ server.tool("send_message", "Send a message to a Let Agents Chat room. Use a sho
|
|
|
1923
2519
|
conversation_id: z
|
|
1924
2520
|
.string()
|
|
1925
2521
|
.optional()
|
|
1926
|
-
.describe("
|
|
1927
|
-
|
|
2522
|
+
.describe("Deprecated for worker writes; registered worker session identity is used."),
|
|
2523
|
+
agent_session_id: z
|
|
2524
|
+
.string()
|
|
2525
|
+
.optional()
|
|
2526
|
+
.describe("Registered agent session to use for this message."),
|
|
2527
|
+
}, async ({ room_id, sender: _sender, text, reply_to, conversation_id: _conversation_id, agent_session_id }) => {
|
|
1928
2528
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1929
2529
|
const targetProjectId = getFallbackProjectId();
|
|
1930
2530
|
if (!targetRoomId && !targetProjectId) {
|
|
1931
2531
|
throw new Error("No room is currently selected. Join a room first or pass room_id.");
|
|
1932
2532
|
}
|
|
1933
|
-
const identity
|
|
2533
|
+
const { identity, agentSession } = await resolveWorkerToolIdentity({
|
|
2534
|
+
roomId: targetRoomId ?? currentRoom?.room_id ?? null,
|
|
2535
|
+
agentSessionId: agent_session_id,
|
|
2536
|
+
});
|
|
1934
2537
|
const message = await roomScopedApiCall({
|
|
1935
2538
|
room_id: targetRoomId,
|
|
1936
2539
|
project_id: targetProjectId,
|
|
@@ -1938,11 +2541,16 @@ server.tool("send_message", "Send a message to a Let Agents Chat room. Use a sho
|
|
|
1938
2541
|
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
|
|
1939
2542
|
options: {
|
|
1940
2543
|
method: "POST",
|
|
1941
|
-
body: JSON.stringify({
|
|
2544
|
+
body: JSON.stringify({
|
|
2545
|
+
sender: identity.actor_label,
|
|
2546
|
+
text,
|
|
2547
|
+
reply_to,
|
|
2548
|
+
...agentSessionCredentials(agentSession),
|
|
2549
|
+
}),
|
|
1942
2550
|
},
|
|
1943
2551
|
});
|
|
1944
2552
|
touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
|
|
1945
|
-
await
|
|
2553
|
+
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity), agentSession);
|
|
1946
2554
|
return {
|
|
1947
2555
|
content: [
|
|
1948
2556
|
{
|
|
@@ -2013,11 +2621,16 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
2013
2621
|
.number()
|
|
2014
2622
|
.optional()
|
|
2015
2623
|
.describe("Maximum wait time in milliseconds (min 1000, capped by LETAGENTS_POLL_MAX_MS / server). If set to 0, the default timeout will be used."),
|
|
2016
|
-
|
|
2624
|
+
agent_session_id: z
|
|
2625
|
+
.string()
|
|
2626
|
+
.optional()
|
|
2627
|
+
.describe("Registered agent session to use. Without this, the MCP transport is treated as a controller and is hidden from connected-agent activity."),
|
|
2628
|
+
}, async ({ room_id, after_message_id, timeout, agent_session_id }) => {
|
|
2017
2629
|
const targetRoomId = getTargetRoomId(room_id);
|
|
2018
2630
|
const targetProjectId = getFallbackProjectId();
|
|
2019
2631
|
const identity = await ensureAgentIdentity();
|
|
2020
|
-
|
|
2632
|
+
const agentSession = resolveAgentSession(targetRoomId ?? currentRoom?.room_id ?? null, agent_session_id);
|
|
2633
|
+
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
|
|
2021
2634
|
const maxPollMs = getPollTimeoutCapMs();
|
|
2022
2635
|
const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), maxPollMs);
|
|
2023
2636
|
const clientTimeout = serverTimeout + (serverTimeout > 120_000 ? 120_000 : 5_000);
|
|
@@ -2026,12 +2639,16 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
2026
2639
|
params.set("after", after_message_id);
|
|
2027
2640
|
params.set("timeout", String(serverTimeout));
|
|
2028
2641
|
const queryString = params.toString();
|
|
2642
|
+
const deliveryHeaders = buildAgentDeliveryHeaders(agentSession);
|
|
2029
2643
|
const firstResult = await roomScopedApiCall({
|
|
2030
2644
|
room_id: targetRoomId,
|
|
2031
2645
|
project_id: targetProjectId,
|
|
2032
2646
|
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`,
|
|
2033
2647
|
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`,
|
|
2034
|
-
options: {
|
|
2648
|
+
options: {
|
|
2649
|
+
signal: AbortSignal.timeout(clientTimeout),
|
|
2650
|
+
headers: deliveryHeaders,
|
|
2651
|
+
},
|
|
2035
2652
|
});
|
|
2036
2653
|
const allMessages = [...(firstResult.messages ?? [])];
|
|
2037
2654
|
const roomIdFromResponse = firstResult.room_id || firstResult.project_id;
|
|
@@ -2064,7 +2681,7 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
2064
2681
|
if (targetRoomId) {
|
|
2065
2682
|
touchRoomSession(targetRoomId, getLastMessageId(output));
|
|
2066
2683
|
}
|
|
2067
|
-
await
|
|
2684
|
+
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
|
|
2068
2685
|
return {
|
|
2069
2686
|
content: [
|
|
2070
2687
|
{
|