deepagents 1.13.1 → 1.13.3

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.
@@ -47,6 +47,22 @@ const EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty conten
47
47
  const MAX_LINE_LENGTH = 5e3;
48
48
  const TOOL_RESULT_TOKEN_LIMIT = 2e4;
49
49
  const TRUNCATION_GUIDANCE = "... [results truncated, try being more specific with your parameters]";
50
+ /**
51
+ * Normalize model- or caller-supplied text pagination bounds.
52
+ *
53
+ * Every backend must slice content and calculate pagination metadata from the
54
+ * same normalized values. Otherwise a fractional or negative argument could
55
+ * return one window while advertising a different `nextOffset`.
56
+ *
57
+ * Binary reads do not use this helper because their backend contract ignores
58
+ * line-based offset and limit values.
59
+ */
60
+ function normalizeReadPagination(offset, limit) {
61
+ return {
62
+ offset: Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0,
63
+ limit: Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 0
64
+ };
65
+ }
50
66
  const MIME_TYPES = {
51
67
  ".png": "image/png",
52
68
  ".jpg": "image/jpeg",
@@ -158,40 +174,60 @@ function sanitizeToolCallId(toolCallId) {
158
174
  return toolCallId.replace(/\./g, "_").replace(/\//g, "_").replace(/\\/g, "_");
159
175
  }
160
176
  /**
161
- * Format file content with line numbers (cat -n style).
162
- *
163
- * Chunks lines longer than MAX_LINE_LENGTH with continuation markers (e.g., 5.1, 5.2).
177
+ * Format file content with line numbers and structured source-line boundaries.
164
178
  *
165
- * @param content - File content as string or list of lines
166
- * @param startLine - Starting line number (default: 1)
167
- * @returns Formatted content with line numbers and continuation markers
179
+ * The boundaries let downstream size limiting truncate only after complete
180
+ * source lines without reparsing the rendered gutter. This keeps presentation
181
+ * details (padding, tab separators, and continuation labels) encapsulated in
182
+ * the formatter that creates them.
168
183
  */
169
- function formatContentWithLineNumbers(content, startLine = 1) {
184
+ function formatContentWithLineNumbersAndBoundaries(content, startLine = 1) {
170
185
  let lines;
171
186
  if (typeof content === "string") {
172
187
  lines = content.split("\n");
173
188
  if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
174
189
  } else lines = content;
175
190
  const resultLines = [];
191
+ const sourceLineBoundaries = [];
192
+ let renderedLength = 0;
193
+ const appendRow = (row, sourceLine, completesSourceLine) => {
194
+ if (resultLines.length > 0) renderedLength += 1;
195
+ resultLines.push(row);
196
+ renderedLength += row.length;
197
+ if (completesSourceLine) sourceLineBoundaries.push({
198
+ sourceLine,
199
+ endOffset: renderedLength
200
+ });
201
+ };
176
202
  for (let i = 0; i < lines.length; i++) {
177
203
  const line = lines[i];
178
204
  const lineNum = i + startLine;
179
- if (line.length <= 5e3) resultLines.push(`${lineNum.toString().padStart(6)}\t${line}`);
180
- else {
181
- const numChunks = Math.ceil(line.length / MAX_LINE_LENGTH);
182
- for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
183
- const start = chunkIdx * MAX_LINE_LENGTH;
184
- const end = Math.min(start + MAX_LINE_LENGTH, line.length);
185
- const chunk = line.substring(start, end);
186
- if (chunkIdx === 0) resultLines.push(`${lineNum.toString().padStart(6)}\t${chunk}`);
187
- else {
188
- const continuationMarker = `${lineNum}.${chunkIdx}`;
189
- resultLines.push(`${continuationMarker.padStart(6)}\t${chunk}`);
190
- }
191
- }
205
+ if (line.length <= 5e3) {
206
+ appendRow(`${lineNum.toString().padStart(6)}\t${line}`, lineNum, true);
207
+ continue;
208
+ }
209
+ const numChunks = Math.ceil(line.length / MAX_LINE_LENGTH);
210
+ for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
211
+ const start = chunkIdx * MAX_LINE_LENGTH;
212
+ const end = Math.min(start + MAX_LINE_LENGTH, line.length);
213
+ const chunk = line.substring(start, end);
214
+ appendRow(`${(chunkIdx === 0 ? `${lineNum}` : `${lineNum}.${chunkIdx}`).padStart(6)}\t${chunk}`, lineNum, chunkIdx === numChunks - 1);
192
215
  }
193
216
  }
194
- return resultLines.join("\n");
217
+ return {
218
+ text: resultLines.join("\n"),
219
+ sourceLineBoundaries
220
+ };
221
+ }
222
+ /**
223
+ * Format file content with line numbers (cat -n style).
224
+ *
225
+ * Lines longer than `MAX_LINE_LENGTH` are split into continuation rows such as
226
+ * `5.1` and `5.2`. Use `formatContentWithLineNumbersAndBoundaries` when a
227
+ * caller also needs safe source-line truncation points.
228
+ */
229
+ function formatContentWithLineNumbers(content, startLine = 1) {
230
+ return formatContentWithLineNumbersAndBoundaries(content, startLine).text;
195
231
  }
196
232
  /**
197
233
  * Check if content is empty and return warning message.
@@ -865,10 +901,23 @@ var StateBackend = class {
865
901
  mimeType: fileDataV2.mimeType
866
902
  };
867
903
  if (typeof fileDataV2.content !== "string") return { error: `File '${filePath}' has binary content but text MIME type` };
868
- return {
869
- content: fileDataV2.content.split("\n").slice(offset, offset + limit).join("\n"),
904
+ const { offset: normalizedOffset, limit: normalizedLimit } = normalizeReadPagination(offset, limit);
905
+ const lines = fileDataV2.content.split("\n");
906
+ const totalLines = lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
907
+ const selected = lines.slice(normalizedOffset, normalizedOffset + normalizedLimit);
908
+ if (selected.length === 0 || normalizedOffset >= totalLines || normalizedLimit === 0) return {
909
+ content: selected.join("\n"),
870
910
  mimeType: fileDataV2.mimeType
871
911
  };
912
+ const endOffset = Math.min(normalizedOffset + selected.length, totalLines);
913
+ return {
914
+ content: selected.join("\n"),
915
+ mimeType: fileDataV2.mimeType,
916
+ totalLines,
917
+ startLine: normalizedOffset + 1,
918
+ endLine: endOffset,
919
+ nextOffset: endOffset < totalLines ? endOffset : void 0
920
+ };
872
921
  }
873
922
  /**
874
923
  * Read file content as raw FileData.
@@ -1584,6 +1633,73 @@ const READ_FILE_TRUNCATION_MSG = `
1584
1633
 
1585
1634
  [Output was truncated due to size limits. The file content is very large. Consider reformatting the file to make it easier to navigate. For example, if this is JSON, use execute(command='jq . {file_path}') to pretty-print it with line breaks. For other formats, you can use appropriate formatting tools to split long lines.]`;
1586
1635
  /**
1636
+ * Render backend pagination metadata as guidance for the model.
1637
+ *
1638
+ * Backends own source-level pagination because only they know how much of the
1639
+ * file was read. The middleware owns presentation: it line-numbers the text
1640
+ * and turns optional metadata into a human-readable footer. Keeping the fields
1641
+ * optional preserves compatibility with custom backends that predate this
1642
+ * contract; those reads simply receive no pagination footer.
1643
+ *
1644
+ * `nextOffset` is the signal that the read is partial. A result at EOF omits it,
1645
+ * so complete reads retain their previous output shape.
1646
+ */
1647
+ function remainingLinesNotice(readResult) {
1648
+ const { startLine, endLine, nextOffset, totalLines } = readResult;
1649
+ if (startLine === void 0 || endLine === void 0 || nextOffset === void 0 || !Number.isSafeInteger(startLine) || !Number.isSafeInteger(endLine) || !Number.isSafeInteger(nextOffset) || startLine < 1 || endLine < startLine || nextOffset !== endLine || totalLines !== void 0 && (!Number.isSafeInteger(totalLines) || totalLines < endLine)) return "";
1650
+ const readCount = endLine - startLine + 1;
1651
+ const readUnit = readCount === 1 ? "line" : "lines";
1652
+ if (totalLines === void 0) return `\n\n[Read ${readCount} ${readUnit} (lines ${startLine}-${endLine}). More lines remain from offset ${nextOffset}.]`;
1653
+ if (endLine >= totalLines) return "";
1654
+ const remaining = totalLines - endLine;
1655
+ return `\n\n[Read ${readCount} ${readUnit} (lines ${startLine}-${endLine} of ${totalLines} total). ${remaining} ${remaining === 1 ? "line" : "lines"} remaining from offset ${nextOffset}.]`;
1656
+ }
1657
+ /**
1658
+ * Fit a line-numbered read into the middleware's output budget without
1659
+ * publishing a resume offset that skips content the model did not see.
1660
+ *
1661
+ * There are two independent forms of limiting:
1662
+ *
1663
+ * 1. The backend paginates the source file with `offset` and `limit`.
1664
+ * 2. The middleware may further shorten that page to fit its token budget.
1665
+ *
1666
+ * If the backend returned lines 1-100 but the middleware only displayed lines
1667
+ * 1-30, forwarding the backend's original `nextOffset: 100` would silently skip
1668
+ * lines 31-100 on the next read. This function therefore truncates only after a
1669
+ * complete source line and rebuilds the remaining-lines notice using the last
1670
+ * line actually displayed.
1671
+ *
1672
+ * Long source lines may occupy several formatted rows (`12`, `12.1`, ...). The
1673
+ * formatter records a structured boundary only after the final chunk, so this
1674
+ * function does not need to inspect or understand the gutter representation.
1675
+ * If no complete source line can fit beside the truncation message, the function
1676
+ * falls back to character truncation and omits pagination guidance rather than
1677
+ * advertising an unsafe offset.
1678
+ */
1679
+ function truncatePaginatedRead(formatted, filePath, readResult, tokenLimit) {
1680
+ const content = formatted.text;
1681
+ const notice = remainingLinesNotice(readResult);
1682
+ if (!tokenLimit || content.length + notice.length < 4 * tokenLimit) return content + notice;
1683
+ const truncationMsg = READ_FILE_TRUNCATION_MSG.replace("{file_path}", filePath);
1684
+ const threshold = 4 * tokenLimit;
1685
+ if (readResult.startLine !== void 0 && readResult.endLine !== void 0) {
1686
+ const finalSourceLine = readResult.endLine;
1687
+ const boundaries = formatted.sourceLineBoundaries.filter((boundary) => boundary.sourceLine <= finalSourceLine);
1688
+ for (let index = boundaries.length - 1; index >= 0; index -= 1) {
1689
+ const boundary = boundaries[index];
1690
+ const adjustedNotice = remainingLinesNotice({
1691
+ totalLines: readResult.totalLines,
1692
+ startLine: readResult.startLine,
1693
+ endLine: boundary.sourceLine,
1694
+ nextOffset: boundary.sourceLine
1695
+ });
1696
+ if (boundary.endOffset + truncationMsg.length + adjustedNotice.length <= threshold) return content.slice(0, boundary.endOffset) + truncationMsg + adjustedNotice;
1697
+ }
1698
+ }
1699
+ const maxContentLength = Math.max(0, threshold - truncationMsg.length);
1700
+ return content.substring(0, maxContentLength) + truncationMsg;
1701
+ }
1702
+ /**
1587
1703
  * Note appended to grep results that were cut short by the match-count cap.
1588
1704
  */
1589
1705
  const GREP_TRUNCATION_NOTE = "Note: the search stopped early because it hit the maximum match count. The matches above are valid but incomplete. Narrow the search (a more specific pattern or a narrower path), or raise max_count, to see the rest.";
@@ -2085,7 +2201,8 @@ function createReadFileTool(backend, options) {
2085
2201
  const permissionError = checkPermission(permissions, "read", input.file_path);
2086
2202
  if (permissionError !== void 0) return toolError(runtime, "read_file", permissionError);
2087
2203
  const resolvedBackend = await resolveBackend(backend, runtime);
2088
- const { file_path, offset = 0, limit = 100 } = input;
2204
+ const { file_path, offset: requestedOffset = 0, limit: requestedLimit = 100 } = input;
2205
+ const { offset, limit } = normalizeReadPagination(requestedOffset, requestedLimit);
2089
2206
  const readResult = await resolvedBackend.read(file_path, offset, limit);
2090
2207
  if (readResult.error) return [{
2091
2208
  type: "text",
@@ -2133,16 +2250,21 @@ function createReadFileTool(backend, options) {
2133
2250
  }
2134
2251
  let content = typeof readResult.content === "string" ? readResult.content : "";
2135
2252
  const lines = content.split("\n");
2136
- if (lines.length > limit) content = lines.slice(0, limit).join("\n");
2137
- let formatted = formatContentWithLineNumbers(content, offset + 1);
2138
- if (toolTokenLimitBeforeEvict && formatted.length >= 4 * toolTokenLimitBeforeEvict) {
2139
- const truncationMsg = READ_FILE_TRUNCATION_MSG.replace("{file_path}", file_path);
2140
- const maxContentLength = 4 * toolTokenLimitBeforeEvict - truncationMsg.length;
2141
- formatted = formatted.substring(0, maxContentLength) + truncationMsg;
2253
+ let paginationResult = readResult;
2254
+ if (lines.length > limit) {
2255
+ content = lines.slice(0, limit).join("\n");
2256
+ if (limit > 0 && readResult.startLine !== void 0 && readResult.endLine !== void 0) {
2257
+ const endLine = Math.min(readResult.startLine + limit - 1, readResult.endLine, readResult.totalLines ?? Number.POSITIVE_INFINITY);
2258
+ paginationResult = {
2259
+ ...readResult,
2260
+ endLine,
2261
+ nextOffset: endLine
2262
+ };
2263
+ }
2142
2264
  }
2143
2265
  return [{
2144
2266
  type: "text",
2145
- text: formatted
2267
+ text: truncatePaginatedRead(formatContentWithLineNumbersAndBoundaries(content, paginationResult.startLine ?? offset + 1), file_path, paginationResult, toolTokenLimitBeforeEvict)
2146
2268
  }];
2147
2269
  }, {
2148
2270
  name: "read_file",
@@ -3260,6 +3382,83 @@ function createSummarizationMiddleware(options) {
3260
3382
  });
3261
3383
  }
3262
3384
  //#endregion
3385
+ //#region src/middleware/utils.ts
3386
+ /**
3387
+ * Utility functions for middleware.
3388
+ *
3389
+ * This module provides shared helpers used across middleware implementations.
3390
+ */
3391
+ /**
3392
+ * Merge custom middleware into an assembled stack by `.name`.
3393
+ *
3394
+ * Matching custom middleware replaces the existing entry in place. New
3395
+ * middleware is appended after the base stack in caller-provided order.
3396
+ */
3397
+ function mergeMiddleware$1(base, custom) {
3398
+ const merged = new Map(base.map((middleware) => [middleware.name, middleware]));
3399
+ for (const middleware of custom) merged.set(middleware.name, middleware);
3400
+ return [...merged.values()];
3401
+ }
3402
+ function middlewareNames(middleware) {
3403
+ return new Set(middleware.map((entry) => entry.name));
3404
+ }
3405
+ function matchingMiddleware(middleware, names) {
3406
+ return middleware.filter((entry) => names.has(entry.name));
3407
+ }
3408
+ /**
3409
+ * Merge custom middleware into default and tail middleware segments.
3410
+ *
3411
+ * Same-name custom entries replace matching defaults in either segment. Novel
3412
+ * custom entries are inserted between the default and tail segments unless
3413
+ * `appendNew` is false.
3414
+ */
3415
+ function mergeMiddlewareStack(defaultMiddleware, customMiddleware, tailMiddleware = [], options = {}) {
3416
+ const defaultMiddlewareNames = middlewareNames(defaultMiddleware);
3417
+ const tailMiddlewareNames = middlewareNames(tailMiddleware);
3418
+ const knownMiddlewareNames = /* @__PURE__ */ new Set([...defaultMiddlewareNames, ...tailMiddlewareNames]);
3419
+ const novelMiddleware = options.appendNew === false ? [] : customMiddleware.filter((entry) => !knownMiddlewareNames.has(entry.name));
3420
+ return [
3421
+ ...mergeMiddleware$1(defaultMiddleware, matchingMiddleware(customMiddleware, defaultMiddlewareNames)),
3422
+ ...novelMiddleware,
3423
+ ...mergeMiddleware$1(tailMiddleware, matchingMiddleware(customMiddleware, tailMiddlewareNames))
3424
+ ];
3425
+ }
3426
+ /**
3427
+ * Append text to a system message.
3428
+ *
3429
+ * Creates a new SystemMessage with the text appended to the existing content.
3430
+ * If the original message has content, the new text is separated by two newlines.
3431
+ *
3432
+ * @param systemMessage - Existing system message or null/undefined.
3433
+ * @param text - Text to add to the system message.
3434
+ * @returns New SystemMessage with the text appended.
3435
+ *
3436
+ * @example
3437
+ * ```typescript
3438
+ * const original = new SystemMessage({ content: "You are a helpful assistant." });
3439
+ * const updated = appendToSystemMessage(original, "Always be concise.");
3440
+ * // Result: SystemMessage with content "You are a helpful assistant.\n\nAlways be concise."
3441
+ * ```
3442
+ */
3443
+ function appendToSystemMessage(systemMessage, text) {
3444
+ if (!systemMessage) return new _langchain_core_messages.SystemMessage({ content: text });
3445
+ const existingContent = systemMessage.content;
3446
+ if (typeof existingContent === "string") {
3447
+ const newContent = existingContent ? `${existingContent}\n\n${text}` : text;
3448
+ return new _langchain_core_messages.SystemMessage({ content: newContent });
3449
+ }
3450
+ if (Array.isArray(existingContent)) {
3451
+ const newContent = [...existingContent];
3452
+ const textToAdd = newContent.length > 0 ? `\n\n${text}` : text;
3453
+ newContent.push({
3454
+ type: "text",
3455
+ text: textToAdd
3456
+ });
3457
+ return new _langchain_core_messages.SystemMessage({ content: newContent });
3458
+ }
3459
+ return new _langchain_core_messages.SystemMessage({ content: text });
3460
+ }
3461
+ //#endregion
3263
3462
  //#region src/middleware/subagents.ts
3264
3463
  /**
3265
3464
  * Config key used by task-tool callers to request dynamic response format.
@@ -3273,6 +3472,8 @@ const SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY = "__deepagents_subagent_response_form
3273
3472
  * Provides a minimal base prompt that can be extended by specific subagent configurations.
3274
3473
  */
3275
3474
  const DEFAULT_SUBAGENT_PROMPT = "In order to complete the objective that the user asks of you, you have access to a number of standard tools.";
3475
+ const FORKED_CONTEXT_KEY = "_deepagentsForkedContext";
3476
+ const FORK_RECURSION_REFUSAL = "You are a subagent and cannot delegate to another subagent. Complete this task yourself instead of calling this tool again.";
3276
3477
  /**
3277
3478
  * State keys excluded when passing state to subagents and when returning
3278
3479
  * updates from subagents. Summarization keys are excluded because their
@@ -3285,6 +3486,18 @@ const EXCLUDED_STATE_KEYS = [
3285
3486
  "skillsMetadata",
3286
3487
  "memoryContents",
3287
3488
  "_summarizationEvent",
3489
+ "_summarizationSessionId",
3490
+ FORKED_CONTEXT_KEY
3491
+ ];
3492
+ /**
3493
+ * State keys excluded when inheriting state into a declarative fork.
3494
+ * Narrower than `EXCLUDED_STATE_KEYS`: a fork's mirrored middleware needs
3495
+ * the parent's private channels (skills metadata, memory contents, etc.)
3496
+ * to rebuild an equivalent prompt.
3497
+ */
3498
+ const FORK_EXCLUDED_STATE_KEYS = [
3499
+ "structuredResponse",
3500
+ "_summarizationEvent",
3288
3501
  "_summarizationSessionId"
3289
3502
  ];
3290
3503
  /**
@@ -3294,25 +3507,37 @@ const EXCLUDED_STATE_KEYS = [
3294
3507
  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.";
3295
3508
  function getTaskToolDescription(subagentDescriptions) {
3296
3509
  return langchain.context`
3297
- Launch an ephemeral subagent to handle a complex, multi-step task in an isolated context window.
3510
+ Launch an ephemeral subagent to handle a complex, multi-step task.
3298
3511
 
3299
3512
  Available agent types and the tools they have access to:
3300
3513
  ${subagentDescriptions.join("\n")}
3301
3514
 
3302
3515
  Specify subagent_type to select the agent. Usage notes:
3303
3516
  - Launch multiple agents concurrently when their tasks are independent, using a single message with multiple tool calls.
3304
- - Each invocation is stateless: the agent sees only the prompt you give it and returns a single final report. Put full detail in the prompt and state exactly what it should return.
3517
+ - Each invocation is stateless by default: the agent sees only the prompt you give it and returns a single final report. Put full detail in the prompt and state exactly what it should return — unless an agent type below says it inherits your conversation instead.
3305
3518
  - The agent's report is not shown to the user; relay a summary yourself.
3306
- - Tell the agent whether to create content, analyze, or only research, since it cannot see the user's intent.
3519
+ - Tell the agent whether to create content, analyze, or only research, since it can't necessarily see the user's intent unless it inherits your conversation, as noted per agent type below.
3307
3520
  - If an agent's description says to use it proactively, do so without waiting to be asked.
3308
3521
  - When only general-purpose is available, use it for any complex, context-heavy task; it has the same capabilities as the main agent.
3309
3522
  `;
3310
3523
  }
3524
+ const FORKED_SUBAGENT_TOOL_NOTE = " (inherits your full conversation and system prompt — no need to restate context here)";
3525
+ const COMPILED_FORKED_SUBAGENT_TOOL_NOTE = " (inherits your conversation history — its system prompt is fixed in its own runnable)";
3526
+ /** Render one subagent's listing line for the task tool description. */
3527
+ function describeSubagentForTool(name, description, forked, compiled = false) {
3528
+ return `- ${name}: ${description}${forked ? compiled ? COMPILED_FORKED_SUBAGENT_TOOL_NOTE : FORKED_SUBAGENT_TOOL_NOTE : ""}`;
3529
+ }
3530
+ const FORK_TASK_PREAMBLE = "[The messages above are a prior conversation you are continuing as the subagent that was just invoked. Any mention in them of delegating to a subagent already happened — you are that subagent, not the one being asked to delegate further. If you try to delegate to another subagent yourself, it will be refused — complete this task directly. Use the specific facts, figures, and identifiers already established in that conversation when completing the task below — do not answer generically when exact details are already available above. Your actual task is below.]\n\n";
3531
+ /**
3532
+ * Whether a declarative subagent spec has `mode: "fork"` set.
3533
+ *
3534
+ * A plain boolean, not a type predicate: `SubAgent` covers both `"fork"` and
3535
+ * `"isolated"`, so there's no distinct type left to narrow to.
3536
+ */
3311
3537
  function isForkedSubAgent(value) {
3312
3538
  if (typeof value !== "object" || value == null) return false;
3313
3539
  if (!("mode" in value)) return false;
3314
- if (value.mode !== "fork") return false;
3315
- return true;
3540
+ return value.mode === "fork";
3316
3541
  }
3317
3542
  /**
3318
3543
  * Base specification for the general-purpose subagent.
@@ -3355,15 +3580,26 @@ const GENERAL_PURPOSE_SUBAGENT = {
3355
3580
  name: "general-purpose",
3356
3581
  description: DEFAULT_GENERAL_PURPOSE_DESCRIPTION,
3357
3582
  systemPrompt: DEFAULT_SUBAGENT_PROMPT,
3358
- mode: "handoff"
3583
+ mode: "isolated"
3359
3584
  };
3585
+ function filterState(state, excludedKeys) {
3586
+ const filtered = {};
3587
+ for (const [key, value] of Object.entries(state)) if (!excludedKeys.includes(key)) filtered[key] = value;
3588
+ return filtered;
3589
+ }
3360
3590
  /**
3361
3591
  * Filter state to exclude certain keys when passing to subagents
3362
3592
  */
3363
3593
  function filterStateForSubagent(state) {
3364
- const filtered = {};
3365
- for (const [key, value] of Object.entries(state)) if (!EXCLUDED_STATE_KEYS.includes(key)) filtered[key] = value;
3366
- return filtered;
3594
+ return filterState(state, EXCLUDED_STATE_KEYS);
3595
+ }
3596
+ /**
3597
+ * Filter state to exclude only the keys a declarative fork must not resume
3598
+ * (structured response, summarization event/session) — see
3599
+ * `FORK_EXCLUDED_STATE_KEYS`.
3600
+ */
3601
+ function filterStateForFork(state) {
3602
+ return filterState(state, FORK_EXCLUDED_STATE_KEYS);
3367
3603
  }
3368
3604
  /**
3369
3605
  * Invalid tool message block types
@@ -3407,6 +3643,15 @@ function stripInFlightAIMessage(messages) {
3407
3643
  const last = messages.at(-1);
3408
3644
  return _langchain_core_messages.AIMessage.isInstance(last) && (last.tool_calls?.length ?? 0) > 0 ? messages.slice(0, -1) : messages;
3409
3645
  }
3646
+ const ForkedContextStateSchema = zod_v4.z.object({ [FORKED_CONTEXT_KEY]: zod_v4.z.boolean().optional() });
3647
+ function createForkTaskToolMiddleware(taskTool) {
3648
+ return (0, langchain.createMiddleware)({
3649
+ name: "forkTaskToolMiddleware",
3650
+ stateSchema: ForkedContextStateSchema,
3651
+ tools: [taskTool],
3652
+ beforeAgent: () => ({ [FORKED_CONTEXT_KEY]: true })
3653
+ });
3654
+ }
3410
3655
  /**
3411
3656
  * Create a runnable agent from a declarative `SubAgent` spec.
3412
3657
  *
@@ -3436,6 +3681,17 @@ function createSubAgent(spec, options) {
3436
3681
  });
3437
3682
  }
3438
3683
  /**
3684
+ * Resolve a fork's system prompt: the parent's inherited prompt, with the
3685
+ * fork's own systemPrompt (if any) appended as an addendum rather than
3686
+ * replacing it.
3687
+ */
3688
+ function resolveForkSystemPrompt(parentSystemPrompt, forkAddendum) {
3689
+ if (!forkAddendum) return parentSystemPrompt ?? "";
3690
+ const addendumText = typeof forkAddendum === "string" ? forkAddendum : forkAddendum.text;
3691
+ if (langchain.SystemMessage.isInstance(parentSystemPrompt)) return appendToSystemMessage(parentSystemPrompt, addendumText);
3692
+ return parentSystemPrompt ? `${parentSystemPrompt}\n\n${addendumText}` : addendumText;
3693
+ }
3694
+ /**
3439
3695
  * Create subagent instances from specifications.
3440
3696
  *
3441
3697
  * Returns compiled agents, raw specs keyed by name (for on-demand
@@ -3443,13 +3699,18 @@ function createSubAgent(spec, options) {
3443
3699
  * of names that should fork the parent's conversation.
3444
3700
  */
3445
3701
  function getSubagents(options) {
3446
- const { defaultModel, defaultTools, defaultMiddleware, generalPurposeMiddleware: gpMiddleware, defaultInterruptOn, subagents, generalPurposeAgent, parentSystemPrompt = null } = options;
3702
+ const { defaultModel, defaultTools, defaultMiddleware, generalPurposeMiddleware: gpMiddleware, defaultInterruptOn, subagents, generalPurposeAgent, parentSystemPrompt = null, mirroredTaskTool } = options;
3447
3703
  const defaultSubagentMiddleware = defaultMiddleware || [];
3448
3704
  const generalPurposeMiddlewareBase = gpMiddleware || defaultSubagentMiddleware;
3449
3705
  const agents = {};
3450
3706
  const specsByName = {};
3451
3707
  const subagentDescriptions = [];
3452
3708
  const forkModeNames = /* @__PURE__ */ new Set();
3709
+ const seenNames = new Set(generalPurposeAgent ? ["general-purpose"] : []);
3710
+ for (const agentParams of subagents) {
3711
+ if (seenNames.has(agentParams.name)) throw new Error(`Duplicate subagent name '${agentParams.name}'; each subagent must have a unique name.`);
3712
+ seenNames.add(agentParams.name);
3713
+ }
3453
3714
  if (generalPurposeAgent) {
3454
3715
  const generalPurposeMiddleware = [...generalPurposeMiddlewareBase];
3455
3716
  if (defaultInterruptOn) generalPurposeMiddleware.push((0, langchain.humanInTheLoopMiddleware)({ interruptOn: defaultInterruptOn }));
@@ -3463,24 +3724,34 @@ function getSubagents(options) {
3463
3724
  };
3464
3725
  agents["general-purpose"] = createSubAgent(gpSpec);
3465
3726
  specsByName["general-purpose"] = gpSpec;
3466
- subagentDescriptions.push(`- general-purpose: ${DEFAULT_GENERAL_PURPOSE_DESCRIPTION}`);
3727
+ subagentDescriptions.push(describeSubagentForTool("general-purpose", DEFAULT_GENERAL_PURPOSE_DESCRIPTION, false));
3467
3728
  }
3468
3729
  for (const agentParams of subagents) {
3469
3730
  const rawMode = agentParams.mode;
3470
- if (rawMode != null && rawMode !== "handoff" && rawMode !== "fork") throw new Error(`SubAgent '${agentParams.name}' has invalid mode '${rawMode}' — must be "handoff" or "fork".`);
3471
- subagentDescriptions.push(`- ${agentParams.name}: ${agentParams.description}`);
3731
+ if (rawMode != null && rawMode !== "isolated" && rawMode !== "fork" && rawMode !== "handoff") throw new Error(`SubAgent '${agentParams.name}' has invalid mode '${rawMode}' — must be "isolated" or "fork".`);
3732
+ const forked = isForkedSubAgent(agentParams);
3733
+ const compiled = "runnable" in agentParams;
3734
+ subagentDescriptions.push(describeSubagentForTool(agentParams.name, agentParams.description, forked, compiled));
3472
3735
  if ("runnable" in agentParams) {
3473
3736
  agents[agentParams.name] = agentParams.runnable;
3474
3737
  specsByName[agentParams.name] = agentParams;
3475
- if (isForkedSubAgent(agentParams)) forkModeNames.add(agentParams.name);
3476
- } else if (isForkedSubAgent(agentParams)) {
3738
+ if (forked) forkModeNames.add(agentParams.name);
3739
+ continue;
3740
+ }
3741
+ const subagentMiddleware = [...defaultSubagentMiddleware, ...agentParams.middleware ?? []];
3742
+ if (forked) {
3743
+ const rawSkills = agentParams.skills;
3744
+ if (Array.isArray(rawSkills) && rawSkills.length > 0) throw new Error(`SubAgent '${agentParams.name}' cannot set skills under mode: "fork"; the parent's skills are inherited instead.`);
3745
+ const resolvedSystemPrompt = resolveForkSystemPrompt(parentSystemPrompt, agentParams.systemPrompt);
3746
+ const fsIndex = subagentMiddleware.findIndex((m) => m.name === "FilesystemMiddleware");
3747
+ subagentMiddleware.splice(fsIndex + 1, 0, createForkTaskToolMiddleware(mirroredTaskTool));
3477
3748
  const resolvedSpec = {
3478
3749
  ...agentParams,
3479
- systemPrompt: parentSystemPrompt ?? "",
3750
+ systemPrompt: resolvedSystemPrompt,
3480
3751
  mode: void 0,
3481
3752
  model: agentParams.model ?? defaultModel,
3482
3753
  tools: agentParams.tools ?? defaultTools,
3483
- middleware: [...defaultSubagentMiddleware, ...agentParams.middleware ?? []],
3754
+ middleware: subagentMiddleware,
3484
3755
  interruptOn: agentParams.interruptOn ?? defaultInterruptOn ?? void 0
3485
3756
  };
3486
3757
  agents[agentParams.name] = createSubAgent(resolvedSpec);
@@ -3489,10 +3760,10 @@ function getSubagents(options) {
3489
3760
  } else {
3490
3761
  const resolvedSpec = {
3491
3762
  ...agentParams,
3492
- mode: "handoff",
3763
+ mode: "isolated",
3493
3764
  model: agentParams.model ?? defaultModel,
3494
3765
  tools: agentParams.tools ?? defaultTools,
3495
- middleware: [...defaultSubagentMiddleware, ...agentParams.middleware ?? []],
3766
+ middleware: subagentMiddleware,
3496
3767
  interruptOn: agentParams.interruptOn ?? defaultInterruptOn ?? void 0
3497
3768
  };
3498
3769
  agents[agentParams.name] = createSubAgent(resolvedSpec);
@@ -3511,16 +3782,12 @@ function getSubagents(options) {
3511
3782
  */
3512
3783
  function createTaskTool(options) {
3513
3784
  const { defaultModel, defaultTools, defaultMiddleware, generalPurposeMiddleware, defaultInterruptOn, subagents, generalPurposeAgent, taskDescription, parentSystemPrompt = null } = options;
3514
- const { agents: subagentGraphs, specsByName, descriptions: subagentDescriptions, forkModeNames } = getSubagents({
3515
- defaultModel,
3516
- defaultTools,
3517
- defaultMiddleware,
3518
- generalPurposeMiddleware,
3519
- defaultInterruptOn,
3520
- subagents,
3521
- generalPurposeAgent,
3522
- parentSystemPrompt
3523
- });
3785
+ const subagentNames = [...generalPurposeAgent ? ["general-purpose"] : [], ...subagents.map((spec) => spec.name)];
3786
+ const subagentDescriptions = [...generalPurposeAgent ? [describeSubagentForTool("general-purpose", DEFAULT_GENERAL_PURPOSE_DESCRIPTION, false)] : [], ...subagents.map((spec) => describeSubagentForTool(spec.name, spec.description, isForkedSubAgent(spec), "runnable" in spec))];
3787
+ const finalTaskDescription = taskDescription ? taskDescription : getTaskToolDescription(subagentDescriptions);
3788
+ let subagentGraphs = {};
3789
+ let specsByName = {};
3790
+ let forkModeNames = /* @__PURE__ */ new Set();
3524
3791
  function selectSubagent(subagentType, config) {
3525
3792
  const spec = specsByName[subagentType];
3526
3793
  const responseFormat = config.configurable?.[SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY];
@@ -3528,18 +3795,19 @@ function createTaskTool(options) {
3528
3795
  if ("runnable" in spec || responseFormat == null) return subagentGraphs[subagentType];
3529
3796
  return createSubAgent(spec, { responseFormat });
3530
3797
  }
3531
- const finalTaskDescription = taskDescription ? taskDescription : getTaskToolDescription(subagentDescriptions);
3532
- return (0, langchain.tool)(async (input, config) => {
3798
+ async function runTask(input, config) {
3533
3799
  const { description, subagent_type } = input;
3800
+ const currentState = (0, _langchain_langgraph.getCurrentTaskInput)();
3801
+ if (currentState[FORKED_CONTEXT_KEY]) return FORK_RECURSION_REFUSAL;
3534
3802
  if (!(subagent_type in subagentGraphs)) {
3535
3803
  const allowedTypes = Object.keys(subagentGraphs).map((k) => `\`${k}\``).join(", ");
3536
3804
  throw new Error(`Error: invoked agent of type ${subagent_type}, the only allowed types are ${allowedTypes}`);
3537
3805
  }
3538
3806
  const shouldFork = forkModeNames.has(subagent_type);
3539
3807
  const subagent = selectSubagent(subagent_type, config);
3540
- const currentState = (0, _langchain_langgraph.getCurrentTaskInput)();
3541
- const subagentState = filterStateForSubagent(currentState);
3542
- if (shouldFork) subagentState.messages = [...getEffectiveMessages(stripInFlightAIMessage(currentState.messages ?? []), currentState), new _langchain_core_messages.HumanMessage({ content: description })];
3808
+ const spec = specsByName[subagent_type];
3809
+ const subagentState = shouldFork && !("runnable" in spec) ? filterStateForFork(currentState) : filterStateForSubagent(currentState);
3810
+ if (shouldFork) subagentState.messages = [...getEffectiveMessages(stripInFlightAIMessage(currentState.messages ?? []), currentState), new _langchain_core_messages.HumanMessage({ content: FORK_TASK_PREAMBLE + description })];
3543
3811
  else subagentState.messages = [new _langchain_core_messages.HumanMessage({ content: description })];
3544
3812
  subagentState._summarizationSessionId = `session_${crypto.randomUUID().substring(0, 8)}`;
3545
3813
  const subagentConfig = {
@@ -3566,14 +3834,35 @@ function createTaskTool(options) {
3566
3834
  return content;
3567
3835
  }
3568
3836
  return returnCommandWithStateUpdate(result, config.toolCall.id);
3569
- }, {
3837
+ }
3838
+ const taskToolSchema = zod_v4.z.object({
3839
+ description: zod_v4.z.string().describe("The task to execute with the selected agent"),
3840
+ subagent_type: zod_v4.z.string().describe(`Name of the agent to use. Available: ${subagentNames.join(", ")}`)
3841
+ });
3842
+ const taskTool = (0, langchain.tool)(runTask, {
3570
3843
  name: "task",
3571
3844
  description: finalTaskDescription,
3572
- schema: zod_v4.z.object({
3573
- description: zod_v4.z.string().describe("The task to execute with the selected agent"),
3574
- subagent_type: zod_v4.z.string().describe(`Name of the agent to use. Available: ${Object.keys(subagentGraphs).join(", ")}`)
3845
+ schema: taskToolSchema
3846
+ });
3847
+ const { agents, specsByName: resolvedSpecsByName, forkModeNames: resolvedForkModeNames } = getSubagents({
3848
+ defaultModel,
3849
+ defaultTools,
3850
+ defaultMiddleware,
3851
+ generalPurposeMiddleware,
3852
+ defaultInterruptOn,
3853
+ subagents,
3854
+ generalPurposeAgent,
3855
+ parentSystemPrompt,
3856
+ mirroredTaskTool: (0, langchain.tool)(runTask, {
3857
+ name: "task",
3858
+ description: finalTaskDescription,
3859
+ schema: taskToolSchema
3575
3860
  })
3576
3861
  });
3862
+ subagentGraphs = agents;
3863
+ specsByName = resolvedSpecsByName;
3864
+ forkModeNames = resolvedForkModeNames;
3865
+ return taskTool;
3577
3866
  }
3578
3867
  /**
3579
3868
  * Create subagent middleware with task tool
@@ -4575,48 +4864,6 @@ function createSkillsMiddleware(options) {
4575
4864
  });
4576
4865
  }
4577
4866
  //#endregion
4578
- //#region src/middleware/utils.ts
4579
- /**
4580
- * Utility functions for middleware.
4581
- *
4582
- * This module provides shared helpers used across middleware implementations.
4583
- */
4584
- /**
4585
- * Merge custom middleware into an assembled stack by `.name`.
4586
- *
4587
- * Matching custom middleware replaces the existing entry in place. New
4588
- * middleware is appended after the base stack in caller-provided order.
4589
- */
4590
- function mergeMiddleware$1(base, custom) {
4591
- const merged = new Map(base.map((middleware) => [middleware.name, middleware]));
4592
- for (const middleware of custom) merged.set(middleware.name, middleware);
4593
- return [...merged.values()];
4594
- }
4595
- function middlewareNames(middleware) {
4596
- return new Set(middleware.map((entry) => entry.name));
4597
- }
4598
- function matchingMiddleware(middleware, names) {
4599
- return middleware.filter((entry) => names.has(entry.name));
4600
- }
4601
- /**
4602
- * Merge custom middleware into default and tail middleware segments.
4603
- *
4604
- * Same-name custom entries replace matching defaults in either segment. Novel
4605
- * custom entries are inserted between the default and tail segments unless
4606
- * `appendNew` is false.
4607
- */
4608
- function mergeMiddlewareStack(defaultMiddleware, customMiddleware, tailMiddleware = [], options = {}) {
4609
- const defaultMiddlewareNames = middlewareNames(defaultMiddleware);
4610
- const tailMiddlewareNames = middlewareNames(tailMiddleware);
4611
- const knownMiddlewareNames = /* @__PURE__ */ new Set([...defaultMiddlewareNames, ...tailMiddlewareNames]);
4612
- const novelMiddleware = options.appendNew === false ? [] : customMiddleware.filter((entry) => !knownMiddlewareNames.has(entry.name));
4613
- return [
4614
- ...mergeMiddleware$1(defaultMiddleware, matchingMiddleware(customMiddleware, defaultMiddlewareNames)),
4615
- ...novelMiddleware,
4616
- ...mergeMiddleware$1(tailMiddleware, matchingMiddleware(customMiddleware, tailMiddlewareNames))
4617
- ];
4618
- }
4619
- //#endregion
4620
4867
  //#region src/middleware/completion_callback.ts
4621
4868
  /**
4622
4869
  * Callback middleware for async subagents.
@@ -5476,8 +5723,9 @@ function hasToolName(tool) {
5476
5723
  return tool !== null && typeof tool === "object" && "name" in tool && typeof tool.name === "string";
5477
5724
  }
5478
5725
  /**
5479
- * Create middleware that removes excluded tools after all tool-injecting
5480
- * middleware has had a chance to add tools to the request.
5726
+ * Create middleware that hides excluded tools from the model and rejects calls
5727
+ * to them. Exclusions calibrate the agent per model; they are not a security
5728
+ * boundary.
5481
5729
  *
5482
5730
  * @internal
5483
5731
  */
@@ -5489,6 +5737,16 @@ function createToolExclusionMiddleware(excludedTools) {
5489
5737
  ...request,
5490
5738
  tools: request.tools?.filter((tool) => !hasToolName(tool) || !excludedTools.has(tool.name))
5491
5739
  });
5740
+ },
5741
+ wrapToolCall(request, handler) {
5742
+ const { name, id } = request.toolCall;
5743
+ if (!excludedTools.has(name)) return handler(request);
5744
+ return new _langchain_core_messages.ToolMessage({
5745
+ content: `Error: ${name} is not available.`,
5746
+ tool_call_id: id ?? "",
5747
+ name,
5748
+ status: "error"
5749
+ });
5492
5750
  }
5493
5751
  });
5494
5752
  }
@@ -6234,8 +6492,18 @@ function createDeepAgent(params = {}) {
6234
6492
  providerHint: getModelProvider(model),
6235
6493
  identifierHint: getModelIdentifier(model)
6236
6494
  });
6237
- const filesystemTools = FILESYSTEM_TOOL_NAMES.filter((toolName) => !harnessProfile.excludedTools.has(toolName));
6238
- const profileFilesystemTools = filesystemTools.length === FILESYSTEM_TOOL_NAMES.length ? void 0 : filesystemTools.includes("read_file") ? filesystemTools : ["read_file", ...filesystemTools];
6495
+ const computeProfileFilesystemTools = (profile) => {
6496
+ const filesystemTools = FILESYSTEM_TOOL_NAMES.filter((toolName) => !profile.excludedTools.has(toolName));
6497
+ return filesystemTools.length === FILESYSTEM_TOOL_NAMES.length ? void 0 : filesystemTools.includes("read_file") ? filesystemTools : ["read_file", ...filesystemTools];
6498
+ };
6499
+ const profileFilesystemTools = computeProfileFilesystemTools(harnessProfile);
6500
+ const resolveSubagentProfile = (subagentModel) => {
6501
+ if (subagentModel == null || subagentModel === model) return harnessProfile;
6502
+ return typeof subagentModel === "string" ? resolveHarnessProfile({ spec: subagentModel }) : resolveHarnessProfile({
6503
+ providerHint: getModelProvider(subagentModel),
6504
+ identifierHint: getModelIdentifier(subagentModel)
6505
+ });
6506
+ };
6239
6507
  const toolOverrides = harnessProfile.toolDescriptionOverrides;
6240
6508
  const effectiveTools = Object.keys(toolOverrides).length > 0 ? tools.map((t) => t.name in toolOverrides ? Object.assign(Object.create(Object.getPrototypeOf(t)), t, { description: toolOverrides[t.name] }) : t) : tools;
6241
6509
  const anthropicModel = isAnthropicModel(model);
@@ -6271,44 +6539,50 @@ function createDeepAgent(params = {}) {
6271
6539
  * Only the general-purpose subagent inherits the main agent's skills.
6272
6540
  * If a custom subagent needs skills, it must specify its own `skills` array.
6273
6541
  */
6274
- const createSubagentDefaultMiddleware = (input) => {
6542
+ const createSubagentDefaultMiddleware = (input, subagentProfile, forked) => {
6275
6543
  const effectivePermissions = input.permissions ?? permissions;
6276
6544
  return [
6277
6545
  createFilesystemMiddleware({
6278
6546
  backend,
6279
6547
  permissions: effectivePermissions,
6280
- tools: profileFilesystemTools
6548
+ tools: computeProfileFilesystemTools(subagentProfile)
6281
6549
  }),
6282
6550
  createSummarizationMiddleware({ backend }),
6283
6551
  createPatchToolCallsMiddleware(),
6284
- ...input.skills != null && input.skills.length > 0 ? [createSkillsMiddleware({
6552
+ ...!forked && input.skills != null && input.skills.length > 0 ? [createSkillsMiddleware({
6285
6553
  backend,
6286
6554
  sources: input.skills
6287
6555
  })] : []
6288
6556
  ];
6289
6557
  };
6290
- const buildSubagentMiddleware = (input, isForkable) => {
6291
- let subagentMiddleware = mergeMiddlewareStack(createSubagentDefaultMiddleware(input), input.middleware ?? [], [
6292
- ...resolveMiddleware(harnessProfile.extraMiddleware),
6558
+ const buildSubagentMiddleware = (input) => {
6559
+ const subagentProfile = resolveSubagentProfile(input.model);
6560
+ const forked = isForkedSubAgent(input);
6561
+ const subagentDefaultMiddleware = createSubagentDefaultMiddleware(input, subagentProfile, forked);
6562
+ if (forked && skills != null && skills.length > 0) subagentDefaultMiddleware.unshift(createSkillsMiddleware({
6563
+ backend,
6564
+ sources: skills
6565
+ }));
6566
+ let subagentMiddleware = mergeMiddlewareStack(subagentDefaultMiddleware, forked && customMiddleware.length > 0 ? mergeMiddleware$1(customMiddleware, input.middleware ?? []) : input.middleware ?? [], [
6567
+ ...resolveMiddleware(subagentProfile.extraMiddleware),
6293
6568
  ...cacheMiddleware,
6294
- ...isForkable ? memoryMiddleware : []
6569
+ ...forked && memory != null && memory.length > 0 ? [createMemoryMiddleware({
6570
+ backend,
6571
+ sources: memory,
6572
+ addCacheControl: anthropicModel
6573
+ })] : []
6295
6574
  ]);
6296
- if (harnessProfile.excludedMiddleware.size > 0) subagentMiddleware = subagentMiddleware.filter((middleware) => !harnessProfile.excludedMiddleware.has(middleware.name));
6575
+ if (subagentProfile.excludedMiddleware.size > 0) subagentMiddleware = subagentMiddleware.filter((middleware) => !subagentProfile.excludedMiddleware.has(middleware.name));
6576
+ if (subagentProfile.excludedTools.size > 0) subagentMiddleware.push(createToolExclusionMiddleware(subagentProfile.excludedTools));
6297
6577
  return subagentMiddleware;
6298
6578
  };
6299
6579
  const normalizeSubagentSpec = (input) => ({
6300
6580
  ...input,
6301
- tools: input.tools ?? [],
6302
- middleware: buildSubagentMiddleware(input, false)
6303
- });
6304
- const normalizeForkedSubagentSpec = (input) => ({
6305
- ...input,
6306
- tools: input.tools ?? [],
6307
- middleware: buildSubagentMiddleware(input, true)
6581
+ middleware: buildSubagentMiddleware(input)
6308
6582
  });
6309
6583
  const allSubagents = subagents;
6310
6584
  const asyncSubAgents = allSubagents.filter((item) => isAsyncSubAgent(item));
6311
- const inlineSubagents = allSubagents.filter((item) => !isAsyncSubAgent(item)).map((item) => "runnable" in item ? item : isForkedSubAgent(item) ? normalizeForkedSubagentSpec(item) : normalizeSubagentSpec(item));
6585
+ const inlineSubagents = allSubagents.filter((item) => !isAsyncSubAgent(item)).map((item) => "runnable" in item ? item : normalizeSubagentSpec(item));
6312
6586
  const gpConfig = harnessProfile.generalPurposeSubagent;
6313
6587
  if (!(gpConfig?.enabled === false) && !inlineSubagents.some((item) => item.name === GENERAL_PURPOSE_SUBAGENT["name"])) {
6314
6588
  const gpSystemPrompt = gpConfig?.systemPrompt ?? applyProfilePrompt(harnessProfile, GENERAL_PURPOSE_SUBAGENT.systemPrompt);
@@ -6800,10 +7074,23 @@ var StoreBackend = class {
6800
7074
  mimeType: fileDataV2.mimeType
6801
7075
  };
6802
7076
  if (typeof fileDataV2.content !== "string") return { error: `File '${filePath}' has binary content but text MIME type` };
6803
- return {
6804
- content: fileDataV2.content.split("\n").slice(offset, offset + limit).join("\n"),
7077
+ const { offset: normalizedOffset, limit: normalizedLimit } = normalizeReadPagination(offset, limit);
7078
+ const lines = fileDataV2.content.split("\n");
7079
+ const totalLines = lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
7080
+ const selected = lines.slice(normalizedOffset, normalizedOffset + normalizedLimit);
7081
+ if (selected.length === 0 || normalizedOffset >= totalLines || normalizedLimit === 0) return {
7082
+ content: selected.join("\n"),
6805
7083
  mimeType: fileDataV2.mimeType
6806
7084
  };
7085
+ const endOffset = Math.min(normalizedOffset + selected.length, totalLines);
7086
+ return {
7087
+ content: selected.join("\n"),
7088
+ mimeType: fileDataV2.mimeType,
7089
+ totalLines,
7090
+ startLine: normalizedOffset + 1,
7091
+ endLine: endOffset,
7092
+ nextOffset: endOffset < totalLines ? endOffset : void 0
7093
+ };
6807
7094
  } catch (e) {
6808
7095
  return { error: e.message };
6809
7096
  }
@@ -7069,12 +7356,20 @@ function splitLinesKeepEnds(content) {
7069
7356
  return lines;
7070
7357
  }
7071
7358
  function sliceReadContent(content, offset, limit) {
7072
- if (!content || content.trim() === "") return { content };
7359
+ if (!content) return { content };
7073
7360
  const lines = splitLinesKeepEnds(content.replace(/\r\n/g, "\n").replace(/\r/g, "\n"));
7074
7361
  const startIndex = offset;
7075
7362
  const endIndex = Math.min(startIndex + limit, lines.length);
7076
7363
  if (startIndex >= lines.length) return { error: `Line offset ${offset} exceeds file length (${lines.length} lines)` };
7077
- return { content: lines.slice(startIndex, endIndex).join("") };
7364
+ const selected = lines.slice(startIndex, endIndex);
7365
+ if (selected.length === 0 || offset < 0 || limit <= 0) return { content: selected.join("") };
7366
+ return {
7367
+ content: selected.join(""),
7368
+ totalLines: lines.length,
7369
+ startLine: startIndex + 1,
7370
+ endLine: endIndex,
7371
+ nextOffset: endIndex < lines.length ? endIndex : void 0
7372
+ };
7078
7373
  }
7079
7374
  function isLangSmithNotFoundError(error) {
7080
7375
  if (typeof error !== "object" || error === null) return false;
@@ -7553,9 +7848,11 @@ var ContextHubBackend = class ContextHubBackend {
7553
7848
  }
7554
7849
  const content = cache[hubPath];
7555
7850
  if (content === void 0) return { error: `File '${filePath}' not found` };
7556
- const sliced = sliceReadContent(content, offset, limit);
7851
+ const { offset: normalizedOffset, limit: normalizedLimit } = normalizeReadPagination(offset, limit);
7852
+ const sliced = sliceReadContent(content, normalizedOffset, normalizedLimit);
7557
7853
  if (sliced.error) return { error: sliced.error };
7558
7854
  return {
7855
+ ...sliced,
7559
7856
  content: sliced.content ?? "",
7560
7857
  mimeType: TEXT_MIME_TYPE
7561
7858
  };
@@ -7894,6 +8191,7 @@ function buildFindCommand(searchPath) {
7894
8191
  const findBase = `find -L ${quotedPath} ${`\\( -path /proc -o -path /sys -o -path /dev -o -path /run -o -path ${quotedPath}/proc -o -path ${quotedPath}/sys -o -path ${quotedPath}/dev -o -path ${quotedPath}/run \\) -prune`} -o -not -path ${quotedPath}`;
7895
8192
  return `{ ${`if find /dev/null -maxdepth 0 -printf '' 2>/dev/null; then ${findBase} -printf '%s\\t%T@\\t%y\\t%p\\n' 2>/dev/null; elif stat -c %s /dev/null >/dev/null 2>&1; then ${findBase} -exec sh -c '${STAT_C_SCRIPT}' _ {} +; else ${findBase} -exec stat -f '%z\t%m\t%Sp\t%N' {} + 2>/dev/null; fi || true`}; } | head -n 50001`;
7896
8193
  }
8194
+ const READ_METADATA_PREFIX = "__DEEPAGENTS_READ_METADATA__";
7897
8195
  /**
7898
8196
  * Pure POSIX shell command for reading files with line numbers.
7899
8197
  * Uses awk for line numbering with offset/limit — works on any Linux including Alpine.
@@ -7907,9 +8205,32 @@ function buildReadCommand(filePath, offset, limit) {
7907
8205
  return [
7908
8206
  `if [ ! -f ${quotedPath} ]; then echo "Error: File not found"; exit 1; fi`,
7909
8207
  `if [ ! -s ${quotedPath} ]; then echo "System reminder: File exists but has empty contents"; exit 0; fi`,
7910
- `awk 'NR >= ${start} && NR <= ${end} { printf "%6d\\t%s\\n", NR, $0 }' ${quotedPath}`
8208
+ `awk 'NR >= ${start} && NR <= ${end} { printf "%6d\\t%s\\n", NR, $0 } END { printf "${READ_METADATA_PREFIX}\\t%d\\n", NR }' ${quotedPath}`
7911
8209
  ].join("; ");
7912
8210
  }
8211
+ function parseReadOutput(output, offset, limit) {
8212
+ const rows = output.split("\n");
8213
+ let metadataIndex = -1;
8214
+ for (let index = rows.length - 1; index >= 0; index -= 1) if (rows[index].startsWith(`${READ_METADATA_PREFIX}\t`)) {
8215
+ metadataIndex = index;
8216
+ break;
8217
+ }
8218
+ if (metadataIndex === -1) return { content: output };
8219
+ const totalLines = Number(rows[metadataIndex].slice(29));
8220
+ if (!Number.isSafeInteger(totalLines) || totalLines < 0) return { content: output };
8221
+ const contentRows = rows.slice(0, metadataIndex);
8222
+ const content = contentRows.length > 0 ? `${contentRows.join("\n")}\n` : "";
8223
+ const startOffset = Math.floor(offset);
8224
+ const endOffset = Math.min(startOffset + Math.floor(limit), totalLines);
8225
+ if (startOffset >= totalLines || endOffset <= startOffset) return { content };
8226
+ return {
8227
+ content,
8228
+ totalLines,
8229
+ startLine: startOffset + 1,
8230
+ endLine: endOffset,
8231
+ nextOffset: endOffset < totalLines ? endOffset : void 0
8232
+ };
8233
+ }
7913
8234
  /**
7914
8235
  * Build a grep command for literal (fixed-string) search.
7915
8236
  * Uses grep -rHnF for recursive, with-filename, with-line-number, fixed-string search.
@@ -7987,15 +8308,17 @@ var BaseSandbox = class {
7987
8308
  mimeType
7988
8309
  };
7989
8310
  }
7990
- if (limit === 0) return {
8311
+ const { offset: normalizedOffset, limit: normalizedLimit } = normalizeReadPagination(offset, limit);
8312
+ if (normalizedLimit === 0) return {
7991
8313
  content: "",
7992
8314
  mimeType
7993
8315
  };
7994
- const command = buildReadCommand(filePath, offset, limit);
8316
+ const command = buildReadCommand(filePath, normalizedOffset, normalizedLimit);
7995
8317
  const result = await this.execute(command);
7996
8318
  if (result.exitCode !== 0) return { error: `File '${filePath}' not found` };
8319
+ const parsed = parseReadOutput(result.output, normalizedOffset, normalizedLimit);
7997
8320
  return {
7998
- content: result.output,
8321
+ ...result.truncated ? { content: parsed.content } : parsed,
7999
8322
  mimeType
8000
8323
  };
8001
8324
  }
@@ -8712,6 +9035,12 @@ Object.defineProperty(exports, "isTextMimeType", {
8712
9035
  return isTextMimeType;
8713
9036
  }
8714
9037
  });
9038
+ Object.defineProperty(exports, "normalizeReadPagination", {
9039
+ enumerable: true,
9040
+ get: function() {
9041
+ return normalizeReadPagination;
9042
+ }
9043
+ });
8715
9044
  Object.defineProperty(exports, "parseHarnessProfileConfig", {
8716
9045
  enumerable: true,
8717
9046
  get: function() {
@@ -8743,4 +9072,4 @@ Object.defineProperty(exports, "serializeProfile", {
8743
9072
  }
8744
9073
  });
8745
9074
 
8746
- //# sourceMappingURL=langsmith-Ck9t7AGW.cjs.map
9075
+ //# sourceMappingURL=langsmith-Ynj9VKxb.cjs.map