@librechat/agents 3.1.64 → 3.1.66-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 (126) hide show
  1. package/dist/cjs/common/enum.cjs +13 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +3 -0
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/hooks/HookRegistry.cjs +162 -0
  6. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
  7. package/dist/cjs/hooks/executeHooks.cjs +276 -0
  8. package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
  9. package/dist/cjs/hooks/matchers.cjs +256 -0
  10. package/dist/cjs/hooks/matchers.cjs.map +1 -0
  11. package/dist/cjs/hooks/types.cjs +27 -0
  12. package/dist/cjs/hooks/types.cjs.map +1 -0
  13. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  14. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +69 -54
  15. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  16. package/dist/cjs/main.cjs +40 -0
  17. package/dist/cjs/main.cjs.map +1 -1
  18. package/dist/cjs/messages/core.cjs +8 -1
  19. package/dist/cjs/messages/core.cjs.map +1 -1
  20. package/dist/cjs/messages/format.cjs +74 -12
  21. package/dist/cjs/messages/format.cjs.map +1 -1
  22. package/dist/cjs/run.cjs +111 -0
  23. package/dist/cjs/run.cjs.map +1 -1
  24. package/dist/cjs/tools/BashExecutor.cjs +175 -0
  25. package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
  26. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +296 -0
  27. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
  28. package/dist/cjs/tools/ReadFile.cjs +43 -0
  29. package/dist/cjs/tools/ReadFile.cjs.map +1 -0
  30. package/dist/cjs/tools/SkillTool.cjs +50 -0
  31. package/dist/cjs/tools/SkillTool.cjs.map +1 -0
  32. package/dist/cjs/tools/ToolNode.cjs +304 -140
  33. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  34. package/dist/cjs/tools/skillCatalog.cjs +84 -0
  35. package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
  36. package/dist/esm/common/enum.mjs +12 -1
  37. package/dist/esm/common/enum.mjs.map +1 -1
  38. package/dist/esm/graphs/Graph.mjs +3 -0
  39. package/dist/esm/graphs/Graph.mjs.map +1 -1
  40. package/dist/esm/hooks/HookRegistry.mjs +160 -0
  41. package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
  42. package/dist/esm/hooks/executeHooks.mjs +273 -0
  43. package/dist/esm/hooks/executeHooks.mjs.map +1 -0
  44. package/dist/esm/hooks/matchers.mjs +251 -0
  45. package/dist/esm/hooks/matchers.mjs.map +1 -0
  46. package/dist/esm/hooks/types.mjs +25 -0
  47. package/dist/esm/hooks/types.mjs.map +1 -0
  48. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  49. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +69 -54
  50. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  51. package/dist/esm/main.mjs +10 -1
  52. package/dist/esm/main.mjs.map +1 -1
  53. package/dist/esm/messages/core.mjs +8 -1
  54. package/dist/esm/messages/core.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/tools/BashExecutor.mjs +169 -0
  60. package/dist/esm/tools/BashExecutor.mjs.map +1 -0
  61. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +287 -0
  62. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
  63. package/dist/esm/tools/ReadFile.mjs +38 -0
  64. package/dist/esm/tools/ReadFile.mjs.map +1 -0
  65. package/dist/esm/tools/SkillTool.mjs +45 -0
  66. package/dist/esm/tools/SkillTool.mjs.map +1 -0
  67. package/dist/esm/tools/ToolNode.mjs +306 -142
  68. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  69. package/dist/esm/tools/skillCatalog.mjs +82 -0
  70. package/dist/esm/tools/skillCatalog.mjs.map +1 -0
  71. package/dist/types/common/enum.d.ts +7 -1
  72. package/dist/types/graphs/Graph.d.ts +2 -0
  73. package/dist/types/hooks/HookRegistry.d.ts +56 -0
  74. package/dist/types/hooks/executeHooks.d.ts +79 -0
  75. package/dist/types/hooks/index.d.ts +6 -0
  76. package/dist/types/hooks/matchers.d.ts +95 -0
  77. package/dist/types/hooks/types.d.ts +309 -0
  78. package/dist/types/index.d.ts +6 -0
  79. package/dist/types/llm/anthropic/types.d.ts +1 -1
  80. package/dist/types/messages/format.d.ts +2 -1
  81. package/dist/types/run.d.ts +1 -0
  82. package/dist/types/tools/BashExecutor.d.ts +45 -0
  83. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
  84. package/dist/types/tools/ReadFile.d.ts +28 -0
  85. package/dist/types/tools/SkillTool.d.ts +40 -0
  86. package/dist/types/tools/ToolNode.d.ts +24 -2
  87. package/dist/types/tools/skillCatalog.d.ts +19 -0
  88. package/dist/types/types/index.d.ts +1 -0
  89. package/dist/types/types/run.d.ts +20 -0
  90. package/dist/types/types/skill.d.ts +9 -0
  91. package/dist/types/types/tools.d.ts +38 -1
  92. package/package.json +2 -2
  93. package/src/common/enum.ts +12 -0
  94. package/src/graphs/Graph.ts +4 -0
  95. package/src/hooks/HookRegistry.ts +208 -0
  96. package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
  97. package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
  98. package/src/hooks/__tests__/integration.test.ts +337 -0
  99. package/src/hooks/__tests__/matchers.test.ts +238 -0
  100. package/src/hooks/__tests__/toolHooks.test.ts +669 -0
  101. package/src/hooks/executeHooks.ts +375 -0
  102. package/src/hooks/index.ts +55 -0
  103. package/src/hooks/matchers.ts +280 -0
  104. package/src/hooks/types.ts +388 -0
  105. package/src/index.ts +8 -0
  106. package/src/llm/anthropic/types.ts +1 -1
  107. package/src/llm/anthropic/utils/message_inputs.ts +93 -68
  108. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +349 -0
  109. package/src/messages/core.ts +8 -1
  110. package/src/messages/format.ts +74 -4
  111. package/src/messages/formatAgentMessages.skills.test.ts +334 -0
  112. package/src/run.ts +126 -0
  113. package/src/tools/BashExecutor.ts +205 -0
  114. package/src/tools/BashProgrammaticToolCalling.ts +397 -0
  115. package/src/tools/ReadFile.ts +39 -0
  116. package/src/tools/SkillTool.ts +46 -0
  117. package/src/tools/ToolNode.ts +391 -169
  118. package/src/tools/__tests__/ReadFile.test.ts +44 -0
  119. package/src/tools/__tests__/SkillTool.test.ts +442 -0
  120. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  121. package/src/tools/__tests__/skillCatalog.test.ts +161 -0
  122. package/src/tools/skillCatalog.ts +126 -0
  123. package/src/types/index.ts +1 -0
  124. package/src/types/run.ts +20 -0
  125. package/src/types/skill.ts +11 -0
  126. package/src/types/tools.ts +41 -1
