letagents 0.12.2 → 0.12.3
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/agent-presence.js +39 -0
- package/dist/mcp/room-events-query.js +18 -0
- package/dist/mcp/server.js +117 -5
- package/dist/shared/agent-presence.js +26 -0
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const IDLE_STATUS_RE = /\b(idle|available|online|polling|monitoring|watching|ready)\b/i;
|
|
2
|
+
const REVIEWING_STATUS_RE = /\b(review|reviewing|approve|approval|approving)\b/i;
|
|
3
|
+
const BLOCKED_STATUS_RE = /\b(blocked|waiting|stuck)\b/i;
|
|
4
|
+
export function classifyPresenceStatusText(statusText, fallback = "working") {
|
|
5
|
+
const normalized = statusText.trim();
|
|
6
|
+
if (!normalized) {
|
|
7
|
+
return fallback;
|
|
8
|
+
}
|
|
9
|
+
if (BLOCKED_STATUS_RE.test(normalized)) {
|
|
10
|
+
return "blocked";
|
|
11
|
+
}
|
|
12
|
+
if (IDLE_STATUS_RE.test(normalized)) {
|
|
13
|
+
return "idle";
|
|
14
|
+
}
|
|
15
|
+
if (REVIEWING_STATUS_RE.test(normalized)) {
|
|
16
|
+
return "reviewing";
|
|
17
|
+
}
|
|
18
|
+
return "working";
|
|
19
|
+
}
|
|
20
|
+
export function deriveTaskPresenceStatus(taskStatus, fallback = "working") {
|
|
21
|
+
switch (taskStatus) {
|
|
22
|
+
case "blocked":
|
|
23
|
+
return "blocked";
|
|
24
|
+
case "in_review":
|
|
25
|
+
return "reviewing";
|
|
26
|
+
case "merged":
|
|
27
|
+
case "done":
|
|
28
|
+
case "accepted":
|
|
29
|
+
return "idle";
|
|
30
|
+
case "assigned":
|
|
31
|
+
case "in_progress":
|
|
32
|
+
return "working";
|
|
33
|
+
default:
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function getRoomIdentityPresenceCacheKey(roomId, actorLabel) {
|
|
38
|
+
return JSON.stringify([roomId, actorLabel]);
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function buildRoomEventsQueryString(input) {
|
|
2
|
+
const params = new URLSearchParams();
|
|
3
|
+
if (input.event_type)
|
|
4
|
+
params.set("event_type", input.event_type);
|
|
5
|
+
if (input.object_id)
|
|
6
|
+
params.set("object_id", input.object_id);
|
|
7
|
+
if (input.actor)
|
|
8
|
+
params.set("actor", input.actor);
|
|
9
|
+
if (input.since)
|
|
10
|
+
params.set("since", input.since);
|
|
11
|
+
if (input.until)
|
|
12
|
+
params.set("until", input.until);
|
|
13
|
+
if (input.after)
|
|
14
|
+
params.set("after", input.after);
|
|
15
|
+
if (input.limit)
|
|
16
|
+
params.set("limit", String(input.limit));
|
|
17
|
+
return params.toString();
|
|
18
|
+
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -16,10 +16,13 @@ import { encodeRoomIdPath, getCanonicalRoomWebPath, looksLikeInviteCode, normali
|
|
|
16
16
|
import { buildAgentActorLabel, formatOwnerAttribution, inferAgentIdeLabel, toTitleCaseCodename, } from "../shared/agent-identity.js";
|
|
17
17
|
import { buildRoomAgentPrompt, normalizeAgentPromptKind, } from "../shared/room-agent-prompts.js";
|
|
18
18
|
import { inspectLocalCodexSession, startLocalCodexSession, stopLocalCodexSession, toPublicCodexLiveSession, } from "./codex-session.js";
|
|
19
|
+
import { classifyPresenceStatusText, deriveTaskPresenceStatus, getRoomIdentityPresenceCacheKey, } from "./agent-presence.js";
|
|
20
|
+
import { buildRoomEventsQueryString } from "./room-events-query.js";
|
|
19
21
|
let currentRoom = null;
|
|
20
22
|
let currentAgentIdentityKey = "";
|
|
21
23
|
let currentAgentIdentity = null;
|
|
22
24
|
let currentAuthenticatedAccount = undefined;
|
|
25
|
+
const roomPresenceByIdentity = new Map();
|
|
23
26
|
// ---------------------------------------------------------------------------
|
|
24
27
|
// Conversation-scoped identity (Option C: per-conversation hints)
|
|
25
28
|
// ---------------------------------------------------------------------------
|
|
@@ -734,6 +737,42 @@ function touchCurrentRoom(lastMessageId) {
|
|
|
734
737
|
}
|
|
735
738
|
touchRoomSession(currentRoom.room_id, lastMessageId);
|
|
736
739
|
}
|
|
740
|
+
function getRememberedRoomPresence(roomId, identity) {
|
|
741
|
+
if (!roomId || !identity) {
|
|
742
|
+
return { status: "idle", status_text: null };
|
|
743
|
+
}
|
|
744
|
+
return (roomPresenceByIdentity.get(getRoomIdentityPresenceCacheKey(roomId, identity.actor_label)) ?? { status: "idle", status_text: null });
|
|
745
|
+
}
|
|
746
|
+
async function syncRoomPresence(roomId, identity, presence) {
|
|
747
|
+
if (!roomId || !identity) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
roomPresenceByIdentity.set(getRoomIdentityPresenceCacheKey(roomId, identity.actor_label), presence);
|
|
751
|
+
try {
|
|
752
|
+
await apiCall(`/rooms/${encodeRoomIdPath(roomId)}/presence`, {
|
|
753
|
+
method: "POST",
|
|
754
|
+
body: JSON.stringify({
|
|
755
|
+
actor_label: identity.actor_label,
|
|
756
|
+
agent_key: identity.canonical_key,
|
|
757
|
+
display_name: identity.display_name,
|
|
758
|
+
owner_label: identity.owner_label,
|
|
759
|
+
ide_label: identity.ide_label,
|
|
760
|
+
status: presence.status,
|
|
761
|
+
status_text: presence.status_text,
|
|
762
|
+
}),
|
|
763
|
+
});
|
|
764
|
+
touchRoomSession(roomId);
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
if (isMissingRouteError(error)) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function heartbeatRoomPresence(roomId, identity) {
|
|
774
|
+
await syncRoomPresence(roomId, identity, getRememberedRoomPresence(roomId, identity));
|
|
775
|
+
}
|
|
737
776
|
function getTargetRoomId(roomId) {
|
|
738
777
|
return roomId || currentRoom?.room_id || null;
|
|
739
778
|
}
|
|
@@ -827,6 +866,10 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
827
866
|
joined_via: joinedVia,
|
|
828
867
|
}));
|
|
829
868
|
const agentIdentity = await ensureAgentIdentity();
|
|
869
|
+
await syncRoomPresence(room.room_id, agentIdentity, {
|
|
870
|
+
status: "idle",
|
|
871
|
+
status_text: "online in room",
|
|
872
|
+
});
|
|
830
873
|
return {
|
|
831
874
|
room,
|
|
832
875
|
response: {
|
|
@@ -855,6 +898,10 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
855
898
|
joined_via: joinedVia,
|
|
856
899
|
}));
|
|
857
900
|
const agentIdentity = await ensureAgentIdentity();
|
|
901
|
+
await syncRoomPresence(room.room_id, agentIdentity, {
|
|
902
|
+
status: "idle",
|
|
903
|
+
status_text: "online in room",
|
|
904
|
+
});
|
|
858
905
|
return {
|
|
859
906
|
room,
|
|
860
907
|
response: {
|
|
@@ -883,6 +930,10 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
883
930
|
joined_via: joinedVia,
|
|
884
931
|
}));
|
|
885
932
|
const agentIdentity = await ensureAgentIdentity();
|
|
933
|
+
await syncRoomPresence(room.room_id, agentIdentity, {
|
|
934
|
+
status: "idle",
|
|
935
|
+
status_text: "online in room",
|
|
936
|
+
});
|
|
886
937
|
return {
|
|
887
938
|
room,
|
|
888
939
|
response: {
|
|
@@ -908,6 +959,10 @@ async function createInviteRoom() {
|
|
|
908
959
|
joined_via: "join_code",
|
|
909
960
|
}));
|
|
910
961
|
const agentIdentity = await ensureAgentIdentity();
|
|
962
|
+
await syncRoomPresence(room.room_id, agentIdentity, {
|
|
963
|
+
status: "idle",
|
|
964
|
+
status_text: "online in room",
|
|
965
|
+
});
|
|
911
966
|
return {
|
|
912
967
|
room,
|
|
913
968
|
response: {
|
|
@@ -1362,6 +1417,10 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
1362
1417
|
},
|
|
1363
1418
|
});
|
|
1364
1419
|
touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
|
|
1420
|
+
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
|
|
1421
|
+
status: classifyPresenceStatusText(status, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity).status),
|
|
1422
|
+
status_text: status,
|
|
1423
|
+
});
|
|
1365
1424
|
return {
|
|
1366
1425
|
content: [
|
|
1367
1426
|
{
|
|
@@ -1420,6 +1479,7 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
|
|
|
1420
1479
|
}),
|
|
1421
1480
|
},
|
|
1422
1481
|
});
|
|
1482
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity);
|
|
1423
1483
|
return {
|
|
1424
1484
|
content: [
|
|
1425
1485
|
{
|
|
@@ -1471,10 +1531,49 @@ server.tool("get_board", "Get the current task board for the room. By default sh
|
|
|
1471
1531
|
break;
|
|
1472
1532
|
afterCursor = lastTask.id;
|
|
1473
1533
|
}
|
|
1534
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, await ensureAgentIdentity());
|
|
1474
1535
|
return {
|
|
1475
1536
|
content: [{ type: "text", text: JSON.stringify({ success: true, tasks: allTasks }, null, 2) }],
|
|
1476
1537
|
};
|
|
1477
1538
|
});
|
|
1539
|
+
server.tool("get_room_events", "Get GitHub events for the room (PRs, issues, reviews, check runs, etc.). " +
|
|
1540
|
+
"Returns a paginated list of normalized GitHub events persisted from webhooks. " +
|
|
1541
|
+
"Use this to check what happened in the repo without parsing chat messages.", {
|
|
1542
|
+
event_type: z.string().optional().describe("Filter by event type: pull_request, issue, issue_comment, pull_request_review, check_run, installation, installation_repositories, repository"),
|
|
1543
|
+
object_id: z.string().optional().describe("Filter by GitHub object ID (e.g. PR number, issue number)"),
|
|
1544
|
+
actor: z.string().optional().describe("Filter by GitHub login of the actor"),
|
|
1545
|
+
since: z.string().optional().describe("ISO timestamp — only events after this time"),
|
|
1546
|
+
until: z.string().optional().describe("ISO timestamp — only events before this time"),
|
|
1547
|
+
after: z.string().optional().describe("Cursor event ID for pagination (from a previous response)"),
|
|
1548
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max events to return (default 50, max 100)"),
|
|
1549
|
+
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1550
|
+
}, async ({ event_type, object_id, actor, since, until, after, limit, room_id }) => {
|
|
1551
|
+
const targetRoomId = getTargetRoomId(room_id);
|
|
1552
|
+
const targetProjectId = getFallbackProjectId();
|
|
1553
|
+
if (!targetRoomId && !targetProjectId) {
|
|
1554
|
+
return {
|
|
1555
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
const qs = buildRoomEventsQueryString({
|
|
1559
|
+
event_type,
|
|
1560
|
+
object_id,
|
|
1561
|
+
actor,
|
|
1562
|
+
since,
|
|
1563
|
+
until,
|
|
1564
|
+
after,
|
|
1565
|
+
limit,
|
|
1566
|
+
});
|
|
1567
|
+
const result = await roomScopedApiCall({
|
|
1568
|
+
room_id: targetRoomId,
|
|
1569
|
+
project_id: targetProjectId,
|
|
1570
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/events${qs ? `?${qs}` : ""}`,
|
|
1571
|
+
project_path: (targetProjectId) => `/rooms/${encodeURIComponent(targetProjectId)}/events${qs ? `?${qs}` : ""}`,
|
|
1572
|
+
});
|
|
1573
|
+
return {
|
|
1574
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, events: result.events ?? [], has_more: result.has_more ?? false }, null, 2) }],
|
|
1575
|
+
};
|
|
1576
|
+
});
|
|
1478
1577
|
server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted' " +
|
|
1479
1578
|
"status. This sets the assignee to you and moves the status to 'assigned'. " +
|
|
1480
1579
|
"Do NOT claim proposed tasks — they need to be accepted first.", {
|
|
@@ -1505,6 +1604,10 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
1505
1604
|
body: JSON.stringify({ status: "assigned", assignee: identity.actor_label }),
|
|
1506
1605
|
},
|
|
1507
1606
|
});
|
|
1607
|
+
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
|
|
1608
|
+
status: "working",
|
|
1609
|
+
status_text: `claimed ${task_id}`,
|
|
1610
|
+
});
|
|
1508
1611
|
return {
|
|
1509
1612
|
content: [
|
|
1510
1613
|
{
|
|
@@ -1555,9 +1658,7 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1555
1658
|
};
|
|
1556
1659
|
}
|
|
1557
1660
|
try {
|
|
1558
|
-
const identity =
|
|
1559
|
-
? (getConversationIdentity(conversation_id) ?? await ensureAgentIdentity())
|
|
1560
|
-
: null;
|
|
1661
|
+
const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
|
|
1561
1662
|
const updated = await roomScopedApiCall({
|
|
1562
1663
|
room_id: targetRoomId,
|
|
1563
1664
|
project_id: targetProjectId,
|
|
@@ -1567,12 +1668,18 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1567
1668
|
method: "PATCH",
|
|
1568
1669
|
body: JSON.stringify({
|
|
1569
1670
|
status,
|
|
1570
|
-
assignee: assignee
|
|
1671
|
+
assignee: status === "assigned" && !assignee ? identity.actor_label : assignee,
|
|
1571
1672
|
pr_url,
|
|
1572
1673
|
workflow_artifacts,
|
|
1573
1674
|
}),
|
|
1574
1675
|
},
|
|
1575
1676
|
});
|
|
1677
|
+
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
|
|
1678
|
+
status: deriveTaskPresenceStatus(status ?? null, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity).status),
|
|
1679
|
+
status_text: status
|
|
1680
|
+
? `${task_id} -> ${status}`
|
|
1681
|
+
: getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity).status_text,
|
|
1682
|
+
});
|
|
1576
1683
|
return {
|
|
1577
1684
|
content: [
|
|
1578
1685
|
{
|
|
@@ -1580,7 +1687,7 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1580
1687
|
text: JSON.stringify({
|
|
1581
1688
|
success: true,
|
|
1582
1689
|
task: updated,
|
|
1583
|
-
agent_identity:
|
|
1690
|
+
agent_identity: toPublicAgentIdentity(identity),
|
|
1584
1691
|
}, null, 2),
|
|
1585
1692
|
},
|
|
1586
1693
|
],
|
|
@@ -1806,6 +1913,7 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
|
|
|
1806
1913
|
},
|
|
1807
1914
|
});
|
|
1808
1915
|
touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
|
|
1916
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity);
|
|
1809
1917
|
return {
|
|
1810
1918
|
content: [
|
|
1811
1919
|
{
|
|
@@ -1850,6 +1958,7 @@ server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
|
|
|
1850
1958
|
break;
|
|
1851
1959
|
afterCursor = lastMsg.id;
|
|
1852
1960
|
}
|
|
1961
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, await ensureAgentIdentity());
|
|
1853
1962
|
const output = { messages: toAgentReadableMessages(allMessages) };
|
|
1854
1963
|
if (roomIdFromResponse) {
|
|
1855
1964
|
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
@@ -1879,6 +1988,8 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
1879
1988
|
}, async ({ room_id, after_message_id, timeout }) => {
|
|
1880
1989
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1881
1990
|
const targetProjectId = getFallbackProjectId();
|
|
1991
|
+
const identity = await ensureAgentIdentity();
|
|
1992
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity);
|
|
1882
1993
|
const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), MAX_POLL_TIMEOUT_MS);
|
|
1883
1994
|
const clientTimeout = serverTimeout + 5000; // 5s buffer over server timeout
|
|
1884
1995
|
const params = new URLSearchParams();
|
|
@@ -1924,6 +2035,7 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
1924
2035
|
if (targetRoomId) {
|
|
1925
2036
|
touchRoomSession(targetRoomId, getLastMessageId(output));
|
|
1926
2037
|
}
|
|
2038
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity);
|
|
1927
2039
|
return {
|
|
1928
2040
|
content: [
|
|
1929
2041
|
{
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const AGENT_PRESENCE_STATUSES = [
|
|
2
|
+
"idle",
|
|
3
|
+
"working",
|
|
4
|
+
"reviewing",
|
|
5
|
+
"blocked",
|
|
6
|
+
];
|
|
7
|
+
export const AGENT_PRESENCE_FRESHNESS = [
|
|
8
|
+
"active",
|
|
9
|
+
"stale",
|
|
10
|
+
];
|
|
11
|
+
export const ACTIVE_AGENT_PRESENCE_WINDOW_MS = 90_000;
|
|
12
|
+
export function normalizeAgentPresenceStatus(value) {
|
|
13
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
14
|
+
return AGENT_PRESENCE_STATUSES.includes(normalized)
|
|
15
|
+
? normalized
|
|
16
|
+
: null;
|
|
17
|
+
}
|
|
18
|
+
export function getAgentPresenceFreshness(lastHeartbeatAt, now = Date.now()) {
|
|
19
|
+
const heartbeatTime = new Date(lastHeartbeatAt).getTime();
|
|
20
|
+
if (!Number.isFinite(heartbeatTime)) {
|
|
21
|
+
return "stale";
|
|
22
|
+
}
|
|
23
|
+
return now - heartbeatTime <= ACTIVE_AGENT_PRESENCE_WINDOW_MS
|
|
24
|
+
? "active"
|
|
25
|
+
: "stale";
|
|
26
|
+
}
|