@librechat/agents 3.1.77 → 3.1.78-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 (181) hide show
  1. package/dist/cjs/common/enum.cjs +54 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +148 -4
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +291 -0
  6. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -0
  7. package/dist/cjs/main.cjs +90 -0
  8. package/dist/cjs/main.cjs.map +1 -1
  9. package/dist/cjs/messages/anthropicToolCache.cjs +102 -0
  10. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -0
  11. package/dist/cjs/messages/prune.cjs +27 -0
  12. package/dist/cjs/messages/prune.cjs.map +1 -1
  13. package/dist/cjs/messages/recency.cjs +99 -0
  14. package/dist/cjs/messages/recency.cjs.map +1 -0
  15. package/dist/cjs/run.cjs +30 -0
  16. package/dist/cjs/run.cjs.map +1 -1
  17. package/dist/cjs/summarization/node.cjs +100 -6
  18. package/dist/cjs/summarization/node.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +635 -23
  20. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  21. package/dist/cjs/tools/local/CompileCheckTool.cjs +227 -0
  22. package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -0
  23. package/dist/cjs/tools/local/FileCheckpointer.cjs +90 -0
  24. package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -0
  25. package/dist/cjs/tools/local/LocalCodingTools.cjs +1098 -0
  26. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -0
  27. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +1042 -0
  28. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -0
  29. package/dist/cjs/tools/local/LocalExecutionTools.cjs +122 -0
  30. package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -0
  31. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +453 -0
  32. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -0
  33. package/dist/cjs/tools/local/attachments.cjs +183 -0
  34. package/dist/cjs/tools/local/attachments.cjs.map +1 -0
  35. package/dist/cjs/tools/local/bashAst.cjs +129 -0
  36. package/dist/cjs/tools/local/bashAst.cjs.map +1 -0
  37. package/dist/cjs/tools/local/editStrategies.cjs +188 -0
  38. package/dist/cjs/tools/local/editStrategies.cjs.map +1 -0
  39. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +141 -0
  40. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -0
  41. package/dist/cjs/tools/local/syntaxCheck.cjs +182 -0
  42. package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -0
  43. package/dist/cjs/tools/local/textEncoding.cjs +30 -0
  44. package/dist/cjs/tools/local/textEncoding.cjs.map +1 -0
  45. package/dist/cjs/tools/local/workspaceFS.cjs +51 -0
  46. package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -0
  47. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
  48. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  49. package/dist/esm/common/enum.mjs +53 -1
  50. package/dist/esm/common/enum.mjs.map +1 -1
  51. package/dist/esm/graphs/Graph.mjs +149 -5
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +289 -0
  54. package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -0
  55. package/dist/esm/main.mjs +17 -2
  56. package/dist/esm/main.mjs.map +1 -1
  57. package/dist/esm/messages/anthropicToolCache.mjs +99 -0
  58. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -0
  59. package/dist/esm/messages/prune.mjs +26 -1
  60. package/dist/esm/messages/prune.mjs.map +1 -1
  61. package/dist/esm/messages/recency.mjs +97 -0
  62. package/dist/esm/messages/recency.mjs.map +1 -0
  63. package/dist/esm/run.mjs +30 -0
  64. package/dist/esm/run.mjs.map +1 -1
  65. package/dist/esm/summarization/node.mjs +100 -6
  66. package/dist/esm/summarization/node.mjs.map +1 -1
  67. package/dist/esm/tools/ToolNode.mjs +635 -23
  68. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  69. package/dist/esm/tools/local/CompileCheckTool.mjs +223 -0
  70. package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -0
  71. package/dist/esm/tools/local/FileCheckpointer.mjs +87 -0
  72. package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -0
  73. package/dist/esm/tools/local/LocalCodingTools.mjs +1075 -0
  74. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -0
  75. package/dist/esm/tools/local/LocalExecutionEngine.mjs +1022 -0
  76. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -0
  77. package/dist/esm/tools/local/LocalExecutionTools.mjs +117 -0
  78. package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -0
  79. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +448 -0
  80. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -0
  81. package/dist/esm/tools/local/attachments.mjs +180 -0
  82. package/dist/esm/tools/local/attachments.mjs.map +1 -0
  83. package/dist/esm/tools/local/bashAst.mjs +126 -0
  84. package/dist/esm/tools/local/bashAst.mjs.map +1 -0
  85. package/dist/esm/tools/local/editStrategies.mjs +185 -0
  86. package/dist/esm/tools/local/editStrategies.mjs.map +1 -0
  87. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +137 -0
  88. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -0
  89. package/dist/esm/tools/local/syntaxCheck.mjs +179 -0
  90. package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -0
  91. package/dist/esm/tools/local/textEncoding.mjs +27 -0
  92. package/dist/esm/tools/local/textEncoding.mjs.map +1 -0
  93. package/dist/esm/tools/local/workspaceFS.mjs +49 -0
  94. package/dist/esm/tools/local/workspaceFS.mjs.map +1 -0
  95. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
  96. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  97. package/dist/types/common/enum.d.ts +39 -1
  98. package/dist/types/graphs/Graph.d.ts +34 -0
  99. package/dist/types/hooks/createWorkspacePolicyHook.d.ts +95 -0
  100. package/dist/types/hooks/index.d.ts +2 -0
  101. package/dist/types/index.d.ts +1 -0
  102. package/dist/types/messages/anthropicToolCache.d.ts +51 -0
  103. package/dist/types/messages/index.d.ts +2 -0
  104. package/dist/types/messages/prune.d.ts +11 -0
  105. package/dist/types/messages/recency.d.ts +64 -0
  106. package/dist/types/run.d.ts +21 -0
  107. package/dist/types/tools/ToolNode.d.ts +145 -2
  108. package/dist/types/tools/local/CompileCheckTool.d.ts +31 -0
  109. package/dist/types/tools/local/FileCheckpointer.d.ts +39 -0
  110. package/dist/types/tools/local/LocalCodingTools.d.ts +57 -0
  111. package/dist/types/tools/local/LocalExecutionEngine.d.ts +149 -0
  112. package/dist/types/tools/local/LocalExecutionTools.d.ts +9 -0
  113. package/dist/types/tools/local/LocalProgrammaticToolCalling.d.ts +21 -0
  114. package/dist/types/tools/local/attachments.d.ts +84 -0
  115. package/dist/types/tools/local/bashAst.d.ts +11 -0
  116. package/dist/types/tools/local/editStrategies.d.ts +28 -0
  117. package/dist/types/tools/local/index.d.ts +12 -0
  118. package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +38 -0
  119. package/dist/types/tools/local/syntaxCheck.d.ts +42 -0
  120. package/dist/types/tools/local/textEncoding.d.ts +21 -0
  121. package/dist/types/tools/local/workspaceFS.d.ts +49 -0
  122. package/dist/types/types/hitl.d.ts +56 -27
  123. package/dist/types/types/run.d.ts +8 -1
  124. package/dist/types/types/summarize.d.ts +30 -0
  125. package/dist/types/types/tools.d.ts +341 -6
  126. package/package.json +21 -2
  127. package/src/common/enum.ts +54 -0
  128. package/src/graphs/Graph.ts +164 -6
  129. package/src/hooks/__tests__/compactHooks.test.ts +38 -2
  130. package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +393 -0
  131. package/src/hooks/createWorkspacePolicyHook.ts +355 -0
  132. package/src/hooks/index.ts +6 -0
  133. package/src/index.ts +1 -0
  134. package/src/messages/__tests__/anthropicToolCache.test.ts +125 -0
  135. package/src/messages/__tests__/recency.test.ts +267 -0
  136. package/src/messages/anthropicToolCache.ts +116 -0
  137. package/src/messages/index.ts +2 -0
  138. package/src/messages/prune.ts +27 -1
  139. package/src/messages/recency.ts +155 -0
  140. package/src/run.ts +31 -0
  141. package/src/scripts/compare_pi_vs_ours.ts +840 -0
  142. package/src/scripts/local_engine.ts +166 -0
  143. package/src/scripts/local_engine_checkpointer.ts +205 -0
  144. package/src/scripts/local_engine_compile.ts +263 -0
  145. package/src/scripts/local_engine_hooks.ts +226 -0
  146. package/src/scripts/local_engine_image.ts +201 -0
  147. package/src/scripts/local_engine_ptc.ts +151 -0
  148. package/src/scripts/local_engine_workspace.ts +258 -0
  149. package/src/scripts/summarization-recency.ts +462 -0
  150. package/src/specs/prune.test.ts +39 -0
  151. package/src/summarization/__tests__/node.test.ts +499 -3
  152. package/src/summarization/node.ts +124 -7
  153. package/src/tools/ToolNode.ts +769 -20
  154. package/src/tools/__tests__/LocalExecutionTools.test.ts +2647 -0
  155. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +175 -0
  156. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +114 -0
  157. package/src/tools/__tests__/ToolNode.session.test.ts +84 -0
  158. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +467 -0
  159. package/src/tools/__tests__/directToolHooks.test.ts +411 -0
  160. package/src/tools/__tests__/localToolNames.test.ts +73 -0
  161. package/src/tools/__tests__/workspaceSeam.test.ts +134 -0
  162. package/src/tools/local/CompileCheckTool.ts +278 -0
  163. package/src/tools/local/FileCheckpointer.ts +93 -0
  164. package/src/tools/local/LocalCodingTools.ts +1342 -0
  165. package/src/tools/local/LocalExecutionEngine.ts +1329 -0
  166. package/src/tools/local/LocalExecutionTools.ts +167 -0
  167. package/src/tools/local/LocalProgrammaticToolCalling.ts +594 -0
  168. package/src/tools/local/__tests__/FileCheckpointer.test.ts +120 -0
  169. package/src/tools/local/__tests__/editStrategies.test.ts +134 -0
  170. package/src/tools/local/attachments.ts +251 -0
  171. package/src/tools/local/bashAst.ts +151 -0
  172. package/src/tools/local/editStrategies.ts +188 -0
  173. package/src/tools/local/index.ts +12 -0
  174. package/src/tools/local/resolveLocalExecutionTools.ts +208 -0
  175. package/src/tools/local/syntaxCheck.ts +243 -0
  176. package/src/tools/local/textEncoding.ts +37 -0
  177. package/src/tools/local/workspaceFS.ts +89 -0
  178. package/src/types/hitl.ts +56 -27
  179. package/src/types/run.ts +12 -1
  180. package/src/types/summarize.ts +31 -0
  181. package/src/types/tools.ts +359 -7
