letagents 0.12.25 → 0.12.27

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.
@@ -7,7 +7,7 @@ import { durableCompletionResult, supervisedToolIsMutation } from "./supervised-
7
7
  const handlersByProvider = new Map();
8
8
  const SUPERVISED_PROVIDERS = new Set(["claude-code", "cursor", "codex", "open-model"]);
9
9
  const WORKSPACE_SCOPED_TOOLS = new Set(["check_repo", "check_repo_visibility", "initialize_repo"]);
10
- function handlersForProvider(provider) {
10
+ function handlersForProvider(provider, apiUrl) {
11
11
  const normalizedProvider = provider.trim().toLowerCase();
12
12
  const existing = handlersByProvider.get(normalizedProvider);
13
13
  if (existing)
@@ -26,7 +26,7 @@ function handlersForProvider(provider) {
26
26
  return {};
27
27
  },
28
28
  };
29
- registerTools(recorder, "supervised_room_turn", normalizedProvider || null, { executionOwner: "daemon" });
29
+ registerTools(recorder, "supervised_room_turn", normalizedProvider || null, { executionOwner: "daemon", apiUrl });
30
30
  handlersByProvider.set(normalizedProvider, handlers);
31
31
  return handlers;
32
32
  }
@@ -73,7 +73,7 @@ function validateExecutionContext(input) {
73
73
  */
74
74
  export async function executeDaemonTool(input) {
75
75
  const context = validateExecutionContext(input);
76
- const tool = handlersForProvider(context.provider).get(context.toolName);
76
+ const tool = handlersForProvider(context.provider, context.apiUrl).get(context.toolName);
77
77
  if (!tool)
78
78
  throw new Error(`Unsupported supervised tool: ${context.toolName}`);
79
79
  const parsedInput = tool.inputSchema
@@ -1,3 +1,5 @@
1
+ import { localSupervisedRoomToolAvailable } from "../../../shared/local-supervised-tools.mjs";
2
+ import { isLocalRoomApi } from "../../../shared/room-api-origin.mjs";
1
3
  import { registerRoomKnowledgeTools } from "./tools/knowledge.js";
2
4
  import { registerAgentSessionTools } from "./tools/agent-sessions.js";
3
5
  import { registerMessageTools, registerStatusTools } from "./tools/messages.js";
@@ -5,15 +7,31 @@ import { registerOnboardingTools } from "./tools/onboarding.js";
5
7
  import { registerRentalTools } from "./tools/rental.js";
6
8
  import { registerRepoInitializationTool, registerRepoVisibilityTool, registerRoomInspectionTools, registerRoomJoinTools, registerRoomResumeTool, } from "./tools/rooms.js";
7
9
  import { registerTaskTools } from "./tools/tasks.js";
8
- import { registerSupervisedRoomTurnTools } from "./tools/supervised-room-turn.js";
10
+ import { registerReplyThreadTool, registerSupervisedRoomTurnTools } from "./tools/supervised-room-turn.js";
9
11
  import { toolSurfaceForExecutionProfile } from "./runtime/tool-surface-policy.js";
10
12
  import { profileAwareToolServer } from "./supervised-tool-facade.js";
11
13
  import { workerAwareToolServer } from "./worker-tool-facade.js";
12
14
  import { registerWorkspaceTools } from "./tools/workspace.js";
13
15
  export function registerTools(server, profile = "autonomous_mcp_worker", supervisedProvider = process.env.LETAGENTS_SUPERVISOR_PROVIDER?.trim() || null, options = {}) {
16
+ const localRoom = profile === "supervised_room_turn" && isLocalRoomApi(options.apiUrl ?? process.env.LETAGENTS_API_URL);
17
+ const routedServer = localRoom ? new Proxy(server, {
18
+ get(target, property, receiver) {
19
+ const value = Reflect.get(target, property, receiver);
20
+ if (property !== "tool")
21
+ return typeof value === "function" ? value.bind(target) : value;
22
+ return (name, ...registration) => {
23
+ if (!localSupervisedRoomToolAvailable(name))
24
+ return undefined;
25
+ if (name === "update_task" && typeof registration[0] === "string") {
26
+ registration[0] += " In local rooms, close verified merged work with status 'done'; no close intent or replacement work lease is needed.";
27
+ }
28
+ return target.tool.call(target, name, ...registration);
29
+ };
30
+ },
31
+ }) : server;
14
32
  const profileTools = options.executionOwner === "daemon"
15
- ? server
16
- : profileAwareToolServer(server, profile, undefined, supervisedProvider);
33
+ ? routedServer
34
+ : profileAwareToolServer(routedServer, profile, undefined, supervisedProvider);
17
35
  const tools = profile === "autonomous_mcp_worker" || profile === "interactive_desktop"
18
36
  ? workerAwareToolServer(profileTools) : profileTools;
19
37
  const surface = toolSurfaceForExecutionProfile(profile);
@@ -28,6 +46,8 @@ export function registerTools(server, profile = "autonomous_mcp_worker", supervi
28
46
  registerTaskTools(tools);
29
47
  registerRepoInitializationTool(tools);
30
48
  registerMessageTools(tools, { includeDeliveryLoop: surface.deliveryLoop });
49
+ if (profile === "supervised_room_turn")
50
+ registerReplyThreadTool(tools);
31
51
  if (profile === "supervised_room_turn" && supervisedProvider === "cursor")
32
52
  registerSupervisedRoomTurnTools(tools);
33
53
  if (surface.onboarding)
@@ -4,7 +4,7 @@ export const LETAGENTS_RUNTIME_CONTRACT_ARG = "--letagents-runtime-contract";
4
4
  * Discover through the production registration path rather than maintaining a
5
5
  * second capability list that can drift from the MCP server.
6
6
  */
7
- export function registeredToolNames(profile, supervisedProvider = null) {
7
+ export function registeredToolNames(profile, supervisedProvider = null, apiUrl = process.env.LETAGENTS_API_URL) {
8
8
  const names = new Set();
9
9
  const recorder = {
10
10
  tool(name) {
@@ -12,16 +12,16 @@ export function registeredToolNames(profile, supervisedProvider = null) {
12
12
  return {};
13
13
  },
14
14
  };
15
- registerTools(recorder, profile, supervisedProvider);
15
+ registerTools(recorder, profile, supervisedProvider, { apiUrl });
16
16
  return [...names].sort();
17
17
  }
18
- export function letAgentsRuntimeContract() {
18
+ export function letAgentsRuntimeContract(apiUrl = process.env.LETAGENTS_API_URL) {
19
19
  return {
20
20
  format: 1,
21
21
  profiles: {
22
- supervised_mcp_polling: { contract: "custodial_polling_v1", tools: registeredToolNames("supervised_mcp_polling", "codex") },
22
+ supervised_mcp_polling: { contract: "custodial_polling_v1", tools: registeredToolNames("supervised_mcp_polling", "codex", apiUrl) },
23
23
  cursor_supervised_room_turn: {
24
- tools: registeredToolNames("supervised_room_turn", "cursor"),
24
+ tools: registeredToolNames("supervised_room_turn", "cursor", apiUrl),
25
25
  },
26
26
  },
27
27
  };
@@ -1,5 +1,12 @@
1
1
  import { z } from "zod";
2
2
  const MAX_ROOM_REPLY_BYTES = 32 * 1024;
3
+ /** Select routing only; the daemon remains the sole final-answer publisher. */
4
+ export function registerReplyThreadTool(server) {
5
+ const tool = server.tool("set_reply_thread", "Place your final answer in the activating room message's thread. Takes no arguments, sends no message, and does not finish the turn. Continue working and use your normal final completion when ready. Unavailable for synthetic task continuations or corrections.", {}, async () => { throw new Error("set_reply_thread requires the active daemon-owned room turn."); });
6
+ // The legacy raw-shape overload strips unknown keys. Preserve a strict schema
7
+ // so a caller cannot mistake ignored room/recipient/text arguments for authority.
8
+ tool.inputSchema = z.object({}).strict();
9
+ }
3
10
  function completionResponse(outcome) {
4
11
  const payload = {
5
12
  accepted: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.12.25",
3
+ "version": "0.12.27",
4
4
  "description": "Let Agents Chat \u2014 MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",
@@ -0,0 +1,3 @@
1
+ export function localSupervisedRoomToolOperation(name: string): string | null;
2
+ export function localSupervisedRoomToolAvailable(name: string): boolean;
3
+ export function defineLocalSupervisedToolHandlers<T>(handlers: Record<string, (context: T) => Promise<unknown>>): Readonly<Record<string, (context: T) => Promise<unknown>>>;
@@ -0,0 +1,31 @@
1
+ // One catalog for supervised local discovery and storage dispatch. Aliases share
2
+ // an operation; handler completeness is checked when the local executor loads.
3
+ const operations = Object.freeze({
4
+ get_current_room: "get_current_room", read_messages: "read_messages",
5
+ send_message: "send_message", send_thread_message: "send_message",
6
+ post_status: "send_message", post_reasoning: "send_message",
7
+ get_board: "get_board", create_task: "add_task", add_task: "add_task",
8
+ claim_task: "claim_task", complete_task: "update_task", update_task: "update_task",
9
+ release_task_lease: "change_task_lease", handoff_task_lease: "change_task_lease",
10
+ claim_task_review: "claim_task_review", get_room_artifacts: "get_room_artifacts",
11
+ publish_room_artifact: "publish_room_artifact", get_message_thread: "get_message_thread",
12
+ });
13
+
14
+ export function localSupervisedRoomToolOperation(name) {
15
+ return Object.hasOwn(operations, name) ? operations[name] : null;
16
+ }
17
+
18
+ export function localSupervisedRoomToolAvailable(name) {
19
+ // Turn controls are handled by the daemon. Local room moves are unsupported.
20
+ return name === "complete_room_turn" || name === "set_reply_thread" || localSupervisedRoomToolOperation(name) !== null;
21
+ }
22
+
23
+ export function defineLocalSupervisedToolHandlers(handlers) {
24
+ const required = new Set(Object.values(operations));
25
+ if (Object.keys(handlers).length !== required.size
26
+ || Object.keys(handlers).some(name => !required.has(name))
27
+ || [...required].some(name => typeof handlers[name] !== "function")) {
28
+ throw new Error("Local room handlers do not match the advertised tool contract.");
29
+ }
30
+ return Object.freeze(handlers);
31
+ }