@librechat/agents 3.1.66 → 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 (147) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +23 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +14 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +72 -0
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/hooks/HookRegistry.cjs +162 -0
  8. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
  9. package/dist/cjs/hooks/executeHooks.cjs +276 -0
  10. package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
  11. package/dist/cjs/hooks/matchers.cjs +256 -0
  12. package/dist/cjs/hooks/matchers.cjs.map +1 -0
  13. package/dist/cjs/hooks/types.cjs +27 -0
  14. package/dist/cjs/hooks/types.cjs.map +1 -0
  15. package/dist/cjs/main.cjs +52 -0
  16. package/dist/cjs/main.cjs.map +1 -1
  17. package/dist/cjs/messages/format.cjs +74 -12
  18. package/dist/cjs/messages/format.cjs.map +1 -1
  19. package/dist/cjs/run.cjs +111 -0
  20. package/dist/cjs/run.cjs.map +1 -1
  21. package/dist/cjs/summarization/node.cjs +44 -0
  22. package/dist/cjs/summarization/node.cjs.map +1 -1
  23. package/dist/cjs/tools/BashExecutor.cjs +175 -0
  24. package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
  25. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +296 -0
  26. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
  27. package/dist/cjs/tools/ReadFile.cjs +43 -0
  28. package/dist/cjs/tools/ReadFile.cjs.map +1 -0
  29. package/dist/cjs/tools/SkillTool.cjs +50 -0
  30. package/dist/cjs/tools/SkillTool.cjs.map +1 -0
  31. package/dist/cjs/tools/SubagentTool.cjs +92 -0
  32. package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
  33. package/dist/cjs/tools/ToolNode.cjs +304 -140
  34. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  35. package/dist/cjs/tools/skillCatalog.cjs +84 -0
  36. package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
  37. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +261 -0
  38. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
  39. package/dist/esm/agents/AgentContext.mjs +23 -3
  40. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  41. package/dist/esm/common/enum.mjs +13 -1
  42. package/dist/esm/common/enum.mjs.map +1 -1
  43. package/dist/esm/graphs/Graph.mjs +72 -0
  44. package/dist/esm/graphs/Graph.mjs.map +1 -1
  45. package/dist/esm/hooks/HookRegistry.mjs +160 -0
  46. package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
  47. package/dist/esm/hooks/executeHooks.mjs +273 -0
  48. package/dist/esm/hooks/executeHooks.mjs.map +1 -0
  49. package/dist/esm/hooks/matchers.mjs +251 -0
  50. package/dist/esm/hooks/matchers.mjs.map +1 -0
  51. package/dist/esm/hooks/types.mjs +25 -0
  52. package/dist/esm/hooks/types.mjs.map +1 -0
  53. package/dist/esm/main.mjs +12 -1
  54. package/dist/esm/main.mjs.map +1 -1
  55. package/dist/esm/messages/format.mjs +66 -4
  56. package/dist/esm/messages/format.mjs.map +1 -1
  57. package/dist/esm/run.mjs +111 -0
  58. package/dist/esm/run.mjs.map +1 -1
  59. package/dist/esm/summarization/node.mjs +44 -0
  60. package/dist/esm/summarization/node.mjs.map +1 -1
  61. package/dist/esm/tools/BashExecutor.mjs +169 -0
  62. package/dist/esm/tools/BashExecutor.mjs.map +1 -0
  63. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +287 -0
  64. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
  65. package/dist/esm/tools/ReadFile.mjs +38 -0
  66. package/dist/esm/tools/ReadFile.mjs.map +1 -0
  67. package/dist/esm/tools/SkillTool.mjs +45 -0
  68. package/dist/esm/tools/SkillTool.mjs.map +1 -0
  69. package/dist/esm/tools/SubagentTool.mjs +85 -0
  70. package/dist/esm/tools/SubagentTool.mjs.map +1 -0
  71. package/dist/esm/tools/ToolNode.mjs +306 -142
  72. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  73. package/dist/esm/tools/skillCatalog.mjs +82 -0
  74. package/dist/esm/tools/skillCatalog.mjs.map +1 -0
  75. package/dist/esm/tools/subagent/SubagentExecutor.mjs +256 -0
  76. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -0
  77. package/dist/types/agents/AgentContext.d.ts +6 -0
  78. package/dist/types/common/enum.d.ts +8 -1
  79. package/dist/types/graphs/Graph.d.ts +2 -0
  80. package/dist/types/hooks/HookRegistry.d.ts +56 -0
  81. package/dist/types/hooks/executeHooks.d.ts +79 -0
  82. package/dist/types/hooks/index.d.ts +6 -0
  83. package/dist/types/hooks/matchers.d.ts +95 -0
  84. package/dist/types/hooks/types.d.ts +320 -0
  85. package/dist/types/index.d.ts +8 -0
  86. package/dist/types/messages/format.d.ts +2 -1
  87. package/dist/types/run.d.ts +1 -0
  88. package/dist/types/summarization/node.d.ts +2 -0
  89. package/dist/types/tools/BashExecutor.d.ts +45 -0
  90. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
  91. package/dist/types/tools/ReadFile.d.ts +28 -0
  92. package/dist/types/tools/SkillTool.d.ts +40 -0
  93. package/dist/types/tools/SubagentTool.d.ts +36 -0
  94. package/dist/types/tools/ToolNode.d.ts +24 -2
  95. package/dist/types/tools/skillCatalog.d.ts +19 -0
  96. package/dist/types/tools/subagent/SubagentExecutor.d.ts +83 -0
  97. package/dist/types/tools/subagent/index.d.ts +2 -0
  98. package/dist/types/types/graph.d.ts +25 -0
  99. package/dist/types/types/index.d.ts +1 -0
  100. package/dist/types/types/llm.d.ts +14 -2
  101. package/dist/types/types/run.d.ts +20 -0
  102. package/dist/types/types/skill.d.ts +9 -0
  103. package/dist/types/types/tools.d.ts +38 -1
  104. package/package.json +2 -1
  105. package/src/agents/AgentContext.ts +26 -2
  106. package/src/common/enum.ts +13 -0
  107. package/src/graphs/Graph.ts +92 -0
  108. package/src/hooks/HookRegistry.ts +208 -0
  109. package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
  110. package/src/hooks/__tests__/compactHooks.test.ts +214 -0
  111. package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
  112. package/src/hooks/__tests__/integration.test.ts +337 -0
  113. package/src/hooks/__tests__/matchers.test.ts +238 -0
  114. package/src/hooks/__tests__/toolHooks.test.ts +669 -0
  115. package/src/hooks/executeHooks.ts +375 -0
  116. package/src/hooks/index.ts +57 -0
  117. package/src/hooks/matchers.ts +280 -0
  118. package/src/hooks/types.ts +404 -0
  119. package/src/index.ts +10 -0
  120. package/src/messages/format.ts +74 -4
  121. package/src/messages/formatAgentMessages.skills.test.ts +334 -0
  122. package/src/run.ts +126 -0
  123. package/src/scripts/multi-agent-subagent.ts +246 -0
  124. package/src/specs/subagent.test.ts +305 -0
  125. package/src/summarization/node.ts +53 -0
  126. package/src/tools/BashExecutor.ts +205 -0
  127. package/src/tools/BashProgrammaticToolCalling.ts +397 -0
  128. package/src/tools/ReadFile.ts +39 -0
  129. package/src/tools/SkillTool.ts +46 -0
  130. package/src/tools/SubagentTool.ts +100 -0
  131. package/src/tools/ToolNode.ts +391 -169
  132. package/src/tools/__tests__/ReadFile.test.ts +44 -0
  133. package/src/tools/__tests__/SkillTool.test.ts +442 -0
  134. package/src/tools/__tests__/SubagentExecutor.test.ts +615 -0
  135. package/src/tools/__tests__/SubagentTool.test.ts +149 -0
  136. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  137. package/src/tools/__tests__/skillCatalog.test.ts +161 -0
  138. package/src/tools/__tests__/subagentHooks.test.ts +215 -0
  139. package/src/tools/skillCatalog.ts +126 -0
  140. package/src/tools/subagent/SubagentExecutor.ts +344 -0
  141. package/src/tools/subagent/index.ts +12 -0
  142. package/src/types/graph.ts +27 -0
  143. package/src/types/index.ts +1 -0
  144. package/src/types/llm.ts +16 -2
  145. package/src/types/run.ts +20 -0
  146. package/src/types/skill.ts +11 -0
  147. package/src/types/tools.ts +41 -1
