deepagents 1.8.8 → 1.9.0-alpha.1

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/index.js CHANGED
@@ -1,92 +1,23 @@
1
- import { AIMessage, HumanMessage, SystemMessage, ToolMessage, anthropicPromptCachingMiddleware, countTokensApproximately, createAgent, createMiddleware, humanInTheLoopMiddleware, todoListMiddleware, tool } from "langchain";
2
- import { Runnable } from "@langchain/core/runnables";
3
- import { Command, REMOVE_ALL_MESSAGES, ReducedValue, StateSchema, getCurrentTaskInput, isCommand } from "@langchain/langgraph";
1
+ import { AIMessage, HumanMessage, SystemMessage, ToolMessage, anthropicPromptCachingMiddleware, context, countTokensApproximately, createAgent, createMiddleware, humanInTheLoopMiddleware, todoListMiddleware, tool } from "langchain";
2
+ import { ChatAnthropic } from "@langchain/anthropic";
3
+ import { Command, REMOVE_ALL_MESSAGES, ReducedValue, StateSchema, getConfig, getCurrentTaskInput, getStore, isCommand } from "@langchain/langgraph";
4
4
  import { z } from "zod/v4";
5
5
  import micromatch from "micromatch";
6
- import { basename } from "path";
7
- import { HumanMessage as HumanMessage$1, RemoveMessage, getBufferString } from "@langchain/core/messages";
6
+ import path, { basename } from "path";
7
+ import { AIMessage as AIMessage$1, HumanMessage as HumanMessage$1, RemoveMessage, getBufferString } from "@langchain/core/messages";
8
+ import * as z$2 from "zod";
8
9
  import { z as z$1 } from "zod";
9
10
  import yaml from "yaml";
11
+ import { Client } from "@langchain/langgraph-sdk";
10
12
  import { ContextOverflowError } from "@langchain/core/errors";
11
13
  import { initChatModel } from "langchain/chat_models/universal";
12
14
  import fs from "node:fs/promises";
13
15
  import fs$1 from "node:fs";
14
- import path from "node:path";
16
+ import path$1 from "node:path";
15
17
  import cp, { spawn } from "node:child_process";
16
18
  import fg from "fast-glob";
17
19
  import { LangSmithResourceNotFoundError, LangSmithSandboxError, SandboxClient } from "langsmith/experimental/sandbox";
18
20
  import os from "node:os";
19
- //#region src/backends/protocol.ts
20
- /**
21
- * Type guard to check if a backend supports execution.
22
- *
23
- * @param backend - Backend instance to check
24
- * @returns True if the backend implements SandboxBackendProtocol
25
- */
26
- function isSandboxBackend(backend) {
27
- return typeof backend.execute === "function" && typeof backend.id === "string" && backend.id !== "";
28
- }
29
- const SANDBOX_ERROR_SYMBOL = Symbol.for("sandbox.error");
30
- /**
31
- * Custom error class for sandbox operations.
32
- *
33
- * @param message - Human-readable error description
34
- * @param code - Structured error code for programmatic handling
35
- * @returns SandboxError with message and code
36
- *
37
- * @example
38
- * ```typescript
39
- * try {
40
- * await sandbox.execute("some command");
41
- * } catch (error) {
42
- * if (error instanceof SandboxError) {
43
- * switch (error.code) {
44
- * case "NOT_INITIALIZED":
45
- * await sandbox.initialize();
46
- * break;
47
- * case "COMMAND_TIMEOUT":
48
- * console.error("Command took too long");
49
- * break;
50
- * default:
51
- * throw error;
52
- * }
53
- * }
54
- * }
55
- * ```
56
- */
57
- var SandboxError = class SandboxError extends Error {
58
- /** Symbol for identifying sandbox error instances */
59
- [SANDBOX_ERROR_SYMBOL] = true;
60
- /** Error name for instanceof checks and logging */
61
- name = "SandboxError";
62
- /**
63
- * Creates a new SandboxError.
64
- *
65
- * @param message - Human-readable error description
66
- * @param code - Structured error code for programmatic handling
67
- */
68
- constructor(message, code, cause) {
69
- super(message);
70
- this.code = code;
71
- this.cause = cause;
72
- Object.setPrototypeOf(this, SandboxError.prototype);
73
- }
74
- static isInstance(error) {
75
- return typeof error === "object" && error !== null && error[SANDBOX_ERROR_SYMBOL] === true;
76
- }
77
- };
78
- /**
79
- * Resolve a backend instance or await a {@link BackendFactory}.
80
- *
81
- * Accepts {@link BackendRuntime} or {@link ToolRuntime} — store typing differs
82
- * between LangGraph checkpoint stores and core `ToolRuntime`; factories receive
83
- * a value that is structurally compatible at runtime.
84
- */
85
- async function resolveBackend(backend, runtime) {
86
- if (typeof backend === "function") return await backend(runtime);
87
- return backend;
88
- }
89
- //#endregion
90
21
  //#region src/backends/utils.ts
91
22
  /**
92
23
  * Shared utility functions for memory backend implementations.
@@ -96,9 +27,37 @@ async function resolveBackend(backend, runtime) {
96
27
  * enable composition without fragile string parsing.
97
28
  */
98
29
  const EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents";
99
- const MAX_LINE_LENGTH = 1e4;
30
+ const MAX_LINE_LENGTH = 5e3;
100
31
  const TOOL_RESULT_TOKEN_LIMIT = 2e4;
101
32
  const TRUNCATION_GUIDANCE = "... [results truncated, try being more specific with your parameters]";
33
+ const MIME_TYPES = {
34
+ ".png": "image/png",
35
+ ".jpg": "image/jpeg",
36
+ ".jpeg": "image/jpeg",
37
+ ".gif": "image/gif",
38
+ ".webp": "image/webp",
39
+ ".svg": "image/svg+xml",
40
+ ".heic": "image/heic",
41
+ ".heif": "image/heif",
42
+ ".mp3": "audio/mpeg",
43
+ ".wav": "audio/wav",
44
+ ".aiff": "audio/aiff",
45
+ ".aac": "audio/aac",
46
+ ".ogg": "audio/ogg",
47
+ ".flac": "audio/flac",
48
+ ".mp4": "video/mp4",
49
+ ".webm": "video/webm",
50
+ ".mpeg": "video/mpeg",
51
+ ".mov": "video/quicktime",
52
+ ".avi": "video/x-msvideo",
53
+ ".flv": "video/x-flv",
54
+ ".mpg": "video/mpeg",
55
+ ".wmv": "video/x-ms-wmv",
56
+ ".3gpp": "video/3gpp",
57
+ ".pdf": "application/pdf",
58
+ ".ppt": "application/vnd.ms-powerpoint",
59
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
60
+ };
102
61
  /**
103
62
  * Sanitize tool_call_id to prevent path traversal and separator issues.
104
63
  *
@@ -126,7 +85,7 @@ function formatContentWithLineNumbers(content, startLine = 1) {
126
85
  for (let i = 0; i < lines.length; i++) {
127
86
  const line = lines[i];
128
87
  const lineNum = i + startLine;
129
- if (line.length <= 1e4) resultLines.push(`${lineNum.toString().padStart(6)}\t${line}`);
88
+ if (line.length <= 5e3) resultLines.push(`${lineNum.toString().padStart(6)}\t${line}`);
130
89
  else {
131
90
  const numChunks = Math.ceil(line.length / MAX_LINE_LENGTH);
132
91
  for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
@@ -160,20 +119,50 @@ function checkEmptyContent(content) {
160
119
  * @returns Content as string with lines joined by newlines
161
120
  */
162
121
  function fileDataToString(fileData) {
163
- return fileData.content.join("\n");
122
+ if (Array.isArray(fileData.content)) return fileData.content.join("\n");
123
+ if (typeof fileData.content === "string") return fileData.content;
124
+ throw new Error("Cannot convert binary FileData to string");
125
+ }
126
+ /**
127
+ * Type guard to check if FileData contains binary content (Uint8Array).
128
+ *
129
+ * @param data - FileData to check
130
+ * @returns True if the content is a Uint8Array (binary)
131
+ */
132
+ function isFileDataBinary(data) {
133
+ return ArrayBuffer.isView(data.content);
164
134
  }
165
135
  /**
166
- * Create a FileData object with timestamps.
136
+ * Create a FileData object.
167
137
  *
168
- * @param content - File content as string
169
- * @param createdAt - Optional creation timestamp (ISO format)
170
- * @returns FileData object with content and timestamps
138
+ * Defaults to v2 format (content as single string). Pass `fileFormat: "v1"` for
139
+ * backward compatibility with older readers during a rolling deployment.
140
+ * Binary content (Uint8Array) is only supported with v2.
141
+ *
142
+ * @param content - File content as a string or binary Uint8Array (v2 only)
143
+ * @param createdAt - Optional creation timestamp (ISO format), defaults to now
144
+ * @param fileFormat - Storage format: "v2" (default) or "v1" (legacy line array)
145
+ * @returns FileData in the requested format
171
146
  */