@@ -0,0 +1,161 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { formatSkillCatalog } from '../skillCatalog';
3
+ import type { SkillCatalogEntry } from '@/types';
4
+
5
+ describe('formatSkillCatalog', () => {
6
+ it('returns empty string for empty array', () => {
7
+ expect(formatSkillCatalog([])).toBe('');
8
+ });
9
+
10
+ it('formats a single skill with header', () => {
11
+ const skills: SkillCatalogEntry[] = [
12
+ {
13
+ name: 'pdf-processor',
14
+ description: 'Processes PDF files into structured data.',
15
+ },
16
+ ];
17
+ const result = formatSkillCatalog(skills);
18
+ expect(result).toBe(
19
+ '## Available Skills\n\n- pdf-processor: Processes PDF files into structured data.'
20
+ );
21
+ });
22
+
23
+ it('formats multiple skills within budget', () => {
24
+ const skills: SkillCatalogEntry[] = [
25
+ { name: 'pdf-processor', description: 'Processes PDF files.' },
26
+ { name: 'review-pr', description: 'Reviews pull requests.' },
27
+ { name: 'meeting-notes', description: 'Formats meeting transcripts.' },
28
+ ];
29
+ const result = formatSkillCatalog(skills);
30
+ expect(result).toContain('## Available Skills');
31
+ expect(result).toContain('- pdf-processor: Processes PDF files.');
32
+ expect(result).toContain('- review-pr: Reviews pull requests.');
33
+ expect(result).toContain('- meeting-notes: Formats meeting transcripts.');
34
+ });
35
+
36
+ it('caps per-entry descriptions at maxEntryChars', () => {
37
+ const longDesc = 'A'.repeat(300);
38
+ const skills: SkillCatalogEntry[] = [
39
+ { name: 'long-skill', description: longDesc },
40
+ ];
41
+ const result = formatSkillCatalog(skills);
42
+ expect(result).toContain('- long-skill: ' + 'A'.repeat(249) + '\u2026');
43
+ expect(result).not.toContain('A'.repeat(300));
44
+ });
45
+
46
+ it('truncates descriptions proportionally when over budget', () => {
47
+ const skills: SkillCatalogEntry[] = Array.from({ length: 10 }, (_, i) => ({
48
+ name: `sk-${i}`,
49
+ description: 'D'.repeat(200),
50
+ }));
51
+ // Budget = 10000 * 0.01 * 4 = 400 chars — enough for names + short descs, not full 200-char descs
52
+ const result = formatSkillCatalog(skills, {
53
+ contextWindowTokens: 10000,
54
+ budgetPercent: 0.01,
55
+ charsPerToken: 4,
56
+ });
57
+ expect(result).toContain('## Available Skills');
58
+ for (let i = 0; i < 10; i++) {
59
+ expect(result).toContain(`sk-${i}`);
60
+ }
61
+ // Full 200-char descriptions should be truncated
62
+ expect(result).not.toContain('D'.repeat(200));
63
+ });
64
+
65
+ it('falls back to names-only when extremely over budget', () => {
66
+ const skills: SkillCatalogEntry[] = Array.from({ length: 10 }, (_, i) => ({
67
+ name: `s${i}`,
68
+ description: 'Very detailed description that is quite long and verbose.',
69
+ }));
70
+ // Budget = 2000 * 0.01 * 4 = 80 chars — enough for names-only but not descriptions
71
+ const result = formatSkillCatalog(skills, {
72
+ contextWindowTokens: 2000,
73
+ budgetPercent: 0.01,
74
+ charsPerToken: 4,
75
+ });
76
+ expect(result).toContain('## Available Skills');
77
+ expect(result).toContain('- s0');
78
+ // Verify entry lines have no descriptions (names-only format)
79
+ const entryLines = result.split('\n').filter((l) => l.startsWith('- '));
80
+ for (const line of entryLines) {
81
+ expect(line).toMatch(/^- s\d+$/);
82
+ }
83
+ });
84
+
85
+ it('respects custom options', () => {
86
+ const skills: SkillCatalogEntry[] = [
87
+ { name: 'test', description: 'A'.repeat(100) },
88
+ ];
89
+ const result = formatSkillCatalog(skills, { maxEntryChars: 50 });
90
+ expect(result).toContain('A'.repeat(49) + '\u2026');
91
+ expect(result).not.toContain('A'.repeat(100));
92
+ });
93
+
94
+ it('includes skills with descriptions shorter than minDescLength', () => {
95
+ const skills: SkillCatalogEntry[] = [
96
+ { name: 'short', description: 'Hi' },
97
+ { name: 'normal', description: 'A normal description here.' },
98
+ ];
99
+ const result = formatSkillCatalog(skills);
100
+ expect(result).toContain('- short: Hi');
101
+ expect(result).toContain('- normal: A normal description here.');
102
+ });
103
+
104
+ it('handles all skills with zero-length descriptions as names-only', () => {
105
+ const skills: SkillCatalogEntry[] = [
106
+ { name: 'alpha', description: '' },
107
+ { name: 'beta', description: '' },
108
+ ];
109
+ const result = formatSkillCatalog(skills);
110
+ expect(result).toBe('## Available Skills\n\n- alpha\n- beta');
111
+ });
112
+
113
+ it('has no trailing or leading whitespace', () => {
114
+ const skills: SkillCatalogEntry[] = [
115
+ { name: 'test', description: 'A test skill.' },
116
+ ];
117
+ const result = formatSkillCatalog(skills);
118
+ expect(result).toBe(result.trim());
119
+ const lines = result.split('\n');
120
+ for (const line of lines) {
121
+ expect(line).toBe(line.trimEnd());
122
+ }
123
+ });
124
+
125
+ it('truncates names-only list when even names exceed budget', () => {
126
+ const skills: SkillCatalogEntry[] = Array.from({ length: 100 }, (_, i) => ({
127
+ name: `skill-with-a-long-name-${i}`,
128
+ description: 'Some description.',
129
+ }));
130
+ // Budget so small that even names-only for 100 skills exceeds it
131
+ const result = formatSkillCatalog(skills, {
132
+ contextWindowTokens: 100,
133
+ budgetPercent: 0.01,
134
+ charsPerToken: 4,
135
+ });
136
+ // Should still have the header and at least one entry, but not all 100
137
+ if (result === '') {
138
+ // Budget too small for even one entry — valid edge case
139
+ expect(result).toBe('');
140
+ } else {
141
+ expect(result).toContain('## Available Skills');
142
+ const entryLines = result.split('\n').filter((l) => l.startsWith('- '));
143
+ expect(entryLines.length).toBeLessThan(100);
144
+ expect(entryLines.length).toBeGreaterThan(0);
145
+ expect(result.length).toBeLessThanOrEqual(100 * 0.01 * 4);
146
+ }
147
+ });
148
+
149
+ it('ignores displayTitle in output', () => {
150
+ const skills: SkillCatalogEntry[] = [
151
+ {
152
+ name: 'my-skill',
153
+ description: 'Does stuff.',
154
+ displayTitle: 'My Fancy Skill',
155
+ },
156
+ ];
157
+ const result = formatSkillCatalog(skills);
158
+ expect(result).not.toContain('My Fancy Skill');
159
+ expect(result).toContain('- my-skill: Does stuff.');
160
+ });
161
+ });
@@ -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
+ }
@@ -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/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
  */