@librechat/agents 3.1.77 → 3.1.78

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 (185) 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 +155 -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 +31 -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 +156 -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 +31 -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/tools/subagent/SubagentExecutor.d.ts +29 -0
  123. package/dist/types/types/hitl.d.ts +56 -27
  124. package/dist/types/types/run.d.ts +8 -1
  125. package/dist/types/types/summarize.d.ts +30 -0
  126. package/dist/types/types/tools.d.ts +341 -6
  127. package/package.json +21 -2
  128. package/src/common/enum.ts +54 -0
  129. package/src/graphs/Graph.ts +173 -6
  130. package/src/hooks/__tests__/compactHooks.test.ts +38 -2
  131. package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +393 -0
  132. package/src/hooks/createWorkspacePolicyHook.ts +355 -0
  133. package/src/hooks/index.ts +6 -0
  134. package/src/index.ts +1 -0
  135. package/src/messages/__tests__/anthropicToolCache.test.ts +125 -0
  136. package/src/messages/__tests__/recency.test.ts +267 -0
  137. package/src/messages/anthropicToolCache.ts +116 -0
  138. package/src/messages/index.ts +2 -0
  139. package/src/messages/prune.ts +27 -1
  140. package/src/messages/recency.ts +155 -0
  141. package/src/run.ts +31 -0
  142. package/src/scripts/compare_pi_vs_ours.ts +840 -0
  143. package/src/scripts/local_engine.ts +166 -0
  144. package/src/scripts/local_engine_checkpointer.ts +205 -0
  145. package/src/scripts/local_engine_compile.ts +263 -0
  146. package/src/scripts/local_engine_hooks.ts +226 -0
  147. package/src/scripts/local_engine_image.ts +201 -0
  148. package/src/scripts/local_engine_ptc.ts +151 -0
  149. package/src/scripts/local_engine_workspace.ts +258 -0
  150. package/src/scripts/subagent-configurable-inheritance.ts +252 -0
  151. package/src/scripts/summarization-recency.ts +462 -0
  152. package/src/specs/prune.test.ts +39 -0
  153. package/src/summarization/__tests__/node.test.ts +499 -3
  154. package/src/summarization/node.ts +124 -7
  155. package/src/tools/ToolNode.ts +769 -20
  156. package/src/tools/__tests__/LocalExecutionTools.test.ts +2647 -0
  157. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +175 -0
  158. package/src/tools/__tests__/SubagentExecutor.test.ts +148 -0
  159. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +114 -0
  160. package/src/tools/__tests__/ToolNode.session.test.ts +84 -0
  161. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +467 -0
  162. package/src/tools/__tests__/directToolHooks.test.ts +411 -0
  163. package/src/tools/__tests__/localToolNames.test.ts +73 -0
  164. package/src/tools/__tests__/workspaceSeam.test.ts +134 -0
  165. package/src/tools/local/CompileCheckTool.ts +278 -0
  166. package/src/tools/local/FileCheckpointer.ts +93 -0
  167. package/src/tools/local/LocalCodingTools.ts +1342 -0
  168. package/src/tools/local/LocalExecutionEngine.ts +1329 -0
  169. package/src/tools/local/LocalExecutionTools.ts +167 -0
  170. package/src/tools/local/LocalProgrammaticToolCalling.ts +594 -0
  171. package/src/tools/local/__tests__/FileCheckpointer.test.ts +120 -0
  172. package/src/tools/local/__tests__/editStrategies.test.ts +134 -0
  173. package/src/tools/local/attachments.ts +251 -0
  174. package/src/tools/local/bashAst.ts +151 -0
  175. package/src/tools/local/editStrategies.ts +188 -0
  176. package/src/tools/local/index.ts +12 -0
  177. package/src/tools/local/resolveLocalExecutionTools.ts +208 -0
  178. package/src/tools/local/syntaxCheck.ts +243 -0
  179. package/src/tools/local/textEncoding.ts +37 -0
  180. package/src/tools/local/workspaceFS.ts +89 -0
  181. package/src/tools/subagent/SubagentExecutor.ts +60 -0
  182. package/src/types/hitl.ts +56 -27
  183. package/src/types/run.ts +12 -1
  184. package/src/types/summarize.ts +31 -0
  185. package/src/types/tools.ts +359 -7
