@letta-ai/letta-code 0.34.27 → 0.34.28

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/letta.js CHANGED
@@ -6044,7 +6044,7 @@ var package_default;
6044
6044
  var init_package = __esm(() => {
6045
6045
  package_default = {
6046
6046
  name: "@letta-ai/letta-code",
6047
- version: "0.34.27",
6047
+ version: "0.34.28",
6048
6048
  lettaStartupLogProtocol: 1,
6049
6049
  lettaConfigDirProtocol: 1,
6050
6050
  description: "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
@@ -168979,7 +168979,7 @@ async function resolveSlackMessageFiles(params) {
168979
168979
  oldest: params.messageTs,
168980
168980
  latest: params.messageTs,
168981
168981
  inclusive: true,
168982
- limit: 1
168982
+ limit: 2
168983
168983
  }));
168984
168984
  const message2 = messages2.find((entry) => entry.ts === params.messageTs);
168985
168985
  return message2 ? collectSlackFiles(message2) : null;
@@ -211476,7 +211476,7 @@ var init_mcp_client = __esm(() => {
211476
211476
  init_streamableHttp();
211477
211477
  DEFAULT_CLIENT_INFO = {
211478
211478
  name: "letta-code",
211479
- version: "0.34.27"
211479
+ version: "0.34.28"
211480
211480
  };
211481
211481
  });
211482
211482
 
@@ -215530,7 +215530,7 @@ Important:
215530
215530
  var init_Bash = () => {};
215531
215531
 
215532
215532
  // src/tools/descriptions/CodeMode.md
215533
- var CodeMode_default = "Run a JavaScript async function body that calls tools and returns only the results you need.\n\nCall built-in tools with `await tools.<name>(args)` and MCP tools with `await tools.mcp__server__tool(args)`; `listTools()` and `describeTool(name)` list tools and their schemas. Failed calls throw. `args` holds optional input, and `console.log()` output is captured.\n\nReturns `{ result, output }`. Like Bash, a script still running after a brief wait continues in the background and its result arrives as a task notification; `run_in_background: true` returns the task ID immediately. TaskStop aborts it.\n\nNested calls follow permission rules; calls that need approval fail, so call those tools directly. This is not a security sandbox.\n\nExample:\n\n const texts = await Promise.all(args.paths.map(file_path => tools.Read({ file_path })));\n return texts.map((text, i) => ({ path: args.paths[i], lines: text.split('\\n').length }));\n";
215533
+ var CodeMode_default = "Run a JavaScript async function body that calls tools and returns only the results you need.\n\nCall built-in and server-side tools with `await tools.<name>(args)` and MCP tools with `await tools.mcp__server__tool(args)`; `listTools()` and `describeTool(name)` list tools and their schemas. Failed calls throw. `args` holds optional input, and `console.log()` output is captured.\n\nReturns `{ result, output }`. Like Bash, a script still running after a brief wait continues in the background and its result arrives as a task notification; `run_in_background: true` returns the task ID immediately. TaskStop aborts it.\n\nNested calls follow permission rules; calls that need approval fail, so call those tools directly. This is not a security sandbox.\n\nExample:\n\n const texts = await Promise.all(args.paths.map(file_path => tools.Read({ file_path })));\n return texts.map((text, i) => ({ path: args.paths[i], lines: text.split('\\n').length }));\n";
215534
215534
  var init_CodeMode = () => {};
215535
215535
 
215536
215536
  // src/tools/descriptions/Edit.md
@@ -215634,7 +215634,7 @@ Do not use this tool for root memory files (already loaded) or skills/ (use Skil
215634
215634
  var init_Memory = () => {};
215635
215635
 
215636
215636
  // src/tools/descriptions/Monitor.md
215637
- var Monitor_default = 'Start a background monitor that streams events from a long-running script. Each stdout line is an event — you keep working and notifications arrive in the chat. Events arrive on their own schedule and are not replies from the user, even if one lands while you\'re waiting for the user to answer a question.\n\nPick by how many notifications you need:\n- **One** ("tell me when the server is ready / the build finishes") → use **Bash with `run_in_background`** and a command that exits when the condition is true, e.g. `until grep -q "Ready in" dev.log; do sleep 0.5; done`. You get a single completion notification when it exits.\n- **One per occurrence, indefinitely** ("tell me every time an ERROR line appears") → Monitor with an unbounded command (`tail -f`, `inotifywait -m`, `while true`).\n- **One per occurrence, until a known end** ("emit each CI step result, stop when the run completes") → Monitor with a command that emits lines and then exits.\n\nYour script\'s stdout is the event stream. Each line becomes a notification. Exit ends the watch.\n # Each matching log line is an event\n tail -f /var/log/app.log | grep --line-buffered "ERROR"\n # Each file change is an event\n inotifywait -m --format \'%e %f\' /watched/dir\n # Node script that emits events as they arrive (e.g. WebSocket listener)\n node watch-for-events.js\n\nFor open github.com pull requests, use WatchPR instead. It covers the whole PR surface and keeps watching after CI finishes. Use Monitor only when WatchPR does not apply.\n\n**Don\'t use an unbounded command for a single notification.** `tail -f`, `inotifywait -m`, and `while true` never exit on their own, so the monitor stays armed until timeout even after the event has fired. For "tell me when X is ready," use Bash `run_in_background` with an `until` loop instead (one notification, ends in seconds). Note that `tail -f log | grep -m 1 ...` does *not* fix this: if the log goes quiet after the match, `tail` never receives SIGPIPE and the pipeline hangs anyway.\n\n- Every pipe stage must flush per line or matches sit in its buffer unseen: `grep` needs `--line-buffered`, `awk` needs `fflush()`. `head` cannot flush at all — `| head -N` delivers nothing until N matches accumulate, then ends the stream.\n- In poll loops, handle transient failures (`curl ... || true`) — one failed request shouldn\'t kill the monitor.\n- Poll intervals: 30s+ for remote APIs (rate limits), 0.5–1s for local checks.\n- Write a specific `description` — it appears in every notification ("errors in deploy.log" not "watching logs").\n- Only stdout is the event stream. Stderr goes to the output file (readable via Read) but does not trigger notifications — for a command you run directly (e.g. `python train.py 2>&1 | grep --line-buffered ...`), merge stderr with `2>&1` so its failures reach your filter. (No effect on `tail -f` of an existing log — that file only contains what its writer redirected.)\n\n**Coverage — silence is not success.** When watching a job or process for an outcome, your filter must match every terminal state, not just the happy path. A monitor that greps only for the success marker stays silent through a crashloop, a hung process, or an unexpected exit — and silence looks identical to "still running." Before arming, ask: *if this process crashed right now, would my filter emit anything?* If not, widen it.\n # Wrong — silent on crash, hang, or any non-success exit\n tail -f run.log | grep --line-buffered "elapsed_steps="\n # Right — one alternation covering progress + the failure signatures you\'d act on\n tail -f run.log | grep -E --line-buffered "elapsed_steps=|Traceback|Error|FAILED|assert|Killed|OOM"\n\nFor poll loops checking job state, emit on every terminal status (`succeeded|failed|cancelled|timeout`), not just success. If you cannot confidently enumerate the failure signatures, broaden the grep alternation rather than narrow it — some extra noise is better than missing a crashloop.\n\n**Output volume**: Every stdout line is a conversation message, so the filter should be selective — but selective means "the lines you\'d act on," not "only good news." Never pipe raw logs; filter to exactly the success and failure signals you care about. Monitors that produce too many events are automatically stopped; restart with a tighter filter if this happens.\n\nStdout lines within 200ms are batched into a single notification, so multiline output from a single event groups naturally.\n\nThe script runs in the same shell environment as Bash. Exit ends the watch (exit code is reported). Timeout → killed. Set `persistent: true` for session-length watches (log tails, long-lived event streams). In managed Cloud sandboxes, every monitor stops after at most 1 hour, including persistent monitors; elsewhere persistent monitors run until TaskStop or session end. For longer Cloud waits, schedule a Wake or cron (which does not keep the sandbox running between turns) and combine it with shorter monitors. Use TaskStop to cancel early.\n\nWhen someone is waiting on the result, set `backup_after_seconds` (e.g. 900). If the monitor goes that long without an event, a durable Wake brings you back to check the real state, so a queued, hung, or dead watch can\'t leave them waiting in silence.\n\n**ws source** — open a WebSocket and stream each incoming text frame as an event. No shell, no polling: the server pushes, you get notified.\n ws: {url: \'wss://events.example.com/stream\', protocols: [\'v1\']},\n description: \'deploy events\',\n\nEach text frame becomes one notification (multiline frames stay as one event). Binary frames are reported as `[binary frame, N bytes]` rather than passed through. Socket close ends the watch with the close code surfaced; errors are surfaced before close. Same rate limiting as bash — a firehose will be suppressed and eventually stopped, so subscribe to a filtered feed where one exists.\n\nPrefer this over `command: \'websocat wss://…\'` — it avoids the extra process and line-buffering pitfalls. Use bash when you need to transform or filter frames with shell tools before they become events.\n';
215637
+ var Monitor_default = 'Start a background monitor that streams events from a long-running script. Each stdout line is an event — you keep working and notifications arrive in the chat. Events arrive on their own schedule and are not replies from the user, even if one lands while you\'re waiting for the user to answer a question.\n\nPick by how many notifications you need:\n- **One** ("tell me when the server is ready / the build finishes") → use **Bash with `run_in_background`** and a command that exits when the condition is true, e.g. `until grep -q "Ready in" dev.log; do sleep 0.5; done`. You get a single completion notification when it exits.\n- **One per occurrence, indefinitely** ("tell me every time an ERROR line appears") → Monitor with an unbounded command (`tail -f`, `inotifywait -m`, `while true`).\n- **One per occurrence, until a known end** ("emit each CI step result, stop when the run completes") → Monitor with a command that emits lines and then exits.\n\nYour script\'s stdout is the event stream. Each line becomes a notification. Exit ends the watch.\n # Each matching log line is an event\n tail -f /var/log/app.log | grep --line-buffered "ERROR"\n # Each file change is an event\n inotifywait -m --format \'%e %f\' /watched/dir\n # Node script that emits events as they arrive (e.g. WebSocket listener)\n node watch-for-events.js\n\nFor open github.com pull requests, use WatchPR instead. It covers the whole PR surface and keeps watching after CI finishes. Use Monitor only when WatchPR does not apply.\n\n**Don\'t use an unbounded command for a single notification.** `tail -f`, `inotifywait -m`, and `while true` never exit on their own, so the monitor stays armed until timeout even after the event has fired. For "tell me when X is ready," use Bash `run_in_background` with an `until` loop instead (one notification, ends in seconds). Note that `tail -f log | grep -m 1 ...` does *not* fix this: if the log goes quiet after the match, `tail` never receives SIGPIPE and the pipeline hangs anyway.\n\n- Every pipe stage must flush per line or matches sit in its buffer unseen: `grep` needs `--line-buffered`, `awk` needs `fflush()`. `head` cannot flush at all — `| head -N` delivers nothing until N matches accumulate, then ends the stream.\n- In poll loops, handle transient failures (`curl ... || true`) — one failed request shouldn\'t kill the monitor.\n- Poll intervals: 30s+ for remote APIs (rate limits), 0.5–1s for local checks.\n- Write a specific `description` — it appears in every notification ("errors in deploy.log" not "watching logs").\n- Only stdout is the event stream. Stderr goes to the output file (readable via Read) but does not trigger notifications — for a command you run directly (e.g. `python train.py 2>&1 | grep --line-buffered ...`), merge stderr with `2>&1` so its failures reach your filter. (No effect on `tail -f` of an existing log — that file only contains what its writer redirected.)\n\n**Coverage — silence is not success.** When watching a job or process for an outcome, your filter must match every terminal state, not just the happy path. A monitor that greps only for the success marker stays silent through a crashloop, a hung process, or an unexpected exit — and silence looks identical to "still running." Before arming, ask: *if this process crashed right now, would my filter emit anything?* If not, widen it.\n # Wrong — silent on crash, hang, or any non-success exit\n tail -f run.log | grep --line-buffered "elapsed_steps="\n # Right — one alternation covering progress + the failure signatures you\'d act on\n tail -f run.log | grep -E --line-buffered "elapsed_steps=|Traceback|Error|FAILED|assert|Killed|OOM"\n\nFor poll loops checking job state, emit on every terminal status (`succeeded|failed|cancelled|timeout`), not just success. If you cannot confidently enumerate the failure signatures, broaden the grep alternation rather than narrow it — some extra noise is better than missing a crashloop.\n\n**Output volume**: Every stdout line is a conversation message, so the filter should be selective — but selective means "the lines you\'d act on," not "only good news." Never pipe raw logs; filter to exactly the success and failure signals you care about. Monitors that produce too many events are automatically stopped; restart with a tighter filter if this happens.\n\nStdout lines within 200ms are batched into a single notification, so multiline output from a single event groups naturally.\n\nThe script runs in the same shell environment as Bash. Exit ends the watch (exit code is reported). Timeout → killed. Set `persistent: true` for session-length watches (log tails, long-lived event streams). In managed Cloud sandboxes, every monitor stops after at most 1 hour, including persistent monitors; elsewhere persistent monitors run until TaskStop or session end. For longer Cloud waits, schedule a Wake or cron (which does not keep the sandbox running between turns) and combine it with shorter monitors. Use TaskStop to cancel early.\n\nWhen someone is waiting on the result, set `backup_after_seconds` (e.g. 900). If the monitor goes that long without an event, a durable Wake brings you back to check the real state, so a queued, hung, or dead watch can\'t leave them waiting in silence.\n\n**ws source** — open a WebSocket and stream each incoming text frame as an event. No shell, no polling: the server pushes, you get notified.\n ws: {url: \'wss://events.example.com/stream\', protocols: [\'v1\']},\n description: \'deploy events\',\n\nEach text frame becomes one notification (multiline frames stay as one event). Binary frames are reported as `[binary frame, N bytes]` rather than passed through. Socket close ends the watch with the close code surfaced; errors are surfaced before close. Same rate limiting as bash — a firehose will be suppressed and eventually stopped, so subscribe to a filtered feed where one exists.\n\nPrefer this over `command: \'websocat wss://…\'` — it avoids the extra process and line-buffering pitfalls. Use bash when you need to transform or filter frames with shell tools before they become events.\n\n**script source** — a JavaScript watch loop with the same globals as CodeMode, plus `emit(text)` (one event) and `sleep(ms)`. Returning or throwing ends the watch.\n';
215638
215638
  var init_Monitor = () => {};
215639
215639
 
215640
215640
  // src/tools/descriptions/Read.md
@@ -224201,6 +224201,34 @@ var init_external_tool_background_eligibility = __esm(() => {
224201
224201
  listenerRegistrations = new WeakMap;
224202
224202
  });
224203
224203
 
224204
+ // src/tools/tool-context-retention.ts
224205
+ function retainToolContext(contextId) {
224206
+ holders.set(contextId, (holders.get(contextId) ?? 0) + 1);
224207
+ }
224208
+ function releaseRetainedToolContext(contextId) {
224209
+ const count = (holders.get(contextId) ?? 0) - 1;
224210
+ if (count > 0) {
224211
+ holders.set(contextId, count);
224212
+ return false;
224213
+ }
224214
+ holders.delete(contextId);
224215
+ return deferredReleases.delete(contextId);
224216
+ }
224217
+ function isToolContextRetained(contextId) {
224218
+ return holders.has(contextId);
224219
+ }
224220
+ function deferToolContextRelease(contextId) {
224221
+ if (!holders.has(contextId))
224222
+ return false;
224223
+ deferredReleases.add(contextId);
224224
+ return true;
224225
+ }
224226
+ var holders, deferredReleases;
224227
+ var init_tool_context_retention = __esm(() => {
224228
+ holders = new Map;
224229
+ deferredReleases = new Set;
224230
+ });
224231
+
224204
224232
  // src/tools/external-tool-content.ts
224205
224233
  function normalizeExternalToolResultContent(content) {
224206
224234
  const normalized = content.flatMap((part) => {
@@ -231005,12 +231033,11 @@ function saveExecutionContext(snapshot) {
231005
231033
  const contexts = getExecutionContexts();
231006
231034
  const contextId = `ctx-${Date.now()}-${toolExecutionContextCounter++}`;
231007
231035
  contexts.set(contextId, snapshot);
231008
- const MAX_CONTEXTS = 4096;
231009
- if (contexts.size > MAX_CONTEXTS) {
231010
- const oldestContextId = contexts.keys().next().value;
231011
- if (oldestContextId) {
231012
- contexts.delete(oldestContextId);
231013
- }
231036
+ for (const id2 of contexts.keys()) {
231037
+ if (contexts.size <= 4096)
231038
+ break;
231039
+ if (!isToolContextRetained(id2))
231040
+ contexts.delete(id2);
231014
231041
  }
231015
231042
  return contextId;
231016
231043
  }
@@ -231056,7 +231083,8 @@ function clearCapturedToolExecutionContexts() {
231056
231083
  getExecutionContexts().clear();
231057
231084
  }
231058
231085
  function releaseToolExecutionContext(contextId) {
231059
- getExecutionContexts().delete(contextId);
231086
+ if (!deferToolContextRelease(contextId))
231087
+ getExecutionContexts().delete(contextId);
231060
231088
  }
231061
231089
  function acquireSwitchLock() {
231062
231090
  const lock = getSwitchLock();
@@ -232208,6 +232236,7 @@ var init_manager2 = __esm(async () => {
232208
232236
  init_telemetry();
232209
232237
  init_external_tool_background();
232210
232238
  init_external_tool_background_eligibility();
232239
+ init_tool_context_retention();
232211
232240
  init_checkout_readiness();
232212
232241
  init_debug();
232213
232242
  init_secrets_store();
@@ -234219,6 +234248,51 @@ var init_mcp = __esm(() => {
234219
234248
 
234220
234249
  // src/tools/script-tool-scope.ts
234221
234250
  import { randomUUID as randomUUID33 } from "node:crypto";
234251
+ async function loadServerTools(agentId, builtins) {
234252
+ const tools = new Map;
234253
+ if (!agentId || !getBackend().capabilities.serverSideToolManagement) {
234254
+ return tools;
234255
+ }
234256
+ try {
234257
+ const agent = await (await getClient()).agents.retrieve(agentId, {
234258
+ include: ["agent.tools"]
234259
+ });
234260
+ const approvalRules = new Set((agent.tool_rules ?? []).filter((rule) => rule.type === "requires_approval").map((rule) => rule.tool_name));
234261
+ for (const tool of agent.tools ?? []) {
234262
+ const name = tool.name;
234263
+ if (!name || tool.tool_type === "external_mcp")
234264
+ continue;
234265
+ if (builtins.has(name) || SCRIPT_EXCLUDED_TOOLS.has(name))
234266
+ continue;
234267
+ tools.set(name, {
234268
+ name,
234269
+ description: tool.description ?? "",
234270
+ inputSchema: tool.json_schema?.parameters ?? {},
234271
+ requiresApproval: tool.default_requires_approval === true || approvalRules.has(name)
234272
+ });
234273
+ }
234274
+ } catch {}
234275
+ return tools;
234276
+ }
234277
+ async function callServerTool(agentId, tool, args, signal) {
234278
+ if (!agentId)
234279
+ throw new Error(`${tool.name} requires an invoking agent.`);
234280
+ if (tool.requiresApproval) {
234281
+ throw new Error(`${tool.name} needs approval, which a script cannot request; call it directly.`);
234282
+ }
234283
+ const result2 = await (await getClient()).agents.tools.run(tool.name, { agent_id: agentId, args }, signal ? { signal } : {});
234284
+ const value = result2.status === "success" ? result2.func_return : result2.stderr?.join(`
234285
+ `) ?? result2.func_return;
234286
+ return {
234287
+ content: [
234288
+ {
234289
+ type: "text",
234290
+ text: typeof value === "string" ? value : JSON.stringify(value ?? null)
234291
+ }
234292
+ ],
234293
+ isError: result2.status !== "success"
234294
+ };
234295
+ }
234222
234296
  function isOwningAgentId(value) {
234223
234297
  return value?.startsWith("agent-") === true && !value.startsWith("agent-free:");
234224
234298
  }
@@ -234245,8 +234319,9 @@ async function createScriptToolScope(parentAgentId, signal) {
234245
234319
  const permissionCwd = getCurrentWorkingDirectory();
234246
234320
  const context3 = toolContextId ? manager.getExecutionContextById(toolContextId) : undefined;
234247
234321
  const builtinSchemas = (context3 ? serializeClientTools(context3.toolRegistry, context3.externalTools, context3.modTools, manager.getServerToolName) : []).filter((schema5) => !SCRIPT_EXCLUDED_TOOLS.has(schema5.name));
234248
- const builtinToolNames = builtinSchemas.map((schema5) => schema5.name);
234249
- const builtins = new Set(builtinToolNames);
234322
+ const builtins = new Set(builtinSchemas.map((schema5) => schema5.name));
234323
+ const serverTools = await loadServerTools(parentAgentId, builtins);
234324
+ const builtinToolNames = [...builtins, ...serverTools.keys()];
234250
234325
  let mcpSession;
234251
234326
  const mcp = () => {
234252
234327
  if (!parentAgentId)
@@ -234264,6 +234339,7 @@ async function createScriptToolScope(parentAgentId, signal) {
234264
234339
  description: schema5.description,
234265
234340
  inputSchema: schema5.parameters
234266
234341
  })),
234342
+ ...serverTools.values(),
234267
234343
  ...mcpTools.map((tool) => ({
234268
234344
  name: tool.name,
234269
234345
  description: tool.description ?? "",
@@ -234273,6 +234349,10 @@ async function createScriptToolScope(parentAgentId, signal) {
234273
234349
  },
234274
234350
  callTool: async (toolName2, toolArgs) => {
234275
234351
  signal?.throwIfAborted();
234352
+ const serverTool = serverTools.get(toolName2);
234353
+ if (serverTool) {
234354
+ return callServerTool(parentAgentId, serverTool, toolArgs, signal);
234355
+ }
234276
234356
  const toolCallId = `script-${randomUUID33()}`;
234277
234357
  const { decision } = await manager.checkToolPermission(toolName2, toolArgs, permissionCwd, permissionModeState && { ...permissionModeState }, parentAgentId ?? undefined, toolContextId, toolCallId);
234278
234358
  if (decision !== "allow") {
@@ -234303,12 +234383,14 @@ var SCRIPT_EXCLUDED_TOOLS;
234303
234383
  var init_script_tool_scope = __esm(() => {
234304
234384
  init_context();
234305
234385
  init_backend2();
234386
+ init_client4();
234306
234387
  init_runtime_context();
234307
234388
  init_client_tool_serialization();
234308
234389
  SCRIPT_EXCLUDED_TOOLS = new Set([
234309
234390
  "AskUserQuestion",
234310
234391
  "Workflow",
234311
- "CodeMode"
234392
+ "CodeMode",
234393
+ "Monitor"
234312
234394
  ]);
234313
234395
  });
234314
234396
 
@@ -234670,13 +234752,10 @@ async function executeWorkflow(spawner, options) {
234670
234752
  return executeScript(spawner, options, meta2);
234671
234753
  }
234672
234754
  async function executeToolScript(options) {
234673
- const run = await executeScript(undefined, options, {
234674
- name: "code-mode",
234675
- description: "Combine tools with JavaScript"
234676
- });
234755
+ const run = await executeScript(undefined, options, { name: "code-mode", description: "Combine tools with JavaScript" }, options.globals);
234677
234756
  return run.result;
234678
234757
  }
234679
- async function executeScript(spawner, options, meta2) {
234758
+ async function executeScript(spawner, options, meta2, extraGlobals) {
234680
234759
  const signal = options.signal ?? new AbortController().signal;
234681
234760
  let settled = false;
234682
234761
  const emit = (event) => {
@@ -234952,7 +235031,8 @@ async function executeScript(spawner, options, meta2) {
234952
235031
  describeTool,
234953
235032
  ...spawner ? { agent, decide, parallel, pipeline: pipeline7, phase, log: log3 } : {},
234954
235033
  args: options.args,
234955
- console: { log: log3, warn: log3, error: log3, info: log3 }
235034
+ console: { log: log3, warn: log3, error: log3, info: log3 },
235035
+ ...extraGlobals
234956
235036
  });
234957
235037
  const wrapped = `(async () => { "use strict";
234958
235038
  ${spawner ? stripMetaExport(options.script) : options.script}
@@ -241570,6 +241650,223 @@ var init_monitor_event_stream = __esm(() => {
241570
241650
  MONITOR_EVENT_BUFFER_CHARS = 1024 * 1024;
241571
241651
  });
241572
241652
 
241653
+ // src/tools/impl/monitor-script.ts
241654
+ function rateLimitToolCalls(call2, signal) {
241655
+ let tokens = TOOL_CALL_BURST;
241656
+ let refilledAt = Date.now();
241657
+ return (toolName2, toolArgs) => {
241658
+ if (signal.aborted)
241659
+ return new Promise(() => {});
241660
+ const now2 = Date.now();
241661
+ const refill = Math.floor((now2 - refilledAt) / TOOL_CALL_REFILL_MS);
241662
+ if (refill > 0) {
241663
+ tokens = Math.min(TOOL_CALL_BURST, tokens + refill);
241664
+ refilledAt += refill * TOOL_CALL_REFILL_MS;
241665
+ }
241666
+ if (tokens === 0) {
241667
+ throw new Error(`Monitor script exceeded ${1000 / TOOL_CALL_REFILL_MS} tool calls per second; add sleep() between polls.`);
241668
+ }
241669
+ tokens -= 1;
241670
+ return call2(toolName2, toolArgs);
241671
+ };
241672
+ }
241673
+ function abortableSleep3(ms, signal) {
241674
+ const delay3 = Number(ms);
241675
+ if (!Number.isFinite(delay3) || delay3 < 0) {
241676
+ return Promise.reject(new Error("sleep(ms) needs a non-negative number."));
241677
+ }
241678
+ return new Promise((resolve34) => {
241679
+ if (signal.aborted)
241680
+ return;
241681
+ const onAbort = () => clearTimeout(timer);
241682
+ const timer = setTimeout(() => {
241683
+ signal.removeEventListener("abort", onAbort);
241684
+ resolve34();
241685
+ }, delay3);
241686
+ signal.addEventListener("abort", onAbort, { once: true });
241687
+ });
241688
+ }
241689
+ function formatReturnValue(value) {
241690
+ if (value === undefined)
241691
+ return "The script returned without a value.";
241692
+ let text2;
241693
+ try {
241694
+ text2 = typeof value === "string" ? value : JSON.stringify(value);
241695
+ } catch (error5) {
241696
+ return `The script returned a value that is not JSON-serializable: ${String(error5)}`;
241697
+ }
241698
+ return text2.length <= MAX_RESULT_CHARS ? text2 : `${text2.slice(0, MAX_RESULT_CHARS)}
241699
+ [truncated; see the output file]`;
241700
+ }
241701
+ async function releaseToolContext(contextId) {
241702
+ if (!contextId || !releaseRetainedToolContext(contextId))
241703
+ return;
241704
+ const manager = await init_manager2().then(() => exports_manager);
241705
+ manager.releaseToolExecutionContext(contextId);
241706
+ }
241707
+ async function startScriptMonitor(args, hooks) {
241708
+ assertBackgroundProcessCapacity();
241709
+ const toolContextId = getRuntimeContext()?.toolContextId ?? null;
241710
+ if (toolContextId)
241711
+ retainToolContext(toolContextId);
241712
+ const controller = new AbortController;
241713
+ const parentAgentId = await resolveScriptParentAgentId(args).catch(async (error5) => {
241714
+ await releaseToolContext(toolContextId);
241715
+ throw error5;
241716
+ });
241717
+ const toolScope = await createScriptToolScope(parentAgentId, controller.signal).catch(async (error5) => {
241718
+ await releaseToolContext(toolContextId);
241719
+ throw error5;
241720
+ });
241721
+ const taskId = getNextMonitorId();
241722
+ const outputFile = createBackgroundOutputFile(taskId);
241723
+ const output = hooks.createOutput(outputFile);
241724
+ const scope = resolveNotificationScope(args.parentScope);
241725
+ const actingUserId = getRuntimeActingUserAttribution();
241726
+ const secrets2 = captureSecretRedactions(parentAgentId ?? undefined);
241727
+ const scrub = (text2) => scrubSecretsFromString(text2, secrets2);
241728
+ let timeout;
241729
+ const stop = (reason, note) => {
241730
+ if (processState.status !== "running")
241731
+ return;
241732
+ processState.completionNotificationSuppressed = true;
241733
+ if (note)
241734
+ output.append(note);
241735
+ hooks.markFinished(taskId, processState, "failed", null);
241736
+ controller.abort(new Error(reason));
241737
+ };
241738
+ const events = createMonitorEventStream({
241739
+ emit(event) {
241740
+ hooks.queueEvent({ taskId, event, scope, actingUserId, secrets: secrets2 });
241741
+ },
241742
+ stopSource() {
241743
+ stop("output rate too high", `
241744
+ [stopped: output rate too high]
241745
+ `);
241746
+ }
241747
+ });
241748
+ const firstLine = args.script.trim().split(`
241749
+ `)[0] ?? "";
241750
+ const processState = {
241751
+ process: {
241752
+ kill() {
241753
+ events.cancel();
241754
+ controller.abort(new Error("Monitor stopped via TaskStop"));
241755
+ }
241756
+ },
241757
+ command: `script: ${firstLine.slice(0, 80)}`,
241758
+ status: "running",
241759
+ exitCode: null,
241760
+ startTime: new Date,
241761
+ outputFile,
241762
+ runtimeScope: scope,
241763
+ actingUserId,
241764
+ kind: "monitor",
241765
+ description: args.description,
241766
+ monitorSource: "script",
241767
+ persistent: args.persistent,
241768
+ secrets: secrets2
241769
+ };
241770
+ backgroundProcesses.set(taskId, processState);
241771
+ notifyBackgroundProcessStateChanged(scope);
241772
+ const emit = (value) => {
241773
+ if (processState.status !== "running")
241774
+ return;
241775
+ const text2 = scrub(typeof value === "string" ? value : String(value));
241776
+ if (!text2.trim())
241777
+ return;
241778
+ const line = text2.endsWith(`
241779
+ `) ? text2 : `${text2}
241780
+ `;
241781
+ if (!output.append(line)) {
241782
+ stop("output file write failed");
241783
+ return;
241784
+ }
241785
+ events.onData(line);
241786
+ };
241787
+ const finish = (status, detail) => {
241788
+ events.finish();
241789
+ output.append(`
241790
+ [${status === "completed" ? "result" : "error"}] ${detail}
241791
+ `);
241792
+ if (processState.status !== "running")
241793
+ return;
241794
+ hooks.markFinished(taskId, processState, status, status === "completed" ? 0 : 1);
241795
+ const durationMs = Date.now() - (processState.startTime?.getTime() ?? 0);
241796
+ addToMessageQueue({
241797
+ kind: "task_notification",
241798
+ text: formatTaskNotification({
241799
+ taskId,
241800
+ status,
241801
+ summary: status === "completed" ? `Monitor "${args.description}" script returned` : `Monitor "${args.description}" script failed`,
241802
+ result: detail,
241803
+ outputFile,
241804
+ usage: { durationMs }
241805
+ }),
241806
+ ...scope,
241807
+ ...actingUserId !== undefined ? { actingUserId } : {}
241808
+ });
241809
+ };
241810
+ const stopped = new Promise((_resolve, reject2) => {
241811
+ controller.signal.addEventListener("abort", () => reject2(controller.signal.reason), { once: true });
241812
+ });
241813
+ const run = executeToolScript({
241814
+ ...toolScope,
241815
+ callTool: rateLimitToolCalls(toolScope.callTool, controller.signal),
241816
+ maxTotalToolCalls: Number.POSITIVE_INFINITY,
241817
+ script: args.script,
241818
+ args: args.args,
241819
+ signal: new AbortController().signal,
241820
+ globals: {
241821
+ emit,
241822
+ sleep: (ms) => abortableSleep3(ms, controller.signal)
241823
+ },
241824
+ onProgress: (event) => {
241825
+ if (event.kind === "log")
241826
+ output.append(`${scrub(event.message)}
241827
+ `);
241828
+ }
241829
+ });
241830
+ run.catch(() => {});
241831
+ Promise.race([run, stopped]).then((value) => finish("completed", scrub(formatReturnValue(value))), (error5) => finish("failed", scrub(error5 instanceof Error ? error5.message : String(error5)))).finally(async () => {
241832
+ if (timeout)
241833
+ clearTimeout(timeout);
241834
+ await toolScope.cleanup();
241835
+ await releaseToolContext(toolContextId);
241836
+ });
241837
+ if (!args.persistent) {
241838
+ timeout = setTimeout(() => {
241839
+ if (processState.status !== "running")
241840
+ return;
241841
+ events.finish();
241842
+ hooks.queueEvent({
241843
+ taskId,
241844
+ event: hooks.timeoutEvent(args.timeout_ms),
241845
+ scope,
241846
+ actingUserId,
241847
+ secrets: secrets2
241848
+ });
241849
+ stop("timeout", `
241850
+ [timeout after ${args.timeout_ms}ms]
241851
+ `);
241852
+ }, args.timeout_ms);
241853
+ unrefTimer(timeout);
241854
+ }
241855
+ return { taskId, outputFile };
241856
+ }
241857
+ var TOOL_CALL_BURST = 120, TOOL_CALL_REFILL_MS = 500, MAX_RESULT_CHARS = 30000;
241858
+ var init_monitor_script = __esm(() => {
241859
+ init_runtime_context();
241860
+ init_script_tool_scope();
241861
+ init_secret_substitution();
241862
+ init_tool_context_retention();
241863
+ init_workflow_engine();
241864
+ init_message_queue_bridge();
241865
+ init_task_notifications();
241866
+ init_monitor_event_stream();
241867
+ init_process_manager();
241868
+ });
241869
+
241573
241870
  // src/tools/impl/monitor.ts
241574
241871
  import { appendFileSync as appendFileSync7, readFileSync as readFileSync37, writeFileSync as writeFileSync28 } from "node:fs";
241575
241872
  import { WebSocket as WebSocket4 } from "ws";
@@ -241698,10 +241995,18 @@ function normalizeMonitorArgs(args) {
241698
241995
  if (normalized.command !== undefined && typeof normalized.command !== "string") {
241699
241996
  throw new Error("Monitor command must be a string");
241700
241997
  }
241998
+ if (normalized.script !== undefined && typeof normalized.script !== "string") {
241999
+ throw new Error("Monitor script must be a string");
242000
+ }
242001
+ const hasScript = typeof normalized.script === "string" && normalized.script.trim() !== "";
241701
242002
  const hasCommand = typeof normalized.command === "string" && normalized.command.length > 0;
241702
242003
  const hasWebSocket = normalized.ws !== undefined && !(typeof normalized.ws === "object" && normalized.ws !== null && normalized.ws.url === "");
241703
- if (Number(hasCommand) + Number(hasWebSocket) !== 1) {
241704
- throw new Error("Monitor requires exactly one of command or ws");
242004
+ if (Number(hasCommand) + Number(hasWebSocket) + Number(hasScript) !== 1) {
242005
+ throw new Error("Monitor requires exactly one of command, ws, or script");
242006
+ }
242007
+ if (!hasScript) {
242008
+ delete normalized.script;
242009
+ delete normalized.args;
241705
242010
  }
241706
242011
  if (!hasCommand) {
241707
242012
  delete normalized.command;
@@ -242152,6 +242457,22 @@ function startWebSocketMonitor(args) {
242152
242457
  }
242153
242458
  return buildMonitorResult(taskId, args.persistent ? 0 : args.timeout_ms, args.persistent, outputFile);
242154
242459
  }
242460
+ async function startScriptSource(args) {
242461
+ const { taskId, outputFile } = await startScriptMonitor({
242462
+ description: args.description,
242463
+ script: args.script,
242464
+ args: args.args,
242465
+ timeout_ms: args.timeout_ms,
242466
+ persistent: args.persistent,
242467
+ parentScope: args.parentScope
242468
+ }, {
242469
+ createOutput: (path27) => new MonitorOutputWriter(path27),
242470
+ queueEvent: queueMonitorEvent,
242471
+ markFinished: markMonitorFinished,
242472
+ timeoutEvent: getMonitorTimeoutEvent
242473
+ });
242474
+ return buildMonitorResult(taskId, args.persistent ? 0 : args.timeout_ms, args.persistent, outputFile);
242475
+ }
242155
242476
  function backupScopeFor(taskId) {
242156
242477
  const processState = backgroundProcesses.get(taskId);
242157
242478
  const runtimeScope = processState?.runtimeScope;
@@ -242163,7 +242484,7 @@ async function monitor(args) {
242163
242484
  args.signal?.throwIfAborted();
242164
242485
  const normalized = normalizeMonitorArgs(args);
242165
242486
  installMonitorProcessExitCleanup();
242166
- const result3 = normalized.command !== undefined ? startCommandMonitor(normalized) : startWebSocketMonitor(normalized);
242487
+ const result3 = normalized.script !== undefined ? await startScriptSource(normalized) : normalized.command !== undefined ? startCommandMonitor(normalized) : startWebSocketMonitor(normalized);
242167
242488
  if (normalized.backup_after_seconds === undefined)
242168
242489
  return result3;
242169
242490
  const note = await armMonitorBackup({
@@ -242207,6 +242528,7 @@ var init_monitor = __esm(async () => {
242207
242528
  init_message_queue_bridge();
242208
242529
  init_task_notifications();
242209
242530
  init_monitor_event_stream();
242531
+ init_monitor_script();
242210
242532
  init_process_manager();
242211
242533
  init_shell_env();
242212
242534
  init_shell_launchers();
@@ -244042,7 +244364,7 @@ class CodexAppServerClient {
244042
244364
  await this.request("initialize", {
244043
244365
  clientInfo: {
244044
244366
  name: "letta-code",
244045
- version: "0.34.27"
244367
+ version: "0.34.28"
244046
244368
  },
244047
244369
  capabilities: null
244048
244370
  });
@@ -254891,7 +255213,14 @@ var init_Monitor2 = __esm(() => {
254891
255213
  },
254892
255214
  required: ["url"],
254893
255215
  additionalProperties: false,
254894
- description: "WebSocket to open. Each text frame is an event; binary frames are reported as a placeholder line. Socket close ends the watch. Cannot be combined with command."
255216
+ description: "WebSocket to open. Each text frame is an event; binary frames are reported as a placeholder line. Socket close ends the watch. Cannot be combined with command or script."
255217
+ },
255218
+ script: {
255219
+ type: "string",
255220
+ description: "JavaScript watch loop with CodeMode's globals plus emit(text) and sleep(ms). Cannot be combined with command or ws."
255221
+ },
255222
+ args: {
255223
+ description: "Optional input exposed to script as args."
254895
255224
  }
254896
255225
  },
254897
255226
  required: ["description"],
@@ -443833,6 +444162,19 @@ function getInterruptApprovalsForEmission(runtime, params) {
443833
444162
  }
443834
444163
  return runtime.pendingInterruptedResults;
443835
444164
  }
444165
+ function emitInterruptApprovals(socket, runtime, params) {
444166
+ const approvals = getInterruptApprovalsForEmission(runtime, params);
444167
+ if (!approvals)
444168
+ return;
444169
+ const { runId, agentId, conversationId } = params;
444170
+ emitToolExecutionFinishedEvents(socket, runtime, {
444171
+ approvals,
444172
+ runId,
444173
+ agentId,
444174
+ conversationId
444175
+ });
444176
+ emitInterruptToolReturnMessage(socket, runtime, approvals, runId || undefined);
444177
+ }
443836
444178
  function populateInterruptQueue(runtime, input) {
443837
444179
  const shouldPopulate = !runtime.pendingInterruptedResults || runtime.pendingInterruptedResults.length === 0 || !runtime.pendingInterruptedContext;
443838
444180
  if (!shouldPopulate)
@@ -449194,7 +449536,8 @@ async function resolveStaleApprovals(runtime, socket, turnLease, deps = {}) {
449194
449536
  runtime.currentToolset = preparedToolContext.toolset;
449195
449537
  runtime.currentToolsetPreference = preparedToolContext.toolsetPreference;
449196
449538
  runtime.currentLoadedTools = preparedToolContext.preparedToolContext.loadedToolNames;
449197
- const recoveryActingUserId = readInterruptedTurn(runtime)?.actingUserId;
449539
+ const recoveryRecord = (deps.readInterruptedTurn ?? readInterruptedTurn)(runtime);
449540
+ const recoveryActingUserId = recoveryRecord?.actingUserId;
449198
449541
  pendingApprovals = await filterStillPending(pendingApprovals);
449199
449542
  while (pendingApprovals.length > 0) {
449200
449543
  assertCurrentTurnLease();
@@ -449203,10 +449546,16 @@ async function resolveStaleApprovals(runtime, socket, turnLease, deps = {}) {
449203
449546
  throw new Error("Ambiguous pending approval batch mapping during recovery");
449204
449547
  }
449205
449548
  rememberPendingApprovalBatchIds(runtime, pendingApprovals, recoveryBatchId);
449206
- const approvalResults = buildFreshDenialApprovals(pendingApprovals, STALE_APPROVAL_RECOVERY_DENIAL_REASON);
449549
+ const pendingToolCallIds = pendingApprovals.map((approval) => approval.toolCallId);
449550
+ const approvalResults = recoveryRecord ? recordedToolResults(recoveryRecord, pendingToolCallIds) : buildFreshDenialApprovals(pendingApprovals, STALE_APPROVAL_RECOVERY_DENIAL_REASON);
449207
449551
  if (approvalResults.length === 0) {
449208
449552
  return null;
449209
449553
  }
449554
+ if (recoveryRecord && allRecordedResults(recoveryRecord).some((result3) => pendingToolCallIds.includes(result3.tool_call_id))) {
449555
+ runtime.pendingInterruptedResults = null;
449556
+ runtime.pendingInterruptedContext = null;
449557
+ runtime.pendingInterruptedToolCallIds = null;
449558
+ }
449210
449559
  try {
449211
449560
  const continuationInput = createTurnInputState([
449212
449561
  {
@@ -449591,6 +449940,7 @@ var init_send = __esm(async () => {
449591
449940
  init_constants3();
449592
449941
  init_cwd();
449593
449942
  init_permission_mode();
449943
+ init_recorded_tool_results();
449594
449944
  init_turn_input_state();
449595
449945
  await __promiseAll([
449596
449946
  init_message(),
@@ -452448,6 +452798,7 @@ async function handleApprovalStop(params) {
452448
452798
  if (shouldInterrupt()) {
452449
452799
  return interruptTermination(nextTurnInput, continuationBatchId);
452450
452800
  }
452801
+ params.onContinuationSendError?.(interruptTermination());
452451
452802
  throw error5;
452452
452803
  }
452453
452804
  if (sendResult.kind === "terminal") {
@@ -459384,7 +459735,7 @@ async function handleIncomingMessageInner(msg, socket, runtime, onStatusChange,
459384
459735
  const conversationId = requestedConversationId ?? "default";
459385
459736
  const turnWorkingDirectory = getConversationWorkingDirectory(runtime.listener, agentId, conversationId);
459386
459737
  const turnPermissionModeState = getOrCreateConversationPermissionModeStateRef(runtime.listener, agentId, conversationId);
459387
- let postStopApprovalRecoveryRetries = 0, deploymentRecoveryAttempts = 0, llmApiErrorRetries = 0, emptyResponseRetries = 0, chatgptPlanSwaps = 0, lastApprovalContinuationAccepted = false, activeDequeuedBatchId = dequeuedBatchId;
459738
+ let postStopApprovalRecoveryRetries = 0, deploymentRecoveryAttempts = 0, llmApiErrorRetries = 0, emptyResponseRetries = 0, chatgptPlanSwaps = 0, lastApprovalContinuationAccepted = false, continuationSendFailed = false, activeDequeuedBatchId = dequeuedBatchId;
459388
459739
  const chatgptExhaustedProviders = new Set;
459389
459740
  const turnCorrelation = existingTurnCorrelation ?? createTurnCorrelation(runtime, msg, activeDequeuedBatchId);
459390
459741
  const msgRunIds = [];
@@ -459392,6 +459743,13 @@ async function handleIncomingMessageInner(msg, socket, runtime, onStatusChange,
459392
459743
  let lastExecutingToolCallIds = [];
459393
459744
  let lastNeedsUserInputToolCallIds = [];
459394
459745
  const durabilityOwnership = createTurnDurabilityOwnership();
459746
+ const recordApprovalProgress = (progress) => {
459747
+ ({
459748
+ lastExecutionResults,
459749
+ lastExecutingToolCallIds,
459750
+ lastNeedsUserInputToolCallIds
459751
+ } = durabilityOwnership.record(progress));
459752
+ };
459395
459753
  const interruptedRevisionRef = { current: initialInterruptedRevision };
459396
459754
  durabilityOwnership.recordInput(msg);
459397
459755
  const turnLease = existingTurnLease ?? runtime.turnLifecycle.begin({
@@ -459891,6 +460249,10 @@ async function handleIncomingMessageInner(msg, socket, runtime, onStatusChange,
459891
460249
  recoveryLineageId,
459892
460250
  recoveryAuthorityStore,
459893
460251
  buildSendOptions,
460252
+ onContinuationSendError: (progress) => {
460253
+ continuationSendFailed = true;
460254
+ recordApprovalProgress(progress);
460255
+ },
459894
460256
  dependencies: recoveryEvidenceWriter ? { recordListenerWork: recoveryEvidenceWriter } : undefined
459895
460257
  });
459896
460258
  if (approvalResult.kind === "error") {
@@ -459911,11 +460273,7 @@ async function handleIncomingMessageInner(msg, socket, runtime, onStatusChange,
459911
460273
  activeDequeuedBatchId = approvalResult.dequeuedBatchId;
459912
460274
  pendingNormalizationInterruptedToolCallIds = approvalResult.pendingNormalizationInterruptedToolCallIds;
459913
460275
  turnToolContextId = approvalResult.turnToolContextId;
459914
- ({
459915
- lastExecutionResults,
459916
- lastExecutingToolCallIds,
459917
- lastNeedsUserInputToolCallIds
459918
- } = durabilityOwnership.record(approvalResult));
460276
+ recordApprovalProgress(approvalResult);
459919
460277
  lastApprovalContinuationAccepted = approvalResult.lastApprovalContinuationAccepted;
459920
460278
  if (approvalResult.kind === "teleport") {
459921
460279
  const pending2 = approvalResult.pendingTeleport;
@@ -459963,29 +460321,24 @@ async function handleIncomingMessageInner(msg, socket, runtime, onStatusChange,
459963
460321
  context: "listener_turn_processing",
459964
460322
  runId: runtime.activeRunId || msgRunIds[msgRunIds.length - 1]
459965
460323
  });
460324
+ const canParkUnsentResults = runtime.turnLifecycle.isCurrent(turnLease) && !lastApprovalContinuationAccepted;
460325
+ if (canParkUnsentResults && (turnAbortSignal.aborted || continuationSendFailed)) {
460326
+ populateInterruptQueue(runtime, {
460327
+ lastExecutionResults,
460328
+ lastExecutingToolCallIds,
460329
+ lastNeedsUserInputToolCallIds,
460330
+ agentId,
460331
+ conversationId
460332
+ });
460333
+ }
459966
460334
  if (turnAbortSignal.aborted) {
459967
- if (runtime.turnLifecycle.isCurrent(turnLease) && !lastApprovalContinuationAccepted) {
459968
- populateInterruptQueue(runtime, {
460335
+ if (canParkUnsentResults) {
460336
+ emitInterruptApprovals(socket, runtime, {
459969
460337
  lastExecutionResults,
459970
- lastExecutingToolCallIds,
459971
- lastNeedsUserInputToolCallIds,
460338
+ runId: runtime.activeRunId || msgRunIds[msgRunIds.length - 1],
459972
460339
  agentId,
459973
460340
  conversationId
459974
460341
  });
459975
- const approvalsForEmission = getInterruptApprovalsForEmission(runtime, {
459976
- lastExecutionResults,
459977
- agentId,
459978
- conversationId
459979
- });
459980
- if (approvalsForEmission) {
459981
- emitToolExecutionFinishedEvents(socket, runtime, {
459982
- approvals: approvalsForEmission,
459983
- runId: runtime.activeRunId || msgRunIds[msgRunIds.length - 1],
459984
- agentId,
459985
- conversationId
459986
- });
459987
- emitInterruptToolReturnMessage(socket, runtime, approvalsForEmission, runtime.activeRunId || msgRunIds[msgRunIds.length - 1] || undefined);
459988
- }
459989
460342
  }
459990
460343
  finishTurn({
459991
460344
  stopReason: "cancelled",
@@ -538684,4 +539037,4 @@ function registerBunOAuthFlows() {
538684
539037
  registerBunOAuthFlows();
538685
539038
  await init_src5().then(() => exports_src2);
538686
539039
 
538687
- //# debugId=B77AE6C44CFEB3F664756E2164756E21
539040
+ //# debugId=5934D32E04CC3ADA64756E2164756E21