@@ -0,0 +1,226 @@
1
+ /**
2
+ * src/scripts/local_engine_hooks.ts
3
+ *
4
+ * Live demonstration that PreToolUse / PostToolUse hooks now fire on
5
+ * the direct-path tools the local engine registers. Wires up:
6
+ *
7
+ * - a `createToolPolicyHook` that DENIES `write_file` and `edit_file`
8
+ * while allowing reads, `bash`, and the search tools, and
9
+ * - an explicit `PostToolUse` hook that prefixes every successful
10
+ * tool result with a "[reviewed]" tag so you can confirm it ran
11
+ * against the in-process tools.
12
+ *
13
+ * The script asks the model to (a) write a file, (b) read it back. Step
14
+ * (a) should be blocked with a `Blocked: ...` ToolMessage and a
15
+ * `PermissionDenied` hook fire; step (b) should succeed with the
16
+ * "[reviewed]" prefix injected by `PostToolUse`.
17
+ *
18
+ * Run with: `npm run local:hooks`
19
+ */
20
+ import { config } from 'dotenv';
21
+ config();
22
+ import { tmpdir } from 'os';
23
+ import { join } from 'path';
24
+ import { mkdtemp, rm, writeFile } from 'fs/promises';
25
+ import { HumanMessage } from '@langchain/core/messages';
26
+ import type { BaseMessage } from '@langchain/core/messages';
27
+ import type * as t from '@/types';
28
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
29
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
30
+ import { HookRegistry, createToolPolicyHook } from '@/hooks';
31
+ import type {
32
+ PostToolUseHookOutput,
33
+ PermissionDeniedHookOutput,
34
+ } from '@/hooks';
35
+ import { getLLMConfig } from '@/utils/llmConfig';
36
+ import { getArgs } from '@/scripts/args';
37
+ import { GraphEvents } from '@/common';
38
+ import { Run } from '@/run';
39
+
40
+ const conversationHistory: BaseMessage[] = [];
41
+
42
+ async function main(): Promise<void> {
43
+ const { userName, provider } = await getArgs();
44
+ const cwd = await mkdtemp(join(tmpdir(), 'lc-local-hooks-'));
45
+ console.log(`[local_engine_hooks] workspace: ${cwd}`);
46
+
47
+ // Seed a file so read_file has something interesting to return.
48
+ await writeFile(
49
+ join(cwd, 'allowed.txt'),
50
+ 'this is a pre-existing file the agent is allowed to read\n',
51
+ 'utf8'
52
+ );
53
+
54
+ // --- HOOK SETUP ---
55
+ const denyEvents: { tool: string; reason?: string }[] = [];
56
+ const reviewedTools = new Set<string>();
57
+
58
+ const hookRegistry = new HookRegistry();
59
+
60
+ // PreToolUse policy: deny mutations, allow everything else.
61
+ hookRegistry.register('PreToolUse', {
62
+ hooks: [
63
+ createToolPolicyHook({
64
+ mode: 'bypass',
65
+ deny: ['write_file', 'edit_file'],
66
+ reason: 'this script blocks {tool} to demonstrate direct-path hooks',
67
+ }),
68
+ ],
69
+ });
70
+
71
+ // PostToolUse: tag successful results so we can see the hook running.
72
+ hookRegistry.register('PostToolUse', {
73
+ hooks: [
74
+ async ({
75
+ toolName,
76
+ toolOutput,
77
+ }): Promise<PostToolUseHookOutput> => {
78
+ reviewedTools.add(toolName);
79
+ const text =
80
+ typeof toolOutput === 'string'
81
+ ? toolOutput
82
+ : JSON.stringify(toolOutput);
83
+ return { updatedOutput: `[reviewed] ${text}` };
84
+ },
85
+ ],
86
+ });
87
+
88
+ // PermissionDenied: observational sink so we can prove the hook fired.
89
+ hookRegistry.register('PermissionDenied', {
90
+ hooks: [
91
+ async ({
92
+ toolName,
93
+ reason,
94
+ }): Promise<PermissionDeniedHookOutput> => {
95
+ denyEvents.push({ tool: toolName, reason });
96
+ return {};
97
+ },
98
+ ],
99
+ });
100
+
101
+ // --- STREAM HANDLERS ---
102
+ const { contentParts, aggregateContent } = createContentAggregator();
103
+ const customHandlers = {
104
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
105
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
106
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
107
+ // Forward ON_RUN_STEP so the aggregator's stepMap is seeded
108
+ // before ON_RUN_STEP_COMPLETED arrives (issue #142).
109
+ [GraphEvents.ON_RUN_STEP]: {
110
+ handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData): void => {
111
+ aggregateContent({ event, data: data as t.RunStep });
112
+ },
113
+ },
114
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
115
+ handle: (
116
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
117
+ data: t.StreamEventData
118
+ ): void => {
119
+ const cast = data as unknown as { result: t.ToolEndEvent };
120
+ const tc = (cast.result as { tool_call?: { name?: string } } | undefined)
121
+ ?.tool_call;
122
+ console.log(`====== ON_RUN_STEP_COMPLETED tool=${tc?.name ?? '?'} ======`);
123
+ aggregateContent({ event, data: cast });
124
+ },
125
+ },
126
+ [GraphEvents.ON_MESSAGE_DELTA]: {
127
+ handle: (
128
+ event: GraphEvents.ON_MESSAGE_DELTA,
129
+ data: t.StreamEventData
130
+ ): void => {
131
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
132
+ },
133
+ },
134
+ [GraphEvents.TOOL_START]: {
135
+ handle: (_event: string, data: t.StreamEventData): void => {
136
+ const obj = data as unknown as { name?: string; input?: unknown };
137
+ console.log(
138
+ `====== TOOL_START tool=${obj.name ?? '?'} input=${JSON.stringify(
139
+ obj.input
140
+ )} ======`
141
+ );
142
+ },
143
+ },
144
+ };
145
+
146
+ const llmConfig = getLLMConfig(provider);
147
+
148
+ const runConfig: t.RunConfig = {
149
+ runId: 'local-engine-hooks-1',
150
+ graphConfig: {
151
+ type: 'standard',
152
+ llmConfig,
153
+ instructions:
154
+ 'You are a coding assistant. The host has loaded a permissions policy. ' +
155
+ 'When a tool call is denied you will see a ToolMessage starting with ' +
156
+ '`Blocked:` — DO NOT retry the same denied tool. If a write is denied, ' +
157
+ 'just acknowledge the denial and continue with operations that are allowed.',
158
+ },
159
+ toolExecution: {
160
+ engine: 'local',
161
+ local: { cwd, timeoutMs: 30_000 },
162
+ },
163
+ hooks: hookRegistry,
164
+ returnContent: true,
165
+ skipCleanup: true,
166
+ customHandlers,
167
+ };
168
+ const run = await Run.create<t.IState>(runConfig);
169
+
170
+ const streamConfig = {
171
+ configurable: { provider, thread_id: 'local-engine-hooks-thread-1' },
172
+ streamMode: 'values',
173
+ version: 'v2' as const,
174
+ };
175
+
176
+ const userMessage = new HumanMessage(
177
+ `Hi ${userName}. Please do these two things in order:\n\n` +
178
+ '1. Use `write_file` to create `blocked.txt` with the content "should never land".\n' +
179
+ '2. Use `read_file` on `allowed.txt` and tell me what is inside.\n\n' +
180
+ 'Then report whether each step succeeded.'
181
+ );
182
+ conversationHistory.push(userMessage);
183
+ console.log('====== USER ======\n' + userMessage.content + '\n');
184
+
185
+ await run.processStream({ messages: conversationHistory }, streamConfig);
186
+ const finalMessages = run.getRunMessages();
187
+ if (finalMessages) {
188
+ conversationHistory.push(...finalMessages);
189
+ }
190
+
191
+ console.log('\n====== FINAL CONTENT PARTS ======');
192
+ console.dir(contentParts, { depth: null });
193
+
194
+ console.log('\n====== HOOK OBSERVATIONS ======');
195
+ console.log(
196
+ `PermissionDenied fired ${denyEvents.length} time(s):`,
197
+ denyEvents
198
+ );
199
+ console.log(
200
+ `PostToolUse saw tools: ${[...reviewedTools].join(', ') || '<none>'}`
201
+ );
202
+
203
+ // The deny-target file must NOT exist.
204
+ const fs = await import('fs/promises');
205
+ const exists = await fs
206
+ .stat(join(cwd, 'blocked.txt'))
207
+ .then(() => true)
208
+ .catch(() => false);
209
+ console.log(
210
+ `blocked.txt landed on disk? ${exists} ${
211
+ exists ? '(BUG)' : '(expected: false)'
212
+ }`
213
+ );
214
+
215
+ await rm(cwd, { recursive: true, force: true });
216
+ }
217
+
218
+ process.on('unhandledRejection', (reason) => {
219
+ console.error('Unhandled Rejection:', reason);
220
+ process.exit(1);
221
+ });
222
+
223
+ main().catch((err) => {
224
+ console.error(err);
225
+ process.exit(1);
226
+ });
@@ -0,0 +1,201 @@
1
+ /**
2
+ * src/scripts/local_engine_image.ts
3
+ *
4
+ * Live demo of the local read_file tool returning an image as an
5
+ * inline `MessageContentComplex[]` attachment when
6
+ * `local.attachReadAttachments: 'images-only'` is set. We seed a tiny
7
+ * red square PNG into the workspace, ask the agent to read it, and
8
+ * verify the model receives the image bytes (not just a refusal stub).
9
+ *
10
+ * Best run with a vision-capable provider, e.g.:
11
+ * npm run local:image -- --provider anthropic
12
+ */
13
+ import { config } from 'dotenv';
14
+ config();
15
+ import { tmpdir } from 'os';
16
+ import { join } from 'path';
17
+ import { copyFile, mkdtemp, rm, writeFile } from 'fs/promises';
18
+ import { HumanMessage, ToolMessage } from '@langchain/core/messages';
19
+ import type { BaseMessage } from '@langchain/core/messages';
20
+ import type * as t from '@/types';
21
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
22
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
23
+ import { getLLMConfig } from '@/utils/llmConfig';
24
+ import { getArgs } from '@/scripts/args';
25
+ import { GraphEvents } from '@/common';
26
+ import { Run } from '@/run';
27
+
28
+ const conversationHistory: BaseMessage[] = [];
29
+
30
+ // Canonical 1x1 transparent PNG fallback if the system PNG isn't readable.
31
+ const TINY_PNG_HEX =
32
+ '89504e470d0a1a0a0000000d49484452000000010000000108060000001f15' +
33
+ 'c4890000000a49444154789c63000100000005000165be7e6e0000000049' +
34
+ '454e44ae426082';
35
+
36
+ const SAMPLE_PNG_PATHS = [
37
+ '/System/Library/CoreServices/Certificate Assistant.app/Contents/Resources/droppedImage.png',
38
+ '/System/Library/CoreServices/Certificate Assistant.app/Contents/Resources/shapeimage_1.png',
39
+ '/System/Library/CoreServices/BluetoothUIServer.app/Contents/Resources/handoff.png',
40
+ ];
41
+
42
+ async function main(): Promise<void> {
43
+ const { userName, provider } = await getArgs();
44
+ const cwd = await mkdtemp(join(tmpdir(), 'lc-local-image-'));
45
+ console.log(`[local_engine_image] workspace: ${cwd}`);
46
+
47
+ const pngPath = join(cwd, 'sample.png');
48
+ let copied = false;
49
+ for (const sample of SAMPLE_PNG_PATHS) {
50
+ try {
51
+ await copyFile(sample, pngPath);
52
+ copied = true;
53
+ break;
54
+ } catch {
55
+ // try next
56
+ }
57
+ }
58
+ if (!copied) {
59
+ await writeFile(pngPath, Buffer.from(TINY_PNG_HEX, 'hex'));
60
+ }
61
+ console.log(`[local_engine_image] wrote ${pngPath} (real-png=${copied})`);
62
+
63
+ const { aggregateContent } = createContentAggregator();
64
+ const customHandlers = {
65
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
66
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
67
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
68
+ // Forward ON_RUN_STEP so the aggregator's stepMap is seeded
69
+ // before ON_RUN_STEP_COMPLETED arrives (issue #142).
70
+ [GraphEvents.ON_RUN_STEP]: {
71
+ handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData): void => {
72
+ aggregateContent({ event, data: data as t.RunStep });
73
+ },
74
+ },
75
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
76
+ handle: (
77
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
78
+ data: t.StreamEventData
79
+ ): void => {
80
+ aggregateContent({
81
+ event,
82
+ data: data as unknown as { result: t.ToolEndEvent },
83
+ });
84
+ },
85
+ },
86
+ [GraphEvents.TOOL_START]: {
87
+ handle: (_event: string, data: t.StreamEventData): void => {
88
+ const obj = data as unknown as { name?: string; input?: unknown };
89
+ console.log(
90
+ `====== TOOL_START tool=${obj.name ?? '?'} input=${JSON.stringify(
91
+ obj.input
92
+ )} ======`
93
+ );
94
+ },
95
+ },
96
+ };
97
+
98
+ const llmConfig = getLLMConfig(provider);
99
+
100
+ const runConfig: t.RunConfig = {
101
+ runId: 'local-engine-image-1',
102
+ graphConfig: {
103
+ type: 'standard',
104
+ llmConfig,
105
+ instructions:
106
+ `You are ${userName}'s assistant. When asked to inspect a file, ` +
107
+ 'use `read_file`. The host has enabled inline image attachments, so ' +
108
+ 'reading an image will deliver the actual image bytes to your vision ' +
109
+ 'context. Describe the image you see.',
110
+ },
111
+ toolExecution: {
112
+ engine: 'local',
113
+ local: {
114
+ cwd,
115
+ attachReadAttachments: 'images-only',
116
+ timeoutMs: 30_000,
117
+ },
118
+ },
119
+ returnContent: true,
120
+ skipCleanup: true,
121
+ customHandlers,
122
+ };
123
+ const run = await Run.create<t.IState>(runConfig);
124
+
125
+ const streamConfig = {
126
+ configurable: { provider, thread_id: 'local-engine-image-thread-1' },
127
+ streamMode: 'values',
128
+ version: 'v2' as const,
129
+ };
130
+
131
+ const userMessage = new HumanMessage(
132
+ 'Please call `read_file` on `sample.png` (in the working directory) and ' +
133
+ 'then briefly describe what the image shows.'
134
+ );
135
+ conversationHistory.push(userMessage);
136
+ console.log('====== USER ======\n' + userMessage.content + '\n');
137
+
138
+ await run.processStream({ messages: conversationHistory }, streamConfig);
139
+ const finalMessages = run.getRunMessages();
140
+ if (finalMessages) {
141
+ conversationHistory.push(...finalMessages);
142
+ }
143
+
144
+ console.log('\n====== TOOL MESSAGES IN HISTORY ======');
145
+ let imageBlockSeen = false;
146
+ for (const msg of conversationHistory) {
147
+ if (msg instanceof ToolMessage) {
148
+ const isArray = Array.isArray(msg.content);
149
+ const blocks = isArray
150
+ ? (msg.content as Array<{ type?: string; image_url?: { url?: string } }>)
151
+ : [];
152
+ const types = blocks.map((b) => b.type ?? '?').join(',');
153
+ const url = blocks.find((b) => b.type === 'image_url')?.image_url?.url;
154
+ console.log(
155
+ `- ToolMessage(name=${msg.name}, content=${
156
+ isArray ? `[${types}]` : typeof msg.content
157
+ }${url ? ` url=${url.slice(0, 40)}…` : ''})`
158
+ );
159
+ if (isArray && blocks.some((b) => b.type === 'image_url')) {
160
+ imageBlockSeen = true;
161
+ }
162
+ }
163
+ }
164
+ console.log(
165
+ `\nImage attachment landed in tool result: ${imageBlockSeen} ${
166
+ imageBlockSeen ? '✔' : '✖'
167
+ }`
168
+ );
169
+
170
+ console.log('\n====== ASSISTANT FINAL TEXT ======');
171
+ const lastAssistant = [...conversationHistory]
172
+ .reverse()
173
+ .find((m) => m._getType() === 'ai');
174
+ if (lastAssistant) {
175
+ const c = lastAssistant.content;
176
+ console.log(
177
+ typeof c === 'string'
178
+ ? c
179
+ : Array.isArray(c)
180
+ ? c
181
+ .map((b) => ('text' in b ? b.text : `<${b.type}>`))
182
+ .join(' ')
183
+ : JSON.stringify(c)
184
+ );
185
+ }
186
+
187
+ if (!imageBlockSeen) {
188
+ process.exitCode = 1;
189
+ }
190
+ await rm(cwd, { recursive: true, force: true });
191
+ }
192
+
193
+ process.on('unhandledRejection', (reason) => {
194
+ console.error('Unhandled Rejection:', reason);
195
+ process.exit(1);
196
+ });
197
+
198
+ main().catch((err) => {
199
+ console.error(err);
200
+ process.exit(1);
201
+ });
@@ -0,0 +1,151 @@
1
+ /**
2
+ * src/scripts/local_engine_ptc.ts
3
+ *
4
+ * Live exercise of the local engine's programmatic tool calling. Asks
5
+ * the model to use `run_tools_with_code` (Python) to do a multi-step
6
+ * coding workflow that calls `write_file`, `read_file`, and
7
+ * `edit_file` from inside a single Python program. The local engine
8
+ * stands up an in-process loopback HTTP bridge guarded by a per-Run
9
+ * bearer token (verified via `crypto.timingSafeEqual`), and the
10
+ * generated Python sends `x-librechat-bridge-token` on every call.
11
+ *
12
+ * Run with: `npm run local:ptc`
13
+ */
14
+ import { config } from 'dotenv';
15
+ config();
16
+ import { tmpdir } from 'os';
17
+ import { join } from 'path';
18
+ import { mkdtemp, readdir, readFile, rm } from 'fs/promises';
19
+ import { HumanMessage } from '@langchain/core/messages';
20
+ import type { BaseMessage } from '@langchain/core/messages';
21
+ import type * as t from '@/types';
22
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
23
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
24
+ import { getLLMConfig } from '@/utils/llmConfig';
25
+ import { getArgs } from '@/scripts/args';
26
+ import { GraphEvents } from '@/common';
27
+ import { Run } from '@/run';
28
+
29
+ const conversationHistory: BaseMessage[] = [];
30
+
31
+ async function main(): Promise<void> {
32
+ const { userName, provider } = await getArgs();
33
+ const cwd = await mkdtemp(join(tmpdir(), 'lc-local-ptc-'));
34
+ console.log(`[local_engine_ptc] workspace: ${cwd}`);
35
+
36
+ const { contentParts, aggregateContent } = createContentAggregator();
37
+
38
+ const customHandlers = {
39
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
40
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
41
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
42
+ // Forward ON_RUN_STEP so the aggregator's stepMap is seeded
43
+ // before ON_RUN_STEP_COMPLETED arrives (issue #142).
44
+ [GraphEvents.ON_RUN_STEP]: {
45
+ handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData): void => {
46
+ aggregateContent({ event, data: data as t.RunStep });
47
+ },
48
+ },
49
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
50
+ handle: (
51
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
52
+ data: t.StreamEventData
53
+ ): void => {
54
+ console.log('====== ON_RUN_STEP_COMPLETED ======');
55
+ const cast = data as unknown as { result: t.ToolEndEvent };
56
+ const tc = (cast.result as { tool_call?: { name?: string } } | undefined)
57
+ ?.tool_call;
58
+ console.log(`tool=${tc?.name ?? '<unknown>'}`);
59
+ aggregateContent({ event, data: cast });
60
+ },
61
+ },
62
+ [GraphEvents.ON_MESSAGE_DELTA]: {
63
+ handle: (
64
+ event: GraphEvents.ON_MESSAGE_DELTA,
65
+ data: t.StreamEventData
66
+ ): void => {
67
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
68
+ },
69
+ },
70
+ [GraphEvents.TOOL_START]: {
71
+ handle: (_event: string, data: t.StreamEventData): void => {
72
+ const obj = data as unknown as { name?: string; input?: unknown };
73
+ console.log(
74
+ `====== TOOL_START tool=${obj.name ?? '?'} input=${JSON.stringify(
75
+ obj.input
76
+ )} ======`
77
+ );
78
+ },
79
+ },
80
+ };
81
+
82
+ const llmConfig = getLLMConfig(provider);
83
+
84
+ const runConfig: t.RunConfig = {
85
+ runId: 'local-engine-ptc-1',
86
+ graphConfig: {
87
+ type: 'standard',
88
+ llmConfig,
89
+ instructions:
90
+ 'You are a coding assistant. Whenever you need to run multiple file/edit ' +
91
+ 'operations together, prefer the `run_tools_with_code` tool with `lang: "py"` ' +
92
+ 'over making many separate tool calls — it lets you sequence read_file/write_file/' +
93
+ 'edit_file in a single Python program. Always use absolute or workspace-relative paths.',
94
+ },
95
+ toolExecution: {
96
+ engine: 'local',
97
+ local: { cwd, timeoutMs: 60_000 },
98
+ },
99
+ returnContent: true,
100
+ skipCleanup: true,
101
+ customHandlers,
102
+ };
103
+ const run = await Run.create<t.IState>(runConfig);
104
+
105
+ const streamConfig = {
106
+ configurable: { provider, thread_id: 'local-engine-ptc-thread-1' },
107
+ streamMode: 'values',
108
+ version: 'v2' as const,
109
+ };
110
+
111
+ const userMessage = new HumanMessage(
112
+ `Hi ${userName}. Use the \`run_tools_with_code\` tool with lang: "py" to do all of the following inside a SINGLE Python program (one tool call):\n\n` +
113
+ '1. Call `write_file` to create `notes.md` with the contents `# Notes\\n- first item\\n`.\n' +
114
+ '2. Call `read_file` to read it back.\n' +
115
+ '3. Call `edit_file` to change `first item` to `FIRST item`.\n' +
116
+ '4. Call `read_file` again and print the final contents.\n\n' +
117
+ 'Print every intermediate value with `print(...)` so I can see them. Do not make multiple tool calls — everything must happen inside one `run_tools_with_code` invocation. After it returns, summarise what you did in one sentence.'
118
+ );
119
+ conversationHistory.push(userMessage);
120
+ console.log('====== USER ======\n' + userMessage.content + '\n');
121
+
122
+ await run.processStream({ messages: conversationHistory }, streamConfig);
123
+ const finalMessages = run.getRunMessages();
124
+ if (finalMessages) {
125
+ conversationHistory.push(...finalMessages);
126
+ }
127
+
128
+ console.log('\n====== FINAL CONTENT PARTS ======');
129
+ console.dir(contentParts, { depth: null });
130
+
131
+ console.log('\n====== WORKSPACE ON DISK ======');
132
+ const entries = await readdir(cwd, { withFileTypes: true });
133
+ for (const e of entries) {
134
+ if (!e.isFile()) continue;
135
+ const path = join(cwd, e.name);
136
+ const body = await readFile(path, 'utf8').catch(() => '<binary>');
137
+ console.log(`--- ${e.name} ---\n${body}\n`);
138
+ }
139
+
140
+ await rm(cwd, { recursive: true, force: true });
141
+ }
142
+
143
+ process.on('unhandledRejection', (reason) => {
144
+ console.error('Unhandled Rejection:', reason);
145
+ process.exit(1);
146
+ });
147
+
148
+ main().catch((err) => {
149
+ console.error(err);
150
+ process.exit(1);
151
+ });