letagents 0.12.9 → 0.12.11
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/README.md +18 -15
- package/dist/api/board-intent-payloads.js +25 -0
- package/dist/mcp/agent-prompt-delivery.js +20 -0
- package/dist/mcp/codex-session/app-server.js +89 -0
- package/dist/mcp/codex-session/index.js +10 -0
- package/dist/mcp/codex-session/rpc-client.js +85 -0
- package/dist/mcp/codex-session/runtime-bridge.js +271 -0
- package/dist/mcp/codex-session/runtime-summary.js +220 -0
- package/dist/mcp/codex-session/session-inspection.js +100 -0
- package/dist/mcp/codex-session/session-mapper.js +51 -0
- package/dist/mcp/codex-session/session-start.js +182 -0
- package/dist/mcp/codex-session/session-status.js +77 -0
- package/dist/mcp/codex-session/session-stop.js +43 -0
- package/dist/mcp/codex-session/session-supervisor.js +111 -0
- package/dist/mcp/codex-session/start-prompt.js +49 -0
- package/dist/mcp/codex-session/types.js +1 -0
- package/dist/mcp/codex-session.js +1 -672
- package/dist/mcp/config-reader.js +10 -2
- package/dist/mcp/git-remote.js +90 -14
- package/dist/mcp/local-state/agent-identity.js +26 -0
- package/dist/mcp/local-state/agent-sessions.js +66 -0
- package/dist/mcp/local-state/auth.js +57 -0
- package/dist/mcp/local-state/codex-live-sessions.js +59 -0
- package/dist/mcp/local-state/index.js +8 -0
- package/dist/mcp/local-state/local-chat.js +808 -0
- package/dist/mcp/local-state/room-sessions.js +51 -0
- package/dist/mcp/local-state/storage.js +95 -0
- package/dist/mcp/local-state/types.js +1 -0
- package/dist/mcp/local-state.js +1 -351
- package/dist/mcp/managed-agent-providers.js +65 -0
- package/dist/mcp/rental-tools/activity.js +37 -0
- package/dist/mcp/rental-tools/budget.js +36 -0
- package/dist/mcp/rental-tools/context.js +92 -0
- package/dist/mcp/rental-tools/lifecycle.js +47 -0
- package/dist/mcp/rental-tools/metering.js +73 -0
- package/dist/mcp/rental-tools/patches.js +100 -0
- package/dist/mcp/rental-tools/provider.js +117 -0
- package/dist/mcp/rental-tools/shared.js +19 -0
- package/dist/mcp/rental-tools/types.js +1 -0
- package/dist/mcp/rental-tools.js +7 -0
- package/dist/mcp/repo-visibility.js +8 -6
- package/dist/mcp/server/register-tools.js +19 -0
- package/dist/mcp/server/repo-context.js +28 -0
- package/dist/mcp/server/resources.js +77 -0
- package/dist/mcp/server/runtime/agent-sessions.js +173 -0
- package/dist/mcp/server/runtime/api.js +104 -0
- package/dist/mcp/server/runtime/auth-cache.js +20 -0
- package/dist/mcp/server/runtime/device-auth.js +48 -0
- package/dist/mcp/server/runtime/identity/config.js +58 -0
- package/dist/mcp/server/runtime/identity/directory.js +94 -0
- package/dist/mcp/server/runtime/identity/liveness.js +32 -0
- package/dist/mcp/server/runtime/identity/names.js +74 -0
- package/dist/mcp/server/runtime/identity/public.js +19 -0
- package/dist/mcp/server/runtime/identity/state.js +32 -0
- package/dist/mcp/server/runtime/identity.js +97 -0
- package/dist/mcp/server/runtime/messages.js +160 -0
- package/dist/mcp/server/runtime/presence.js +52 -0
- package/dist/mcp/server/runtime/room-api.js +42 -0
- package/dist/mcp/server/runtime/room-state.js +122 -0
- package/dist/mcp/server/runtime/rooms.js +374 -0
- package/dist/mcp/server/runtime/supervisor-bridge.js +94 -0
- package/dist/mcp/server/runtime/worker-bearer.js +67 -0
- package/dist/mcp/server/runtime.js +14 -0
- package/dist/mcp/server/tools/agent-sessions.js +415 -0
- package/dist/mcp/server/tools/messages/index.js +14 -0
- package/dist/mcp/server/tools/messages/message-lookup.js +75 -0
- package/dist/mcp/server/tools/messages/read-tool.js +176 -0
- package/dist/mcp/server/tools/messages/reasoning-tool.js +179 -0
- package/dist/mcp/server/tools/messages/response.js +17 -0
- package/dist/mcp/server/tools/messages/send-tool.js +160 -0
- package/dist/mcp/server/tools/messages/status-tool.js +87 -0
- package/dist/mcp/server/tools/messages/wait-tool.js +403 -0
- package/dist/mcp/server/tools/messages.js +1 -0
- package/dist/mcp/server/tools/onboarding/device-auth-tools.js +158 -0
- package/dist/mcp/server/tools/onboarding/name-tool.js +93 -0
- package/dist/mcp/server/tools/onboarding/responses.js +10 -0
- package/dist/mcp/server/tools/onboarding/status-tool.js +125 -0
- package/dist/mcp/server/tools/onboarding.js +8 -0
- package/dist/mcp/server/tools/rental/activity-tools.js +43 -0
- package/dist/mcp/server/tools/rental/context-tools.js +43 -0
- package/dist/mcp/server/tools/rental/index.js +17 -0
- package/dist/mcp/server/tools/rental/metering-tools.js +39 -0
- package/dist/mcp/server/tools/rental/patch-tools.js +53 -0
- package/dist/mcp/server/tools/rental/provider-tools.js +44 -0
- package/dist/mcp/server/tools/rental/response.js +10 -0
- package/dist/mcp/server/tools/rental/types.js +1 -0
- package/dist/mcp/server/tools/rental.js +1 -0
- package/dist/mcp/server/tools/rooms/index.js +5 -0
- package/dist/mcp/server/tools/rooms/inspection-tools.js +140 -0
- package/dist/mcp/server/tools/rooms/join-tools.js +37 -0
- package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +113 -0
- package/dist/mcp/server/tools/rooms/repo-visibility-tool.js +20 -0
- package/dist/mcp/server/tools/rooms/response.js +10 -0
- package/dist/mcp/server/tools/rooms/resume-tool.js +48 -0
- package/dist/mcp/server/tools/rooms.js +1 -0
- package/dist/mcp/server/tools/tasks/api.js +222 -0
- package/dist/mcp/server/tools/tasks/artifact-tools.js +75 -0
- package/dist/mcp/server/tools/tasks/board-intent-tools.js +326 -0
- package/dist/mcp/server/tools/tasks/board-tools.js +73 -0
- package/dist/mcp/server/tools/tasks/context.js +35 -0
- package/dist/mcp/server/tools/tasks/event-tools.js +40 -0
- package/dist/mcp/server/tools/tasks/index.js +16 -0
- package/dist/mcp/server/tools/tasks/lease-tools.js +145 -0
- package/dist/mcp/server/tools/tasks/mutation-tools.js +121 -0
- package/dist/mcp/server/tools/tasks/response.js +13 -0
- package/dist/mcp/server/tools/tasks/schemas.js +68 -0
- package/dist/mcp/server/tools/tasks/verdict-tools.js +51 -0
- package/dist/mcp/server/tools/tasks.js +1 -0
- package/dist/mcp/server.js +12 -3157
- package/dist/mcp/sse-client.js +9 -17
- package/dist/shared/activation-routing.js +272 -0
- package/dist/shared/agent-identity.js +21 -5
- package/dist/shared/agent-presence.js +5 -3
- package/dist/shared/agent-session-bearer.js +28 -0
- package/dist/shared/board-manager-failover.js +16 -0
- package/dist/shared/rental/lrt.js +214 -0
- package/dist/shared/rental/meter-types.js +14 -0
- package/dist/shared/room-agent-prompts.js +14 -1
- package/package.json +31 -16
- package/dist/mcp/__tests__/config-reader.test.js +0 -93
- package/dist/mcp/__tests__/git-remote.test.js +0 -37
- package/dist/mcp/__tests__/server-helpers.test.js +0 -121
- package/dist/shared/agent-reasoning.js +0 -49
- /package/dist/{mcp → shared}/codenames.js +0 -0
package/dist/mcp/sse-client.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { encodeRoomIdPath } from "./room-id.js";
|
|
2
|
-
import { buildRoomAgentPrompt, normalizeAgentPromptKind } from "../shared/room-agent-prompts.js";
|
|
3
2
|
export class SseClient {
|
|
4
3
|
apiUrl;
|
|
5
4
|
getAccessToken;
|
|
@@ -42,8 +41,8 @@ export class SseClient {
|
|
|
42
41
|
this.unsubscribe(roomId);
|
|
43
42
|
}
|
|
44
43
|
}
|
|
45
|
-
getHeaders() {
|
|
46
|
-
const token = this.getAccessToken?.();
|
|
44
|
+
async getHeaders() {
|
|
45
|
+
const token = await this.getAccessToken?.();
|
|
47
46
|
if (!token) {
|
|
48
47
|
return { Accept: "text/event-stream" };
|
|
49
48
|
}
|
|
@@ -83,7 +82,7 @@ export class SseClient {
|
|
|
83
82
|
}
|
|
84
83
|
async openStream(url, signal, onMessage) {
|
|
85
84
|
const response = await fetch(url, {
|
|
86
|
-
headers: this.getHeaders(),
|
|
85
|
+
headers: await this.getHeaders(),
|
|
87
86
|
signal,
|
|
88
87
|
});
|
|
89
88
|
if (!response.ok) {
|
|
@@ -123,19 +122,12 @@ export class SseClient {
|
|
|
123
122
|
if (dataLines.length === 0) {
|
|
124
123
|
return;
|
|
125
124
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
return {
|
|
134
|
-
...message,
|
|
135
|
-
visible_text: message.text,
|
|
136
|
-
agent_prompt: buildRoomAgentPrompt(kind),
|
|
137
|
-
prompt_injected: kind === "inline",
|
|
138
|
-
};
|
|
125
|
+
// No prompt enrichment here: the only SSE subscriber (room-state.ts) discards
|
|
126
|
+
// the message body, and expanding the room-agent prompt on this background
|
|
127
|
+
// path would consume the once-per-session full-prompt delivery before the
|
|
128
|
+
// agent ever sees it. Prompt expansion happens only on visible tool
|
|
129
|
+
// responses (runtime/messages.ts).
|
|
130
|
+
onMessage(JSON.parse(dataLines.join("\n")));
|
|
139
131
|
}
|
|
140
132
|
isMissingRouteError(error) {
|
|
141
133
|
return (error instanceof Error &&
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
const NON_AGENT_AT_HANDLES = new Set([
|
|
2
|
+
"charset",
|
|
3
|
+
"container",
|
|
4
|
+
"counter-style",
|
|
5
|
+
"font-face",
|
|
6
|
+
"font-feature-values",
|
|
7
|
+
"font-palette-values",
|
|
8
|
+
"import",
|
|
9
|
+
"keyframes",
|
|
10
|
+
"layer",
|
|
11
|
+
"media",
|
|
12
|
+
"namespace",
|
|
13
|
+
"page",
|
|
14
|
+
"package",
|
|
15
|
+
"property",
|
|
16
|
+
"scope",
|
|
17
|
+
"starting-style",
|
|
18
|
+
"supports",
|
|
19
|
+
"types",
|
|
20
|
+
"viewport",
|
|
21
|
+
]);
|
|
22
|
+
const TASK_OWNER_FOLLOW_UP_PATTERNS = [
|
|
23
|
+
/^(?:ok(?:ay)?|right|cool|great|nice)?[\s,]*(?:try again|retry|rerun|re-run|continue|proceed|go ahead|carry on)\b/,
|
|
24
|
+
/^(?:ok(?:ay)?|right|cool|great|nice)?[\s,]*(?:open|create|make|raise)\s+(?:a\s+)?pr\b/,
|
|
25
|
+
/^(?:ok(?:ay)?|right|cool|great|nice)?[\s,]*(?:push|merge|ship|fix|test|run|update)\s+(?:it|that|this|again|tests?|the\s+tests?|ci)\b/,
|
|
26
|
+
/\b(?:try again|open\s+(?:a\s+)?pr|create\s+(?:a\s+)?pr|make\s+(?:a\s+)?pr|push it|merge it|update it)\b/,
|
|
27
|
+
];
|
|
28
|
+
export function attachAgentMessageActivation(message, identity, context = {}) {
|
|
29
|
+
return {
|
|
30
|
+
...message,
|
|
31
|
+
activation: {
|
|
32
|
+
for_current_agent: decideAgentMessageActivation(message, identity, context),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function attachAgentMessageActivations(messages, identity, context = {}) {
|
|
37
|
+
if (!identity || identity.session_kind !== "worker") {
|
|
38
|
+
return [...messages];
|
|
39
|
+
}
|
|
40
|
+
return messages.map((message) => attachAgentMessageActivation(message, identity, context));
|
|
41
|
+
}
|
|
42
|
+
export function decideAgentMessageActivation(message, identity, context = {}) {
|
|
43
|
+
if (normalizeSender(message.source) === "managed_agent_failure") {
|
|
44
|
+
return decision("silent", "system_event");
|
|
45
|
+
}
|
|
46
|
+
if (senderMatchesIdentity(message.sender, identity)) {
|
|
47
|
+
return decision("silent", "self_message");
|
|
48
|
+
}
|
|
49
|
+
const mentions = extractMentionHandles(message.text);
|
|
50
|
+
if (mentions.some(isBroadcastHandle)) {
|
|
51
|
+
return decision("activate", "broadcast");
|
|
52
|
+
}
|
|
53
|
+
if (mentions.some((mention) => identityAliases(identity).has(normalizeMentionIdentityHandle(mention)))) {
|
|
54
|
+
return decision("activate", "explicit_mention");
|
|
55
|
+
}
|
|
56
|
+
if (hasBroadcastAddress(message.text)) {
|
|
57
|
+
return decision("activate", "broadcast");
|
|
58
|
+
}
|
|
59
|
+
if (mentions.some(isLikelyAgentMentionHandle)) {
|
|
60
|
+
return decision("silent", "explicit_other_mention");
|
|
61
|
+
}
|
|
62
|
+
if (senderMatchesIdentity(message.reply_to?.sender, identity)) {
|
|
63
|
+
return decision("activate", "reply_target");
|
|
64
|
+
}
|
|
65
|
+
if (isAgentReplyTarget(message.reply_to) && !isThreadReply(message)) {
|
|
66
|
+
return decision("silent", "other_reply_target");
|
|
67
|
+
}
|
|
68
|
+
if (isThreadReply(message) && threadParticipantsIncludeIdentity(message, identity)) {
|
|
69
|
+
return decision("activate", "thread_participant");
|
|
70
|
+
}
|
|
71
|
+
const taskOwnerDecision = decideTaskOwnerActivation(message, identity, context);
|
|
72
|
+
if (taskOwnerDecision) {
|
|
73
|
+
return taskOwnerDecision;
|
|
74
|
+
}
|
|
75
|
+
return decision("unclear", "unaddressed");
|
|
76
|
+
}
|
|
77
|
+
function decision(decisionValue, reason) {
|
|
78
|
+
return {
|
|
79
|
+
decision: decisionValue,
|
|
80
|
+
reason,
|
|
81
|
+
addressed: decisionValue === "activate",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function isThreadReply(message) {
|
|
85
|
+
const ownId = normalizedString(message.id);
|
|
86
|
+
const rootId = normalizedString(message.thread_root_id) || normalizedString(message.thread?.root_message_id);
|
|
87
|
+
return Boolean(ownId && rootId && ownId !== rootId);
|
|
88
|
+
}
|
|
89
|
+
function isAgentReplyTarget(replyTo) {
|
|
90
|
+
return normalizeSender(replyTo?.source) === "agent";
|
|
91
|
+
}
|
|
92
|
+
function threadParticipantsIncludeIdentity(message, identity) {
|
|
93
|
+
const senders = [
|
|
94
|
+
message.reply_to?.sender,
|
|
95
|
+
message.thread?.latest_reply?.sender,
|
|
96
|
+
...(message.thread?.participants ?? []).map((participant) => participant.sender),
|
|
97
|
+
];
|
|
98
|
+
return senders.some((sender) => senderMatchesIdentity(sender, identity));
|
|
99
|
+
}
|
|
100
|
+
function decideTaskOwnerActivation(message, identity, context) {
|
|
101
|
+
if (!isTaskOwnerFollowUp(message.text)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const owners = uniqueActiveWorkOwners(context.activeTaskLeases ?? []);
|
|
105
|
+
if (owners.length !== 1) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const owner = owners[0];
|
|
109
|
+
if (senderMatchesLeaseOwner(message.sender, owner)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (leaseOwnerMatchesIdentity(owner, identity)) {
|
|
113
|
+
return decision("activate", "task_owner");
|
|
114
|
+
}
|
|
115
|
+
if (identityOverlapsLeaseOwner(identity, owner)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return decision("silent", "task_owner");
|
|
119
|
+
}
|
|
120
|
+
function uniqueActiveWorkOwners(leases) {
|
|
121
|
+
const ownersByKey = new Map();
|
|
122
|
+
for (const lease of leases) {
|
|
123
|
+
if (lease.kind !== "work" || lease.status !== "active")
|
|
124
|
+
continue;
|
|
125
|
+
const key = leaseOwnerKey(lease);
|
|
126
|
+
if (!key)
|
|
127
|
+
continue;
|
|
128
|
+
ownersByKey.set(key, lease);
|
|
129
|
+
}
|
|
130
|
+
return [...ownersByKey.values()];
|
|
131
|
+
}
|
|
132
|
+
function leaseOwnerKey(lease) {
|
|
133
|
+
const sessionId = normalizedString(lease.agent_session_id);
|
|
134
|
+
if (sessionId)
|
|
135
|
+
return `session:${sessionId}`;
|
|
136
|
+
const instanceId = normalizedString(lease.agent_instance_id);
|
|
137
|
+
const agentKey = normalizeSender(lease.agent_key);
|
|
138
|
+
if (instanceId)
|
|
139
|
+
return `instance:${agentKey}:${instanceId}`;
|
|
140
|
+
if (agentKey)
|
|
141
|
+
return `agent:${agentKey}`;
|
|
142
|
+
const actorLabel = normalizeSender(lease.actor_label);
|
|
143
|
+
return actorLabel ? `label:${actorLabel}` : null;
|
|
144
|
+
}
|
|
145
|
+
function leaseOwnerMatchesIdentity(lease, identity) {
|
|
146
|
+
const leaseSessionId = normalizedString(lease.agent_session_id);
|
|
147
|
+
if (leaseSessionId) {
|
|
148
|
+
return leaseSessionId === normalizedString(identity.agent_session_id);
|
|
149
|
+
}
|
|
150
|
+
const leaseInstanceId = normalizedString(lease.agent_instance_id);
|
|
151
|
+
if (leaseInstanceId) {
|
|
152
|
+
return (leaseInstanceId === normalizedString(identity.agent_instance_id) &&
|
|
153
|
+
normalizeSender(lease.agent_key) === normalizeSender(identity.agent_key));
|
|
154
|
+
}
|
|
155
|
+
const leaseAgentKey = normalizeSender(lease.agent_key);
|
|
156
|
+
if (leaseAgentKey) {
|
|
157
|
+
return leaseAgentKey === normalizeSender(identity.agent_key);
|
|
158
|
+
}
|
|
159
|
+
return senderMatchesIdentity(lease.actor_label, identity);
|
|
160
|
+
}
|
|
161
|
+
function senderMatchesLeaseOwner(sender, lease) {
|
|
162
|
+
const normalizedSender = normalizeSender(sender);
|
|
163
|
+
if (!normalizedSender)
|
|
164
|
+
return false;
|
|
165
|
+
return leaseOwnerAliases(lease).has(normalizedSender);
|
|
166
|
+
}
|
|
167
|
+
function identityOverlapsLeaseOwner(identity, lease) {
|
|
168
|
+
const identityAliasesForOwner = aliasesForValues([
|
|
169
|
+
identity.actor_label,
|
|
170
|
+
identity.display_name,
|
|
171
|
+
identity.agent_key,
|
|
172
|
+
identity.agent_instance_id,
|
|
173
|
+
identity.agent_session_id,
|
|
174
|
+
]);
|
|
175
|
+
for (const ownerAlias of leaseOwnerAliases(lease)) {
|
|
176
|
+
if (identityAliasesForOwner.has(ownerAlias)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
function leaseOwnerAliases(lease) {
|
|
183
|
+
return aliasesForValues([
|
|
184
|
+
lease.actor_label,
|
|
185
|
+
...String(lease.actor_label || "").split("|"),
|
|
186
|
+
lease.agent_key,
|
|
187
|
+
lease.agent_instance_id,
|
|
188
|
+
lease.agent_session_id,
|
|
189
|
+
]);
|
|
190
|
+
}
|
|
191
|
+
function aliasesForValues(values) {
|
|
192
|
+
const aliases = new Set();
|
|
193
|
+
for (const value of values) {
|
|
194
|
+
const senderAlias = normalizeSender(value);
|
|
195
|
+
if (senderAlias)
|
|
196
|
+
aliases.add(senderAlias);
|
|
197
|
+
const handleAlias = normalizeHandle(value);
|
|
198
|
+
if (handleAlias)
|
|
199
|
+
aliases.add(handleAlias);
|
|
200
|
+
}
|
|
201
|
+
return aliases;
|
|
202
|
+
}
|
|
203
|
+
function senderMatchesIdentity(sender, identity) {
|
|
204
|
+
const normalizedSender = normalizeSender(sender);
|
|
205
|
+
if (!normalizedSender)
|
|
206
|
+
return false;
|
|
207
|
+
const aliases = identityAliases(identity);
|
|
208
|
+
if (aliases.has(normalizedSender))
|
|
209
|
+
return true;
|
|
210
|
+
return String(sender || "")
|
|
211
|
+
.split("|")
|
|
212
|
+
.some((part) => aliases.has(normalizeSender(part)));
|
|
213
|
+
}
|
|
214
|
+
function identityAliases(identity) {
|
|
215
|
+
const aliases = new Set();
|
|
216
|
+
const values = [
|
|
217
|
+
identity.actor_label,
|
|
218
|
+
identity.display_name,
|
|
219
|
+
identity.agent_key,
|
|
220
|
+
identity.agent_key.split("/").pop(),
|
|
221
|
+
];
|
|
222
|
+
for (const alias of aliasesForValues(values))
|
|
223
|
+
aliases.add(alias);
|
|
224
|
+
return aliases;
|
|
225
|
+
}
|
|
226
|
+
function extractMentionHandles(text) {
|
|
227
|
+
const raw = typeof text === "string" ? text : "";
|
|
228
|
+
const mentions = [];
|
|
229
|
+
for (const match of raw.matchAll(/(^|[\s([{:;,])@([A-Za-z0-9][A-Za-z0-9_.:-]*(?:\/[A-Za-z0-9][A-Za-z0-9_.-]*)*)/g)) {
|
|
230
|
+
mentions.push(match[2]);
|
|
231
|
+
}
|
|
232
|
+
return mentions;
|
|
233
|
+
}
|
|
234
|
+
function isBroadcastHandle(handle) {
|
|
235
|
+
const normalized = normalizeHandle(handle);
|
|
236
|
+
return normalized === "agents" || normalized === "everyone" || normalized === "room";
|
|
237
|
+
}
|
|
238
|
+
function isLikelyAgentMentionHandle(handle) {
|
|
239
|
+
const raw = normalizedString(handle);
|
|
240
|
+
if (!raw)
|
|
241
|
+
return false;
|
|
242
|
+
const normalized = raw.toLowerCase();
|
|
243
|
+
const firstSegment = normalized.split("/", 1)[0].replace(/_/g, "-");
|
|
244
|
+
if (normalized.startsWith("agent:"))
|
|
245
|
+
return true;
|
|
246
|
+
if (normalized.includes("/") && normalized === raw)
|
|
247
|
+
return false;
|
|
248
|
+
return !NON_AGENT_AT_HANDLES.has(firstSegment);
|
|
249
|
+
}
|
|
250
|
+
function hasBroadcastAddress(text) {
|
|
251
|
+
const raw = typeof text === "string" ? text.toLowerCase() : "";
|
|
252
|
+
return /\b(everyone|all agents|you guys|both of you|any agent|whoever owns this)\b/.test(raw);
|
|
253
|
+
}
|
|
254
|
+
function isTaskOwnerFollowUp(text) {
|
|
255
|
+
const raw = typeof text === "string" ? text.trim().toLowerCase() : "";
|
|
256
|
+
if (!raw)
|
|
257
|
+
return false;
|
|
258
|
+
return TASK_OWNER_FOLLOW_UP_PATTERNS.some((pattern) => pattern.test(raw));
|
|
259
|
+
}
|
|
260
|
+
function normalizedString(value) {
|
|
261
|
+
return typeof value === "string" ? value.trim() : "";
|
|
262
|
+
}
|
|
263
|
+
function normalizeSender(value) {
|
|
264
|
+
return normalizedString(value).toLowerCase().replace(/\s+/g, " ");
|
|
265
|
+
}
|
|
266
|
+
function normalizeHandle(value) {
|
|
267
|
+
return normalizedString(value).toLowerCase().replace(/[^a-z0-9_.:/-]+/g, "");
|
|
268
|
+
}
|
|
269
|
+
function normalizeMentionIdentityHandle(value) {
|
|
270
|
+
const normalized = normalizeHandle(value);
|
|
271
|
+
return normalized.startsWith("agent:") ? normalized.slice("agent:".length) : normalized;
|
|
272
|
+
}
|
|
@@ -35,24 +35,40 @@ export function formatOwnerAttribution(ownerLabel) {
|
|
|
35
35
|
return /s$/i.test(trimmed) ? `${trimmed}' agent` : `${trimmed}'s agent`;
|
|
36
36
|
}
|
|
37
37
|
export function inferAgentIdeLabel(value) {
|
|
38
|
-
const
|
|
38
|
+
const raw = normalizeWhitespace(String(value ?? ""));
|
|
39
|
+
const normalized = raw.toLowerCase();
|
|
39
40
|
if (!normalized) {
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
42
|
-
if (
|
|
43
|
+
if (matchesRuntimePrefix(raw, "codex")) {
|
|
43
44
|
return "Codex";
|
|
44
45
|
}
|
|
45
|
-
if (
|
|
46
|
+
if (matchesRuntimePrefix(raw, "antigravity")) {
|
|
46
47
|
return "Antigravity";
|
|
47
48
|
}
|
|
48
|
-
if (
|
|
49
|
+
if (matchesRuntimePrefix(raw, "claude")) {
|
|
49
50
|
return "Claude";
|
|
50
51
|
}
|
|
51
|
-
if (
|
|
52
|
+
if (matchesRuntimePrefix(raw, "orchestrator")) {
|
|
52
53
|
return "Orchestrator";
|
|
53
54
|
}
|
|
54
55
|
return null;
|
|
55
56
|
}
|
|
57
|
+
function matchesRuntimePrefix(value, prefix) {
|
|
58
|
+
const normalized = value.toLowerCase();
|
|
59
|
+
if (normalized === prefix || normalized.startsWith(`${prefix}-`)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
const compact = value.replace(/[^a-zA-Z0-9]+/g, "");
|
|
63
|
+
if (compact.toLowerCase() === prefix) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (!compact.toLowerCase().startsWith(prefix)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const next = compact[prefix.length];
|
|
70
|
+
return Boolean(next && /[A-Z0-9]/.test(next));
|
|
71
|
+
}
|
|
56
72
|
export function buildAgentActorLabel(input) {
|
|
57
73
|
const displayName = normalizeWhitespace(input.display_name) || "Agent";
|
|
58
74
|
const ownerAttribution = formatOwnerAttribution(input.owner_label);
|
|
@@ -15,6 +15,7 @@ export const ROOM_AGENT_RECONNECT_GRACE_MS = 10_000;
|
|
|
15
15
|
export const ROOM_AGENT_DELIVERY_TRANSPORTS = [
|
|
16
16
|
"long_poll",
|
|
17
17
|
"sse",
|
|
18
|
+
"desktop_events",
|
|
18
19
|
];
|
|
19
20
|
export const ROOM_AGENT_SESSION_KINDS = [
|
|
20
21
|
"controller",
|
|
@@ -42,8 +43,9 @@ export function getAgentPresenceFreshnessFromReachability(isReachable) {
|
|
|
42
43
|
return isReachable ? "active" : "stale";
|
|
43
44
|
}
|
|
44
45
|
export function isAgentDeliverySessionReachable(input, now = Date.now()) {
|
|
45
|
-
if (input.activeConnectionCount
|
|
46
|
-
return
|
|
46
|
+
if (input.activeConnectionCount > 0 && getAgentPresenceFreshness(input.updatedAt ?? "", now) === "active") {
|
|
47
|
+
return true;
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
const graceExpiresAt = new Date(input.reconnectGraceExpiresAt ?? "").getTime();
|
|
50
|
+
return Number.isFinite(graceExpiresAt) && graceExpiresAt >= now;
|
|
49
51
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const AGENT_SESSION_BEARER_CAPABILITIES = [
|
|
2
|
+
"messages.read",
|
|
3
|
+
"messages.write",
|
|
4
|
+
"coordination.read",
|
|
5
|
+
"coordination.self_write",
|
|
6
|
+
"coordination.propose",
|
|
7
|
+
"artifacts.read",
|
|
8
|
+
"artifacts.self_write",
|
|
9
|
+
];
|
|
10
|
+
export const DEFAULT_AGENT_SESSION_BEARER_CAPABILITIES = [
|
|
11
|
+
...AGENT_SESSION_BEARER_CAPABILITIES,
|
|
12
|
+
];
|
|
13
|
+
export function isAgentSessionBearerCapability(value) {
|
|
14
|
+
return AGENT_SESSION_BEARER_CAPABILITIES.includes(value);
|
|
15
|
+
}
|
|
16
|
+
// The new direct worker principal is deliberately opt-in until F2 uses it.
|
|
17
|
+
export function isAgentSessionBearerFeatureEnabled() {
|
|
18
|
+
return /^(1|true|yes)$/i.test(process.env.LETAGENTS_AGENT_SESSION_BEARER_ENABLED ?? "");
|
|
19
|
+
}
|
|
20
|
+
export function isSupervisorHostGrantFeatureEnabled() {
|
|
21
|
+
return /^(1|true|yes)$/i.test(process.env.LETAGENTS_SUPERVISOR_HOST_GRANT_ENABLED ?? "");
|
|
22
|
+
}
|
|
23
|
+
export function getAgentSessionBearerTtlMs() {
|
|
24
|
+
const configured = Number.parseInt(process.env.LETAGENTS_AGENT_SESSION_BEARER_TTL_MS ?? "", 10);
|
|
25
|
+
if (!Number.isFinite(configured))
|
|
26
|
+
return 30 * 60 * 1000;
|
|
27
|
+
return Math.min(Math.max(configured, 60_000), 24 * 60 * 60 * 1000);
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const BOARD_MANAGER_FAILOVER_MODES = ["off", "announce", "auto"];
|
|
2
|
+
/**
|
|
3
|
+
* What the liveness sweep does when the active Board Manager's worker session
|
|
4
|
+
* goes unreachable: 'off' ignores it, 'announce' posts a room warning with a
|
|
5
|
+
* suggested successor, 'auto' (default) promotes the best reachable worker.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_BOARD_MANAGER_FAILOVER = "auto";
|
|
8
|
+
export function isBoardManagerFailoverMode(value) {
|
|
9
|
+
return BOARD_MANAGER_FAILOVER_MODES.includes(value);
|
|
10
|
+
}
|
|
11
|
+
export function normalizeBoardManagerFailoverMode(value) {
|
|
12
|
+
const normalized = value?.trim().toLowerCase();
|
|
13
|
+
return normalized && isBoardManagerFailoverMode(normalized)
|
|
14
|
+
? normalized
|
|
15
|
+
: DEFAULT_BOARD_MANAGER_FAILOVER;
|
|
16
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LetAgents Rental Token (LRT) primitives.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions. No I/O, no IDE-local reads, no DB. Importable from
|
|
5
|
+
* both the desktop main process and the API server.
|
|
6
|
+
*
|
|
7
|
+
* Spec §17.3 (LRT definition + suggested weights) and §17.13
|
|
8
|
+
* (confidence-based safety buffers).
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Default LRT weights. Match the suggested starting values in §17.3:
|
|
12
|
+
* input=1, output=4, cache_creation=1.25, cache_read=0.1, reasoning=4,
|
|
13
|
+
* tool=0. Other weights extend the formula for providers that bill on
|
|
14
|
+
* those axes.
|
|
15
|
+
*
|
|
16
|
+
* These are the public defaults; individual providers (per
|
|
17
|
+
* {@link PROVIDER_LRT_WEIGHTS}) may override any field.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_LRT_WEIGHTS = Object.freeze({
|
|
20
|
+
input: 1.0,
|
|
21
|
+
output: 4.0,
|
|
22
|
+
cacheCreation: 1.25,
|
|
23
|
+
cacheRead: 0.1,
|
|
24
|
+
reasoning: 4.0,
|
|
25
|
+
tool: 0.0,
|
|
26
|
+
command: 0.0,
|
|
27
|
+
request: 0.0,
|
|
28
|
+
usdToLrt: 0.0,
|
|
29
|
+
creditsToLrt: 0.0,
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Per-provider weight overrides applied on top of {@link DEFAULT_LRT_WEIGHTS}.
|
|
33
|
+
*
|
|
34
|
+
* V1 intentionally ships **no** baked-in non-zero overrides. Hard-coded
|
|
35
|
+
* placeholder weights (e.g. an arbitrary LRT-per-request for Cursor)
|
|
36
|
+
* would cause Budget Sentinel to systematically over- or under-count
|
|
37
|
+
* before any real calibration history exists. Instead, request-based
|
|
38
|
+
* and credit-based providers calibrate at the **session/adapter level**
|
|
39
|
+
* via the `calibratedWeights` argument to {@link computeLrt}, which the
|
|
40
|
+
* meter adapter populates from observed LRT-per-{request,credit,USD}
|
|
41
|
+
* once {@link CalibrationHistory} has enough samples.
|
|
42
|
+
*
|
|
43
|
+
* Future per-provider defaults can be added here once calibrated values
|
|
44
|
+
* are well-known across the fleet (e.g. \"LRT per Cursor premium request\"
|
|
45
|
+
* has converged after N sessions).
|
|
46
|
+
*/
|
|
47
|
+
export const PROVIDER_LRT_WEIGHTS = Object.freeze({
|
|
48
|
+
claude_code: {
|
|
49
|
+
// Claude Code emits exact token logs locally; defaults work.
|
|
50
|
+
},
|
|
51
|
+
codex: {
|
|
52
|
+
// Codex exposes token logs + credits; rely on tokens when present.
|
|
53
|
+
},
|
|
54
|
+
antigravity: {
|
|
55
|
+
// Antigravity exposes percent-window + reset only; token weights still
|
|
56
|
+
// apply when tokens can be observed, otherwise the calibrated estimator
|
|
57
|
+
// does the heavy lifting (see estimateLrtFromPercentWindow below).
|
|
58
|
+
},
|
|
59
|
+
cursor: {
|
|
60
|
+
// Request weight intentionally left at the 0 default — adapters
|
|
61
|
+
// supply a calibrated per-request weight via `computeLrt`'s
|
|
62
|
+
// `calibratedWeights` argument once history is available.
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the effective weight table for one provider, optionally
|
|
67
|
+
* applying a session-level calibrated override on top.
|
|
68
|
+
*
|
|
69
|
+
* Lookup order, highest precedence first:
|
|
70
|
+
*
|
|
71
|
+
* 1. `calibratedWeights` argument (per-session adapter override)
|
|
72
|
+
* 2. PROVIDER_LRT_WEIGHTS[provider] (well-known per-provider override)
|
|
73
|
+
* 3. DEFAULT_LRT_WEIGHTS (§17.3 starting weights)
|
|
74
|
+
*/
|
|
75
|
+
export function resolveWeights(provider, calibratedWeights) {
|
|
76
|
+
const providerOverride = PROVIDER_LRT_WEIGHTS[provider] ?? {};
|
|
77
|
+
return { ...DEFAULT_LRT_WEIGHTS, ...providerOverride, ...(calibratedWeights ?? {}) };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Compute LRT consumed by a {@link UsageDelta}.
|
|
81
|
+
*
|
|
82
|
+
* Spec §17.3. The formula is:
|
|
83
|
+
*
|
|
84
|
+
* lrt =
|
|
85
|
+
* input_tokens × input_weight
|
|
86
|
+
* + output_tokens × output_weight
|
|
87
|
+
* + cache_creation_tokens × cache_creation_weight
|
|
88
|
+
* + cache_read_tokens × cache_read_weight
|
|
89
|
+
* + reasoning_tokens × reasoning_weight
|
|
90
|
+
* + tool_events × tool_weight
|
|
91
|
+
* + command_runs × command_weight
|
|
92
|
+
* + requests × request_weight
|
|
93
|
+
* + credits × credits_to_lrt
|
|
94
|
+
* + usd × usd_to_lrt
|
|
95
|
+
*
|
|
96
|
+
* `calibratedWeights` lets the meter adapter pass a per-session
|
|
97
|
+
* calibrated override (e.g. an observed LRT-per-request for Cursor
|
|
98
|
+
* derived from {@link CalibrationHistory}). Without it, only the
|
|
99
|
+
* default + provider weights apply, which means request/credit/USD
|
|
100
|
+
* components contribute 0 LRT until an adapter calibrates them.
|
|
101
|
+
*
|
|
102
|
+
* Negative deltas are clamped to zero — a meter that goes "backwards"
|
|
103
|
+
* is treated as a reset event, not a refund.
|
|
104
|
+
*/
|
|
105
|
+
export function computeLrt(delta, provider, calibratedWeights) {
|
|
106
|
+
const w = resolveWeights(provider, calibratedWeights);
|
|
107
|
+
const tokens = Math.max(0, delta.inputTokens) * w.input
|
|
108
|
+
+ Math.max(0, delta.outputTokens) * w.output
|
|
109
|
+
+ Math.max(0, delta.cacheCreationTokens) * w.cacheCreation
|
|
110
|
+
+ Math.max(0, delta.cacheReadTokens) * w.cacheRead
|
|
111
|
+
+ Math.max(0, delta.reasoningTokens) * w.reasoning;
|
|
112
|
+
const events = Math.max(0, delta.toolCalls) * w.tool
|
|
113
|
+
+ Math.max(0, delta.commandRuns) * w.command
|
|
114
|
+
+ Math.max(0, delta.requests) * w.request;
|
|
115
|
+
const money = Math.max(0, delta.credits) * w.creditsToLrt
|
|
116
|
+
+ Math.max(0, delta.usd) * w.usdToLrt;
|
|
117
|
+
return tokens + events + money;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Estimate LRT remaining when the only signal is a percent-window
|
|
121
|
+
* reading plus observed LRT consumed in the current window.
|
|
122
|
+
*
|
|
123
|
+
* used_so_far = LRT observed since window start
|
|
124
|
+
* percent_used = 1 - percent_remaining
|
|
125
|
+
* window_total = used_so_far / percent_used
|
|
126
|
+
* remaining = window_total - used_so_far
|
|
127
|
+
*
|
|
128
|
+
* Spec §17.4 percent-window calibration example.
|
|
129
|
+
*
|
|
130
|
+
* Returns `null` when the estimate cannot be computed (e.g. we have
|
|
131
|
+
* not observed any LRT yet, so the denominator is undefined).
|
|
132
|
+
*/
|
|
133
|
+
export function estimateLrtFromPercentWindow(percentRemaining, lrtObservedThisWindow) {
|
|
134
|
+
if (!Number.isFinite(percentRemaining) || percentRemaining < 0 || percentRemaining > 1) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (!Number.isFinite(lrtObservedThisWindow) || lrtObservedThisWindow <= 0) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const percentUsed = 1 - percentRemaining;
|
|
141
|
+
if (percentUsed <= 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const windowTotal = lrtObservedThisWindow / percentUsed;
|
|
145
|
+
const remaining = windowTotal * percentRemaining;
|
|
146
|
+
return Math.max(0, remaining);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Confidence-based stop thresholds.
|
|
150
|
+
*
|
|
151
|
+
* Spec §17.13: lower confidence stops earlier to avoid overrun when
|
|
152
|
+
* the meter lags.
|
|
153
|
+
*
|
|
154
|
+
* official_exact / local_exact → stop at 98 %
|
|
155
|
+
* derived → stop at 95 %
|
|
156
|
+
* calibrated → stop at 92 %
|
|
157
|
+
* estimated → stop at 87 %
|
|
158
|
+
* weak_estimate → stop at 80 %
|
|
159
|
+
* unknown → stop at 75 % (or earlier per policy)
|
|
160
|
+
*
|
|
161
|
+
* Returned as the *fraction of budget* at which we close the rails.
|
|
162
|
+
* The Budget Sentinel multiplies an LRT limit by this to compute the
|
|
163
|
+
* effective ceiling before exhaustion.
|
|
164
|
+
*/
|
|
165
|
+
export const STOP_THRESHOLD_BY_CONFIDENCE = Object.freeze({
|
|
166
|
+
official_exact: 0.98,
|
|
167
|
+
local_exact: 0.98,
|
|
168
|
+
derived: 0.95,
|
|
169
|
+
calibrated: 0.92,
|
|
170
|
+
estimated: 0.87,
|
|
171
|
+
weak_estimate: 0.80,
|
|
172
|
+
unknown: 0.75,
|
|
173
|
+
});
|
|
174
|
+
/**
|
|
175
|
+
* Return the effective LRT ceiling a session is allowed to spend given
|
|
176
|
+
* its budget and the meter's confidence.
|
|
177
|
+
*
|
|
178
|
+
* The result is `Math.floor(lrtLimit × stopThreshold)`.
|
|
179
|
+
*/
|
|
180
|
+
export function effectiveLrtCeiling(lrtLimit, confidence) {
|
|
181
|
+
if (!Number.isFinite(lrtLimit) || lrtLimit <= 0) {
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
const fraction = STOP_THRESHOLD_BY_CONFIDENCE[confidence] ?? STOP_THRESHOLD_BY_CONFIDENCE.unknown;
|
|
185
|
+
return Math.floor(lrtLimit * fraction);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Order on confidence levels — exact > local_exact > derived > calibrated
|
|
189
|
+
* > estimated > weak_estimate > unknown. Used to decide whether a new
|
|
190
|
+
* snapshot upgrades or downgrades meter confidence on a session.
|
|
191
|
+
*/
|
|
192
|
+
const CONFIDENCE_ORDER = [
|
|
193
|
+
"unknown",
|
|
194
|
+
"weak_estimate",
|
|
195
|
+
"estimated",
|
|
196
|
+
"calibrated",
|
|
197
|
+
"derived",
|
|
198
|
+
"local_exact",
|
|
199
|
+
"official_exact",
|
|
200
|
+
];
|
|
201
|
+
/**
|
|
202
|
+
* Compare two confidence values. Returns a negative number when `a` is
|
|
203
|
+
* weaker than `b`, positive when stronger, 0 when equal.
|
|
204
|
+
*/
|
|
205
|
+
export function compareConfidence(a, b) {
|
|
206
|
+
return CONFIDENCE_ORDER.indexOf(a) - CONFIDENCE_ORDER.indexOf(b);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Whether the meter signal qualifies as "exact" for marketplace
|
|
210
|
+
* eligibility (§7.4 preflight). Either local-exact or vendor-exact.
|
|
211
|
+
*/
|
|
212
|
+
export function isExactConfidence(confidence) {
|
|
213
|
+
return confidence === "official_exact" || confidence === "local_exact";
|
|
214
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared rental meter contract.
|
|
3
|
+
*
|
|
4
|
+
* These types describe how a per-IDE meter adapter reports native quota
|
|
5
|
+
* state and rental usage from the desktop/MCP-local process to the API
|
|
6
|
+
* server. They live in `src/shared/` because both sides import them.
|
|
7
|
+
*
|
|
8
|
+
* Per spec §17.7 (Meter Adapter Contract) and §17.8 (Rental Scope
|
|
9
|
+
* Attribution). Adapters run **desktop-side** (Electron main process
|
|
10
|
+
* or MCP-local) because they read local IDE files such as JSONL session
|
|
11
|
+
* logs and SQLite conversation rows; the API server only ingests the
|
|
12
|
+
* normalized snapshots they report.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|