letagents 0.12.11 → 0.12.12
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/local-state/agent-sessions.js +15 -0
- package/dist/mcp/server/register-tools.js +23 -12
- package/dist/mcp/server/runtime/agent-sessions.js +45 -5
- package/dist/mcp/server/runtime/api.js +29 -1
- package/dist/mcp/server/runtime/execution-profile.js +19 -0
- package/dist/mcp/server/runtime/identity/directory.js +2 -2
- package/dist/mcp/server/runtime/presence.js +4 -2
- package/dist/mcp/server/runtime/room-api.js +24 -7
- package/dist/mcp/server/runtime/room-state.js +46 -0
- package/dist/mcp/server/runtime/rooms.js +16 -2
- package/dist/mcp/server/runtime/supervised-room-authority.js +8 -0
- package/dist/mcp/server/runtime/supervisor-bridge.js +658 -21
- package/dist/mcp/server/runtime/tool-surface-policy.js +26 -0
- package/dist/mcp/server/runtime/worker-bearer.js +40 -6
- package/dist/mcp/server/runtime-contract.js +27 -0
- package/dist/mcp/server/runtime.js +14 -3
- package/dist/mcp/server/supervised-tool-facade.js +105 -0
- package/dist/mcp/server/tools/agent-sessions.js +74 -7
- package/dist/mcp/server/tools/messages/index.js +3 -2
- package/dist/mcp/server/tools/messages/send-tool.js +3 -0
- package/dist/mcp/server/tools/messages/wait-tool.js +56 -5
- package/dist/mcp/server/tools/onboarding/status-tool.js +5 -4
- package/dist/mcp/server/tools/rooms/inspection-tools.js +26 -13
- package/dist/mcp/server/tools/supervised-room-turn.js +42 -0
- package/dist/mcp/server.js +14 -7
- package/dist/shared/activation-routing.js +44 -0
- package/package.json +6 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const TOOL_SURFACE_BY_PROFILE = {
|
|
2
|
+
supervised_room_turn: {
|
|
3
|
+
agentSessionLifecycle: false,
|
|
4
|
+
deliveryLoop: false,
|
|
5
|
+
onboarding: false,
|
|
6
|
+
rental: false,
|
|
7
|
+
roomResume: false,
|
|
8
|
+
},
|
|
9
|
+
autonomous_mcp_worker: {
|
|
10
|
+
agentSessionLifecycle: true,
|
|
11
|
+
deliveryLoop: true,
|
|
12
|
+
onboarding: true,
|
|
13
|
+
rental: true,
|
|
14
|
+
roomResume: true,
|
|
15
|
+
},
|
|
16
|
+
interactive_desktop: {
|
|
17
|
+
agentSessionLifecycle: true,
|
|
18
|
+
deliveryLoop: true,
|
|
19
|
+
onboarding: true,
|
|
20
|
+
rental: true,
|
|
21
|
+
roomResume: true,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export function toolSurfaceForExecutionProfile(profile) {
|
|
25
|
+
return TOOL_SURFACE_BY_PROFILE[profile];
|
|
26
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const LETAGENTS_AGENT_SESSION_BEARER_ENV = "LETAGENTS_AGENT_SESSION_BEARER";
|
|
2
|
+
export const LETAGENTS_SUPERVISED_BOUNDED_TURNS_ENV = "LETAGENTS_SUPERVISED_BOUNDED_TURNS";
|
|
2
3
|
export class WorkerBearerRuntimeConfigurationError extends Error {
|
|
3
4
|
constructor(message) {
|
|
4
5
|
super(message);
|
|
@@ -7,8 +8,21 @@ export class WorkerBearerRuntimeConfigurationError extends Error {
|
|
|
7
8
|
}
|
|
8
9
|
export function getWorkerBearerRuntime() {
|
|
9
10
|
const bearer = process.env.LETAGENTS_AGENT_SESSION_BEARER?.trim();
|
|
10
|
-
|
|
11
|
+
const supervised = process.env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() === "1";
|
|
12
|
+
const profile = process.env.LETAGENTS_EXECUTION_PROFILE?.trim();
|
|
13
|
+
if (supervised !== (profile === "supervised_room_turn")) {
|
|
14
|
+
return {
|
|
15
|
+
mode: "invalid",
|
|
16
|
+
error: "LETAGENTS_EXECUTION_PROFILE=supervised_room_turn and LETAGENTS_SUPERVISED_BOUNDED_TURNS=1 must be configured together.",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (!bearer && !supervised)
|
|
11
20
|
return { mode: "owner" };
|
|
21
|
+
if (bearer && supervised) {
|
|
22
|
+
return {
|
|
23
|
+
mode: "invalid",
|
|
24
|
+
error: "Daemon-supervised bounded turns refuse LETAGENTS_AGENT_SESSION_BEARER; credentials must be borrowed from the exact supervisor generation.",
|
|
25
|
+
};
|
|
12
26
|
}
|
|
13
27
|
const apiUrl = process.env.LETAGENTS_API_URL?.trim();
|
|
14
28
|
if (!apiUrl) {
|
|
@@ -36,13 +50,13 @@ export function getWorkerBearerRuntime() {
|
|
|
36
50
|
error: "Worker bearer mode requires LETAGENTS_API_URL to be a valid HTTP(S) URL.",
|
|
37
51
|
};
|
|
38
52
|
}
|
|
39
|
-
if (process.env.LETAGENTS_TOKEN?.trim()) {
|
|
53
|
+
if (bearer && process.env.LETAGENTS_TOKEN?.trim()) {
|
|
40
54
|
return {
|
|
41
55
|
mode: "invalid",
|
|
42
56
|
error: "Worker bearer mode refuses LETAGENTS_TOKEN. Remove the owner token from this process before starting the worker.",
|
|
43
57
|
};
|
|
44
58
|
}
|
|
45
|
-
return { mode: "worker", bearer };
|
|
59
|
+
return bearer ? { mode: "worker", bearer } : { mode: "supervised" };
|
|
46
60
|
}
|
|
47
61
|
export function requireValidWorkerBearerRuntime() {
|
|
48
62
|
const runtime = getWorkerBearerRuntime();
|
|
@@ -51,17 +65,37 @@ export function requireValidWorkerBearerRuntime() {
|
|
|
51
65
|
}
|
|
52
66
|
return runtime;
|
|
53
67
|
}
|
|
68
|
+
export function isSupervisedBoundedTurn() {
|
|
69
|
+
return requireValidWorkerBearerRuntime().mode === "supervised";
|
|
70
|
+
}
|
|
54
71
|
export function workerModeDisabledToolResult(toolDescription = "This owner-auth onboarding tool") {
|
|
55
72
|
const runtime = getWorkerBearerRuntime();
|
|
56
73
|
if (runtime.mode === "invalid") {
|
|
57
74
|
return { success: false, error: "worker_bearer_configuration_invalid", message: runtime.error };
|
|
58
75
|
}
|
|
59
|
-
if (runtime.mode === "worker") {
|
|
76
|
+
if (runtime.mode === "worker" || runtime.mode === "supervised") {
|
|
60
77
|
return {
|
|
61
78
|
success: false,
|
|
62
|
-
error: "worker_bearer_mode",
|
|
63
|
-
message:
|
|
79
|
+
error: runtime.mode === "worker" ? "worker_bearer_mode" : "supervised_bounded_mode",
|
|
80
|
+
message: runtime.mode === "worker"
|
|
81
|
+
? `${toolDescription} is disabled while LETAGENTS_AGENT_SESSION_BEARER is configured.`
|
|
82
|
+
: `${toolDescription} is disabled during a daemon-supervised bounded turn.`,
|
|
64
83
|
};
|
|
65
84
|
}
|
|
66
85
|
return null;
|
|
67
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Supervised room delivery belongs to the desktop daemon. A bounded provider
|
|
89
|
+
* turn must never recreate the permanent MCP polling loop, even if its prompt
|
|
90
|
+
* asks it to do so.
|
|
91
|
+
*/
|
|
92
|
+
export function supervisedBoundedDeliveryDisabledToolResult(toolName = "wait_for_messages") {
|
|
93
|
+
if (process.env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() !== "1") {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
error: "supervised_bounded_delivery",
|
|
99
|
+
message: `${toolName} is disabled because supervised room delivery is owned by the desktop daemon.`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { registerTools } from "./register-tools.js";
|
|
2
|
+
export const LETAGENTS_RUNTIME_CONTRACT_ARG = "--letagents-runtime-contract";
|
|
3
|
+
/**
|
|
4
|
+
* Discover through the production registration path rather than maintaining a
|
|
5
|
+
* second capability list that can drift from the MCP server.
|
|
6
|
+
*/
|
|
7
|
+
export function registeredToolNames(profile, supervisedProvider = null) {
|
|
8
|
+
const names = new Set();
|
|
9
|
+
const recorder = {
|
|
10
|
+
tool(name) {
|
|
11
|
+
names.add(name);
|
|
12
|
+
return {};
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
registerTools(recorder, profile, supervisedProvider);
|
|
16
|
+
return [...names].sort();
|
|
17
|
+
}
|
|
18
|
+
export function letAgentsRuntimeContract() {
|
|
19
|
+
return {
|
|
20
|
+
format: 1,
|
|
21
|
+
profiles: {
|
|
22
|
+
cursor_supervised_room_turn: {
|
|
23
|
+
tools: registeredToolNames("supervised_room_turn", "cursor"),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
// Compatibility facade for MCP server runtime helpers. The implementation lives
|
|
2
2
|
// in src/mcp/server/runtime/* so tool modules can import focused responsibilities
|
|
3
3
|
// without turning this file back into the runtime god module.
|
|
4
|
+
import { isLocalRoomStorageEnabled as isStoredLocalRoomStorageEnabled, touchRoomSession as touchStoredRoomSession, } from "../local-state.js";
|
|
5
|
+
import { isSupervisedBoundedTurn } from "./runtime/worker-bearer.js";
|
|
6
|
+
/** A daemon-supervised turn must always use its exact cloud worker route. */
|
|
7
|
+
export async function isLocalRoomStorageEnabled(roomId) {
|
|
8
|
+
return !isSupervisedBoundedTurn() && isStoredLocalRoomStorageEnabled(roomId);
|
|
9
|
+
}
|
|
10
|
+
export function touchRoomSession(roomId, lastMessageId) {
|
|
11
|
+
if (!isSupervisedBoundedTurn())
|
|
12
|
+
touchStoredRoomSession(roomId, lastMessageId);
|
|
13
|
+
}
|
|
4
14
|
export { API_URL, ApiError, apiCall, getAuthorizationHeader, getLetagentsToken, isMissingRouteError, parseApiErrorPayload, resolveApiPath, } from "./runtime/api.js";
|
|
5
15
|
export { clearAuthenticatedAccountCache, getAuthenticatedAccountCache, setAuthenticatedAccountCache, } from "./runtime/auth-cache.js";
|
|
6
16
|
export { RepoRoomAuthRequiredError, maybeHandleRepoRoomAuthRequired, startPendingDeviceAuth, toRepoRoomAuthRequiredResult, } from "./runtime/device-auth.js";
|
|
7
17
|
export { AGENT_INSTANCE_UUID, currentAgentIdentity, currentAgentIdentityKey, detectAgentIdeLabel, detectAgentRuntimeLabel, ensureAgentIdentity, getConversationIdentity, getSessionLivenessRegistration, resolveOwnerContext, setConversationIdentity, storeCurrentAgentIdentity, toPublicAgentIdentity, withAgentIdentity, } from "./runtime/identity.js";
|
|
8
|
-
export { agentSessionCredentials, buildAgentDeliveryHeaders, ensureLocalWorkerAgentSession, getAgentSessionRepoBranch, identityFromAgentSession, requireWorkerAgentSession, resolveAgentSession, resolveWorkerToolIdentity, toPublicAgentSession, WORKER_BEARER_AGENT_SESSION_ID, } from "./runtime/agent-sessions.js";
|
|
18
|
+
export { agentSessionCredentials, buildAgentDeliveryHeaders, ensureLocalWorkerAgentSession, getAgentSessionRepoBranch, identityFromAgentSession, requireWorkerAgentSession, resolveAgentSession, resolveClientRequestedBase, resolveWorkerToolIdentity, toPublicAgentSession, WORKER_BEARER_AGENT_SESSION_ID, } from "./runtime/agent-sessions.js";
|
|
9
19
|
export { appendIncludePromptOnly, getLastMessageId, normalizeOptionalToolString, toAgentReadableMessages, withJoinRoomAgentPrompt, } from "./runtime/messages.js";
|
|
10
|
-
export { currentRoom, attachMcpServer, getFallbackProjectId, getTargetRoomId, rememberRoom, shutdownRuntime, toPublicRoomResponse, toPublicRoomState, toPublicStoredRoomSession, toRoomState, touchCurrentRoom, withCanonicalRoomLink, } from "./runtime/room-state.js";
|
|
20
|
+
export { currentRoom, attachMcpServer, getCurrentSupervisedRoomAuthority, getFallbackProjectId, getTargetRoomId, rememberRoom, runWithCurrentSupervisedRoom, shutdownRuntime, toPublicRoomResponse, toPublicCurrentRoomState, toPublicRoomState, toPublicStoredRoomSession, toRoomState, touchCurrentRoom, withCanonicalRoomLink, } from "./runtime/room-state.js";
|
|
11
21
|
export { getRememberedRoomPresence, heartbeatRoomPresence, syncRoomPresence, } from "./runtime/presence.js";
|
|
12
22
|
export { roomScopedApiCall } from "./runtime/room-api.js";
|
|
23
|
+
export { borrowSupervisedWorkerCredential, borrowCurrentSupervisedWorkerCredential, bindSupervisedWorkerSession, checkpointSupervisedWorkerCursor, isRetryableSupervisorBridgeError, scheduleSupervisedWorkerCursorCheckpoint, resolveCurrentSupervisedWorkerSession, } from "./runtime/supervisor-bridge.js";
|
|
13
24
|
export { autoJoinFromContext, buildJoinResponse, createInviteRoom, getCurrentLiveSessionPayload, joinInviteCode, joinNamedRoom, joinRoomIdentifier, joinRoomIdentifierWithoutImplicitGitRefCreate, normalizeJoinSessionMode, } from "./runtime/rooms.js";
|
|
14
|
-
export { clearPendingDeviceAuth, clearStoredAuth, clearStoredAuth as clearStoredAuthorization, endStoredAgentSession, getCurrentAgentSession, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAgentSession, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, listStoredCodexLiveSessions, saveAgentSession, setPendingDeviceAuth, setStoredAuth, setStoredAgentIdentity,
|
|
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";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { runWithCurrentSupervisedRoom } from "./runtime/room-state.js";
|
|
2
|
+
import { completeCurrentSupervisedEffect, prepareCurrentSupervisedEffect, } from "./runtime/supervisor-bridge.js";
|
|
3
|
+
const READ_TOOLS = new Set([
|
|
4
|
+
"get_current_room",
|
|
5
|
+
"check_repo",
|
|
6
|
+
"check_repo_visibility",
|
|
7
|
+
"read_messages",
|
|
8
|
+
"wait_for_messages",
|
|
9
|
+
"get_board",
|
|
10
|
+
"get_board_settings",
|
|
11
|
+
"get_room_artifacts",
|
|
12
|
+
"get_room_events",
|
|
13
|
+
"list_board_intents",
|
|
14
|
+
"get_onboarding_status",
|
|
15
|
+
"status_local_codex_session",
|
|
16
|
+
"rental_list_requests",
|
|
17
|
+
]);
|
|
18
|
+
function instruction(text, data = {}) {
|
|
19
|
+
const payload = { ...data, instruction: text };
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
22
|
+
structuredContent: payload,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const productionDependencies = {
|
|
26
|
+
prepareEffect: prepareCurrentSupervisedEffect,
|
|
27
|
+
completeEffect: completeCurrentSupervisedEffect,
|
|
28
|
+
withRoom: runWithCurrentSupervisedRoom,
|
|
29
|
+
};
|
|
30
|
+
export function profileAwareToolServer(server, profile, dependencies = productionDependencies, supervisedProvider = process.env.LETAGENTS_SUPERVISOR_PROVIDER?.trim() || null) {
|
|
31
|
+
if (profile !== "supervised_room_turn")
|
|
32
|
+
return server;
|
|
33
|
+
return new Proxy(server, {
|
|
34
|
+
get(target, property, receiver) {
|
|
35
|
+
if (property !== "tool") {
|
|
36
|
+
const value = Reflect.get(target, property, receiver);
|
|
37
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
38
|
+
}
|
|
39
|
+
return (name, ...registration) => {
|
|
40
|
+
const callback = registration.at(-1);
|
|
41
|
+
if (typeof callback !== "function")
|
|
42
|
+
throw new Error(`Tool ${name} has no callback.`);
|
|
43
|
+
const wrapped = async (...call) => {
|
|
44
|
+
const extra = (call.at(-1) ?? {});
|
|
45
|
+
const input = call.length > 1 ? call[0] : {};
|
|
46
|
+
if (extra.requestId === undefined || extra.requestId === null || String(extra.requestId).trim() === "") {
|
|
47
|
+
throw new Error(`Supervised tool ${name} is missing its MCP request id; refusing an effect that cannot be deduplicated safely.`);
|
|
48
|
+
}
|
|
49
|
+
const prepared = await dependencies.prepareEffect({
|
|
50
|
+
toolName: name,
|
|
51
|
+
input,
|
|
52
|
+
mcpRequestId: String(extra.requestId),
|
|
53
|
+
mutation: !READ_TOOLS.has(name),
|
|
54
|
+
});
|
|
55
|
+
return dependencies.withRoom(prepared.roomId, async () => {
|
|
56
|
+
if (prepared.state === "completed")
|
|
57
|
+
return prepared.result;
|
|
58
|
+
if (prepared.state === "uncertain") {
|
|
59
|
+
return instruction("This mutating tool may already have completed, but its result was not durably checkpointed. Verify the external state before issuing a new request; this exact request will not be repeated automatically.", {
|
|
60
|
+
code: "SUPERVISED_EFFECT_OUTCOME_UNCERTAIN",
|
|
61
|
+
effect_id: prepared.effectId,
|
|
62
|
+
detail: prepared.error,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (prepared.action === "use_final_answer") {
|
|
66
|
+
return instruction(supervisedProvider === "cursor"
|
|
67
|
+
? "Do not send the activating room reply with a message tool. Keep working, then record the one public answer with complete_room_turn; Cursor's aggregate final text is live evidence only."
|
|
68
|
+
: "Do not send the activating room reply with a message tool. Return it as your final answer; the daemon will publish it exactly once.", {
|
|
69
|
+
code: "USE_FINAL_ANSWER",
|
|
70
|
+
source_message_id: prepared.sourceMessageId,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (prepared.action === "room_move_prepared") {
|
|
74
|
+
return instruction(supervisedProvider === "cursor"
|
|
75
|
+
? "The room move is prepared. Finish the work, then call complete_room_turn with the public response; the daemon will publish that proposal and then move the agent."
|
|
76
|
+
: "The room move is prepared. Finish this turn normally; the daemon will publish the activating response and then move the agent.", {
|
|
77
|
+
code: "ROOM_MOVE_PREPARED",
|
|
78
|
+
destination_room: prepared.destinationRoom,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
let result;
|
|
82
|
+
try {
|
|
83
|
+
result = await callback(...call);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
try {
|
|
87
|
+
await dependencies.completeEffect({ effectId: prepared.effectId, error: error instanceof Error ? error.message : String(error) });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Preserve the callback error. An unacknowledged journal entry
|
|
91
|
+
// remains executing, which is safer than repeating the effect.
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
// Completion transport is deliberately outside the callback catch.
|
|
96
|
+
// A reporting failure must never relabel a successful action failed.
|
|
97
|
+
await dependencies.completeEffect({ effectId: prepared.effectId, result });
|
|
98
|
+
return result;
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
return target.tool.call(target, name, ...registration.slice(0, -1), wrapped);
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -2,9 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
import { scheduleCodexRuntimeStreamBridgeBind, } from "../../codex-session.js";
|
|
3
3
|
import { getManagedAgentProvider, toManagedAgentStartResponse, } from "../../managed-agent-providers.js";
|
|
4
4
|
import { encodeRoomIdPath, looksLikeInviteCode, normalizeInviteCode } from "../../room-id.js";
|
|
5
|
-
import { AGENT_INSTANCE_UUID, RepoRoomAuthRequiredError, apiCall, agentSessionCredentials, currentRoom, detectAgentIdeLabel, detectAgentRuntimeLabel, endStoredAgentSession, ensureAgentIdentity, getSessionLivenessRegistration, getAgentSessionRepoBranch, getStoredAgentSession, getTargetRoomId, ensureLocalWorkerAgentSession, isLocalRoomStorageEnabled, joinRoomIdentifier, resolveLocalRoomStorageIdentifiers, saveAgentSession, toPublicAgentSession, toPublicRoomState, toRepoRoomAuthRequiredResult, withAgentIdentity, resolveWorkerToolIdentity, } from "../runtime.js";
|
|
5
|
+
import { AGENT_INSTANCE_UUID, RepoRoomAuthRequiredError, apiCall, agentSessionCredentials, currentRoom, detectAgentIdeLabel, detectAgentRuntimeLabel, endStoredAgentSession, ensureAgentIdentity, getSessionLivenessRegistration, getAgentSessionRepoBranch, getStoredAgentSession, getStoredAgentSessionsForRoomIdentity, getTargetRoomId, ensureLocalWorkerAgentSession, isLocalRoomStorageEnabled, joinRoomIdentifier, resolveLocalRoomStorageIdentifiers, resolveClientRequestedBase, saveAgentSession, toPublicAgentSession, toPublicRoomState, toRepoRoomAuthRequiredResult, withAgentIdentity, resolveWorkerToolIdentity, } from "../runtime.js";
|
|
6
6
|
import { requireValidWorkerBearerRuntime, workerModeDisabledToolResult, } from "../runtime/worker-bearer.js";
|
|
7
|
-
import {
|
|
7
|
+
import { bindSupervisedWorkerSessionWithContext } from "../runtime/supervisor-bridge.js";
|
|
8
8
|
export function registerAgentSessionTools(server) {
|
|
9
9
|
// -- register_agent_session -------------------------------------------------
|
|
10
10
|
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.", {
|
|
@@ -27,8 +27,29 @@ export function registerAgentSessionTools(server) {
|
|
|
27
27
|
cwd: z
|
|
28
28
|
.string()
|
|
29
29
|
.optional()
|
|
30
|
-
.describe("
|
|
30
|
+
.describe("Worker working directory used for branch detection and exact supervised Codex binding. Defaults to the MCP server's working directory."),
|
|
31
31
|
}, async ({ room_id, session_kind, runtime, display_name, cwd }) => {
|
|
32
|
+
const workerRuntime = requireValidWorkerBearerRuntime();
|
|
33
|
+
if (workerRuntime.mode === "supervised") {
|
|
34
|
+
// Resolve before currentRoom, config, branch, or local storage. The
|
|
35
|
+
// daemon context is the only authority for a bounded worker's room.
|
|
36
|
+
const { agentSession } = await resolveWorkerToolIdentity({ roomId: room_id?.trim() || null });
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: JSON.stringify({
|
|
42
|
+
success: true,
|
|
43
|
+
worker_bearer_mode: false,
|
|
44
|
+
supervised_bounded_mode: true,
|
|
45
|
+
agent_session: toPublicAgentSession(agentSession),
|
|
46
|
+
agent_session_id: agentSession.session_id,
|
|
47
|
+
use_agent_session_id: "This exact daemon-supervised agent_session_id may be passed to room tools. The credential remains daemon-private.",
|
|
48
|
+
}, null, 2),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
32
53
|
const targetRoomId = getTargetRoomId(room_id);
|
|
33
54
|
if (!targetRoomId) {
|
|
34
55
|
return {
|
|
@@ -70,7 +91,7 @@ export function registerAgentSessionTools(server) {
|
|
|
70
91
|
}
|
|
71
92
|
const { cloudRoomId } = await resolveLocalRoomStorageIdentifiers(targetRoomId);
|
|
72
93
|
const apiRoomId = cloudRoomId || targetRoomId;
|
|
73
|
-
if (
|
|
94
|
+
if (workerRuntime.mode === "worker") {
|
|
74
95
|
// The supplied bearer is issued for an existing server-side agent
|
|
75
96
|
// session. Do not call the owner-only registration endpoint or write
|
|
76
97
|
// any session credential to local storage.
|
|
@@ -81,7 +102,8 @@ export function registerAgentSessionTools(server) {
|
|
|
81
102
|
type: "text",
|
|
82
103
|
text: JSON.stringify({
|
|
83
104
|
success: true,
|
|
84
|
-
worker_bearer_mode:
|
|
105
|
+
worker_bearer_mode: workerRuntime.mode === "worker",
|
|
106
|
+
supervised_bounded_mode: false,
|
|
85
107
|
agent_session: toPublicAgentSession(agentSession),
|
|
86
108
|
agent_session_id: agentSession.session_id,
|
|
87
109
|
use_agent_session_id: "This local worker-bearer session marker may be passed to room tools. The supplied bearer remains the only server credential.",
|
|
@@ -104,6 +126,22 @@ export function registerAgentSessionTools(server) {
|
|
|
104
126
|
],
|
|
105
127
|
};
|
|
106
128
|
}
|
|
129
|
+
// Stable-base signal (task_66): declare the intent behind display_name
|
|
130
|
+
// so the server can converge a replayed decorated label without ever
|
|
131
|
+
// guessing from numeric shape.
|
|
132
|
+
const priorRoomSessions = getStoredAgentSessionsForRoomIdentity(apiRoomId, identity.canonical_key);
|
|
133
|
+
const requestedSessionKind = session_kind ?? "worker";
|
|
134
|
+
const requestedBaseDisplayName = resolveClientRequestedBase({
|
|
135
|
+
explicitDisplayName: display_name,
|
|
136
|
+
identityDisplayName: identity.display_name,
|
|
137
|
+
priorSessions: priorRoomSessions,
|
|
138
|
+
});
|
|
139
|
+
const replacementSession = requestedSessionKind === "worker"
|
|
140
|
+
? priorRoomSessions.find((session) => !session.ended_at
|
|
141
|
+
&& session.session_kind === "worker"
|
|
142
|
+
&& session.agent_instance_id === AGENT_INSTANCE_UUID
|
|
143
|
+
&& Boolean(session.session_token)) ?? null
|
|
144
|
+
: null;
|
|
107
145
|
const created = await apiCall(`/rooms/${encodeRoomIdPath(apiRoomId)}/agent-sessions`, {
|
|
108
146
|
method: "POST",
|
|
109
147
|
body: JSON.stringify({
|
|
@@ -112,10 +150,13 @@ export function registerAgentSessionTools(server) {
|
|
|
112
150
|
ide_label: identity.ide_label ?? detectAgentIdeLabel(),
|
|
113
151
|
agent_instance_id: AGENT_INSTANCE_UUID,
|
|
114
152
|
display_name: display_name?.trim() || identity.display_name,
|
|
153
|
+
requested_base_display_name: requestedBaseDisplayName,
|
|
115
154
|
session_kind: session_kind ?? "worker",
|
|
116
155
|
runtime: requestedRuntime,
|
|
117
156
|
repo_branch: repoBranch,
|
|
118
157
|
registration_liveness: getSessionLivenessRegistration(requestedRuntime),
|
|
158
|
+
replace_agent_session_id: replacementSession?.session_id ?? null,
|
|
159
|
+
replace_agent_session_token: replacementSession?.session_token ?? null,
|
|
119
160
|
}),
|
|
120
161
|
});
|
|
121
162
|
const sessionId = typeof created.session_id === "string" ? created.session_id : "";
|
|
@@ -123,7 +164,10 @@ export function registerAgentSessionTools(server) {
|
|
|
123
164
|
if (!sessionId || !sessionToken) {
|
|
124
165
|
throw new Error("Agent session registration response was missing session credentials.");
|
|
125
166
|
}
|
|
126
|
-
|
|
167
|
+
if (replacementSession) {
|
|
168
|
+
endStoredAgentSession(replacementSession.session_id, typeof created.created_at === "string" ? created.created_at : new Date().toISOString());
|
|
169
|
+
}
|
|
170
|
+
let session = saveAgentSession({
|
|
127
171
|
session_id: sessionId,
|
|
128
172
|
session_token: sessionToken,
|
|
129
173
|
room_id: typeof created.room_id === "string" ? created.room_id : apiRoomId,
|
|
@@ -138,6 +182,11 @@ export function registerAgentSessionTools(server) {
|
|
|
138
182
|
agent_key: typeof created.agent_key === "string" ? created.agent_key : identity.canonical_key,
|
|
139
183
|
agent_instance_id: typeof created.agent_instance_id === "string" ? created.agent_instance_id : AGENT_INSTANCE_UUID,
|
|
140
184
|
display_name: typeof created.display_name === "string" ? created.display_name : identity.display_name,
|
|
185
|
+
// Prefer the server-recorded allocation base; fall back to the base we
|
|
186
|
+
// declared so a later resume still replays a stable signal.
|
|
187
|
+
requested_base_display_name: (typeof created.assigned_base_display_name === "string" && created.assigned_base_display_name.trim())
|
|
188
|
+
|| requestedBaseDisplayName
|
|
189
|
+
|| null,
|
|
141
190
|
owner_label: typeof created.owner_label === "string" ? created.owner_label : identity.owner_label,
|
|
142
191
|
ide_label: typeof created.ide_label === "string" ? created.ide_label : identity.ide_label ?? detectAgentIdeLabel(),
|
|
143
192
|
repo_branch: typeof created.repo_branch === "string" ? created.repo_branch : repoBranch,
|
|
@@ -146,7 +195,13 @@ export function registerAgentSessionTools(server) {
|
|
|
146
195
|
last_seen_at: typeof created.last_seen_at === "string" ? created.last_seen_at : new Date().toISOString(),
|
|
147
196
|
ended_at: typeof created.ended_at === "string" ? created.ended_at : null,
|
|
148
197
|
});
|
|
149
|
-
await
|
|
198
|
+
const supervisorBinding = await bindSupervisedWorkerSessionWithContext(session, process.env, { cwd: cwd?.trim() || process.cwd() });
|
|
199
|
+
if (supervisorBinding.supervisorContextCwd) {
|
|
200
|
+
session = saveAgentSession({
|
|
201
|
+
...session,
|
|
202
|
+
supervisor_context_cwd: supervisorBinding.supervisorContextCwd,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
150
205
|
scheduleCodexRuntimeStreamBridgeBind(session);
|
|
151
206
|
return {
|
|
152
207
|
content: [
|
|
@@ -167,6 +222,18 @@ export function registerAgentSessionTools(server) {
|
|
|
167
222
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room or the stored session room."),
|
|
168
223
|
agent_session_id: z.string().optional().describe("Registered agent session to disconnect."),
|
|
169
224
|
}, async ({ room_id, agent_session_id }) => {
|
|
225
|
+
if (requireValidWorkerBearerRuntime().mode === "supervised") {
|
|
226
|
+
return {
|
|
227
|
+
content: [{
|
|
228
|
+
type: "text",
|
|
229
|
+
text: JSON.stringify({
|
|
230
|
+
success: false,
|
|
231
|
+
error: "supervised_bounded_mode",
|
|
232
|
+
message: "Agent-session disconnection is owned by the desktop supervisor during a bounded turn.",
|
|
233
|
+
}, null, 2),
|
|
234
|
+
}],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
170
237
|
let targetRoomId = getTargetRoomId(room_id);
|
|
171
238
|
const localSession = agent_session_id
|
|
172
239
|
? getStoredAgentSession(agent_session_id)
|
|
@@ -7,8 +7,9 @@ export function registerStatusTools(server) {
|
|
|
7
7
|
registerPostStatusTool(server);
|
|
8
8
|
registerPostReasoningTool(server);
|
|
9
9
|
}
|
|
10
|
-
export function registerMessageTools(server) {
|
|
10
|
+
export function registerMessageTools(server, options = {}) {
|
|
11
11
|
registerSendMessageTool(server);
|
|
12
12
|
registerReadMessagesTool(server);
|
|
13
|
-
|
|
13
|
+
if (options.includeDeliveryLoop !== false)
|
|
14
|
+
registerWaitForMessagesTool(server);
|
|
14
15
|
}
|
|
@@ -112,6 +112,9 @@ async function sendMessageFromTool(input) {
|
|
|
112
112
|
});
|
|
113
113
|
touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
|
|
114
114
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity), agentSession);
|
|
115
|
+
// The `replied` receipt transition is server-owned: message creation marks
|
|
116
|
+
// the publisher's receipt on the reply target atomically with the reply
|
|
117
|
+
// itself, for MCP workers and supervised daemon publications alike.
|
|
115
118
|
return jsonToolResponse({
|
|
116
119
|
...message,
|
|
117
120
|
agent_identity: toPublicAgentIdentity(identity),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getPollTimeoutCapMs } from "../../../../shared/poll-timeout-cap.js";
|
|
3
3
|
import { encodeRoomIdPath } from "../../../room-id.js";
|
|
4
|
-
import { appendIncludePromptOnly, buildAgentDeliveryHeaders, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getLocalChatMessages, getLastMessageId, getRememberedRoomPresence, getTargetRoomId, identityFromAgentSession, isLocalRoomStorageEnabled, listLocalTasks, resolveLocalRoomStorageIdentifiers, resolveAgentSession, roomScopedApiCall, syncRoomPresence, toAgentReadableMessages, touchRoomSession, WORKER_BEARER_AGENT_SESSION_ID, waitForLocalChatMessages, } from "../../runtime.js";
|
|
5
|
-
import { requireValidWorkerBearerRuntime } from "../../runtime/worker-bearer.js";
|
|
4
|
+
import { agentSessionCredentials, appendIncludePromptOnly, buildAgentDeliveryHeaders, bindSupervisedWorkerSession, scheduleSupervisedWorkerCursorCheckpoint, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getLocalChatMessages, getLastMessageId, getRememberedRoomPresence, getTargetRoomId, identityFromAgentSession, isLocalRoomStorageEnabled, listLocalTasks, resolveLocalRoomStorageIdentifiers, resolveAgentSession, roomScopedApiCall, syncRoomPresence, toAgentReadableMessages, touchRoomSession, WORKER_BEARER_AGENT_SESSION_ID, waitForLocalChatMessages, } from "../../runtime.js";
|
|
5
|
+
import { requireValidWorkerBearerRuntime, supervisedBoundedDeliveryDisabledToolResult, } from "../../runtime/worker-bearer.js";
|
|
6
6
|
import { attachAgentMessageActivations } from "../../../../shared/activation-routing.js";
|
|
7
7
|
import { findLocalMessageById, findRemoteMessageById } from "./message-lookup.js";
|
|
8
8
|
import { fetchRecentRemoteMessages } from "./read-tool.js";
|
|
@@ -235,7 +235,7 @@ export async function collectThreadContextMessages(input) {
|
|
|
235
235
|
return contextMessages;
|
|
236
236
|
}
|
|
237
237
|
export function registerWaitForMessagesTool(server) {
|
|
238
|
-
server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat room (HTTP long-poll). Messages labeled activation.for_current_agent.decision=\"silent\" are skipped for the current worker; when last_observed_message_id is present, use it as cursor progress even if messages is empty. Threaded replies include thread_parent_id/thread.root_message_id; use send_thread_message with that id to keep focused side discussion out of the main room. For multi-hour runs, call in a loop: always pass after_message_id from the last message you processed or last_observed_message_id so an empty result means 'nothing new yet', not 'stop working'.
|
|
238
|
+
server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat room (HTTP long-poll). Messages labeled activation.for_current_agent.decision=\"silent\" are skipped for the current worker; when last_observed_message_id is present, use it as cursor progress even if messages is empty. Threaded replies include thread_parent_id/thread.root_message_id; use send_thread_message with that id to keep focused side discussion out of the main room. For multi-hour runs, call in a loop: always pass after_message_id from the last message you processed or last_observed_message_id so an empty result means 'nothing new yet', not 'stop working'. Legacy/manual room workers may send a brief continue instruction when another legacy/manual participant posts a premature 'I will wait' closing line. Never send that nudge to or about a daemon-supervised participant, and daemon-supervised workers must not emit it; their supervisor owns wake and retry. Per-call wait is capped (default max 180s unless LETAGENTS_POLL_MAX_MS is set on API and MCP).", {
|
|
239
239
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
|
|
240
240
|
after_message_id: z
|
|
241
241
|
.string()
|
|
@@ -250,12 +250,32 @@ export function registerWaitForMessagesTool(server) {
|
|
|
250
250
|
.optional()
|
|
251
251
|
.describe("Registered agent session to use. Without this, the MCP transport is treated as controller traffic and is hidden from connected-agent activity."),
|
|
252
252
|
}, async ({ room_id, after_message_id, timeout, agent_session_id }) => {
|
|
253
|
+
// This guard intentionally runs before room resolution, identity setup,
|
|
254
|
+
// local SQLite reads, presence writes, or HTTP traffic. In supervised
|
|
255
|
+
// bounded-turn mode the daemon is the sole inbox owner.
|
|
256
|
+
const boundedDeliveryDisabled = supervisedBoundedDeliveryDisabledToolResult();
|
|
257
|
+
if (boundedDeliveryDisabled) {
|
|
258
|
+
return jsonToolResponse(boundedDeliveryDisabled);
|
|
259
|
+
}
|
|
253
260
|
const targetRoomId = getTargetRoomId(room_id);
|
|
254
261
|
const targetProjectId = getFallbackProjectId();
|
|
255
262
|
const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
|
|
256
263
|
const identity = await ensureAgentIdentity();
|
|
257
264
|
const sessionRoomId = targetRoomId ?? currentRoom?.room_id ?? localRoomId ?? null;
|
|
258
265
|
const agentSession = resolveWaitAgentSession(sessionRoomId, agent_session_id);
|
|
266
|
+
if (agentSession) {
|
|
267
|
+
// Registration (or a successor generation) must bind strictly once.
|
|
268
|
+
// Later waits use a read-only exact verification capped at 250ms, so a
|
|
269
|
+
// wedged daemon cannot consume the room-poll budget and an old worker
|
|
270
|
+
// cannot read after a successor generation takes ownership.
|
|
271
|
+
await bindSupervisedWorkerSession(agentSession, process.env, { allowConfirmedFastPath: true });
|
|
272
|
+
// A cursor is acknowledged only when the worker explicitly uses it to
|
|
273
|
+
// request the next page. Persisting a cursor from the response we are
|
|
274
|
+
// still constructing could skip a message if serialization, presence,
|
|
275
|
+
// or the provider turn fails afterward.
|
|
276
|
+
if (after_message_id)
|
|
277
|
+
scheduleSupervisedWorkerCursorCheckpoint(agentSession, after_message_id);
|
|
278
|
+
}
|
|
259
279
|
const maxPollMs = getPollTimeoutCapMs();
|
|
260
280
|
const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), maxPollMs);
|
|
261
281
|
if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
|
|
@@ -289,7 +309,8 @@ export function registerWaitForMessagesTool(server) {
|
|
|
289
309
|
includeTaskOwnerLeases: !replayingExistingMessages,
|
|
290
310
|
});
|
|
291
311
|
const routing = filterSilentActivationMessages(messages);
|
|
292
|
-
|
|
312
|
+
const observedCursor = routing.last_observed_message_id ?? getLastMessageId(result);
|
|
313
|
+
touchRoomSession(effectiveLocalRoomId, observedCursor);
|
|
293
314
|
const threadContext = await collectThreadContextMessages({
|
|
294
315
|
messages: routing.messages,
|
|
295
316
|
localRoomId: effectiveLocalRoomId,
|
|
@@ -395,7 +416,37 @@ export function registerWaitForMessagesTool(server) {
|
|
|
395
416
|
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
396
417
|
}
|
|
397
418
|
if (targetRoomId) {
|
|
398
|
-
|
|
419
|
+
const observedCursor = routing.last_observed_message_id ?? getLastMessageId(output);
|
|
420
|
+
touchRoomSession(targetRoomId, observedCursor);
|
|
421
|
+
if (allMessages.length > 0 && agentSession) {
|
|
422
|
+
const firstMsg = allMessages[0];
|
|
423
|
+
const lastMsg = allMessages[allMessages.length - 1];
|
|
424
|
+
if (typeof firstMsg?.id === "string" && typeof lastMsg?.id === "string") {
|
|
425
|
+
try {
|
|
426
|
+
await roomScopedApiCall({
|
|
427
|
+
room_id: targetRoomId,
|
|
428
|
+
project_id: targetProjectId,
|
|
429
|
+
room_path: (r) => `/rooms/${encodeRoomIdPath(r)}/agents/self/observation`,
|
|
430
|
+
project_path: (p) => `/projects/${encodeURIComponent(p)}/agents/self/observation`,
|
|
431
|
+
options: {
|
|
432
|
+
method: "PUT",
|
|
433
|
+
body: JSON.stringify({
|
|
434
|
+
first_message_id: firstMsg.id,
|
|
435
|
+
last_message_id: lastMsg.id,
|
|
436
|
+
...agentSessionCredentials(agentSession),
|
|
437
|
+
}),
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Non-blocking telemetry
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Delivery alone is observation evidence (the span above), never a
|
|
447
|
+
// "responding" receipt: an agent that ignores an activation must not
|
|
448
|
+
// present as responding. Receipts advance only on real transitions —
|
|
449
|
+
// send-tool marks "replied" when the agent actually publishes a reply.
|
|
399
450
|
}
|
|
400
451
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
|
|
401
452
|
return jsonToolResponse(output);
|
|
@@ -56,16 +56,17 @@ export function registerGetOnboardingStatusTool(server) {
|
|
|
56
56
|
if (workerRuntime.mode === "invalid") {
|
|
57
57
|
return jsonTextResponse({ success: false, error: "worker_bearer_configuration_invalid", message: workerRuntime.error });
|
|
58
58
|
}
|
|
59
|
-
if (workerRuntime.mode === "worker") {
|
|
59
|
+
if (workerRuntime.mode === "worker" || workerRuntime.mode === "supervised") {
|
|
60
60
|
return jsonTextResponse({
|
|
61
61
|
api_url: API_URL,
|
|
62
|
-
worker_bearer_mode:
|
|
62
|
+
worker_bearer_mode: workerRuntime.mode === "worker",
|
|
63
|
+
supervised_bounded_mode: workerRuntime.mode === "supervised",
|
|
63
64
|
authenticated: true,
|
|
64
|
-
auth_source: "worker_bearer",
|
|
65
|
+
auth_source: workerRuntime.mode === "worker" ? "worker_bearer" : "daemon_supervised",
|
|
65
66
|
account: null,
|
|
66
67
|
pending_device_auth: null,
|
|
67
68
|
next_step: "join_room",
|
|
68
|
-
note: "Owner-auth onboarding and saved-auth state are disabled in worker
|
|
69
|
+
note: "Owner-auth onboarding and saved-auth state are disabled in worker credential mode.",
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
72
|
const workingDir = cwd || process.cwd();
|