deepagents 1.12.2 → 1.12.4

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.
@@ -302,13 +302,13 @@ function performStringReplacement(content, oldString, newString, replaceAll) {
302
302
  function truncateIfTooLong(result) {
303
303
  if (Array.isArray(result)) {
304
304
  const totalChars = result.reduce((sum, item) => sum + item.length, 0);
305
- if (totalChars > 2e4 * 4) {
305
+ if (totalChars > 8e4) {
306
306
  const truncateAt = Math.floor(result.length * TOOL_RESULT_TOKEN_LIMIT * 4 / totalChars);
307
307
  return [...result.slice(0, truncateAt), TRUNCATION_GUIDANCE];
308
308
  }
309
309
  return result;
310
310
  }
311
- if (result.length > 2e4 * 4) return result.substring(0, TOOL_RESULT_TOKEN_LIMIT * 4) + "\n... [results truncated, try being more specific with your parameters]";
311
+ if (result.length > 8e4) return result.substring(0, TOOL_RESULT_TOKEN_LIMIT * 4) + "\n... [results truncated, try being more specific with your parameters]";
312
312
  return result;
313
313
  }
314
314
  /**
@@ -401,6 +401,30 @@ function globSearchFiles(files, pattern, path = "/") {
401
401
  return matches.map(([fp]) => fp).join("\n");
402
402
  }
403
403
  /**
404
+ * Format grep search results based on output mode.
405
+ *
406
+ * @param results - Dictionary mapping file paths to list of [line_num, line_content] tuples
407
+ * @param outputMode - Output format - "files_with_matches", "content", or "count"
408
+ * @returns Formatted string output
409
+ */
410
+ function formatGrepResults(results, outputMode) {
411
+ if (outputMode === "files_with_matches") return Object.keys(results).sort().join("\n");
412
+ if (outputMode === "count") {
413
+ const lines = [];
414
+ for (const filePath of Object.keys(results).sort()) {
415
+ const count = results[filePath].length;
416
+ lines.push(`${filePath}: ${count}`);
417
+ }
418
+ return lines.join("\n");
419
+ }
420
+ const lines = [];
421
+ for (const filePath of Object.keys(results).sort()) {
422
+ lines.push(`${filePath}:`);
423
+ for (const [lineNum, line] of results[filePath]) lines.push(` ${lineNum}: ${line}`);
424
+ }
425
+ return lines.join("\n");
426
+ }
427
+ /**
404
428
  * Return structured grep matches from an in-memory files mapping.
405
429
  *
406
430
  * Performs literal text search (not regex). Binary files are skipped.
@@ -431,6 +455,24 @@ function grepMatchesFromFiles(files, pattern, path = null, glob = null) {
431
455
  return matches;
432
456
  }
433
457
  /**
458
+ * Group structured matches into the legacy dict form used by formatters.
459
+ */
460
+ function buildGrepResultsDict(matches) {
461
+ const grouped = {};
462
+ for (const m of matches) {
463
+ if (!grouped[m.path]) grouped[m.path] = [];
464
+ grouped[m.path].push([m.line, m.text]);
465
+ }
466
+ return grouped;
467
+ }
468
+ /**
469
+ * Format structured grep matches using existing formatting logic.
470
+ */
471
+ function formatGrepMatches(matches, outputMode) {
472
+ if (matches.length === 0) return "No matches found";
473
+ return formatGrepResults(buildGrepResultsDict(matches), outputMode);
474
+ }
475
+ /**
434
476
  * Determine MIME type from a file path's extension.
435
477
  *
436
478
  * Defaults to "text/plain" for unknown extensions. Only the known non-text
@@ -1421,7 +1463,7 @@ const TOOLS_EXCLUDED_FROM_EVICTION = FILESYSTEM_TOOL_NAMES.filter((name) => name
1421
1463
  * Base64-encoded content is ~33% larger, so 10MB raw ≈ 13.3MB in context.
1422
1464
  * This keeps inline multimodal payloads within all major provider limits.
1423
1465
  */
1424
- const MAX_BINARY_READ_SIZE_BYTES = 10 * 1024 * 1024;
1466
+ const MAX_BINARY_READ_SIZE_BYTES = 10485760;
1425
1467
  /**
1426
1468
  * Template for truncation message in read_file.
1427
1469
  * {file_path} will be filled in at runtime.
@@ -1510,8 +1552,9 @@ function buildEvictedHumanContent(message, replacementText) {
1510
1552
  */
1511
1553
  function buildTruncatedHumanMessage(message, filePath) {
1512
1554
  const contentSample = createContentPreview(extractTextFromMessage(message));
1555
+ const evictedContent = buildEvictedHumanContent(message, TOO_LARGE_HUMAN_MSG.replace("{file_path}", filePath).replace("{content_sample}", contentSample));
1513
1556
  return new HumanMessage({
1514
- content: buildEvictedHumanContent(message, TOO_LARGE_HUMAN_MSG.replace("{file_path}", filePath).replace("{content_sample}", contentSample)),
1557
+ content: evictedContent,
1515
1558
  id: message.id,
1516
1559
  additional_kwargs: { ...message.additional_kwargs },
1517
1560
  response_metadata: { ...message.response_metadata }
@@ -1785,7 +1828,7 @@ function createReadFileTool(backend, options) {
1785
1828
  const sizeBytes = Math.ceil(base64Data.length * 3 / 4);
1786
1829
  if (sizeBytes > 10485760) return [{
1787
1830
  type: "text",
1788
- 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)`
1831
+ text: `Error: file too large to read (${Math.round(sizeBytes / 1048576)}MB exceeds ${MAX_BINARY_READ_SIZE_BYTES / 1048576}MB limit for binary files)`
1789
1832
  }];
1790
1833
  if (mimeType.startsWith("image/")) return [{
1791
1834
  type: "image",
@@ -1932,23 +1975,14 @@ function createGrepTool(backend, options) {
1932
1975
  const permissionError = checkPermission(permissions, "read", input.path ?? "/");
1933
1976
  if (permissionError !== void 0) return toolError(runtime, "grep", permissionError);
1934
1977
  const resolvedBackend = await resolveBackend(backend, runtime);
1935
- const { pattern, path = "/", glob = null } = input;
1978
+ const { pattern, path = "/", glob = null, output_mode = "content" } = input;
1936
1979
  const maxCount = input.max_count ?? grepMaxCount;
1937
1980
  const result = await resolvedBackend.grep(pattern, path, glob, maxCount);
1938
1981
  if (result.error) return result.error;
1939
1982
  const matches = filterByPermissions(result.matches ?? [], permissions, "read", (m) => m.path);
1940
1983
  if (matches.length === 0) return `No matches found for pattern '${pattern}'`;
1941
- const lines = [];
1942
- let currentFile = null;
1943
- for (const match of matches) {
1944
- if (match.path !== currentFile) {
1945
- currentFile = match.path;
1946
- lines.push(`\n${currentFile}:`);
1947
- }
1948
- lines.push(` ${match.line}: ${match.text}`);
1949
- }
1950
- const truncated = truncateIfTooLong(lines);
1951
- let content = Array.isArray(truncated) ? truncated.join("\n") : truncated;
1984
+ const truncated = truncateIfTooLong(formatGrepMatches(matches, output_mode));
1985
+ let content = typeof truncated === "string" ? truncated : truncated.join("\n");
1952
1986
  if (result.truncated) content += `\n\n${GREP_TRUNCATION_NOTE}`;
1953
1987
  return content;
1954
1988
  }, {
@@ -1958,7 +1992,12 @@ function createGrepTool(backend, options) {
1958
1992
  pattern: z.string().describe("Literal text pattern to search for (not regex)"),
1959
1993
  path: z.string().optional().default("/").describe("Base path to search from (default: /)"),
1960
1994
  glob: z.string().optional().nullable().default(null).describe("Optional glob pattern to filter files (e.g., '*.py')"),
1961
- max_count: z.number().int().positive().optional().nullable().default(null).describe("Optional cap on the total number of matches returned across all files. Leave unset to use the configured default. When the cap is hit, results are truncated and a note says so; narrow the pattern or path to see the rest.")
1995
+ max_count: z.coerce.number().int().positive().optional().nullable().default(null).describe("Optional cap on the total number of matches returned across all files. Leave unset to use the configured default. When the cap is hit, results are truncated and a note says so; narrow the pattern or path to see the rest."),
1996
+ output_mode: z.enum([
1997
+ "files_with_matches",
1998
+ "content",
1999
+ "count"
2000
+ ]).optional().default("content").describe("Output format: 'files_with_matches' lists matching file paths, 'content' shows matching lines (default), 'count' shows match counts per file")
1962
2001
  })
1963
2002
  });
1964
2003
  }
@@ -2095,9 +2134,10 @@ function createFilesystemMiddleware(options = {}) {
2095
2134
  const evictPath = `/large_tool_results/${sanitizeToolCallId(fallbackToolCallId || msg.tool_call_id)}.txt`;
2096
2135
  const writeResult = await resolvedBackend.write(evictPath, textContent);
2097
2136
  const contentSample = createContentPreview(textContent);
2137
+ const replacementText = writeResult.error ? `Tool result too large, but the result could not be saved to the filesystem: ${writeResult.error}` : TOO_LARGE_TOOL_MSG.replace("{tool_call_id}", msg.tool_call_id).replace("{file_path}", evictPath).replace("{content_sample}", contentSample);
2098
2138
  return {
2099
2139
  message: new ToolMessage({
2100
- content: writeResult.error ? `Tool result too large, but the result could not be saved to the filesystem: ${writeResult.error}` : TOO_LARGE_TOOL_MSG.replace("{tool_call_id}", msg.tool_call_id).replace("{file_path}", evictPath).replace("{content_sample}", contentSample),
2140
+ content: replacementText,
2101
2141
  tool_call_id: msg.tool_call_id,
2102
2142
  name: msg.name,
2103
2143
  id: msg.id,
@@ -2214,23 +2254,18 @@ const SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY = "__deepagents_subagent_response_form
2214
2254
  */
2215
2255
  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.";
2216
2256
  /**
2217
- * State keys that are excluded when passing state to subagents and when returning
2218
- * updates from subagents.
2219
- *
2220
- * When returning updates:
2221
- * 1. The messages key is handled explicitly to ensure only the final message is included
2222
- * 2. The todos and structuredResponse keys are excluded as they do not have a defined reducer
2223
- * and no clear meaning for returning them from a subagent to the main agent.
2224
- * 3. The skillsMetadata and memoryContents keys are automatically excluded from subagent output
2225
- * to prevent parent state from leaking to child agents. Each agent loads its own skills/memory
2226
- * independently based on its middleware configuration.
2257
+ * State keys excluded when passing state to subagents and when returning
2258
+ * updates from subagents. Summarization keys are excluded because their
2259
+ * cutoffIndex is only valid against the message list it was computed from.
2227
2260
  */
2228
2261
  const EXCLUDED_STATE_KEYS = [
2229
2262
  "messages",
2230
2263
  "todos",
2231
2264
  "structuredResponse",
2232
2265
  "skillsMetadata",
2233
- "memoryContents"
2266
+ "memoryContents",
2267
+ "_summarizationEvent",
2268
+ "_summarizationSessionId"
2234
2269
  ];
2235
2270
  /**
2236
2271
  * Default description for the general-purpose subagent.
@@ -2442,6 +2477,7 @@ function createTaskTool(options) {
2442
2477
  }
2443
2478
  return subagentGraphs[subagentType];
2444
2479
  }
2480
+ const finalTaskDescription = taskDescription ? taskDescription : getTaskToolDescription(subagentDescriptions);
2445
2481
  return tool(async (input, config) => {
2446
2482
  const { description, subagent_type } = input;
2447
2483
  if (!(subagent_type in subagentGraphs)) {
@@ -2451,6 +2487,7 @@ function createTaskTool(options) {
2451
2487
  const subagent = selectSubagent(subagent_type, config);
2452
2488
  const subagentState = filterStateForSubagent(getCurrentTaskInput());
2453
2489
  subagentState.messages = [new HumanMessage$1({ content: description })];
2490
+ subagentState._summarizationSessionId = `session_${crypto.randomUUID().substring(0, 8)}`;
2454
2491
  const subagentConfig = {
2455
2492
  ...config,
2456
2493
  metadata: {
@@ -2477,7 +2514,7 @@ function createTaskTool(options) {
2477
2514
  return returnCommandWithStateUpdate(result, config.toolCall.id);
2478
2515
  }, {
2479
2516
  name: "task",
2480
- description: taskDescription ? taskDescription : getTaskToolDescription(subagentDescriptions),
2517
+ description: finalTaskDescription,
2481
2518
  schema: z.object({
2482
2519
  description: z.string().describe("The task to execute with the selected agent"),
2483
2520
  subagent_type: z.string().describe(`Name of the agent to use. Available: ${Object.keys(subagentGraphs).join(", ")}`)
@@ -2489,18 +2526,19 @@ function createTaskTool(options) {
2489
2526
  */
2490
2527
  function createSubAgentMiddleware(options) {
2491
2528
  const { defaultModel, defaultTools = [], defaultMiddleware = null, generalPurposeMiddleware = null, defaultInterruptOn = null, subagents = [], systemPrompt = null, generalPurposeAgent = true, taskDescription = null } = options;
2529
+ const taskTool = createTaskTool({
2530
+ defaultModel,
2531
+ defaultTools,
2532
+ defaultMiddleware,
2533
+ generalPurposeMiddleware,
2534
+ defaultInterruptOn,
2535
+ subagents,
2536
+ generalPurposeAgent,
2537
+ taskDescription
2538
+ });
2492
2539
  return createMiddleware({
2493
2540
  name: "subAgentMiddleware",
2494
- tools: [createTaskTool({
2495
- defaultModel,
2496
- defaultTools,
2497
- defaultMiddleware,
2498
- generalPurposeMiddleware,
2499
- defaultInterruptOn,
2500
- subagents,
2501
- generalPurposeAgent,
2502
- taskDescription
2503
- })],
2541
+ tools: [taskTool],
2504
2542
  wrapModelCall: async (request, handler) => {
2505
2543
  if (systemPrompt !== null) return handler({
2506
2544
  ...request,
@@ -2690,6 +2728,43 @@ function isAnthropicModel(model) {
2690
2728
  return model.getName() === "ChatAnthropic";
2691
2729
  }
2692
2730
  /**
2731
+ * A one-shot promise whose settlement is controlled externally.
2732
+ *
2733
+ * Use this when one part of a workflow must wait for an event that is owned
2734
+ * elsewhere—for example, a queued mutation waiting for the worker that will
2735
+ * push it. `Deferred` is awaitable because it implements `PromiseLike`, and
2736
+ * `.promise` is available when a concrete `Promise` is required.
2737
+ *
2738
+ * The first call to `resolve` or `reject` wins; later calls are ignored. This
2739
+ * class deliberately does not provide cancellation, reset, or notification
2740
+ * semantics. It models exactly one eventual outcome.
2741
+ */
2742
+ var Deferred = class {
2743
+ promise;
2744
+ settled = false;
2745
+ resolvePromise;
2746
+ rejectPromise;
2747
+ constructor() {
2748
+ this.promise = new Promise((resolve, reject) => {
2749
+ this.resolvePromise = resolve;
2750
+ this.rejectPromise = reject;
2751
+ });
2752
+ }
2753
+ resolve(value) {
2754
+ if (this.settled) return;
2755
+ this.settled = true;
2756
+ this.resolvePromise(value);
2757
+ }
2758
+ reject(reason) {
2759
+ if (this.settled) return;
2760
+ this.settled = true;
2761
+ this.rejectPromise(reason);
2762
+ }
2763
+ then(onfulfilled, onrejected) {
2764
+ return this.promise.then(onfulfilled, onrejected);
2765
+ }
2766
+ };
2767
+ /**
2693
2768
  * Detect whether a model is an AWS Bedrock Converse model.
2694
2769
  *
2695
2770
  * Accepts the wider `RunnableInterface` shape (the type of `request.model`
@@ -3005,7 +3080,7 @@ function createMemoryMiddleware(options) {
3005
3080
  * });
3006
3081
  * ```
3007
3082
  */
3008
- const MAX_SKILL_FILE_SIZE = 10 * 1024 * 1024;
3083
+ const MAX_SKILL_FILE_SIZE = 10485760;
3009
3084
  const DEFAULT_SKILL_READ_LINE_LIMIT = 1e3;
3010
3085
  const MAX_SKILL_NAME_LENGTH = 64;
3011
3086
  const MAX_SKILL_DESCRIPTION_LENGTH = 1024;
@@ -3768,7 +3843,6 @@ function createCompletionCallbackMiddleware(options) {
3768
3843
  * from `langchain` directly.
3769
3844
  */
3770
3845
  const DEFAULT_MESSAGES_TO_KEEP = 20;
3771
- const DEFAULT_TRIM_TOKEN_LIMIT = 4e3;
3772
3846
  const FALLBACK_TRIGGER = {
3773
3847
  type: "tokens",
3774
3848
  value: 17e4
@@ -3885,7 +3959,7 @@ function isSummaryMessage(msg) {
3885
3959
  * @returns AgentMiddleware for summarization and history offloading
3886
3960
  */
3887
3961
  function createSummarizationMiddleware(options) {
3888
- const { model, backend, summaryPrompt = DEFAULT_SUMMARY_PROMPT, trimTokensToSummarize = DEFAULT_TRIM_TOKEN_LIMIT, historyPathPrefix = "/conversation_history" } = options;
3962
+ const { model, backend, summaryPrompt = DEFAULT_SUMMARY_PROMPT, trimTokensToSummarize, historyPathPrefix = "/conversation_history" } = options;
3889
3963
  let trigger = options.trigger;
3890
3964
  let keep = options.keep ?? {
3891
3965
  type: "messages",
@@ -4089,7 +4163,9 @@ function createSummarizationMiddleware(options) {
4089
4163
  * This gives a more accurate picture of what actually gets sent to the model.
4090
4164
  */
4091
4165
  function countTotalTokens(messages, systemMessage, tools) {
4092
- return countTokensApproximately(systemMessage && SystemMessage.isInstance(systemMessage) ? [systemMessage, ...messages] : [...messages], tools && Array.isArray(tools) && tools.length > 0 ? tools : null);
4166
+ const countedMessages = systemMessage && SystemMessage.isInstance(systemMessage) ? [systemMessage, ...messages] : [...messages];
4167
+ const toolsArray = tools && Array.isArray(tools) && tools.length > 0 ? tools : null;
4168
+ return countTokensApproximately(countedMessages, toolsArray);
4093
4169
  }
4094
4170
  /**
4095
4171
  * Truncate ToolMessage content so that the total payload fits within the
@@ -4236,7 +4312,8 @@ function createSummarizationMiddleware(options) {
4236
4312
  */
4237
4313
  async function createSummary(messages, chatModel) {
4238
4314
  let messagesToSummarize = messages;
4239
- if (countTokensApproximately(messages) > trimTokensToSummarize) {
4315
+ const tokens = countTokensApproximately(messages);
4316
+ if (trimTokensToSummarize !== void 0 && tokens > trimTokensToSummarize) {
4240
4317
  let kept = 0;
4241
4318
  const trimmedMessages = [];
4242
4319
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -4249,8 +4326,7 @@ function createSummarizationMiddleware(options) {
4249
4326
  }
4250
4327
  const conversation = getBufferString(messagesToSummarize);
4251
4328
  const prompt = summaryPrompt.replace("{conversation}", conversation);
4252
- const response = await chatModel.invoke([new HumanMessage({ content: prompt })]);
4253
- return typeof response.content === "string" ? response.content : JSON.stringify(response.content);
4329
+ return (await chatModel.invoke([new HumanMessage({ content: prompt })])).text;
4254
4330
  }
4255
4331
  /**
4256
4332
  * Build the summary message with file path reference.
@@ -6543,9 +6619,36 @@ var StoreBackend = class {
6543
6619
  /**
6544
6620
  * ContextHubBackend: Store files in a LangSmith Hub agent repo (persistent).
6545
6621
  */
6546
- const URL_COMMIT_SUFFIX_RE = /:([0-9a-f]{8,64})$/i;
6622
+ const CONTEXT_URL_COMMIT_PATH_RE = /^\/context\/([^/]+)\/([0-9a-f]{8})$/;
6623
+ const LEGACY_URL_COMMIT_PATH_RE = /^\/hub\/([^/]+)\/([^/:]+):([0-9a-f]{8})$/;
6624
+ const MUTATION_COALESCE_MS = 50;
6625
+ const MAX_CONFLICT_RETRIES = 3;
6547
6626
  const TEXT_MIME_TYPE = "text/plain";
6548
6627
  const FNMATCH_OPTIONS = { bash: true };
6628
+ function parseHubTargetIdentifier(identifier) {
6629
+ if (!identifier || identifier.split("/").length > 2 || identifier.startsWith("/") || identifier.endsWith("/") || identifier.split(":").length > 2) return null;
6630
+ const [ownerNamePart] = identifier.split(":");
6631
+ if (ownerNamePart.includes("/")) {
6632
+ const [owner, name] = ownerNamePart.split("/", 2);
6633
+ return owner && name ? [owner, name] : null;
6634
+ }
6635
+ return ownerNamePart ? ["-", ownerNamePart] : null;
6636
+ }
6637
+ function parseCommitHashFromUrl(url, identifier) {
6638
+ try {
6639
+ const pathname = decodeURIComponent(new URL(url).pathname);
6640
+ const target = parseHubTargetIdentifier(identifier);
6641
+ if (target === null) return null;
6642
+ const [targetOwner, targetName] = target;
6643
+ const contextMatch = CONTEXT_URL_COMMIT_PATH_RE.exec(pathname);
6644
+ if (contextMatch !== null && contextMatch[1] === targetName) return contextMatch[2];
6645
+ const legacyMatch = LEGACY_URL_COMMIT_PATH_RE.exec(pathname);
6646
+ if (legacyMatch !== null && legacyMatch[1] === targetOwner && legacyMatch[2] === targetName) return legacyMatch[3];
6647
+ return null;
6648
+ } catch {
6649
+ return null;
6650
+ }
6651
+ }
6549
6652
  function getErrorMessage(error) {
6550
6653
  if (typeof error === "string") return error;
6551
6654
  if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") return error.message;
@@ -6584,6 +6687,12 @@ function getLangSmithStatus(error) {
6584
6687
  const maybeError = error;
6585
6688
  if (typeof maybeError.status === "number") return maybeError.status;
6586
6689
  }
6690
+ function createLangSmithConflictError(message) {
6691
+ const error = new Error(message);
6692
+ error.name = "LangSmithConflictError";
6693
+ error.status = 409;
6694
+ return error;
6695
+ }
6587
6696
  function mapHubFileOperationError(error) {
6588
6697
  const status = getLangSmithStatus(error);
6589
6698
  if (status === 401 || status === 403) return "permission_denied";
@@ -6593,12 +6702,49 @@ function mapHubFileOperationError(error) {
6593
6702
  /**
6594
6703
  * Backend that stores files in a LangSmith Hub agent repo (persistent).
6595
6704
  */
6705
+ /**
6706
+ * Backend that stores files in a LangSmith Hub agent repository.
6707
+ *
6708
+ * ## Mutation model
6709
+ *
6710
+ * Mutations are accepted in call order, coalesced for a short window, and
6711
+ * pushed by one worker. Only one batch is in flight at a time; mutations that
6712
+ * arrive during a push form the next batch. This serializes one backend
6713
+ * instance's writes while still reducing the number of Hub commits.
6714
+ *
6715
+ * Reads use an optimistic view: the last durable cache overlaid with the
6716
+ * in-flight batch and then the pending batch. A read can therefore observe an
6717
+ * accepted mutation before it is durable; a failed push invalidates that view
6718
+ * and the next operation reloads from Hub.
6719
+ *
6720
+ * A `409` parent conflict triggers an authoritative pull and rematerializes
6721
+ * the in-flight batch over the fetched tree before retrying. Edits replay their
6722
+ * original replacement intent; absolute writes, deletes, and uploads replay as
6723
+ * absolute changes. Retries are bounded by `MAX_CONFLICT_RETRIES`.
6724
+ */
6596
6725
  var ContextHubBackend = class ContextHubBackend {
6597
6726
  identifier;
6598
6727
  client;
6728
+ /** Last durable Hub file state; `null` means the next access must load it. */
6599
6729
  cache = null;
6600
6730
  linkedEntries = {};
6731
+ /** Parent hash for the durable cache, used for optimistic-concurrency pushes. */
6601
6732
  commitHash = null;
6733
+ /** Shared cold-load promise so concurrent first operations perform one pull. */
6734
+ loadPromise = null;
6735
+ /** Promise chain serializing mutation acceptance and optimistic projections. */
6736
+ mutationOrder = Promise.resolve();
6737
+ /** Mutations accepted for the next coalesced push. */
6738
+ pendingBatch = null;
6739
+ /** The batch currently submitted to Hub and visible to optimistic reads. */
6740
+ inFlightBatch = null;
6741
+ /** The single queue-draining worker, when active. */
6742
+ workerPromise = null;
6743
+ /**
6744
+ * Blocks cache consumers while a successful push without a parseable commit
6745
+ * hash is being confirmed by an authoritative pull.
6746
+ */
6747
+ snapshotPublication = null;
6602
6748
  constructor(identifier, options = {}) {
6603
6749
  this.identifier = identifier;
6604
6750
  this.client = options.client ?? new Client$1();
@@ -6609,49 +6755,319 @@ var ContextHubBackend = class ContextHubBackend {
6609
6755
  static toHubUnavailableError(error) {
6610
6756
  return `Hub unavailable: ${getErrorMessage(error)}`;
6611
6757
  }
6612
- async loadTree() {
6758
+ async fetchTree() {
6613
6759
  let context;
6614
6760
  try {
6615
6761
  context = await this.client.pullAgent(this.identifier);
6616
6762
  } catch (error) {
6617
- if (isLangSmithNotFoundError(error)) {
6618
- this.cache = {};
6619
- this.linkedEntries = {};
6620
- this.commitHash = null;
6621
- return;
6622
- }
6763
+ if (isLangSmithNotFoundError(error)) return {
6764
+ cache: {},
6765
+ linkedEntries: {},
6766
+ commitHash: null
6767
+ };
6623
6768
  throw error;
6624
6769
  }
6625
- this.commitHash = context.commit_hash;
6626
- this.cache = {};
6627
- this.linkedEntries = {};
6628
- for (const [path, entry] of Object.entries(context.files)) if (entry.type === "file") this.cache[path] = entry.content;
6629
- else if ((entry.type === "agent" || entry.type === "skill") && typeof entry.repo_handle === "string") this.linkedEntries[path] = entry.repo_handle;
6770
+ const cache = {};
6771
+ const linkedEntries = {};
6772
+ for (const [path, entry] of Object.entries(context.files)) if (entry.type === "file") cache[path] = entry.content;
6773
+ else if ((entry.type === "agent" || entry.type === "skill") && typeof entry.repo_handle === "string") linkedEntries[path] = entry.repo_handle;
6774
+ return {
6775
+ cache,
6776
+ linkedEntries,
6777
+ commitHash: context.commit_hash
6778
+ };
6630
6779
  }
6631
- async ensureCache() {
6632
- if (this.cache === null) await this.loadTree();
6780
+ publishSnapshot(snapshot) {
6781
+ this.cache = snapshot.cache;
6782
+ this.linkedEntries = snapshot.linkedEntries;
6783
+ this.commitHash = snapshot.commitHash;
6784
+ }
6785
+ async loadTree() {
6786
+ this.publishSnapshot(await this.fetchTree());
6787
+ }
6788
+ beginSnapshotPublication() {
6789
+ if (this.snapshotPublication !== null) throw new Error("Context Hub snapshot publication is already pending");
6790
+ this.snapshotPublication = new Deferred();
6791
+ }
6792
+ finishSnapshotPublication() {
6793
+ const publication = this.snapshotPublication;
6794
+ this.snapshotPublication = null;
6795
+ publication?.resolve();
6796
+ }
6797
+ async ensureCacheLoaded() {
6798
+ while (this.snapshotPublication !== null) await this.snapshotPublication;
6799
+ if (this.cache === null) {
6800
+ let loadPromise = this.loadPromise;
6801
+ if (loadPromise === null) {
6802
+ loadPromise = this.loadTree();
6803
+ this.loadPromise = loadPromise;
6804
+ }
6805
+ try {
6806
+ await loadPromise;
6807
+ } finally {
6808
+ if (this.loadPromise === loadPromise) this.loadPromise = null;
6809
+ }
6810
+ }
6633
6811
  if (this.cache === null) throw new Error("Context Hub cache failed to initialize");
6634
- return this.cache;
6635
- }
6636
- async commit(changes) {
6637
- if (Object.keys(changes).length === 0) return;
6638
- const payload = {};
6639
- for (const [path, content] of Object.entries(changes)) payload[path] = content === null ? null : {
6640
- type: "file",
6641
- content
6812
+ }
6813
+ async ensureCache() {
6814
+ await this.ensureCacheLoaded();
6815
+ return this.visibleCache();
6816
+ }
6817
+ static applyChanges(cache, changes) {
6818
+ const next = { ...cache };
6819
+ for (const [path, content] of Object.entries(changes)) if (content === null) delete next[path];
6820
+ else next[path] = content;
6821
+ return next;
6822
+ }
6823
+ /**
6824
+ * Build the read-your-writes view without publishing speculative data as the
6825
+ * durable cache. Later batches overlay earlier ones, matching worker order.
6826
+ */
6827
+ visibleCache() {
6828
+ let visible = { ...this.cache ?? {} };
6829
+ if (this.inFlightBatch !== null) visible = ContextHubBackend.applyChanges(visible, this.inFlightBatch.changes);
6830
+ if (this.pendingBatch !== null) visible = ContextHubBackend.applyChanges(visible, this.pendingBatch.changes);
6831
+ return visible;
6832
+ }
6833
+ invalidateCache() {
6834
+ this.cache = null;
6835
+ this.linkedEntries = {};
6836
+ this.commitHash = null;
6837
+ this.loadPromise = null;
6838
+ }
6839
+ async acquireMutationTurn() {
6840
+ let release;
6841
+ const previous = this.mutationOrder;
6842
+ this.mutationOrder = new Promise((resolve) => {
6843
+ release = resolve;
6844
+ });
6845
+ await previous;
6846
+ return release;
6847
+ }
6848
+ /**
6849
+ * Serialize validation and enqueueing so each operation is evaluated against
6850
+ * a stable optimistic projection. Cache loading begins before acquiring the
6851
+ * turn, allowing concurrent cold-start callers to share the same pull.
6852
+ */
6853
+ async acceptMutation(operation) {
6854
+ const turn = this.acquireMutationTurn();
6855
+ const cacheOutcome = this.ensureCacheLoaded().then(() => ({ loaded: true }), (error) => ({
6856
+ loaded: false,
6857
+ error
6858
+ }));
6859
+ const release = await turn;
6860
+ try {
6861
+ const outcome = await cacheOutcome;
6862
+ if (!outcome.loaded) throw outcome.error;
6863
+ while (this.cache === null) await this.ensureCacheLoaded();
6864
+ return operation(this.visibleCache());
6865
+ } finally {
6866
+ release();
6867
+ }
6868
+ }
6869
+ /**
6870
+ * Start a batch's coalescing window. The worker waits for this signal before
6871
+ * detaching the batch; cancellation resolves it immediately so failures do
6872
+ * not leave the worker waiting on a timer.
6873
+ */
6874
+ createMutationBatch() {
6875
+ const batch = {
6876
+ changes: {},
6877
+ waiters: [],
6878
+ ready: new Deferred(),
6879
+ timer: null
6642
6880
  };
6643
- const url = await this.client.pushAgent(this.identifier, {
6644
- files: payload,
6645
- ...this.commitHash ? { parentCommit: this.commitHash } : {}
6881
+ batch.timer = setTimeout(() => {
6882
+ batch.timer = null;
6883
+ batch.ready.resolve();
6884
+ }, MUTATION_COALESCE_MS);
6885
+ return batch;
6886
+ }
6887
+ cancelBatchTimer(batch) {
6888
+ if (batch.timer !== null) {
6889
+ clearTimeout(batch.timer);
6890
+ batch.timer = null;
6891
+ }
6892
+ batch.ready.resolve();
6893
+ }
6894
+ enqueueCommit(changes, intent = {
6895
+ kind: "changes",
6896
+ changes: { ...changes }
6897
+ }) {
6898
+ if (Object.keys(changes).length === 0) return Promise.resolve();
6899
+ let batch = this.pendingBatch;
6900
+ if (batch === null) {
6901
+ batch = this.createMutationBatch();
6902
+ this.pendingBatch = batch;
6903
+ }
6904
+ Object.assign(batch.changes, changes);
6905
+ const completion = new Deferred();
6906
+ batch.waiters.push({
6907
+ intent,
6908
+ completion
6646
6909
  });
6647
- const match = URL_COMMIT_SUFFIX_RE.exec(url);
6648
- if (match) this.commitHash = match[1];
6649
- if (this.cache !== null) {
6650
- const deletions = new Set(Object.entries(changes).filter(([, content]) => content === null).map(([path]) => path));
6651
- const updates = Object.fromEntries(Object.entries(changes).filter((entry) => entry[1] !== null));
6652
- this.cache = {
6653
- ...Object.fromEntries(Object.entries(this.cache).filter(([path]) => !deletions.has(path))),
6654
- ...updates
6910
+ this.startWorker();
6911
+ return completion.promise;
6912
+ }
6913
+ /**
6914
+ * Replay ordered intents over an authoritative base after a conflict. This
6915
+ * rebuilds the push payload and optimistic overlay. An edit that no longer
6916
+ * applies throws the supplied conflict error; absolute changes are reapplied.
6917
+ */
6918
+ rematerializeBatch(batch, base, conflictError) {
6919
+ let cache = { ...base };
6920
+ const changes = {};
6921
+ for (const waiter of batch.waiters) {
6922
+ const { intent } = waiter;
6923
+ if (intent.kind === "changes") {
6924
+ Object.assign(changes, intent.changes);
6925
+ cache = ContextHubBackend.applyChanges(cache, intent.changes);
6926
+ continue;
6927
+ }
6928
+ const current = cache[intent.path];
6929
+ if (current === void 0) throw conflictError;
6930
+ const replacementResult = performStringReplacement(current, intent.oldString, intent.newString, intent.replaceAll);
6931
+ if (typeof replacementResult === "string") throw conflictError;
6932
+ const [newContent, occurrences] = replacementResult;
6933
+ const editChanges = { [intent.path]: newContent };
6934
+ Object.assign(changes, editChanges);
6935
+ cache = ContextHubBackend.applyChanges(cache, editChanges);
6936
+ intent.updateOccurrences(occurrences);
6937
+ }
6938
+ batch.changes = changes;
6939
+ return cache;
6940
+ }
6941
+ rematerializeAfterConflict(batch, snapshot, conflictError) {
6942
+ const cache = this.rematerializeBatch(batch, snapshot.cache, conflictError);
6943
+ let pendingReplayError = null;
6944
+ if (this.pendingBatch !== null) try {
6945
+ this.rematerializeBatch(this.pendingBatch, cache, conflictError);
6946
+ } catch (error) {
6947
+ if (error !== conflictError) throw error;
6948
+ pendingReplayError = error;
6949
+ }
6950
+ this.publishSnapshot(snapshot);
6951
+ if (pendingReplayError !== null) this.failPendingBatch(pendingReplayError);
6952
+ }
6953
+ rematerializePendingBatch(snapshot) {
6954
+ if (this.pendingBatch === null) return null;
6955
+ const conflictError = createLangSmithConflictError("Pending Context Hub mutation conflicts with authoritative state");
6956
+ try {
6957
+ this.rematerializeBatch(this.pendingBatch, snapshot.cache, conflictError);
6958
+ return null;
6959
+ } catch (error) {
6960
+ if (error !== conflictError) throw error;
6961
+ return conflictError;
6962
+ }
6963
+ }
6964
+ startWorker() {
6965
+ if (this.workerPromise !== null) return;
6966
+ const worker = this.drainMutationQueue().catch((error) => {
6967
+ this.failAllBatches(error);
6968
+ }).finally(() => {
6969
+ if (this.workerPromise === worker) {
6970
+ this.workerPromise = null;
6971
+ if (this.pendingBatch !== null) this.startWorker();
6972
+ }
6973
+ });
6974
+ this.workerPromise = worker;
6975
+ }
6976
+ /**
6977
+ * Drain coalesced batches sequentially. A completed batch publishes durable
6978
+ * state before settling its callers; a failed batch invalidates local state
6979
+ * and rejects both in-flight and queued callers so the next mutation reloads.
6980
+ */
6981
+ async drainMutationQueue() {
6982
+ while (this.pendingBatch !== null) {
6983
+ const batch = this.pendingBatch;
6984
+ await batch.ready;
6985
+ if (this.pendingBatch !== batch) continue;
6986
+ this.pendingBatch = null;
6987
+ this.inFlightBatch = batch;
6988
+ let pendingReplayError = null;
6989
+ try {
6990
+ const result = await this.pushBatch(batch);
6991
+ if (result.kind === "snapshot") {
6992
+ pendingReplayError = this.rematerializePendingBatch(result.snapshot);
6993
+ this.publishSnapshot(result.snapshot);
6994
+ } else {
6995
+ this.cache = ContextHubBackend.applyChanges(this.cache ?? {}, batch.changes);
6996
+ this.commitHash = result.commitHash;
6997
+ }
6998
+ } catch (error) {
6999
+ this.inFlightBatch = null;
7000
+ this.invalidateCache();
7001
+ this.finishSnapshotPublication();
7002
+ for (const waiter of batch.waiters) waiter.completion.reject(error);
7003
+ this.failPendingBatch(error);
7004
+ return;
7005
+ }
7006
+ this.inFlightBatch = null;
7007
+ this.finishSnapshotPublication();
7008
+ for (const waiter of batch.waiters) waiter.completion.resolve();
7009
+ if (pendingReplayError !== null) {
7010
+ this.failPendingBatch(pendingReplayError);
7011
+ return;
7012
+ }
7013
+ }
7014
+ }
7015
+ failPendingBatch(error) {
7016
+ const pending = this.pendingBatch;
7017
+ if (pending === null) return;
7018
+ this.pendingBatch = null;
7019
+ this.cancelBatchTimer(pending);
7020
+ for (const waiter of pending.waiters) waiter.completion.reject(error);
7021
+ }
7022
+ failAllBatches(error) {
7023
+ const inFlight = this.inFlightBatch;
7024
+ this.inFlightBatch = null;
7025
+ this.invalidateCache();
7026
+ this.finishSnapshotPublication();
7027
+ if (inFlight !== null) {
7028
+ this.cancelBatchTimer(inFlight);
7029
+ for (const waiter of inFlight.waiters) waiter.completion.reject(error);
7030
+ }
7031
+ this.failPendingBatch(error);
7032
+ }
7033
+ /**
7034
+ * Push a materialized batch with the durable commit as its parent. On a 409,
7035
+ * refresh Hub state, replay the batch, and retry with the new parent. A push
7036
+ * response without a trustworthy hash is confirmed by a pull before callers
7037
+ * are allowed to observe it as durable.
7038
+ */
7039
+ async pushBatch(batch) {
7040
+ for (let attempt = 0;; attempt += 1) {
7041
+ const payload = {};
7042
+ for (const [path, content] of Object.entries(batch.changes)) payload[path] = content === null ? null : {
7043
+ type: "file",
7044
+ content
7045
+ };
7046
+ let url;
7047
+ try {
7048
+ url = await this.client.pushAgent(this.identifier, {
7049
+ files: payload,
7050
+ ...this.commitHash ? { parentCommit: this.commitHash } : {}
7051
+ });
7052
+ } catch (error) {
7053
+ if (getLangSmithStatus(error) !== 409 || attempt >= MAX_CONFLICT_RETRIES) throw error;
7054
+ const snapshot = await this.fetchTree();
7055
+ this.rematerializeAfterConflict(batch, snapshot, error);
7056
+ continue;
7057
+ }
7058
+ const pushedCommitHash = parseCommitHashFromUrl(url, this.identifier);
7059
+ if (pushedCommitHash === null) {
7060
+ this.beginSnapshotPublication();
7061
+ const snapshot = await this.fetchTree();
7062
+ if (snapshot.commitHash === null) throw new Error("Context Hub commit succeeded but its hash could not be resolved");
7063
+ return {
7064
+ kind: "snapshot",
7065
+ snapshot
7066
+ };
7067
+ }
7068
+ return {
7069
+ kind: "commit",
7070
+ commitHash: pushedCommitHash
6655
7071
  };
6656
7072
  }
6657
7073
  }
@@ -6779,53 +7195,71 @@ var ContextHubBackend = class ContextHubBackend {
6779
7195
  async write(filePath, content) {
6780
7196
  const hubPath = ContextHubBackend.stripPrefix(filePath);
6781
7197
  try {
6782
- await this.ensureCache();
6783
- await this.commit({ [hubPath]: content });
7198
+ const accepted = await this.acceptMutation(() => {
7199
+ return {
7200
+ result: {
7201
+ path: filePath,
7202
+ filesUpdate: null
7203
+ },
7204
+ completion: this.enqueueCommit({ [hubPath]: content })
7205
+ };
7206
+ });
7207
+ await accepted.completion;
7208
+ return accepted.result;
6784
7209
  } catch (error) {
6785
- if (isLangSmithError(error)) {
6786
- this.cache = null;
6787
- return { error: ContextHubBackend.toHubUnavailableError(error) };
6788
- }
7210
+ if (isLangSmithError(error)) return { error: ContextHubBackend.toHubUnavailableError(error) };
6789
7211
  throw error;
6790
7212
  }
6791
- return {
6792
- path: filePath,
6793
- filesUpdate: null
6794
- };
6795
7213
  }
6796
7214
  async edit(filePath, oldString, newString, replaceAll = false) {
6797
7215
  const hubPath = ContextHubBackend.stripPrefix(filePath);
6798
7216
  try {
6799
- const current = (await this.ensureCache())[hubPath];
6800
- if (current === void 0) return { error: `Error: File '${filePath}' not found` };
6801
- const replacementResult = performStringReplacement(current, oldString, newString, replaceAll);
6802
- if (typeof replacementResult === "string") return { error: replacementResult };
6803
- const [newContent, occurrences] = replacementResult;
6804
- await this.commit({ [hubPath]: newContent });
6805
- return {
6806
- path: filePath,
6807
- filesUpdate: null,
6808
- occurrences
6809
- };
7217
+ const accepted = await this.acceptMutation((cache) => {
7218
+ const current = cache[hubPath];
7219
+ if (current === void 0) return { result: { error: `Error: File '${filePath}' not found` } };
7220
+ const replacementResult = performStringReplacement(current, oldString, newString, replaceAll);
7221
+ if (typeof replacementResult === "string") return { result: { error: replacementResult } };
7222
+ const [newContent, occurrences] = replacementResult;
7223
+ const result = {
7224
+ path: filePath,
7225
+ filesUpdate: null,
7226
+ occurrences
7227
+ };
7228
+ return {
7229
+ result,
7230
+ completion: this.enqueueCommit({ [hubPath]: newContent }, {
7231
+ kind: "edit",
7232
+ path: hubPath,
7233
+ oldString,
7234
+ newString,
7235
+ replaceAll,
7236
+ updateOccurrences: (replayedOccurrences) => {
7237
+ result.occurrences = replayedOccurrences;
7238
+ }
7239
+ })
7240
+ };
7241
+ });
7242
+ await accepted.completion;
7243
+ return accepted.result;
6810
7244
  } catch (error) {
6811
- if (isLangSmithError(error)) {
6812
- this.cache = null;
6813
- return { error: ContextHubBackend.toHubUnavailableError(error) };
6814
- }
7245
+ if (isLangSmithError(error)) return { error: ContextHubBackend.toHubUnavailableError(error) };
6815
7246
  throw error;
6816
7247
  }
6817
7248
  }
6818
7249
  async delete(filePath) {
6819
7250
  const hubPath = ContextHubBackend.stripPrefix(filePath);
6820
7251
  try {
6821
- if (!(hubPath in await this.ensureCache())) return { error: `Error: File '${filePath}' not found` };
6822
- await this.commit({ [hubPath]: null });
6823
- return { path: filePath };
7252
+ const accepted = await this.acceptMutation((cache) => {
7253
+ if (!(hubPath in cache)) return { result: { error: `Error: File '${filePath}' not found` } };
7254
+ return {
7255
+ result: { path: filePath },
7256
+ completion: this.enqueueCommit({ [hubPath]: null })
7257
+ };
7258
+ });
7259
+ await accepted.completion;
7260
+ return accepted.result;
6824
7261
  } catch (error) {
6825
- if (isLangSmithError(error)) {
6826
- this.cache = null;
6827
- return { error: ContextHubBackend.toHubUnavailableError(error) };
6828
- }
7262
+ if (isLangSmithError(error)) return { error: ContextHubBackend.toHubUnavailableError(error) };
6829
7263
  throw error;
6830
7264
  }
6831
7265
  }
@@ -6842,13 +7276,15 @@ var ContextHubBackend = class ContextHubBackend {
6842
7276
  }
6843
7277
  let commitError = null;
6844
7278
  if (Object.keys(validFiles).length > 0) try {
6845
- await this.ensureCache();
6846
- await this.commit(validFiles);
7279
+ await (await this.acceptMutation(() => {
7280
+ return {
7281
+ result: null,
7282
+ completion: this.enqueueCommit(validFiles)
7283
+ };
7284
+ })).completion;
6847
7285
  } catch (error) {
6848
- if (isLangSmithError(error)) {
6849
- this.cache = null;
6850
- commitError = mapHubFileOperationError(error);
6851
- } else throw error;
7286
+ if (isLangSmithError(error)) commitError = mapHubFileOperationError(error);
7287
+ else throw error;
6852
7288
  }
6853
7289
  return decoded.map(([path, text]) => {
6854
7290
  if (text === null) return {
@@ -7533,4 +7969,4 @@ var LangSmithSandbox = class LangSmithSandbox extends BaseSandbox {
7533
7969
  //#endregion
7534
7970
  export { filesValue as A, StateBackend as B, createSummarizationMiddleware as C, MAX_SKILL_NAME_LENGTH as D, MAX_SKILL_FILE_SIZE as E, SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY as F, resolveBackend as G, applyGrepMaxCount as H, createSubAgent as I, checkEmptyContent as J, adaptBackendProtocol as K, createSubAgentMiddleware as L, DEFAULT_GENERAL_PURPOSE_DESCRIPTION as M, DEFAULT_SUBAGENT_PROMPT as N, createSkillsMiddleware as O, GENERAL_PURPOSE_SUBAGENT as P, createFilesystemMiddleware as R, computeSummarizationDefaults as S, MAX_SKILL_DESCRIPTION_LENGTH as T, isSandboxBackend as U, SandboxError as V, isSandboxProtocol as W, isTextMimeType as X, getMimeType as Y, performStringReplacement as Z, createHarnessProfile as _, ASYNC_TASK_SYSTEM_PROMPT as a, createAsyncSubAgentMiddleware as b, TASK_SYSTEM_PROMPT as c, registerHarnessProfile as d, generalPurposeSubagentConfigSchema as f, EMPTY_HARNESS_PROFILE as g, serializeProfile as h, StoreBackend as i, createPatchToolCallsMiddleware as j, createMemoryMiddleware as k, createDeepAgent as l, parseHarnessProfileConfig as m, BaseSandbox as n, BASE_AGENT_PROMPT as o, harnessProfileConfigSchema as p, adaptSandboxProtocol as q, ContextHubBackend as r, EXECUTION_SYSTEM_PROMPT as s, LangSmithSandbox as t, getHarnessProfile as u, REQUIRED_MIDDLEWARE_NAMES as v, createCompletionCallbackMiddleware as w, isAsyncSubAgent as x, ConfigurationError as y, CompositeBackend as z };
7535
7971
 
7536
- //# sourceMappingURL=langsmith-b3Dpu8rS.js.map
7972
+ //# sourceMappingURL=langsmith-DRyafCNe.js.map