@librechat/agents 3.1.66-dev.0 → 3.1.67-dev.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.
Files changed (60) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +47 -18
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +1 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +69 -0
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/hooks/types.cjs.map +1 -1
  8. package/dist/cjs/main.cjs +12 -0
  9. package/dist/cjs/main.cjs.map +1 -1
  10. package/dist/cjs/summarization/node.cjs +44 -0
  11. package/dist/cjs/summarization/node.cjs.map +1 -1
  12. package/dist/cjs/tools/SubagentTool.cjs +92 -0
  13. package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
  14. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +261 -0
  15. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
  16. package/dist/esm/agents/AgentContext.mjs +47 -18
  17. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  18. package/dist/esm/common/enum.mjs +1 -0
  19. package/dist/esm/common/enum.mjs.map +1 -1
  20. package/dist/esm/graphs/Graph.mjs +69 -0
  21. package/dist/esm/graphs/Graph.mjs.map +1 -1
  22. package/dist/esm/hooks/types.mjs.map +1 -1
  23. package/dist/esm/main.mjs +2 -0
  24. package/dist/esm/main.mjs.map +1 -1
  25. package/dist/esm/summarization/node.mjs +44 -0
  26. package/dist/esm/summarization/node.mjs.map +1 -1
  27. package/dist/esm/tools/SubagentTool.mjs +85 -0
  28. package/dist/esm/tools/SubagentTool.mjs.map +1 -0
  29. package/dist/esm/tools/subagent/SubagentExecutor.mjs +256 -0
  30. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -0
  31. package/dist/types/agents/AgentContext.d.ts +12 -0
  32. package/dist/types/common/enum.d.ts +2 -1
  33. package/dist/types/hooks/types.d.ts +12 -1
  34. package/dist/types/index.d.ts +2 -0
  35. package/dist/types/summarization/node.d.ts +2 -0
  36. package/dist/types/tools/SubagentTool.d.ts +36 -0
  37. package/dist/types/tools/subagent/SubagentExecutor.d.ts +83 -0
  38. package/dist/types/tools/subagent/index.d.ts +2 -0
  39. package/dist/types/types/graph.d.ts +25 -0
  40. package/dist/types/types/llm.d.ts +14 -2
  41. package/package.json +2 -1
  42. package/src/agents/AgentContext.ts +54 -17
  43. package/src/agents/__tests__/AgentContext.test.ts +110 -0
  44. package/src/common/enum.ts +1 -0
  45. package/src/graphs/Graph.ts +88 -0
  46. package/src/hooks/__tests__/compactHooks.test.ts +214 -0
  47. package/src/hooks/index.ts +4 -2
  48. package/src/hooks/types.ts +17 -1
  49. package/src/index.ts +2 -0
  50. package/src/scripts/multi-agent-subagent.ts +246 -0
  51. package/src/specs/subagent.test.ts +305 -0
  52. package/src/summarization/node.ts +53 -0
  53. package/src/tools/SubagentTool.ts +100 -0
  54. package/src/tools/__tests__/SubagentExecutor.test.ts +615 -0
  55. package/src/tools/__tests__/SubagentTool.test.ts +149 -0
  56. package/src/tools/__tests__/subagentHooks.test.ts +215 -0
  57. package/src/tools/subagent/SubagentExecutor.ts +344 -0
  58. package/src/tools/subagent/index.ts +12 -0
  59. package/src/types/graph.ts +27 -0
  60. package/src/types/llm.ts +16 -2
