@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.
- package/dist/cjs/agents/AgentContext.cjs +23 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +14 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +72 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +162 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
- package/dist/cjs/hooks/executeHooks.cjs +276 -0
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
- package/dist/cjs/hooks/matchers.cjs +256 -0
- package/dist/cjs/hooks/matchers.cjs.map +1 -0
- package/dist/cjs/hooks/types.cjs +27 -0
- package/dist/cjs/hooks/types.cjs.map +1 -0
- package/dist/cjs/main.cjs +52 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +74 -12
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +111 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +44 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +175 -0
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +296 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/ReadFile.cjs +43 -0
- package/dist/cjs/tools/ReadFile.cjs.map +1 -0
- package/dist/cjs/tools/SkillTool.cjs +50 -0
- package/dist/cjs/tools/SkillTool.cjs.map +1 -0
- package/dist/cjs/tools/SubagentTool.cjs +92 -0
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
- package/dist/cjs/tools/ToolNode.cjs +304 -140
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/skillCatalog.cjs +84 -0
- package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +261 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +23 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +13 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +72 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +160 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
- package/dist/esm/hooks/executeHooks.mjs +273 -0
- package/dist/esm/hooks/executeHooks.mjs.map +1 -0
- package/dist/esm/hooks/matchers.mjs +251 -0
- package/dist/esm/hooks/matchers.mjs.map +1 -0
- package/dist/esm/hooks/types.mjs +25 -0
- package/dist/esm/hooks/types.mjs.map +1 -0
- package/dist/esm/main.mjs +12 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +66 -4
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +111 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +44 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +169 -0
- package/dist/esm/tools/BashExecutor.mjs.map +1 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +287 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/ReadFile.mjs +38 -0
- package/dist/esm/tools/ReadFile.mjs.map +1 -0
- package/dist/esm/tools/SkillTool.mjs +45 -0
- package/dist/esm/tools/SkillTool.mjs.map +1 -0
- package/dist/esm/tools/SubagentTool.mjs +85 -0
- package/dist/esm/tools/SubagentTool.mjs.map +1 -0
- package/dist/esm/tools/ToolNode.mjs +306 -142
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/skillCatalog.mjs +82 -0
- package/dist/esm/tools/skillCatalog.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +256 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +6 -0
- package/dist/types/common/enum.d.ts +8 -1
- package/dist/types/graphs/Graph.d.ts +2 -0
- package/dist/types/hooks/HookRegistry.d.ts +56 -0
- package/dist/types/hooks/executeHooks.d.ts +79 -0
- package/dist/types/hooks/index.d.ts +6 -0
- package/dist/types/hooks/matchers.d.ts +95 -0
- package/dist/types/hooks/types.d.ts +320 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/messages/format.d.ts +2 -1
- package/dist/types/run.d.ts +1 -0
- package/dist/types/summarization/node.d.ts +2 -0
- package/dist/types/tools/BashExecutor.d.ts +45 -0
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
- package/dist/types/tools/ReadFile.d.ts +28 -0
- package/dist/types/tools/SkillTool.d.ts +40 -0
- package/dist/types/tools/SubagentTool.d.ts +36 -0
- package/dist/types/tools/ToolNode.d.ts +24 -2
- package/dist/types/tools/skillCatalog.d.ts +19 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +83 -0
- package/dist/types/tools/subagent/index.d.ts +2 -0
- package/dist/types/types/graph.d.ts +25 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/llm.d.ts +14 -2
- package/dist/types/types/run.d.ts +20 -0
- package/dist/types/types/skill.d.ts +9 -0
- package/dist/types/types/tools.d.ts +38 -1
- package/package.json +2 -1
- package/src/agents/AgentContext.ts +26 -2
- package/src/common/enum.ts +13 -0
- package/src/graphs/Graph.ts +92 -0
- package/src/hooks/HookRegistry.ts +208 -0
- package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
- package/src/hooks/__tests__/compactHooks.test.ts +214 -0
- package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
- package/src/hooks/__tests__/integration.test.ts +337 -0
- package/src/hooks/__tests__/matchers.test.ts +238 -0
- package/src/hooks/__tests__/toolHooks.test.ts +669 -0
- package/src/hooks/executeHooks.ts +375 -0
- package/src/hooks/index.ts +57 -0
- package/src/hooks/matchers.ts +280 -0
- package/src/hooks/types.ts +404 -0
- package/src/index.ts +10 -0
- package/src/messages/format.ts +74 -4
- package/src/messages/formatAgentMessages.skills.test.ts +334 -0
- package/src/run.ts +126 -0
- package/src/scripts/multi-agent-subagent.ts +246 -0
- package/src/specs/subagent.test.ts +305 -0
- package/src/summarization/node.ts +53 -0
- package/src/tools/BashExecutor.ts +205 -0
- package/src/tools/BashProgrammaticToolCalling.ts +397 -0
- package/src/tools/ReadFile.ts +39 -0
- package/src/tools/SkillTool.ts +46 -0
- package/src/tools/SubagentTool.ts +100 -0
- package/src/tools/ToolNode.ts +391 -169
- package/src/tools/__tests__/ReadFile.test.ts +44 -0
- package/src/tools/__tests__/SkillTool.test.ts +442 -0
- package/src/tools/__tests__/SubagentExecutor.test.ts +615 -0
- package/src/tools/__tests__/SubagentTool.test.ts +149 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
- package/src/tools/__tests__/skillCatalog.test.ts +161 -0
- package/src/tools/__tests__/subagentHooks.test.ts +215 -0
- package/src/tools/skillCatalog.ts +126 -0
- package/src/tools/subagent/SubagentExecutor.ts +344 -0
- package/src/tools/subagent/index.ts +12 -0
- package/src/types/graph.ts +27 -0
- package/src/types/index.ts +1 -0
- package/src/types/llm.ts +16 -2
- package/src/types/run.ts +20 -0
- package/src/types/skill.ts +11 -0
- 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';
|
package/src/types/graph.ts
CHANGED
|
@@ -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 {
|
package/src/types/index.ts
CHANGED
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
|
-
|
|
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
|
+
};
|
package/src/types/tools.ts
CHANGED
|
@@ -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 {
|
|
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
|
*/
|