@@ -0,0 +1,166 @@
1
+ /**
2
+ * src/scripts/local_engine.ts
3
+ *
4
+ * Live end-to-end exercise of the local execution engine. Spins up a
5
+ * temporary workspace, points an agent at it via
6
+ * `toolExecution: { engine: 'local' }`, and asks the model to do a
7
+ * small coding task that exercises `write_file`, `read_file`,
8
+ * `edit_file`, `bash`, and `list_directory`. After the run, dumps the
9
+ * workspace contents so you can verify the tools actually mutated
10
+ * disk.
11
+ *
12
+ * Run with: `npm run local`
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-engine-'));
34
+ console.log(`[local_engine] workspace: ${cwd}`);
35
+
36
+ const { contentParts, aggregateContent } = createContentAggregator();
37
+ const customHandlers = {
38
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
39
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
40
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
41
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
42
+ handle: (
43
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
44
+ data: t.StreamEventData
45
+ ): void => {
46
+ console.log('====== ON_RUN_STEP_COMPLETED ======');
47
+ const cast = data as unknown as { result: t.ToolEndEvent };
48
+ const tc = (cast.result as { tool_call?: { name?: string } } | undefined)
49
+ ?.tool_call;
50
+ console.log(`tool=${tc?.name ?? '<unknown>'}`);
51
+ aggregateContent({ event, data: cast });
52
+ },
53
+ },
54
+ [GraphEvents.ON_RUN_STEP]: {
55
+ handle: (
56
+ event: GraphEvents.ON_RUN_STEP,
57
+ data: t.StreamEventData
58
+ ): void => {
59
+ aggregateContent({ event, data: data as t.RunStep });
60
+ },
61
+ },
62
+ [GraphEvents.ON_RUN_STEP_DELTA]: {
63
+ handle: (
64
+ event: GraphEvents.ON_RUN_STEP_DELTA,
65
+ data: t.StreamEventData
66
+ ): void => {
67
+ aggregateContent({ event, data: data as t.RunStepDeltaEvent });
68
+ },
69
+ },
70
+ [GraphEvents.ON_MESSAGE_DELTA]: {
71
+ handle: (
72
+ event: GraphEvents.ON_MESSAGE_DELTA,
73
+ data: t.StreamEventData
74
+ ): void => {
75
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
76
+ },
77
+ },
78
+ [GraphEvents.TOOL_START]: {
79
+ handle: (_event: string, data: t.StreamEventData): void => {
80
+ const obj = data as unknown as { name?: string; input?: unknown };
81
+ console.log(
82
+ `====== TOOL_START tool=${obj.name ?? '?'} input=${JSON.stringify(
83
+ obj.input
84
+ )} ======`
85
+ );
86
+ },
87
+ },
88
+ };
89
+
90
+ const llmConfig = getLLMConfig(provider);
91
+
92
+ const runConfig: t.RunConfig = {
93
+ runId: 'local-engine-1',
94
+ graphConfig: {
95
+ type: 'standard',
96
+ llmConfig,
97
+ instructions:
98
+ 'You are a friendly AI coding assistant with access to local file and shell tools. ' +
99
+ 'Always operate inside the configured working directory and prefer the smallest tool ' +
100
+ 'that gets the job done (read_file before grep, write_file before bash heredocs, etc.). ' +
101
+ `Address the user by their name (${userName}).`,
102
+ },
103
+ toolExecution: {
104
+ engine: 'local',
105
+ local: {
106
+ cwd,
107
+ // Sandbox is opt-in; leave off for this baseline script. The engine
108
+ // logs a one-time warning when it runs without sandboxing.
109
+ bashAst: 'auto',
110
+ timeoutMs: 30_000,
111
+ },
112
+ },
113
+ returnContent: true,
114
+ skipCleanup: true,
115
+ customHandlers,
116
+ };
117
+ const run = await Run.create<t.IState>(runConfig);
118
+
119
+ const streamConfig = {
120
+ configurable: { provider, thread_id: 'local-engine-thread-1' },
121
+ streamMode: 'values',
122
+ version: 'v2' as const,
123
+ };
124
+
125
+ const userMessage = new HumanMessage(
126
+ `Hi, I'm ${userName}. In the current working directory, please do the following step by step:\n\n` +
127
+ '1. Create a file called `greet.py` with a tiny Python function `greet(name)` that returns "Hello, <name>!".\n' +
128
+ '2. Use `bash` to run `python3 -c "from greet import greet; print(greet(\\"world\\"))"` and confirm it prints "Hello, world!".\n' +
129
+ '3. Use `edit_file` to change the greeting to "Hi, <name>!" (only that one literal change).\n' +
130
+ '4. Re-run the same bash command and confirm the new output.\n' +
131
+ '5. Use `list_directory` to show what files exist now.\n\n' +
132
+ 'Before exiting, briefly summarise the steps you took.'
133
+ );
134
+ conversationHistory.push(userMessage);
135
+
136
+ console.log('====== USER ======\n' + userMessage.content + '\n');
137
+ await run.processStream({ messages: conversationHistory }, streamConfig);
138
+ const finalMessages = run.getRunMessages();
139
+ if (finalMessages) {
140
+ conversationHistory.push(...finalMessages);
141
+ }
142
+
143
+ console.log('\n====== FINAL CONTENT PARTS ======');
144
+ console.dir(contentParts, { depth: null });
145
+
146
+ console.log('\n====== WORKSPACE ON DISK ======');
147
+ const entries = await readdir(cwd, { withFileTypes: true });
148
+ for (const e of entries) {
149
+ if (!e.isFile()) continue;
150
+ const path = join(cwd, e.name);
151
+ const body = await readFile(path, 'utf8').catch(() => '<binary>');
152
+ console.log(`--- ${e.name} ---\n${body}\n`);
153
+ }
154
+
155
+ await rm(cwd, { recursive: true, force: true });
156
+ }
157
+
158
+ process.on('unhandledRejection', (reason) => {
159
+ console.error('Unhandled Rejection:', reason);
160
+ process.exit(1);
161
+ });
162
+
163
+ main().catch((err) => {
164
+ console.error(err);
165
+ process.exit(1);
166
+ });
@@ -0,0 +1,205 @@
1
+ /**
2
+ * src/scripts/local_engine_checkpointer.ts
3
+ *
4
+ * Live demonstration of the file checkpointer. We:
5
+ *
6
+ * 1. Build the local-coding-tool bundle ourselves with
7
+ * `createLocalCodingToolBundle({ fileCheckpointing: true })` so we
8
+ * can keep a handle on the checkpointer.
9
+ * 2. Pass those tools to the run as `graphConfig.tools`. We do NOT
10
+ * use `toolExecution.engine: 'local'` here — that path auto-binds
11
+ * its own internal tools without exposing a checkpointer back to
12
+ * us. The point is to show how a host wires checkpointing on top
13
+ * of the local tools when it cares about rewind.
14
+ * 3. Seed two files with known contents.
15
+ * 4. Ask the model to MUTATE both via `edit_file` / `write_file`.
16
+ * 5. Verify the contents changed on disk.
17
+ * 6. Call `checkpointer.rewind()` and verify the original contents
18
+ * came back.
19
+ *
20
+ * Run with: `npm run local:checkpointer`
21
+ */
22
+ import { config } from 'dotenv';
23
+ config();
24
+ import { tmpdir } from 'os';
25
+ import { join } from 'path';
26
+ import { mkdtemp, readFile, rm, writeFile } from 'fs/promises';
27
+ import { HumanMessage } from '@langchain/core/messages';
28
+ import type { BaseMessage } from '@langchain/core/messages';
29
+ import type * as t from '@/types';
30
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
31
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
32
+ import { createLocalCodingToolBundle } from '@/tools/local';
33
+ import { getLLMConfig } from '@/utils/llmConfig';
34
+ import { getArgs } from '@/scripts/args';
35
+ import { GraphEvents } from '@/common';
36
+ import { Run } from '@/run';
37
+
38
+ const conversationHistory: BaseMessage[] = [];
39
+
40
+ const FILE_A = 'config.json';
41
+ const FILE_B = 'README.md';
42
+ const ORIGINAL_A = '{\n "version": "1.0.0",\n "name": "demo"\n}\n';
43
+ const ORIGINAL_B = '# Demo\n\nThis README is intentionally tiny.\n';
44
+
45
+ async function main(): Promise<void> {
46
+ const { userName, provider } = await getArgs();
47
+ const cwd = await mkdtemp(join(tmpdir(), 'lc-local-cp-'));
48
+ console.log(`[local_engine_checkpointer] workspace: ${cwd}`);
49
+
50
+ await writeFile(join(cwd, FILE_A), ORIGINAL_A, 'utf8');
51
+ await writeFile(join(cwd, FILE_B), ORIGINAL_B, 'utf8');
52
+
53
+ // Build the local-engine tool bundle with file checkpointing on so
54
+ // we can call `bundle.checkpointer.rewind()` after the run.
55
+ const bundle = createLocalCodingToolBundle({
56
+ cwd,
57
+ fileCheckpointing: true,
58
+ timeoutMs: 30_000,
59
+ });
60
+ if (bundle.checkpointer == null) {
61
+ throw new Error(
62
+ 'expected bundle.checkpointer to be defined when fileCheckpointing is true'
63
+ );
64
+ }
65
+
66
+ const { contentParts, aggregateContent } = createContentAggregator();
67
+ const customHandlers = {
68
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
69
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
70
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
71
+ // Forward ON_RUN_STEP so the aggregator's stepMap is seeded
72
+ // before ON_RUN_STEP_COMPLETED arrives — without this the
73
+ // aggregator logs "No run step or runId found for completed step
74
+ // event" once per tool call (issue #142).
75
+ [GraphEvents.ON_RUN_STEP]: {
76
+ handle: (
77
+ event: GraphEvents.ON_RUN_STEP,
78
+ data: t.StreamEventData
79
+ ): void => {
80
+ aggregateContent({ event, data: data as t.RunStep });
81
+ },
82
+ },
83
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
84
+ handle: (
85
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
86
+ data: t.StreamEventData
87
+ ): void => {
88
+ const cast = data as unknown as { result: t.ToolEndEvent };
89
+ const tc = (cast.result as { tool_call?: { name?: string } } | undefined)
90
+ ?.tool_call;
91
+ console.log(`====== ON_RUN_STEP_COMPLETED tool=${tc?.name ?? '?'} ======`);
92
+ aggregateContent({ event, data: cast });
93
+ },
94
+ },
95
+ [GraphEvents.ON_MESSAGE_DELTA]: {
96
+ handle: (
97
+ event: GraphEvents.ON_MESSAGE_DELTA,
98
+ data: t.StreamEventData
99
+ ): void => {
100
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
101
+ },
102
+ },
103
+ [GraphEvents.TOOL_START]: {
104
+ handle: (_event: string, data: t.StreamEventData): void => {
105
+ const obj = data as unknown as { name?: string; input?: unknown };
106
+ console.log(
107
+ `====== TOOL_START tool=${obj.name ?? '?'} input=${JSON.stringify(
108
+ obj.input
109
+ )} ======`
110
+ );
111
+ },
112
+ },
113
+ };
114
+
115
+ const llmConfig = getLLMConfig(provider);
116
+
117
+ const runConfig: t.RunConfig = {
118
+ runId: 'local-engine-cp-1',
119
+ graphConfig: {
120
+ type: 'standard',
121
+ llmConfig,
122
+ tools: bundle.tools,
123
+ instructions:
124
+ 'You are a coding assistant with local file tools. Make exactly the changes ' +
125
+ 'the user asks for, no more. Use `edit_file` for single-line literal ' +
126
+ 'replacements and `write_file` for full overwrites.',
127
+ },
128
+ returnContent: true,
129
+ skipCleanup: true,
130
+ customHandlers,
131
+ };
132
+ const run = await Run.create<t.IState>(runConfig);
133
+
134
+ const streamConfig = {
135
+ configurable: { provider, thread_id: 'local-engine-cp-thread-1' },
136
+ streamMode: 'values',
137
+ version: 'v2' as const,
138
+ };
139
+
140
+ const userMessage = new HumanMessage(
141
+ `Hi ${userName}. Please do exactly the following:\n\n` +
142
+ `1. Use \`edit_file\` on \`${FILE_A}\` to replace \`"version": "1.0.0"\` with \`"version": "2.0.0"\`.\n` +
143
+ `2. Use \`write_file\` on \`${FILE_B}\` to overwrite it with the contents:\n` +
144
+ ' "# Demo\\n\\nThis README has been overwritten by the agent.\\n"\n\n' +
145
+ 'Confirm both changes succeeded. Do not touch any other files.'
146
+ );
147
+ conversationHistory.push(userMessage);
148
+ console.log('====== USER ======\n' + userMessage.content + '\n');
149
+
150
+ await run.processStream({ messages: conversationHistory }, streamConfig);
151
+ const finalMessages = run.getRunMessages();
152
+ if (finalMessages) {
153
+ conversationHistory.push(...finalMessages);
154
+ }
155
+
156
+ console.log('\n====== FINAL CONTENT PARTS ======');
157
+ console.dir(contentParts, { depth: null });
158
+
159
+ // After the run: verify the agent actually changed the files.
160
+ const afterA = await readFile(join(cwd, FILE_A), 'utf8');
161
+ const afterB = await readFile(join(cwd, FILE_B), 'utf8');
162
+ console.log('\n====== AFTER AGENT EDITS ======');
163
+ console.log(`--- ${FILE_A} ---\n${afterA}`);
164
+ console.log(`--- ${FILE_B} ---\n${afterB}`);
165
+
166
+ const aChanged = afterA !== ORIGINAL_A;
167
+ const bChanged = afterB !== ORIGINAL_B;
168
+ console.log(`changed ${FILE_A}: ${aChanged}`);
169
+ console.log(`changed ${FILE_B}: ${bChanged}`);
170
+
171
+ console.log('\n====== CAPTURED CHECKPOINTS ======');
172
+ console.log(bundle.checkpointer.capturedPaths());
173
+
174
+ // Now rewind and verify originals come back.
175
+ const restoredCount = await bundle.checkpointer.rewind();
176
+ console.log(`\n====== AFTER REWIND (restored ${restoredCount} files) ======`);
177
+ const rewoundA = await readFile(join(cwd, FILE_A), 'utf8');
178
+ const rewoundB = await readFile(join(cwd, FILE_B), 'utf8');
179
+ console.log(`--- ${FILE_A} ---\n${rewoundA}`);
180
+ console.log(`--- ${FILE_B} ---\n${rewoundB}`);
181
+
182
+ const aBack = rewoundA === ORIGINAL_A;
183
+ const bBack = rewoundB === ORIGINAL_B;
184
+ console.log(`${FILE_A} restored to original: ${aBack}`);
185
+ console.log(`${FILE_B} restored to original: ${bBack}`);
186
+
187
+ if (!(aBack && bBack)) {
188
+ console.error('\n[checkpointer] rewind did not restore both files (BUG)');
189
+ process.exitCode = 1;
190
+ } else {
191
+ console.log('\n[checkpointer] all files restored to pre-run state ✔');
192
+ }
193
+
194
+ await rm(cwd, { recursive: true, force: true });
195
+ }
196
+
197
+ process.on('unhandledRejection', (reason) => {
198
+ console.error('Unhandled Rejection:', reason);
199
+ process.exit(1);
200
+ });
201
+
202
+ main().catch((err) => {
203
+ console.error(err);
204
+ process.exit(1);
205
+ });
@@ -0,0 +1,263 @@
1
+ /**
2
+ * src/scripts/local_engine_compile.ts
3
+ *
4
+ * Live demonstration of the cheap typecheck integration shipped as
5
+ * an alternative to full LSP:
6
+ *
7
+ * 1. Post-edit syntax check (`local.postEditSyntaxCheck: 'auto'`)
8
+ * runs `node --check` after every edit_file / write_file on
9
+ * `.js`/`.mjs` files — the model sees the parse error in the
10
+ * tool result and self-corrects on the next turn.
11
+ *
12
+ * 2. `compile_check` tool runs the project's standard typecheck
13
+ * command (auto-detected from project markers, or supplied via
14
+ * the `command` arg) and surfaces the result.
15
+ *
16
+ * The script seeds a tiny package.json + tsconfig + index.ts in a
17
+ * temp dir, then asks the model to:
18
+ * (a) `write_file` an intentionally broken `index.js`,
19
+ * (b) call `compile_check` to see the error,
20
+ * (c) fix the file via `edit_file`,
21
+ * (d) call `compile_check` again to confirm green.
22
+ *
23
+ * Run with: `npm run local:compile`
24
+ */
25
+ import { config } from 'dotenv';
26
+ config();
27
+ import { tmpdir } from 'os';
28
+ import { join } from 'path';
29
+ import { mkdtemp, rm, symlink, writeFile } from 'fs/promises';
30
+ import { resolve } from 'path';
31
+ import { HumanMessage, ToolMessage } from '@langchain/core/messages';
32
+ import type { BaseMessage } from '@langchain/core/messages';
33
+ import type * as t from '@/types';
34
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
35
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
36
+ import { getLLMConfig } from '@/utils/llmConfig';
37
+ import { getArgs } from '@/scripts/args';
38
+ import { GraphEvents } from '@/common';
39
+ import { Run } from '@/run';
40
+
41
+ const conversationHistory: BaseMessage[] = [];
42
+
43
+ async function main(): Promise<void> {
44
+ const { userName, provider } = await getArgs();
45
+ const cwd = await mkdtemp(join(tmpdir(), 'lc-local-compile-'));
46
+ console.log(`[local_engine_compile] workspace: ${cwd}`);
47
+
48
+ // Seed a tiny TypeScript project so `compile_check` can actually
49
+ // resolve a real toolchain (tsc). We include a healthy index.ts
50
+ // baseline; the LLM will be asked to add a broken file alongside.
51
+ await writeFile(
52
+ join(cwd, 'tsconfig.json'),
53
+ JSON.stringify(
54
+ {
55
+ compilerOptions: {
56
+ target: 'ES2020',
57
+ module: 'commonjs',
58
+ strict: true,
59
+ noEmit: true,
60
+ skipLibCheck: true,
61
+ },
62
+ include: ['*.ts'],
63
+ },
64
+ null,
65
+ 2
66
+ ),
67
+ 'utf8'
68
+ );
69
+ await writeFile(
70
+ join(cwd, 'index.ts'),
71
+ 'export function add(a: number, b: number): number {\n return a + b;\n}\n',
72
+ 'utf8'
73
+ );
74
+
75
+ // Make tsc available in the temp workspace by symlinking the
76
+ // surrounding repo's node_modules. This avoids a slow first-run
77
+ // npm install while the model waits, and lets `npx --no-install
78
+ // tsc` resolve typescript instantly. `process.cwd()` is the
79
+ // package root when running via `npm run`. We fall back
80
+ // gracefully on platforms where the symlink isn't possible.
81
+ const repoNodeModules = resolve(process.cwd(), 'node_modules');
82
+ try {
83
+ await symlink(repoNodeModules, join(cwd, 'node_modules'), 'dir');
84
+ } catch (err) {
85
+ console.warn(
86
+ '[local_engine_compile] could not symlink node_modules:',
87
+ (err as Error).message
88
+ );
89
+ }
90
+ await writeFile(
91
+ join(cwd, 'package.json'),
92
+ JSON.stringify({ name: 'lc-local-compile-demo', private: true }, null, 2),
93
+ 'utf8'
94
+ );
95
+
96
+ const { aggregateContent } = createContentAggregator();
97
+ const customHandlers = {
98
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
99
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
100
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
101
+ // Forward ON_RUN_STEP so the aggregator's stepMap is seeded
102
+ // before ON_RUN_STEP_COMPLETED arrives — without this the
103
+ // aggregator logs "No run step or runId found for completed step
104
+ // event" once per tool call (issue #142).
105
+ [GraphEvents.ON_RUN_STEP]: {
106
+ handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData): void => {
107
+ aggregateContent({ event, data: data as t.RunStep });
108
+ },
109
+ },
110
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
111
+ handle: (
112
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
113
+ data: t.StreamEventData
114
+ ): void => {
115
+ aggregateContent({
116
+ event,
117
+ data: data as unknown as { result: t.ToolEndEvent },
118
+ });
119
+ },
120
+ },
121
+ [GraphEvents.TOOL_START]: {
122
+ handle: (_event: string, data: t.StreamEventData): void => {
123
+ const obj = data as unknown as { name?: string; input?: unknown };
124
+ const inputStr = JSON.stringify(obj.input);
125
+ console.log(
126
+ `====== TOOL_START tool=${obj.name ?? '?'} input=${
127
+ inputStr.length > 200 ? inputStr.slice(0, 200) + '…' : inputStr
128
+ } ======`
129
+ );
130
+ },
131
+ },
132
+ };
133
+
134
+ const llmConfig = getLLMConfig(provider);
135
+
136
+ const runConfig: t.RunConfig = {
137
+ runId: 'local-engine-compile-1',
138
+ graphConfig: {
139
+ type: 'standard',
140
+ llmConfig,
141
+ instructions:
142
+ `You are ${userName}'s coding assistant. The host has wired a ` +
143
+ 'post-edit syntax check (you will see `[syntax-check warning ...]` blocks ' +
144
+ 'in tool results) and the `compile_check` tool. Use them to verify your ' +
145
+ 'edits. When a syntax check warns you of an error, FIX IT before doing ' +
146
+ 'anything else. Keep responses tight.',
147
+ },
148
+ toolExecution: {
149
+ engine: 'local',
150
+ local: {
151
+ cwd,
152
+ postEditSyntaxCheck: 'auto',
153
+ timeoutMs: 30_000,
154
+ },
155
+ },
156
+ returnContent: true,
157
+ skipCleanup: true,
158
+ customHandlers,
159
+ };
160
+ const run = await Run.create<t.IState>(runConfig);
161
+
162
+ const streamConfig = {
163
+ configurable: { provider, thread_id: 'local-engine-compile-thread-1' },
164
+ streamMode: 'values',
165
+ version: 'v2' as const,
166
+ };
167
+
168
+ const userMessage = new HumanMessage(
169
+ `Hi ${userName}. Here's the workflow. The workspace is a tiny TypeScript project (tsconfig.json + index.ts).\n\n` +
170
+ '1. First, exercise the post-edit syntax check. Use `write_file` to create `broken.js` with EXACTLY this content (it is intentionally broken):\n' +
171
+ '```\n' +
172
+ 'function broken(a,) {\n' +
173
+ ' return a +;\n' +
174
+ '}\n' +
175
+ '```\n' +
176
+ '2. After the write, the post-edit syntax check should warn you with `[syntax-check warning ...]`. Read the warning carefully and fix the file via `edit_file` or `write_file`. The smallest valid fix is fine.\n\n' +
177
+ '3. Now exercise `compile_check`. Use `write_file` to create `broken.ts` with EXACTLY this content (it has a TYPE error, not a syntax error — `tsc` will catch it):\n' +
178
+ '```\n' +
179
+ 'export const x: number = "not a number";\n' +
180
+ '```\n' +
181
+ '4. Call `compile_check` (no args). Anthropic\'s tsc should fail with a type error.\n' +
182
+ '5. Fix the type error in `broken.ts` (e.g. change the literal to a number) via `edit_file`.\n' +
183
+ '6. Call `compile_check` again to confirm green.\n\n' +
184
+ 'Tell me what each `compile_check` call reported. Be concise.'
185
+ );
186
+ conversationHistory.push(userMessage);
187
+ console.log('====== USER ======\n' + userMessage.content + '\n');
188
+
189
+ await run.processStream({ messages: conversationHistory }, streamConfig);
190
+ const finalMessages = run.getRunMessages();
191
+ if (finalMessages) {
192
+ conversationHistory.push(...finalMessages);
193
+ }
194
+
195
+ let sawSyntaxWarning = false;
196
+ let compileCheckCalls = 0;
197
+ let compileCheckFailedAtLeastOnce = false;
198
+ let compileCheckPassedAtLeastOnce = false;
199
+ console.log('\n====== TOOL MESSAGES IN HISTORY ======');
200
+ for (const msg of conversationHistory) {
201
+ if (msg instanceof ToolMessage) {
202
+ const content =
203
+ typeof msg.content === 'string'
204
+ ? msg.content
205
+ : JSON.stringify(msg.content);
206
+ const head = content.slice(0, 240);
207
+ console.log(`- ${msg.name}: ${head.replace(/\n/g, ' ⏎ ')}`);
208
+ if (content.includes('[syntax-check')) sawSyntaxWarning = true;
209
+ if (msg.name === 'compile_check') {
210
+ compileCheckCalls += 1;
211
+ if (content.includes('PASSED')) compileCheckPassedAtLeastOnce = true;
212
+ if (content.includes('FAILED')) compileCheckFailedAtLeastOnce = true;
213
+ }
214
+ }
215
+ }
216
+
217
+ console.log('\n====== ASSISTANT FINAL TEXT ======');
218
+ const lastAssistant = [...conversationHistory]
219
+ .reverse()
220
+ .find((m) => m._getType() === 'ai');
221
+ if (lastAssistant) {
222
+ const c = lastAssistant.content;
223
+ console.log(
224
+ typeof c === 'string'
225
+ ? c
226
+ : Array.isArray(c)
227
+ ? c
228
+ .map((b) => ('text' in b ? b.text : `<${b.type}>`))
229
+ .join(' ')
230
+ : JSON.stringify(c)
231
+ );
232
+ }
233
+
234
+ console.log('\n====== ASSERTIONS ======');
235
+ console.log(
236
+ `post-edit syntax-check warning observed: ${sawSyntaxWarning} ${
237
+ sawSyntaxWarning ? '✔' : '✖'
238
+ }`
239
+ );
240
+ console.log(
241
+ `compile_check called ${compileCheckCalls} time(s); FAILED-then-PASSED cycle observed: ${
242
+ compileCheckFailedAtLeastOnce && compileCheckPassedAtLeastOnce
243
+ } ${compileCheckFailedAtLeastOnce && compileCheckPassedAtLeastOnce ? '✔' : '✖'}`
244
+ );
245
+
246
+ if (
247
+ !sawSyntaxWarning ||
248
+ !(compileCheckFailedAtLeastOnce && compileCheckPassedAtLeastOnce)
249
+ ) {
250
+ process.exitCode = 1;
251
+ }
252
+ await rm(cwd, { recursive: true, force: true });
253
+ }
254
+
255
+ process.on('unhandledRejection', (reason) => {
256
+ console.error('Unhandled Rejection:', reason);
257
+ process.exit(1);
258
+ });
259
+
260
+ main().catch((err) => {
261
+ console.error(err);
262
+ process.exit(1);
263
+ });