@@ -0,0 +1,126 @@
1
+ // src/tools/skillCatalog.ts
2
+ import type { SkillCatalogEntry } from '@/types';
3
+
4
+ const HEADER = '## Available Skills';
5
+ const DEFAULT_CONTEXT_WINDOW_TOKENS = 200_000;
6
+ const DEFAULT_BUDGET_PERCENT = 0.01;
7
+ const DEFAULT_MAX_ENTRY_CHARS = 250;
8
+ const DEFAULT_MIN_DESC_LENGTH = 20;
9
+ const DEFAULT_CHARS_PER_TOKEN = 4;
10
+
11
+ export type SkillCatalogOptions = {
12
+ /** Total context window in tokens. Default: 200_000 */
13
+ contextWindowTokens?: number;
14
+ /** Fraction of context budget for catalog. Default: 0.01 (1%) */
15
+ budgetPercent?: number;
16
+ /** Max chars per entry description. Default: 250 */
17
+ maxEntryChars?: number;
18
+ /** Descriptions below this length trigger names-only fallback. Default: 20 */
19
+ minDescLength?: number;
20
+ /** Approximate chars per token for budget calculation. Default: 4 */
21
+ charsPerToken?: number;
22
+ };
23
+
24
+ /**
25
+ * Formats a skill catalog for injection into agent context.
26
+ * Uses a truncation ladder: full descriptions, proportional truncation, names-only.
27
+ * Returns empty string for empty input.
28
+ */
29
+ export function formatSkillCatalog(
30
+ skills: SkillCatalogEntry[],
31
+ opts?: SkillCatalogOptions
32
+ ): string {
33
+ if (skills.length === 0) return '';
34
+
35
+ const contextWindowTokens =
36
+ opts?.contextWindowTokens ?? DEFAULT_CONTEXT_WINDOW_TOKENS;
37
+ const budgetPercent = opts?.budgetPercent ?? DEFAULT_BUDGET_PERCENT;
38
+ const maxEntryChars = Math.max(
39
+ 1,
40
+ opts?.maxEntryChars ?? DEFAULT_MAX_ENTRY_CHARS
41
+ );
42
+ const minDescLength = opts?.minDescLength ?? DEFAULT_MIN_DESC_LENGTH;
43
+ const charsPerToken = opts?.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
44
+
45
+ const budgetChars = Math.floor(
46
+ contextWindowTokens * budgetPercent * charsPerToken
47
+ );
48
+
49
+ const capped = skills.map((s) => ({
50
+ name: s.name,
51
+ description:
52
+ s.description.length > maxEntryChars
53
+ ? s.description.slice(0, maxEntryChars - 1) + '\u2026'
54
+ : s.description,
55
+ }));
56
+
57
+ const fullOutput = formatEntries(capped);
58
+ if (fullOutput.length <= budgetChars) return fullOutput;
59
+
60
+ const headerLen = HEADER.length + 2;
61
+ const newlineChars = capped.length > 1 ? capped.length - 1 : 0;
62
+ const availableChars = budgetChars - headerLen - newlineChars;
63
+ const perEntryOverhead = 4;
64
+ const nameCharsTotal = capped.reduce(
65
+ (sum, s) => sum + s.name.length + perEntryOverhead,
66
+ 0
67
+ );
68
+ const availableForDescs = availableChars - nameCharsTotal;
69
+
70
+ if (availableForDescs <= 0) {
71
+ return fitNamesOnly(capped, budgetChars);
72
+ }
73
+
74
+ const maxDescPerEntry = Math.floor(availableForDescs / capped.length);
75
+
76
+ if (maxDescPerEntry < minDescLength) {
77
+ return fitNamesOnly(capped, budgetChars);
78
+ }
79
+
80
+ const truncated = capped.map((s) => ({
81
+ name: s.name,
82
+ description:
83
+ s.description.length > maxDescPerEntry
84
+ ? s.description.slice(0, maxDescPerEntry - 1) + '\u2026'
85
+ : s.description,
86
+ }));
87
+
88
+ const result = formatEntries(truncated);
89
+ if (result.length <= budgetChars) return result;
90
+ return fitNamesOnly(capped, budgetChars);
91
+ }
92
+
93
+ function formatEntries(
94
+ entries: { name: string; description: string }[]
95
+ ): string {
96
+ const lines = entries.map((e) =>
97
+ e.description ? `- ${e.name}: ${e.description}` : `- ${e.name}`
98
+ );
99
+ return `${HEADER}\n\n${lines.join('\n')}`;
100
+ }
101
+
102
+ /** Names-only fallback that drops trailing entries if the list still exceeds budget. */
103
+ function fitNamesOnly(
104
+ entries: { name: string }[],
105
+ budgetChars: number
106
+ ): string {
107
+ // Format: "HEADER\n\n- name1\n- name2\n..."
108
+ // Running sum avoids O(n²) repeated string construction.
109
+ const prefix = HEADER.length + 2; // "HEADER\n\n"
110
+ const entryOverhead = 2; // "- "
111
+ let total = prefix;
112
+ let fitCount = 0;
113
+
114
+ for (let i = 0; i < entries.length; i++) {
115
+ const added = (i > 0 ? 1 : 0) + entryOverhead + entries[i].name.length;
116
+ if (total + added > budgetChars) break;
117
+ total += added;
118
+ fitCount = i + 1;
119
+ }
120
+
121
+ if (fitCount === 0) return '';
122
+ const namesOnly = entries
123
+ .slice(0, fitCount)
124
+ .map((s) => ({ name: s.name, description: '' }));
125
+ return formatEntries(namesOnly);
126
+ }
@@ -0,0 +1,344 @@
1
+ import { nanoid } from 'nanoid';
2
+ import { HumanMessage } from '@langchain/core/messages';
3
+ import type { BaseMessage } from '@langchain/core/messages';
4
+ import type {
5
+ AgentInputs,
6
+ StandardGraphInput,
7
+ ResolvedSubagentConfig,
8
+ SubagentConfig,
9
+ TokenCounter,
10
+ } from '@/types';
11
+ import type { AggregatedHookResult, HookRegistry } from '@/hooks';
12
+ import type { AgentContext } from '@/agents/AgentContext';
13
+ import type { StandardGraph } from '@/graphs/Graph';
14
+ import { executeHooks } from '@/hooks';
15
+
16
+ const DEFAULT_MAX_TURNS = 25;
17
+ const RECURSION_MULTIPLIER = 3;
18
+ const ERROR_MESSAGE_MAX_CHARS = 200;
19
+
20
+ const HOOK_FALLBACK: AggregatedHookResult = Object.freeze({
21
+ additionalContexts: [] as string[],
22
+ errors: [] as string[],
23
+ });
24
+
25
+ export type SubagentExecuteParams = {
26
+ description: string;
27
+ subagentType: string;
28
+ threadId?: string;
29
+ };
30
+
31
+ export type SubagentExecuteResult = {
32
+ content: string;
33
+ messages: BaseMessage[];
34
+ };
35
+
36
+ /**
37
+ * Factory that constructs a child graph for subagent execution. Injected
38
+ * rather than imported so that `SubagentExecutor` does not have a runtime
39
+ * dependency on `StandardGraph` — this avoids a circular dependency between
40
+ * `src/graphs/Graph.ts` and `src/tools/subagent/` that would otherwise break
41
+ * Rollup's chunking under `preserveModules`.
42
+ */
43
+ export type ChildGraphFactory = (input: StandardGraphInput) => StandardGraph;
44
+
45
+ export type SubagentExecutorOptions = {
46
+ configs: Map<string, ResolvedSubagentConfig>;
47
+ parentSignal?: AbortSignal;
48
+ hookRegistry?: HookRegistry;
49
+ parentRunId: string;
50
+ parentAgentId?: string;
51
+ tokenCounter?: TokenCounter;
52
+ /** Remaining nesting budget. 0 or negative blocks execution. */
53
+ maxDepth?: number;
54
+ /**
55
+ * Factory for constructing the isolated child graph. Callers pass
56
+ * `(input) => new StandardGraph(input)` — injected to break a circular
57
+ * module dependency.
58
+ */
59
+ createChildGraph: ChildGraphFactory;
60
+ };
61
+
62
+ export class SubagentExecutor {
63
+ private readonly configs: Map<string, ResolvedSubagentConfig>;
64
+ private readonly parentSignal?: AbortSignal;
65
+ private readonly hookRegistry?: HookRegistry;
66
+ private readonly parentRunId: string;
67
+ private readonly parentAgentId?: string;
68
+ private readonly tokenCounter?: TokenCounter;
69
+ private readonly maxDepth: number;
70
+ private readonly createChildGraph: ChildGraphFactory;
71
+
72
+ constructor(options: SubagentExecutorOptions) {
73
+ this.configs = options.configs;
74
+ this.parentSignal = options.parentSignal;
75
+ this.hookRegistry = options.hookRegistry;
76
+ this.parentRunId = options.parentRunId;
77
+ this.parentAgentId = options.parentAgentId;
78
+ this.tokenCounter = options.tokenCounter;
79
+ this.maxDepth = options.maxDepth ?? 1;
80
+ this.createChildGraph = options.createChildGraph;
81
+ }
82
+
83
+ async execute(params: SubagentExecuteParams): Promise<SubagentExecuteResult> {
84
+ const { description, subagentType, threadId } = params;
85
+ const config = this.configs.get(subagentType);
86
+
87
+ if (!config) {
88
+ const available = [...this.configs.keys()].join(', ');
89
+ return {
90
+ content: `Error: Unknown subagent type "${subagentType}". Available types: ${available}`,
91
+ messages: [],
92
+ };
93
+ }
94
+
95
+ if (this.maxDepth <= 0) {
96
+ return {
97
+ content: 'Error: Maximum subagent nesting depth exceeded.',
98
+ messages: [],
99
+ };
100
+ }
101
+
102
+ const childAgentId =
103
+ config.agentInputs.agentId ||
104
+ `${this.parentAgentId ?? 'agent'}_sub_${nanoid(8)}`;
105
+
106
+ if (
107
+ this.hookRegistry?.hasHookFor('SubagentStart', this.parentRunId) === true
108
+ ) {
109
+ const hookResult = await executeHooks({
110
+ registry: this.hookRegistry,
111
+ input: {
112
+ hook_event_name: 'SubagentStart',
113
+ runId: this.parentRunId,
114
+ threadId,
115
+ parentAgentId: this.parentAgentId,
116
+ agentId: childAgentId,
117
+ agentType: subagentType,
118
+ inputs: [new HumanMessage(description)],
119
+ },
120
+ sessionId: this.parentRunId,
121
+ matchQuery: subagentType,
122
+ }).catch((): AggregatedHookResult => HOOK_FALLBACK);
123
+
124
+ /**
125
+ * `ask` is treated identically to `deny` in the subagent context:
126
+ * subagents are non-interactive, so there is no prompt path for `ask`.
127
+ * Both decisions block execution and return a "Blocked" tool result.
128
+ */
129
+ if (hookResult.decision === 'deny' || hookResult.decision === 'ask') {
130
+ return {
131
+ content: `Blocked: ${hookResult.reason ?? 'Blocked by hook'}`,
132
+ messages: [],
133
+ };
134
+ }
135
+ }
136
+
137
+ const childInputs = buildChildInputs(config, childAgentId, this.maxDepth);
138
+ const childRunId = `${this.parentRunId}_sub_${nanoid(8)}`;
139
+ const maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
140
+
141
+ const childGraph = this.createChildGraph({
142
+ runId: childRunId,
143
+ signal: this.parentSignal,
144
+ agents: [childInputs],
145
+ tokenCounter: this.tokenCounter,
146
+ });
147
+
148
+ let result: { messages: BaseMessage[] };
149
+ try {
150
+ const workflow = childGraph.createWorkflow();
151
+ /**
152
+ * Detach the child invocation from the parent's callback chain.
153
+ * Without this, `streamEvents` in the parent's `Run.processStream`
154
+ * captures events from the child graph's LLM calls (e.g.
155
+ * `on_chat_model_stream` for the "researcher" agent) and delivers
156
+ * them to the parent's handlers. The parent then tries to resolve
157
+ * the child's agent ID in its own `agentContexts` map and throws
158
+ * "No agent context found for agent ID …". Setting `callbacks: []`
159
+ * overrides the inherited callbacks for this invoke; combined with
160
+ * the child's own empty `handlerRegistry`/`hookRegistry`, the child
161
+ * runs fully isolated.
162
+ *
163
+ * `runName` gives the child a distinct LangSmith trace root (avoids
164
+ * nested trace pollution).
165
+ */
166
+ result = await workflow.invoke(
167
+ { messages: [new HumanMessage(description)] },
168
+ {
169
+ recursionLimit: maxTurns * RECURSION_MULTIPLIER,
170
+ signal: this.parentSignal,
171
+ callbacks: [],
172
+ runName: `subagent:${subagentType}`,
173
+ configurable: {
174
+ thread_id: childRunId,
175
+ },
176
+ }
177
+ );
178
+ } catch (error) {
179
+ childGraph.clearHeavyState();
180
+ return {
181
+ content: `Subagent error: ${truncateErrorMessage(error)}`,
182
+ messages: [],
183
+ };
184
+ }
185
+
186
+ const filteredContent = filterSubagentResult(result.messages);
187
+
188
+ if (
189
+ this.hookRegistry?.hasHookFor('SubagentStop', this.parentRunId) === true
190
+ ) {
191
+ /**
192
+ * Awaited (not fire-and-forget) for deterministic test synchronization
193
+ * and consistency with PostCompact. The parent is already waiting on the
194
+ * tool result, so the small extra latency is acceptable. Errors are
195
+ * swallowed — SubagentStop is observational.
196
+ */
197
+ await executeHooks({
198
+ registry: this.hookRegistry,
199
+ input: {
200
+ hook_event_name: 'SubagentStop',
201
+ runId: this.parentRunId,
202
+ threadId,
203
+ agentId: childAgentId,
204
+ agentType: subagentType,
205
+ messages: result.messages,
206
+ },
207
+ sessionId: this.parentRunId,
208
+ matchQuery: subagentType,
209
+ }).catch(() => {
210
+ /* SubagentStop is observational — swallow errors */
211
+ });
212
+ }
213
+
214
+ childGraph.clearHeavyState();
215
+
216
+ return { content: filteredContent, messages: result.messages };
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Walk messages from last to first, returning the text content of the most
222
+ * recent AIMessage that has any. Non-text blocks (tool_use, thinking,
223
+ * redacted_thinking, tool_result) are stripped. If the last AIMessage is
224
+ * pure tool_use (e.g. the subagent hit `maxTurns` mid-tool-call), the walk
225
+ * continues to earlier AIMessages so partial progress is salvaged — this
226
+ * matches Claude Code's behavior in `agentToolUtils.finalizeAgentTool`.
227
+ * Returns "Task completed" only when no AIMessage in the history contains
228
+ * any text.
229
+ */
230
+ export function filterSubagentResult(messages: BaseMessage[]): string {
231
+ for (let i = messages.length - 1; i >= 0; i--) {
232
+ if (messages[i]._getType() !== 'ai') {
233
+ continue;
234
+ }
235
+
236
+ const content = messages[i].content;
237
+
238
+ if (typeof content === 'string') {
239
+ if (content) return content;
240
+ continue;
241
+ }
242
+
243
+ if (!Array.isArray(content)) {
244
+ continue;
245
+ }
246
+
247
+ const textParts: string[] = [];
248
+ for (const block of content) {
249
+ if (typeof block === 'string') {
250
+ textParts.push(block);
251
+ } else if ('type' in block && block.type === 'text' && 'text' in block) {
252
+ textParts.push(block.text as string);
253
+ }
254
+ }
255
+
256
+ if (textParts.length > 0) {
257
+ return textParts.join('\n');
258
+ }
259
+ }
260
+
261
+ return 'Task completed';
262
+ }
263
+
264
+ /**
265
+ * Resolve self-spawn configs by filling in agentInputs from the parent context.
266
+ * Returns configs with agentInputs guaranteed present. Throws on duplicate
267
+ * `type` values to prevent silent config shadowing.
268
+ */
269
+ export function resolveSubagentConfigs(
270
+ configs: SubagentConfig[],
271
+ parentContext: AgentContext
272
+ ): ResolvedSubagentConfig[] {
273
+ const resolved = configs
274
+ .map((config) => {
275
+ if (config.agentInputs != null) {
276
+ return config as ResolvedSubagentConfig;
277
+ }
278
+ if (config.self !== true || parentContext._sourceInputs == null) {
279
+ return null;
280
+ }
281
+ return {
282
+ ...config,
283
+ agentInputs: { ...parentContext._sourceInputs },
284
+ } as ResolvedSubagentConfig;
285
+ })
286
+ .filter((c): c is ResolvedSubagentConfig => c != null);
287
+
288
+ const seenTypes = new Set<string>();
289
+ for (const config of resolved) {
290
+ if (seenTypes.has(config.type)) {
291
+ throw new Error(
292
+ `Duplicate subagent type "${config.type}". Each SubagentConfig must have a unique "type" field.`
293
+ );
294
+ }
295
+ seenTypes.add(config.type);
296
+ }
297
+
298
+ return resolved;
299
+ }
300
+
301
+ /**
302
+ * Build child AgentInputs from a resolved config, stripping nesting and
303
+ * event-driven fields. When `allowNested: true`, the child's
304
+ * `maxSubagentDepth` is decremented so that depth is consumed as the call
305
+ * chain deepens across graph boundaries — the parent's executor-level check
306
+ * alone cannot see into the child graph's separate executor.
307
+ *
308
+ * @remarks Advanced utility: exported primarily for testing and by
309
+ * {@link SubagentExecutor}. Host applications configuring subagents should
310
+ * not need to call this directly — it is invoked internally when a subagent
311
+ * tool is dispatched. The depth-countdown contract (parent's `maxDepth` in,
312
+ * child's decremented `maxSubagentDepth` on the returned inputs) is the
313
+ * mechanism that bounds nesting across graph boundaries; callers must
314
+ * respect it.
315
+ */
316
+ export function buildChildInputs(
317
+ config: ResolvedSubagentConfig,
318
+ childAgentId: string,
319
+ parentMaxDepth: number
320
+ ): AgentInputs {
321
+ const { agentInputs } = config;
322
+ const childInputs: AgentInputs = {
323
+ ...agentInputs,
324
+ agentId: childAgentId,
325
+ toolDefinitions: undefined,
326
+ };
327
+
328
+ if (config.allowNested === true) {
329
+ childInputs.maxSubagentDepth = Math.max(0, parentMaxDepth - 1);
330
+ } else {
331
+ childInputs.subagentConfigs = undefined;
332
+ childInputs.maxSubagentDepth = undefined;
333
+ }
334
+
335
+ return childInputs;
336
+ }
337
+
338
+ function truncateErrorMessage(error: unknown): string {
339
+ const message = error instanceof Error ? error.message : String(error);
340
+ if (message.length <= ERROR_MESSAGE_MAX_CHARS) {
341
+ return message;
342
+ }
343
+ return `${message.slice(0, ERROR_MESSAGE_MAX_CHARS)}...`;
344
+ }
@@ -0,0 +1,12 @@
1
+ export {
2
+ SubagentExecutor,
3
+ filterSubagentResult,
4
+ resolveSubagentConfigs,
5
+ buildChildInputs,
6
+ } from './SubagentExecutor';
7
+ export type {
8
+ SubagentExecuteParams,
9
+ SubagentExecuteResult,
10
+ SubagentExecutorOptions,
11
+ ChildGraphFactory,
12
+ } from './SubagentExecutor';
@@ -388,6 +388,29 @@ export type MultiAgentGraphInput = StandardGraphInput & {
388
388
  edges: GraphEdge[];
389
389
  };
390
390
 
391
+ /** Configuration for a subagent type that can be spawned by a parent agent. */
392
+ export type SubagentConfig = {
393
+ /** Identifier used in the tool's `subagent_type` enum (e.g. 'researcher', 'coder'). */
394
+ type: string;
395
+ /** Human-readable display name. */
396
+ name: string;
397
+ /** What this subagent specializes in — shown to the LLM. */
398
+ description: string;
399
+ /** Full agent config for the child graph. Omit when `self` is true. */
400
+ agentInputs?: AgentInputs;
401
+ /** When true, reuse the parent's AgentInputs (context isolation without separate config). */
402
+ self?: boolean;
403
+ /** Max AGENT→TOOLS cycles before forced stop (default: 25). */
404
+ maxTurns?: number;
405
+ /** Allow this subagent to spawn its own subagents (default: false). */
406
+ allowNested?: boolean;
407
+ };
408
+
409
+ /** SubagentConfig with agentInputs guaranteed present (self-spawn resolved). */
410
+ export type ResolvedSubagentConfig = SubagentConfig & {
411
+ agentInputs: AgentInputs;
412
+ };
413
+
391
414
  export interface AgentInputs {
392
415
  agentId: string;
393
416
  /** Human-readable name for the agent (used in handoff context). Defaults to agentId if not provided. */
@@ -431,6 +454,10 @@ export interface AgentInputs {
431
454
  maxToolResultChars?: number;
432
455
  /** Pre-computed tool schema token count (from cache). Skips recalculation when provided. */
433
456
  toolSchemaTokens?: number;
457
+ /** Subagent configurations for hierarchical delegation. Each defines a child agent type. */
458
+ subagentConfigs?: SubagentConfig[];
459
+ /** Maximum subagent nesting depth. Default 1 means top-level agents can spawn subagents but subagents cannot nest further. */
460
+ maxSubagentDepth?: number;
434
461
  }
435
462
 
436
463
  export interface ContextPruningConfig {
@@ -2,6 +2,7 @@
2
2
  export * from './graph';
3
3
  export * from './llm';
4
4
  export * from './run';
5
+ export * from './skill';
5
6
  export * from './stream';
6
7
  export * from './tools';
7
8
  export * from './summarize';
package/src/types/llm.ts CHANGED
@@ -45,7 +45,20 @@ export type AzureClientOptions = Partial<OpenAIChatInput> &
45
45
  } & BaseChatModelParams & {
46
46
  configuration?: OAIClientOptions;
47
47
  };
48
- export type ThinkingConfig = AnthropicInput['thinking'];
48
+ /**
49
+ * Controls whether Claude's reasoning content is returned in adaptive
50
+ * thinking responses. Added for Claude Opus 4.7, which omits thinking by
51
+ * default unless the caller opts in with `'summarized'`.
52
+ * @see https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7#thinking-content-omitted-by-default
53
+ */
54
+ export type ThinkingDisplay = 'summarized' | 'omitted';
55
+ export type ThinkingConfigAdaptive = {
56
+ type: 'adaptive';
57
+ display?: ThinkingDisplay;
58
+ };
59
+ export type ThinkingConfig =
60
+ | NonNullable<AnthropicInput['thinking']>
61
+ | ThinkingConfigAdaptive;
49
62
  export type ChatOpenAIToolType =
50
63
  | BindToolsInput
51
64
  | OpenAIClient.ChatCompletionTool;
@@ -60,7 +73,8 @@ export type GoogleThinkingConfig = {
60
73
  thinkingLevel?: string;
61
74
  };
62
75
  export type OpenAIClientOptions = ChatOpenAIFields;
63
- export type AnthropicClientOptions = AnthropicInput & {
76
+ export type AnthropicClientOptions = Omit<AnthropicInput, 'thinking'> & {
77
+ thinking?: ThinkingConfig;
64
78
  promptCache?: boolean;
65
79
  };
66
80
  export type MistralAIClientOptions = ChatMistralAIInput;
package/src/types/run.ts CHANGED
@@ -11,6 +11,8 @@ import type * as s from '@/types/stream';
11
11
  import type * as e from '@/common/enum';
12
12
  import type * as g from '@/types/graph';
13
13
  import type * as l from '@/types/llm';
14
+ import type { ToolSessionMap } from '@/types/tools';
15
+ import type { HookRegistry } from '@/hooks';
14
16
 
15
17
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
18
  export type ZodObjectAny = z.ZodObject<any, any, any, any>;
@@ -112,6 +114,18 @@ export type RunConfig = {
112
114
  runId: string;
113
115
  graphConfig: LegacyGraphConfig | StandardGraphConfig | MultiAgentGraphConfig;
114
116
  customHandlers?: Record<string, g.EventHandler>;
117
+ /**
118
+ * Pre-constructed hook registry for this run. Hooks fire at lifecycle
119
+ * points in `processStream` (RunStart, UserPromptSubmit, Stop,
120
+ * StopFailure) and around tool calls (PreToolUse, PostToolUse,
121
+ * PostToolUseFailure, PermissionDenied).
122
+ *
123
+ * Pass `undefined` (the default) to skip all hook dispatch. When a
124
+ * registry is provided, the run attaches it to the `Graph` so internal
125
+ * nodes can fire hooks too, and clears the session in the `finally`
126
+ * block to prevent leaks.
127
+ */
128
+ hooks?: HookRegistry;
115
129
  returnContent?: boolean;
116
130
  tokenCounter?: TokenCounter;
117
131
  indexTokenCountMap?: Record<string, number>;
@@ -126,6 +140,12 @@ export type RunConfig = {
126
140
  calibrationRatio?: number;
127
141
  /** Skip post-stream cleanup (clearHeavyState) — useful for tests that inspect graph state after processStream */
128
142
  skipCleanup?: boolean;
143
+ /**
144
+ * Initial session state to seed the Graph's ToolSessionMap.
145
+ * Used to carry over code environment sessions from skill file priming
146
+ * at run start, so ToolNode can inject session_id + files into tool calls.
147
+ */
148
+ initialSessions?: ToolSessionMap;
129
149
  };
130
150
 
131
151
  export type ProvidedCallbacks =
@@ -0,0 +1,11 @@
1
+ // src/types/skill.ts
2
+
3
+ /** Minimal skill metadata for catalog assembly. The host provides these from its own data layer. */
4
+ export type SkillCatalogEntry = {
5
+ /** Kebab-case identifier (what the model passes to SkillTool) */
6
+ name: string;
7
+ /** One-line description for the catalog listing */
8
+ description: string;
9
+ /** Optional human-readable label (UI only, not shown to model) */
10
+ displayTitle?: string;
11
+ };
@@ -2,7 +2,8 @@
2
2
  import type { StructuredToolInterface } from '@langchain/core/tools';
3
3
  import type { RunnableToolLike } from '@langchain/core/runnables';
4
4
  import type { ToolCall } from '@langchain/core/messages/tool';
5
- import type { ToolErrorData } from './stream';
5
+ import type { HookRegistry } from '@/hooks';
6
+ import type { MessageContentComplex, ToolErrorData } from './stream';
6
7
  import { EnvVar } from '@/common';
7
8
 
8
9
  /** Replacement type for `import type { ToolCall } from '@langchain/core/messages/tool'` in order to have stringified args typed */
@@ -49,6 +50,12 @@ export type ToolNodeOptions = {
49
50
  agentId?: string;
50
51
  /** Tool names that must be executed directly (via runTool) even in event-driven mode (e.g., graph-managed handoff tools) */
51
52
  directToolNames?: Set<string>;
53
+ /**
54
+ * Hook registry for PreToolUse/PostToolUse lifecycle hooks.
55
+ * Only fires for event-driven tool calls (`dispatchToolEvents`). Tools
56
+ * routed through `directToolNames` bypass hook dispatch entirely.
57
+ */
58
+ hookRegistry?: HookRegistry;
52
59
  /** Max context tokens for the agent — used to compute tool result truncation limits. */
53
60
  maxContextTokens?: number;
54
61
  /**
@@ -186,6 +193,26 @@ export type ToolExecuteBatchRequest = {
186
193
  reject: (error: Error) => void;
187
194
  };
188
195
 
196
+ /**
197
+ * A message injected into graph state by any tool execution handler.
198
+ * Generic mechanism: any tool returning `injectedMessages` in its `ToolExecuteResult`
199
+ * will have these appended to state after the ToolMessage for this call.
200
+ */
201
+ export type InjectedMessage = {
202
+ /** 'user' for skill body injection, 'system' for context hints.
203
+ * Both are converted to HumanMessage at runtime; the original role
204
+ * is preserved in additional_kwargs.role. */
205
+ role: 'user' | 'system';
206
+ /** Message content: string for simple text, array for complex multi-part content */
207
+ content: string | MessageContentComplex[];
208
+ /** When true, the message is framework-internal: not shown in UI, not counted as a user turn */
209
+ isMeta?: boolean;
210
+ /** Origin tag for downstream consumers (UI, pruner, compaction) */
211
+ source?: 'skill' | 'hook' | 'system';
212
+ /** Only set when source is 'skill', for compaction preservation */
213
+ skillName?: string;
214
+ };
215
+
189
216
  /** Result for a single tool call in event-driven execution */
190
217
  export type ToolExecuteResult = {
191
218
  /** Matches ToolCallRequest.id */
@@ -198,6 +225,13 @@ export type ToolExecuteResult = {
198
225
  status: 'success' | 'error';
199
226
  /** Error message if status is 'error' */
200
227
  errorMessage?: string;
228
+ /**
229
+ * Messages to inject into graph state after the ToolMessage for this call.
230
+ * Placed after tool results to respect provider message ordering (tool_call -> tool_result adjacency).
231
+ * The host's message formatter may merge injected user messages with the preceding tool_result turn.
232
+ * Generic mechanism: any tool execution handler can use this.
233
+ */
234
+ injectedMessages?: InjectedMessage[];
201
235
  };
202
236
 
203
237
  /** Map of tool names to tool definitions */
@@ -318,6 +352,12 @@ export type ProgrammaticExecutionArtifact = {
318
352
  files?: FileRefs;
319
353
  };
320
354
 
355
+ /** Parameters for creating a bash execution tool (same API as CodeExecutor, bash-only) */
356
+ export type BashExecutionToolParams = CodeExecutionToolParams;
357
+
358
+ /** Parameters for creating a bash programmatic tool calling tool (same API as PTC, bash-only) */
359
+ export type BashProgrammaticToolCallingParams = ProgrammaticToolCallingParams;
360
+
321
361
  /**
322
362
  * Initialization parameters for the PTC tool
323
363
  */