172
- function createFileData(content, createdAt) {
173
- const lines = typeof content === "string" ? content.split("\n") : content;
147
+ function createFileData(content, createdAt, fileFormat = "v2", mimeType) {
174
148
  const now = (/* @__PURE__ */ new Date()).toISOString();
149
+ if (fileFormat === "v1" && ArrayBuffer.isView(content)) throw new Error("Binary data is not supported with v1 file formats. Please use v2 file format");
150
+ if (fileFormat === "v2") {
151
+ if (ArrayBuffer.isView(content)) return {
152
+ content: new Uint8Array(content.buffer, content.byteOffset, content.byteLength),
153
+ mimeType: mimeType ?? "application/octet-stream",
154
+ created_at: createdAt || now,
155
+ modified_at: now
156
+ };
157
+ return {
158
+ content,
159
+ mimeType: mimeType ?? "text/plain",
160
+ created_at: createdAt || now,
161
+ modified_at: now
162
+ };
163
+ }
175
164
  return {
176
- content: lines,
165
+ content: typeof content === "string" ? content.split("\n") : content,
177
166
  created_at: createdAt || now,
178
167
  modified_at: now
179
168
  };
@@ -186,33 +175,20 @@ function createFileData(content, createdAt) {
186
175
  * @returns Updated FileData object
187
176
  */
188
177
  function updateFileData(fileData, content) {
189
- const lines = typeof content === "string" ? content.split("\n") : content;
190
178
  const now = (/* @__PURE__ */ new Date()).toISOString();
179
+ if (isFileDataV1(fileData)) return {
180
+ content: typeof content === "string" ? content.split("\n") : content,
181
+ created_at: fileData.created_at,
182
+ modified_at: now
183
+ };
191
184
  return {
192
- content: lines,
185
+ content,
186
+ mimeType: fileData.mimeType,
193
187
  created_at: fileData.created_at,
194
188
  modified_at: now
195
189
  };
196
190
  }
197
191
  /**
198
- * Format file data for read response with line numbers.
199
- *
200
- * @param fileData - FileData object
201
- * @param offset - Line offset (0-indexed)
202
- * @param limit - Maximum number of lines
203
- * @returns Formatted content or error message
204
- */
205
- function formatReadResponse(fileData, offset, limit) {
206
- const content = fileDataToString(fileData);
207
- const emptyMsg = checkEmptyContent(content);
208
- if (emptyMsg) return emptyMsg;
209
- const lines = content.split("\n");
210
- const startIdx = offset;
211
- const endIdx = Math.min(startIdx + limit, lines.length);
212
- if (startIdx >= lines.length) return `Error: Line offset ${offset} exceeds file length (${lines.length} lines)`;
213
- return formatContentWithLineNumbers(lines.slice(startIdx, endIdx), startIdx + 1);
214
- }
215
- /**
216
192
  * Perform string replacement with occurrence validation.
217
193
  *
218
194
  * @param content - Original content
@@ -323,11 +299,8 @@ function globSearchFiles(files, pattern, path = "/") {
323
299
  /**
324
300
  * Return structured grep matches from an in-memory files mapping.
325
301
  *
326
- * Performs literal text search (not regex).
327
- *
328
- * Returns a list of GrepMatch on success, or a string for invalid inputs.
329
- * We deliberately do not raise here to keep backends non-throwing in tool
330
- * contexts and preserve user-facing error messages.
302
+ * Performs literal text search (not regex). Binary files are skipped.
303
+ * Returns an empty array when no matches are found or on invalid input.
331
304
  */
332
305
  function grepMatchesFromFiles(files, pattern, path = null, glob = null) {
333
306
  let normalizedPath;
@@ -342,19 +315,231 @@ function grepMatchesFromFiles(files, pattern, path = null, glob = null) {
342
315
  nobrace: false
343
316
  })));
344
317
  const matches = [];
345
- for (const [filePath, fileData] of Object.entries(filtered)) for (let i = 0; i < fileData.content.length; i++) {
346
- const line = fileData.content[i];
347
- const lineNum = i + 1;
348
- if (line.includes(pattern)) matches.push({
349
- path: filePath,
350
- line: lineNum,
351
- text: line
352
- });
318
+ for (const [filePath, fileData] of Object.entries(filtered)) {
319
+ if (!isTextMimeType(migrateToFileDataV2(fileData, filePath).mimeType)) continue;
320
+ const lines = fileDataToString(fileData).split("\n");
321
+ for (let i = 0; i < lines.length; i++) {
322
+ const line = lines[i];
323
+ const lineNum = i + 1;
324
+ if (line.includes(pattern)) matches.push({
325
+ path: filePath,
326
+ line: lineNum,
327
+ text: line
328
+ });
329
+ }
353
330
  }
354
331
  return matches;
355
332
  }
333
+ /**
334
+ * Determine MIME type from a file path's extension.
335
+ *
336
+ * Returns "text/plain" for unknown extensions.
337
+ *
338
+ * @param filePath - File path to inspect
339
+ * @returns MIME type string (e.g., "image/png", "text/plain")
340
+ */
341
+ function getMimeType(filePath) {
342
+ return MIME_TYPES[path.extname(filePath).toLocaleLowerCase()] || "text/plain";
343
+ }
344
+ /**
345
+ * Check whether a MIME type represents text content.
346
+ *
347
+ * @param mimeType - MIME type string to check
348
+ * @returns True if the MIME type is text-based
349
+ */
350
+ function isTextMimeType(mimeType) {
351
+ return mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/javascript" || mimeType === "image/svg+xml";
352
+ }
353
+ /**
354
+ * Type guard to check if FileData is v1 format (content as line array).
355
+ *
356
+ * @param data - FileData to check
357
+ * @returns True if data is FileDataV1
358
+ */
359
+ function isFileDataV1(data) {
360
+ return Array.isArray(data.content);
361
+ }
362
+ /**
363
+ * Convert FileData to v2 format, joining v1 line arrays into a single string.
364
+ *
365
+ * If the data is already v2, returns it unchanged.
366
+ *
367
+ * @param data - FileData in either format
368
+ * @returns FileDataV2 with content as string (text) or Uint8Array (binary)
369
+ */
370
+ function migrateToFileDataV2(data, filePath) {
371
+ if (isFileDataV1(data)) return {
372
+ content: data.content.join("\n"),
373
+ mimeType: getMimeType(filePath),
374
+ created_at: data.created_at,
375
+ modified_at: data.modified_at
376
+ };
377
+ if (!("mimeType" in data) || !data.mimeType) return {
378
+ ...data,
379
+ mimeType: getMimeType(filePath)
380
+ };
381
+ return data;
382
+ }
383
+ /**
384
+ * Adapt a v1 {@link BackendProtocol} to {@link BackendProtocolV2}.
385
+ *
386
+ * If the backend already implements v2, it is returned as-is.
387
+ * For v1 backends, wraps returns in Result types:
388
+ * - `read()` string returns wrapped in {@link ReadResult}
389
+ * - `readRaw()` FileData returns wrapped in {@link ReadRawResult}
390
+ * - `grep()` returns wrapped in {@link GrepResult}
391
+ * - `ls()` FileInfo[] returns wrapped in {@link LsResult}
392
+ * - `glob()` FileInfo[] returns wrapped in {@link GlobResult}
393
+ *
394
+ * Note: For sandbox instances, use {@link adaptSandboxProtocol} instead.
395
+ *
396
+ * @param backend - Backend instance (v1 or v2)
397
+ * @returns BackendProtocolV2-compatible backend
398
+ */
399
+ function adaptBackendProtocol(backend) {
400
+ return {
401
+ async ls(path) {
402
+ const result = await ("ls" in backend ? backend.ls(path) : backend.lsInfo(path));
403
+ if (Array.isArray(result)) return { files: result };
404
+ return result;
405
+ },
406
+ async readRaw(filePath) {
407
+ const result = await backend.readRaw(filePath);
408
+ if ("data" in result || "error" in result) return result;
409
+ return { data: migrateToFileDataV2(result, filePath) };
410
+ },
411
+ async glob(pattern, path) {
412
+ const result = await ("glob" in backend ? backend.glob(pattern, path) : backend.globInfo(pattern, path));
413
+ if (Array.isArray(result)) return { files: result };
414
+ return result;
415
+ },
416
+ write: (filePath, content) => backend.write(filePath, content),
417
+ edit: (filePath, oldString, newString, replaceAll) => backend.edit(filePath, oldString, newString, replaceAll),
418
+ uploadFiles: backend.uploadFiles ? (files) => backend.uploadFiles(files) : void 0,
419
+ downloadFiles: backend.downloadFiles ? (paths) => backend.downloadFiles(paths) : void 0,
420
+ async read(filePath, offset, limit) {
421
+ const result = await backend.read(filePath, offset, limit);
422
+ if (typeof result === "string") return { content: result };
423
+ return result;
424
+ },
425
+ async grep(pattern, path, glob) {
426
+ const result = await ("grep" in backend ? backend.grep(pattern, path, glob) : backend.grepRaw(pattern, path, glob));
427
+ if (Array.isArray(result)) return { matches: result };
428
+ if (typeof result === "string") return { error: result };
429
+ return result;
430
+ }
431
+ };
432
+ }
433
+ /**
434
+ * Adapt a sandbox backend from v1 to v2 interface.
435
+ *
436
+ * This extends {@link adaptBackendProtocol} to also preserve sandbox-specific
437
+ * properties from {@link SandboxBackendProtocol}: `execute` and `id`.
438
+ *
439
+ * @param sandbox - Sandbox backend (v1 or v2)
440
+ * @returns SandboxBackendProtocolV2-compatible sandbox
441
+ */
442
+ function adaptSandboxProtocol(sandbox) {
443
+ const adapted = adaptBackendProtocol(sandbox);
444
+ adapted.execute = (cmd) => sandbox.execute(cmd);
445
+ Object.defineProperty(adapted, "id", {
446
+ value: sandbox.id,
447
+ enumerable: true,
448
+ configurable: true
449
+ });
450
+ return adapted;
451
+ }
452
+ //#endregion
453
+ //#region src/backends/protocol.ts
454
+ /**
455
+ * Type guard to check if a backend supports execution.
456
+ *
457
+ * @param backend - Backend instance to check
458
+ * @returns True if the backend implements SandboxBackendProtocolV2
459
+ */
460
+ function isSandboxBackend(backend) {
461
+ return backend != null && typeof backend === "object" && typeof backend.execute === "function" && typeof backend.id === "string" && backend.id !== "";
462
+ }
463
+ /**
464
+ * Type guard to check if a backend is a sandbox protocol (v1 or v2).
465
+ *
466
+ * Checks for the presence of `execute` function and `id` string,
467
+ * which are the defining features of sandbox protocols.
468
+ *
469
+ * @param backend - Backend instance to check
470
+ * @returns True if the backend implements sandbox protocol (v1 or v2)
471
+ */
472
+ function isSandboxProtocol(backend) {
473
+ return backend != null && typeof backend === "object" && typeof backend.execute === "function" && typeof backend.id === "string" && backend.id !== "";
474
+ }
475
+ const SANDBOX_ERROR_SYMBOL = Symbol.for("sandbox.error");
476
+ /**
477
+ * Custom error class for sandbox operations.
478
+ *
479
+ * @param message - Human-readable error description
480
+ * @param code - Structured error code for programmatic handling
481
+ * @returns SandboxError with message and code
482
+ *
483
+ * @example
484
+ * ```typescript
485
+ * try {
486
+ * await sandbox.execute("some command");
487
+ * } catch (error) {
488
+ * if (error instanceof SandboxError) {
489
+ * switch (error.code) {
490
+ * case "NOT_INITIALIZED":
491
+ * await sandbox.initialize();
492
+ * break;
493
+ * case "COMMAND_TIMEOUT":
494
+ * console.error("Command took too long");
495
+ * break;
496
+ * default:
497
+ * throw error;
498
+ * }
499
+ * }
500
+ * }
501
+ * ```
502
+ */
503
+ var SandboxError = class SandboxError extends Error {
504
+ /** Symbol for identifying sandbox error instances */
505
+ [SANDBOX_ERROR_SYMBOL] = true;
506
+ /** Error name for instanceof checks and logging */
507
+ name = "SandboxError";
508
+ /**
509
+ * Creates a new SandboxError.
510
+ *
511
+ * @param message - Human-readable error description
512
+ * @param code - Structured error code for programmatic handling
513
+ */
514
+ constructor(message, code, cause) {
515
+ super(message);
516
+ this.code = code;
517
+ this.cause = cause;
518
+ Object.setPrototypeOf(this, SandboxError.prototype);
519
+ }
520
+ static isInstance(error) {
521
+ return typeof error === "object" && error !== null && error[SANDBOX_ERROR_SYMBOL] === true;
522
+ }
523
+ };
524
+ /**
525
+ * Resolve a backend instance or await a {@link BackendFactory}.
526
+ *
527
+ * Accepts {@link BackendRuntime} or {@link ToolRuntime} — store typing differs
528
+ * between LangGraph checkpoint stores and core `ToolRuntime`; factories receive
529
+ * a value that is structurally compatible at runtime.
530
+ *
531
+ * @internal
532
+ */
533
+ async function resolveBackend(backend, runtime) {
534
+ if (typeof backend === "function") {
535
+ const resolved = await backend(runtime);
536
+ return isSandboxProtocol(resolved) ? adaptSandboxProtocol(resolved) : adaptBackendProtocol(resolved);
537
+ }
538
+ return isSandboxProtocol(backend) ? adaptSandboxProtocol(backend) : adaptBackendProtocol(backend);
539
+ }
356
540
  //#endregion
357
541
  //#region src/backends/state.ts
542
+ const PREGEL_SEND_KEY = "__pregel_send";
358
543
  /**
359
544
  * Backend that stores files in agent state (ephemeral).
360
545
  *
@@ -368,23 +553,60 @@ function grepMatchesFromFiles(files, pattern, path = null, glob = null) {
368
553
  */
369
554
  var StateBackend = class {
370
555
  runtime;
371
- constructor(runtime) {
372
- this.runtime = runtime;
556
+ fileFormat;
557
+ constructor(runtimeOrOptions, options) {
558
+ if (runtimeOrOptions != null && typeof runtimeOrOptions === "object" && "state" in runtimeOrOptions) {
559
+ this.runtime = runtimeOrOptions;
560
+ this.fileFormat = options?.fileFormat ?? "v2";
561
+ } else {
562
+ this.runtime = void 0;
563
+ this.fileFormat = runtimeOrOptions?.fileFormat ?? "v2";
564
+ }
565
+ }
566
+ /**
567
+ * Whether this instance was constructed with the legacy factory pattern.
568
+ *
569
+ * When true, state is read from the injected `runtime` and `filesUpdate`
570
+ * is returned to the caller. When false, state is read from LangGraph's
571
+ * execution context and updates are sent via `__pregel_send`.
572
+ */
573
+ get isLegacy() {
574
+ return this.runtime !== void 0;
373
575
  }
374
576
  /**
375
577
  * Get files from current state.
578
+ *
579
+ * In legacy mode, reads from the injected {@link BackendRuntime}.
580
+ * In zero-arg mode, reads from the LangGraph execution context via
581
+ * {@link getCurrentTaskInput}.
376
582
  */
377
583
  getFiles() {
378
- return this.runtime.state.files || {};
584
+ if (this.runtime) return this.runtime.state.files || {};
585
+ return getCurrentTaskInput()?.files || {};
586
+ }
587
+ /**
588
+ * Push a files state update through LangGraph's internal send channel.
589
+ *
590
+ * In zero-arg mode, sends the update via the `__pregel_send` function
591
+ * from {@link getConfig}, mirroring Python's `CONFIG_KEY_SEND`.
592
+ * In legacy mode, this is a no-op — the caller uses `filesUpdate`
593
+ * from the return value instead.
594
+ *
595
+ * @param update - Map of file paths to their updated {@link FileData}
596
+ */
597
+ sendFilesUpdate(update) {
598
+ if (this.isLegacy) return;
599
+ const send = getConfig().configurable?.[PREGEL_SEND_KEY];
600
+ if (typeof send === "function") send([["files", update]]);
379
601
  }
380
602
  /**
381
603
  * List files and directories in the specified directory (non-recursive).
382
604
  *
383
605
  * @param path - Absolute path to directory
384
- * @returns List of FileInfo objects for files and directories directly in the directory.
606
+ * @returns LsResult with list of FileInfo objects on success or error on failure.
385
607
  * Directories have a trailing / in their path and is_dir=true.
386
608
  */
387
- lsInfo(path) {
609
+ ls(path) {
388
610
  const files = this.getFiles();
389
611
  const infos = [];
390
612
  const subdirs = /* @__PURE__ */ new Set();
@@ -397,7 +619,7 @@ var StateBackend = class {
397
619
  subdirs.add(normalizedPath + subdirName + "/");
398
620
  continue;
399
621
  }
400
- const size = fd.content.join("\n").length;
622
+ const size = isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length;
401
623
  infos.push({
402
624
  path: k,
403
625
  is_dir: false,
@@ -412,31 +634,43 @@ var StateBackend = class {
412
634
  modified_at: ""
413
635
  });
414
636
  infos.sort((a, b) => a.path.localeCompare(b.path));
415
- return infos;
637
+ return { files: infos };
416
638
  }
417
639
  /**
418
- * Read file content with line numbers.
640
+ * Read file content.
641
+ *
642
+ * Text files are paginated by line offset/limit.
643
+ * Binary files return full Uint8Array content (offset/limit ignored).
419
644
  *
420
645
  * @param filePath - Absolute file path
421
646
  * @param offset - Line offset to start reading from (0-indexed)
422
647
  * @param limit - Maximum number of lines to read
423
- * @returns Formatted file content with line numbers, or error message
648
+ * @returns ReadResult with content on success or error on failure
424
649
  */
425
650
  read(filePath, offset = 0, limit = 500) {
426
651
  const fileData = this.getFiles()[filePath];
427
- if (!fileData) return `Error: File '${filePath}' not found`;
428
- return formatReadResponse(fileData, offset, limit);
652
+ if (!fileData) return { error: `File '${filePath}' not found` };
653
+ const fileDataV2 = migrateToFileDataV2(fileData, filePath);
654
+ if (!isTextMimeType(fileDataV2.mimeType)) return {
655
+ content: fileDataV2.content,
656
+ mimeType: fileDataV2.mimeType
657
+ };
658
+ if (typeof fileDataV2.content !== "string") return { error: `File '${filePath}' has binary content but text MIME type` };
659
+ return {
660
+ content: fileDataV2.content.split("\n").slice(offset, offset + limit).join("\n"),
661
+ mimeType: fileDataV2.mimeType
662
+ };
429
663
  }
430
664
  /**
431
665
  * Read file content as raw FileData.
432
666
  *
433
667
  * @param filePath - Absolute file path
434
- * @returns Raw file content as FileData
668
+ * @returns ReadRawResult with raw file data on success or error on failure
435
669
  */
436
670
  readRaw(filePath) {
437
671
  const fileData = this.getFiles()[filePath];
438
- if (!fileData) throw new Error(`File '${filePath}' not found`);
439
- return fileData;
672
+ if (!fileData) return { error: `File '${filePath}' not found` };
673
+ return { data: fileData };
440
674
  }
441
675
  /**
442
676
  * Create a new file with content.
@@ -444,7 +678,13 @@ var StateBackend = class {
444
678
  */
445
679
  write(filePath, content) {
446
680
  if (filePath in this.getFiles()) return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
447
- const newFileData = createFileData(content);
681
+ const mimeType = getMimeType(filePath);
682
+ const newFileData = createFileData(content, void 0, this.fileFormat, mimeType);
683
+ const update = { [filePath]: newFileData };
684
+ if (!this.isLegacy) {
685
+ this.sendFilesUpdate(update);
686
+ return { path: filePath };
687
+ }
448
688
  return {
449
689
  path: filePath,
450
690
  filesUpdate: { [filePath]: newFileData }
@@ -461,6 +701,14 @@ var StateBackend = class {
461
701
  if (typeof result === "string") return { error: result };
462
702
  const [newContent, occurrences] = result;
463
703
  const newFileData = updateFileData(fileData, newContent);
704
+ const update = { [filePath]: newFileData };
705
+ if (!this.isLegacy) {
706
+ this.sendFilesUpdate(update);
707
+ return {
708
+ path: filePath,
709
+ occurrences
710
+ };
711
+ }
464
712
  return {
465
713
  path: filePath,
466
714
  filesUpdate: { [filePath]: newFileData },
@@ -468,23 +716,24 @@ var StateBackend = class {
468
716
  };
469
717
  }
470
718
  /**
471
- * Structured search results or error string for invalid input.
719
+ * Search file contents for a literal text pattern.
720
+ * Binary files are skipped.
472
721
  */
473
- grepRaw(pattern, path = "/", glob = null) {
474
- return grepMatchesFromFiles(this.getFiles(), pattern, path, glob);
722
+ grep(pattern, path = "/", glob = null) {
723
+ return { matches: grepMatchesFromFiles(this.getFiles(), pattern, path, glob) };
475
724
  }
476
725
  /**
477
726
  * Structured glob matching returning FileInfo objects.
478
727
  */
479
- globInfo(pattern, path = "/") {
728
+ glob(pattern, path = "/") {
480
729
  const files = this.getFiles();
481
730
  const result = globSearchFiles(files, pattern, path);
482
- if (result === "No files found") return [];
731
+ if (result === "No files found") return { files: [] };
483
732
  const paths = result.split("\n");
484
733
  const infos = [];
485
734
  for (const p of paths) {
486
735
  const fd = files[p];
487
- const size = fd ? fd.content.join("\n").length : 0;
736
+ const size = fd ? isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length : 0;
488
737
  infos.push({
489
738
  path: p,
490
739
  is_dir: false,
@@ -492,7 +741,7 @@ var StateBackend = class {
492
741
  modified_at: fd?.modified_at || ""
493
742
  });
494
743
  }
495
- return infos;
744
+ return { files: infos };
496
745
  }
497
746
  /**
498
747
  * Upload multiple files.
@@ -507,7 +756,9 @@ var StateBackend = class {
507
756
  const responses = [];
508
757
  const updates = {};
509
758
  for (const [path, content] of files) try {
510
- updates[path] = createFileData(new TextDecoder().decode(content));
759
+ const mimeType = getMimeType(path);
760
+ if (this.fileFormat === "v2" && !isTextMimeType(mimeType)) updates[path] = createFileData(content, void 0, "v2", mimeType);
761
+ else updates[path] = createFileData(new TextDecoder().decode(content), void 0, this.fileFormat, mimeType);
511
762
  responses.push({
512
763
  path,
513
764
  error: null
@@ -518,6 +769,10 @@ var StateBackend = class {
518
769
  error: "invalid_path"
519
770
  });
520
771
  }
772
+ if (!this.isLegacy) {
773
+ if (Object.keys(updates).length > 0) this.sendFilesUpdate(updates);
774
+ return responses;
775
+ }
521
776
  const result = responses;
522
777
  result.filesUpdate = updates;
523
778
  return result;
@@ -541,11 +796,17 @@ var StateBackend = class {
541
796
  });
542
797
  continue;
543
798
  }
544
- const contentStr = fileDataToString(fileData);
545
- const content = new TextEncoder().encode(contentStr);
546
- responses.push({
799
+ const fileDataV2 = migrateToFileDataV2(fileData, path);
800
+ if (typeof fileDataV2.content === "string") {
801
+ const content = new TextEncoder().encode(fileDataV2.content);
802
+ responses.push({
803
+ path,
804
+ content,
805
+ error: null
806
+ });
807
+ } else responses.push({
547
808
  path,
548
- content,
809
+ content: fileDataV2.content,
549
810
  error: null
550
811
  });
551
812
  }
@@ -561,6 +822,7 @@ var StateBackend = class {
561
822
  * - Pluggable backends (StateBackend, StoreBackend, FilesystemBackend, CompositeBackend)
562
823
  * - Tool result eviction for large outputs
563
824
  */
825
+ const INT_FORMATTER = new Intl.NumberFormat("en-US");
564
826
  /**
565
827
  * Tools that should be excluded from the large result eviction logic.
566
828
  *
@@ -607,6 +869,12 @@ const TOOLS_EXCLUDED_FROM_EVICTION = [
607
869
  "write_file"
608
870
  ];
609
871
  /**
872
+ * Maximum size for binary (non-text) files read via read_file, in bytes.
873
+ * Base64-encoded content is ~33% larger, so 10MB raw ≈ 13.3MB in context.
874
+ * This keeps inline multimodal payloads within all major provider limits.
875
+ */
876
+ const MAX_BINARY_READ_SIZE_BYTES = 10 * 1024 * 1024;
877
+ /**
610
878
  * Template for truncation message in read_file.
611
879
  * {file_path} will be filled in at runtime.
612
880
  */
@@ -616,16 +884,18 @@ const READ_FILE_TRUNCATION_MSG = `
616
884
  /**
617
885
  * Message template for evicted tool results.
618
886
  */
619
- const TOO_LARGE_TOOL_MSG = `Tool result too large, the result of this tool call {tool_call_id} was saved in the filesystem at this path: {file_path}
620
- You can read the result from the filesystem by using the read_file tool, but make sure to only read part of the result at a time.
621
- You can do this by specifying an offset and limit in the read_file tool call.
622
- For example, to read the first 100 lines, you can use the read_file tool with offset=0 and limit=100.
887
+ const TOO_LARGE_TOOL_MSG = context`
888
+ Tool result too large, the result of this tool call {tool_call_id} was saved in the filesystem at this path: {file_path}
889
+ You can read the result from the filesystem by using the read_file tool, but make sure to only read part of the result at a time.
890
+ You can do this by specifying an offset and limit in the read_file tool call.
891
+ For example, to read the first 100 lines, you can use the read_file tool with offset=0 and limit=100.
623
892
 
624
- Here is a preview showing the head and tail of the result (lines of the form
625
- ... [N lines truncated] ...
626
- indicate omitted lines in the middle of the content):
893
+ Here is a preview showing the head and tail of the result (lines of the form
894
+ ... [N lines truncated] ...
895
+ indicate omitted lines in the middle of the content):
627
896
 
628
- {content_sample}`;
897
+ {content_sample}
898
+ `;
629
899
  /**
630
900
  * Message template for evicted HumanMessages.
631
901
  */
@@ -701,14 +971,27 @@ function createContentPreview(contentStr, headLines = 5, tailLines = 5) {
701
971
  return headSample + truncationNotice + tailSample;
702
972
  }
703
973
  /**
704
- * Zod v3 schema for FileData (re-export from backends)
974
+ * Zod schema for legacy FileDataV1 (content as line array).
705
975
  */
706
- const FileDataSchema = z.object({
976
+ const FileDataV1Schema = z.object({
707
977
  content: z.array(z.string()),
708
978
  created_at: z.string(),
709
979
  modified_at: z.string()
710
980
  });
711
981
  /**
982
+ * Zod schema for FileDataV2 (content as string for text or Uint8Array for binary).
983
+ */
984
+ const FileDataV2Schema = z.object({
985
+ content: z.union([z.string(), z.instanceof(Uint8Array)]),
986
+ mimeType: z.string(),
987
+ created_at: z.string(),
988
+ modified_at: z.string()
989
+ });
990
+ /**
991
+ * Zod v3 schema for FileData (re-export from backends)
992
+ */
993
+ const FileDataSchema = z.union([FileDataV1Schema, FileDataV2Schema]);
994
+ /**
712
995
  * Reducer for files state that merges file updates with support for deletions.
713
996
  * When a file value is null, the file is deleted from state.
714
997
  * When a file value is non-null, it is added or updated in state.
@@ -744,118 +1027,141 @@ const FilesystemStateSchema = new StateSchema({ files: new ReducedValue(z.record
744
1027
  inputSchema: z.record(z.string(), FileDataSchema.nullable()).optional(),
745
1028
  reducer: fileDataReducer
746
1029
  }) });
747
- const FILESYSTEM_SYSTEM_PROMPT = `## Filesystem Tools \`ls\`, \`read_file\`, \`write_file\`, \`edit_file\`, \`glob\`, \`grep\`
1030
+ const FILESYSTEM_SYSTEM_PROMPT = context`
1031
+ ## Following Conventions
748
1032
 
749
- You have access to a filesystem which you can interact with using these tools.
750
- All file paths must start with a /.
1033
+ - Read files before editing understand existing content before making changes
1034
+ - Mimic existing style, naming conventions, and patterns
751
1035
 
752
- - ls: list files in a directory (requires absolute path)
753
- - read_file: read a file from the filesystem
754
- - write_file: write to a file in the filesystem
755
- - edit_file: edit a file in the filesystem
756
- - glob: find files matching a pattern (e.g., "**/*.py")
757
- - grep: search for text within files`;
758
- const LS_TOOL_DESCRIPTION = `Lists all files in a directory.
1036
+ ## Filesystem Tools \`ls\`, \`read_file\`, \`write_file\`, \`edit_file\`, \`glob\`, \`grep\`
759
1037
 
760
- This is useful for exploring the filesystem and finding the right file to read or edit.
761
- You should almost ALWAYS use this tool before using the read_file or edit_file tools.`;
762
- const READ_FILE_TOOL_DESCRIPTION = `Reads a file from the filesystem.
1038
+ You have access to a filesystem which you can interact with using these tools.
1039
+ All file paths must start with a /.
763
1040
 
764
- Assume this tool is able to read all files. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
1041
+ - ls: list files in a directory (requires absolute path)
1042
+ - read_file: read a file from the filesystem
1043
+ - write_file: write to a file in the filesystem
1044
+ - edit_file: edit a file in the filesystem
1045
+ - glob: find files matching a pattern (e.g., "**/*.py")
1046
+ - grep: search for text within files
1047
+ `;
1048
+ const LS_TOOL_DESCRIPTION = context`
1049
+ Lists all files in a directory.
765
1050
 
766
- Usage:
767
- - By default, it reads up to 100 lines starting from the beginning of the file
768
- - **IMPORTANT for large files and codebase exploration**: Use pagination with offset and limit parameters to avoid context overflow
769
- - First scan: read_file(path, limit=100) to see file structure
770
- - Read more sections: read_file(path, offset=100, limit=200) for next 200 lines
771
- - Only omit limit (read full file) when necessary for editing
772
- - Specify offset and limit: read_file(path, offset=0, limit=100) reads first 100 lines
773
- - Results are returned using cat -n format, with line numbers starting at 1
774
- - Lines longer than 10,000 characters will be split into multiple lines with continuation markers (e.g., 5.1, 5.2, etc.). When you specify a limit, these continuation lines count towards the limit.
775
- - You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
776
- - If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
777
- - You should ALWAYS make sure a file has been read before editing it.`;
778
- const WRITE_FILE_TOOL_DESCRIPTION = `Writes to a new file in the filesystem.
1051
+ This is useful for exploring the filesystem and finding the right file to read or edit.
1052
+ You should almost ALWAYS use this tool before using the read_file or edit_file tools.
1053
+ `;
1054
+ const READ_FILE_TOOL_DESCRIPTION = context`
1055
+ Reads a file from the filesystem.
1056
+
1057
+ Assume this tool is able to read all files. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
1058
+
1059
+ Usage:
1060
+ - By default, it reads up to 100 lines starting from the beginning of the file
1061
+ - **IMPORTANT for large files and codebase exploration**: Use pagination with offset and limit parameters to avoid context overflow
1062
+ - First scan: read_file(path, limit=100) to see file structure
1063
+ - Read more sections: read_file(path, offset=100, limit=200) for next 200 lines
1064
+ - Only omit limit (read full file) when necessary for editing
1065
+ - Specify offset and limit: read_file(path, offset=0, limit=100) reads first 100 lines
1066
+ - Results are returned using cat -n format, with line numbers starting at 1
1067
+ - Lines longer than ${INT_FORMATTER.format(MAX_LINE_LENGTH)} characters will be split into multiple lines with continuation markers (e.g., 5.1, 5.2, etc.). When you specify a limit, these continuation lines count towards the limit.
1068
+ - You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
1069
+ - If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
1070
+ - You should ALWAYS make sure a file has been read before editing it.
1071
+ `;
1072
+ const WRITE_FILE_TOOL_DESCRIPTION = context`
1073
+ Writes to a new file in the filesystem.
779
1074
 
780
- Usage:
781
- - The write_file tool will create a new file.
782
- - Prefer to edit existing files (with the edit_file tool) over creating new ones when possible.`;
783
- const EDIT_FILE_TOOL_DESCRIPTION = `Performs exact string replacements in files.
1075
+ Usage:
1076
+ - The write_file tool will create a new file.
1077
+ - Prefer to edit existing files (with the edit_file tool) over creating new ones when possible.
1078
+ `;
1079
+ const EDIT_FILE_TOOL_DESCRIPTION = context`
1080
+ Performs exact string replacements in files.
784
1081
 
785
- Usage:
786
- - You must read the file before editing. This tool will error if you attempt an edit without reading the file first.
787
- - When editing, preserve the exact indentation (tabs/spaces) from the read output. Never include line number prefixes in old_string or new_string.
788
- - ALWAYS prefer editing existing files over creating new ones.
789
- - Only use emojis if the user explicitly requests it.`;
790
- const GLOB_TOOL_DESCRIPTION = `Find files matching a glob pattern.
1082
+ Usage:
1083
+ - You must read the file before editing. This tool will error if you attempt an edit without reading the file first.
1084
+ - When editing, preserve the exact indentation (tabs/spaces) from the read output. Never include line number prefixes in old_string or new_string.
1085
+ - ALWAYS prefer editing existing files over creating new ones.
1086
+ - Only use emojis if the user explicitly requests it.
1087
+ `;
1088
+ const GLOB_TOOL_DESCRIPTION = context`
1089
+ Find files matching a glob pattern.
791
1090
 
792
- Supports standard glob patterns: \`*\` (any characters), \`**\` (any directories), \`?\` (single character).
793
- Returns a list of absolute file paths that match the pattern.
1091
+ Supports standard glob patterns: \`*\` (any characters), \`**\` (any directories), \`?\` (single character).
1092
+ Returns a list of absolute file paths that match the pattern.
794
1093
 
795
- Examples:
796
- - \`**/*.py\` - Find all Python files
797
- - \`*.txt\` - Find all text files in root
798
- - \`/subdir/**/*.md\` - Find all markdown files under /subdir`;
799
- const GREP_TOOL_DESCRIPTION = `Search for a text pattern across files.
1094
+ Examples:
1095
+ - \`**/*.py\` - Find all Python files
1096
+ - \`*.txt\` - Find all text files in root
1097
+ - \`/subdir/**/*.md\` - Find all markdown files under /subdir
1098
+ `;
1099
+ const GREP_TOOL_DESCRIPTION = context`
1100
+ Search for a text pattern across files.
800
1101
 
801
- Searches for literal text (not regex) and returns matching files or content based on output_mode.
802
- Special characters like parentheses, brackets, pipes, etc. are treated as literal characters, not regex operators.
1102
+ Searches for literal text (not regex) and returns matching files or content based on output_mode.
1103
+ Special characters like parentheses, brackets, pipes, etc. are treated as literal characters, not regex operators.
803
1104
 
804
- Examples:
805
- - Search all files: \`grep(pattern="TODO")\`
806
- - Search Python files only: \`grep(pattern="import", glob="*.py")\`
807
- - Show matching lines: \`grep(pattern="error", output_mode="content")\`
808
- - Search for code with special chars: \`grep(pattern="def __init__(self):")\``;
809
- const EXECUTE_TOOL_DESCRIPTION = `Executes a shell command in an isolated sandbox environment.
1105
+ Examples:
1106
+ - Search all files: \`grep(pattern="TODO")\`
1107
+ - Search Python files only: \`grep(pattern="import", glob="*.py")\`
1108
+ - Show matching lines: \`grep(pattern="error", output_mode="content")\`
1109
+ - Search for code with special chars: \`grep(pattern="def __init__(self):")\`
1110
+ `;
1111
+ const EXECUTE_TOOL_DESCRIPTION = context`
1112
+ Executes a shell command in an isolated sandbox environment.
810
1113
 
811
- Usage:
812
- Executes a given command in the sandbox environment with proper handling and security measures.
813
- Before executing the command, please follow these steps:
1114
+ Usage:
1115
+ Executes a given command in the sandbox environment with proper handling and security measures.
1116
+ Before executing the command, please follow these steps:
814
1117
 
815
- 1. Directory Verification:
816
- - If the command will create new directories or files, first use the ls tool to verify the parent directory exists and is the correct location
817
- - For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory
1118
+ 1. Directory Verification:
1119
+ - If the command will create new directories or files, first use the ls tool to verify the parent directory exists and is the correct location
1120
+ - For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory
818
1121
 
819
- 2. Command Execution:
820
- - Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
821
- - Examples of proper quoting:
822
- - cd "/Users/name/My Documents" (correct)
823
- - cd /Users/name/My Documents (incorrect - will fail)
824
- - python "/path/with spaces/script.py" (correct)
825
- - python /path/with spaces/script.py (incorrect - will fail)
826
- - After ensuring proper quoting, execute the command
827
- - Capture the output of the command
1122
+ 2. Command Execution:
1123
+ - Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
1124
+ - Examples of proper quoting:
1125
+ - cd "/Users/name/My Documents" (correct)
1126
+ - cd /Users/name/My Documents (incorrect - will fail)
1127
+ - python "/path/with spaces/script.py" (correct)
1128
+ - python /path/with spaces/script.py (incorrect - will fail)
1129
+ - After ensuring proper quoting, execute the command
1130
+ - Capture the output of the command
828
1131
 
829
- Usage notes:
830
- - Commands run in an isolated sandbox environment
831
- - Returns combined stdout/stderr output with exit code
832
- - If the output is very large, it may be truncated
833
- - VERY IMPORTANT: You MUST avoid using search commands like find and grep. Instead use the grep, glob tools to search. You MUST avoid read tools like cat, head, tail, and use read_file to read files.
834
- - When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings)
835
- - Use '&&' when commands depend on each other (e.g., "mkdir dir && cd dir")
836
- - Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
837
- - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of cd
1132
+ Usage notes:
1133
+ - Commands run in an isolated sandbox environment
1134
+ - Returns combined stdout/stderr output with exit code
1135
+ - If the output is very large, it may be truncated
1136
+ - VERY IMPORTANT: You MUST avoid using search commands like find and grep. Instead use the grep, glob tools to search. You MUST avoid read tools like cat, head, tail, and use read_file to read files.
1137
+ - When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings)
1138
+ - Use '&&' when commands depend on each other (e.g., "mkdir dir && cd dir")
1139
+ - Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
1140
+ - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of cd
838
1141
 
839
- Examples:
840
- Good examples:
841
- - execute(command="pytest /foo/bar/tests")
842
- - execute(command="python /path/to/script.py")
843
- - execute(command="npm install && npm test")
1142
+ Examples:
1143
+ Good examples:
1144
+ - execute(command="pytest /foo/bar/tests")
1145
+ - execute(command="python /path/to/script.py")
1146
+ - execute(command="npm install && npm test")
844
1147
 
845
- Bad examples (avoid these):
846
- - execute(command="cd /foo/bar && pytest tests") # Use absolute path instead
847
- - execute(command="cat file.txt") # Use read_file tool instead
848
- - execute(command="find . -name '*.py'") # Use glob tool instead
849
- - execute(command="grep -r 'pattern' .") # Use grep tool instead
1148
+ Bad examples (avoid these):
1149
+ - execute(command="cd /foo/bar && pytest tests") # Use absolute path instead
1150
+ - execute(command="cat file.txt") # Use read_file tool instead
1151
+ - execute(command="find . -name '*.py'") # Use glob tool instead
1152
+ - execute(command="grep -r 'pattern' .") # Use grep tool instead
850
1153
 
851
- Note: This tool is only available if the backend supports execution (SandboxBackendProtocol).
852
- If execution is not supported, the tool will return an error message.`;
853
- const EXECUTION_SYSTEM_PROMPT = `## Execute Tool \`execute\`
1154
+ Note: This tool is only available if the backend supports execution (SandboxBackendProtocol).
1155
+ If execution is not supported, the tool will return an error message.
1156
+ `;
1157
+ const EXECUTION_SYSTEM_PROMPT = context`
1158
+ ## Execute Tool \`execute\`
854
1159
 
855
- You have access to an \`execute\` tool for running shell commands in a sandboxed environment.
856
- Use this tool to run commands, scripts, tests, builds, and other shell operations.
1160
+ You have access to an \`execute\` tool for running shell commands in a sandboxed environment.
1161
+ Use this tool to run commands, scripts, tests, builds, and other shell operations.
857
1162
 
858
- - execute: run a shell command in the sandbox (returns output and exit code)`;
1163
+ - execute: run a shell command in the sandbox (returns output and exit code)
1164
+ `;
859
1165
  /**
860
1166
  * Create ls tool using backend.
861
1167
  */
@@ -864,7 +1170,9 @@ function createLsTool(backend, options) {
864
1170
  return tool(async (input, runtime) => {
865
1171
  const resolvedBackend = await resolveBackend(backend, runtime);
866
1172
  const path = input.path || "/";
867
- const infos = await resolvedBackend.lsInfo(path);
1173
+ const lsResult = await resolvedBackend.ls(path);
1174
+ if (lsResult.error) return `Error listing files: ${lsResult.error}`;
1175
+ const infos = lsResult.files || [];
868
1176
  if (infos.length === 0) return `No files found in ${path}`;
869
1177
  const lines = [];
870
1178
  for (const info of infos) if (info.is_dir) lines.push(`${info.path} (directory)`);
@@ -889,15 +1197,64 @@ function createReadFileTool(backend, options) {
889
1197
  return tool(async (input, runtime) => {
890
1198
  const resolvedBackend = await resolveBackend(backend, runtime);
891
1199
  const { file_path, offset = 0, limit = 100 } = input;
892
- let result = await resolvedBackend.read(file_path, offset, limit);
893
- const lines = result.split("\n");
894
- if (lines.length > limit) result = lines.slice(0, limit).join("\n");
895
- if (toolTokenLimitBeforeEvict && result.length >= 4 * toolTokenLimitBeforeEvict) {
1200
+ const readResult = await resolvedBackend.read(file_path, offset, limit);
1201
+ if (readResult.error) return [{
1202
+ type: "text",
1203
+ text: `Error: ${readResult.error}`
1204
+ }];
1205
+ const mimeType = readResult.mimeType ?? getMimeType(file_path);
1206
+ if (!isTextMimeType(mimeType)) {
1207
+ const binaryContent = readResult.content;
1208
+ if (!binaryContent) return [{
1209
+ type: "text",
1210
+ text: `Error: expected binary content for '${file_path}'`
1211
+ }];
1212
+ let base64Data;
1213
+ if (typeof binaryContent === "string") base64Data = binaryContent;
1214
+ else if (ArrayBuffer.isView(binaryContent)) base64Data = Buffer.from(binaryContent).toString("base64");
1215
+ else {
1216
+ const values = Object.values(binaryContent);
1217
+ base64Data = Buffer.from(new Uint8Array(values)).toString("base64");
1218
+ }
1219
+ const sizeBytes = Math.ceil(base64Data.length * 3 / 4);
1220
+ if (sizeBytes > 10485760) return [{
1221
+ type: "text",
1222
+ text: `Error: file too large to read (${Math.round(sizeBytes / (1024 * 1024))}MB exceeds ${MAX_BINARY_READ_SIZE_BYTES / (1024 * 1024)}MB limit for binary files)`
1223
+ }];
1224
+ if (mimeType.startsWith("image/")) return [{
1225
+ type: "image",
1226
+ mimeType,
1227
+ data: base64Data
1228
+ }];
1229
+ if (mimeType.startsWith("audio/")) return [{
1230
+ type: "audio",
1231
+ mimeType,
1232
+ data: base64Data
1233
+ }];
1234
+ if (mimeType.startsWith("video/")) return [{
1235
+ type: "video",
1236
+ mimeType,
1237
+ data: base64Data
1238
+ }];
1239
+ return [{
1240
+ type: "file",
1241
+ mimeType,
1242
+ data: base64Data
1243
+ }];
1244
+ }
1245
+ let content = typeof readResult.content === "string" ? readResult.content : "";
1246
+ const lines = content.split("\n");
1247
+ if (lines.length > limit) content = lines.slice(0, limit).join("\n");
1248
+ let formatted = formatContentWithLineNumbers(content, offset + 1);
1249
+ if (toolTokenLimitBeforeEvict && formatted.length >= 4 * toolTokenLimitBeforeEvict) {
896
1250
  const truncationMsg = READ_FILE_TRUNCATION_MSG.replace("{file_path}", file_path);
897
1251
  const maxContentLength = 4 * toolTokenLimitBeforeEvict - truncationMsg.length;
898
- result = result.substring(0, maxContentLength) + truncationMsg;
1252
+ formatted = formatted.substring(0, maxContentLength) + truncationMsg;
899
1253
  }
900
- return result;
1254
+ return [{
1255
+ type: "text",
1256
+ text: formatted
1257
+ }];
901
1258
  }, {
902
1259
  name: "read_file",
903
1260
  description: customDescription || READ_FILE_TOOL_DESCRIPTION,
@@ -978,7 +1335,9 @@ function createGlobTool(backend, options) {
978
1335
  return tool(async (input, runtime) => {
979
1336
  const resolvedBackend = await resolveBackend(backend, runtime);
980
1337
  const { pattern, path = "/" } = input;
981
- const infos = await resolvedBackend.globInfo(pattern, path);
1338
+ const globResult = await resolvedBackend.glob(pattern, path);
1339
+ if (globResult.error) return `Error finding files: ${globResult.error}`;
1340
+ const infos = globResult.files || [];
982
1341
  if (infos.length === 0) return `No files found matching pattern '${pattern}'`;
983
1342
  const result = truncateIfTooLong(infos.map((info) => info.path));
984
1343
  if (Array.isArray(result)) return result.join("\n");
@@ -1000,12 +1359,13 @@ function createGrepTool(backend, options) {
1000
1359
  return tool(async (input, runtime) => {
1001
1360
  const resolvedBackend = await resolveBackend(backend, runtime);
1002
1361
  const { pattern, path = "/", glob = null } = input;
1003
- const result = await resolvedBackend.grepRaw(pattern, path, glob);
1004
- if (typeof result === "string") return result;
1005
- if (result.length === 0) return `No matches found for pattern '${pattern}'`;
1362
+ const result = await resolvedBackend.grep(pattern, path, glob);
1363
+ if (result.error) return result.error;
1364
+ const matches = result.matches ?? [];
1365
+ if (matches.length === 0) return `No matches found for pattern '${pattern}'`;
1006
1366
  const lines = [];
1007
1367
  let currentFile = null;
1008
- for (const match of result) {
1368
+ for (const match of matches) {
1009
1369
  if (match.path !== currentFile) {
1010
1370
  currentFile = match.path;
1011
1371
  lines.push(`\n${currentFile}:`);
@@ -1021,7 +1381,7 @@ function createGrepTool(backend, options) {
1021
1381
  schema: z.object({
1022
1382
  pattern: z.string().describe("Regex pattern to search for"),
1023
1383
  path: z.string().optional().default("/").describe("Base path to search from (default: /)"),
1024
- glob: z.string().optional().nullable().describe("Optional glob pattern to filter files (e.g., '*.py')")
1384
+ glob: z.string().optional().nullable().default(null).describe("Optional glob pattern to filter files (e.g., '*.py')")
1025
1385
  })
1026
1386
  });
1027
1387
  }
@@ -1221,117 +1581,117 @@ const EXCLUDED_STATE_KEYS = [
1221
1581
  */
1222
1582
  const DEFAULT_GENERAL_PURPOSE_DESCRIPTION = "General-purpose agent for researching complex questions, searching for files and content, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. This agent has access to all tools as the main agent.";
1223
1583
  function getTaskToolDescription(subagentDescriptions) {
1224
- return `
1225
- Launch an ephemeral subagent to handle complex, multi-step independent tasks with isolated context windows.
1584
+ return context`
1585
+ Launch an ephemeral subagent to handle complex, multi-step independent tasks with isolated context windows.
1226
1586
 
1227
- Available agent types and the tools they have access to:
1228
- ${subagentDescriptions.join("\n")}
1587
+ Available agent types and the tools they have access to:
1588
+ ${subagentDescriptions.join("\n")}
1229
1589
 
1230
- When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
1590
+ When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
1231
1591
 
1232
- ## Usage notes:
1233
- 1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
1234
- 2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
1235
- 3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
1236
- 4. The agent's outputs should generally be trusted
1237
- 5. Clearly tell the agent whether you expect it to create content, perform analysis, or just do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
1238
- 6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
1239
- 7. When only the general-purpose agent is provided, you should use it for all tasks. It is great for isolating context and token usage, and completing specific, complex tasks, as it has all the same capabilities as the main agent.
1592
+ ## Usage notes:
1593
+ 1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
1594
+ 2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
1595
+ 3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
1596
+ 4. The agent's outputs should generally be trusted
1597
+ 5. Clearly tell the agent whether you expect it to create content, perform analysis, or just do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
1598
+ 6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
1599
+ 7. When only the general-purpose agent is provided, you should use it for all tasks. It is great for isolating context and token usage, and completing specific, complex tasks, as it has all the same capabilities as the main agent.
1240
1600
 
1241
- ### Example usage of the general-purpose agent:
1601
+ ### Example usage of the general-purpose agent:
1242
1602
 
1243
- <example_agent_descriptions>
1244
- "general-purpose": use this agent for general purpose tasks, it has access to all tools as the main agent.
1245
- </example_agent_descriptions>
1603
+ <example_agent_descriptions>
1604
+ "general-purpose": use this agent for general purpose tasks, it has access to all tools as the main agent.
1605
+ </example_agent_descriptions>
1246
1606
 
1247
- <example>
1248
- User: "I want to conduct research on the accomplishments of Lebron James, Michael Jordan, and Kobe Bryant, and then compare them."
1249
- Assistant: *Uses the task tool in parallel to conduct isolated research on each of the three players*
1250
- Assistant: *Synthesizes the results of the three isolated research tasks and responds to the User*
1251
- <commentary>
1252
- Research is a complex, multi-step task in it of itself.
1253
- The research of each individual player is not dependent on the research of the other players.
1254
- The assistant uses the task tool to break down the complex objective into three isolated tasks.
1255
- Each research task only needs to worry about context and tokens about one player, then returns synthesized information about each player as the Tool Result.
1256
- This means each research task can dive deep and spend tokens and context deeply researching each player, but the final result is synthesized information, and saves us tokens in the long run when comparing the players to each other.
1257
- </commentary>
1258
- </example>
1607
+ <example>
1608
+ User: "I want to conduct research on the accomplishments of Lebron James, Michael Jordan, and Kobe Bryant, and then compare them."
1609
+ Assistant: *Uses the task tool in parallel to conduct isolated research on each of the three players*
1610
+ Assistant: *Synthesizes the results of the three isolated research tasks and responds to the User*
1611
+ <commentary>
1612
+ Research is a complex, multi-step task in it of itself.
1613
+ The research of each individual player is not dependent on the research of the other players.
1614
+ The assistant uses the task tool to break down the complex objective into three isolated tasks.
1615
+ Each research task only needs to worry about context and tokens about one player, then returns synthesized information about each player as the Tool Result.
1616
+ This means each research task can dive deep and spend tokens and context deeply researching each player, but the final result is synthesized information, and saves us tokens in the long run when comparing the players to each other.
1617
+ </commentary>
1618
+ </example>
1259
1619
 
1260
- <example>
1261
- User: "Analyze a single large code repository for security vulnerabilities and generate a report."
1262
- Assistant: *Launches a single \`task\` subagent for the repository analysis*
1263
- Assistant: *Receives report and integrates results into final summary*
1264
- <commentary>
1265
- Subagent is used to isolate a large, context-heavy task, even though there is only one. This prevents the main thread from being overloaded with details.
1266
- If the user then asks followup questions, we have a concise report to reference instead of the entire history of analysis and tool calls, which is good and saves us time and money.
1267
- </commentary>
1268
- </example>
1620
+ <example>
1621
+ User: "Analyze a single large code repository for security vulnerabilities and generate a report."
1622
+ Assistant: *Launches a single \`task\` subagent for the repository analysis*
1623
+ Assistant: *Receives report and integrates results into final summary*
1624
+ <commentary>
1625
+ Subagent is used to isolate a large, context-heavy task, even though there is only one. This prevents the main thread from being overloaded with details.
1626
+ If the user then asks followup questions, we have a concise report to reference instead of the entire history of analysis and tool calls, which is good and saves us time and money.
1627
+ </commentary>
1628
+ </example>
1269
1629
 
1270
- <example>
1271
- User: "Schedule two meetings for me and prepare agendas for each."
1272
- Assistant: *Calls the task tool in parallel to launch two \`task\` subagents (one per meeting) to prepare agendas*
1273
- Assistant: *Returns final schedules and agendas*
1274
- <commentary>
1275
- Tasks are simple individually, but subagents help silo agenda preparation.
1276
- Each subagent only needs to worry about the agenda for one meeting.
1277
- </commentary>
1278
- </example>
1630
+ <example>
1631
+ User: "Schedule two meetings for me and prepare agendas for each."
1632
+ Assistant: *Calls the task tool in parallel to launch two \`task\` subagents (one per meeting) to prepare agendas*
1633
+ Assistant: *Returns final schedules and agendas*
1634
+ <commentary>
1635
+ Tasks are simple individually, but subagents help silo agenda preparation.
1636
+ Each subagent only needs to worry about the agenda for one meeting.
1637
+ </commentary>
1638
+ </example>
1279
1639
 
1280
- <example>
1281
- User: "I want to order a pizza from Dominos, order a burger from McDonald's, and order a salad from Subway."
1282
- Assistant: *Calls tools directly in parallel to order a pizza from Dominos, a burger from McDonald's, and a salad from Subway*
1283
- <commentary>
1284
- The assistant did not use the task tool because the objective is super simple and clear and only requires a few trivial tool calls.
1285
- It is better to just complete the task directly and NOT use the \`task\`tool.
1286
- </commentary>
1287
- </example>
1640
+ <example>
1641
+ User: "I want to order a pizza from Dominos, order a burger from McDonald's, and order a salad from Subway."
1642
+ Assistant: *Calls tools directly in parallel to order a pizza from Dominos, a burger from McDonald's, and a salad from Subway*
1643
+ <commentary>
1644
+ The assistant did not use the task tool because the objective is super simple and clear and only requires a few trivial tool calls.
1645
+ It is better to just complete the task directly and NOT use the \`task\`tool.
1646
+ </commentary>
1647
+ </example>
1288
1648
 
1289
- ### Example usage with custom agents:
1649
+ ### Example usage with custom agents:
1290
1650
 
1291
- <example_agent_descriptions>
1292
- "content-reviewer": use this agent after you are done creating significant content or documents
1293
- "greeting-responder": use this agent when to respond to user greetings with a friendly joke
1294
- "research-analyst": use this agent to conduct thorough research on complex topics
1295
- </example_agent_description>
1651
+ <example_agent_descriptions>
1652
+ "content-reviewer": use this agent after you are done creating significant content or documents
1653
+ "greeting-responder": use this agent when to respond to user greetings with a friendly joke
1654
+ "research-analyst": use this agent to conduct thorough research on complex topics
1655
+ </example_agent_description>
1296
1656
 
1297
- <example>
1298
- user: "Please write a function that checks if a number is prime"
1299
- assistant: Sure let me write a function that checks if a number is prime
1300
- assistant: First let me use the Write tool to write a function that checks if a number is prime
1301
- assistant: I'm going to use the Write tool to write the following code:
1302
- <code>
1303
- function isPrime(n) {
1304
- if (n <= 1) return false
1305
- for (let i = 2; i * i <= n; i++) {
1306
- if (n % i === 0) return false
1307
- }
1308
- return true
1309
- }
1310
- </code>
1311
- <commentary>
1312
- Since significant content was created and the task was completed, now use the content-reviewer agent to review the work
1313
- </commentary>
1314
- assistant: Now let me use the content-reviewer agent to review the code
1315
- assistant: Uses the Task tool to launch with the content-reviewer agent
1316
- </example>
1657
+ <example>
1658
+ user: "Please write a function that checks if a number is prime"
1659
+ assistant: Sure let me write a function that checks if a number is prime
1660
+ assistant: First let me use the Write tool to write a function that checks if a number is prime
1661
+ assistant: I'm going to use the Write tool to write the following code:
1662
+ <code>
1663
+ function isPrime(n) {{
1664
+ if (n <= 1) return false
1665
+ for (let i = 2; i * i <= n; i++) {{
1666
+ if (n % i === 0) return false
1667
+ }}
1668
+ return true
1669
+ }}
1670
+ </code>
1671
+ <commentary>
1672
+ Since significant content was created and the task was completed, now use the content-reviewer agent to review the work
1673
+ </commentary>
1674
+ assistant: Now let me use the content-reviewer agent to review the code
1675
+ assistant: Uses the Task tool to launch with the content-reviewer agent
1676
+ </example>
1317
1677
 
1318
- <example>
1319
- user: "Can you help me research the environmental impact of different renewable energy sources and create a comprehensive report?"
1320
- <commentary>
1321
- This is a complex research task that would benefit from using the research-analyst agent to conduct thorough analysis
1322
- </commentary>
1323
- assistant: I'll help you research the environmental impact of renewable energy sources. Let me use the research-analyst agent to conduct comprehensive research on this topic.
1324
- assistant: Uses the Task tool to launch with the research-analyst agent, providing detailed instructions about what research to conduct and what format the report should take
1325
- </example>
1678
+ <example>
1679
+ user: "Can you help me research the environmental impact of different renewable energy sources and create a comprehensive report?"
1680
+ <commentary>
1681
+ This is a complex research task that would benefit from using the research-analyst agent to conduct thorough analysis
1682
+ </commentary>
1683
+ assistant: I'll help you research the environmental impact of renewable energy sources. Let me use the research-analyst agent to conduct comprehensive research on this topic.
1684
+ assistant: Uses the Task tool to launch with the research-analyst agent, providing detailed instructions about what research to conduct and what format the report should take
1685
+ </example>
1326
1686
 
1327
- <example>
1328
- user: "Hello"
1329
- <commentary>
1330
- Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
1331
- </commentary>
1332
- assistant: "I'm going to use the Task tool to launch with the greeting-responder agent"
1333
- </example>
1334
- `.trim();
1687
+ <example>
1688
+ user: "Hello"
1689
+ <commentary>
1690
+ Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
1691
+ </commentary>
1692
+ assistant: "I'm going to use the Task tool to launch with the greeting-responder agent"
1693
+ </example>
1694
+ `;
1335
1695
  }
1336
1696
  /**
1337
1697
  * System prompt section that explains how to use the task tool for spawning subagents.
@@ -1346,33 +1706,35 @@ assistant: "I'm going to use the Task tool to launch with the greeting-responder
1346
1706
  * You can provide a custom `systemPrompt` to `createSubAgentMiddleware` to override
1347
1707
  * or extend this default.
1348
1708
  */
1349
- const TASK_SYSTEM_PROMPT = `## \`task\` (subagent spawner)
1709
+ const TASK_SYSTEM_PROMPT = context`
1710
+ ## \`task\` (subagent spawner)
1350
1711
 
1351
- You have access to a \`task\` tool to launch short-lived subagents that handle isolated tasks. These agents are ephemeral — they live only for the duration of the task and return a single result.
1712
+ You have access to a \`task\` tool to launch short-lived subagents that handle isolated tasks. These agents are ephemeral — they live only for the duration of the task and return a single result.
1352
1713
 
1353
- When to use the task tool:
1354
- - When a task is complex and multi-step, and can be fully delegated in isolation
1355
- - When a task is independent of other tasks and can run in parallel
1356
- - When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread
1357
- - When sandboxing improves reliability (e.g. code execution, structured searches, data formatting)
1358
- - When you only care about the output of the subagent, and not the intermediate steps (ex. performing a lot of research and then returned a synthesized report, performing a series of computations or lookups to achieve a concise, relevant answer.)
1714
+ When to use the task tool:
1715
+ - When a task is complex and multi-step, and can be fully delegated in isolation
1716
+ - When a task is independent of other tasks and can run in parallel
1717
+ - When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread
1718
+ - When sandboxing improves reliability (e.g. code execution, structured searches, data formatting)
1719
+ - When you only care about the output of the subagent, and not the intermediate steps (ex. performing a lot of research and then returned a synthesized report, performing a series of computations or lookups to achieve a concise, relevant answer.)
1359
1720
 
1360
- Subagent lifecycle:
1361
- 1. **Spawn** → Provide clear role, instructions, and expected output
1362
- 2. **Run** → The subagent completes the task autonomously
1363
- 3. **Return** → The subagent provides a single structured result
1364
- 4. **Reconcile** → Incorporate or synthesize the result into the main thread
1721
+ Subagent lifecycle:
1722
+ 1. **Spawn** → Provide clear role, instructions, and expected output
1723
+ 2. **Run** → The subagent completes the task autonomously
1724
+ 3. **Return** → The subagent provides a single structured result
1725
+ 4. **Reconcile** → Incorporate or synthesize the result into the main thread
1365
1726
 
1366
- When NOT to use the task tool:
1367
- - If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them)
1368
- - If the task is trivial (a few tool calls or simple lookup)
1369
- - If delegating does not reduce token usage, complexity, or context switching
1370
- - If splitting would add latency without benefit
1727
+ When NOT to use the task tool:
1728
+ - If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them)
1729
+ - If the task is trivial (a few tool calls or simple lookup)
1730
+ - If delegating does not reduce token usage, complexity, or context switching
1731
+ - If splitting would add latency without benefit
1371
1732
 
1372
- ## Important Task Tool Usage Notes to Remember
1373
- - Whenever possible, parallelize the work that you do. This is true for both tool_calls, and for tasks. Whenever you have independent steps to complete - make tool_calls, or kick off tasks (subagents) in parallel to accomplish them faster. This saves time for the user, which is incredibly important.
1374
- - Remember to use the \`task\` tool to silo independent tasks within a multi-part objective.
1375
- - You should use the \`task\` tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient.`;
1733
+ ## Important Task Tool Usage Notes to Remember
1734
+ - Whenever possible, parallelize the work that you do. This is true for both tool_calls, and for tasks. Whenever you have independent steps to complete - make tool_calls, or kick off tasks (subagents) in parallel to accomplish them faster. This saves time for the user, which is incredibly important.
1735
+ - Remember to use the \`task\` tool to silo independent tasks within a multi-part objective.
1736
+ - You should use the \`task\` tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient.
1737
+ `;
1376
1738
  /**
1377
1739
  * Base specification for the general-purpose subagent.
1378
1740
  *
@@ -1781,65 +2143,67 @@ const MemoryStateSchema = new StateSchema({
1781
2143
  * Default system prompt template for memory.
1782
2144
  * Ported from Python's comprehensive memory guidelines.
1783
2145
  */
1784
- const MEMORY_SYSTEM_PROMPT = `<agent_memory>
1785
- {memory_contents}
1786
- </agent_memory>
2146
+ const MEMORY_SYSTEM_PROMPT = context`
2147
+ <agent_memory>
2148
+ {memory_contents}
2149
+ </agent_memory>
1787
2150
 
1788
- <memory_guidelines>
1789
- The above <agent_memory> was loaded in from files in your filesystem. As you learn from your interactions with the user, you can save new knowledge by calling the \`edit_file\` tool.
2151
+ <memory_guidelines>
2152
+ The above <agent_memory> was loaded in from files in your filesystem. As you learn from your interactions with the user, you can save new knowledge by calling the \`edit_file\` tool.
1790
2153
 
1791
- **Learning from feedback:**
1792
- - One of your MAIN PRIORITIES is to learn from your interactions with the user. These learnings can be implicit or explicit. This means that in the future, you will remember this important information.
1793
- - When you need to remember something, updating memory must be your FIRST, IMMEDIATE action - before responding to the user, before calling other tools, before doing anything else. Just update memory immediately.
1794
- - When user says something is better/worse, capture WHY and encode it as a pattern.
1795
- - Each correction is a chance to improve permanently - don't just fix the immediate issue, update your instructions.
1796
- - A great opportunity to update your memories is when the user interrupts a tool call and provides feedback. You should update your memories immediately before revising the tool call.
1797
- - Look for the underlying principle behind corrections, not just the specific mistake.
1798
- - The user might not explicitly ask you to remember something, but if they provide information that is useful for future use, you should update your memories immediately.
2154
+ **Learning from feedback:**
2155
+ - One of your MAIN PRIORITIES is to learn from your interactions with the user. These learnings can be implicit or explicit. This means that in the future, you will remember this important information.
2156
+ - When you need to remember something, updating memory must be your FIRST, IMMEDIATE action - before responding to the user, before calling other tools, before doing anything else. Just update memory immediately.
2157
+ - When user says something is better/worse, capture WHY and encode it as a pattern.
2158
+ - Each correction is a chance to improve permanently - don't just fix the immediate issue, update your instructions.
2159
+ - A great opportunity to update your memories is when the user interrupts a tool call and provides feedback. You should update your memories immediately before revising the tool call.
2160
+ - Look for the underlying principle behind corrections, not just the specific mistake.
2161
+ - The user might not explicitly ask you to remember something, but if they provide information that is useful for future use, you should update your memories immediately.
1799
2162
 
1800
- **Asking for information:**
1801
- - If you lack context to perform an action (e.g. send a Slack DM, requires a user ID/email) you should explicitly ask the user for this information.
1802
- - It is preferred for you to ask for information, don't assume anything that you do not know!
1803
- - When the user provides information that is useful for future use, you should update your memories immediately.
2163
+ **Asking for information:**
2164
+ - If you lack context to perform an action (e.g. send a Slack DM, requires a user ID/email) you should explicitly ask the user for this information.
2165
+ - It is preferred for you to ask for information, don't assume anything that you do not know!
2166
+ - When the user provides information that is useful for future use, you should update your memories immediately.
1804
2167
 
1805
- **When to update memories:**
1806
- - When the user explicitly asks you to remember something (e.g., "remember my email", "save this preference")
1807
- - When the user describes your role or how you should behave (e.g., "you are a web researcher", "always do X")
1808
- - When the user gives feedback on your work - capture what was wrong and how to improve
1809
- - When the user provides information required for tool use (e.g., slack channel ID, email addresses)
1810
- - When the user provides context useful for future tasks, such as how to use tools, or which actions to take in a particular situation
1811
- - When you discover new patterns or preferences (coding styles, conventions, workflows)
2168
+ **When to update memories:**
2169
+ - When the user explicitly asks you to remember something (e.g., "remember my email", "save this preference")
2170
+ - When the user describes your role or how you should behave (e.g., "you are a web researcher", "always do X")
2171
+ - When the user gives feedback on your work - capture what was wrong and how to improve
2172
+ - When the user provides information required for tool use (e.g., slack channel ID, email addresses)
2173
+ - When the user provides context useful for future tasks, such as how to use tools, or which actions to take in a particular situation
2174
+ - When you discover new patterns or preferences (coding styles, conventions, workflows)
1812
2175
 
1813
- **When to NOT update memories:**
1814
- - When the information is temporary or transient (e.g., "I'm running late", "I'm on my phone right now")
1815
- - When the information is a one-time task request (e.g., "Find me a recipe", "What's 25 * 4?")
1816
- - When the information is a simple question that doesn't reveal lasting preferences (e.g., "What day is it?", "Can you explain X?")
1817
- - When the information is an acknowledgment or small talk (e.g., "Sounds good!", "Hello", "Thanks for that")
1818
- - When the information is stale or irrelevant in future conversations
1819
- - Never store API keys, access tokens, passwords, or any other credentials in any file, memory, or system prompt.
1820
- - If the user asks where to put API keys or provides an API key, do NOT echo or save it.
2176
+ **When to NOT update memories:**
2177
+ - When the information is temporary or transient (e.g., "I'm running late", "I'm on my phone right now")
2178
+ - When the information is a one-time task request (e.g., "Find me a recipe", "What's 25 * 4?")
2179
+ - When the information is a simple question that doesn't reveal lasting preferences (e.g., "What day is it?", "Can you explain X?")
2180
+ - When the information is an acknowledgment or small talk (e.g., "Sounds good!", "Hello", "Thanks for that")
2181
+ - When the information is stale or irrelevant in future conversations
2182
+ - Never store API keys, access tokens, passwords, or any other credentials in any file, memory, or system prompt.
2183
+ - If the user asks where to put API keys or provides an API key, do NOT echo or save it.
1821
2184
 
1822
- **Examples:**
1823
- Example 1 (remembering user information):
1824
- User: Can you connect to my google account?
1825
- Agent: Sure, I'll connect to your google account, what's your google account email?
1826
- User: john@example.com
1827
- Agent: Let me save this to my memory.
1828
- Tool Call: edit_file(...) -> remembers that the user's google account email is john@example.com
2185
+ **Examples:**
2186
+ Example 1 (remembering user information):
2187
+ User: Can you connect to my google account?
2188
+ Agent: Sure, I'll connect to your google account, what's your google account email?
2189
+ User: john@example.com
2190
+ Agent: Let me save this to my memory.
2191
+ Tool Call: edit_file(...) -> remembers that the user's google account email is john@example.com
1829
2192
 
1830
- Example 2 (remembering implicit user preferences):
1831
- User: Can you write me an example for creating a deep agent in LangChain?
1832
- Agent: Sure, I'll write you an example for creating a deep agent in LangChain <example code in Python>
1833
- User: Can you do this in JavaScript
1834
- Agent: Let me save this to my memory.
1835
- Tool Call: edit_file(...) -> remembers that the user prefers to get LangChain code examples in JavaScript
1836
- Agent: Sure, here is the JavaScript example<example code in JavaScript>
2193
+ Example 2 (remembering implicit user preferences):
2194
+ User: Can you write me an example for creating a deep agent in LangChain?
2195
+ Agent: Sure, I'll write you an example for creating a deep agent in LangChain <example code in Python>
2196
+ User: Can you do this in JavaScript
2197
+ Agent: Let me save this to my memory.
2198
+ Tool Call: edit_file(...) -> remembers that the user prefers to get LangChain code examples in JavaScript
2199
+ Agent: Sure, here is the JavaScript example<example code in JavaScript>
1837
2200
 
1838
- Example 3 (do not remember transient information):
1839
- User: I'm going to play basketball tonight so I will be offline for a few hours.
1840
- Agent: Okay I'll add a block to your calendar.
1841
- Tool Call: create_calendar_event(...) -> just calls a tool, does not commit anything to memory, as it is transient information
1842
- </memory_guidelines>`;
2201
+ Example 3 (do not remember transient information):
2202
+ User: I'm going to play basketball tonight so I will be offline for a few hours.
2203
+ Agent: Okay I'll add a block to your calendar.
2204
+ Tool Call: create_calendar_event(...) -> just calls a tool, does not commit anything to memory, as it is transient information
2205
+ </memory_guidelines>
2206
+ `;
1843
2207
  /**
1844
2208
  * Format loaded memory contents for injection into prompt.
1845
2209
  * Pairs memory locations with their contents for clarity.
@@ -1859,12 +2223,14 @@ function formatMemoryContents(contents, sources) {
1859
2223
  * @returns File content if found, null otherwise.
1860
2224
  */
1861
2225
  async function loadMemoryFromBackend(backend, path) {
1862
- if (!backend.downloadFiles) {
1863
- const content = await backend.read(path);
1864
- if (content.startsWith("Error:")) return null;
1865
- return content;
1866
- }
1867
- const results = await backend.downloadFiles([path]);
2226
+ const adaptedBackend = adaptBackendProtocol(backend);
2227
+ if (!adaptedBackend.downloadFiles) {
2228
+ const content = await adaptedBackend.read(path);
2229
+ if (content.error) return null;
2230
+ if (typeof content.content !== "string") return null;
2231
+ return content.content;
2232
+ }
2233
+ const results = await adaptedBackend.downloadFiles([path]);
1868
2234
  if (results.length !== 1) throw new Error(`Expected 1 response for path ${path}, got ${results.length}`);
1869
2235
  const response = results[0];
1870
2236
  if (response.error != null) {
@@ -2035,7 +2401,7 @@ Skills follow a **progressive disclosure** pattern - you know they exist (name +
2035
2401
  1. **Recognize when a skill applies**: Check if the user's task matches any skill's description
2036
2402
  2. **Read the skill's full instructions**: The skill list above shows the exact path to use with read_file
2037
2403
  3. **Follow the skill's instructions**: SKILL.md contains step-by-step workflows, best practices, and examples
2038
- 4. **Access supporting files**: Skills may include Python scripts, configs, or reference docs - use absolute paths
2404
+ 4. **Access supporting files**: Skills may include scripts, configs, or reference docs - use absolute paths
2039
2405
 
2040
2406
  **When to Use Skills:**
2041
2407
  - When the user's request matches a skill's domain (e.g., "research X" → web-research skill)
@@ -2047,7 +2413,7 @@ Skills follow a **progressive disclosure** pattern - you know they exist (name +
2047
2413
  - The skill list above shows the full path for each skill's SKILL.md file
2048
2414
 
2049
2415
  **Executing Skill Scripts:**
2050
- Skills may contain Python scripts or other executable files. Always use absolute paths from the skill list.
2416
+ Skills may contain scripts or other executable files. Always use absolute paths from the skill list.
2051
2417
 
2052
2418
  **Example Workflow:**
2053
2419
 
@@ -2216,12 +2582,15 @@ function parseSkillMetadataFromContent(content, skillPath, directoryName) {
2216
2582
  * List all skills from a backend source.
2217
2583
  */
2218
2584
  async function listSkillsFromBackend(backend, sourcePath) {
2585
+ const adaptedBackend = adaptBackendProtocol(backend);
2219
2586
  const skills = [];
2220
2587
  const pathSep = sourcePath.includes("\\") ? "\\" : "/";
2221
2588
  const normalizedPath = sourcePath.endsWith("/") || sourcePath.endsWith("\\") ? sourcePath : `${sourcePath}${pathSep}`;
2222
2589
  let fileInfos;
2223
2590
  try {
2224
- fileInfos = await backend.lsInfo(normalizedPath);
2591
+ const lsResult = await adaptedBackend.ls(normalizedPath);
2592
+ if (lsResult.error || !lsResult.files) return [];
2593
+ fileInfos = lsResult.files;
2225
2594
  } catch {
2226
2595
  return [];
2227
2596
  }
@@ -2233,16 +2602,17 @@ async function listSkillsFromBackend(backend, sourcePath) {
2233
2602
  if (entry.type !== "directory") continue;
2234
2603
  const skillMdPath = `${normalizedPath}${entry.name}${pathSep}SKILL.md`;
2235
2604
  let content;
2236
- if (backend.downloadFiles) {
2237
- const results = await backend.downloadFiles([skillMdPath]);
2605
+ if (adaptedBackend.downloadFiles) {
2606
+ const results = await adaptedBackend.downloadFiles([skillMdPath]);
2238
2607
  if (results.length !== 1) continue;
2239
2608
  const response = results[0];
2240
2609
  if (response.error != null || response.content == null) continue;
2241
2610
  content = new TextDecoder().decode(response.content);
2242
2611
  } else {
2243
- const readResult = await backend.read(skillMdPath);
2244
- if (readResult.startsWith("Error:")) continue;
2245
- content = readResult;
2612
+ const readResult = await adaptedBackend.read(skillMdPath);
2613
+ if (readResult.error) continue;
2614
+ if (typeof readResult.content !== "string") continue;
2615
+ content = readResult.content;
2246
2616
  }
2247
2617
  const metadata = parseSkillMetadataFromContent(content, skillMdPath, entry.name);
2248
2618
  if (metadata) skills.push(metadata);
@@ -2265,74 +2635,302 @@ function formatSkillsLocations(sources) {
2265
2635
  return lines.join("\n");
2266
2636
  }
2267
2637
  /**
2268
- * Format skills metadata for display in system prompt.
2269
- * Shows allowed tools for each skill if specified.
2638
+ * Format skills metadata for display in system prompt.
2639
+ * Shows allowed tools for each skill if specified.
2640
+ */
2641
+ function formatSkillsList(skills, sources) {
2642
+ if (skills.length === 0) return `(No skills available yet. You can create skills in ${sources.map((s) => `\`${s}\``).join(" or ")})`;
2643
+ const lines = [];
2644
+ for (const skill of skills) {
2645
+ const annotations = formatSkillAnnotations(skill);
2646
+ let descLine = `- **${skill.name}**: ${skill.description}`;
2647
+ if (annotations) descLine += ` (${annotations})`;
2648
+ lines.push(descLine);
2649
+ if (skill.allowedTools && skill.allowedTools.length > 0) lines.push(` → Allowed tools: ${skill.allowedTools.join(", ")}`);
2650
+ lines.push(` → Read \`${skill.path}\` for full instructions`);
2651
+ }
2652
+ return lines.join("\n");
2653
+ }
2654
+ /**
2655
+ * Create backend-agnostic middleware for loading and exposing agent skills.
2656
+ *
2657
+ * This middleware loads skills from configurable backend sources and injects
2658
+ * skill metadata into the system prompt. It implements the progressive disclosure
2659
+ * pattern: skill names and descriptions are shown in the prompt, but the agent
2660
+ * reads full SKILL.md content only when needed.
2661
+ *
2662
+ * @param options - Configuration options
2663
+ * @returns AgentMiddleware for skills loading and injection
2664
+ *
2665
+ * @example
2666
+ * ```typescript
2667
+ * const middleware = createSkillsMiddleware({
2668
+ * backend: new FilesystemBackend({ rootDir: "/" }),
2669
+ * sources: ["/skills/user/", "/skills/project/"],
2670
+ * });
2671
+ * ```
2672
+ */
2673
+ function createSkillsMiddleware(options) {
2674
+ const { backend, sources } = options;
2675
+ let loadedSkills = [];
2676
+ return createMiddleware({
2677
+ name: "SkillsMiddleware",
2678
+ stateSchema: SkillsStateSchema,
2679
+ async beforeAgent(state) {
2680
+ if (loadedSkills.length > 0) return;
2681
+ if ("skillsMetadata" in state && Array.isArray(state.skillsMetadata) && state.skillsMetadata.length > 0) {
2682
+ loadedSkills = state.skillsMetadata;
2683
+ return;
2684
+ }
2685
+ const resolvedBackend = await resolveBackend(backend, { state });
2686
+ const allSkills = /* @__PURE__ */ new Map();
2687
+ for (const sourcePath of sources) try {
2688
+ const skills = await listSkillsFromBackend(resolvedBackend, sourcePath);
2689
+ for (const skill of skills) allSkills.set(skill.name, skill);
2690
+ } catch (error) {
2691
+ console.debug(`[BackendSkillsMiddleware] Failed to load skills from ${sourcePath}:`, error);
2692
+ }
2693
+ loadedSkills = Array.from(allSkills.values());
2694
+ return { skillsMetadata: loadedSkills };
2695
+ },
2696
+ wrapModelCall(request, handler) {
2697
+ const skillsMetadata = loadedSkills.length > 0 ? loadedSkills : request.state?.skillsMetadata || [];
2698
+ const skillsLocations = formatSkillsLocations(sources);
2699
+ const skillsList = formatSkillsList(skillsMetadata, sources);
2700
+ const skillsSection = SKILLS_SYSTEM_PROMPT.replace("{skills_locations}", skillsLocations).replace("{skills_list}", skillsList);
2701
+ const newSystemMessage = request.systemMessage.concat(skillsSection);
2702
+ return handler({
2703
+ ...request,
2704
+ systemMessage: newSystemMessage
2705
+ });
2706
+ }
2707
+ });
2708
+ }
2709
+ //#endregion
2710
+ //#region src/middleware/completion_callback.ts
2711
+ /**
2712
+ * Callback middleware for async subagents.
2713
+ *
2714
+ * @experimental - this middleware is experimental and may change in future releases.
2715
+ *
2716
+ * This middleware sends a notification to a callback thread when a subagent
2717
+ * completes successfully or raises an error. The callback agent can then
2718
+ * process that notification instead of relying only on polling via
2719
+ * `check_async_task`.
2720
+ *
2721
+ * ## Architecture
2722
+ *
2723
+ * A parent agent launches a subagent with `start_async_task` and can later
2724
+ * inspect task state with `check_async_task`. This middleware adds an optional
2725
+ * completion signal by creating a run on the callback thread when the subagent
2726
+ * finishes.
2727
+ *
2728
+ * ```
2729
+ * Parent Subagent
2730
+ * | |
2731
+ * |--- start_async_task -----> |
2732
+ * |<-- task_id (immediately) - |
2733
+ * | | (working...)
2734
+ * | | (done!)
2735
+ * | |
2736
+ * |<-- runs.create( |
2737
+ * | callback_thread, |
2738
+ * | "completed: ...") |
2739
+ * | |
2740
+ * | (processes result) |
2741
+ * ```
2742
+ *
2743
+ * The middleware calls `runs.create()` on the callback thread. From the
2744
+ * callback agent's perspective, this appears as a new user message containing
2745
+ * structured output from the subagent.
2746
+ *
2747
+ * ## Callback context
2748
+ *
2749
+ * - `callbackGraphId` identifies the callback graph or assistant. It is
2750
+ * provided when the middleware is constructed.
2751
+ * - `url` and `headers` optionally configure a remote callback destination.
2752
+ * Omit `url` for same-deployment ASGI transport.
2753
+ * - `callback_thread_id` is stored in the subagent state by the parent's
2754
+ * `start_async_task` tool. Because it is stored in state rather than config,
2755
+ * it survives thread updates and interrupts.
2756
+ * - If `callback_thread_id` is not present in state, the middleware does
2757
+ * nothing.
2758
+ *
2759
+ * ## Usage
2760
+ *
2761
+ * ```typescript
2762
+ * import { createCompletionCallbackMiddleware } from "deepagents";
2763
+ *
2764
+ * // Same deployment (callback agent and subagent share a server):
2765
+ * const notifier = createCompletionCallbackMiddleware({
2766
+ * callbackGraphId: "supervisor",
2767
+ * });
2768
+ *
2769
+ * // Remote deployment (callback destination on a different server):
2770
+ * const notifier = createCompletionCallbackMiddleware({
2771
+ * callbackGraphId: "supervisor",
2772
+ * url: "https://my-deployment.langsmith.dev",
2773
+ * });
2774
+ *
2775
+ * const agent = createDeepAgent({
2776
+ * model,
2777
+ * middleware: [notifier],
2778
+ * });
2779
+ * ```
2780
+ *
2781
+ * The middleware reads `callbackThreadId` from the agent state at the end of
2782
+ * execution. This value is injected by the parent's `start_async_task` tool
2783
+ * when it creates the run.
2784
+ *
2785
+ * @module
2786
+ */
2787
+ /** Maximum characters to include from the last message in notifications. */
2788
+ const MAX_MESSAGE_LENGTH = 500;
2789
+ /** Suffix appended when truncating long messages. */
2790
+ const TRUNCATION_SUFFIX = "... [full result truncated]";
2791
+ /** State key for the callback thread ID. */
2792
+ const CALLBACK_THREAD_ID_KEY = "callbackThreadId";
2793
+ /**
2794
+ * State extension for subagents that use completion callbacks.
2795
+ *
2796
+ * @experimental - this state schema is experimental and may change in future releases.
2797
+ *
2798
+ * `callbackThreadId` is written by the parent's `start_async_task` tool
2799
+ * and read by `CompletionCallbackMiddleware` when sending callback
2800
+ * notifications.
2801
+ */
2802
+ const CompletionCallbackStateSchema = z$2.object({ [CALLBACK_THREAD_ID_KEY]: z$2.string().optional() });
2803
+ /**
2804
+ * Build headers for the callback LangGraph server.
2805
+ *
2806
+ * Ensures `x-auth-scheme: langsmith` is present unless explicitly overridden.
2807
+ */
2808
+ function resolveHeaders(headers) {
2809
+ const resolved = { ...headers };
2810
+ if (!("x-auth-scheme" in resolved)) resolved["x-auth-scheme"] = "langsmith";
2811
+ return resolved;
2812
+ }
2813
+ /**
2814
+ * Send a notification run to the callback thread.
2815
+ *
2816
+ * @param callbackGraphId - The callback graph ID used as `assistant_id`
2817
+ * in the `runs.create` call.
2818
+ * @param callbackThreadId - The callback thread ID.
2819
+ * @param message - The message content to send.
2820
+ * @param options - Optional url and headers for the callback server.
2270
2821
  */
2271
- function formatSkillsList(skills, sources) {
2272
- if (skills.length === 0) return `(No skills available yet. You can create skills in ${sources.map((s) => `\`${s}\``).join(" or ")})`;
2273
- const lines = [];
2274
- for (const skill of skills) {
2275
- const annotations = formatSkillAnnotations(skill);
2276
- let descLine = `- **${skill.name}**: ${skill.description}`;
2277
- if (annotations) descLine += ` (${annotations})`;
2278
- lines.push(descLine);
2279
- if (skill.allowedTools && skill.allowedTools.length > 0) lines.push(` → Allowed tools: ${skill.allowedTools.join(", ")}`);
2280
- lines.push(` → Read \`${skill.path}\` for full instructions`);
2822
+ async function notifyParent(callbackGraphId, callbackThreadId, message, options) {
2823
+ try {
2824
+ await new Client({
2825
+ apiUrl: options?.url ?? void 0,
2826
+ apiKey: null,
2827
+ defaultHeaders: resolveHeaders(options?.headers)
2828
+ }).runs.create(callbackThreadId, callbackGraphId, { input: { messages: [{
2829
+ role: "user",
2830
+ content: message
2831
+ }] } });
2832
+ } catch (e) {
2833
+ console.warn(`[CompletionCallbackMiddleware] Failed to notify callback thread ${callbackThreadId}:`, e);
2281
2834
  }
2282
- return lines.join("\n");
2283
2835
  }
2284
2836
  /**
2285
- * Create backend-agnostic middleware for loading and exposing agent skills.
2837
+ * Extract a summary from the subagent's final message.
2286
2838
  *
2287
- * This middleware loads skills from configurable backend sources and injects
2288
- * skill metadata into the system prompt. It implements the progressive disclosure
2289
- * pattern: skill names and descriptions are shown in the prompt, but the agent
2290
- * reads full SKILL.md content only when needed.
2839
+ * Returns at most 500 characters from the last message's content.
2840
+ * Throws if no messages exist or if the last message is not an AIMessage.
2291
2841
  *
2292
- * @param options - Configuration options
2293
- * @returns AgentMiddleware for skills loading and injection
2842
+ * @param state - The agent state dict.
2843
+ * @param taskId - Optional task ID to include in truncation hint.
2844
+ */
2845
+ function extractLastMessage(state, taskId) {
2846
+ const messages = state.messages;
2847
+ if (!messages || messages.length === 0) throw new Error(`Expected at least one message in state ${JSON.stringify(state)}`);
2848
+ const last = messages[messages.length - 1];
2849
+ if (!AIMessage$1.isInstance(last)) throw new TypeError(`Expected an AIMessage, got ${typeof last === "object" && last !== null ? last.constructor?.name ?? typeof last : typeof last} instead`);
2850
+ let textContent = last.text;
2851
+ if (textContent.length > MAX_MESSAGE_LENGTH) {
2852
+ textContent = textContent.slice(0, MAX_MESSAGE_LENGTH) + TRUNCATION_SUFFIX;
2853
+ if (taskId) textContent += ` Result truncated. Use \`check_async_task(task_id='${taskId}')\` to retrieve the full result if needed.`;
2854
+ }
2855
+ return textContent;
2856
+ }
2857
+ /**
2858
+ * Create a completion callback middleware for async subagents.
2859
+ *
2860
+ * **Experimental** — this middleware is experimental and may change.
2861
+ *
2862
+ * This middleware is added to a subagent's middleware stack. On success or
2863
+ * model-call error, it sends a notification to the configured callback
2864
+ * thread by calling `runs.create()`.
2865
+ *
2866
+ * The callback destination is configured with `callbackGraphId` and
2867
+ * optional `url` and `headers`. The target thread is read from
2868
+ * `callbackThreadId` in the subagent state.
2869
+ *
2870
+ * If `callbackThreadId` is not present in state, the middleware does
2871
+ * nothing.
2872
+ *
2873
+ * @param options - Configuration options.
2874
+ * @returns An `AgentMiddleware` instance.
2294
2875
  *
2295
2876
  * @example
2296
2877
  * ```typescript
2297
- * const middleware = createSkillsMiddleware({
2298
- * backend: new FilesystemBackend({ rootDir: "/" }),
2299
- * sources: ["/skills/user/", "/skills/project/"],
2878
+ * import { createCompletionCallbackMiddleware } from "deepagents";
2879
+ *
2880
+ * const notifier = createCompletionCallbackMiddleware({
2881
+ * callbackGraphId: "supervisor",
2882
+ * });
2883
+ *
2884
+ * const agent = createDeepAgent({
2885
+ * model: "claude-sonnet-4-5-20250929",
2886
+ * middleware: [notifier],
2300
2887
  * });
2301
2888
  * ```
2302
2889
  */
2303
- function createSkillsMiddleware(options) {
2304
- const { backend, sources } = options;
2305
- let loadedSkills = [];
2890
+ function createCompletionCallbackMiddleware(options) {
2891
+ const { callbackGraphId, url, headers } = options;
2892
+ /**
2893
+ * Send a notification to the callback destination.
2894
+ */
2895
+ async function sendNotification(callbackThreadId, message) {
2896
+ await notifyParent(callbackGraphId, callbackThreadId, message, {
2897
+ url,
2898
+ headers
2899
+ });
2900
+ }
2901
+ /**
2902
+ * Read the subagent's own thread_id from runtime config.
2903
+ *
2904
+ * The subagent's `thread_id` is the same as the `task_id` from the
2905
+ * parent's perspective.
2906
+ */
2907
+ function getTaskId(runtime) {
2908
+ return runtime?.configurable?.thread_id;
2909
+ }
2910
+ /**
2911
+ * Build a notification string with task_id prefix.
2912
+ */
2913
+ function formatNotification(body, runtime) {
2914
+ const taskId = getTaskId(runtime);
2915
+ return `${taskId ? `[task_id=${taskId}]` : ""}${body}`;
2916
+ }
2306
2917
  return createMiddleware({
2307
- name: "SkillsMiddleware",
2308
- stateSchema: SkillsStateSchema,
2309
- async beforeAgent(state) {
2310
- if (loadedSkills.length > 0) return;
2311
- if ("skillsMetadata" in state && Array.isArray(state.skillsMetadata) && state.skillsMetadata.length > 0) {
2312
- loadedSkills = state.skillsMetadata;
2313
- return;
2314
- }
2315
- const resolvedBackend = await resolveBackend(backend, { state });
2316
- const allSkills = /* @__PURE__ */ new Map();
2317
- for (const sourcePath of sources) try {
2318
- const skills = await listSkillsFromBackend(resolvedBackend, sourcePath);
2319
- for (const skill of skills) allSkills.set(skill.name, skill);
2320
- } catch (error) {
2321
- console.debug(`[BackendSkillsMiddleware] Failed to load skills from ${sourcePath}:`, error);
2322
- }
2323
- loadedSkills = Array.from(allSkills.values());
2324
- return { skillsMetadata: loadedSkills };
2918
+ name: "CompletionCallbackMiddleware",
2919
+ stateSchema: CompletionCallbackStateSchema,
2920
+ async afterAgent(state, runtime) {
2921
+ const callbackThreadId = state[CALLBACK_THREAD_ID_KEY];
2922
+ if (callbackThreadId == null) throw new Error(`Missing required state key '${CALLBACK_THREAD_ID_KEY}'`);
2923
+ const taskId = getTaskId(runtime);
2924
+ await sendNotification(callbackThreadId, formatNotification(`Completed. Result: ${extractLastMessage(state, typeof taskId === "string" ? taskId : void 0)}`, runtime));
2325
2925
  },
2326
- wrapModelCall(request, handler) {
2327
- const skillsMetadata = loadedSkills.length > 0 ? loadedSkills : request.state?.skillsMetadata || [];
2328
- const skillsLocations = formatSkillsLocations(sources);
2329
- const skillsList = formatSkillsList(skillsMetadata, sources);
2330
- const skillsSection = SKILLS_SYSTEM_PROMPT.replace("{skills_locations}", skillsLocations).replace("{skills_list}", skillsList);
2331
- const newSystemMessage = request.systemMessage.concat(skillsSection);
2332
- return handler({
2333
- ...request,
2334
- systemMessage: newSystemMessage
2335
- });
2926
+ async wrapModelCall(request, handler) {
2927
+ try {
2928
+ return await handler(request);
2929
+ } catch (e) {
2930
+ const callbackThreadId = request.state[CALLBACK_THREAD_ID_KEY];
2931
+ if (typeof callbackThreadId === "string") await sendNotification(callbackThreadId, formatNotification("The agent encountered an error while calling the model.", request.runtime));
2932
+ throw e;
2933
+ }
2336
2934
  }
2337
2935
  });
2338
2936
  }
@@ -2860,15 +3458,17 @@ function createSummarizationMiddleware(options) {
2860
3458
  */
2861
3459
  function buildSummaryMessage(summary, filePath) {
2862
3460
  let content;
2863
- if (filePath) content = `You are in the middle of a conversation that has been summarized.
3461
+ if (filePath) content = context`
3462
+ You are in the middle of a conversation that has been summarized.
2864
3463
 
2865
- The full conversation history has been saved to ${filePath} should you need to refer back to it for details.
3464
+ The full conversation history has been saved to ${filePath} should you need to refer back to it for details.
2866
3465
 
2867
- A condensed summary follows:
3466
+ A condensed summary follows:
2868
3467
 
2869
- <summary>
2870
- ${summary}
2871
- </summary>`;
3468
+ <summary>
3469
+ ${summary}
3470
+ </summary>
3471
+ `;
2872
3472
  else content = `Here is a summary of the conversation to date:\n\n${summary}`;
2873
3473
  return new HumanMessage({
2874
3474
  content,
@@ -2934,92 +3534,661 @@ ${summary}
2934
3534
  if (!isContextOverflow(err)) throw err;
2935
3535
  }
2936
3536
  }
2937
- const previousEvent = request.state._summarizationEvent;
2938
- const previousCutoffIndex = previousEvent != null ? previousEvent.cutoffIndex : void 0;
2939
- const { summaryMessage, filePath, stateCutoffIndex } = await summarizeMessages(messagesToSummarize, resolvedModel, request.state, previousCutoffIndex, cutoffIndex);
2940
- let modifiedMessages = [summaryMessage, ...preservedMessages];
2941
- const modifiedTokens = countTotalTokens(modifiedMessages, request.systemMessage, request.tools);
2942
- let finalStateCutoffIndex = stateCutoffIndex;
2943
- let finalSummaryMessage = summaryMessage;
2944
- let finalFilePath = filePath;
3537
+ const previousEvent = request.state._summarizationEvent;
3538
+ const previousCutoffIndex = previousEvent != null ? previousEvent.cutoffIndex : void 0;
3539
+ const { summaryMessage, filePath, stateCutoffIndex } = await summarizeMessages(messagesToSummarize, resolvedModel, request.state, previousCutoffIndex, cutoffIndex);
3540
+ let modifiedMessages = [summaryMessage, ...preservedMessages];
3541
+ const modifiedTokens = countTotalTokens(modifiedMessages, request.systemMessage, request.tools);
3542
+ let finalStateCutoffIndex = stateCutoffIndex;
3543
+ let finalSummaryMessage = summaryMessage;
3544
+ let finalFilePath = filePath;
3545
+ try {
3546
+ await handler({
3547
+ ...request,
3548
+ messages: modifiedMessages
3549
+ });
3550
+ } catch (err) {
3551
+ if (!isContextOverflow(err)) throw err;
3552
+ if (maxInputTokens && modifiedTokens > 0) {
3553
+ const observedRatio = maxInputTokens / modifiedTokens;
3554
+ if (observedRatio > tokenEstimationMultiplier) tokenEstimationMultiplier = observedRatio * 1.1;
3555
+ }
3556
+ const reSumResult = await summarizeMessages([...messagesToSummarize, ...preservedMessages], resolvedModel, request.state, previousCutoffIndex, truncatedMessages.length);
3557
+ finalSummaryMessage = reSumResult.summaryMessage;
3558
+ finalFilePath = reSumResult.filePath;
3559
+ finalStateCutoffIndex = reSumResult.stateCutoffIndex;
3560
+ modifiedMessages = [reSumResult.summaryMessage];
3561
+ await handler({
3562
+ ...request,
3563
+ messages: modifiedMessages
3564
+ });
3565
+ }
3566
+ return new Command({ update: {
3567
+ _summarizationEvent: {
3568
+ cutoffIndex: finalStateCutoffIndex,
3569
+ summaryMessage: finalSummaryMessage,
3570
+ filePath: finalFilePath
3571
+ },
3572
+ _summarizationSessionId: getSessionId(request.state)
3573
+ } });
3574
+ }
3575
+ return createMiddleware({
3576
+ name: "SummarizationMiddleware",
3577
+ stateSchema: SummarizationStateSchema,
3578
+ async wrapModelCall(request, handler) {
3579
+ const effectiveMessages = getEffectiveMessages(request.messages ?? [], request.state);
3580
+ if (effectiveMessages.length === 0) return handler(request);
3581
+ /**
3582
+ * Resolve the chat model and get max input tokens from its profile.
3583
+ */
3584
+ const resolvedModel = await getChatModel();
3585
+ const maxInputTokens = getMaxInputTokens(resolvedModel);
3586
+ applyModelDefaults(resolvedModel);
3587
+ /**
3588
+ * Step 1: Truncate args if configured
3589
+ */
3590
+ const { messages: truncatedMessages } = truncateArgs(effectiveMessages, maxInputTokens, request.systemMessage, request.tools);
3591
+ /**
3592
+ * Step 2: Check if summarization should happen.
3593
+ * Count tokens including system message and tools to match what's
3594
+ * actually sent to the model (matching Python implementation).
3595
+ */
3596
+ const totalTokens = countTotalTokens(truncatedMessages, request.systemMessage, request.tools);
3597
+ /**
3598
+ * If no summarization needed, try passing through.
3599
+ * If the handler throws a ContextOverflowError, fall back to
3600
+ * emergency summarization (matching Python's behavior).
3601
+ */
3602
+ if (!shouldSummarize(truncatedMessages, totalTokens, maxInputTokens)) try {
3603
+ return await handler({
3604
+ ...request,
3605
+ messages: truncatedMessages
3606
+ });
3607
+ } catch (err) {
3608
+ if (!isContextOverflow(err)) throw err;
3609
+ if (maxInputTokens && totalTokens > 0) {
3610
+ const observedRatio = maxInputTokens / totalTokens;
3611
+ if (observedRatio > tokenEstimationMultiplier) tokenEstimationMultiplier = observedRatio * 1.1;
3612
+ }
3613
+ }
3614
+ /**
3615
+ * Step 3: Perform summarization
3616
+ */
3617
+ return performSummarization(request, handler, truncatedMessages, resolvedModel, maxInputTokens);
3618
+ }
3619
+ });
3620
+ }
3621
+ //#endregion
3622
+ //#region src/middleware/async_subagents.ts
3623
+ function toolCallIdFromRuntime(runtime) {
3624
+ return runtime.toolCall?.id ?? runtime.toolCallId ?? "";
3625
+ }
3626
+ /**
3627
+ * Zod schema for {@link AsyncTask}.
3628
+ *
3629
+ * Used by the {@link ReducedValue} in the state schema so that LangGraph
3630
+ * can validate and serialize task records stored in `asyncTasks`.
3631
+ */
3632
+ const AsyncTaskSchema = z.object({
3633
+ taskId: z.string(),
3634
+ agentName: z.string(),
3635
+ threadId: z.string(),
3636
+ runId: z.string(),
3637
+ status: z.string(),
3638
+ createdAt: z.string(),
3639
+ description: z.string().optional(),
3640
+ updatedAt: z.string().optional(),
3641
+ checkedAt: z.string().optional()
3642
+ });
3643
+ /**
3644
+ * State schema for the async subagent middleware.
3645
+ *
3646
+ * Declares `asyncTasks` as a reduced state channel so that individual
3647
+ * tool updates (launch, check, update, cancel, list) merge into the existing
3648
+ * tasks dict rather than replacing it wholesale.
3649
+ */
3650
+ const AsyncTaskStateSchema = new StateSchema({ asyncTasks: new ReducedValue(z.record(z.string(), AsyncTaskSchema).default(() => ({})), {
3651
+ inputSchema: z.record(z.string(), AsyncTaskSchema).optional(),
3652
+ reducer: asyncTasksReducer
3653
+ }) });
3654
+ /**
3655
+ * Reducer for the `asyncTasks` state channel.
3656
+ *
3657
+ * Merges task updates into the existing tasks dict using shallow spread.
3658
+ * This allows individual tools to update a single task without overwriting
3659
+ * the full map — only the keys present in `update` are replaced.
3660
+ *
3661
+ * @param existing - The current tasks dict from state (may be undefined on first write).
3662
+ * @param update - New or updated task entries to merge in.
3663
+ * @returns Merged tasks dict.
3664
+ */
3665
+ function asyncTasksReducer(existing, update) {
3666
+ return {
3667
+ ...existing || {},
3668
+ ...update || {}
3669
+ };
3670
+ }
3671
+ /**
3672
+ * Description template for the `start_async_task` tool.
3673
+ *
3674
+ * The `{available_agents}` placeholder is replaced at middleware creation
3675
+ * time with a formatted list of configured async subagent names and descriptions.
3676
+ */
3677
+ const ASYNC_TASK_TOOL_DESCRIPTION = `Launch an async subagent on a remote server. The subagent runs in the background and returns a task ID immediately.
3678
+
3679
+ Available async agent types:
3680
+ {available_agents}
3681
+
3682
+ ## Usage notes:
3683
+ 1. This tool launches a background task and returns immediately with a task ID. Report the task ID to the user and stop — do NOT immediately check status.
3684
+ 2. Use \`check_async_task\` only when the user asks for a status update or result.
3685
+ 3. Use \`update_async_task\` to send new instructions to a running task.
3686
+ 4. Multiple async subagents can run concurrently — launch several and let them run in the background.
3687
+ 5. The subagent runs on a remote server, so it has its own tools and capabilities.`;
3688
+ /**
3689
+ * Default system prompt appended to the main agent's system message when
3690
+ * async subagent middleware is active.
3691
+ *
3692
+ * Provides the agent with instructions on how to use the five async subagent
3693
+ * tools (launch, check, update, cancel, list) including workflow ordering,
3694
+ * critical rules about polling behavior, and guidance on when to use async
3695
+ * subagents vs. synchronous delegation.
3696
+ */
3697
+ const ASYNC_TASK_SYSTEM_PROMPT = `## Async subagents (remote servers)
3698
+
3699
+ You have access to async subagent tools that launch background tasks on remote servers.
3700
+
3701
+ ### Tools:
3702
+ - \`start_async_task\`: Start a new background task. Returns a task ID immediately.
3703
+ - \`check_async_task\`: Check the status of a running task. Returns status and result if complete.
3704
+ - \`update_async_task\`: Send an update or new instructions to a running task.
3705
+ - \`cancel_async_task\`: Cancel a running task that is no longer needed.
3706
+ - \`list_async_tasks\`: List all tracked tasks with live statuses. Use this to check all tasks at once.
3707
+
3708
+ ### Workflow:
3709
+ 1. **Launch** — Use \`start_async_task\` to start a task. Report the task ID to the user and stop.
3710
+ Do NOT immediately check the status — the task runs in the background while you and the user continue other work.
3711
+ 2. **Check (on request)** — Only use \`check_async_task\` when the user explicitly asks for a status update or
3712
+ result. If the status is "running", report that and stop — do not poll in a loop.
3713
+ 3. **Update** (optional) — Use \`update_async_task\` to send new instructions to a running task. This interrupts
3714
+ the current run and starts a fresh one on the same thread. The task_id stays the same.
3715
+ 4. **Cancel** (optional) — Use \`cancel_async_task\` to stop a task that is no longer needed.
3716
+ 5. **Collect** — When \`check_async_task\` returns status "success", the result is included in the response.
3717
+ 6. **List** — Use \`list_async_tasks\` to see live statuses for all tasks at once, or to recall task IDs after context compaction.
3718
+
3719
+ ### Critical rules:
3720
+ - After launching, ALWAYS return control to the user immediately. Never auto-check after launching.
3721
+ - Never poll \`check_async_task\` in a loop. Check once per user request, then stop.
3722
+ - If a check returns "running", tell the user and wait for them to ask again.
3723
+ - Task statuses in conversation history are ALWAYS stale — a task that was "running" may now be done.
3724
+ NEVER report a status from a previous tool result. ALWAYS call a tool to get the current status:
3725
+ use \`list_async_tasks\` when the user asks about multiple tasks or "all tasks",
3726
+ use \`check_async_task\` when the user asks about a specific task.
3727
+ - Always show the full task_id — never truncate or abbreviate it.
3728
+
3729
+ ### When to use async subagents:
3730
+ - Long-running tasks that would block the main agent
3731
+ - Tasks that benefit from running on specialized remote deployments
3732
+ - When you want to run multiple tasks concurrently and collect results later`;
3733
+ /**
3734
+ * Task statuses that will never change.
3735
+ *
3736
+ * When listing tasks, live-status fetches are skipped for tasks whose
3737
+ * cached status is in this set, since they are guaranteed to be final.
3738
+ */
3739
+ /**
3740
+ * Names of the tools added by the async subagent middleware.
3741
+ *
3742
+ * Exported so `agent.ts` can include them in `BUILTIN_TOOL_NAMES` and
3743
+ * surface a `ConfigurationError` if a user-provided tool collides.
3744
+ */
3745
+ const ASYNC_TASK_TOOL_NAMES = [
3746
+ "start_async_task",
3747
+ "check_async_task",
3748
+ "update_async_task",
3749
+ "cancel_async_task",
3750
+ "list_async_tasks"
3751
+ ];
3752
+ const TERMINAL_STATUSES = new Set([
3753
+ "cancelled",
3754
+ "success",
3755
+ "error",
3756
+ "timeout",
3757
+ "interrupted"
3758
+ ]);
3759
+ /**
3760
+ * Look up a tracked task from state by its `taskId`.
3761
+ *
3762
+ * @param taskId - The task ID to look up (will be trimmed).
3763
+ * @param state - The current agent state containing `asyncTasks`.
3764
+ * @returns The tracked task on success, or an error string.
3765
+ */
3766
+ function resolveTrackedTask(taskId, state) {
3767
+ const tracked = (state.asyncTasks ?? {})[taskId.trim()];
3768
+ if (!tracked) return `No tracked task found for taskId: '${taskId}'`;
3769
+ return tracked;
3770
+ }
3771
+ /**
3772
+ * Build a check result from a run's current status and thread state values.
3773
+ *
3774
+ * For successful runs, extracts the last message's content from the remote
3775
+ * thread's state values. For errored runs, includes a generic error message.
3776
+ *
3777
+ * @param run - The run object from the SDK.
3778
+ * @param threadId - The thread ID for the run.
3779
+ * @param threadValues - The `values` from `ThreadState` (the remote subagent's state).
3780
+ */
3781
+ function buildCheckResult(run, threadId, threadValues) {
3782
+ const checkResult = {
3783
+ status: run.status,
3784
+ threadId
3785
+ };
3786
+ if (run.status === "success") {
3787
+ const messages = (Array.isArray(threadValues) ? {} : threadValues)?.messages ?? [];
3788
+ if (messages.length > 0) {
3789
+ const last = messages[messages.length - 1];
3790
+ const rawContent = typeof last === "object" && last !== null && "content" in last ? last.content : last;
3791
+ checkResult.result = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
3792
+ } else checkResult.result = "Completed with no output messages.";
3793
+ } else if (run.status === "error") checkResult.error = "The async subagent encountered an error.";
3794
+ return checkResult;
3795
+ }
3796
+ /**
3797
+ * Filter tasks by cached status from agent state.
3798
+ *
3799
+ * Filtering uses the cached status, not live server status. Live statuses
3800
+ * are fetched after filtering by the calling tool.
3801
+ *
3802
+ * @param tasks - All tracked tasks from state.
3803
+ * @param statusFilter - If nullish or `'all'`, return all tasks.
3804
+ * Otherwise return only tasks whose cached status matches.
3805
+ */
3806
+ function filterTasks(tasks, statusFilter) {
3807
+ if (!statusFilter || statusFilter === "all") return Object.values(tasks);
3808
+ return Object.values(tasks).filter((task) => task.status === statusFilter);
3809
+ }
3810
+ /**
3811
+ * Fetch the current run status from the server.
3812
+ *
3813
+ * Returns the cached status immediately for terminal tasks (avoiding
3814
+ * unnecessary API calls). Falls back to the cached status on SDK errors.
3815
+ */
3816
+ async function fetchLiveTaskStatus(clients, task) {
3817
+ if (TERMINAL_STATUSES.has(task.status)) return task.status;
3818
+ try {
3819
+ return (await clients.getClient(task.agentName).runs.get(task.threadId, task.runId)).status;
3820
+ } catch {
3821
+ return task.status;
3822
+ }
3823
+ }
3824
+ /**
3825
+ * Format a single task as a display string for list output.
3826
+ */
3827
+ function formatTaskEntry(task, status) {
3828
+ return `- taskId: ${task.taskId} agent: ${task.agentName} status: ${status}`;
3829
+ }
3830
+ /**
3831
+ * Lazily-created, cached LangGraph SDK clients keyed by (url, headers).
3832
+ *
3833
+ * Agents that share the same URL and headers will reuse a single `Client`
3834
+ * instance, avoiding unnecessary connections.
3835
+ */
3836
+ var ClientCache = class {
3837
+ agents;
3838
+ clients = /* @__PURE__ */ new Map();
3839
+ constructor(agents) {
3840
+ this.agents = agents;
3841
+ }
3842
+ /**
3843
+ * Build headers for a remote Agent Protocol server.
3844
+ *
3845
+ * Adds `x-auth-scheme: langsmith` by default unless already provided.
3846
+ * For self-hosted servers that don't require this header, it is typically
3847
+ * ignored. Override via the `headers` field on the AsyncSubAgent config.
3848
+ */
3849
+ resolveHeaders(spec) {
3850
+ const headers = { ...spec.headers || {} };
3851
+ if (!("x-auth-scheme" in headers)) headers["x-auth-scheme"] = "langsmith";
3852
+ return headers;
3853
+ }
3854
+ /**
3855
+ * Build a stable cache key from a spec's url and resolved headers.
3856
+ */
3857
+ cacheKey(spec) {
3858
+ const headers = this.resolveHeaders(spec);
3859
+ const headerStr = Object.entries(headers).sort().flat().join(":");
3860
+ return `${spec.url ?? ""}|${headerStr}`;
3861
+ }
3862
+ /**
3863
+ * Get or create a `Client` for the named agent.
3864
+ */
3865
+ getClient(name) {
3866
+ const spec = this.agents[name];
3867
+ const key = this.cacheKey(spec);
3868
+ const existing = this.clients.get(key);
3869
+ if (existing) return existing;
3870
+ const headers = this.resolveHeaders(spec);
3871
+ const client = new Client({
3872
+ apiUrl: spec.url,
3873
+ defaultHeaders: headers
3874
+ });
3875
+ this.clients.set(key, client);
3876
+ return client;
3877
+ }
3878
+ };
3879
+ /**
3880
+ * Extract the callback thread ID from the tool runtime.
3881
+ *
3882
+ * The thread ID is included in the subagent's input state so the subagent
3883
+ * can notify the parent when it completes (via
3884
+ * `CompletionCallbackMiddleware`).
3885
+ *
3886
+ * @returns Object with `callbackThreadId` if available. Empty object otherwise.
3887
+ */
3888
+ function extractCallbackContext(runtime) {
3889
+ const threadId = (runtime.config?.configurable)?.thread_id;
3890
+ if (typeof threadId === "string" && threadId) return { callbackThreadId: threadId };
3891
+ return {};
3892
+ }
3893
+ /**
3894
+ * Build the `start_async_task` tool.
3895
+ *
3896
+ * Creates a thread on the remote server, starts a run, and returns a
3897
+ * `Command` that persists the new task in state.
3898
+ */
3899
+ function buildStartTool(agentMap, clients, toolDescription) {
3900
+ return tool(async (input, runtime) => {
3901
+ if (!(input.agentName in agentMap)) {
3902
+ const allowed = Object.keys(agentMap).map((k) => `\`${k}\``).join(", ");
3903
+ return `Unknown async subagent type \`${input.agentName}\`. Available types: ${allowed}`;
3904
+ }
3905
+ const spec = agentMap[input.agentName];
3906
+ const callbackContext = extractCallbackContext(runtime);
2945
3907
  try {
2946
- await handler({
2947
- ...request,
2948
- messages: modifiedMessages
2949
- });
2950
- } catch (err) {
2951
- if (!isContextOverflow(err)) throw err;
2952
- if (maxInputTokens && modifiedTokens > 0) {
2953
- const observedRatio = maxInputTokens / modifiedTokens;
2954
- if (observedRatio > tokenEstimationMultiplier) tokenEstimationMultiplier = observedRatio * 1.1;
2955
- }
2956
- const reSumResult = await summarizeMessages([...messagesToSummarize, ...preservedMessages], resolvedModel, request.state, previousCutoffIndex, truncatedMessages.length);
2957
- finalSummaryMessage = reSumResult.summaryMessage;
2958
- finalFilePath = reSumResult.filePath;
2959
- finalStateCutoffIndex = reSumResult.stateCutoffIndex;
2960
- modifiedMessages = [reSumResult.summaryMessage];
2961
- await handler({
2962
- ...request,
2963
- messages: modifiedMessages
3908
+ const client = clients.getClient(input.agentName);
3909
+ const thread = await client.threads.create();
3910
+ const run = await client.runs.create(thread.thread_id, spec.graphId, { input: {
3911
+ messages: [{
3912
+ role: "user",
3913
+ content: input.description
3914
+ }],
3915
+ ...callbackContext
3916
+ } });
3917
+ const taskId = thread.thread_id;
3918
+ const task = {
3919
+ taskId,
3920
+ agentName: input.agentName,
3921
+ threadId: taskId,
3922
+ runId: run.run_id,
3923
+ status: "running",
3924
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3925
+ description: input.description
3926
+ };
3927
+ return new Command({ update: {
3928
+ messages: [new ToolMessage({
3929
+ content: `Launched async subagent. taskId: ${taskId}`,
3930
+ tool_call_id: toolCallIdFromRuntime(runtime)
3931
+ })],
3932
+ asyncTasks: { [taskId]: task }
3933
+ } });
3934
+ } catch (e) {
3935
+ return `Failed to launch async subagent '${input.agentName}': ${e}`;
3936
+ }
3937
+ }, {
3938
+ name: "start_async_task",
3939
+ description: toolDescription,
3940
+ schema: z.object({
3941
+ description: z.string().describe("A detailed description of the task for the async subagent to perform."),
3942
+ agentName: z.string().describe("The type of async subagent to use. Must be one of the available types listed in the tool description.")
3943
+ })
3944
+ });
3945
+ }
3946
+ /**
3947
+ * Build the `check_async_task` tool.
3948
+ *
3949
+ * Fetches the current run status from the remote server and, if the run
3950
+ * succeeded, retrieves the thread state to extract the result.
3951
+ */
3952
+ function buildCheckTool(clients) {
3953
+ return tool(async (input, runtime) => {
3954
+ const task = resolveTrackedTask(input.taskId, runtime.state);
3955
+ if (typeof task === "string") return task;
3956
+ const client = clients.getClient(task.agentName);
3957
+ let run;
3958
+ try {
3959
+ run = await client.runs.get(task.threadId, task.runId);
3960
+ } catch (e) {
3961
+ return `Failed to get run status: ${e}`;
3962
+ }
3963
+ let threadValues = {};
3964
+ if (run.status === "success") try {
3965
+ threadValues = (await client.threads.getState(task.threadId)).values || {};
3966
+ } catch {}
3967
+ const result = buildCheckResult(run, task.threadId, threadValues);
3968
+ const updatedTask = {
3969
+ taskId: task.taskId,
3970
+ agentName: task.agentName,
3971
+ threadId: task.threadId,
3972
+ runId: task.runId,
3973
+ status: result.status,
3974
+ createdAt: task.createdAt,
3975
+ updatedAt: result.status !== task.status ? (/* @__PURE__ */ new Date()).toISOString() : task.updatedAt,
3976
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
3977
+ };
3978
+ return new Command({ update: {
3979
+ messages: [new ToolMessage({
3980
+ content: JSON.stringify(result),
3981
+ tool_call_id: toolCallIdFromRuntime(runtime)
3982
+ })],
3983
+ asyncTasks: { [task.taskId]: updatedTask }
3984
+ } });
3985
+ }, {
3986
+ name: "check_async_task",
3987
+ description: "Check the status of an async subagent task. Returns the current status and, if complete, the result.",
3988
+ schema: z.object({ taskId: z.string().describe("The exact taskId string returned by start_async_task. Pass it verbatim.") })
3989
+ });
3990
+ }
3991
+ /**
3992
+ * Build the `update_async_task` tool.
3993
+ *
3994
+ * Sends a follow-up message to a running async subagent by creating a new
3995
+ * run on the same thread with `multitaskStrategy: "interrupt"`. The subagent
3996
+ * sees the full conversation history plus the new message. The `taskId`
3997
+ * remains the same; only the internal `runId` is updated.
3998
+ */
3999
+ function buildUpdateTool(agentMap, clients) {
4000
+ return tool(async (input, runtime) => {
4001
+ const tracked = resolveTrackedTask(input.taskId, runtime.state);
4002
+ if (typeof tracked === "string") return tracked;
4003
+ const spec = agentMap[tracked.agentName];
4004
+ try {
4005
+ const run = await clients.getClient(tracked.agentName).runs.create(tracked.threadId, spec.graphId, {
4006
+ input: { messages: [{
4007
+ role: "user",
4008
+ content: input.message
4009
+ }] },
4010
+ multitaskStrategy: "interrupt"
2964
4011
  });
4012
+ const task = {
4013
+ taskId: tracked.taskId,
4014
+ agentName: tracked.agentName,
4015
+ threadId: tracked.threadId,
4016
+ runId: run.run_id,
4017
+ status: "running",
4018
+ createdAt: tracked.createdAt,
4019
+ description: input.message,
4020
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4021
+ checkedAt: tracked.checkedAt
4022
+ };
4023
+ return new Command({ update: {
4024
+ messages: [new ToolMessage({
4025
+ content: `Updated async subagent. taskId: ${tracked.taskId}`,
4026
+ tool_call_id: toolCallIdFromRuntime(runtime)
4027
+ })],
4028
+ asyncTasks: { [tracked.taskId]: task }
4029
+ } });
4030
+ } catch (e) {
4031
+ return `Failed to update async subagent: ${e}`;
4032
+ }
4033
+ }, {
4034
+ name: "update_async_task",
4035
+ description: "send updated instructions to an async subagent. Interrupts the current run and starts a new one on the same thread so the subagent sees the full conversation history plus your new message. The taskId remains the same.",
4036
+ schema: z.object({
4037
+ taskId: z.string().describe("The exact taskId string returned by start_async_task. Pass it verbatim."),
4038
+ message: z.string().describe("Follow-up instructions or context to send to the subagent")
4039
+ })
4040
+ });
4041
+ }
4042
+ /**
4043
+ * Build the `cancel_async_task` tool.
4044
+ *
4045
+ * Cancels the current run on the remote server and updates the task's
4046
+ * cached status to `"cancelled"`.
4047
+ */
4048
+ function buildCancelTool(clients) {
4049
+ return tool(async (input, runtime) => {
4050
+ const tracked = resolveTrackedTask(input.taskId, runtime.state);
4051
+ if (typeof tracked === "string") return tracked;
4052
+ const client = clients.getClient(tracked.agentName);
4053
+ try {
4054
+ await client.runs.cancel(tracked.threadId, tracked.runId);
4055
+ } catch (e) {
4056
+ return `Failed to cancel run: ${e}`;
2965
4057
  }
4058
+ const updated = {
4059
+ taskId: tracked.taskId,
4060
+ agentName: tracked.agentName,
4061
+ threadId: tracked.threadId,
4062
+ runId: tracked.runId,
4063
+ status: "cancelled",
4064
+ createdAt: tracked.createdAt,
4065
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4066
+ checkedAt: tracked.checkedAt
4067
+ };
2966
4068
  return new Command({ update: {
2967
- _summarizationEvent: {
2968
- cutoffIndex: finalStateCutoffIndex,
2969
- summaryMessage: finalSummaryMessage,
2970
- filePath: finalFilePath
2971
- },
2972
- _summarizationSessionId: getSessionId(request.state)
4069
+ messages: [new ToolMessage({
4070
+ content: `Cancelled async subagent task: ${tracked.taskId}`,
4071
+ tool_call_id: toolCallIdFromRuntime(runtime)
4072
+ })],
4073
+ asyncTasks: { [tracked.taskId]: updated }
2973
4074
  } });
2974
- }
4075
+ }, {
4076
+ name: "cancel_async_task",
4077
+ description: "Cancel a running async subagent task. Use this to stop a task that is no longer needed.",
4078
+ schema: z.object({ taskId: z.string().describe("The exact taskId string returned by start_async_task. Pass it verbatim.") })
4079
+ });
4080
+ }
4081
+ /**
4082
+ * Build the `list_async_tasks` tool.
4083
+ *
4084
+ * Lists all tracked tasks with their live statuses fetched in parallel.
4085
+ * Supports optional filtering by cached status.
4086
+ */
4087
+ function buildListTool(clients) {
4088
+ return tool(async (input, runtime) => {
4089
+ const filtered = filterTasks(runtime.state.asyncTasks ?? {}, input.statusFilter ?? void 0);
4090
+ if (filtered.length === 0) return "No async subagent tasks tracked";
4091
+ const statuses = await Promise.all(filtered.map((task) => fetchLiveTaskStatus(clients, task)));
4092
+ const updatedTasks = {};
4093
+ const entries = [];
4094
+ for (let idx = 0; idx < filtered.length; idx++) {
4095
+ const task = filtered[idx];
4096
+ const status = statuses[idx];
4097
+ const taskEntry = formatTaskEntry(task, status);
4098
+ entries.push(taskEntry);
4099
+ updatedTasks[task.taskId] = {
4100
+ taskId: task.taskId,
4101
+ agentName: task.agentName,
4102
+ threadId: task.threadId,
4103
+ runId: task.runId,
4104
+ status,
4105
+ createdAt: task.createdAt,
4106
+ updatedAt: status !== task.status ? (/* @__PURE__ */ new Date()).toISOString() : task.updatedAt,
4107
+ checkedAt: task.checkedAt
4108
+ };
4109
+ }
4110
+ return new Command({ update: {
4111
+ messages: [new ToolMessage({
4112
+ content: `${entries.length} tracked task(s):\n${entries.join("\n")}`,
4113
+ tool_call_id: toolCallIdFromRuntime(runtime)
4114
+ })],
4115
+ asyncTasks: updatedTasks
4116
+ } });
4117
+ }, {
4118
+ name: "list_async_tasks",
4119
+ description: "List tracked async subagent tasks with their current live statuses. Be default shows all tasks. Use `statusFilter` to narrow by status (e.g., 'running', 'success', 'error', 'cancelled'). Use `check_async_task` to get the full result of a specific completed task.",
4120
+ schema: z.object({ statusFilter: z.string().nullish().describe("Filter tasks by status. One of: 'running', 'success', 'error', 'cancelled', 'all'. Defaults to 'all'.") })
4121
+ });
4122
+ }
4123
+ /**
4124
+ * Create middleware that adds async subagent tools to an agent.
4125
+ *
4126
+ * Provides five tools for launching, checking, updating, cancelling, and
4127
+ * listing background tasks on remote Agent Protocol servers. Task state is
4128
+ * persisted in the `asyncTasks` state channel so it survives
4129
+ * context compaction.
4130
+ *
4131
+ * Works with any Agent Protocol-compliant server — LangGraph Platform (managed)
4132
+ * or self-hosted (e.g. a Hono/Express server implementing the Agent Protocol spec).
4133
+ *
4134
+ * @throws {Error} If no async subagents are provided or names are duplicated.
4135
+ *
4136
+ * @example
4137
+ * ```ts
4138
+ * const middleware = createAsyncSubAgentMiddleware({
4139
+ * asyncSubAgents: [{
4140
+ * name: "researcher",
4141
+ * description: "Research agent for deep analysis",
4142
+ * url: "https://my-agent-protocol-server.example.com",
4143
+ * graphId: "research_agent",
4144
+ * }],
4145
+ * });
4146
+ * ```
4147
+ */
4148
+ /**
4149
+ * Type guard to distinguish async SubAgents from sync SubAgents/CompiledSubAgents.
4150
+ *
4151
+ * Uses the presence of the `graphId` field as the runtime discriminant —
4152
+ * `AsyncSubAgent` requires it, while `SubAgent` and `CompiledSubAgent` do not have it.
4153
+ */
4154
+ function isAsyncSubAgent(subAgent) {
4155
+ return "graphId" in subAgent;
4156
+ }
4157
+ function createAsyncSubAgentMiddleware(options) {
4158
+ const { asyncSubAgents, systemPrompt = ASYNC_TASK_SYSTEM_PROMPT } = options;
4159
+ if (!asyncSubAgents || asyncSubAgents.length === 0) throw new Error("At least one async subagent must be specified");
4160
+ const names = asyncSubAgents.map((a) => a.name);
4161
+ const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
4162
+ if (duplicates.length > 0) throw new Error(`Duplicate async subagent names: ${[...new Set(duplicates)].join(", ")}`);
4163
+ const agentMap = Object.fromEntries(asyncSubAgents.map((a) => [a.name, a]));
4164
+ const clients = new ClientCache(agentMap);
4165
+ const agentsDescription = asyncSubAgents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
4166
+ const tools = [
4167
+ buildStartTool(agentMap, clients, ASYNC_TASK_TOOL_DESCRIPTION.replace("{available_agents}", agentsDescription)),
4168
+ buildCheckTool(clients),
4169
+ buildUpdateTool(agentMap, clients),
4170
+ buildCancelTool(clients),
4171
+ buildListTool(clients)
4172
+ ];
4173
+ const fullSystemPrompt = systemPrompt ? `${systemPrompt}\n\nAvailable async subagent types:\n${agentsDescription}` : null;
2975
4174
  return createMiddleware({
2976
- name: "SummarizationMiddleware",
2977
- stateSchema: SummarizationStateSchema,
2978
- async wrapModelCall(request, handler) {
2979
- const effectiveMessages = getEffectiveMessages(request.messages ?? [], request.state);
2980
- if (effectiveMessages.length === 0) return handler(request);
2981
- /**
2982
- * Resolve the chat model and get max input tokens from its profile.
2983
- */
2984
- const resolvedModel = await getChatModel();
2985
- const maxInputTokens = getMaxInputTokens(resolvedModel);
2986
- applyModelDefaults(resolvedModel);
2987
- /**
2988
- * Step 1: Truncate args if configured
2989
- */
2990
- const { messages: truncatedMessages } = truncateArgs(effectiveMessages, maxInputTokens, request.systemMessage, request.tools);
2991
- /**
2992
- * Step 2: Check if summarization should happen.
2993
- * Count tokens including system message and tools to match what's
2994
- * actually sent to the model (matching Python implementation).
2995
- */
2996
- const totalTokens = countTotalTokens(truncatedMessages, request.systemMessage, request.tools);
2997
- /**
2998
- * If no summarization needed, try passing through.
2999
- * If the handler throws a ContextOverflowError, fall back to
3000
- * emergency summarization (matching Python's behavior).
3001
- */
3002
- if (!shouldSummarize(truncatedMessages, totalTokens, maxInputTokens)) try {
3003
- return await handler({
3004
- ...request,
3005
- messages: truncatedMessages
3006
- });
3007
- } catch (err) {
3008
- if (!isContextOverflow(err)) throw err;
3009
- if (maxInputTokens && totalTokens > 0) {
3010
- const observedRatio = maxInputTokens / totalTokens;
3011
- if (observedRatio > tokenEstimationMultiplier) tokenEstimationMultiplier = observedRatio * 1.1;
3012
- }
3013
- }
3014
- /**
3015
- * Step 3: Perform summarization
3016
- */
3017
- return performSummarization(request, handler, truncatedMessages, resolvedModel, maxInputTokens);
4175
+ name: "asyncSubAgentMiddleware",
4176
+ stateSchema: AsyncTaskStateSchema,
4177
+ tools,
4178
+ wrapModelCall: async (request, handler) => {
4179
+ if (fullSystemPrompt !== null) return handler({
4180
+ ...request,
4181
+ systemMessage: request.systemMessage.concat(new SystemMessage({ content: fullSystemPrompt }))
4182
+ });
4183
+ return handler(request);
3018
4184
  }
3019
4185
  });
3020
4186
  }
3021
4187
  //#endregion
3022
4188
  //#region src/backends/store.ts
4189
+ /**
4190
+ * StoreBackend: Adapter for LangGraph's BaseStore (persistent, cross-thread).
4191
+ */
3023
4192
  const NAMESPACE_COMPONENT_RE = /^[A-Za-z0-9\-_.@+:~]+$/;
3024
4193
  /**
3025
4194
  * Validate a namespace array.
@@ -3054,34 +4223,55 @@ function validateNamespace(namespace) {
3054
4223
  var StoreBackend = class {
3055
4224
  stateAndStore;
3056
4225
  _namespace;
3057
- constructor(stateAndStore, options) {
3058
- this.stateAndStore = stateAndStore;
3059
- if (options?.namespace) this._namespace = validateNamespace(options.namespace);
4226
+ fileFormat;
4227
+ constructor(stateAndStoreOrOptions, options) {
4228
+ let opts;
4229
+ if (stateAndStoreOrOptions != null && typeof stateAndStoreOrOptions === "object" && "state" in stateAndStoreOrOptions) {
4230
+ this.stateAndStore = stateAndStoreOrOptions;
4231
+ opts = options;
4232
+ } else {
4233
+ this.stateAndStore = void 0;
4234
+ opts = stateAndStoreOrOptions;
4235
+ }
4236
+ if (opts?.namespace) this._namespace = validateNamespace(opts.namespace);
4237
+ this.fileFormat = opts?.fileFormat ?? "v2";
3060
4238
  }
3061
4239
  /**
3062
- * Get the store instance.
4240
+ * Get the BaseStore instance for persistent storage operations.
4241
+ *
4242
+ * In legacy mode, reads from the injected {@link StateAndStore}.
4243
+ * In zero-arg mode, retrieves the store from the LangGraph execution
4244
+ * context via {@link getLangGraphStore}.
3063
4245
  *
3064
4246
  * @returns BaseStore instance
3065
- * @throws Error if no store is available
4247
+ * @throws Error if no store is available in either mode
3066
4248
  */
3067
4249
  getStore() {
3068
- const store = this.stateAndStore.store;
3069
- if (!store) throw new Error("Store is required but not available in StateAndStore");
4250
+ if (this.stateAndStore) {
4251
+ const store = this.stateAndStore.store;
4252
+ if (!store) throw new Error("Store is required but not available in runtime");
4253
+ return store;
4254
+ }
4255
+ const store = getStore();
4256
+ if (!store) throw new Error("Store is required but not available in LangGraph execution context. Ensure the graph was configured with a store.");
3070
4257
  return store;
3071
4258
  }
3072
4259
  /**
3073
4260
  * Get the namespace for store operations.
3074
4261
  *
3075
- * If a custom namespace was provided, returns it directly.
3076
- *
3077
- * Otherwise, falls back to legacy behavior:
3078
- * - If assistantId is set: [assistantId, "filesystem"]
3079
- * - Otherwise: ["filesystem"]
4262
+ * Resolution order:
4263
+ * 1. Explicit namespace from constructor options (both modes)
4264
+ * 2. Legacy mode: `[assistantId, "filesystem"]` fallback from {@link StateAndStore}
4265
+ * 3. Zero-arg mode without namespace: `["filesystem"]` with a deprecation warning
4266
+ * nudging callers to pass an explicit namespace
4267
+ * 4. Legacy mode without assistantId: `["filesystem"]`
3080
4268
  */
3081
4269
  getNamespace() {
3082
4270
  if (this._namespace) return this._namespace;
3083
- const assistantId = this.stateAndStore.assistantId;
3084
- if (assistantId) return [assistantId, "filesystem"];
4271
+ if (this.stateAndStore) {
4272
+ const assistantId = this.stateAndStore.assistantId;
4273
+ if (assistantId) return [assistantId, "filesystem"];
4274
+ }
3085
4275
  return ["filesystem"];
3086
4276
  }
3087
4277
  /**
@@ -3093,9 +4283,10 @@ var StoreBackend = class {
3093
4283
  */
3094
4284
  convertStoreItemToFileData(storeItem) {
3095
4285
  const value = storeItem.value;
3096
- if (!value.content || !Array.isArray(value.content) || typeof value.created_at !== "string" || typeof value.modified_at !== "string") throw new Error(`Store item does not contain valid FileData fields. Got keys: ${Object.keys(value).join(", ")}`);
4286
+ if (!(value.content !== void 0 && (Array.isArray(value.content) || typeof value.content === "string" || ArrayBuffer.isView(value.content))) || typeof value.created_at !== "string" || typeof value.modified_at !== "string") throw new Error(`Store item does not contain valid FileData fields. Got keys: ${Object.keys(value).join(", ")}`);
3097
4287
  return {
3098
4288
  content: value.content,
4289
+ ...value.mimeType ? { mimeType: value.mimeType } : {},
3099
4290
  created_at: value.created_at,
3100
4291
  modified_at: value.modified_at
3101
4292
  };
@@ -3104,11 +4295,12 @@ var StoreBackend = class {
3104
4295
  * Convert FileData to a value suitable for store.put().
3105
4296
  *
3106
4297
  * @param fileData - The FileData to convert
3107
- * @returns Object with content, created_at, and modified_at fields
4298
+ * @returns Object with content, mimeType, created_at, and modified_at fields
3108
4299
  */
3109
4300
  convertFileDataToStoreValue(fileData) {
3110
4301
  return {
3111
4302
  content: fileData.content,
4303
+ ..."mimeType" in fileData ? { mimeType: fileData.mimeType } : {},
3112
4304
  created_at: fileData.created_at,
3113
4305
  modified_at: fileData.modified_at
3114
4306
  };
@@ -3143,10 +4335,10 @@ var StoreBackend = class {
3143
4335
  * List files and directories in the specified directory (non-recursive).
3144
4336
  *
3145
4337
  * @param path - Absolute path to directory
3146
- * @returns List of FileInfo objects for files and directories directly in the directory.
4338
+ * @returns LsResult with list of FileInfo objects on success or error on failure.
3147
4339
  * Directories have a trailing / in their path and is_dir=true.
3148
4340
  */
3149
- async lsInfo(path) {
4341
+ async ls(path) {
3150
4342
  const store = this.getStore();
3151
4343
  const namespace = this.getNamespace();
3152
4344
  const items = await this.searchStorePaginated(store, namespace);
@@ -3164,7 +4356,7 @@ var StoreBackend = class {
3164
4356
  }
3165
4357
  try {
3166
4358
  const fd = this.convertStoreItemToFileData(item);
3167
- const size = fd.content.join("\n").length;
4359
+ const size = isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length;
3168
4360
  infos.push({
3169
4361
  path: itemKey,
3170
4362
  is_dir: false,
@@ -3182,35 +4374,49 @@ var StoreBackend = class {
3182
4374
  modified_at: ""
3183
4375
  });
3184
4376
  infos.sort((a, b) => a.path.localeCompare(b.path));
3185
- return infos;
4377
+ return { files: infos };
3186
4378
  }
3187
4379
  /**
3188
- * Read file content with line numbers.
4380
+ * Read file content.
4381
+ *
4382
+ * Text files are paginated by line offset/limit.
4383
+ * Binary files return full Uint8Array content (offset/limit ignored).
3189
4384
  *
3190
4385
  * @param filePath - Absolute file path
3191
4386
  * @param offset - Line offset to start reading from (0-indexed)
3192
4387
  * @param limit - Maximum number of lines to read
3193
- * @returns Formatted file content with line numbers, or error message
4388
+ * @returns ReadResult with content on success or error on failure
3194
4389
  */
3195
4390
  async read(filePath, offset = 0, limit = 500) {
3196
4391
  try {
3197
- return formatReadResponse(await this.readRaw(filePath), offset, limit);
4392
+ const readRawResult = await this.readRaw(filePath);
4393
+ if (readRawResult.error || !readRawResult.data) return { error: readRawResult.error || "File data not found" };
4394
+ const fileDataV2 = migrateToFileDataV2(readRawResult.data, filePath);
4395
+ if (!isTextMimeType(fileDataV2.mimeType)) return {
4396
+ content: fileDataV2.content,
4397
+ mimeType: fileDataV2.mimeType
4398
+ };
4399
+ if (typeof fileDataV2.content !== "string") return { error: `File '${filePath}' has binary content but text MIME type` };
4400
+ return {
4401
+ content: fileDataV2.content.split("\n").slice(offset, offset + limit).join("\n"),
4402
+ mimeType: fileDataV2.mimeType
4403
+ };
3198
4404
  } catch (e) {
3199
- return `Error: ${e.message}`;
4405
+ return { error: e.message };
3200
4406
  }
3201
4407
  }
3202
4408
  /**
3203
4409
  * Read file content as raw FileData.
3204
4410
  *
3205
4411
  * @param filePath - Absolute file path
3206
- * @returns Raw file content as FileData
4412
+ * @returns ReadRawResult with raw file data on success or error on failure
3207
4413
  */
3208
4414
  async readRaw(filePath) {
3209
4415
  const store = this.getStore();
3210
4416
  const namespace = this.getNamespace();
3211
4417
  const item = await store.get(namespace, filePath);
3212
- if (!item) throw new Error(`File '${filePath}' not found`);
3213
- return this.convertStoreItemToFileData(item);
4418
+ if (!item) return { error: `File '${filePath}' not found` };
4419
+ return { data: this.convertStoreItemToFileData(item) };
3214
4420
  }
3215
4421
  /**
3216
4422
  * Create a new file with content.
@@ -3220,7 +4426,8 @@ var StoreBackend = class {
3220
4426
  const store = this.getStore();
3221
4427
  const namespace = this.getNamespace();
3222
4428
  if (await store.get(namespace, filePath)) return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
3223
- const fileData = createFileData(content);
4429
+ const mimeType = getMimeType(filePath);
4430
+ const fileData = createFileData(content, void 0, this.fileFormat, mimeType);
3224
4431
  const storeValue = this.convertFileDataToStoreValue(fileData);
3225
4432
  await store.put(namespace, filePath, storeValue);
3226
4433
  return {
@@ -3255,9 +4462,10 @@ var StoreBackend = class {
3255
4462
  }
3256
4463
  }
3257
4464
  /**
3258
- * Structured search results or error string for invalid input.
4465
+ * Search file contents for a literal text pattern.
4466
+ * Binary files are skipped.
3259
4467
  */
3260
- async grepRaw(pattern, path = "/", glob = null) {
4468
+ async grep(pattern, path = "/", glob = null) {
3261
4469
  const store = this.getStore();
3262
4470
  const namespace = this.getNamespace();
3263
4471
  const items = await this.searchStorePaginated(store, namespace);
@@ -3267,12 +4475,12 @@ var StoreBackend = class {
3267
4475
  } catch {
3268
4476
  continue;
3269
4477
  }
3270
- return grepMatchesFromFiles(files, pattern, path, glob);
4478
+ return { matches: grepMatchesFromFiles(files, pattern, path, glob) };
3271
4479
  }
3272
4480
  /**
3273
4481
  * Structured glob matching returning FileInfo objects.
3274
4482
  */
3275
- async globInfo(pattern, path = "/") {
4483
+ async glob(pattern, path = "/") {
3276
4484
  const store = this.getStore();
3277
4485
  const namespace = this.getNamespace();
3278
4486
  const items = await this.searchStorePaginated(store, namespace);
@@ -3283,12 +4491,12 @@ var StoreBackend = class {
3283
4491
  continue;
3284
4492
  }
3285
4493
  const result = globSearchFiles(files, pattern, path);
3286
- if (result === "No files found") return [];
4494
+ if (result === "No files found") return { files: [] };
3287
4495
  const paths = result.split("\n");
3288
4496
  const infos = [];
3289
4497
  for (const p of paths) {
3290
4498
  const fd = files[p];
3291
- const size = fd ? fd.content.join("\n").length : 0;
4499
+ const size = fd ? isFileDataV1(fd) ? fd.content.join("\n").length : isFileDataBinary(fd) ? fd.content.byteLength : fd.content.length : 0;
3292
4500
  infos.push({
3293
4501
  path: p,
3294
4502
  is_dir: false,
@@ -3296,7 +4504,7 @@ var StoreBackend = class {
3296
4504
  modified_at: fd?.modified_at || ""
3297
4505
  });
3298
4506
  }
3299
- return infos;
4507
+ return { files: infos };
3300
4508
  }
3301
4509
  /**
3302
4510
  * Upload multiple files.
@@ -3309,7 +4517,11 @@ var StoreBackend = class {
3309
4517
  const namespace = this.getNamespace();
3310
4518
  const responses = [];
3311
4519
  for (const [path, content] of files) try {
3312
- const fileData = createFileData(new TextDecoder().decode(content));
4520
+ const mimeType = getMimeType(path);
4521
+ const isBinary = this.fileFormat === "v2" && !isTextMimeType(mimeType);
4522
+ let fileData;
4523
+ if (isBinary) fileData = createFileData(content, void 0, "v2", mimeType);
4524
+ else fileData = createFileData(new TextDecoder().decode(content), void 0, this.fileFormat, mimeType);
3313
4525
  const storeValue = this.convertFileDataToStoreValue(fileData);
3314
4526
  await store.put(namespace, path, storeValue);
3315
4527
  responses.push({
@@ -3344,11 +4556,17 @@ var StoreBackend = class {
3344
4556
  });
3345
4557
  continue;
3346
4558
  }
3347
- const contentStr = fileDataToString(this.convertStoreItemToFileData(item));
3348
- const content = new TextEncoder().encode(contentStr);
3349
- responses.push({
4559
+ const fileDataV2 = migrateToFileDataV2(this.convertStoreItemToFileData(item), path);
4560
+ if (typeof fileDataV2.content === "string") {
4561
+ const content = new TextEncoder().encode(fileDataV2.content);
4562
+ responses.push({
4563
+ path,
4564
+ content,
4565
+ error: null
4566
+ });
4567
+ } else responses.push({
3350
4568
  path,
3351
- content,
4569
+ content: fileDataV2.content,
3352
4570
  error: null
3353
4571
  });
3354
4572
  } catch {
@@ -3386,7 +4604,7 @@ var FilesystemBackend = class {
3386
4604
  maxFileSizeBytes;
3387
4605
  constructor(options = {}) {
3388
4606
  const { rootDir, virtualMode = false, maxFileSizeMb = 10 } = options;
3389
- this.cwd = rootDir ? path.resolve(rootDir) : process.cwd();
4607
+ this.cwd = rootDir ? path$1.resolve(rootDir) : process.cwd();
3390
4608
  this.virtualMode = virtualMode;
3391
4609
  this.maxFileSizeBytes = maxFileSizeMb * 1024 * 1024;
3392
4610
  }
@@ -3406,13 +4624,13 @@ var FilesystemBackend = class {
3406
4624
  if (this.virtualMode) {
3407
4625
  const vpath = key.startsWith("/") ? key : "/" + key;
3408
4626
  if (vpath.includes("..") || vpath.startsWith("~")) throw new Error("Path traversal not allowed");
3409
- const full = path.resolve(this.cwd, vpath.substring(1));
3410
- const relative = path.relative(this.cwd, full);
3411
- if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Path: ${full} outside root directory: ${this.cwd}`);
4627
+ const full = path$1.resolve(this.cwd, vpath.substring(1));
4628
+ const relative = path$1.relative(this.cwd, full);
4629
+ if (relative.startsWith("..") || path$1.isAbsolute(relative)) throw new Error(`Path: ${full} outside root directory: ${this.cwd}`);
3412
4630
  return full;
3413
4631
  }
3414
- if (path.isAbsolute(key)) return key;
3415
- return path.resolve(this.cwd, key);
4632
+ if (path$1.isAbsolute(key)) return key;
4633
+ return path$1.resolve(this.cwd, key);
3416
4634
  }
3417
4635
  /**
3418
4636
  * List files and directories in the specified directory (non-recursive).
@@ -3421,15 +4639,15 @@ var FilesystemBackend = class {
3421
4639
  * @returns List of FileInfo objects for files and directories directly in the directory.
3422
4640
  * Directories have a trailing / in their path and is_dir=true.
3423
4641
  */
3424
- async lsInfo(dirPath) {
4642
+ async ls(dirPath) {
3425
4643
  try {
3426
4644
  const resolvedPath = this.resolvePath(dirPath);
3427
- if (!(await fs.stat(resolvedPath)).isDirectory()) return [];
4645
+ if (!(await fs.stat(resolvedPath)).isDirectory()) return { files: [] };
3428
4646
  const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
3429
4647
  const results = [];
3430
- const cwdStr = this.cwd.endsWith(path.sep) ? this.cwd : this.cwd + path.sep;
4648
+ const cwdStr = this.cwd.endsWith(path$1.sep) ? this.cwd : this.cwd + path$1.sep;
3431
4649
  for (const entry of entries) {
3432
- const fullPath = path.join(resolvedPath, entry.name);
4650
+ const fullPath = path$1.join(resolvedPath, entry.name);
3433
4651
  try {
3434
4652
  const entryStat = await fs.stat(fullPath);
3435
4653
  const isFile = entryStat.isFile();
@@ -3442,7 +4660,7 @@ var FilesystemBackend = class {
3442
4660
  modified_at: entryStat.mtime.toISOString()
3443
4661
  });
3444
4662
  else if (isDir) results.push({
3445
- path: fullPath + path.sep,
4663
+ path: fullPath + path$1.sep,
3446
4664
  is_dir: true,
3447
4665
  size: 0,
3448
4666
  modified_at: entryStat.mtime.toISOString()
@@ -3452,7 +4670,7 @@ var FilesystemBackend = class {
3452
4670
  if (fullPath.startsWith(cwdStr)) relativePath = fullPath.substring(cwdStr.length);
3453
4671
  else if (fullPath.startsWith(this.cwd)) relativePath = fullPath.substring(this.cwd.length).replace(/^[/\\]/, "");
3454
4672
  else relativePath = fullPath;
3455
- relativePath = relativePath.split(path.sep).join("/");
4673
+ relativePath = relativePath.split(path$1.sep).join("/");
3456
4674
  const virtPath = "/" + relativePath;
3457
4675
  if (isFile) results.push({
3458
4676
  path: virtPath,
@@ -3472,9 +4690,9 @@ var FilesystemBackend = class {
3472
4690
  }
3473
4691
  }
3474
4692
  results.sort((a, b) => a.path.localeCompare(b.path));
3475
- return results;
4693
+ return { files: results };
3476
4694
  } catch {
3477
- return [];
4695
+ return { files: [] };
3478
4696
  }
3479
4697
  }
3480
4698
  /**
@@ -3488,62 +4706,105 @@ var FilesystemBackend = class {
3488
4706
  async read(filePath, offset = 0, limit = 500) {
3489
4707
  try {
3490
4708
  const resolvedPath = this.resolvePath(filePath);
4709
+ const mimeType = getMimeType(filePath);
4710
+ const isBinary = !isTextMimeType(mimeType);
3491
4711
  let content;
3492
4712
  if (SUPPORTS_NOFOLLOW) {
3493
- if (!(await fs.stat(resolvedPath)).isFile()) return `Error: File '${filePath}' not found`;
4713
+ if (!(await fs.stat(resolvedPath)).isFile()) return { error: `File '${filePath}' not found` };
3494
4714
  const fd = await fs.open(resolvedPath, fs$1.constants.O_RDONLY | fs$1.constants.O_NOFOLLOW);
3495
4715
  try {
4716
+ if (isBinary) {
4717
+ const buffer = await fd.readFile();
4718
+ return {
4719
+ content: new Uint8Array(buffer),
4720
+ mimeType
4721
+ };
4722
+ }
3496
4723
  content = await fd.readFile({ encoding: "utf-8" });
3497
4724
  } finally {
3498
4725
  await fd.close();
3499
4726
  }
3500
4727
  } else {
3501
4728
  const stat = await fs.lstat(resolvedPath);
3502
- if (stat.isSymbolicLink()) return `Error: Symlinks are not allowed: ${filePath}`;
3503
- if (!stat.isFile()) return `Error: File '${filePath}' not found`;
4729
+ if (stat.isSymbolicLink()) return { error: `Symlinks are not allowed: ${filePath}` };
4730
+ if (!stat.isFile()) return { error: `File '${filePath}' not found` };
4731
+ if (isBinary) {
4732
+ const buffer = await fs.readFile(resolvedPath);
4733
+ return {
4734
+ content: new Uint8Array(buffer),
4735
+ mimeType
4736
+ };
4737
+ }
3504
4738
  content = await fs.readFile(resolvedPath, "utf-8");
3505
4739
  }
3506
4740
  const emptyMsg = checkEmptyContent(content);
3507
- if (emptyMsg) return emptyMsg;
4741
+ if (emptyMsg) return {
4742
+ content: emptyMsg,
4743
+ mimeType
4744
+ };
3508
4745
  const lines = content.split("\n");
3509
4746
  const startIdx = offset;
3510
4747
  const endIdx = Math.min(startIdx + limit, lines.length);
3511
- if (startIdx >= lines.length) return `Error: Line offset ${offset} exceeds file length (${lines.length} lines)`;
3512
- return formatContentWithLineNumbers(lines.slice(startIdx, endIdx), startIdx + 1);
4748
+ if (startIdx >= lines.length) return { error: `Line offset ${offset} exceeds file length (${lines.length} lines)` };
4749
+ return {
4750
+ content: lines.slice(startIdx, endIdx).join("\n"),
4751
+ mimeType
4752
+ };
3513
4753
  } catch (e) {
3514
- return `Error reading file '${filePath}': ${e.message}`;
4754
+ return { error: `Error reading file '${filePath}': ${e.message}` };
3515
4755
  }
3516
4756
  }
3517
4757
  /**
3518
4758
  * Read file content as raw FileData.
3519
4759
  *
3520
4760
  * @param filePath - Absolute file path
3521
- * @returns Raw file content as FileData
4761
+ * @returns ReadRawResult with raw file data on success or error on failure
3522
4762
  */
3523
4763
  async readRaw(filePath) {
3524
4764
  const resolvedPath = this.resolvePath(filePath);
4765
+ const mimeType = getMimeType(filePath);
4766
+ const isBinary = !isTextMimeType(mimeType);
3525
4767
  let content;
3526
4768
  let stat;
3527
4769
  if (SUPPORTS_NOFOLLOW) {
3528
4770
  stat = await fs.stat(resolvedPath);
3529
- if (!stat.isFile()) throw new Error(`File '${filePath}' not found`);
4771
+ if (!stat.isFile()) return { error: `File '${filePath}' not found` };
3530
4772
  const fd = await fs.open(resolvedPath, fs$1.constants.O_RDONLY | fs$1.constants.O_NOFOLLOW);
3531
4773
  try {
4774
+ if (isBinary) {
4775
+ const buffer = await fd.readFile();
4776
+ return { data: {
4777
+ content: new Uint8Array(buffer),
4778
+ mimeType,
4779
+ created_at: stat.ctime.toISOString(),
4780
+ modified_at: stat.mtime.toISOString()
4781
+ } };
4782
+ }
3532
4783
  content = await fd.readFile({ encoding: "utf-8" });
3533
4784
  } finally {
3534
4785
  await fd.close();
3535
4786
  }
3536
4787
  } else {
3537
4788
  stat = await fs.lstat(resolvedPath);
3538
- if (stat.isSymbolicLink()) throw new Error(`Symlinks are not allowed: ${filePath}`);
3539
- if (!stat.isFile()) throw new Error(`File '${filePath}' not found`);
4789
+ if (stat.isSymbolicLink()) return { error: `Symlinks are not allowed: ${filePath}` };
4790
+ if (!stat.isFile()) return { error: `File '${filePath}' not found` };
4791
+ if (isBinary) {
4792
+ const buffer = await fs.readFile(resolvedPath);
4793
+ return { data: {
4794
+ content: new Uint8Array(buffer),
4795
+ mimeType,
4796
+ created_at: stat.ctime.toISOString(),
4797
+ modified_at: stat.mtime.toISOString()
4798
+ } };
4799
+ }
3540
4800
  content = await fs.readFile(resolvedPath, "utf-8");
3541
4801
  }
3542
- return {
3543
- content: content.split("\n"),
4802
+ return { data: {
4803
+ content,
4804
+ mimeType,
3544
4805
  created_at: stat.ctime.toISOString(),
3545
4806
  modified_at: stat.mtime.toISOString()
3546
- };
4807
+ } };
3547
4808
  }
3548
4809
  /**
3549
4810
  * Create a new file with content.
@@ -3552,19 +4813,26 @@ var FilesystemBackend = class {
3552
4813
  async write(filePath, content) {
3553
4814
  try {
3554
4815
  const resolvedPath = this.resolvePath(filePath);
4816
+ const isBinary = !isTextMimeType(getMimeType(filePath));
3555
4817
  try {
3556
4818
  if ((await fs.lstat(resolvedPath)).isSymbolicLink()) return { error: `Cannot write to ${filePath} because it is a symlink. Symlinks are not allowed.` };
3557
4819
  return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
3558
4820
  } catch {}
3559
- await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
4821
+ await fs.mkdir(path$1.dirname(resolvedPath), { recursive: true });
3560
4822
  if (SUPPORTS_NOFOLLOW) {
3561
4823
  const flags = fs$1.constants.O_WRONLY | fs$1.constants.O_CREAT | fs$1.constants.O_TRUNC | fs$1.constants.O_NOFOLLOW;
3562
4824
  const fd = await fs.open(resolvedPath, flags, 420);
3563
4825
  try {
3564
- await fd.writeFile(content, "utf-8");
4826
+ if (isBinary) {
4827
+ const buffer = Buffer.from(content, "base64");
4828
+ await fd.writeFile(buffer);
4829
+ } else await fd.writeFile(content, "utf-8");
3565
4830
  } finally {
3566
4831
  await fd.close();
3567
4832
  }
4833
+ } else if (isBinary) {
4834
+ const buffer = Buffer.from(content, "base64");
4835
+ await fs.writeFile(resolvedPath, buffer);
3568
4836
  } else await fs.writeFile(resolvedPath, content, "utf-8");
3569
4837
  return {
3570
4838
  path: filePath,
@@ -3627,17 +4895,17 @@ var FilesystemBackend = class {
3627
4895
  * @param glob - Optional glob pattern to filter which files to search.
3628
4896
  * @returns List of GrepMatch dicts containing path, line number, and matched text.
3629
4897
  */
3630
- async grepRaw(pattern, dirPath = "/", glob = null) {
4898
+ async grep(pattern, dirPath = "/", glob = null) {
3631
4899
  let baseFull;
3632
4900
  try {
3633
4901
  baseFull = this.resolvePath(dirPath || ".");
3634
4902
  } catch {
3635
- return [];
4903
+ return { matches: [] };
3636
4904
  }
3637
4905
  try {
3638
4906
  await fs.stat(baseFull);
3639
4907
  } catch {
3640
- return [];
4908
+ return { matches: [] };
3641
4909
  }
3642
4910
  let results = await this.ripgrepSearch(pattern, baseFull, glob);
3643
4911
  if (results === null) results = await this.literalSearch(pattern, baseFull, glob);
@@ -3647,7 +4915,7 @@ var FilesystemBackend = class {
3647
4915
  line: lineNum,
3648
4916
  text: lineText
3649
4917
  });
3650
- return matches;
4918
+ return { matches };
3651
4919
  }
3652
4920
  /**
3653
4921
  * Search using ripgrep with fixed-string (literal) mode.
@@ -3684,10 +4952,10 @@ var FilesystemBackend = class {
3684
4952
  if (!ftext) continue;
3685
4953
  let virtPath;
3686
4954
  if (this.virtualMode) try {
3687
- const resolved = path.resolve(ftext);
3688
- const relative = path.relative(this.cwd, resolved);
4955
+ const resolved = path$1.resolve(ftext);
4956
+ const relative = path$1.relative(this.cwd, resolved);
3689
4957
  if (relative.startsWith("..")) continue;
3690
- virtPath = "/" + relative.split(path.sep).join("/");
4958
+ virtPath = "/" + relative.split(path$1.sep).join("/");
3691
4959
  } catch {
3692
4960
  continue;
3693
4961
  }
@@ -3721,13 +4989,14 @@ var FilesystemBackend = class {
3721
4989
  async literalSearch(pattern, baseFull, includeGlob) {
3722
4990
  const results = {};
3723
4991
  const files = await fg("**/*", {
3724
- cwd: (await fs.stat(baseFull)).isDirectory() ? baseFull : path.dirname(baseFull),
4992
+ cwd: (await fs.stat(baseFull)).isDirectory() ? baseFull : path$1.dirname(baseFull),
3725
4993
  absolute: true,
3726
4994
  onlyFiles: true,
3727
4995
  dot: true
3728
4996
  });
3729
4997
  for (const fp of files) try {
3730
- if (includeGlob && !micromatch.isMatch(path.basename(fp), includeGlob)) continue;
4998
+ if (!isTextMimeType(getMimeType(fp))) continue;
4999
+ if (includeGlob && !micromatch.isMatch(path$1.basename(fp), includeGlob)) continue;
3731
5000
  if ((await fs.stat(fp)).size > this.maxFileSizeBytes) continue;
3732
5001
  const lines = (await fs.readFile(fp, "utf-8")).split("\n");
3733
5002
  for (let i = 0; i < lines.length; i++) {
@@ -3735,9 +5004,9 @@ var FilesystemBackend = class {
3735
5004
  if (line.includes(pattern)) {
3736
5005
  let virtPath;
3737
5006
  if (this.virtualMode) try {
3738
- const relative = path.relative(this.cwd, fp);
5007
+ const relative = path$1.relative(this.cwd, fp);
3739
5008
  if (relative.startsWith("..")) continue;
3740
- virtPath = "/" + relative.split(path.sep).join("/");
5009
+ virtPath = "/" + relative.split(path$1.sep).join("/");
3741
5010
  } catch {
3742
5011
  continue;
3743
5012
  }
@@ -3754,13 +5023,13 @@ var FilesystemBackend = class {
3754
5023
  /**
3755
5024
  * Structured glob matching returning FileInfo objects.
3756
5025
  */
3757
- async globInfo(pattern, searchPath = "/") {
5026
+ async glob(pattern, searchPath = "/") {
3758
5027
  if (pattern.startsWith("/")) pattern = pattern.substring(1);
3759
5028
  const resolvedSearchPath = searchPath === "/" ? this.cwd : this.resolvePath(searchPath);
3760
5029
  try {
3761
- if (!(await fs.stat(resolvedSearchPath)).isDirectory()) return [];
5030
+ if (!(await fs.stat(resolvedSearchPath)).isDirectory()) return { files: [] };
3762
5031
  } catch {
3763
- return [];
5032
+ return { files: [] };
3764
5033
  }
3765
5034
  const results = [];
3766
5035
  try {
@@ -3773,7 +5042,7 @@ var FilesystemBackend = class {
3773
5042
  for (const matchedPath of matches) try {
3774
5043
  const stat = await fs.stat(matchedPath);
3775
5044
  if (!stat.isFile()) continue;
3776
- const normalizedPath = matchedPath.split("/").join(path.sep);
5045
+ const normalizedPath = matchedPath.split("/").join(path$1.sep);
3777
5046
  if (!this.virtualMode) results.push({
3778
5047
  path: normalizedPath,
3779
5048
  is_dir: false,
@@ -3781,12 +5050,12 @@ var FilesystemBackend = class {
3781
5050
  modified_at: stat.mtime.toISOString()
3782
5051
  });
3783
5052
  else {
3784
- const cwdStr = this.cwd.endsWith(path.sep) ? this.cwd : this.cwd + path.sep;
5053
+ const cwdStr = this.cwd.endsWith(path$1.sep) ? this.cwd : this.cwd + path$1.sep;
3785
5054
  let relativePath;
3786
5055
  if (normalizedPath.startsWith(cwdStr)) relativePath = normalizedPath.substring(cwdStr.length);
3787
5056
  else if (normalizedPath.startsWith(this.cwd)) relativePath = normalizedPath.substring(this.cwd.length).replace(/^[/\\]/, "");
3788
5057
  else relativePath = normalizedPath;
3789
- relativePath = relativePath.split(path.sep).join("/");
5058
+ relativePath = relativePath.split(path$1.sep).join("/");
3790
5059
  const virt = "/" + relativePath;
3791
5060
  results.push({
3792
5061
  path: virt,
@@ -3800,7 +5069,7 @@ var FilesystemBackend = class {
3800
5069
  }
3801
5070
  } catch {}
3802
5071
  results.sort((a, b) => a.path.localeCompare(b.path));
3803
- return results;
5072
+ return { files: results };
3804
5073
  }
3805
5074
  /**
3806
5075
  * Upload multiple files to the filesystem.
@@ -3812,7 +5081,7 @@ var FilesystemBackend = class {
3812
5081
  const responses = [];
3813
5082
  for (const [filePath, content] of files) try {
3814
5083
  const resolvedPath = this.resolvePath(filePath);
3815
- await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
5084
+ await fs.mkdir(path$1.dirname(resolvedPath), { recursive: true });
3816
5085
  await fs.writeFile(resolvedPath, content);
3817
5086
  responses.push({
3818
5087
  path: filePath,
@@ -3895,9 +5164,9 @@ var CompositeBackend = class {
3895
5164
  routes;
3896
5165
  sortedRoutes;
3897
5166
  constructor(defaultBackend, routes) {
3898
- this.default = defaultBackend;
3899
- this.routes = routes;
3900
- this.sortedRoutes = Object.entries(routes).sort((a, b) => b[0].length - a[0].length);
5167
+ this.default = isSandboxProtocol(defaultBackend) ? adaptSandboxProtocol(defaultBackend) : adaptBackendProtocol(defaultBackend);
5168
+ this.routes = Object.fromEntries(Object.entries(routes).map(([k, v]) => [k, isSandboxProtocol(v) ? adaptSandboxProtocol(v) : adaptBackendProtocol(v)]));
5169
+ this.sortedRoutes = Object.entries(this.routes).sort((a, b) => b[0].length - a[0].length);
3901
5170
  }
3902
5171
  /** Delegates to default backend's id if it is a sandbox, otherwise empty string. */
3903
5172
  get id() {
@@ -3921,25 +5190,27 @@ var CompositeBackend = class {
3921
5190
  * List files and directories in the specified directory (non-recursive).
3922
5191
  *
3923
5192
  * @param path - Absolute path to directory
3924
- * @returns List of FileInfo objects with route prefixes added, for files and directories
3925
- * directly in the directory. Directories have a trailing / in their path and is_dir=true.
5193
+ * @returns LsResult with list of FileInfo objects (with route prefixes added) on success or error on failure.
5194
+ * Directories have a trailing / in their path and is_dir=true.
3926
5195
  */
3927
- async lsInfo(path) {
5196
+ async ls(path) {
3928
5197
  for (const [routePrefix, backend] of this.sortedRoutes) if (path.startsWith(routePrefix.replace(/\/$/, ""))) {
3929
5198
  const suffix = path.substring(routePrefix.length);
3930
5199
  const searchPath = suffix ? "/" + suffix : "/";
3931
- const infos = await backend.lsInfo(searchPath);
5200
+ const result = await backend.ls(searchPath);
5201
+ if (result.error) return result;
3932
5202
  const prefixed = [];
3933
- for (const fi of infos) prefixed.push({
5203
+ for (const fi of result.files || []) prefixed.push({
3934
5204
  ...fi,
3935
5205
  path: routePrefix.slice(0, -1) + fi.path
3936
5206
  });
3937
- return prefixed;
5207
+ return { files: prefixed };
3938
5208
  }
3939
5209
  if (path === "/") {
3940
5210
  const results = [];
3941
- const defaultInfos = await this.default.lsInfo(path);
3942
- results.push(...defaultInfos);
5211
+ const defaultResult = await this.default.ls(path);
5212
+ if (defaultResult.error) return defaultResult;
5213
+ results.push(...defaultResult.files || []);
3943
5214
  for (const [routePrefix] of this.sortedRoutes) results.push({
3944
5215
  path: routePrefix,
3945
5216
  is_dir: true,
@@ -3947,9 +5218,9 @@ var CompositeBackend = class {
3947
5218
  modified_at: ""
3948
5219
  });
3949
5220
  results.sort((a, b) => a.path.localeCompare(b.path));
3950
- return results;
5221
+ return { files: results };
3951
5222
  }
3952
- return await this.default.lsInfo(path);
5223
+ return await this.default.ls(path);
3953
5224
  }
3954
5225
  /**
3955
5226
  * Read file content, routing to appropriate backend.
@@ -3967,7 +5238,7 @@ var CompositeBackend = class {
3967
5238
  * Read file content as raw FileData.
3968
5239
  *
3969
5240
  * @param filePath - Absolute file path
3970
- * @returns Raw file content as FileData
5241
+ * @returns ReadRawResult with raw file data on success or error on failure
3971
5242
  */
3972
5243
  async readRaw(filePath) {
3973
5244
  const [backend, strippedKey] = this.getBackendAndKey(filePath);
@@ -3976,53 +5247,59 @@ var CompositeBackend = class {
3976
5247
  /**
3977
5248
  * Structured search results or error string for invalid input.
3978
5249
  */
3979
- async grepRaw(pattern, path = "/", glob = null) {
5250
+ async grep(pattern, path = "/", glob = null) {
3980
5251
  for (const [routePrefix, backend] of this.sortedRoutes) if (path.startsWith(routePrefix.replace(/\/$/, ""))) {
3981
5252
  const searchPath = path.substring(routePrefix.length - 1);
3982
- const raw = await backend.grepRaw(pattern, searchPath || "/", glob);
3983
- if (typeof raw === "string") return raw;
3984
- return raw.map((m) => ({
5253
+ const raw = await backend.grep(pattern, searchPath || "/", glob);
5254
+ if (raw.error) return raw;
5255
+ return { matches: (raw.matches || []).map((m) => ({
3985
5256
  ...m,
3986
5257
  path: routePrefix.slice(0, -1) + m.path
3987
- }));
5258
+ })) };
3988
5259
  }
3989
5260
  const allMatches = [];
3990
- const rawDefault = await this.default.grepRaw(pattern, path, glob);
3991
- if (typeof rawDefault === "string") return rawDefault;
3992
- allMatches.push(...rawDefault);
5261
+ const rawDefault = await this.default.grep(pattern, path, glob);
5262
+ if (rawDefault.error) return rawDefault;
5263
+ allMatches.push(...rawDefault.matches || []);
3993
5264
  for (const [routePrefix, backend] of Object.entries(this.routes)) {
3994
- const raw = await backend.grepRaw(pattern, "/", glob);
3995
- if (typeof raw === "string") return raw;
3996
- allMatches.push(...raw.map((m) => ({
5265
+ const raw = await backend.grep(pattern, "/", glob);
5266
+ if (raw.error) return raw;
5267
+ const matches = (raw.matches || []).map((m) => ({
3997
5268
  ...m,
3998
5269
  path: routePrefix.slice(0, -1) + m.path
3999
- })));
5270
+ }));
5271
+ allMatches.push(...matches);
4000
5272
  }
4001
- return allMatches;
5273
+ return { matches: allMatches };
4002
5274
  }
4003
5275
  /**
4004
5276
  * Structured glob matching returning FileInfo objects.
4005
5277
  */
4006
- async globInfo(pattern, path = "/") {
5278
+ async glob(pattern, path = "/") {
4007
5279
  const results = [];
4008
5280
  for (const [routePrefix, backend] of this.sortedRoutes) if (path.startsWith(routePrefix.replace(/\/$/, ""))) {
4009
5281
  const searchPath = path.substring(routePrefix.length - 1);
4010
- return (await backend.globInfo(pattern, searchPath || "/")).map((fi) => ({
5282
+ const result = await backend.glob(pattern, searchPath || "/");
5283
+ if (result.error) return result;
5284
+ return { files: (result.files || []).map((fi) => ({
4011
5285
  ...fi,
4012
5286
  path: routePrefix.slice(0, -1) + fi.path
4013
- }));
5287
+ })) };
4014
5288
  }
4015
- const defaultInfos = await this.default.globInfo(pattern, path);
4016
- results.push(...defaultInfos);
5289
+ const defaultResult = await this.default.glob(pattern, path);
5290
+ if (defaultResult.error) return defaultResult;
5291
+ results.push(...defaultResult.files || []);
4017
5292
  for (const [routePrefix, backend] of Object.entries(this.routes)) {
4018
- const infos = await backend.globInfo(pattern, "/");
4019
- results.push(...infos.map((fi) => ({
5293
+ const result = await backend.glob(pattern, "/");
5294
+ if (result.error) continue;
5295
+ const files = (result.files || []).map((fi) => ({
4020
5296
  ...fi,
4021
5297
  path: routePrefix.slice(0, -1) + fi.path
4022
- })));
5298
+ }));
5299
+ results.push(...files);
4023
5300
  }
4024
5301
  results.sort((a, b) => a.path.localeCompare(b.path));
4025
- return results;
5302
+ return { files: results };
4026
5303
  }
4027
5304
  /**
4028
5305
  * Create a new file, routing to appropriate backend.
@@ -4250,7 +5527,7 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
4250
5527
  */
4251
5528
  async read(filePath, offset = 0, limit = 500) {
4252
5529
  const result = await super.read(filePath, offset, limit);
4253
- if (typeof result === "string" && result.startsWith("Error reading file") && result.includes("ENOENT")) return `Error: File '${filePath}' not found`;
5530
+ if (result.error?.includes("ENOENT")) return { error: `File '${filePath}' not found` };
4254
5531
  return result;
4255
5532
  }
4256
5533
  /**
@@ -4267,25 +5544,26 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
4267
5544
  /**
4268
5545
  * List directory contents, returning paths relative to rootDir.
4269
5546
  */
4270
- async lsInfo(dirPath) {
4271
- const results = await super.lsInfo(dirPath);
4272
- if (this.virtualMode) return results;
4273
- const cwdPrefix = this.cwd.endsWith(path.sep) ? this.cwd : this.cwd + path.sep;
4274
- return results.map((info) => ({
5547
+ async ls(dirPath) {
5548
+ const result = await super.ls(dirPath);
5549
+ if (result.error) return result;
5550
+ if (this.virtualMode) return result;
5551
+ const cwdPrefix = this.cwd.endsWith(path$1.sep) ? this.cwd : this.cwd + path$1.sep;
5552
+ return { files: (result.files || []).map((info) => ({
4275
5553
  ...info,
4276
5554
  path: info.path.startsWith(cwdPrefix) ? info.path.slice(cwdPrefix.length) : info.path
4277
- }));
5555
+ })) };
4278
5556
  }
4279
5557
  /**
4280
5558
  * Glob matching that returns relative paths and includes directories.
4281
5559
  */
4282
- async globInfo(pattern, searchPath = "/") {
5560
+ async glob(pattern, searchPath = "/") {
4283
5561
  if (pattern.startsWith("/")) pattern = pattern.substring(1);
4284
- const resolvedSearchPath = searchPath === "/" || searchPath === "" ? this.cwd : this.virtualMode ? path.resolve(this.cwd, searchPath.replace(/^\//, "")) : path.resolve(this.cwd, searchPath);
5562
+ const resolvedSearchPath = searchPath === "/" || searchPath === "" ? this.cwd : this.virtualMode ? path$1.resolve(this.cwd, searchPath.replace(/^\//, "")) : path$1.resolve(this.cwd, searchPath);
4285
5563
  try {
4286
- if (!(await fs.stat(resolvedSearchPath)).isDirectory()) return [];
5564
+ if (!(await fs.stat(resolvedSearchPath)).isDirectory()) return { files: [] };
4287
5565
  } catch {
4288
- return [];
5566
+ return { files: [] };
4289
5567
  }
4290
5568
  const formatPath = (rel) => this.virtualMode ? `/${rel}` : rel;
4291
5569
  const globOpts = {
@@ -4302,7 +5580,7 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
4302
5580
  })]);
4303
5581
  const statFile = async (match) => {
4304
5582
  try {
4305
- const entryStat = await fs.stat(path.join(resolvedSearchPath, match));
5583
+ const entryStat = await fs.stat(path$1.join(resolvedSearchPath, match));
4306
5584
  if (entryStat.isFile()) return {
4307
5585
  path: formatPath(match),
4308
5586
  is_dir: false,
@@ -4314,7 +5592,7 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
4314
5592
  };
4315
5593
  const statDir = async (match) => {
4316
5594
  try {
4317
- const entryStat = await fs.stat(path.join(resolvedSearchPath, match));
5595
+ const entryStat = await fs.stat(path$1.join(resolvedSearchPath, match));
4318
5596
  if (entryStat.isDirectory()) return {
4319
5597
  path: formatPath(match),
4320
5598
  is_dir: true,
@@ -4327,7 +5605,7 @@ var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
4327
5605
  const [fileInfos, dirInfos] = await Promise.all([Promise.all(fileMatches.map(statFile)), Promise.all(dirMatches.map(statDir))]);
4328
5606
  const results = [...fileInfos, ...dirInfos].filter((info) => info !== null);
4329
5607
  results.sort((a, b) => a.path.localeCompare(b.path));
4330
- return results;
5608
+ return { files: results };
4331
5609
  }
4332
5610
  /**
4333
5611
  * Execute a shell command directly on the host system.
@@ -4602,9 +5880,9 @@ var BaseSandbox = class {
4602
5880
  * including Alpine. No Python or Node.js needed.
4603
5881
  *
4604
5882
  * @param path - Absolute path to directory
4605
- * @returns List of FileInfo objects for files and directories directly in the directory.
5883
+ * @returns LsResult with list of FileInfo objects on success or error on failure.
4606
5884
  */
4607
- async lsInfo(path) {
5885
+ async ls(path) {
4608
5886
  const command = buildLsCommand(path);
4609
5887
  const result = await this.execute(command);
4610
5888
  const infos = [];
@@ -4619,7 +5897,7 @@ var BaseSandbox = class {
4619
5897
  modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
4620
5898
  });
4621
5899
  }
4622
- return infos;
5900
+ return { files: infos };
4623
5901
  }
4624
5902
  /**
4625
5903
  * Read file content with line numbers.
@@ -4634,11 +5912,26 @@ var BaseSandbox = class {
4634
5912
  * @returns Formatted file content with line numbers, or error message
4635
5913
  */
4636
5914
  async read(filePath, offset = 0, limit = 500) {
4637
- if (limit === 0) return "";
5915
+ const mimeType = getMimeType(filePath);
5916
+ if (!isTextMimeType(mimeType)) {
5917
+ const results = await this.downloadFiles([filePath]);
5918
+ if (results[0].error || !results[0].content) return { error: `File '${filePath}' not found` };
5919
+ return {
5920
+ content: results[0].content,
5921
+ mimeType
5922
+ };
5923
+ }
5924
+ if (limit === 0) return {
5925
+ content: "",
5926
+ mimeType
5927
+ };
4638
5928
  const command = buildReadCommand(filePath, offset, limit);
4639
5929
  const result = await this.execute(command);
4640
- if (result.exitCode !== 0) return `Error: File '${filePath}' not found`;
4641
- return result.output;
5930
+ if (result.exitCode !== 0) return { error: `File '${filePath}' not found` };
5931
+ return {
5932
+ content: result.output,
5933
+ mimeType
5934
+ };
4642
5935
  }
4643
5936
  /**
4644
5937
  * Read file content as raw FileData.
@@ -4646,18 +5939,25 @@ var BaseSandbox = class {
4646
5939
  * Uses downloadFiles() directly — no runtime needed on the sandbox host.
4647
5940
  *
4648
5941
  * @param filePath - Absolute file path
4649
- * @returns Raw file content as FileData
5942
+ * @returns ReadRawResult with raw file data on success or error on failure
4650
5943
  */
4651
5944
  async readRaw(filePath) {
4652
5945
  const results = await this.downloadFiles([filePath]);
4653
- if (results[0].error || !results[0].content) throw new Error(`File '${filePath}' not found`);
4654
- const lines = new TextDecoder().decode(results[0].content).split("\n");
5946
+ if (results[0].error || !results[0].content) return { error: `File '${filePath}' not found` };
4655
5947
  const now = (/* @__PURE__ */ new Date()).toISOString();
4656
- return {
4657
- content: lines,
5948
+ const mimeType = getMimeType(filePath);
5949
+ if (!isTextMimeType(mimeType)) return { data: {
5950
+ content: results[0].content,
5951
+ mimeType,
4658
5952
  created_at: now,
4659
5953
  modified_at: now
4660
- };
5954
+ } };
5955
+ return { data: {
5956
+ content: new TextDecoder().decode(results[0].content),
5957
+ mimeType,
5958
+ created_at: now,
5959
+ modified_at: now
5960
+ } };
4661
5961
  }
4662
5962
  /**
4663
5963
  * Search for a literal text pattern in files using grep.
@@ -4667,23 +5967,25 @@ var BaseSandbox = class {
4667
5967
  * @param glob - Optional glob pattern to filter which files to search.
4668
5968
  * @returns List of GrepMatch dicts containing path, line number, and matched text.
4669
5969
  */
4670
- async grepRaw(pattern, path = "/", glob = null) {
5970
+ async grep(pattern, path = "/", glob = null) {
4671
5971
  const command = buildGrepCommand(pattern, path, glob);
4672
5972
  const output = (await this.execute(command)).output.trim();
4673
- if (!output) return [];
5973
+ if (!output) return { matches: [] };
4674
5974
  const matches = [];
4675
5975
  for (const line of output.split("\n")) {
4676
5976
  const parts = line.split(":");
4677
5977
  if (parts.length >= 3) {
5978
+ const filePath = parts[0];
5979
+ if (!isTextMimeType(getMimeType(filePath))) continue;
4678
5980
  const lineNum = parseInt(parts[1], 10);
4679
5981
  if (!isNaN(lineNum)) matches.push({
4680
- path: parts[0],
5982
+ path: filePath,
4681
5983
  line: lineNum,
4682
5984
  text: parts.slice(2).join(":")
4683
5985
  });
4684
5986
  }
4685
5987
  }
4686
- return matches;
5988
+ return { matches };
4687
5989
  }
4688
5990
  /**
4689
5991
  * Structured glob matching returning FileInfo objects.
@@ -4698,7 +6000,7 @@ var BaseSandbox = class {
4698
6000
  * - `?` matches a single character except `/`
4699
6001
  * - `[...]` character classes
4700
6002
  */
4701
- async globInfo(pattern, path = "/") {
6003
+ async glob(pattern, path = "/") {
4702
6004
  const command = buildFindCommand(path);
4703
6005
  const result = await this.execute(command);
4704
6006
  const regex = globToPathRegex(pattern);
@@ -4716,7 +6018,7 @@ var BaseSandbox = class {
4716
6018
  modified_at: (/* @__PURE__ */ new Date(parsed.mtime * 1e3)).toISOString()
4717
6019
  });
4718
6020
  }
4719
- return infos;
6021
+ return { files: infos };
4720
6022
  }
4721
6023
  /**
4722
6024
  * Create a new file with content.
@@ -4729,8 +6031,11 @@ var BaseSandbox = class {
4729
6031
  const existCheck = await this.downloadFiles([filePath]);
4730
6032
  if (existCheck[0].content !== null && existCheck[0].error === null) return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
4731
6033
  } catch {}
4732
- const encoder = new TextEncoder();
4733
- const results = await this.uploadFiles([[filePath, encoder.encode(content)]]);
6034
+ const mimeType = getMimeType(filePath);
6035
+ let fileContent;
6036
+ if (isTextMimeType(mimeType)) fileContent = new TextEncoder().encode(content);
6037
+ else fileContent = Buffer.from(content, "base64");
6038
+ const results = await this.uploadFiles([[filePath, fileContent]]);
4734
6039
  if (results[0].error) return { error: `Failed to write to ${filePath}: ${results[0].error}` };
4735
6040
  return {
4736
6041
  path: filePath,
@@ -5065,9 +6370,44 @@ function createCacheBreakpointMiddleware() {
5065
6370
  }
5066
6371
  //#endregion
5067
6372
  //#region src/agent.ts
5068
- const BASE_PROMPT = `In order to complete the objective that the user asks of you, you have access to a number of standard tools.`;
6373
+ const BASE_AGENT_PROMPT = context`
6374
+ You are a Deep Agent, an AI assistant that helps users accomplish tasks using tools. You respond with text and tool calls. The user can see your responses and tool outputs in real time.
6375
+
6376
+ ## Core Behavior
6377
+
6378
+ - Be concise and direct. Don't over-explain unless asked.
6379
+ - NEVER add unnecessary preamble (\"Sure!\", \"Great question!\", \"I'll now...\").
6380
+ - Don't say \"I'll now do X\" — just do it.
6381
+ - If the request is ambiguous, ask questions before acting.
6382
+ - If asked how to approach something, explain first, then act.
6383
+
6384
+ ## Professional Objectivity
6385
+
6386
+ - Prioritize accuracy over validating the user's beliefs
6387
+ - Disagree respectfully when the user is incorrect
6388
+ - Avoid unnecessary superlatives, praise, or emotional validation
6389
+
6390
+ ## Doing Tasks
6391
+
6392
+ When the user asks you to do something:
6393
+
6394
+ 1. **Understand first** — read relevant files, check existing patterns. Quick but thorough — gather enough evidence to start, then iterate.
6395
+ 2. **Act** — implement the solution. Work quickly but accurately.
6396
+ 3. **Verify** — check your work against what was asked, not against your own output. Your first attempt is rarely correct — iterate.
6397
+
6398
+ Keep working until the task is fully complete. Don't stop partway and explain what you would do — just do it. Only yield back to the user when the task is done or you're genuinely blocked.
6399
+
6400
+ **When things go wrong:**
6401
+ - If something fails repeatedly, stop and analyze *why* — don't keep retrying the same approach.
6402
+ - If you're blocked, tell the user what's wrong and ask for guidance.
6403
+
6404
+ ## Progress Updates
6405
+
6406
+ For longer tasks, provide brief progress updates at reasonable intervals — a concise sentence recapping what you've done and what's next.
6407
+ `;
5069
6408
  const BUILTIN_TOOL_NAMES = new Set([
5070
6409
  ...FILESYSTEM_TOOL_NAMES,
6410
+ ...ASYNC_TASK_TOOL_NAMES,
5071
6411
  "task",
5072
6412
  "write_todos"
5073
6413
  ]);
@@ -5084,19 +6424,18 @@ function isAnthropicModel(model) {
5084
6424
  return model.getName() === "ChatAnthropic";
5085
6425
  }
5086
6426
  /**
5087
- * Create a Deep Agent with middleware-based architecture.
6427
+ * Create a Deep Agent.
5088
6428
  *
5089
- * Matches Python's create_deep_agent function, using middleware for all features:
5090
- * - Todo management (todoListMiddleware)
5091
- * - Filesystem tools (createFilesystemMiddleware)
5092
- * - Subagent delegation (createSubAgentMiddleware)
5093
- * - Conversation summarization (createSummarizationMiddleware) with backend offloading
5094
- * - Prompt caching (anthropicPromptCachingMiddleware)
5095
- * - Tool call patching (createPatchToolCallsMiddleware)
5096
- * - Human-in-the-loop (humanInTheLoopMiddleware) - optional
6429
+ * This is the main entry point for building a production-style agent with
6430
+ * deepagents. It gives you a strong default runtime (filesystem, tasks,
6431
+ * subagents, summarization) and lets you opt into skills, memory,
6432
+ * human-in-the-loop interrupts, async subagents, and custom middleware.
6433
+ *
6434
+ * The runtime is intentionally opinionated: defaults work out of the box, and
6435
+ * when you customize behavior, the middleware ordering stays deterministic.
5097
6436
  *
5098
6437
  * @param params Configuration parameters for the agent
5099
- * @returns ReactAgent instance ready for invocation with properly inferred state types
6438
+ * @returns Deep Agent instance with inferred state/response types
5100
6439
  *
5101
6440
  * @example
5102
6441
  * ```typescript
@@ -5115,92 +6454,92 @@ function isAnthropicModel(model) {
5115
6454
  * ```
5116
6455
  */
5117
6456
  function createDeepAgent(params = {}) {
5118
- const { model = "claude-sonnet-4-5-20250929", tools = [], systemPrompt, middleware: customMiddleware = [], subagents = [], responseFormat, contextSchema, checkpointer, store, backend, interruptOn, name, memory, skills } = params;
6457
+ const { model = new ChatAnthropic("claude-sonnet-4-6"), tools = [], systemPrompt, middleware: customMiddleware = [], subagents = [], responseFormat, contextSchema, checkpointer, store, backend = (config) => new StateBackend(config), interruptOn, name, memory, skills } = params;
5119
6458
  const collidingTools = tools.map((t) => t.name).filter((n) => typeof n === "string" && BUILTIN_TOOL_NAMES.has(n));
5120
6459
  if (collidingTools.length > 0) throw new ConfigurationError(`Tool name(s) [${collidingTools.join(", ")}] conflict with built-in tools. Rename your custom tools to avoid this.`, "TOOL_NAME_COLLISION");
5121
6460
  const anthropicModel = isAnthropicModel(model);
5122
- const finalSystemPrompt = new SystemMessage({ content: systemPrompt ? typeof systemPrompt === "string" ? [{
5123
- type: "text",
5124
- text: `${systemPrompt}\n\n${BASE_PROMPT}`
5125
- }] : [{
5126
- type: "text",
5127
- text: BASE_PROMPT
5128
- }, ...typeof systemPrompt.content === "string" ? [{
5129
- type: "text",
5130
- text: systemPrompt.content
5131
- }] : systemPrompt.content] : [{
5132
- type: "text",
5133
- text: BASE_PROMPT
5134
- }] });
5135
- /**
5136
- * Create backend configuration for filesystem middleware
5137
- * If no backend is provided, use a factory that creates a StateBackend
5138
- */
5139
- const filesystemBackend = backend ? backend : (runtime) => new StateBackend(runtime);
5140
- /**
5141
- * Skills middleware (created conditionally for runtime use)
5142
- */
5143
- const skillsMiddlewareArray = skills != null && skills.length > 0 ? [createSkillsMiddleware({
5144
- backend: filesystemBackend,
5145
- sources: skills
5146
- })] : [];
5147
- /**
5148
- * Memory middleware (created conditionally for runtime use)
5149
- */
5150
- const memoryMiddlewareArray = memory != null && memory.length > 0 ? [createMemoryMiddleware({
5151
- backend: filesystemBackend,
5152
- sources: memory,
5153
- addCacheControl: anthropicModel
5154
- })] : [];
6461
+ const cacheMiddleware = anthropicModel ? [anthropicPromptCachingMiddleware({
6462
+ unsupportedModelBehavior: "ignore",
6463
+ minMessagesToCache: 1
6464
+ }), createCacheBreakpointMiddleware()] : [];
5155
6465
  /**
5156
6466
  * Process subagents to add SkillsMiddleware for those with their own skills.
5157
6467
  *
5158
6468
  * Custom subagents do NOT inherit skills from the main agent by default.
5159
- * Only the general-purpose subagent inherits the main agent's skills (via defaultMiddleware).
6469
+ * Only the general-purpose subagent inherits the main agent's skills.
5160
6470
  * If a custom subagent needs skills, it must specify its own `skills` array.
5161
6471
  */
5162
- const processedSubagents = subagents.map((subagent) => {
5163
- /**
5164
- * CompiledSubAgent - use as-is (already has its own middleware baked in)
5165
- */
5166
- if (Runnable.isRunnable(subagent)) return subagent;
5167
- /**
5168
- * SubAgent without skills - use as-is
5169
- */
5170
- if (!("skills" in subagent) || subagent.skills?.length === 0) return subagent;
5171
- /**
5172
- * SubAgent with skills - add SkillsMiddleware BEFORE user's middleware
5173
- * Order: base middleware (via defaultMiddleware) → skills → user's middleware
5174
- * This matches Python's ordering in create_deep_agent
5175
- */
5176
- const subagentSkillsMiddleware = createSkillsMiddleware({
5177
- backend: filesystemBackend,
5178
- sources: subagent.skills ?? []
5179
- });
6472
+ const normalizeSubagentSpec = (input) => {
6473
+ const subagentMiddleware = [
6474
+ todoListMiddleware(),
6475
+ createFilesystemMiddleware({ backend }),
6476
+ createSummarizationMiddleware({
6477
+ backend,
6478
+ model
6479
+ }),
6480
+ createPatchToolCallsMiddleware(),
6481
+ ...input.skills != null && input.skills.length > 0 ? [createSkillsMiddleware({
6482
+ backend,
6483
+ sources: input.skills
6484
+ })] : [],
6485
+ ...input.middleware ?? [],
6486
+ ...cacheMiddleware
6487
+ ];
5180
6488
  return {
5181
- ...subagent,
5182
- middleware: [subagentSkillsMiddleware, ...subagent.middleware || []]
6489
+ ...input,
6490
+ tools: input.tools ?? [],
6491
+ middleware: subagentMiddleware
5183
6492
  };
5184
- });
5185
- /**
5186
- * Middleware for custom subagents (does NOT include skills from main agent).
5187
- * Custom subagents must define their own `skills` property to get skills.
5188
- *
5189
- * Uses createSummarizationMiddleware (deepagents version) with backend support
5190
- * and auto-computed defaults from model profile, matching Python's create_deep_agent.
5191
- * When trigger is not provided, defaults are lazily computed:
5192
- * - With model profile: fraction-based (trigger=0.85, keep=0.10)
5193
- * - Without profile: fixed (trigger=170k tokens, keep=6 messages)
5194
- */
5195
- const subagentMiddleware = [
6493
+ };
6494
+ const allSubagents = subagents;
6495
+ const asyncSubAgents = allSubagents.filter((item) => isAsyncSubAgent(item));
6496
+ const inlineSubagents = allSubagents.filter((item) => !isAsyncSubAgent(item)).map((item) => "runnable" in item ? item : normalizeSubagentSpec(item));
6497
+ if (!inlineSubagents.some((item) => item.name === GENERAL_PURPOSE_SUBAGENT["name"])) {
6498
+ const generalPurposeSpec = normalizeSubagentSpec({
6499
+ ...GENERAL_PURPOSE_SUBAGENT,
6500
+ model,
6501
+ skills,
6502
+ tools
6503
+ });
6504
+ inlineSubagents.unshift(generalPurposeSpec);
6505
+ }
6506
+ const skillsMiddleware = skills != null && skills.length > 0 ? [createSkillsMiddleware({
6507
+ backend,
6508
+ sources: skills
6509
+ })] : [];
6510
+ const [todoMiddleware, fsMiddleware, subagentMiddleware, summarizationMiddleware, patchToolCallsMiddleware] = [
5196
6511
  todoListMiddleware(),
5197
- createFilesystemMiddleware({ backend: filesystemBackend }),
6512
+ createFilesystemMiddleware({ backend }),
6513
+ createSubAgentMiddleware({
6514
+ defaultModel: model,
6515
+ defaultTools: tools,
6516
+ defaultInterruptOn: interruptOn,
6517
+ subagents: inlineSubagents,
6518
+ generalPurposeAgent: false
6519
+ }),
5198
6520
  createSummarizationMiddleware({
5199
6521
  model,
5200
- backend: filesystemBackend
6522
+ backend
5201
6523
  }),
5202
6524
  createPatchToolCallsMiddleware()
5203
6525
  ];
6526
+ const middleware = [
6527
+ todoMiddleware,
6528
+ ...skillsMiddleware,
6529
+ fsMiddleware,
6530
+ subagentMiddleware,
6531
+ summarizationMiddleware,
6532
+ patchToolCallsMiddleware,
6533
+ ...asyncSubAgents.length > 0 ? [createAsyncSubAgentMiddleware({ asyncSubAgents })] : [],
6534
+ ...customMiddleware,
6535
+ ...cacheMiddleware,
6536
+ ...memory && memory.length > 0 ? [createMemoryMiddleware({
6537
+ backend,
6538
+ sources: memory,
6539
+ addCacheControl: anthropicModel
6540
+ })] : [],
6541
+ ...interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []
6542
+ ];
5204
6543
  /**
5205
6544
  * Return as DeepAgent with proper DeepAgentTypeConfig
5206
6545
  * - Response: InferStructuredResponse<TResponse> (unwraps ToolStrategy<T>/ProviderStrategy<T> → T)
@@ -5212,54 +6551,32 @@ function createDeepAgent(params = {}) {
5212
6551
  */
5213
6552
  return createAgent({
5214
6553
  model,
5215
- systemPrompt: finalSystemPrompt,
6554
+ systemPrompt: typeof systemPrompt === "string" ? new SystemMessage({ contentBlocks: [{
6555
+ type: "text",
6556
+ text: systemPrompt
6557
+ }, {
6558
+ type: "text",
6559
+ text: BASE_AGENT_PROMPT
6560
+ }] }) : SystemMessage.isInstance(systemPrompt) ? new SystemMessage({ contentBlocks: [...systemPrompt.contentBlocks, {
6561
+ type: "text",
6562
+ text: BASE_AGENT_PROMPT
6563
+ }] }) : new SystemMessage({ contentBlocks: [{
6564
+ type: "text",
6565
+ text: BASE_AGENT_PROMPT
6566
+ }] }),
5216
6567
  tools,
5217
- middleware: [
5218
- ...[
5219
- todoListMiddleware(),
5220
- createFilesystemMiddleware({ backend: filesystemBackend }),
5221
- createSubAgentMiddleware({
5222
- defaultModel: model,
5223
- defaultTools: tools,
5224
- defaultMiddleware: [...subagentMiddleware, ...anthropicModel ? [anthropicPromptCachingMiddleware({
5225
- unsupportedModelBehavior: "ignore",
5226
- minMessagesToCache: 1
5227
- }), createCacheBreakpointMiddleware()] : []],
5228
- generalPurposeMiddleware: [
5229
- ...subagentMiddleware,
5230
- ...skillsMiddlewareArray,
5231
- ...anthropicModel ? [anthropicPromptCachingMiddleware({
5232
- unsupportedModelBehavior: "ignore",
5233
- minMessagesToCache: 1
5234
- }), createCacheBreakpointMiddleware()] : []
5235
- ],
5236
- defaultInterruptOn: interruptOn,
5237
- subagents: processedSubagents,
5238
- generalPurposeAgent: true
5239
- }),
5240
- createSummarizationMiddleware({
5241
- model,
5242
- backend: filesystemBackend
5243
- }),
5244
- createPatchToolCallsMiddleware()
5245
- ],
5246
- ...skillsMiddlewareArray,
5247
- ...customMiddleware,
5248
- ...anthropicModel ? [anthropicPromptCachingMiddleware({
5249
- unsupportedModelBehavior: "ignore",
5250
- minMessagesToCache: 1
5251
- }), createCacheBreakpointMiddleware()] : [],
5252
- ...memoryMiddlewareArray,
5253
- ...interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : []
5254
- ],
5255
- ...responseFormat != null && { responseFormat },
6568
+ middleware,
6569
+ ...responseFormat !== null && { responseFormat },
5256
6570
  contextSchema,
5257
6571
  checkpointer,
5258
6572
  store,
5259
6573
  name
5260
6574
  }).withConfig({
5261
6575
  recursionLimit: 1e4,
5262
- metadata: { ls_integration: "deepagents" }
6576
+ metadata: {
6577
+ ls_integration: "deepagents",
6578
+ lc_agent_name: name
6579
+ }
5263
6580
  });
5264
6581
  }
5265
6582
  //#endregion
@@ -5280,13 +6597,13 @@ function createDeepAgent(params = {}) {
5280
6597
  * @returns Path to the project root if found, null otherwise.
5281
6598
  */
5282
6599
  function findProjectRoot(startPath) {
5283
- let current = path.resolve(startPath || process.cwd());
5284
- while (current !== path.dirname(current)) {
5285
- const gitDir = path.join(current, ".git");
6600
+ let current = path$1.resolve(startPath || process.cwd());
6601
+ while (current !== path$1.dirname(current)) {
6602
+ const gitDir = path$1.join(current, ".git");
5286
6603
  if (fs$1.existsSync(gitDir)) return current;
5287
- current = path.dirname(current);
6604
+ current = path$1.dirname(current);
5288
6605
  }
5289
- const rootGitDir = path.join(current, ".git");
6606
+ const rootGitDir = path$1.join(current, ".git");
5290
6607
  if (fs$1.existsSync(rootGitDir)) return current;
5291
6608
  return null;
5292
6609
  }
@@ -5308,14 +6625,14 @@ function isValidAgentName(agentName) {
5308
6625
  */
5309
6626
  function createSettings(options = {}) {
5310
6627
  const projectRoot = findProjectRoot(options.startPath);
5311
- const userDeepagentsDir = path.join(os.homedir(), ".deepagents");
6628
+ const userDeepagentsDir = path$1.join(os.homedir(), ".deepagents");
5312
6629
  return {
5313
6630
  projectRoot,
5314
6631
  userDeepagentsDir,
5315
6632
  hasProject: projectRoot !== null,
5316
6633
  getAgentDir(agentName) {
5317
6634
  if (!isValidAgentName(agentName)) throw new Error(`Invalid agent name: ${JSON.stringify(agentName)}. Agent names can only contain letters, numbers, hyphens, underscores, and spaces.`);
5318
- return path.join(userDeepagentsDir, agentName);
6635
+ return path$1.join(userDeepagentsDir, agentName);
5319
6636
  },
5320
6637
  ensureAgentDir(agentName) {
5321
6638
  const agentDir = this.getAgentDir(agentName);
@@ -5323,14 +6640,14 @@ function createSettings(options = {}) {
5323
6640
  return agentDir;
5324
6641
  },
5325
6642
  getUserAgentMdPath(agentName) {
5326
- return path.join(this.getAgentDir(agentName), "agent.md");
6643
+ return path$1.join(this.getAgentDir(agentName), "agent.md");
5327
6644
  },
5328
6645
  getProjectAgentMdPath() {
5329
6646
  if (!projectRoot) return null;
5330
- return path.join(projectRoot, ".deepagents", "agent.md");
6647
+ return path$1.join(projectRoot, ".deepagents", "agent.md");
5331
6648
  },
5332
6649
  getUserSkillsDir(agentName) {
5333
- return path.join(this.getAgentDir(agentName), "skills");
6650
+ return path$1.join(this.getAgentDir(agentName), "skills");
5334
6651
  },
5335
6652
  ensureUserSkillsDir(agentName) {
5336
6653
  const skillsDir = this.getUserSkillsDir(agentName);
@@ -5339,7 +6656,7 @@ function createSettings(options = {}) {
5339
6656
  },
5340
6657
  getProjectSkillsDir() {
5341
6658
  if (!projectRoot) return null;
5342
- return path.join(projectRoot, ".deepagents", "skills");
6659
+ return path$1.join(projectRoot, ".deepagents", "skills");
5343
6660
  },
5344
6661
  ensureProjectSkillsDir() {
5345
6662
  const skillsDir = this.getProjectSkillsDir();
@@ -5349,7 +6666,7 @@ function createSettings(options = {}) {
5349
6666
  },
5350
6667
  ensureProjectDeepagentsDir() {
5351
6668
  if (!projectRoot) return null;
5352
- const deepagentsDir = path.join(projectRoot, ".deepagents");
6669
+ const deepagentsDir = path$1.join(projectRoot, ".deepagents");
5353
6670
  fs$1.mkdirSync(deepagentsDir, { recursive: true });
5354
6671
  return deepagentsDir;
5355
6672
  }
@@ -5601,7 +6918,7 @@ function isSafePath(targetPath, baseDir) {
5601
6918
  try {
5602
6919
  const resolvedPath = fs$1.realpathSync(targetPath);
5603
6920
  const resolvedBase = fs$1.realpathSync(baseDir);
5604
- return resolvedPath.startsWith(resolvedBase + path.sep) || resolvedPath === resolvedBase;
6921
+ return resolvedPath.startsWith(resolvedBase + path$1.sep) || resolvedPath === resolvedBase;
5605
6922
  } catch {
5606
6923
  return false;
5607
6924
  }
@@ -5680,7 +6997,7 @@ function parseSkillMetadata(skillMdPath, source) {
5680
6997
  console.warn(`Skipping ${skillMdPath}: missing required 'name' or 'description'`);
5681
6998
  return null;
5682
6999
  }
5683
- const directoryName = path.basename(path.dirname(skillMdPath));
7000
+ const directoryName = path$1.basename(path$1.dirname(skillMdPath));
5684
7001
  const validation = validateSkillName(String(name), directoryName);
5685
7002
  if (!validation.valid) console.warn(`Skill '${name}' in ${skillMdPath} does not follow Agent Skills spec: ${validation.error}. Consider renaming to be spec-compliant.`);
5686
7003
  let descriptionStr = String(description);
@@ -5723,7 +7040,7 @@ function parseSkillMetadata(skillMdPath, source) {
5723
7040
  * @returns List of skill metadata
5724
7041
  */
5725
7042
  function listSkillsFromDir(skillsDir, source) {
5726
- const expandedDir = skillsDir.startsWith("~") ? path.join(process.env.HOME || process.env.USERPROFILE || "", skillsDir.slice(1)) : skillsDir;
7043
+ const expandedDir = skillsDir.startsWith("~") ? path$1.join(process.env.HOME || process.env.USERPROFILE || "", skillsDir.slice(1)) : skillsDir;
5727
7044
  if (!fs$1.existsSync(expandedDir)) return [];
5728
7045
  let resolvedBase;
5729
7046
  try {
@@ -5739,10 +7056,10 @@ function listSkillsFromDir(skillsDir, source) {
5739
7056
  return [];
5740
7057
  }
5741
7058
  for (const entry of entries) {
5742
- const skillDir = path.join(resolvedBase, entry.name);
7059
+ const skillDir = path$1.join(resolvedBase, entry.name);
5743
7060
  if (!isSafePath(skillDir, resolvedBase)) continue;
5744
7061
  if (!entry.isDirectory()) continue;
5745
- const skillMdPath = path.join(skillDir, "SKILL.md");
7062
+ const skillMdPath = path$1.join(skillDir, "SKILL.md");
5746
7063
  if (!fs$1.existsSync(skillMdPath)) continue;
5747
7064
  if (!isSafePath(skillMdPath, resolvedBase)) continue;
5748
7065
  const metadata = parseSkillMetadata(skillMdPath, source);
@@ -5773,6 +7090,6 @@ function listSkills(options) {
5773
7090
  return Array.from(allSkills.values());
5774
7091
  }
5775
7092
  //#endregion
5776
- export { BaseSandbox, CompositeBackend, ConfigurationError, DEFAULT_GENERAL_PURPOSE_DESCRIPTION, DEFAULT_SUBAGENT_PROMPT, FilesystemBackend, GENERAL_PURPOSE_SUBAGENT, LangSmithSandbox, LocalShellBackend, MAX_SKILL_DESCRIPTION_LENGTH, MAX_SKILL_FILE_SIZE, MAX_SKILL_NAME_LENGTH, SandboxError, StateBackend, StoreBackend, TASK_SYSTEM_PROMPT, computeSummarizationDefaults, createAgentMemoryMiddleware, createDeepAgent, createFilesystemMiddleware, createMemoryMiddleware, createPatchToolCallsMiddleware, createSettings, createSkillsMiddleware, createSubAgentMiddleware, createSummarizationMiddleware, filesValue, findProjectRoot, isSandboxBackend, listSkills, parseSkillMetadata, resolveBackend };
7093
+ export { BaseSandbox, CompositeBackend, ConfigurationError, DEFAULT_GENERAL_PURPOSE_DESCRIPTION, DEFAULT_SUBAGENT_PROMPT, FilesystemBackend, GENERAL_PURPOSE_SUBAGENT, LangSmithSandbox, LocalShellBackend, MAX_SKILL_DESCRIPTION_LENGTH, MAX_SKILL_FILE_SIZE, MAX_SKILL_NAME_LENGTH, SandboxError, StateBackend, StoreBackend, TASK_SYSTEM_PROMPT, adaptBackendProtocol, adaptSandboxProtocol, computeSummarizationDefaults, createAgentMemoryMiddleware, createAsyncSubAgentMiddleware, createCompletionCallbackMiddleware, createDeepAgent, createFilesystemMiddleware, createMemoryMiddleware, createPatchToolCallsMiddleware, createSettings, createSkillsMiddleware, createSubAgentMiddleware, createSummarizationMiddleware, filesValue, findProjectRoot, isAsyncSubAgent, isSandboxBackend, isSandboxProtocol, listSkills, parseSkillMetadata, resolveBackend };
5777
7094
 
5778
7095
  //# sourceMappingURL=index.js.map