deepagents 1.7.5 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/index.cjs +949 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +339 -152
- package/dist/index.d.ts +339 -152
- package/dist/index.js +943 -19
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
package/dist/index.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { AIMessage, HumanMessage, SystemMessage, ToolMessage, anthropicPromptCachingMiddleware, createAgent, createMiddleware, humanInTheLoopMiddleware,
|
|
1
|
+
import { AIMessage, HumanMessage, SystemMessage, ToolMessage, anthropicPromptCachingMiddleware, countTokensApproximately, createAgent, createMiddleware, humanInTheLoopMiddleware, todoListMiddleware, tool } from "langchain";
|
|
2
2
|
import { Runnable } from "@langchain/core/runnables";
|
|
3
3
|
import { Command, REMOVE_ALL_MESSAGES, ReducedValue, StateSchema, getCurrentTaskInput, isCommand } from "@langchain/langgraph";
|
|
4
4
|
import { z } from "zod/v4";
|
|
5
5
|
import micromatch from "micromatch";
|
|
6
6
|
import { basename } from "path";
|
|
7
|
-
import { HumanMessage as HumanMessage$1, RemoveMessage } from "@langchain/core/messages";
|
|
7
|
+
import { HumanMessage as HumanMessage$1, RemoveMessage, getBufferString } from "@langchain/core/messages";
|
|
8
8
|
import { z as z$1 } from "zod";
|
|
9
9
|
import yaml from "yaml";
|
|
10
|
-
import "uuid";
|
|
11
|
-
import "langchain/
|
|
10
|
+
import { v4 } from "uuid";
|
|
11
|
+
import { ContextOverflowError } from "@langchain/core/errors";
|
|
12
|
+
import { initChatModel } from "langchain/chat_models/universal";
|
|
12
13
|
import fs from "node:fs/promises";
|
|
13
14
|
import fs$1 from "node:fs";
|
|
14
15
|
import path from "node:path";
|
|
15
|
-
import { spawn } from "node:child_process";
|
|
16
|
+
import cp, { spawn } from "node:child_process";
|
|
16
17
|
import fg from "fast-glob";
|
|
17
18
|
import os from "node:os";
|
|
18
19
|
|
|
@@ -219,7 +220,7 @@ function performStringReplacement(content, oldString, newString, replaceAll) {
|
|
|
219
220
|
if (oldString === "") return "Error: oldString cannot be empty when file has content";
|
|
220
221
|
const occurrences = content.split(oldString).length - 1;
|
|
221
222
|
if (occurrences === 0) return `Error: String not found in file: '${oldString}'`;
|
|
222
|
-
if (occurrences > 1 && !replaceAll) return `Error: String '${oldString}' appears ${occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context.`;
|
|
223
|
+
if (occurrences > 1 && !replaceAll) return `Error: String '${oldString}' has multiple occurrences (appears ${occurrences} times) in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context.`;
|
|
223
224
|
return [content.split(oldString).join(newString), occurrences];
|
|
224
225
|
}
|
|
225
226
|
/**
|
|
@@ -868,7 +869,7 @@ function createWriteFileTool(backend, options) {
|
|
|
868
869
|
description: customDescription || WRITE_FILE_TOOL_DESCRIPTION,
|
|
869
870
|
schema: z.object({
|
|
870
871
|
file_path: z.string().describe("Absolute path to the file to write"),
|
|
871
|
-
content: z.string().describe("Content to write to the file")
|
|
872
|
+
content: z.string().default("").describe("Content to write to the file")
|
|
872
873
|
})
|
|
873
874
|
});
|
|
874
875
|
}
|
|
@@ -1357,7 +1358,8 @@ function getSubagents(options) {
|
|
|
1357
1358
|
model: defaultModel,
|
|
1358
1359
|
systemPrompt: DEFAULT_SUBAGENT_PROMPT,
|
|
1359
1360
|
tools: defaultTools,
|
|
1360
|
-
middleware: generalPurposeMiddleware
|
|
1361
|
+
middleware: generalPurposeMiddleware,
|
|
1362
|
+
name: "general-purpose"
|
|
1361
1363
|
});
|
|
1362
1364
|
subagentDescriptions.push(`- general-purpose: ${DEFAULT_GENERAL_PURPOSE_DESCRIPTION}`);
|
|
1363
1365
|
}
|
|
@@ -1372,7 +1374,8 @@ function getSubagents(options) {
|
|
|
1372
1374
|
model: agentParams.model ?? defaultModel,
|
|
1373
1375
|
systemPrompt: agentParams.systemPrompt,
|
|
1374
1376
|
tools: agentParams.tools ?? defaultTools,
|
|
1375
|
-
middleware
|
|
1377
|
+
middleware,
|
|
1378
|
+
name: agentParams.name
|
|
1376
1379
|
});
|
|
1377
1380
|
}
|
|
1378
1381
|
}
|
|
@@ -2234,6 +2237,76 @@ function createSkillsMiddleware(options) {
|
|
|
2234
2237
|
* For simple use cases without backend offloading, use `summarizationMiddleware`
|
|
2235
2238
|
* from `langchain` directly.
|
|
2236
2239
|
*/
|
|
2240
|
+
const DEFAULT_MESSAGES_TO_KEEP = 20;
|
|
2241
|
+
const DEFAULT_TRIM_TOKEN_LIMIT = 4e3;
|
|
2242
|
+
const FALLBACK_TRIGGER = {
|
|
2243
|
+
type: "tokens",
|
|
2244
|
+
value: 17e4
|
|
2245
|
+
};
|
|
2246
|
+
const FALLBACK_KEEP = {
|
|
2247
|
+
type: "messages",
|
|
2248
|
+
value: 6
|
|
2249
|
+
};
|
|
2250
|
+
const FALLBACK_TRUNCATE_ARGS = {
|
|
2251
|
+
trigger: {
|
|
2252
|
+
type: "messages",
|
|
2253
|
+
value: 20
|
|
2254
|
+
},
|
|
2255
|
+
keep: {
|
|
2256
|
+
type: "messages",
|
|
2257
|
+
value: 20
|
|
2258
|
+
}
|
|
2259
|
+
};
|
|
2260
|
+
const PROFILE_TRIGGER = {
|
|
2261
|
+
type: "fraction",
|
|
2262
|
+
value: .85
|
|
2263
|
+
};
|
|
2264
|
+
const PROFILE_KEEP = {
|
|
2265
|
+
type: "fraction",
|
|
2266
|
+
value: .1
|
|
2267
|
+
};
|
|
2268
|
+
const PROFILE_TRUNCATE_ARGS = {
|
|
2269
|
+
trigger: {
|
|
2270
|
+
type: "fraction",
|
|
2271
|
+
value: .85
|
|
2272
|
+
},
|
|
2273
|
+
keep: {
|
|
2274
|
+
type: "fraction",
|
|
2275
|
+
value: .1
|
|
2276
|
+
}
|
|
2277
|
+
};
|
|
2278
|
+
/**
|
|
2279
|
+
* Compute summarization defaults based on model profile.
|
|
2280
|
+
* Mirrors Python's `_compute_summarization_defaults`.
|
|
2281
|
+
*
|
|
2282
|
+
* If the model has a profile with `maxInputTokens`, uses fraction-based
|
|
2283
|
+
* settings. Otherwise, uses fixed token/message counts.
|
|
2284
|
+
*
|
|
2285
|
+
* @param resolvedModel - The resolved chat model instance.
|
|
2286
|
+
*/
|
|
2287
|
+
function computeSummarizationDefaults(resolvedModel) {
|
|
2288
|
+
if (resolvedModel.profile && typeof resolvedModel.profile === "object" && "maxInputTokens" in resolvedModel.profile && typeof resolvedModel.profile.maxInputTokens === "number") return {
|
|
2289
|
+
trigger: PROFILE_TRIGGER,
|
|
2290
|
+
keep: PROFILE_KEEP,
|
|
2291
|
+
truncateArgsSettings: PROFILE_TRUNCATE_ARGS
|
|
2292
|
+
};
|
|
2293
|
+
return {
|
|
2294
|
+
trigger: FALLBACK_TRIGGER,
|
|
2295
|
+
keep: FALLBACK_KEEP,
|
|
2296
|
+
truncateArgsSettings: FALLBACK_TRUNCATE_ARGS
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
const DEFAULT_SUMMARY_PROMPT = `You are a conversation summarizer. Your task is to create a concise summary of the conversation that captures:
|
|
2300
|
+
1. The main topics discussed
|
|
2301
|
+
2. Key decisions or conclusions reached
|
|
2302
|
+
3. Any important context that would be needed for continuing the conversation
|
|
2303
|
+
|
|
2304
|
+
Keep the summary focused and informative. Do not include unnecessary details.
|
|
2305
|
+
|
|
2306
|
+
Conversation to summarize:
|
|
2307
|
+
{conversation}
|
|
2308
|
+
|
|
2309
|
+
Summary:`;
|
|
2237
2310
|
/**
|
|
2238
2311
|
* Zod schema for a summarization event that tracks what was summarized and
|
|
2239
2312
|
* where the cutoff is.
|
|
@@ -2254,6 +2327,548 @@ const SummarizationStateSchema = z$1.object({
|
|
|
2254
2327
|
_summarizationSessionId: z$1.string().optional(),
|
|
2255
2328
|
_summarizationEvent: SummarizationEventSchema.optional()
|
|
2256
2329
|
});
|
|
2330
|
+
/**
|
|
2331
|
+
* Check if a message is a previous summarization message.
|
|
2332
|
+
* Summary messages are HumanMessage objects with lc_source='summarization' in additional_kwargs.
|
|
2333
|
+
*/
|
|
2334
|
+
function isSummaryMessage(msg) {
|
|
2335
|
+
if (!HumanMessage.isInstance(msg)) return false;
|
|
2336
|
+
return msg.additional_kwargs?.lc_source === "summarization";
|
|
2337
|
+
}
|
|
2338
|
+
/**
|
|
2339
|
+
* Create summarization middleware with backend support for conversation history offloading.
|
|
2340
|
+
*
|
|
2341
|
+
* This middleware:
|
|
2342
|
+
* 1. Monitors conversation length against configured thresholds
|
|
2343
|
+
* 2. When triggered, offloads old messages to backend storage
|
|
2344
|
+
* 3. Generates a summary of offloaded messages
|
|
2345
|
+
* 4. Replaces old messages with the summary, preserving recent context
|
|
2346
|
+
*
|
|
2347
|
+
* @param options - Configuration options
|
|
2348
|
+
* @returns AgentMiddleware for summarization and history offloading
|
|
2349
|
+
*/
|
|
2350
|
+
function createSummarizationMiddleware(options) {
|
|
2351
|
+
const { model, backend, summaryPrompt = DEFAULT_SUMMARY_PROMPT, trimTokensToSummarize = DEFAULT_TRIM_TOKEN_LIMIT, historyPathPrefix = "/conversation_history" } = options;
|
|
2352
|
+
let trigger = options.trigger;
|
|
2353
|
+
let keep = options.keep ?? {
|
|
2354
|
+
type: "messages",
|
|
2355
|
+
value: DEFAULT_MESSAGES_TO_KEEP
|
|
2356
|
+
};
|
|
2357
|
+
let truncateArgsSettings = options.truncateArgsSettings;
|
|
2358
|
+
let defaultsComputed = trigger != null;
|
|
2359
|
+
let truncateTrigger = truncateArgsSettings?.trigger;
|
|
2360
|
+
let truncateKeep = truncateArgsSettings?.keep ?? {
|
|
2361
|
+
type: "messages",
|
|
2362
|
+
value: 20
|
|
2363
|
+
};
|
|
2364
|
+
let maxArgLength = truncateArgsSettings?.maxLength ?? 2e3;
|
|
2365
|
+
let truncationText = truncateArgsSettings?.truncationText ?? "...(argument truncated)";
|
|
2366
|
+
/**
|
|
2367
|
+
* Lazily compute defaults from model profile when trigger was not provided.
|
|
2368
|
+
* Called once when the model is first resolved.
|
|
2369
|
+
*/
|
|
2370
|
+
function applyModelDefaults(resolvedModel) {
|
|
2371
|
+
if (defaultsComputed) return;
|
|
2372
|
+
defaultsComputed = true;
|
|
2373
|
+
const defaults = computeSummarizationDefaults(resolvedModel);
|
|
2374
|
+
trigger = defaults.trigger;
|
|
2375
|
+
keep = options.keep ?? defaults.keep;
|
|
2376
|
+
if (!options.truncateArgsSettings) {
|
|
2377
|
+
truncateArgsSettings = defaults.truncateArgsSettings;
|
|
2378
|
+
truncateTrigger = defaults.truncateArgsSettings.trigger;
|
|
2379
|
+
truncateKeep = defaults.truncateArgsSettings.keep ?? {
|
|
2380
|
+
type: "messages",
|
|
2381
|
+
value: 20
|
|
2382
|
+
};
|
|
2383
|
+
maxArgLength = defaults.truncateArgsSettings.maxLength ?? 2e3;
|
|
2384
|
+
truncationText = defaults.truncateArgsSettings.truncationText ?? "...(argument truncated)";
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
let sessionId = null;
|
|
2388
|
+
let tokenEstimationMultiplier = 1;
|
|
2389
|
+
/**
|
|
2390
|
+
* Resolve backend from instance or factory.
|
|
2391
|
+
*/
|
|
2392
|
+
function getBackend(state) {
|
|
2393
|
+
if (typeof backend === "function") return backend({ state });
|
|
2394
|
+
return backend;
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Get or create session ID for history file naming.
|
|
2398
|
+
*/
|
|
2399
|
+
function getSessionId(state) {
|
|
2400
|
+
if (state._summarizationSessionId) return state._summarizationSessionId;
|
|
2401
|
+
if (!sessionId) sessionId = `session_${v4().substring(0, 8)}`;
|
|
2402
|
+
return sessionId;
|
|
2403
|
+
}
|
|
2404
|
+
/**
|
|
2405
|
+
* Get the history file path.
|
|
2406
|
+
*/
|
|
2407
|
+
function getHistoryPath(state) {
|
|
2408
|
+
return `${historyPathPrefix}/${getSessionId(state)}.md`;
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* Cached resolved model to avoid repeated initChatModel calls
|
|
2412
|
+
*/
|
|
2413
|
+
let cachedModel = void 0;
|
|
2414
|
+
/**
|
|
2415
|
+
* Resolve the chat model.
|
|
2416
|
+
* Uses initChatModel to support any model provider from a string name.
|
|
2417
|
+
* The resolved model is cached for subsequent calls.
|
|
2418
|
+
*/
|
|
2419
|
+
async function getChatModel() {
|
|
2420
|
+
if (cachedModel) return cachedModel;
|
|
2421
|
+
if (typeof model === "string") cachedModel = await initChatModel(model);
|
|
2422
|
+
else cachedModel = model;
|
|
2423
|
+
return cachedModel;
|
|
2424
|
+
}
|
|
2425
|
+
/**
|
|
2426
|
+
* Get the max input tokens from the model's profile.
|
|
2427
|
+
* Similar to Python's _get_profile_limits.
|
|
2428
|
+
*
|
|
2429
|
+
* When the profile is unavailable, returns undefined. In that case the
|
|
2430
|
+
* middleware uses fixed token/message-count fallback defaults for
|
|
2431
|
+
* trigger/keep, and relies on the ContextOverflowError catch as a
|
|
2432
|
+
* safety net if the prompt still exceeds the model's actual limit.
|
|
2433
|
+
*/
|
|
2434
|
+
function getMaxInputTokens(resolvedModel) {
|
|
2435
|
+
const profile = resolvedModel.profile;
|
|
2436
|
+
if (profile && typeof profile === "object" && "maxInputTokens" in profile && typeof profile.maxInputTokens === "number") return profile.maxInputTokens;
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Check if summarization should be triggered.
|
|
2440
|
+
*/
|
|
2441
|
+
function shouldSummarize(messages, totalTokens, maxInputTokens) {
|
|
2442
|
+
if (!trigger) return false;
|
|
2443
|
+
const adjustedTokens = totalTokens * tokenEstimationMultiplier;
|
|
2444
|
+
const triggers = Array.isArray(trigger) ? trigger : [trigger];
|
|
2445
|
+
for (const t of triggers) {
|
|
2446
|
+
if (t.type === "messages" && messages.length >= t.value) return true;
|
|
2447
|
+
if (t.type === "tokens" && adjustedTokens >= t.value) return true;
|
|
2448
|
+
if (t.type === "fraction" && maxInputTokens) {
|
|
2449
|
+
if (adjustedTokens >= Math.floor(maxInputTokens * t.value)) return true;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return false;
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Find a safe cutoff point that doesn't split AI/Tool message pairs.
|
|
2456
|
+
*
|
|
2457
|
+
* If the message at `cutoffIndex` is a ToolMessage, this adjusts the boundary
|
|
2458
|
+
* so that related AI and Tool messages stay together. Two strategies are used:
|
|
2459
|
+
*
|
|
2460
|
+
* 1. **Move backward** to include the AIMessage that produced the tool calls,
|
|
2461
|
+
* keeping the pair in the preserved set. Preferred when it doesn't move
|
|
2462
|
+
* the cutoff too far back.
|
|
2463
|
+
*
|
|
2464
|
+
* 2. **Advance forward** past all consecutive ToolMessages, putting the entire
|
|
2465
|
+
* pair into the summarized set. Used when moving backward would preserve
|
|
2466
|
+
* too many messages (e.g., a single AIMessage made 20+ tool calls).
|
|
2467
|
+
*/
|
|
2468
|
+
function findSafeCutoffPoint(messages, cutoffIndex) {
|
|
2469
|
+
if (cutoffIndex >= messages.length || !ToolMessage.isInstance(messages[cutoffIndex])) return cutoffIndex;
|
|
2470
|
+
let forwardIdx = cutoffIndex;
|
|
2471
|
+
while (forwardIdx < messages.length && ToolMessage.isInstance(messages[forwardIdx])) forwardIdx++;
|
|
2472
|
+
const toolCallIds = /* @__PURE__ */ new Set();
|
|
2473
|
+
for (let i = cutoffIndex; i < forwardIdx; i++) {
|
|
2474
|
+
const toolMsg = messages[i];
|
|
2475
|
+
if (toolMsg.tool_call_id) toolCallIds.add(toolMsg.tool_call_id);
|
|
2476
|
+
}
|
|
2477
|
+
let backwardIdx = null;
|
|
2478
|
+
for (let i = cutoffIndex - 1; i >= 0; i--) {
|
|
2479
|
+
const msg = messages[i];
|
|
2480
|
+
if (AIMessage.isInstance(msg) && msg.tool_calls) {
|
|
2481
|
+
const aiToolCallIds = new Set(msg.tool_calls.map((tc) => tc.id).filter((id) => id != null));
|
|
2482
|
+
for (const id of toolCallIds) if (aiToolCallIds.has(id)) {
|
|
2483
|
+
backwardIdx = i;
|
|
2484
|
+
break;
|
|
2485
|
+
}
|
|
2486
|
+
if (backwardIdx !== null) break;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
if (backwardIdx === null) return forwardIdx;
|
|
2490
|
+
if (cutoffIndex - backwardIdx > cutoffIndex / 2 && cutoffIndex > 2) return forwardIdx;
|
|
2491
|
+
return backwardIdx;
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Determine cutoff index for messages to summarize.
|
|
2495
|
+
* Messages at index < cutoff will be summarized.
|
|
2496
|
+
* Messages at index >= cutoff will be preserved.
|
|
2497
|
+
*
|
|
2498
|
+
* Uses findSafeCutoffPoint to ensure tool call/result pairs stay together.
|
|
2499
|
+
*/
|
|
2500
|
+
function determineCutoffIndex(messages, maxInputTokens) {
|
|
2501
|
+
let rawCutoff;
|
|
2502
|
+
if (keep.type === "messages") {
|
|
2503
|
+
if (messages.length <= keep.value) return 0;
|
|
2504
|
+
rawCutoff = messages.length - keep.value;
|
|
2505
|
+
} else if (keep.type === "tokens" || keep.type === "fraction") {
|
|
2506
|
+
const targetTokenCount = keep.type === "fraction" && maxInputTokens ? Math.floor(maxInputTokens * keep.value) : keep.value;
|
|
2507
|
+
let tokensKept = 0;
|
|
2508
|
+
rawCutoff = 0;
|
|
2509
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2510
|
+
const msgTokens = countTokensApproximately([messages[i]]);
|
|
2511
|
+
if (tokensKept + msgTokens > targetTokenCount) {
|
|
2512
|
+
rawCutoff = i + 1;
|
|
2513
|
+
break;
|
|
2514
|
+
}
|
|
2515
|
+
tokensKept += msgTokens;
|
|
2516
|
+
}
|
|
2517
|
+
} else return 0;
|
|
2518
|
+
return findSafeCutoffPoint(messages, rawCutoff);
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Check if argument truncation should be triggered.
|
|
2522
|
+
*/
|
|
2523
|
+
function shouldTruncateArgs(messages, totalTokens, maxInputTokens) {
|
|
2524
|
+
if (!truncateTrigger) return false;
|
|
2525
|
+
const adjustedTokens = totalTokens * tokenEstimationMultiplier;
|
|
2526
|
+
if (truncateTrigger.type === "messages") return messages.length >= truncateTrigger.value;
|
|
2527
|
+
if (truncateTrigger.type === "tokens") return adjustedTokens >= truncateTrigger.value;
|
|
2528
|
+
if (truncateTrigger.type === "fraction" && maxInputTokens) return adjustedTokens >= Math.floor(maxInputTokens * truncateTrigger.value);
|
|
2529
|
+
return false;
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Determine cutoff index for argument truncation.
|
|
2533
|
+
* Uses findSafeCutoffPoint to ensure tool call/result pairs stay together.
|
|
2534
|
+
*/
|
|
2535
|
+
function determineTruncateCutoffIndex(messages, maxInputTokens) {
|
|
2536
|
+
let rawCutoff;
|
|
2537
|
+
if (truncateKeep.type === "messages") {
|
|
2538
|
+
if (messages.length <= truncateKeep.value) return messages.length;
|
|
2539
|
+
rawCutoff = messages.length - truncateKeep.value;
|
|
2540
|
+
} else if (truncateKeep.type === "tokens" || truncateKeep.type === "fraction") {
|
|
2541
|
+
const targetTokenCount = truncateKeep.type === "fraction" && maxInputTokens ? Math.floor(maxInputTokens * truncateKeep.value) : truncateKeep.value;
|
|
2542
|
+
let tokensKept = 0;
|
|
2543
|
+
rawCutoff = 0;
|
|
2544
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2545
|
+
const msgTokens = countTokensApproximately([messages[i]]);
|
|
2546
|
+
if (tokensKept + msgTokens > targetTokenCount) {
|
|
2547
|
+
rawCutoff = i + 1;
|
|
2548
|
+
break;
|
|
2549
|
+
}
|
|
2550
|
+
tokensKept += msgTokens;
|
|
2551
|
+
}
|
|
2552
|
+
} else return messages.length;
|
|
2553
|
+
return findSafeCutoffPoint(messages, rawCutoff);
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Count tokens including system message and tools, matching Python's approach.
|
|
2557
|
+
* This gives a more accurate picture of what actually gets sent to the model.
|
|
2558
|
+
*/
|
|
2559
|
+
function countTotalTokens(messages, systemMessage, tools) {
|
|
2560
|
+
return countTokensApproximately(systemMessage && SystemMessage.isInstance(systemMessage) ? [systemMessage, ...messages] : [...messages], tools && Array.isArray(tools) && tools.length > 0 ? tools : null);
|
|
2561
|
+
}
|
|
2562
|
+
/**
|
|
2563
|
+
* Truncate ToolMessage content so that the total payload fits within the
|
|
2564
|
+
* model's context window. Each ToolMessage gets an equal share of the
|
|
2565
|
+
* remaining token budget after accounting for non-tool messages, system
|
|
2566
|
+
* message, and tool schemas.
|
|
2567
|
+
*
|
|
2568
|
+
* This is critical for conversations where a single AIMessage triggers
|
|
2569
|
+
* many tool calls whose results collectively exceed the context window.
|
|
2570
|
+
* Without this, findSafeCutoffPoint cannot split the AI/Tool group and
|
|
2571
|
+
* summarization would discard everything, causing the model to re-call
|
|
2572
|
+
* the same tools in an infinite loop.
|
|
2573
|
+
*/
|
|
2574
|
+
function compactToolResults(messages, maxInputTokens, systemMessage, tools) {
|
|
2575
|
+
const toolMessageIndices = [];
|
|
2576
|
+
for (let i = 0; i < messages.length; i++) if (ToolMessage.isInstance(messages[i])) toolMessageIndices.push(i);
|
|
2577
|
+
if (toolMessageIndices.length === 0) return {
|
|
2578
|
+
messages,
|
|
2579
|
+
modified: false
|
|
2580
|
+
};
|
|
2581
|
+
const overheadTokens = countTotalTokens(messages.filter((m) => !ToolMessage.isInstance(m)), systemMessage, tools);
|
|
2582
|
+
const adjustedMax = maxInputTokens / tokenEstimationMultiplier;
|
|
2583
|
+
const budgetForTools = Math.max(adjustedMax * .7 - overheadTokens, 1e3);
|
|
2584
|
+
const perToolBudgetChars = Math.floor(budgetForTools / toolMessageIndices.length) * 4;
|
|
2585
|
+
let modified = false;
|
|
2586
|
+
const result = [...messages];
|
|
2587
|
+
for (const idx of toolMessageIndices) {
|
|
2588
|
+
const msg = messages[idx];
|
|
2589
|
+
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
2590
|
+
if (content.length > perToolBudgetChars) {
|
|
2591
|
+
result[idx] = new ToolMessage({
|
|
2592
|
+
content: content.substring(0, perToolBudgetChars) + "\n...(result truncated)",
|
|
2593
|
+
tool_call_id: msg.tool_call_id,
|
|
2594
|
+
name: msg.name
|
|
2595
|
+
});
|
|
2596
|
+
modified = true;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
return {
|
|
2600
|
+
messages: result,
|
|
2601
|
+
modified
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2605
|
+
* Truncate large tool arguments in old messages.
|
|
2606
|
+
*/
|
|
2607
|
+
function truncateArgs(messages, maxInputTokens, systemMessage, tools) {
|
|
2608
|
+
if (!shouldTruncateArgs(messages, countTotalTokens(messages, systemMessage, tools), maxInputTokens)) return {
|
|
2609
|
+
messages,
|
|
2610
|
+
modified: false
|
|
2611
|
+
};
|
|
2612
|
+
const cutoffIndex = determineTruncateCutoffIndex(messages, maxInputTokens);
|
|
2613
|
+
if (cutoffIndex >= messages.length) return {
|
|
2614
|
+
messages,
|
|
2615
|
+
modified: false
|
|
2616
|
+
};
|
|
2617
|
+
const truncatedMessages = [];
|
|
2618
|
+
let modified = false;
|
|
2619
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2620
|
+
const msg = messages[i];
|
|
2621
|
+
if (i < cutoffIndex && AIMessage.isInstance(msg) && msg.tool_calls) {
|
|
2622
|
+
const truncatedToolCalls = msg.tool_calls.map((toolCall) => {
|
|
2623
|
+
const args = toolCall.args || {};
|
|
2624
|
+
const truncatedArgs = {};
|
|
2625
|
+
let toolModified = false;
|
|
2626
|
+
for (const [key, value] of Object.entries(args)) if (typeof value === "string" && value.length > maxArgLength && (toolCall.name === "write_file" || toolCall.name === "edit_file")) {
|
|
2627
|
+
truncatedArgs[key] = value.substring(0, 20) + truncationText;
|
|
2628
|
+
toolModified = true;
|
|
2629
|
+
} else truncatedArgs[key] = value;
|
|
2630
|
+
if (toolModified) {
|
|
2631
|
+
modified = true;
|
|
2632
|
+
return {
|
|
2633
|
+
...toolCall,
|
|
2634
|
+
args: truncatedArgs
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
return toolCall;
|
|
2638
|
+
});
|
|
2639
|
+
if (modified) {
|
|
2640
|
+
const truncatedMsg = new AIMessage({
|
|
2641
|
+
content: msg.content,
|
|
2642
|
+
tool_calls: truncatedToolCalls,
|
|
2643
|
+
additional_kwargs: msg.additional_kwargs
|
|
2644
|
+
});
|
|
2645
|
+
truncatedMessages.push(truncatedMsg);
|
|
2646
|
+
} else truncatedMessages.push(msg);
|
|
2647
|
+
} else truncatedMessages.push(msg);
|
|
2648
|
+
}
|
|
2649
|
+
return {
|
|
2650
|
+
messages: truncatedMessages,
|
|
2651
|
+
modified
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Filter out previous summary messages.
|
|
2656
|
+
*/
|
|
2657
|
+
function filterSummaryMessages(messages) {
|
|
2658
|
+
return messages.filter((msg) => !isSummaryMessage(msg));
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Offload messages to backend.
|
|
2662
|
+
*/
|
|
2663
|
+
async function offloadToBackend(resolvedBackend, messages, state) {
|
|
2664
|
+
const path = getHistoryPath(state);
|
|
2665
|
+
const filteredMessages = filterSummaryMessages(messages);
|
|
2666
|
+
const newSection = `## Summarized at ${(/* @__PURE__ */ new Date()).toISOString()}\n\n${getBufferString(filteredMessages)}\n\n`;
|
|
2667
|
+
let existingContent = "";
|
|
2668
|
+
try {
|
|
2669
|
+
if (resolvedBackend.downloadFiles) {
|
|
2670
|
+
const responses = await resolvedBackend.downloadFiles([path]);
|
|
2671
|
+
if (responses.length > 0 && responses[0].content && !responses[0].error) existingContent = new TextDecoder().decode(responses[0].content);
|
|
2672
|
+
}
|
|
2673
|
+
} catch {}
|
|
2674
|
+
const combinedContent = existingContent + newSection;
|
|
2675
|
+
try {
|
|
2676
|
+
let result;
|
|
2677
|
+
if (existingContent) result = await resolvedBackend.edit(path, existingContent, combinedContent);
|
|
2678
|
+
else result = await resolvedBackend.write(path, combinedContent);
|
|
2679
|
+
if (result.error) {
|
|
2680
|
+
console.warn(`Failed to offload conversation history to ${path}: ${result.error}`);
|
|
2681
|
+
return null;
|
|
2682
|
+
}
|
|
2683
|
+
return path;
|
|
2684
|
+
} catch (e) {
|
|
2685
|
+
console.warn(`Exception offloading conversation history to ${path}:`, e);
|
|
2686
|
+
return null;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
/**
|
|
2690
|
+
* Create summary of messages.
|
|
2691
|
+
*/
|
|
2692
|
+
async function createSummary(messages, chatModel) {
|
|
2693
|
+
let messagesToSummarize = messages;
|
|
2694
|
+
if (countTokensApproximately(messages) > trimTokensToSummarize) {
|
|
2695
|
+
let kept = 0;
|
|
2696
|
+
const trimmedMessages = [];
|
|
2697
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2698
|
+
const msgTokens = countTokensApproximately([messages[i]]);
|
|
2699
|
+
if (kept + msgTokens > trimTokensToSummarize) break;
|
|
2700
|
+
trimmedMessages.unshift(messages[i]);
|
|
2701
|
+
kept += msgTokens;
|
|
2702
|
+
}
|
|
2703
|
+
messagesToSummarize = trimmedMessages;
|
|
2704
|
+
}
|
|
2705
|
+
const conversation = getBufferString(messagesToSummarize);
|
|
2706
|
+
const prompt = summaryPrompt.replace("{conversation}", conversation);
|
|
2707
|
+
const response = await chatModel.invoke([new HumanMessage({ content: prompt })]);
|
|
2708
|
+
return typeof response.content === "string" ? response.content : JSON.stringify(response.content);
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Build the summary message with file path reference.
|
|
2712
|
+
*/
|
|
2713
|
+
function buildSummaryMessage(summary, filePath) {
|
|
2714
|
+
let content;
|
|
2715
|
+
if (filePath) content = `You are in the middle of a conversation that has been summarized.
|
|
2716
|
+
|
|
2717
|
+
The full conversation history has been saved to ${filePath} should you need to refer back to it for details.
|
|
2718
|
+
|
|
2719
|
+
A condensed summary follows:
|
|
2720
|
+
|
|
2721
|
+
<summary>
|
|
2722
|
+
${summary}
|
|
2723
|
+
</summary>`;
|
|
2724
|
+
else content = `Here is a summary of the conversation to date:\n\n${summary}`;
|
|
2725
|
+
return new HumanMessage({
|
|
2726
|
+
content,
|
|
2727
|
+
additional_kwargs: { lc_source: "summarization" }
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
/**
|
|
2731
|
+
* Reconstruct the effective message list based on any previous summarization event.
|
|
2732
|
+
*
|
|
2733
|
+
* After summarization, instead of using all messages from state, we use the summary
|
|
2734
|
+
* message plus messages after the cutoff index. This avoids full state rewrites.
|
|
2735
|
+
*/
|
|
2736
|
+
function getEffectiveMessages(messages, state) {
|
|
2737
|
+
const event = state._summarizationEvent;
|
|
2738
|
+
if (!event) return messages;
|
|
2739
|
+
const result = [event.summaryMessage];
|
|
2740
|
+
result.push(...messages.slice(event.cutoffIndex));
|
|
2741
|
+
return result;
|
|
2742
|
+
}
|
|
2743
|
+
/**
|
|
2744
|
+
* Summarize a set of messages using the given model and build the
|
|
2745
|
+
* summary message + backend offload. Returns the summary message,
|
|
2746
|
+
* the file path, and the state cutoff index.
|
|
2747
|
+
*/
|
|
2748
|
+
async function summarizeMessages(messagesToSummarize, resolvedModel, state, previousCutoffIndex, cutoffIndex) {
|
|
2749
|
+
const filePath = await offloadToBackend(getBackend(state), messagesToSummarize, state);
|
|
2750
|
+
if (filePath === null) console.warn(`[SummarizationMiddleware] Backend offload failed during summarization. Proceeding with summary generation.`);
|
|
2751
|
+
return {
|
|
2752
|
+
summaryMessage: buildSummaryMessage(await createSummary(messagesToSummarize, resolvedModel), filePath),
|
|
2753
|
+
filePath,
|
|
2754
|
+
stateCutoffIndex: previousCutoffIndex != null ? previousCutoffIndex + cutoffIndex - 1 : cutoffIndex
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Check if an error (possibly wrapped in MiddlewareError layers) is a
|
|
2759
|
+
* ContextOverflowError by walking the `cause` chain.
|
|
2760
|
+
*/
|
|
2761
|
+
function isContextOverflow(err) {
|
|
2762
|
+
let cause = err;
|
|
2763
|
+
while (cause != null) {
|
|
2764
|
+
if (ContextOverflowError.isInstance(cause)) return true;
|
|
2765
|
+
cause = typeof cause === "object" && cause !== null && "cause" in cause ? cause.cause : void 0;
|
|
2766
|
+
}
|
|
2767
|
+
return false;
|
|
2768
|
+
}
|
|
2769
|
+
async function performSummarization(request, handler, truncatedMessages, resolvedModel, maxInputTokens) {
|
|
2770
|
+
const cutoffIndex = determineCutoffIndex(truncatedMessages, maxInputTokens);
|
|
2771
|
+
if (cutoffIndex <= 0) return handler({
|
|
2772
|
+
...request,
|
|
2773
|
+
messages: truncatedMessages
|
|
2774
|
+
});
|
|
2775
|
+
const messagesToSummarize = truncatedMessages.slice(0, cutoffIndex);
|
|
2776
|
+
const preservedMessages = truncatedMessages.slice(cutoffIndex);
|
|
2777
|
+
if (preservedMessages.length === 0 && maxInputTokens) {
|
|
2778
|
+
const compact = compactToolResults(truncatedMessages, maxInputTokens, request.systemMessage, request.tools);
|
|
2779
|
+
if (compact.modified) try {
|
|
2780
|
+
return await handler({
|
|
2781
|
+
...request,
|
|
2782
|
+
messages: compact.messages
|
|
2783
|
+
});
|
|
2784
|
+
} catch (err) {
|
|
2785
|
+
if (!isContextOverflow(err)) throw err;
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
const previousEvent = request.state._summarizationEvent;
|
|
2789
|
+
const previousCutoffIndex = previousEvent != null ? previousEvent.cutoffIndex : void 0;
|
|
2790
|
+
const { summaryMessage, filePath, stateCutoffIndex } = await summarizeMessages(messagesToSummarize, resolvedModel, request.state, previousCutoffIndex, cutoffIndex);
|
|
2791
|
+
let modifiedMessages = [summaryMessage, ...preservedMessages];
|
|
2792
|
+
const modifiedTokens = countTotalTokens(modifiedMessages, request.systemMessage, request.tools);
|
|
2793
|
+
let finalStateCutoffIndex = stateCutoffIndex;
|
|
2794
|
+
let finalSummaryMessage = summaryMessage;
|
|
2795
|
+
let finalFilePath = filePath;
|
|
2796
|
+
try {
|
|
2797
|
+
await handler({
|
|
2798
|
+
...request,
|
|
2799
|
+
messages: modifiedMessages
|
|
2800
|
+
});
|
|
2801
|
+
} catch (err) {
|
|
2802
|
+
if (!isContextOverflow(err)) throw err;
|
|
2803
|
+
if (maxInputTokens && modifiedTokens > 0) {
|
|
2804
|
+
const observedRatio = maxInputTokens / modifiedTokens;
|
|
2805
|
+
if (observedRatio > tokenEstimationMultiplier) tokenEstimationMultiplier = observedRatio * 1.1;
|
|
2806
|
+
}
|
|
2807
|
+
const reSumResult = await summarizeMessages([...messagesToSummarize, ...preservedMessages], resolvedModel, request.state, previousCutoffIndex, truncatedMessages.length);
|
|
2808
|
+
finalSummaryMessage = reSumResult.summaryMessage;
|
|
2809
|
+
finalFilePath = reSumResult.filePath;
|
|
2810
|
+
finalStateCutoffIndex = reSumResult.stateCutoffIndex;
|
|
2811
|
+
modifiedMessages = [reSumResult.summaryMessage];
|
|
2812
|
+
await handler({
|
|
2813
|
+
...request,
|
|
2814
|
+
messages: modifiedMessages
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
return new Command({ update: {
|
|
2818
|
+
_summarizationEvent: {
|
|
2819
|
+
cutoffIndex: finalStateCutoffIndex,
|
|
2820
|
+
summaryMessage: finalSummaryMessage,
|
|
2821
|
+
filePath: finalFilePath
|
|
2822
|
+
},
|
|
2823
|
+
_summarizationSessionId: getSessionId(request.state)
|
|
2824
|
+
} });
|
|
2825
|
+
}
|
|
2826
|
+
return createMiddleware({
|
|
2827
|
+
name: "SummarizationMiddleware",
|
|
2828
|
+
stateSchema: SummarizationStateSchema,
|
|
2829
|
+
async wrapModelCall(request, handler) {
|
|
2830
|
+
const effectiveMessages = getEffectiveMessages(request.messages ?? [], request.state);
|
|
2831
|
+
if (effectiveMessages.length === 0) return handler(request);
|
|
2832
|
+
/**
|
|
2833
|
+
* Resolve the chat model and get max input tokens from its profile.
|
|
2834
|
+
*/
|
|
2835
|
+
const resolvedModel = await getChatModel();
|
|
2836
|
+
const maxInputTokens = getMaxInputTokens(resolvedModel);
|
|
2837
|
+
applyModelDefaults(resolvedModel);
|
|
2838
|
+
/**
|
|
2839
|
+
* Step 1: Truncate args if configured
|
|
2840
|
+
*/
|
|
2841
|
+
const { messages: truncatedMessages } = truncateArgs(effectiveMessages, maxInputTokens, request.systemMessage, request.tools);
|
|
2842
|
+
/**
|
|
2843
|
+
* Step 2: Check if summarization should happen.
|
|
2844
|
+
* Count tokens including system message and tools to match what's
|
|
2845
|
+
* actually sent to the model (matching Python implementation).
|
|
2846
|
+
*/
|
|
2847
|
+
const totalTokens = countTotalTokens(truncatedMessages, request.systemMessage, request.tools);
|
|
2848
|
+
/**
|
|
2849
|
+
* If no summarization needed, try passing through.
|
|
2850
|
+
* If the handler throws a ContextOverflowError, fall back to
|
|
2851
|
+
* emergency summarization (matching Python's behavior).
|
|
2852
|
+
*/
|
|
2853
|
+
if (!shouldSummarize(truncatedMessages, totalTokens, maxInputTokens)) try {
|
|
2854
|
+
return await handler({
|
|
2855
|
+
...request,
|
|
2856
|
+
messages: truncatedMessages
|
|
2857
|
+
});
|
|
2858
|
+
} catch (err) {
|
|
2859
|
+
if (!isContextOverflow(err)) throw err;
|
|
2860
|
+
if (maxInputTokens && totalTokens > 0) {
|
|
2861
|
+
const observedRatio = maxInputTokens / totalTokens;
|
|
2862
|
+
if (observedRatio > tokenEstimationMultiplier) tokenEstimationMultiplier = observedRatio * 1.1;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Step 3: Perform summarization
|
|
2867
|
+
*/
|
|
2868
|
+
return performSummarization(request, handler, truncatedMessages, resolvedModel, maxInputTokens);
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2257
2872
|
|
|
2258
2873
|
//#endregion
|
|
2259
2874
|
//#region src/backends/store.ts
|
|
@@ -3336,6 +3951,311 @@ var CompositeBackend = class {
|
|
|
3336
3951
|
}
|
|
3337
3952
|
};
|
|
3338
3953
|
|
|
3954
|
+
//#endregion
|
|
3955
|
+
//#region src/backends/local-shell.ts
|
|
3956
|
+
/**
|
|
3957
|
+
* LocalShellBackend: Node.js implementation of the filesystem backend with unrestricted local shell execution.
|
|
3958
|
+
*
|
|
3959
|
+
* This backend extends FilesystemBackend to add shell command execution on the local
|
|
3960
|
+
* host system. It provides NO sandboxing or isolation - all operations run directly
|
|
3961
|
+
* on the host machine with full system access.
|
|
3962
|
+
*
|
|
3963
|
+
* @module
|
|
3964
|
+
*/
|
|
3965
|
+
/**
|
|
3966
|
+
* Filesystem backend with unrestricted local shell command execution.
|
|
3967
|
+
*
|
|
3968
|
+
* This backend extends FilesystemBackend to add shell command execution
|
|
3969
|
+
* capabilities. Commands are executed directly on the host system without any
|
|
3970
|
+
* sandboxing, process isolation, or security restrictions.
|
|
3971
|
+
*
|
|
3972
|
+
* **Security Warning:**
|
|
3973
|
+
* This backend grants agents BOTH direct filesystem access AND unrestricted
|
|
3974
|
+
* shell execution on your local machine. Use with extreme caution and only in
|
|
3975
|
+
* appropriate environments.
|
|
3976
|
+
*
|
|
3977
|
+
* **Appropriate use cases:**
|
|
3978
|
+
* - Local development CLIs (coding assistants, development tools)
|
|
3979
|
+
* - Personal development environments where you trust the agent's code
|
|
3980
|
+
* - CI/CD pipelines with proper secret management
|
|
3981
|
+
*
|
|
3982
|
+
* **Inappropriate use cases:**
|
|
3983
|
+
* - Production environments (e.g., web servers, APIs, multi-tenant systems)
|
|
3984
|
+
* - Processing untrusted user input or executing untrusted code
|
|
3985
|
+
*
|
|
3986
|
+
* Use StateBackend, StoreBackend, or extend BaseSandbox for production.
|
|
3987
|
+
*
|
|
3988
|
+
* @example
|
|
3989
|
+
* ```typescript
|
|
3990
|
+
* import { LocalShellBackend } from "@langchain/deepagents";
|
|
3991
|
+
*
|
|
3992
|
+
* // Create backend with explicit environment
|
|
3993
|
+
* const backend = new LocalShellBackend({
|
|
3994
|
+
* rootDir: "/home/user/project",
|
|
3995
|
+
* env: { PATH: "/usr/bin:/bin" },
|
|
3996
|
+
* });
|
|
3997
|
+
*
|
|
3998
|
+
* // Execute shell commands (runs directly on host)
|
|
3999
|
+
* const result = await backend.execute("ls -la");
|
|
4000
|
+
* console.log(result.output);
|
|
4001
|
+
* console.log(result.exitCode);
|
|
4002
|
+
*
|
|
4003
|
+
* // Use filesystem operations (inherited from FilesystemBackend)
|
|
4004
|
+
* const content = await backend.read("/README.md");
|
|
4005
|
+
* await backend.write("/output.txt", "Hello world");
|
|
4006
|
+
*
|
|
4007
|
+
* // Inherit all environment variables
|
|
4008
|
+
* const backend2 = new LocalShellBackend({
|
|
4009
|
+
* rootDir: "/home/user/project",
|
|
4010
|
+
* inheritEnv: true,
|
|
4011
|
+
* });
|
|
4012
|
+
* ```
|
|
4013
|
+
*/
|
|
4014
|
+
var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
4015
|
+
#timeout;
|
|
4016
|
+
#maxOutputBytes;
|
|
4017
|
+
#env;
|
|
4018
|
+
#sandboxId;
|
|
4019
|
+
#initialized = false;
|
|
4020
|
+
constructor(options = {}) {
|
|
4021
|
+
const { rootDir, virtualMode = false, timeout = 120, maxOutputBytes = 1e5, env, inheritEnv = false } = options;
|
|
4022
|
+
super({
|
|
4023
|
+
rootDir,
|
|
4024
|
+
virtualMode,
|
|
4025
|
+
maxFileSizeMb: 10
|
|
4026
|
+
});
|
|
4027
|
+
this.#timeout = timeout;
|
|
4028
|
+
this.#maxOutputBytes = maxOutputBytes;
|
|
4029
|
+
const bytes = new Uint8Array(4);
|
|
4030
|
+
crypto.getRandomValues(bytes);
|
|
4031
|
+
this.#sandboxId = `local-${[...bytes].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
4032
|
+
if (inheritEnv) {
|
|
4033
|
+
this.#env = { ...process.env };
|
|
4034
|
+
if (env) Object.assign(this.#env, env);
|
|
4035
|
+
} else this.#env = env ?? {};
|
|
4036
|
+
}
|
|
4037
|
+
/** Unique identifier for this backend instance (format: "local-{random_hex}"). */
|
|
4038
|
+
get id() {
|
|
4039
|
+
return this.#sandboxId;
|
|
4040
|
+
}
|
|
4041
|
+
/** Whether the backend has been initialized and is ready to use. */
|
|
4042
|
+
get isInitialized() {
|
|
4043
|
+
return this.#initialized;
|
|
4044
|
+
}
|
|
4045
|
+
/** Alias for `isInitialized`, matching the standard sandbox interface. */
|
|
4046
|
+
get isRunning() {
|
|
4047
|
+
return this.#initialized;
|
|
4048
|
+
}
|
|
4049
|
+
/**
|
|
4050
|
+
* Initialize the backend by ensuring the rootDir exists.
|
|
4051
|
+
*
|
|
4052
|
+
* Creates the rootDir (and any parent directories) if it does not already
|
|
4053
|
+
* exist. Safe to call on an existing directory. Must be called before
|
|
4054
|
+
* `execute()`, or use the static `LocalShellBackend.create()` factory.
|
|
4055
|
+
*
|
|
4056
|
+
* @throws {SandboxError} If already initialized (`ALREADY_INITIALIZED`)
|
|
4057
|
+
*/
|
|
4058
|
+
async initialize() {
|
|
4059
|
+
if (this.#initialized) throw new SandboxError("Backend is already initialized. Each LocalShellBackend instance can only be initialized once.", "ALREADY_INITIALIZED");
|
|
4060
|
+
await fs.mkdir(this.cwd, { recursive: true });
|
|
4061
|
+
this.#initialized = true;
|
|
4062
|
+
}
|
|
4063
|
+
/**
|
|
4064
|
+
* Mark the backend as no longer running.
|
|
4065
|
+
*
|
|
4066
|
+
* For local shell backends there is no remote resource to tear down,
|
|
4067
|
+
* so this simply flips the `isRunning` / `isInitialized` flag.
|
|
4068
|
+
*/
|
|
4069
|
+
async close() {
|
|
4070
|
+
this.#initialized = false;
|
|
4071
|
+
}
|
|
4072
|
+
/**
|
|
4073
|
+
* Read a file, adapting error messages to the standard sandbox format.
|
|
4074
|
+
*/
|
|
4075
|
+
async read(filePath, offset = 0, limit = 500) {
|
|
4076
|
+
const result = await super.read(filePath, offset, limit);
|
|
4077
|
+
if (typeof result === "string" && result.startsWith("Error reading file") && result.includes("ENOENT")) return `Error: File '${filePath}' not found`;
|
|
4078
|
+
return result;
|
|
4079
|
+
}
|
|
4080
|
+
/**
|
|
4081
|
+
* Edit a file, adapting error messages to the standard sandbox format.
|
|
4082
|
+
*/
|
|
4083
|
+
async edit(filePath, oldString, newString, replaceAll = false) {
|
|
4084
|
+
const result = await super.edit(filePath, oldString, newString, replaceAll);
|
|
4085
|
+
if (result.error?.includes("ENOENT")) return {
|
|
4086
|
+
...result,
|
|
4087
|
+
error: `Error: File '${filePath}' not found`
|
|
4088
|
+
};
|
|
4089
|
+
return result;
|
|
4090
|
+
}
|
|
4091
|
+
/**
|
|
4092
|
+
* List directory contents, returning paths relative to rootDir.
|
|
4093
|
+
*/
|
|
4094
|
+
async lsInfo(dirPath) {
|
|
4095
|
+
const results = await super.lsInfo(dirPath);
|
|
4096
|
+
if (this.virtualMode) return results;
|
|
4097
|
+
const cwdPrefix = this.cwd.endsWith(path.sep) ? this.cwd : this.cwd + path.sep;
|
|
4098
|
+
return results.map((info) => ({
|
|
4099
|
+
...info,
|
|
4100
|
+
path: info.path.startsWith(cwdPrefix) ? info.path.slice(cwdPrefix.length) : info.path
|
|
4101
|
+
}));
|
|
4102
|
+
}
|
|
4103
|
+
/**
|
|
4104
|
+
* Glob matching that returns relative paths and includes directories.
|
|
4105
|
+
*/
|
|
4106
|
+
async globInfo(pattern, searchPath = "/") {
|
|
4107
|
+
if (pattern.startsWith("/")) pattern = pattern.substring(1);
|
|
4108
|
+
const resolvedSearchPath = searchPath === "/" || searchPath === "" ? this.cwd : this.virtualMode ? path.resolve(this.cwd, searchPath.replace(/^\//, "")) : path.resolve(this.cwd, searchPath);
|
|
4109
|
+
try {
|
|
4110
|
+
if (!(await fs.stat(resolvedSearchPath)).isDirectory()) return [];
|
|
4111
|
+
} catch {
|
|
4112
|
+
return [];
|
|
4113
|
+
}
|
|
4114
|
+
const formatPath = (rel) => this.virtualMode ? `/${rel}` : rel;
|
|
4115
|
+
const globOpts = {
|
|
4116
|
+
cwd: resolvedSearchPath,
|
|
4117
|
+
absolute: false,
|
|
4118
|
+
dot: true
|
|
4119
|
+
};
|
|
4120
|
+
const [fileMatches, dirMatches] = await Promise.all([fg(pattern, {
|
|
4121
|
+
...globOpts,
|
|
4122
|
+
onlyFiles: true
|
|
4123
|
+
}), fg(pattern, {
|
|
4124
|
+
...globOpts,
|
|
4125
|
+
onlyDirectories: true
|
|
4126
|
+
})]);
|
|
4127
|
+
const statFile = async (match) => {
|
|
4128
|
+
try {
|
|
4129
|
+
const entryStat = await fs.stat(path.join(resolvedSearchPath, match));
|
|
4130
|
+
if (entryStat.isFile()) return {
|
|
4131
|
+
path: formatPath(match),
|
|
4132
|
+
is_dir: false,
|
|
4133
|
+
size: entryStat.size,
|
|
4134
|
+
modified_at: entryStat.mtime.toISOString()
|
|
4135
|
+
};
|
|
4136
|
+
} catch {}
|
|
4137
|
+
return null;
|
|
4138
|
+
};
|
|
4139
|
+
const statDir = async (match) => {
|
|
4140
|
+
try {
|
|
4141
|
+
const entryStat = await fs.stat(path.join(resolvedSearchPath, match));
|
|
4142
|
+
if (entryStat.isDirectory()) return {
|
|
4143
|
+
path: formatPath(match),
|
|
4144
|
+
is_dir: true,
|
|
4145
|
+
size: 0,
|
|
4146
|
+
modified_at: entryStat.mtime.toISOString()
|
|
4147
|
+
};
|
|
4148
|
+
} catch {}
|
|
4149
|
+
return null;
|
|
4150
|
+
};
|
|
4151
|
+
const [fileInfos, dirInfos] = await Promise.all([Promise.all(fileMatches.map(statFile)), Promise.all(dirMatches.map(statDir))]);
|
|
4152
|
+
const results = [...fileInfos, ...dirInfos].filter((info) => info !== null);
|
|
4153
|
+
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
4154
|
+
return results;
|
|
4155
|
+
}
|
|
4156
|
+
/**
|
|
4157
|
+
* Execute a shell command directly on the host system.
|
|
4158
|
+
*
|
|
4159
|
+
* Commands are executed directly on your host system using `spawn()`
|
|
4160
|
+
* with `shell: true`. There is NO sandboxing, isolation, or security
|
|
4161
|
+
* restrictions. The command runs with your user's full permissions.
|
|
4162
|
+
*
|
|
4163
|
+
* The command is executed using the system shell with the working directory
|
|
4164
|
+
* set to the backend's rootDir. Stdout and stderr are combined into a single
|
|
4165
|
+
* output stream, with stderr lines prefixed with `[stderr]`.
|
|
4166
|
+
*
|
|
4167
|
+
* @param command - Shell command string to execute
|
|
4168
|
+
* @returns ExecuteResponse containing output, exit code, and truncation flag
|
|
4169
|
+
*/
|
|
4170
|
+
async execute(command) {
|
|
4171
|
+
if (!command || typeof command !== "string") return {
|
|
4172
|
+
output: "Error: Command must be a non-empty string.",
|
|
4173
|
+
exitCode: 1,
|
|
4174
|
+
truncated: false
|
|
4175
|
+
};
|
|
4176
|
+
return new Promise((resolve) => {
|
|
4177
|
+
let stdout = "";
|
|
4178
|
+
let stderr = "";
|
|
4179
|
+
let timedOut = false;
|
|
4180
|
+
const child = cp.spawn(command, {
|
|
4181
|
+
shell: true,
|
|
4182
|
+
env: this.#env,
|
|
4183
|
+
cwd: this.cwd
|
|
4184
|
+
});
|
|
4185
|
+
const timer = setTimeout(() => {
|
|
4186
|
+
timedOut = true;
|
|
4187
|
+
child.kill("SIGTERM");
|
|
4188
|
+
}, this.#timeout * 1e3);
|
|
4189
|
+
child.stdout.on("data", (data) => {
|
|
4190
|
+
stdout += data.toString();
|
|
4191
|
+
});
|
|
4192
|
+
child.stderr.on("data", (data) => {
|
|
4193
|
+
stderr += data.toString();
|
|
4194
|
+
});
|
|
4195
|
+
child.on("error", (err) => {
|
|
4196
|
+
clearTimeout(timer);
|
|
4197
|
+
resolve({
|
|
4198
|
+
output: `Error executing command: ${err.message}`,
|
|
4199
|
+
exitCode: 1,
|
|
4200
|
+
truncated: false
|
|
4201
|
+
});
|
|
4202
|
+
});
|
|
4203
|
+
child.on("close", (code, signal) => {
|
|
4204
|
+
clearTimeout(timer);
|
|
4205
|
+
if (timedOut || signal === "SIGTERM") {
|
|
4206
|
+
resolve({
|
|
4207
|
+
output: `Error: Command timed out after ${this.#timeout.toFixed(1)} seconds.`,
|
|
4208
|
+
exitCode: 124,
|
|
4209
|
+
truncated: false
|
|
4210
|
+
});
|
|
4211
|
+
return;
|
|
4212
|
+
}
|
|
4213
|
+
const outputParts = [];
|
|
4214
|
+
if (stdout) outputParts.push(stdout);
|
|
4215
|
+
if (stderr) {
|
|
4216
|
+
const stderrLines = stderr.trim().split("\n");
|
|
4217
|
+
outputParts.push(...stderrLines.map((line) => `[stderr] ${line}`));
|
|
4218
|
+
}
|
|
4219
|
+
let output = outputParts.length > 0 ? outputParts.join("\n") : "<no output>";
|
|
4220
|
+
let truncated = false;
|
|
4221
|
+
if (output.length > this.#maxOutputBytes) {
|
|
4222
|
+
output = output.slice(0, this.#maxOutputBytes);
|
|
4223
|
+
output += `\n\n... Output truncated at ${this.#maxOutputBytes} bytes.`;
|
|
4224
|
+
truncated = true;
|
|
4225
|
+
}
|
|
4226
|
+
const exitCode = code ?? 1;
|
|
4227
|
+
if (exitCode !== 0) output = `${output.trimEnd()}\n\nExit code: ${exitCode}`;
|
|
4228
|
+
resolve({
|
|
4229
|
+
output,
|
|
4230
|
+
exitCode,
|
|
4231
|
+
truncated
|
|
4232
|
+
});
|
|
4233
|
+
});
|
|
4234
|
+
});
|
|
4235
|
+
}
|
|
4236
|
+
/**
|
|
4237
|
+
* Create and initialize a new LocalShellBackend in one step.
|
|
4238
|
+
*
|
|
4239
|
+
* This is the recommended way to create a backend when the rootDir may
|
|
4240
|
+
* not exist yet. It combines construction and initialization (ensuring
|
|
4241
|
+
* rootDir exists) into a single async operation.
|
|
4242
|
+
*
|
|
4243
|
+
* @param options - Configuration options for the backend
|
|
4244
|
+
* @returns An initialized and ready-to-use backend
|
|
4245
|
+
*/
|
|
4246
|
+
static async create(options = {}) {
|
|
4247
|
+
const { initialFiles, ...backendOptions } = options;
|
|
4248
|
+
const backend = new LocalShellBackend(backendOptions);
|
|
4249
|
+
await backend.initialize();
|
|
4250
|
+
if (initialFiles) {
|
|
4251
|
+
const encoder = new TextEncoder();
|
|
4252
|
+
const files = Object.entries(initialFiles).map(([filePath, content]) => [filePath, encoder.encode(content)]);
|
|
4253
|
+
await backend.uploadFiles(files);
|
|
4254
|
+
}
|
|
4255
|
+
return backend;
|
|
4256
|
+
}
|
|
4257
|
+
};
|
|
4258
|
+
|
|
3339
4259
|
//#endregion
|
|
3340
4260
|
//#region src/backends/sandbox.ts
|
|
3341
4261
|
/**
|
|
@@ -3677,7 +4597,7 @@ const BASE_PROMPT = `In order to complete the objective that the user asks of yo
|
|
|
3677
4597
|
* - Todo management (todoListMiddleware)
|
|
3678
4598
|
* - Filesystem tools (createFilesystemMiddleware)
|
|
3679
4599
|
* - Subagent delegation (createSubAgentMiddleware)
|
|
3680
|
-
* - Conversation summarization (
|
|
4600
|
+
* - Conversation summarization (createSummarizationMiddleware) with backend offloading
|
|
3681
4601
|
* - Prompt caching (anthropicPromptCachingMiddleware)
|
|
3682
4602
|
* - Tool call patching (createPatchToolCallsMiddleware)
|
|
3683
4603
|
* - Human-in-the-loop (humanInTheLoopMiddleware) - optional
|
|
@@ -3765,21 +4685,26 @@ function createDeepAgent(params = {}) {
|
|
|
3765
4685
|
/**
|
|
3766
4686
|
* Middleware for custom subagents (does NOT include skills from main agent).
|
|
3767
4687
|
* Custom subagents must define their own `skills` property to get skills.
|
|
4688
|
+
*
|
|
4689
|
+
* Uses createSummarizationMiddleware (deepagents version) with backend support
|
|
4690
|
+
* and auto-computed defaults from model profile, matching Python's create_deep_agent.
|
|
4691
|
+
* When trigger is not provided, defaults are lazily computed:
|
|
4692
|
+
* - With model profile: fraction-based (trigger=0.85, keep=0.10)
|
|
4693
|
+
* - Without profile: fixed (trigger=170k tokens, keep=6 messages)
|
|
3768
4694
|
*/
|
|
3769
4695
|
const subagentMiddleware = [
|
|
3770
4696
|
todoListMiddleware(),
|
|
3771
4697
|
createFilesystemMiddleware({ backend: filesystemBackend }),
|
|
3772
|
-
|
|
4698
|
+
createSummarizationMiddleware({
|
|
3773
4699
|
model,
|
|
3774
|
-
|
|
3775
|
-
keep: { messages: 6 }
|
|
4700
|
+
backend: filesystemBackend
|
|
3776
4701
|
}),
|
|
3777
4702
|
anthropicPromptCachingMiddleware({ unsupportedModelBehavior: "ignore" }),
|
|
3778
4703
|
createPatchToolCallsMiddleware()
|
|
3779
4704
|
];
|
|
3780
4705
|
/**
|
|
3781
4706
|
* Return as DeepAgent with proper DeepAgentTypeConfig
|
|
3782
|
-
* - Response: TResponse (
|
|
4707
|
+
* - Response: InferStructuredResponse<TResponse> (unwraps ToolStrategy<T>/ProviderStrategy<T> → T)
|
|
3783
4708
|
* - State: undefined (state comes from middleware)
|
|
3784
4709
|
* - Context: ContextSchema
|
|
3785
4710
|
* - Middleware: AllMiddleware (built-in + custom + subagent middleware for state inference)
|
|
@@ -3803,10 +4728,9 @@ function createDeepAgent(params = {}) {
|
|
|
3803
4728
|
subagents: processedSubagents,
|
|
3804
4729
|
generalPurposeAgent: true
|
|
3805
4730
|
}),
|
|
3806
|
-
|
|
4731
|
+
createSummarizationMiddleware({
|
|
3807
4732
|
model,
|
|
3808
|
-
|
|
3809
|
-
keep: { messages: 6 }
|
|
4733
|
+
backend: filesystemBackend
|
|
3810
4734
|
}),
|
|
3811
4735
|
anthropicPromptCachingMiddleware({ unsupportedModelBehavior: "ignore" }),
|
|
3812
4736
|
createPatchToolCallsMiddleware()
|
|
@@ -3816,7 +4740,7 @@ function createDeepAgent(params = {}) {
|
|
|
3816
4740
|
...interruptOn ? [humanInTheLoopMiddleware({ interruptOn })] : [],
|
|
3817
4741
|
...customMiddleware
|
|
3818
4742
|
],
|
|
3819
|
-
responseFormat,
|
|
4743
|
+
...responseFormat != null && { responseFormat },
|
|
3820
4744
|
contextSchema,
|
|
3821
4745
|
checkpointer,
|
|
3822
4746
|
store,
|
|
@@ -4369,5 +5293,5 @@ function listSkills(options) {
|
|
|
4369
5293
|
}
|
|
4370
5294
|
|
|
4371
5295
|
//#endregion
|
|
4372
|
-
export { BaseSandbox, CompositeBackend, DEFAULT_GENERAL_PURPOSE_DESCRIPTION, DEFAULT_SUBAGENT_PROMPT, FilesystemBackend, GENERAL_PURPOSE_SUBAGENT, MAX_SKILL_DESCRIPTION_LENGTH, MAX_SKILL_FILE_SIZE, MAX_SKILL_NAME_LENGTH, SandboxError, StateBackend, StoreBackend, TASK_SYSTEM_PROMPT, createAgentMemoryMiddleware, createDeepAgent, createFilesystemMiddleware, createMemoryMiddleware, createPatchToolCallsMiddleware, createSettings, createSkillsMiddleware, createSubAgentMiddleware, filesValue, findProjectRoot, isSandboxBackend, listSkills, parseSkillMetadata };
|
|
5296
|
+
export { BaseSandbox, CompositeBackend, DEFAULT_GENERAL_PURPOSE_DESCRIPTION, DEFAULT_SUBAGENT_PROMPT, FilesystemBackend, GENERAL_PURPOSE_SUBAGENT, LocalShellBackend, MAX_SKILL_DESCRIPTION_LENGTH, MAX_SKILL_FILE_SIZE, MAX_SKILL_NAME_LENGTH, SandboxError, StateBackend, StoreBackend, TASK_SYSTEM_PROMPT, computeSummarizationDefaults, createAgentMemoryMiddleware, createDeepAgent, createFilesystemMiddleware, createMemoryMiddleware, createPatchToolCallsMiddleware, createSettings, createSkillsMiddleware, createSubAgentMiddleware, createSummarizationMiddleware, filesValue, findProjectRoot, isSandboxBackend, listSkills, parseSkillMetadata };
|
|
4373
5297
|
//# sourceMappingURL=index.js.map
|