letagents 0.12.12 → 0.12.13
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/git-remote.js +7 -7
- package/dist/mcp/local-state/agent-sessions.js +63 -5
- package/dist/mcp/local-state/local-chat.js +189 -48
- package/dist/mcp/local-state/storage.js +16 -9
- package/dist/mcp/server/daemon-tool-executor.js +92 -0
- package/dist/mcp/server/register-tools.js +4 -2
- package/dist/mcp/server/runtime/agent-sessions.js +14 -5
- package/dist/mcp/server/runtime/api.js +11 -3
- package/dist/mcp/server/runtime/daemon-tool-context.js +11 -0
- package/dist/mcp/server/runtime/messages.js +55 -0
- package/dist/mcp/server/runtime/room-state.js +13 -3
- package/dist/mcp/server/runtime/rooms.js +51 -30
- package/dist/mcp/server/runtime/supervisor-bridge.js +49 -8
- package/dist/mcp/server/runtime/worker-bearer.js +5 -1
- package/dist/mcp/server/runtime.js +3 -3
- package/dist/mcp/server/supervised-tool-facade.js +34 -5
- package/dist/mcp/server/tools/messages/read-tool.js +54 -97
- package/dist/mcp/server/tools/messages/reasoning-tool.js +2 -0
- package/dist/mcp/server/tools/messages/send-tool.js +2 -0
- package/dist/mcp/server/tools/messages/status-tool.js +2 -0
- package/dist/mcp/server/tools/messages/wait-tool.js +290 -68
- package/dist/mcp/server/tools/onboarding/status-tool.js +4 -4
- package/dist/mcp/server/tools/rooms/inspection-tools.js +15 -10
- package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +2 -1
- package/dist/mcp/server/tools/tasks/board-tools.js +34 -2
- package/dist/mcp/sse-client.js +163 -20
- package/dist/shared/activation-routing.js +146 -23
- package/dist/shared/agent-presence.js +6 -0
- package/dist/shared/desktop-release-manifest.js +63 -0
- package/dist/shared/desktop-release.js +60 -0
- package/dist/shared/scoped-ids.js +6 -0
- package/package.json +6 -2
- package/shared/message-contracts.d.mts +32 -0
- package/shared/message-contracts.mjs +109 -0
- package/shared/routing-aliases.d.mts +18 -0
- package/shared/routing-aliases.mjs +66 -0
- package/shared/sqlite-thread-routing.d.mts +72 -0
- package/shared/sqlite-thread-routing.mjs +1038 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getStoredAgentSession, isLocalRoomStorageEnabled, saveAgentSession, } from "../../local-state.js";
|
|
1
|
+
import { getStoredAgentSession, isLocalRoomStorageEnabled, replaceLocalWorkerAgentSession, saveAgentSession, } from "../../local-state.js";
|
|
2
2
|
import { getGitCurrentBranch } from "../../git-remote.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { normalizeAgentBaseName } from "../../../shared/codenames.js";
|
|
@@ -7,6 +7,7 @@ import { LETAGENTS_AGENT_SESSION_ID_HEADER, LETAGENTS_AGENT_SESSION_TOKEN_HEADER
|
|
|
7
7
|
import { AGENT_INSTANCE_UUID, detectAgentIdeLabel, detectAgentRuntimeLabel, ensureAgentIdentity, } from "./identity.js";
|
|
8
8
|
import { isSupervisedBoundedTurn, requireValidWorkerBearerRuntime } from "./worker-bearer.js";
|
|
9
9
|
import { resolveCurrentSupervisedWorkerSession } from "./supervisor-bridge.js";
|
|
10
|
+
import { getDaemonToolExecutionContext, getRuntimeWorkingDirectory } from "./daemon-tool-context.js";
|
|
10
11
|
// A worker bearer already represents a server-side worker session. This local
|
|
11
12
|
// marker lets the MCP tool contract stay session-shaped without persisting or
|
|
12
13
|
// transmitting a second set of credentials.
|
|
@@ -119,7 +120,12 @@ export function requireWorkerAgentSession(roomId, sessionId) {
|
|
|
119
120
|
export async function resolveWorkerToolIdentity(input) {
|
|
120
121
|
const runtimeMode = requireValidWorkerBearerRuntime().mode;
|
|
121
122
|
if (runtimeMode === "supervised") {
|
|
122
|
-
const
|
|
123
|
+
const daemonContext = getDaemonToolExecutionContext();
|
|
124
|
+
const agentSession = daemonContext?.agentSession
|
|
125
|
+
?? await resolveCurrentSupervisedWorkerSession(input.roomId);
|
|
126
|
+
if (input.roomId && input.roomId !== agentSession.room_id) {
|
|
127
|
+
throw new Error(`Daemon-supervised worker session is registered for ${agentSession.room_id}, not ${input.roomId}.`);
|
|
128
|
+
}
|
|
123
129
|
if (input.agentSessionId
|
|
124
130
|
&& input.agentSessionId !== WORKER_BEARER_AGENT_SESSION_ID
|
|
125
131
|
&& input.agentSessionId !== agentSession.session_id) {
|
|
@@ -173,7 +179,7 @@ export async function ensureLocalWorkerAgentSession(roomId, input = {}) {
|
|
|
173
179
|
const now = new Date().toISOString();
|
|
174
180
|
const runtime = input.runtime?.trim() || detectAgentRuntimeLabel();
|
|
175
181
|
const displayName = input.displayName?.trim() || identity.display_name;
|
|
176
|
-
|
|
182
|
+
const session = {
|
|
177
183
|
session_id: `local_${randomUUID()}`,
|
|
178
184
|
session_token: `local_${randomUUID()}`,
|
|
179
185
|
room_id: roomId,
|
|
@@ -195,10 +201,13 @@ export async function ensureLocalWorkerAgentSession(roomId, input = {}) {
|
|
|
195
201
|
updated_at: now,
|
|
196
202
|
last_seen_at: now,
|
|
197
203
|
ended_at: null,
|
|
198
|
-
}
|
|
204
|
+
};
|
|
205
|
+
return session.session_kind === "worker"
|
|
206
|
+
? replaceLocalWorkerAgentSession(session)
|
|
207
|
+
: saveAgentSession(session);
|
|
199
208
|
}
|
|
200
209
|
export function getAgentSessionRepoBranch(cwd) {
|
|
201
|
-
const workingDir = cwd?.trim() ||
|
|
210
|
+
const workingDir = cwd?.trim() || getRuntimeWorkingDirectory();
|
|
202
211
|
return getGitCurrentBranch(workingDir);
|
|
203
212
|
}
|
|
204
213
|
export function agentSessionCredentials(agentSession) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { clearAuthenticatedAccountCache } from "./auth-cache.js";
|
|
2
|
+
import { getDaemonToolExecutionContext } from "./daemon-tool-context.js";
|
|
2
3
|
import { requireValidWorkerBearerRuntime } from "./worker-bearer.js";
|
|
3
4
|
import { borrowCurrentSupervisedWorkerCredential, } from "./supervisor-bridge.js";
|
|
4
5
|
let ownerAuthStoreLoader = () => import("../../local-state.js");
|
|
@@ -20,12 +21,18 @@ export class SupervisedWorkerCredentialError extends Error {
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
async function getSupervisedCredential() {
|
|
24
|
+
const daemonContext = getDaemonToolExecutionContext();
|
|
25
|
+
if (daemonContext)
|
|
26
|
+
return daemonContext.bearer;
|
|
23
27
|
const result = await supervisedCredentialBorrower();
|
|
24
28
|
if (result.state === "available")
|
|
25
29
|
return result.credential;
|
|
26
30
|
throw new SupervisedWorkerCredentialError(result.state === "deferred" ? "SUPERVISED_CREDENTIAL_UNAVAILABLE" : "SUPERVISED_CREDENTIAL_STALE");
|
|
27
31
|
}
|
|
28
32
|
export const API_URL = (process.env.LETAGENTS_API_URL || "http://localhost:3001").replace(/\/+$/, "");
|
|
33
|
+
export function getApiUrl() {
|
|
34
|
+
return getDaemonToolExecutionContext()?.apiUrl.replace(/\/+$/, "") ?? API_URL;
|
|
35
|
+
}
|
|
29
36
|
export class ApiError extends Error {
|
|
30
37
|
status;
|
|
31
38
|
body;
|
|
@@ -76,8 +83,9 @@ export function resolveApiPath(urlOrPath) {
|
|
|
76
83
|
return "/auth/device/start";
|
|
77
84
|
}
|
|
78
85
|
try {
|
|
79
|
-
const
|
|
80
|
-
const
|
|
86
|
+
const apiUrl = getApiUrl();
|
|
87
|
+
const parsed = new URL(urlOrPath, `${apiUrl}/`);
|
|
88
|
+
const apiBase = new URL(`${apiUrl}/`);
|
|
81
89
|
if (parsed.origin !== apiBase.origin) {
|
|
82
90
|
return "/auth/device/start";
|
|
83
91
|
}
|
|
@@ -109,7 +117,7 @@ export async function apiCall(path, options) {
|
|
|
109
117
|
headers.set("Authorization", authorizationHeader);
|
|
110
118
|
}
|
|
111
119
|
}
|
|
112
|
-
const res = await fetch(`${
|
|
120
|
+
const res = await fetch(`${getApiUrl()}${path}`, {
|
|
113
121
|
...options,
|
|
114
122
|
headers,
|
|
115
123
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const daemonToolContext = new AsyncLocalStorage();
|
|
3
|
+
export function getDaemonToolExecutionContext() {
|
|
4
|
+
return daemonToolContext.getStore() ?? null;
|
|
5
|
+
}
|
|
6
|
+
export function runWithDaemonToolExecutionContext(context, callback) {
|
|
7
|
+
return daemonToolContext.run(context, callback);
|
|
8
|
+
}
|
|
9
|
+
export function getRuntimeWorkingDirectory() {
|
|
10
|
+
return getDaemonToolExecutionContext()?.cwd ?? process.cwd();
|
|
11
|
+
}
|
|
@@ -151,6 +151,61 @@ export function toAgentReadableMessages(messages, contextMessages = []) {
|
|
|
151
151
|
const summaries = buildThreadSummaries(records, recordsById);
|
|
152
152
|
return (messages ?? []).map((message) => toAgentReadableMessage(message, recordsById, summaries));
|
|
153
153
|
}
|
|
154
|
+
export const AGENT_MESSAGE_OUTPUT_MAX_BYTES = 4 * 1024 * 1024;
|
|
155
|
+
export const AGENT_MESSAGE_BODY_MAX_BYTES = AGENT_MESSAGE_OUTPUT_MAX_BYTES / 2;
|
|
156
|
+
function jsonUtf8Bytes(value) {
|
|
157
|
+
const serialized = JSON.stringify(value);
|
|
158
|
+
return Buffer.byteLength(serialized === undefined ? "null" : serialized, "utf8");
|
|
159
|
+
}
|
|
160
|
+
function compactOversizedAgentMessage(message) {
|
|
161
|
+
if (!isRecord(message)) {
|
|
162
|
+
return { content_truncated: true, value_type: typeof message };
|
|
163
|
+
}
|
|
164
|
+
const text = typeof message.text === "string" ? message.text : "";
|
|
165
|
+
return {
|
|
166
|
+
...(typeof message.id === "string" ? { id: message.id } : {}),
|
|
167
|
+
...(typeof message.sender === "string" ? { sender: message.sender } : {}),
|
|
168
|
+
...(typeof message.source === "string" ? { source: message.source } : {}),
|
|
169
|
+
...(typeof message.timestamp === "string" ? { timestamp: message.timestamp } : {}),
|
|
170
|
+
...(typeof message.thread_root_id === "string"
|
|
171
|
+
? { thread_root_id: message.thread_root_id }
|
|
172
|
+
: {}),
|
|
173
|
+
...(typeof message.thread_reply_to_id === "string"
|
|
174
|
+
? { thread_reply_to_id: message.thread_reply_to_id }
|
|
175
|
+
: {}),
|
|
176
|
+
text: text.slice(0, 16 * 1024),
|
|
177
|
+
content_truncated: true,
|
|
178
|
+
original_utf8_bytes: jsonUtf8Bytes(message),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export function boundAgentMessageOutput(messages, options = {}) {
|
|
182
|
+
const direction = options.direction ?? "prefix";
|
|
183
|
+
const maxBytes = Math.max(1024, Math.floor(options.maxBytes ?? AGENT_MESSAGE_OUTPUT_MAX_BYTES));
|
|
184
|
+
const ordered = direction === "suffix" ? [...messages].reverse() : [...messages];
|
|
185
|
+
const selected = [];
|
|
186
|
+
let outputBytes = 2; // JSON array brackets.
|
|
187
|
+
for (const message of ordered) {
|
|
188
|
+
let candidate = message;
|
|
189
|
+
let candidateBytes = jsonUtf8Bytes(candidate);
|
|
190
|
+
if (candidateBytes + 2 > maxBytes) {
|
|
191
|
+
candidate = compactOversizedAgentMessage(candidate);
|
|
192
|
+
candidateBytes = jsonUtf8Bytes(candidate);
|
|
193
|
+
}
|
|
194
|
+
const separatorBytes = selected.length > 0 ? 1 : 0;
|
|
195
|
+
if (outputBytes + separatorBytes + candidateBytes > maxBytes)
|
|
196
|
+
break;
|
|
197
|
+
selected.push(candidate);
|
|
198
|
+
outputBytes += separatorBytes + candidateBytes;
|
|
199
|
+
}
|
|
200
|
+
if (direction === "suffix")
|
|
201
|
+
selected.reverse();
|
|
202
|
+
return {
|
|
203
|
+
messages: selected,
|
|
204
|
+
truncated: selected.length < messages.length,
|
|
205
|
+
omittedMessageCount: messages.length - selected.length,
|
|
206
|
+
outputBytes,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
154
209
|
export function appendIncludePromptOnly(path) {
|
|
155
210
|
return `${path}${path.includes("?") ? "&" : "?"}include_prompt_only=1`;
|
|
156
211
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SseClient } from "../../sse-client.js";
|
|
2
2
|
import { getStoredAgentIdentity, saveRoomSession, touchRoomSession, } from "../../local-state.js";
|
|
3
3
|
import { getCanonicalRoomWebPath, } from "../../room-id.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getApiUrl, getLetagentsToken } from "./api.js";
|
|
5
5
|
import { AGENT_INSTANCE_UUID, currentAgentIdentity, currentAgentIdentityKey, } from "./identity.js";
|
|
6
6
|
import { isSupervisedBoundedTurn } from "./worker-bearer.js";
|
|
7
7
|
import { getCurrentSupervisedRoomAuthority, runWithSupervisedRoomAuthority, } from "./supervised-room-authority.js";
|
|
@@ -15,7 +15,7 @@ export function shutdownRuntime() {
|
|
|
15
15
|
sseClient?.unsubscribeAll();
|
|
16
16
|
}
|
|
17
17
|
function getSseClient() {
|
|
18
|
-
sseClient ??= new SseClient(
|
|
18
|
+
sseClient ??= new SseClient(getApiUrl(), () => getLetagentsToken());
|
|
19
19
|
return sseClient;
|
|
20
20
|
}
|
|
21
21
|
function getCurrentStreamAgentIdentity() {
|
|
@@ -32,6 +32,7 @@ function getCurrentStreamAgentIdentity() {
|
|
|
32
32
|
export function toRoomState(input) {
|
|
33
33
|
return {
|
|
34
34
|
room_id: input.room_id,
|
|
35
|
+
navigation_locator: input.navigation_locator ?? null,
|
|
35
36
|
project_id: input.project_id ?? null,
|
|
36
37
|
code: input.code ?? null,
|
|
37
38
|
display_name: input.display_name ?? null,
|
|
@@ -40,8 +41,12 @@ export function toRoomState(input) {
|
|
|
40
41
|
is_local: input.is_local ?? false,
|
|
41
42
|
};
|
|
42
43
|
}
|
|
44
|
+
export function currentRoomMatchesLocator(locator) {
|
|
45
|
+
const value = locator?.trim();
|
|
46
|
+
return Boolean(value && currentRoom && (currentRoom.room_id === value || currentRoom.navigation_locator === value));
|
|
47
|
+
}
|
|
43
48
|
function getCanonicalRoomWebUrl(roomId) {
|
|
44
|
-
return new URL(getCanonicalRoomWebPath(roomId), `${
|
|
49
|
+
return new URL(getCanonicalRoomWebPath(roomId), `${getApiUrl()}/`).toString();
|
|
45
50
|
}
|
|
46
51
|
export function withCanonicalRoomLink(roomId, payload) {
|
|
47
52
|
return {
|
|
@@ -109,6 +114,11 @@ export function rememberRoom(state, lastMessageId) {
|
|
|
109
114
|
}, (_message) => {
|
|
110
115
|
touchRoomSession(state.room_id);
|
|
111
116
|
mcpServer?.server.sendResourceListChanged();
|
|
117
|
+
}, () => {
|
|
118
|
+
// The SSE cursor crossed a broker/bridge loss boundary. MCP resources
|
|
119
|
+
// are pull-based, so invalidating the list is the full-state repair.
|
|
120
|
+
touchRoomSession(state.room_id);
|
|
121
|
+
mcpServer?.server.sendResourceListChanged();
|
|
112
122
|
});
|
|
113
123
|
return state;
|
|
114
124
|
}
|
|
@@ -20,7 +20,7 @@ function isNotFoundApiError(error) {
|
|
|
20
20
|
return error instanceof ApiError && error.status === 404;
|
|
21
21
|
}
|
|
22
22
|
function parseGeneratedGitRefRoomIdentifier(identifier) {
|
|
23
|
-
const match = /^
|
|
23
|
+
const match = /^github\.com\/([^/\s]+\/[^/\s]+)\/focus\/git:(branch|tag):[A-Za-z0-9_-]+$/.exec(identifier.trim());
|
|
24
24
|
if (!match) {
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
@@ -67,6 +67,7 @@ export async function joinRoomIdentifier(identifier, joinedVia, options = {}) {
|
|
|
67
67
|
const agentIdentity = await ensureAgentIdentity();
|
|
68
68
|
const room = rememberRoom(toRoomState({
|
|
69
69
|
room_id: joinedRoomId,
|
|
70
|
+
navigation_locator: joinedRoomId === roomId ? null : roomId,
|
|
70
71
|
project_id: typeof response.project_id === "string" ? response.project_id : null,
|
|
71
72
|
code: typeof response.code === "string"
|
|
72
73
|
? response.code
|
|
@@ -260,7 +261,27 @@ export async function joinNamedRoom(name, sessionMode) {
|
|
|
260
261
|
session_mode: sessionMode,
|
|
261
262
|
});
|
|
262
263
|
}
|
|
263
|
-
function
|
|
264
|
+
async function bindWorkerRoomLocator(roomLocator, source) {
|
|
265
|
+
const response = await apiCall(`/rooms/resolve/${encodeURIComponent(roomLocator)}`);
|
|
266
|
+
if (response.room_exists !== true) {
|
|
267
|
+
throw new ApiError(404, JSON.stringify({ error: "Room not found", code: "ROOM_NOT_FOUND" }));
|
|
268
|
+
}
|
|
269
|
+
const roomId = typeof response.canonical_room_id === "string"
|
|
270
|
+
? response.canonical_room_id
|
|
271
|
+
: roomLocator;
|
|
272
|
+
return {
|
|
273
|
+
room: rememberRoom(toRoomState({
|
|
274
|
+
room_id: roomId,
|
|
275
|
+
navigation_locator: roomId === roomLocator ? null : roomLocator,
|
|
276
|
+
project_id: roomId,
|
|
277
|
+
display_name: roomId,
|
|
278
|
+
git_room: response.git_room ?? null,
|
|
279
|
+
joined_via: source === ".letagents.json" ? "config" : "git-remote",
|
|
280
|
+
})),
|
|
281
|
+
source,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function bindWorkerRoomFromContext() {
|
|
264
285
|
const configRoom = getRoomFromConfig();
|
|
265
286
|
if (configRoom) {
|
|
266
287
|
const gitContext = buildActiveGitRoomContext({
|
|
@@ -268,26 +289,26 @@ function bindWorkerRoomFromContext() {
|
|
|
268
289
|
currentBranch: getGitCurrentBranch(),
|
|
269
290
|
defaultBranch: getGitDefaultBranch(),
|
|
270
291
|
});
|
|
271
|
-
const roomId = gitContext.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
292
|
+
const roomId = gitContext.activeRoomLocator ?? configRoom;
|
|
293
|
+
try {
|
|
294
|
+
return await bindWorkerRoomLocator(roomId, ".letagents.json");
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
if (roomId === configRoom || !isNotFoundApiError(error))
|
|
298
|
+
throw error;
|
|
299
|
+
return bindWorkerRoomLocator(configRoom, ".letagents.json");
|
|
300
|
+
}
|
|
280
301
|
}
|
|
281
302
|
const gitContext = getGitRoomContext();
|
|
282
|
-
if (gitContext.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
303
|
+
if (gitContext.activeRoomLocator) {
|
|
304
|
+
try {
|
|
305
|
+
return await bindWorkerRoomLocator(gitContext.activeRoomLocator, "git remote");
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (!gitContext.repoRoom || !isNotFoundApiError(error))
|
|
309
|
+
throw error;
|
|
310
|
+
return bindWorkerRoomLocator(gitContext.repoRoom, "git remote");
|
|
311
|
+
}
|
|
291
312
|
}
|
|
292
313
|
const savedCurrentRoom = getStoredCurrentRoom();
|
|
293
314
|
if (!savedCurrentRoom) {
|
|
@@ -316,9 +337,9 @@ export async function autoJoinFromContext() {
|
|
|
316
337
|
return;
|
|
317
338
|
}
|
|
318
339
|
if (workerRuntime.mode === "worker") {
|
|
319
|
-
const bound = bindWorkerRoomFromContext();
|
|
340
|
+
const bound = await bindWorkerRoomFromContext();
|
|
320
341
|
if (bound) {
|
|
321
|
-
console.error(`🏠 Bound worker bearer to room '${bound.room.room_id}' (from ${bound.source}
|
|
342
|
+
console.error(`🏠 Bound worker bearer to existing room '${bound.room.room_id}' (from ${bound.source}).`);
|
|
322
343
|
}
|
|
323
344
|
else {
|
|
324
345
|
console.error("ℹ️ Worker bearer has no .letagents.json, git remote, or saved room to bind locally.");
|
|
@@ -332,17 +353,17 @@ export async function autoJoinFromContext() {
|
|
|
332
353
|
currentBranch: getGitCurrentBranch(),
|
|
333
354
|
defaultBranch: getGitDefaultBranch(),
|
|
334
355
|
});
|
|
335
|
-
if (gitContext.
|
|
336
|
-
const joinedBranchRoom = await joinExistingRoomIdentifier(gitContext.
|
|
356
|
+
if (gitContext.activeRefRoomLocator && gitContext.currentBranch) {
|
|
357
|
+
const joinedBranchRoom = await joinExistingRoomIdentifier(gitContext.activeRefRoomLocator, "config");
|
|
337
358
|
if (joinedBranchRoom) {
|
|
338
359
|
await ensureAgentIdentity();
|
|
339
|
-
console.error(`🏠 Auto-joined existing branch room '${gitContext.
|
|
360
|
+
console.error(`🏠 Auto-joined existing branch room '${gitContext.activeRefRoomLocator}' (from .letagents.json + branch '${gitContext.currentBranch}')`);
|
|
340
361
|
return;
|
|
341
362
|
}
|
|
342
363
|
}
|
|
343
364
|
await joinRoomIdentifier(configRoom, "config");
|
|
344
365
|
await ensureAgentIdentity();
|
|
345
|
-
const branchNote = gitContext.
|
|
366
|
+
const branchNote = gitContext.activeRefRoomLocator && gitContext.currentBranch
|
|
346
367
|
? `; branch '${gitContext.currentBranch}' has no existing Git Room`
|
|
347
368
|
: "";
|
|
348
369
|
console.error(`🏠 Auto-joined room '${configRoom}' (from .letagents.json${branchNote})`);
|
|
@@ -350,17 +371,17 @@ export async function autoJoinFromContext() {
|
|
|
350
371
|
}
|
|
351
372
|
const gitContext = getGitRoomContext();
|
|
352
373
|
if (gitContext.repoRoom) {
|
|
353
|
-
if (gitContext.
|
|
354
|
-
const joinedBranchRoom = await joinExistingRoomIdentifier(gitContext.
|
|
374
|
+
if (gitContext.activeRefRoomLocator && gitContext.currentBranch) {
|
|
375
|
+
const joinedBranchRoom = await joinExistingRoomIdentifier(gitContext.activeRefRoomLocator, "git-remote");
|
|
355
376
|
if (joinedBranchRoom) {
|
|
356
377
|
await ensureAgentIdentity();
|
|
357
|
-
console.error(`🏠 Auto-joined existing branch room '${gitContext.
|
|
378
|
+
console.error(`🏠 Auto-joined existing branch room '${gitContext.activeRefRoomLocator}' (inferred from git remote and branch '${gitContext.currentBranch}' — consider adding a .letagents.json)`);
|
|
358
379
|
return;
|
|
359
380
|
}
|
|
360
381
|
}
|
|
361
382
|
await joinRoomIdentifier(gitContext.repoRoom, "git-remote");
|
|
362
383
|
await ensureAgentIdentity();
|
|
363
|
-
const branchNote = gitContext.
|
|
384
|
+
const branchNote = gitContext.activeRefRoomLocator && gitContext.currentBranch
|
|
364
385
|
? `; branch '${gitContext.currentBranch}' has no existing Git Room`
|
|
365
386
|
: "";
|
|
366
387
|
console.error(`🏠 Auto-joined room '${gitContext.repoRoom}' (inferred from git remote${branchNote} — consider adding a .letagents.json)`);
|
|
@@ -3,6 +3,7 @@ import { lstat, readFile, realpath } from "node:fs/promises";
|
|
|
3
3
|
import { createConnection } from "node:net";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { parsePositivePgIntegerScopedId } from "../../../../shared/message-contracts.mjs";
|
|
6
7
|
import { getCurrentSupervisedRoomAuthority } from "./supervised-room-authority.js";
|
|
7
8
|
const NEGOTIATION_PROTOCOL_VERSION = 1;
|
|
8
9
|
const SUPPORTED_SUPERVISOR_PROTOCOL_VERSIONS = new Set([1, 2]);
|
|
@@ -18,6 +19,42 @@ const pendingCursorCheckpoints = new Map();
|
|
|
18
19
|
const activeCursorCheckpointDrains = new Set();
|
|
19
20
|
const cursorCheckpointRetryTimers = new Map();
|
|
20
21
|
const CURSOR_CHECKPOINT_RETRY_DELAYS_MS = [250, 1_000, 3_000];
|
|
22
|
+
export async function executeCurrentSupervisedTool(input, env = process.env, options = {}) {
|
|
23
|
+
const coordinates = await requireCurrentSupervisedCoordinates(env, options);
|
|
24
|
+
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
25
|
+
const negotiated = await negotiateSupervisor(coordinates.socketPath, timeoutMs);
|
|
26
|
+
if (negotiated.generation === null)
|
|
27
|
+
throw new Error("The supervised daemon generation is unavailable.");
|
|
28
|
+
const response = await supervisorRequest(coordinates.socketPath, {
|
|
29
|
+
version: negotiated.protocolVersion,
|
|
30
|
+
id: randomUUID(),
|
|
31
|
+
method: "supervisor.execute_bounded_tool",
|
|
32
|
+
params: {
|
|
33
|
+
entry_id: coordinates.entryId,
|
|
34
|
+
work_attempt_id: coordinates.workAttemptId,
|
|
35
|
+
execution_generation_id: coordinates.executionGenerationId,
|
|
36
|
+
...(coordinates.providerTurnId ? { provider_turn_id: coordinates.providerTurnId } : {}),
|
|
37
|
+
daemon_generation: negotiated.generation,
|
|
38
|
+
mcp_request_id: input.mcpRequestId,
|
|
39
|
+
tool_name: input.toolName,
|
|
40
|
+
input: input.input,
|
|
41
|
+
},
|
|
42
|
+
}, null);
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
if (/Unsupported daemon method:\s*supervisor\.execute_bounded_tool/i.test(response.error ?? "")) {
|
|
45
|
+
return { state: "unsupported" };
|
|
46
|
+
}
|
|
47
|
+
throw new Error(response.error || "The daemon-owned supervised tool was rejected.");
|
|
48
|
+
}
|
|
49
|
+
const result = response.result && typeof response.result === "object"
|
|
50
|
+
? response.result
|
|
51
|
+
: {};
|
|
52
|
+
const roomId = typeof result.room_id === "string" ? result.room_id.trim() : "";
|
|
53
|
+
if (!roomId || roomId.length > 1_024 || /[\u0000-\u001f\u007f]/.test(roomId)) {
|
|
54
|
+
throw new Error("The supervised daemon did not return valid exact room authority.");
|
|
55
|
+
}
|
|
56
|
+
return { state: "completed", roomId, result: result.result };
|
|
57
|
+
}
|
|
21
58
|
export async function prepareCurrentSupervisedEffect(input, env = process.env, options = {}) {
|
|
22
59
|
const coordinates = await requireCurrentSupervisedCoordinates(env, options);
|
|
23
60
|
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
@@ -412,6 +449,8 @@ export function scheduleSupervisedWorkerCursorCheckpoint(session, roomCursor, en
|
|
|
412
449
|
});
|
|
413
450
|
}
|
|
414
451
|
async function enqueueSupervisedWorkerCursorCheckpoint(session, roomCursor, env, options) {
|
|
452
|
+
if (parseRoomMessageNumber(roomCursor) === null)
|
|
453
|
+
return;
|
|
415
454
|
const coordinates = await resolveSupervisorCoordinates(session, env, options);
|
|
416
455
|
if (!coordinates)
|
|
417
456
|
return;
|
|
@@ -509,13 +548,14 @@ function isNewerRoomCursor(candidate, current) {
|
|
|
509
548
|
return false;
|
|
510
549
|
const candidateNumber = parseRoomMessageNumber(candidate);
|
|
511
550
|
const currentNumber = parseRoomMessageNumber(current);
|
|
512
|
-
if (candidateNumber
|
|
513
|
-
return
|
|
514
|
-
|
|
551
|
+
if (candidateNumber === null)
|
|
552
|
+
return false;
|
|
553
|
+
if (currentNumber === null)
|
|
554
|
+
return true;
|
|
555
|
+
return candidateNumber > currentNumber;
|
|
515
556
|
}
|
|
516
557
|
function parseRoomMessageNumber(cursor) {
|
|
517
|
-
|
|
518
|
-
return match ? BigInt(match[1]) : null;
|
|
558
|
+
return parsePositivePgIntegerScopedId(cursor, "msg");
|
|
519
559
|
}
|
|
520
560
|
/** Transport failures are retryable bookkeeping failures, not worker failures. */
|
|
521
561
|
export function isRetryableSupervisorBridgeError(error) {
|
|
@@ -695,16 +735,17 @@ function supervisorRequest(socketPath, request, timeoutMs) {
|
|
|
695
735
|
const socket = createConnection(socketPath);
|
|
696
736
|
let buffer = "";
|
|
697
737
|
let finished = false;
|
|
698
|
-
const timer = setTimeout(() => {
|
|
738
|
+
const timer = timeoutMs === null ? null : setTimeout(() => {
|
|
699
739
|
socket.destroy();
|
|
700
740
|
finish(() => reject(new Error("Timed out communicating with the supervisor daemon.")));
|
|
701
741
|
}, timeoutMs);
|
|
702
|
-
timer
|
|
742
|
+
timer?.unref();
|
|
703
743
|
const finish = (operation) => {
|
|
704
744
|
if (finished)
|
|
705
745
|
return;
|
|
706
746
|
finished = true;
|
|
707
|
-
|
|
747
|
+
if (timer)
|
|
748
|
+
clearTimeout(timer);
|
|
708
749
|
operation();
|
|
709
750
|
};
|
|
710
751
|
socket.setEncoding("utf8");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getDaemonToolExecutionContext } from "./daemon-tool-context.js";
|
|
1
2
|
export const LETAGENTS_AGENT_SESSION_BEARER_ENV = "LETAGENTS_AGENT_SESSION_BEARER";
|
|
2
3
|
export const LETAGENTS_SUPERVISED_BOUNDED_TURNS_ENV = "LETAGENTS_SUPERVISED_BOUNDED_TURNS";
|
|
3
4
|
export class WorkerBearerRuntimeConfigurationError extends Error {
|
|
@@ -7,6 +8,8 @@ export class WorkerBearerRuntimeConfigurationError extends Error {
|
|
|
7
8
|
}
|
|
8
9
|
}
|
|
9
10
|
export function getWorkerBearerRuntime() {
|
|
11
|
+
if (getDaemonToolExecutionContext())
|
|
12
|
+
return { mode: "supervised" };
|
|
10
13
|
const bearer = process.env.LETAGENTS_AGENT_SESSION_BEARER?.trim();
|
|
11
14
|
const supervised = process.env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() === "1";
|
|
12
15
|
const profile = process.env.LETAGENTS_EXECUTION_PROFILE?.trim();
|
|
@@ -90,7 +93,8 @@ export function workerModeDisabledToolResult(toolDescription = "This owner-auth
|
|
|
90
93
|
* asks it to do so.
|
|
91
94
|
*/
|
|
92
95
|
export function supervisedBoundedDeliveryDisabledToolResult(toolName = "wait_for_messages") {
|
|
93
|
-
if (
|
|
96
|
+
if (!getDaemonToolExecutionContext()
|
|
97
|
+
&& process.env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() !== "1") {
|
|
94
98
|
return null;
|
|
95
99
|
}
|
|
96
100
|
return {
|
|
@@ -16,10 +16,10 @@ export { clearAuthenticatedAccountCache, getAuthenticatedAccountCache, setAuthen
|
|
|
16
16
|
export { RepoRoomAuthRequiredError, maybeHandleRepoRoomAuthRequired, startPendingDeviceAuth, toRepoRoomAuthRequiredResult, } from "./runtime/device-auth.js";
|
|
17
17
|
export { AGENT_INSTANCE_UUID, currentAgentIdentity, currentAgentIdentityKey, detectAgentIdeLabel, detectAgentRuntimeLabel, ensureAgentIdentity, getConversationIdentity, getSessionLivenessRegistration, resolveOwnerContext, setConversationIdentity, storeCurrentAgentIdentity, toPublicAgentIdentity, withAgentIdentity, } from "./runtime/identity.js";
|
|
18
18
|
export { agentSessionCredentials, buildAgentDeliveryHeaders, ensureLocalWorkerAgentSession, getAgentSessionRepoBranch, identityFromAgentSession, requireWorkerAgentSession, resolveAgentSession, resolveClientRequestedBase, resolveWorkerToolIdentity, toPublicAgentSession, WORKER_BEARER_AGENT_SESSION_ID, } from "./runtime/agent-sessions.js";
|
|
19
|
-
export { appendIncludePromptOnly, getLastMessageId, normalizeOptionalToolString, toAgentReadableMessages, withJoinRoomAgentPrompt, } from "./runtime/messages.js";
|
|
20
|
-
export { currentRoom, attachMcpServer, getCurrentSupervisedRoomAuthority, getFallbackProjectId, getTargetRoomId, rememberRoom, runWithCurrentSupervisedRoom, shutdownRuntime, toPublicRoomResponse, toPublicCurrentRoomState, toPublicRoomState, toPublicStoredRoomSession, toRoomState, touchCurrentRoom, withCanonicalRoomLink, } from "./runtime/room-state.js";
|
|
19
|
+
export { appendIncludePromptOnly, AGENT_MESSAGE_BODY_MAX_BYTES, AGENT_MESSAGE_OUTPUT_MAX_BYTES, boundAgentMessageOutput, getLastMessageId, normalizeOptionalToolString, toAgentReadableMessages, withJoinRoomAgentPrompt, } from "./runtime/messages.js";
|
|
20
|
+
export { currentRoom, currentRoomMatchesLocator, attachMcpServer, getCurrentSupervisedRoomAuthority, getFallbackProjectId, getTargetRoomId, rememberRoom, runWithCurrentSupervisedRoom, shutdownRuntime, toPublicRoomResponse, toPublicCurrentRoomState, toPublicRoomState, toPublicStoredRoomSession, toRoomState, touchCurrentRoom, withCanonicalRoomLink, } from "./runtime/room-state.js";
|
|
21
21
|
export { getRememberedRoomPresence, heartbeatRoomPresence, syncRoomPresence, } from "./runtime/presence.js";
|
|
22
22
|
export { roomScopedApiCall } from "./runtime/room-api.js";
|
|
23
23
|
export { borrowSupervisedWorkerCredential, borrowCurrentSupervisedWorkerCredential, bindSupervisedWorkerSession, checkpointSupervisedWorkerCursor, isRetryableSupervisorBridgeError, scheduleSupervisedWorkerCursorCheckpoint, resolveCurrentSupervisedWorkerSession, } from "./runtime/supervisor-bridge.js";
|
|
24
24
|
export { autoJoinFromContext, buildJoinResponse, createInviteRoom, getCurrentLiveSessionPayload, joinInviteCode, joinNamedRoom, joinRoomIdentifier, joinRoomIdentifierWithoutImplicitGitRefCreate, normalizeJoinSessionMode, } from "./runtime/rooms.js";
|
|
25
|
-
export { clearPendingDeviceAuth, clearStoredAuth, clearStoredAuth as clearStoredAuthorization, endStoredAgentSession, getCurrentAgentSession, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAgentSession, getStoredAgentSessionsForRoomIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, listStoredCodexLiveSessions, saveAgentSession, setPendingDeviceAuth, setStoredAuth, setStoredAgentIdentity, addLocalChatMessage, addLocalTask, claimLocalTaskReviewLease, getLatestLocalChatMessages, getLocalChatMessages, getLocalTask, listLocalTasks, isLocalChatStorageEnabled, resolveLocalRoomStorageIdentifiers, releaseLocalTaskReviewLease, updateLocalTask, waitForLocalChatMessages, } from "../local-state.js";
|
|
25
|
+
export { clearPendingDeviceAuth, clearStoredAuth, clearStoredAuth as clearStoredAuthorization, endStoredAgentSession, getCurrentAgentSession, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAgentSession, getCurrentAgentSessionSnapshot, getStoredAgentSessionsForRoomIdentity, getStoredActiveAgentSessionsForRoom, getStoredAgentRoutingStateSnapshot, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, listStoredCodexLiveSessions, saveAgentSession, setPendingDeviceAuth, setStoredAuth, setStoredAgentIdentity, addLocalChatMessage, addLocalTask, claimLocalTaskReviewLease, getLatestLocalChatMessages, getLocalImportedRoutingAuthority, getLocalChatMessages, getLocalChatThreadRoutingMembership, getLocalTask, listLocalActiveTaskOwnerLeases, listLocalTasks, isLocalChatStorageEnabled, resolveLocalRoomStorageIdentifiers, releaseLocalTaskReviewLease, updateLocalTask, waitForLocalChatMessages, } from "../local-state.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { runWithCurrentSupervisedRoom } from "./runtime/room-state.js";
|
|
2
|
-
import { completeCurrentSupervisedEffect, prepareCurrentSupervisedEffect, } from "./runtime/supervisor-bridge.js";
|
|
2
|
+
import { completeCurrentSupervisedEffect, executeCurrentSupervisedTool, prepareCurrentSupervisedEffect, } from "./runtime/supervisor-bridge.js";
|
|
3
3
|
const READ_TOOLS = new Set([
|
|
4
4
|
"get_current_room",
|
|
5
5
|
"check_repo",
|
|
@@ -15,6 +15,13 @@ const READ_TOOLS = new Set([
|
|
|
15
15
|
"status_local_codex_session",
|
|
16
16
|
"rental_list_requests",
|
|
17
17
|
]);
|
|
18
|
+
export function supervisedToolIsMutation(toolName) {
|
|
19
|
+
return !READ_TOOLS.has(toolName);
|
|
20
|
+
}
|
|
21
|
+
// The desktop daemon's local control protocol intentionally uses small bounded
|
|
22
|
+
// frames. A read result can be returned live to the provider without copying
|
|
23
|
+
// the entire payload into the durable effect journal.
|
|
24
|
+
const MAX_DURABLE_READ_RESULT_BYTES = 16 * 1024;
|
|
18
25
|
function instruction(text, data = {}) {
|
|
19
26
|
const payload = { ...data, instruction: text };
|
|
20
27
|
return {
|
|
@@ -22,7 +29,19 @@ function instruction(text, data = {}) {
|
|
|
22
29
|
structuredContent: payload,
|
|
23
30
|
};
|
|
24
31
|
}
|
|
32
|
+
export function durableCompletionResult(result, mutation) {
|
|
33
|
+
if (mutation)
|
|
34
|
+
return result;
|
|
35
|
+
const serializedBytes = Buffer.byteLength(JSON.stringify(result), "utf8");
|
|
36
|
+
if (serializedBytes <= MAX_DURABLE_READ_RESULT_BYTES)
|
|
37
|
+
return result;
|
|
38
|
+
return instruction("The read completed, but its large result was returned live instead of being copied into the durable journal. Issue a fresh read request if this exact request is replayed after a restart.", {
|
|
39
|
+
code: "SUPERVISED_READ_RESULT_NOT_RETAINED",
|
|
40
|
+
serialized_bytes: serializedBytes,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
25
43
|
const productionDependencies = {
|
|
44
|
+
executeTool: executeCurrentSupervisedTool,
|
|
26
45
|
prepareEffect: prepareCurrentSupervisedEffect,
|
|
27
46
|
completeEffect: completeCurrentSupervisedEffect,
|
|
28
47
|
withRoom: runWithCurrentSupervisedRoom,
|
|
@@ -37,6 +56,7 @@ export function profileAwareToolServer(server, profile, dependencies = productio
|
|
|
37
56
|
return typeof value === "function" ? value.bind(target) : value;
|
|
38
57
|
}
|
|
39
58
|
return (name, ...registration) => {
|
|
59
|
+
const mutation = supervisedToolIsMutation(name);
|
|
40
60
|
const callback = registration.at(-1);
|
|
41
61
|
if (typeof callback !== "function")
|
|
42
62
|
throw new Error(`Tool ${name} has no callback.`);
|
|
@@ -46,12 +66,18 @@ export function profileAwareToolServer(server, profile, dependencies = productio
|
|
|
46
66
|
if (extra.requestId === undefined || extra.requestId === null || String(extra.requestId).trim() === "") {
|
|
47
67
|
throw new Error(`Supervised tool ${name} is missing its MCP request id; refusing an effect that cannot be deduplicated safely.`);
|
|
48
68
|
}
|
|
49
|
-
const
|
|
69
|
+
const executionRequest = {
|
|
50
70
|
toolName: name,
|
|
51
71
|
input,
|
|
52
72
|
mcpRequestId: String(extra.requestId),
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
};
|
|
74
|
+
if (dependencies.executeTool) {
|
|
75
|
+
const executed = await dependencies.executeTool(executionRequest);
|
|
76
|
+
if (executed.state === "completed") {
|
|
77
|
+
return dependencies.withRoom(executed.roomId, () => executed.result);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const prepared = await dependencies.prepareEffect({ ...executionRequest, mutation });
|
|
55
81
|
return dependencies.withRoom(prepared.roomId, async () => {
|
|
56
82
|
if (prepared.state === "completed")
|
|
57
83
|
return prepared.result;
|
|
@@ -94,7 +120,10 @@ export function profileAwareToolServer(server, profile, dependencies = productio
|
|
|
94
120
|
}
|
|
95
121
|
// Completion transport is deliberately outside the callback catch.
|
|
96
122
|
// A reporting failure must never relabel a successful action failed.
|
|
97
|
-
await dependencies.completeEffect({
|
|
123
|
+
await dependencies.completeEffect({
|
|
124
|
+
effectId: prepared.effectId,
|
|
125
|
+
result: durableCompletionResult(result, mutation),
|
|
126
|
+
});
|
|
98
127
|
return result;
|
|
99
128
|
});
|
|
100
129
|
};
|