@@ -86,6 +86,12 @@ export declare class AgentContext {
86
86
  toolDefinitions?: t.LCTool[];
87
87
  /** Set of tool names discovered via tool search (to be loaded) */
88
88
  discoveredToolNames: Set<string>;
89
+ /** Original AgentInputs used to create this context — used for self-spawn subagent resolution. */
90
+ _sourceInputs?: t.AgentInputs;
91
+ /** Subagent configurations for hierarchical delegation. */
92
+ subagentConfigs?: t.SubagentConfig[];
93
+ /** Maximum subagent nesting depth. */
94
+ maxSubagentDepth?: number;
89
95
  /** Instructions for this agent */
90
96
  instructions?: string;
91
97
  /** Additional instructions for this agent */
@@ -230,6 +236,8 @@ export declare class AgentContext {
230
236
  * token counts.
231
237
  */
232
238
  updateTokenMapWithInstructions(baseTokenMap: Record<string, number>): void;
239
+ /** Active tool definitions for token accounting (excludes deferred-and-undiscovered entries). */
240
+ private getActiveToolDefinitions;
233
241
  /**
234
242
  * Calculate tool tokens and add to instruction tokens
235
243
  * Note: System message tokens are calculated during systemRunnable creation
@@ -284,6 +292,10 @@ export declare class AgentContext {
284
292
  /**
285
293
  * Returns a structured breakdown of how the context token budget is consumed.
286
294
  * Useful for diagnostics when context overflow or pruning issues occur.
295
+ *
296
+ * Note: `toolCount` reflects discoveries immediately, but `toolSchemaTokens`
297
+ * is a snapshot taken during `calculateInstructionTokens` and is not
298
+ * recomputed when `markToolsAsDiscovered` is called mid-run.
287
299
  */
288
300
  getTokenBudgetBreakdown(messages?: BaseMessage[]): t.TokenBudgetBreakdown;
289
301
  /**
@@ -140,7 +140,8 @@ export declare enum Constants {
140
140
  SKILL_TOOL = "skill",
141
141
  READ_FILE = "read_file",
142
142
  BASH_TOOL = "bash_tool",
143
- BASH_PROGRAMMATIC_TOOL_CALLING = "run_tools_with_bash"
143
+ BASH_PROGRAMMATIC_TOOL_CALLING = "run_tools_with_bash",
144
+ SUBAGENT = "subagent"
144
145
  }
145
146
  /** Tool names that use the code execution environment (shared session, file tracking). */
146
147
  export declare const CODE_EXECUTION_TOOLS: ReadonlySet<string>;
@@ -109,11 +109,22 @@ export interface StopFailureHookInput extends BaseHookInput {
109
109
  export interface PreCompactHookInput extends BaseHookInput {
110
110
  hook_event_name: 'PreCompact';
111
111
  messagesBeforeCount: number;
112
- trigger: 'threshold' | 'manual' | 'error';
112
+ /**
113
+ * What triggered compaction. Matches `SummarizationTrigger.type` from the
114
+ * agent's summarization config. `'default'` means no trigger was
115
+ * configured and compaction fired because messages were pruned.
116
+ */
117
+ trigger: 'token_ratio' | 'remaining_tokens' | 'messages_to_refine' | 'default' | (string & {});
113
118
  }
114
119
  export interface PostCompactHookInput extends BaseHookInput {
115
120
  hook_event_name: 'PostCompact';
116
121
  summary: string;
122
+ /**
123
+ * Number of messages remaining after compaction. The summarize node
124
+ * returns a `removeAll` signal that clears all messages from state;
125
+ * the summary itself is injected into the system prompt, not as a
126
+ * message. This is `0` at the point of hook dispatch.
127
+ */
117
128
  messagesAfterCount: number;
118
129
  }
119
130
  /** Discriminated union of every hook input shape. */
@@ -11,6 +11,8 @@ export * from './tools/BashExecutor';
11
11
  export * from './tools/ProgrammaticToolCalling';
12
12
  export * from './tools/BashProgrammaticToolCalling';
13
13
  export * from './tools/SkillTool';
14
+ export * from './tools/SubagentTool';
15
+ export * from './tools/subagent';
14
16
  export * from './tools/ReadFile';
15
17
  export * from './tools/skillCatalog';
16
18
  export * from './tools/ToolSearch';
@@ -1,6 +1,7 @@
1
1
  import type { RunnableConfig } from '@langchain/core/runnables';
2
2
  import type { BaseMessage } from '@langchain/core/messages';
3
3
  import type { AgentContext } from '@/agents/AgentContext';
4
+ import type { HookRegistry } from '@/hooks';
4
5
  import type * as t from '@/types';
5
6
  /** Structured checkpoint prompt for fresh summarization (no prior summary). */
6
7
  export declare const DEFAULT_SUMMARIZATION_PROMPT = "Hold on, before you continue I need you to write me a checkpoint of everything so far. Your context window is filling up and this checkpoint replaces the messages above, so capture everything you need to pick right back up.\n\nDon't second-guess or fact-check anything you did, your tool results reflect exactly what happened. If a tool result appears truncated, that's just a display artifact from context management: the tool executed fully. Just record what you did and what you observed. Only the checkpoint, don't respond to me or continue the conversation.\n\n## Checkpoint\n\n## Goal\nWhat I asked you to do and any sub-goals you identified.\n\n## Constraints & Preferences\nAny rules, preferences, or configuration I established.\n\n## Progress\n### Done\n- What you completed and the outcomes\n\n### In Progress\n- What you're currently working on\n\n## Key Decisions\nDecisions you made and why.\n\n## Next Steps\nConcrete task actions remaining, in priority order.\n\n## Critical Context\nExact identifiers, names, error messages, URLs, and details you need to preserve verbatim.\n\nRules:\n- Record what you did and observed, don't judge or re-evaluate it\n- For each tool call: the tool name, key inputs, and the outcome\n- Preserve exact identifiers, names, errors, and references verbatim\n- Short declarative sentences\n- Skip empty sections";
@@ -14,6 +15,7 @@ interface CreateSummarizeNodeParams {
14
15
  config?: RunnableConfig;
15
16
  runId?: string;
16
17
  isMultiAgent: boolean;
18
+ hookRegistry?: HookRegistry;
17
19
  dispatchRunStep: (runStep: t.RunStep, config?: RunnableConfig) => Promise<void>;
18
20
  dispatchRunStepCompleted: (stepId: string, result: t.StepCompleted, config?: RunnableConfig) => Promise<void>;
19
21
  };
@@ -0,0 +1,36 @@
1
+ import { Constants } from '@/common';
2
+ import type { SubagentConfig } from '@/types';
3
+ import type { JsonSchemaType, LCTool } from '@/types/tools';
4
+ export declare const SubagentToolName = Constants.SUBAGENT;
5
+ export declare const SubagentToolDescription = "Delegate a task to a specialized subagent that runs in an isolated context window. The subagent executes independently and returns only its final text result \u2014 all intermediate tool calls, reasoning, and context stay isolated.\n\nWHEN TO USE:\n- The task is self-contained and can be described in a single prompt.\n- You want to offload verbose or exploratory work without bloating your own context.\n- A specialized subagent is available for the task domain.\n\nWHAT HAPPENS:\n- A fresh agent is created with the task description as its only input.\n- The subagent runs to completion using its own tools and context.\n- Only the final text response is returned to you.\n\nCONSTRAINTS:\n- subagent_type must match one of the available types listed below.\n- The subagent cannot see your conversation history.";
6
+ export declare const SubagentToolSchema: {
7
+ readonly type: "object";
8
+ readonly properties: {
9
+ readonly description: {
10
+ readonly type: "string";
11
+ readonly description: "Complete task description for the subagent. This is the ONLY information it receives — include all necessary context, requirements, and constraints.";
12
+ };
13
+ readonly subagent_type: {
14
+ readonly type: "string";
15
+ readonly description: "Which subagent type to delegate to. Must be one of the available types.";
16
+ };
17
+ };
18
+ readonly required: string[];
19
+ };
20
+ export declare const SubagentToolDefinition: LCTool;
21
+ /**
22
+ * Build the name, schema, and description params for `tool()` from available configs.
23
+ * Used by `Graph.createAgentNode()` when constructing the runtime tool instance.
24
+ * Extends `SubagentToolSchema` by populating `subagent_type.enum` dynamically.
25
+ */
26
+ export declare function buildSubagentToolParams(configs: SubagentConfig[]): {
27
+ name: string;
28
+ schema: JsonSchemaType;
29
+ description: string;
30
+ };
31
+ /**
32
+ * Create a SubagentTool LCTool definition with dynamic enum and description
33
+ * populated from the available subagent configs.
34
+ * Used for the tool registry in event-driven mode.
35
+ */
36
+ export declare function createSubagentToolDefinition(configs: SubagentConfig[]): LCTool;
@@ -0,0 +1,83 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+ import type { AgentInputs, StandardGraphInput, ResolvedSubagentConfig, SubagentConfig, TokenCounter } from '@/types';
3
+ import type { HookRegistry } from '@/hooks';
4
+ import type { AgentContext } from '@/agents/AgentContext';
5
+ import type { StandardGraph } from '@/graphs/Graph';
6
+ export type SubagentExecuteParams = {
7
+ description: string;
8
+ subagentType: string;
9
+ threadId?: string;
10
+ };
11
+ export type SubagentExecuteResult = {
12
+ content: string;
13
+ messages: BaseMessage[];
14
+ };
15
+ /**
16
+ * Factory that constructs a child graph for subagent execution. Injected
17
+ * rather than imported so that `SubagentExecutor` does not have a runtime
18
+ * dependency on `StandardGraph` — this avoids a circular dependency between
19
+ * `src/graphs/Graph.ts` and `src/tools/subagent/` that would otherwise break
20
+ * Rollup's chunking under `preserveModules`.
21
+ */
22
+ export type ChildGraphFactory = (input: StandardGraphInput) => StandardGraph;
23
+ export type SubagentExecutorOptions = {
24
+ configs: Map<string, ResolvedSubagentConfig>;
25
+ parentSignal?: AbortSignal;
26
+ hookRegistry?: HookRegistry;
27
+ parentRunId: string;
28
+ parentAgentId?: string;
29
+ tokenCounter?: TokenCounter;
30
+ /** Remaining nesting budget. 0 or negative blocks execution. */
31
+ maxDepth?: number;
32
+ /**
33
+ * Factory for constructing the isolated child graph. Callers pass
34
+ * `(input) => new StandardGraph(input)` — injected to break a circular
35
+ * module dependency.
36
+ */
37
+ createChildGraph: ChildGraphFactory;
38
+ };
39
+ export declare class SubagentExecutor {
40
+ private readonly configs;
41
+ private readonly parentSignal?;
42
+ private readonly hookRegistry?;
43
+ private readonly parentRunId;
44
+ private readonly parentAgentId?;
45
+ private readonly tokenCounter?;
46
+ private readonly maxDepth;
47
+ private readonly createChildGraph;
48
+ constructor(options: SubagentExecutorOptions);
49
+ execute(params: SubagentExecuteParams): Promise<SubagentExecuteResult>;
50
+ }
51
+ /**
52
+ * Walk messages from last to first, returning the text content of the most
53
+ * recent AIMessage that has any. Non-text blocks (tool_use, thinking,
54
+ * redacted_thinking, tool_result) are stripped. If the last AIMessage is
55
+ * pure tool_use (e.g. the subagent hit `maxTurns` mid-tool-call), the walk
56
+ * continues to earlier AIMessages so partial progress is salvaged — this
57
+ * matches Claude Code's behavior in `agentToolUtils.finalizeAgentTool`.
58
+ * Returns "Task completed" only when no AIMessage in the history contains
59
+ * any text.
60
+ */
61
+ export declare function filterSubagentResult(messages: BaseMessage[]): string;
62
+ /**
63
+ * Resolve self-spawn configs by filling in agentInputs from the parent context.
64
+ * Returns configs with agentInputs guaranteed present. Throws on duplicate
65
+ * `type` values to prevent silent config shadowing.
66
+ */
67
+ export declare function resolveSubagentConfigs(configs: SubagentConfig[], parentContext: AgentContext): ResolvedSubagentConfig[];
68
+ /**
69
+ * Build child AgentInputs from a resolved config, stripping nesting and
70
+ * event-driven fields. When `allowNested: true`, the child's
71
+ * `maxSubagentDepth` is decremented so that depth is consumed as the call
72
+ * chain deepens across graph boundaries — the parent's executor-level check
73
+ * alone cannot see into the child graph's separate executor.
74
+ *
75
+ * @remarks Advanced utility: exported primarily for testing and by
76
+ * {@link SubagentExecutor}. Host applications configuring subagents should
77
+ * not need to call this directly — it is invoked internally when a subagent
78
+ * tool is dispatched. The depth-countdown contract (parent's `maxDepth` in,
79
+ * child's decremented `maxSubagentDepth` on the returned inputs) is the
80
+ * mechanism that bounds nesting across graph boundaries; callers must
81
+ * respect it.
82
+ */
83
+ export declare function buildChildInputs(config: ResolvedSubagentConfig, childAgentId: string, parentMaxDepth: number): AgentInputs;
@@ -0,0 +1,2 @@
1
+ export { SubagentExecutor, filterSubagentResult, resolveSubagentConfigs, buildChildInputs, } from './SubagentExecutor';
2
+ export type { SubagentExecuteParams, SubagentExecuteResult, SubagentExecutorOptions, ChildGraphFactory, } from './SubagentExecutor';
@@ -248,6 +248,27 @@ export type GraphEdge = {
248
248
  export type MultiAgentGraphInput = StandardGraphInput & {
249
249
  edges: GraphEdge[];
250
250
  };
251
+ /** Configuration for a subagent type that can be spawned by a parent agent. */
252
+ export type SubagentConfig = {
253
+ /** Identifier used in the tool's `subagent_type` enum (e.g. 'researcher', 'coder'). */
254
+ type: string;
255
+ /** Human-readable display name. */
256
+ name: string;
257
+ /** What this subagent specializes in — shown to the LLM. */
258
+ description: string;
259
+ /** Full agent config for the child graph. Omit when `self` is true. */
260
+ agentInputs?: AgentInputs;
261
+ /** When true, reuse the parent's AgentInputs (context isolation without separate config). */
262
+ self?: boolean;
263
+ /** Max AGENT→TOOLS cycles before forced stop (default: 25). */
264
+ maxTurns?: number;
265
+ /** Allow this subagent to spawn its own subagents (default: false). */
266
+ allowNested?: boolean;
267
+ };
268
+ /** SubagentConfig with agentInputs guaranteed present (self-spawn resolved). */
269
+ export type ResolvedSubagentConfig = SubagentConfig & {
270
+ agentInputs: AgentInputs;
271
+ };
251
272
  export interface AgentInputs {
252
273
  agentId: string;
253
274
  /** Human-readable name for the agent (used in handoff context). Defaults to agentId if not provided. */
@@ -294,6 +315,10 @@ export interface AgentInputs {
294
315
  maxToolResultChars?: number;
295
316
  /** Pre-computed tool schema token count (from cache). Skips recalculation when provided. */
296
317
  toolSchemaTokens?: number;
318
+ /** Subagent configurations for hierarchical delegation. Each defines a child agent type. */
319
+ subagentConfigs?: SubagentConfig[];
320
+ /** Maximum subagent nesting depth. Default 1 means top-level agents can spawn subagents but subagents cannot nest further. */
321
+ maxSubagentDepth?: number;
297
322
  }
298
323
  export interface ContextPruningConfig {
299
324
  enabled?: boolean;
@@ -28,7 +28,18 @@ export type AzureClientOptions = Partial<OpenAIChatInput> & Partial<AzureOpenAII
28
28
  } & BaseChatModelParams & {
29
29
  configuration?: OAIClientOptions;
30
30
  };
31
- export type ThinkingConfig = AnthropicInput['thinking'];
31
+ /**
32
+ * Controls whether Claude's reasoning content is returned in adaptive
33
+ * thinking responses. Added for Claude Opus 4.7, which omits thinking by
34
+ * default unless the caller opts in with `'summarized'`.
35
+ * @see https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7#thinking-content-omitted-by-default
36
+ */
37
+ export type ThinkingDisplay = 'summarized' | 'omitted';
38
+ export type ThinkingConfigAdaptive = {
39
+ type: 'adaptive';
40
+ display?: ThinkingDisplay;
41
+ };
42
+ export type ThinkingConfig = NonNullable<AnthropicInput['thinking']> | ThinkingConfigAdaptive;
32
43
  export type ChatOpenAIToolType = BindToolsInput | OpenAIClient.ChatCompletionTool;
33
44
  export type CommonToolType = StructuredTool | ChatOpenAIToolType;
34
45
  export type AnthropicReasoning = {
@@ -41,7 +52,8 @@ export type GoogleThinkingConfig = {
41
52
  thinkingLevel?: string;
42
53
  };
43
54
  export type OpenAIClientOptions = ChatOpenAIFields;
44
- export type AnthropicClientOptions = AnthropicInput & {
55
+ export type AnthropicClientOptions = Omit<AnthropicInput, 'thinking'> & {
56
+ thinking?: ThinkingConfig;
45
57
  promptCache?: boolean;
46
58
  };
47
59
  export type MistralAIClientOptions = ChatMistralAIInput;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.1.66-dev.0",
3
+ "version": "3.1.67-dev.0",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -77,6 +77,7 @@
77
77
  "multi-agent-chain": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/multi-agent-chain.ts",
78
78
  "multi-agent-sequence": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/multi-agent-sequence.ts",
79
79
  "multi-agent-conditional": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/multi-agent-conditional.ts",
80
+ "multi-agent-subagent": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/multi-agent-subagent.ts",
80
81
  "multi-agent-supervisor": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/multi-agent-supervisor.ts",
81
82
  "test-handoff-preamble": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/test-handoff-preamble.ts",
82
83
  "multi-agent-list-handoff": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/test-multi-agent-list-handoff.ts",
@@ -55,6 +55,8 @@ export class AgentContext {
55
55
  contextPruningConfig,
56
56
  maxToolResultChars,
57
57
  toolSchemaTokens,
58
+ subagentConfigs,
59
+ maxSubagentDepth,
58
60
  } = agentConfig;
59
61
 
60
62
  const agentContext = new AgentContext({
@@ -82,6 +84,10 @@ export class AgentContext {
82
84
  maxToolResultChars,
83
85
  });
84
86
 
87
+ agentContext._sourceInputs = agentConfig;
88
+ agentContext.subagentConfigs = subagentConfigs;
89
+ agentContext.maxSubagentDepth = maxSubagentDepth;
90
+
85
91
  if (initialSummary?.text != null && initialSummary.text !== '') {
86
92
  agentContext.setInitialSummary(
87
93
  initialSummary.text,
@@ -198,6 +204,12 @@ export class AgentContext {
198
204
  toolDefinitions?: t.LCTool[];
199
205
  /** Set of tool names discovered via tool search (to be loaded) */
200
206
  discoveredToolNames: Set<string> = new Set();
207
+ /** Original AgentInputs used to create this context — used for self-spawn subagent resolution. */
208
+ _sourceInputs?: t.AgentInputs;
209
+ /** Subagent configurations for hierarchical delegation. */
210
+ subagentConfigs?: t.SubagentConfig[];
211
+ /** Maximum subagent nesting depth. */
212
+ maxSubagentDepth?: number;
201
213
  /** Instructions for this agent */
202
214
  instructions?: string;
203
215
  /** Additional instructions for this agent */
@@ -664,6 +676,17 @@ export class AgentContext {
664
676
  this.indexTokenCountMap = { ...baseTokenMap };
665
677
  }
666
678
 
679
+ /** Active tool definitions for token accounting (excludes deferred-and-undiscovered entries). */
680
+ private getActiveToolDefinitions(): t.LCTool[] {
681
+ if (!this.toolDefinitions) {
682
+ return [];
683
+ }
684
+ return this.toolDefinitions.filter(
685
+ (def) =>
686
+ def.defer_loading !== true || this.discoveredToolNames.has(def.name)
687
+ );
688
+ }
689
+
667
690
  /**
668
691
  * Calculate tool tokens and add to instruction tokens
669
692
  * Note: System message tokens are calculated during systemRunnable creation
@@ -674,8 +697,20 @@ export class AgentContext {
674
697
  let toolTokens = 0;
675
698
  const countedToolNames = new Set<string>();
676
699
 
677
- if (this.tools && this.tools.length > 0) {
678
- for (const tool of this.tools) {
700
+ /**
701
+ * Iterate both `tools` (user-provided instance tools) and `graphTools`
702
+ * (graph-managed tools like handoff + subagent). `graphTools` is often
703
+ * populated after `fromConfig()` kicks off the initial calculation, so
704
+ * callers that mutate `graphTools` must re-trigger this method to
705
+ * refresh `toolSchemaTokens`.
706
+ */
707
+ const instanceTools: t.GraphTools = [
708
+ ...((this.tools as t.GenericTool[] | undefined) ?? []),
709
+ ...((this.graphTools as t.GenericTool[] | undefined) ?? []),
710
+ ];
711
+
712
+ if (instanceTools.length > 0) {
713
+ for (const tool of instanceTools) {
679
714
  const genericTool = tool as Record<string, unknown>;
680
715
  if (
681
716
  genericTool.schema != null &&
@@ -697,21 +732,19 @@ export class AgentContext {
697
732
  }
698
733
  }
699
734
 
700
- if (this.toolDefinitions && this.toolDefinitions.length > 0) {
701
- for (const def of this.toolDefinitions) {
702
- if (countedToolNames.has(def.name)) {
703
- continue;
704
- }
705
- const schema = {
706
- type: 'function',
707
- function: {
708
- name: def.name,
709
- description: def.description ?? '',
710
- parameters: def.parameters ?? {},
711
- },
712
- };
713
- toolTokens += tokenCounter(new SystemMessage(JSON.stringify(schema)));
735
+ for (const def of this.getActiveToolDefinitions()) {
736
+ if (countedToolNames.has(def.name)) {
737
+ continue;
714
738
  }
739
+ const schema = {
740
+ type: 'function',
741
+ function: {
742
+ name: def.name,
743
+ description: def.description ?? '',
744
+ parameters: def.parameters ?? {},
745
+ },
746
+ };
747
+ toolTokens += tokenCounter(new SystemMessage(JSON.stringify(schema)));
715
748
  }
716
749
 
717
750
  const isAnthropic =
@@ -860,11 +893,15 @@ export class AgentContext {
860
893
  /**
861
894
  * Returns a structured breakdown of how the context token budget is consumed.
862
895
  * Useful for diagnostics when context overflow or pruning issues occur.
896
+ *
897
+ * Note: `toolCount` reflects discoveries immediately, but `toolSchemaTokens`
898
+ * is a snapshot taken during `calculateInstructionTokens` and is not
899
+ * recomputed when `markToolsAsDiscovered` is called mid-run.
863
900
  */
864
901
  getTokenBudgetBreakdown(messages?: BaseMessage[]): t.TokenBudgetBreakdown {
865
902
  const maxContextTokens = this.maxContextTokens ?? 0;
866
903
  const toolCount =
867
- (this.tools?.length ?? 0) + (this.toolDefinitions?.length ?? 0);
904
+ (this.tools?.length ?? 0) + this.getActiveToolDefinitions().length;
868
905
  const messageCount = messages?.length ?? 0;
869
906
 
870
907
  let messageTokens = 0;
@@ -375,6 +375,116 @@ describe('AgentContext', () => {
375
375
 
376
376
  expect(ctx.instructionTokens).toBeGreaterThan(initialTokens);
377
377
  });
378
+
379
+ it('excludes deferred-undiscovered toolDefinitions from toolSchemaTokens', async () => {
380
+ const activeDef: t.LCTool = {
381
+ name: 'active_tool',
382
+ description: 'Always loaded',
383
+ parameters: { type: 'object', properties: {} },
384
+ };
385
+ const deferredDef: t.LCTool = {
386
+ name: 'deferred_tool',
387
+ description: 'Loaded via tool search',
388
+ parameters: { type: 'object', properties: {} },
389
+ defer_loading: true,
390
+ };
391
+
392
+ const ctxBase = createBasicContext({
393
+ agentConfig: { toolDefinitions: [activeDef] },
394
+ tokenCounter: mockTokenCounter,
395
+ });
396
+ const ctxWithDeferred = createBasicContext({
397
+ agentConfig: { toolDefinitions: [activeDef, deferredDef] },
398
+ tokenCounter: mockTokenCounter,
399
+ });
400
+
401
+ await ctxBase.tokenCalculationPromise;
402
+ await ctxWithDeferred.tokenCalculationPromise;
403
+
404
+ expect(ctxWithDeferred.toolSchemaTokens).toBe(ctxBase.toolSchemaTokens);
405
+ });
406
+
407
+ it('includes deferred toolDefinitions once discovered via discoveredTools input', async () => {
408
+ const toolDefinitions: t.LCTool[] = [
409
+ {
410
+ name: 'deferred_tool',
411
+ description: 'Loaded via tool search',
412
+ parameters: { type: 'object', properties: {} },
413
+ defer_loading: true,
414
+ },
415
+ ];
416
+
417
+ const ctxUndiscovered = createBasicContext({
418
+ agentConfig: { toolDefinitions },
419
+ tokenCounter: mockTokenCounter,
420
+ });
421
+ const ctxDiscovered = createBasicContext({
422
+ agentConfig: { toolDefinitions, discoveredTools: ['deferred_tool'] },
423
+ tokenCounter: mockTokenCounter,
424
+ });
425
+
426
+ await ctxUndiscovered.tokenCalculationPromise;
427
+ await ctxDiscovered.tokenCalculationPromise;
428
+
429
+ expect(ctxUndiscovered.toolSchemaTokens).toBe(0);
430
+ expect(ctxDiscovered.toolSchemaTokens).toBeGreaterThan(0);
431
+ });
432
+
433
+ it('getTokenBudgetBreakdown toolCount excludes deferred-undiscovered toolDefinitions', () => {
434
+ const toolDefinitions: t.LCTool[] = [
435
+ {
436
+ name: 'active',
437
+ parameters: { type: 'object', properties: {} },
438
+ },
439
+ {
440
+ name: 'deferred',
441
+ defer_loading: true,
442
+ parameters: { type: 'object', properties: {} },
443
+ },
444
+ ];
445
+
446
+ const ctx = createBasicContext({ agentConfig: { toolDefinitions } });
447
+
448
+ expect(ctx.getTokenBudgetBreakdown().toolCount).toBe(1);
449
+ });
450
+
451
+ it('getTokenBudgetBreakdown toolCount reflects newly discovered deferred tools', () => {
452
+ const toolDefinitions: t.LCTool[] = [
453
+ {
454
+ name: 'deferred',
455
+ defer_loading: true,
456
+ parameters: { type: 'object', properties: {} },
457
+ },
458
+ ];
459
+
460
+ const ctx = createBasicContext({ agentConfig: { toolDefinitions } });
461
+
462
+ expect(ctx.getTokenBudgetBreakdown().toolCount).toBe(0);
463
+ ctx.markToolsAsDiscovered(['deferred']);
464
+ expect(ctx.getTokenBudgetBreakdown().toolCount).toBe(1);
465
+ });
466
+
467
+ it('toolSchemaTokens snapshot does not auto-update after markToolsAsDiscovered', async () => {
468
+ const toolDefinitions: t.LCTool[] = [
469
+ {
470
+ name: 'deferred',
471
+ description: 'Loaded via tool search',
472
+ parameters: { type: 'object', properties: {} },
473
+ defer_loading: true,
474
+ },
475
+ ];
476
+
477
+ const ctx = createBasicContext({
478
+ agentConfig: { toolDefinitions },
479
+ tokenCounter: mockTokenCounter,
480
+ });
481
+
482
+ await ctx.tokenCalculationPromise;
483
+ expect(ctx.toolSchemaTokens).toBe(0);
484
+
485
+ ctx.markToolsAsDiscovered(['deferred']);
486
+ expect(ctx.toolSchemaTokens).toBe(0);
487
+ });
378
488
  });
379
489
 
380
490
  describe('reset()', () => {
@@ -186,6 +186,7 @@ export enum Constants {
186
186
  READ_FILE = 'read_file',
187
187
  BASH_TOOL = 'bash_tool',
188
188
  BASH_PROGRAMMATIC_TOOL_CALLING = 'run_tools_with_bash',
189
+ SUBAGENT = 'subagent',
189
190
  }
190
191
 
191
192
  /** Tool names that use the code execution environment (shared session, file tracking). */