@juspay/neurolink 9.51.4 → 9.53.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 (186) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +19 -0
  3. package/dist/agent/directTools.d.ts +2 -2
  4. package/dist/auth/errors.d.ts +1 -1
  5. package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
  6. package/dist/auth/providers/BaseAuthProvider.d.ts +1 -1
  7. package/dist/autoresearch/config.d.ts +11 -0
  8. package/dist/autoresearch/config.js +108 -0
  9. package/dist/autoresearch/errors.d.ts +40 -0
  10. package/dist/autoresearch/errors.js +20 -0
  11. package/dist/autoresearch/index.d.ts +23 -0
  12. package/dist/autoresearch/index.js +34 -0
  13. package/dist/autoresearch/phasePolicy.d.ts +9 -0
  14. package/dist/autoresearch/phasePolicy.js +69 -0
  15. package/dist/autoresearch/promptCompiler.d.ts +15 -0
  16. package/dist/autoresearch/promptCompiler.js +120 -0
  17. package/dist/autoresearch/repoPolicy.d.ts +32 -0
  18. package/dist/autoresearch/repoPolicy.js +128 -0
  19. package/dist/autoresearch/resultRecorder.d.ts +20 -0
  20. package/dist/autoresearch/resultRecorder.js +130 -0
  21. package/dist/autoresearch/runner.d.ts +10 -0
  22. package/dist/autoresearch/runner.js +102 -0
  23. package/dist/autoresearch/stateStore.d.ts +12 -0
  24. package/dist/autoresearch/stateStore.js +163 -0
  25. package/dist/autoresearch/summaryParser.d.ts +16 -0
  26. package/dist/autoresearch/summaryParser.js +94 -0
  27. package/dist/autoresearch/tools.d.ts +257 -0
  28. package/dist/autoresearch/tools.js +617 -0
  29. package/dist/autoresearch/worker.d.ts +71 -0
  30. package/dist/autoresearch/worker.js +417 -0
  31. package/dist/browser/neurolink.min.js +340 -324
  32. package/dist/cli/commands/autoresearch.d.ts +41 -0
  33. package/dist/cli/commands/autoresearch.js +487 -0
  34. package/dist/cli/commands/config.d.ts +1 -1
  35. package/dist/cli/commands/task.d.ts +2 -0
  36. package/dist/cli/commands/task.js +32 -3
  37. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  38. package/dist/cli/parser.js +4 -1
  39. package/dist/core/baseProvider.js +18 -0
  40. package/dist/core/factory.d.ts +2 -2
  41. package/dist/core/factory.js +4 -4
  42. package/dist/evaluation/errors/EvaluationError.d.ts +1 -1
  43. package/dist/factories/providerFactory.d.ts +4 -4
  44. package/dist/factories/providerFactory.js +20 -7
  45. package/dist/factories/providerRegistry.d.ts +5 -0
  46. package/dist/factories/providerRegistry.js +45 -26
  47. package/dist/lib/agent/directTools.d.ts +2 -2
  48. package/dist/lib/auth/errors.d.ts +1 -1
  49. package/dist/lib/auth/middleware/AuthMiddleware.d.ts +1 -1
  50. package/dist/lib/auth/providers/BaseAuthProvider.d.ts +1 -1
  51. package/dist/lib/autoresearch/config.d.ts +11 -0
  52. package/dist/lib/autoresearch/config.js +109 -0
  53. package/dist/lib/autoresearch/errors.d.ts +40 -0
  54. package/dist/lib/autoresearch/errors.js +21 -0
  55. package/dist/lib/autoresearch/index.d.ts +23 -0
  56. package/dist/lib/autoresearch/index.js +35 -0
  57. package/dist/lib/autoresearch/phasePolicy.d.ts +9 -0
  58. package/dist/lib/autoresearch/phasePolicy.js +70 -0
  59. package/dist/lib/autoresearch/promptCompiler.d.ts +15 -0
  60. package/dist/lib/autoresearch/promptCompiler.js +121 -0
  61. package/dist/lib/autoresearch/repoPolicy.d.ts +32 -0
  62. package/dist/lib/autoresearch/repoPolicy.js +129 -0
  63. package/dist/lib/autoresearch/resultRecorder.d.ts +20 -0
  64. package/dist/lib/autoresearch/resultRecorder.js +131 -0
  65. package/dist/lib/autoresearch/runner.d.ts +10 -0
  66. package/dist/lib/autoresearch/runner.js +103 -0
  67. package/dist/lib/autoresearch/stateStore.d.ts +12 -0
  68. package/dist/lib/autoresearch/stateStore.js +164 -0
  69. package/dist/lib/autoresearch/summaryParser.d.ts +16 -0
  70. package/dist/lib/autoresearch/summaryParser.js +95 -0
  71. package/dist/lib/autoresearch/tools.d.ts +257 -0
  72. package/dist/lib/autoresearch/tools.js +618 -0
  73. package/dist/lib/autoresearch/worker.d.ts +71 -0
  74. package/dist/lib/autoresearch/worker.js +418 -0
  75. package/dist/lib/core/baseProvider.js +18 -0
  76. package/dist/lib/core/factory.d.ts +2 -2
  77. package/dist/lib/core/factory.js +4 -4
  78. package/dist/lib/evaluation/errors/EvaluationError.d.ts +1 -1
  79. package/dist/lib/factories/providerFactory.d.ts +4 -4
  80. package/dist/lib/factories/providerFactory.js +20 -7
  81. package/dist/lib/factories/providerRegistry.d.ts +5 -0
  82. package/dist/lib/factories/providerRegistry.js +45 -26
  83. package/dist/lib/files/fileTools.d.ts +1 -1
  84. package/dist/lib/neurolink.d.ts +21 -0
  85. package/dist/lib/neurolink.js +91 -8
  86. package/dist/lib/providers/amazonBedrock.d.ts +6 -1
  87. package/dist/lib/providers/amazonBedrock.js +14 -2
  88. package/dist/lib/providers/amazonSagemaker.d.ts +7 -1
  89. package/dist/lib/providers/amazonSagemaker.js +21 -3
  90. package/dist/lib/providers/anthropic.d.ts +4 -1
  91. package/dist/lib/providers/anthropic.js +18 -5
  92. package/dist/lib/providers/azureOpenai.d.ts +2 -1
  93. package/dist/lib/providers/azureOpenai.js +10 -5
  94. package/dist/lib/providers/googleAiStudio.d.ts +4 -1
  95. package/dist/lib/providers/googleAiStudio.js +6 -7
  96. package/dist/lib/providers/googleVertex.d.ts +3 -1
  97. package/dist/lib/providers/googleVertex.js +96 -17
  98. package/dist/lib/providers/huggingFace.d.ts +2 -1
  99. package/dist/lib/providers/huggingFace.js +4 -4
  100. package/dist/lib/providers/litellm.d.ts +5 -1
  101. package/dist/lib/providers/litellm.js +16 -11
  102. package/dist/lib/providers/mistral.d.ts +2 -1
  103. package/dist/lib/providers/mistral.js +2 -2
  104. package/dist/lib/providers/ollama.d.ts +3 -1
  105. package/dist/lib/providers/ollama.js +2 -2
  106. package/dist/lib/providers/openAI.d.ts +5 -1
  107. package/dist/lib/providers/openAI.js +15 -5
  108. package/dist/lib/providers/openRouter.d.ts +5 -1
  109. package/dist/lib/providers/openRouter.js +19 -7
  110. package/dist/lib/providers/openaiCompatible.d.ts +4 -1
  111. package/dist/lib/providers/openaiCompatible.js +18 -4
  112. package/dist/lib/tasks/autoresearchTaskExecutor.d.ts +32 -0
  113. package/dist/lib/tasks/autoresearchTaskExecutor.js +303 -0
  114. package/dist/lib/tasks/errors.d.ts +3 -1
  115. package/dist/lib/tasks/errors.js +1 -0
  116. package/dist/lib/tasks/taskExecutor.d.ts +4 -2
  117. package/dist/lib/tasks/taskExecutor.js +8 -1
  118. package/dist/lib/tasks/taskManager.js +27 -3
  119. package/dist/lib/tasks/tools/taskTools.d.ts +1 -1
  120. package/dist/lib/telemetry/attributes.d.ts +15 -0
  121. package/dist/lib/telemetry/attributes.js +16 -0
  122. package/dist/lib/telemetry/tracers.d.ts +1 -0
  123. package/dist/lib/telemetry/tracers.js +1 -0
  124. package/dist/lib/types/autoresearchTypes.d.ts +194 -0
  125. package/dist/lib/types/autoresearchTypes.js +18 -0
  126. package/dist/lib/types/common.d.ts +11 -0
  127. package/dist/lib/types/configTypes.d.ts +7 -0
  128. package/dist/lib/types/generateTypes.d.ts +13 -0
  129. package/dist/lib/types/index.d.ts +16 -14
  130. package/dist/lib/types/index.js +21 -17
  131. package/dist/lib/types/providers.d.ts +75 -0
  132. package/dist/lib/types/streamTypes.d.ts +7 -1
  133. package/dist/lib/types/taskTypes.d.ts +38 -0
  134. package/dist/lib/workflow/config.d.ts +3 -3
  135. package/dist/neurolink.d.ts +21 -0
  136. package/dist/neurolink.js +91 -8
  137. package/dist/providers/amazonBedrock.d.ts +6 -1
  138. package/dist/providers/amazonBedrock.js +14 -2
  139. package/dist/providers/amazonSagemaker.d.ts +7 -1
  140. package/dist/providers/amazonSagemaker.js +21 -3
  141. package/dist/providers/anthropic.d.ts +4 -1
  142. package/dist/providers/anthropic.js +18 -5
  143. package/dist/providers/azureOpenai.d.ts +2 -1
  144. package/dist/providers/azureOpenai.js +10 -5
  145. package/dist/providers/googleAiStudio.d.ts +4 -1
  146. package/dist/providers/googleAiStudio.js +6 -7
  147. package/dist/providers/googleVertex.d.ts +3 -1
  148. package/dist/providers/googleVertex.js +96 -17
  149. package/dist/providers/huggingFace.d.ts +2 -1
  150. package/dist/providers/huggingFace.js +4 -4
  151. package/dist/providers/litellm.d.ts +5 -1
  152. package/dist/providers/litellm.js +16 -11
  153. package/dist/providers/mistral.d.ts +2 -1
  154. package/dist/providers/mistral.js +2 -2
  155. package/dist/providers/ollama.d.ts +3 -1
  156. package/dist/providers/ollama.js +2 -2
  157. package/dist/providers/openAI.d.ts +5 -1
  158. package/dist/providers/openAI.js +15 -5
  159. package/dist/providers/openRouter.d.ts +5 -1
  160. package/dist/providers/openRouter.js +19 -7
  161. package/dist/providers/openaiCompatible.d.ts +4 -1
  162. package/dist/providers/openaiCompatible.js +18 -4
  163. package/dist/rag/errors/RAGError.d.ts +1 -1
  164. package/dist/tasks/autoresearchTaskExecutor.d.ts +32 -0
  165. package/dist/tasks/autoresearchTaskExecutor.js +302 -0
  166. package/dist/tasks/errors.d.ts +3 -1
  167. package/dist/tasks/errors.js +1 -0
  168. package/dist/tasks/taskExecutor.d.ts +4 -2
  169. package/dist/tasks/taskExecutor.js +8 -1
  170. package/dist/tasks/taskManager.js +27 -3
  171. package/dist/tasks/tools/taskTools.d.ts +1 -1
  172. package/dist/telemetry/attributes.d.ts +15 -0
  173. package/dist/telemetry/attributes.js +16 -0
  174. package/dist/telemetry/tracers.d.ts +1 -0
  175. package/dist/telemetry/tracers.js +1 -0
  176. package/dist/types/autoresearchTypes.d.ts +194 -0
  177. package/dist/types/autoresearchTypes.js +17 -0
  178. package/dist/types/common.d.ts +11 -0
  179. package/dist/types/configTypes.d.ts +7 -0
  180. package/dist/types/generateTypes.d.ts +13 -0
  181. package/dist/types/index.d.ts +16 -14
  182. package/dist/types/index.js +21 -17
  183. package/dist/types/providers.d.ts +75 -0
  184. package/dist/types/streamTypes.d.ts +7 -1
  185. package/dist/types/taskTypes.d.ts +38 -0
  186. package/package.json +3 -2
@@ -0,0 +1,618 @@
1
+ /**
2
+ * Research tools factory for AutoResearch system.
3
+ *
4
+ * These tools allow an AI agent to conduct autonomous experiments:
5
+ * reading/writing code, running experiments, recording results, and
6
+ * managing the research lifecycle (accept/revert/checkpoint).
7
+ *
8
+ * @module autoresearch/tools
9
+ */
10
+ import { execFileSync } from "node:child_process";
11
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
+ import path from "node:path";
13
+ import { tool } from "ai";
14
+ import { z } from "zod";
15
+ import { withTimeout } from "../utils/errorHandling.js";
16
+ import { logger } from "../utils/logger.js";
17
+ import { parseExperimentSummary } from "./summaryParser.js";
18
+ /**
19
+ * Create research management tools bound to a research session.
20
+ *
21
+ * These tools follow the same factory pattern as `createTaskTools()` in
22
+ * `src/lib/tasks/tools/taskTools.ts`. Dependencies are captured via closure,
23
+ * eliminating the need for module-level singleton state.
24
+ *
25
+ * @param deps - The research dependencies to bind to
26
+ * @returns Record of tool name to tool definition
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const tools = createResearchTools({ config, stateStore, repoPolicy, runner, recorder });
31
+ * // tools.research_get_context, tools.research_read_file, etc.
32
+ * ```
33
+ */
34
+ export function createResearchTools(deps) {
35
+ const { config, stateStore, repoPolicy, runner, recorder } = deps;
36
+ return {
37
+ /**
38
+ * Get current research context including state, config, and recent results.
39
+ */
40
+ research_get_context: tool({
41
+ description: "Get the current research context including branch, commits, metrics, recent results, paths, phase, and run count.",
42
+ inputSchema: z.object({}),
43
+ execute: async () => {
44
+ try {
45
+ const state = await stateStore.load();
46
+ if (!state) {
47
+ return {
48
+ success: false,
49
+ error: "No research state found. Initialize first.",
50
+ };
51
+ }
52
+ // Get recent results (last 10)
53
+ const allRecords = await recorder.readAll();
54
+ const recentResults = allRecords.slice(-10);
55
+ return {
56
+ success: true,
57
+ branch: state.branch,
58
+ acceptedCommit: state.acceptedCommit,
59
+ bestMetric: state.bestMetric,
60
+ recentResults,
61
+ mutablePaths: config.mutablePaths,
62
+ immutablePaths: config.immutablePaths,
63
+ currentPhase: state.currentPhase,
64
+ runCount: state.runCount,
65
+ tag: state.tag,
66
+ keepCount: state.keepCount,
67
+ };
68
+ }
69
+ catch (error) {
70
+ logger.error("[researchTools] research_get_context failed", {
71
+ error: String(error),
72
+ });
73
+ return {
74
+ success: false,
75
+ error: error instanceof Error ? error.message : String(error),
76
+ };
77
+ }
78
+ },
79
+ }),
80
+ /**
81
+ * Read a file from the repository if allowed by policy.
82
+ */
83
+ research_read_file: tool({
84
+ description: "Read the contents of a file from the repository. Only readable if in mutablePaths, immutablePaths, or is the programPath.",
85
+ inputSchema: z.object({
86
+ path: z.string().describe("Relative file path from repo root"),
87
+ }),
88
+ execute: async ({ path: filePath }) => {
89
+ try {
90
+ if (!repoPolicy.isReadAllowed(filePath)) {
91
+ return {
92
+ success: false,
93
+ error: `Read not allowed for path: ${filePath}. Must be in mutablePaths, immutablePaths, or programPath.`,
94
+ };
95
+ }
96
+ const fullPath = path.join(config.repoPath, filePath);
97
+ const content = readFileSync(fullPath, "utf-8");
98
+ return {
99
+ success: true,
100
+ path: filePath,
101
+ content,
102
+ };
103
+ }
104
+ catch (error) {
105
+ logger.error("[researchTools] research_read_file failed", {
106
+ path: filePath,
107
+ error: String(error),
108
+ });
109
+ return {
110
+ success: false,
111
+ error: error instanceof Error ? error.message : String(error),
112
+ path: filePath,
113
+ };
114
+ }
115
+ },
116
+ }),
117
+ /**
118
+ * Write a candidate file to the repository if allowed by policy.
119
+ */
120
+ research_write_candidate: tool({
121
+ description: "Write content to a file in the repository. Only allowed for paths in mutablePaths.",
122
+ inputSchema: z.object({
123
+ path: z.string().describe("Relative file path from repo root"),
124
+ content: z.string().describe("Content to write to the file"),
125
+ }),
126
+ execute: async ({ path: filePath, content }) => {
127
+ try {
128
+ if (!repoPolicy.isWriteAllowed(filePath)) {
129
+ return {
130
+ success: false,
131
+ error: `Write not allowed for path: ${filePath}. Must be in mutablePaths.`,
132
+ };
133
+ }
134
+ // Detect and fix literal escape sequences that LLMs sometimes produce.
135
+ // If the content has literal \n but no real newlines (for files > ~10 lines),
136
+ // the AI serialized newlines incorrectly.
137
+ let sanitizedContent = content;
138
+ const realNewlines = (content.match(/\n/g) || []).length;
139
+ const literalBackslashN = (content.match(/\\n/g) || []).length;
140
+ if (realNewlines < 5 && literalBackslashN > 20) {
141
+ // Content looks like it has literal \n instead of real newlines
142
+ sanitizedContent = content
143
+ .replace(/\\n/g, "\n")
144
+ .replace(/\\t/g, "\t")
145
+ .replace(/\\\\/g, "\\");
146
+ logger.warn(`[researchTools] Detected literal escape sequences in write content for ${filePath}. ` +
147
+ `Fixed ${literalBackslashN} literal \\n → real newlines.`);
148
+ }
149
+ const fullPath = path.join(config.repoPath, filePath);
150
+ writeFileSync(fullPath, sanitizedContent, "utf-8");
151
+ return {
152
+ success: true,
153
+ path: filePath,
154
+ bytesWritten: Buffer.byteLength(sanitizedContent, "utf-8"),
155
+ };
156
+ }
157
+ catch (error) {
158
+ logger.error("[researchTools] research_write_candidate failed", {
159
+ path: filePath,
160
+ error: String(error),
161
+ });
162
+ return {
163
+ success: false,
164
+ error: error instanceof Error ? error.message : String(error),
165
+ path: filePath,
166
+ };
167
+ }
168
+ },
169
+ }),
170
+ /**
171
+ * Get git diff of mutable paths only.
172
+ */
173
+ research_diff: tool({
174
+ description: "Get the git diff showing changes to mutablePaths only. Returns empty string if no changes.",
175
+ inputSchema: z.object({}),
176
+ execute: async () => {
177
+ try {
178
+ // Get diff for each mutable path
179
+ const diffs = [];
180
+ for (const mutablePath of config.mutablePaths) {
181
+ try {
182
+ const diff = execFileSync("git", ["diff", "--", mutablePath], {
183
+ cwd: config.repoPath,
184
+ encoding: "utf-8",
185
+ });
186
+ if (diff.trim()) {
187
+ diffs.push(diff);
188
+ }
189
+ }
190
+ catch (err) {
191
+ // Only suppress if path doesn't exist
192
+ const fullPath = path.join(config.repoPath, mutablePath);
193
+ if (!existsSync(fullPath)) {
194
+ continue; // Path doesn't exist yet, skip
195
+ }
196
+ throw err; // Real git error, let outer handler catch it
197
+ }
198
+ }
199
+ const combinedDiff = diffs.join("\n");
200
+ return {
201
+ success: true,
202
+ diff: combinedDiff,
203
+ hasChanges: combinedDiff.length > 0,
204
+ };
205
+ }
206
+ catch (error) {
207
+ logger.error("[researchTools] research_diff failed", {
208
+ error: String(error),
209
+ });
210
+ return {
211
+ success: false,
212
+ error: error instanceof Error ? error.message : String(error),
213
+ };
214
+ }
215
+ },
216
+ }),
217
+ /**
218
+ * Commit staged changes as a candidate.
219
+ */
220
+ research_commit_candidate: tool({
221
+ description: "Commit staged changes as a candidate experiment. Validates branch and paths, stages mutablePaths, creates commit, and updates state with candidateCommit.",
222
+ inputSchema: z.object({
223
+ message: z.string().describe("Git commit message"),
224
+ }),
225
+ execute: async ({ message }) => {
226
+ try {
227
+ const state = await stateStore.load();
228
+ if (!state) {
229
+ return { success: false, error: "No research state found." };
230
+ }
231
+ // Stage mutable paths first (so validateCommit checks the staged index)
232
+ for (const mutablePath of config.mutablePaths) {
233
+ try {
234
+ execFileSync("git", ["add", "--", mutablePath], {
235
+ cwd: config.repoPath,
236
+ encoding: "utf-8",
237
+ });
238
+ }
239
+ catch (addErr) {
240
+ // Only ignore if the path doesn't exist; rethrow real git errors
241
+ const msg = addErr instanceof Error ? addErr.message : String(addErr);
242
+ if (!msg.includes("did not match any files") &&
243
+ !msg.includes("pathspec")) {
244
+ throw addErr;
245
+ }
246
+ }
247
+ }
248
+ // Validate commit (checks staged files against policy)
249
+ const validation = await repoPolicy.validateCommit(state.branch);
250
+ if (!validation.valid) {
251
+ // Unstage on validation failure
252
+ for (const mutablePath of config.mutablePaths) {
253
+ try {
254
+ execFileSync("git", ["restore", "--staged", "--", mutablePath], {
255
+ cwd: config.repoPath,
256
+ encoding: "utf-8",
257
+ });
258
+ }
259
+ catch {
260
+ /* ignore */
261
+ }
262
+ }
263
+ return {
264
+ success: false,
265
+ error: `Commit validation failed: ${validation.violations.join(", ")}`,
266
+ violations: validation.violations,
267
+ };
268
+ }
269
+ // Create commit (--no-verify skips pre-commit hooks which may fail in worktrees)
270
+ execFileSync("git", ["commit", "--no-verify", "-m", message], {
271
+ cwd: config.repoPath,
272
+ encoding: "utf-8",
273
+ });
274
+ // Get the new commit hash
275
+ const candidateCommit = execFileSync("git", ["rev-parse", "--short=7", "HEAD"], {
276
+ cwd: config.repoPath,
277
+ encoding: "utf-8",
278
+ }).trim();
279
+ // Update state — clear run-derived fields so next experiment starts fresh
280
+ await stateStore.update({
281
+ candidateCommit,
282
+ lastSummary: null,
283
+ lastStatus: null,
284
+ });
285
+ return {
286
+ success: true,
287
+ candidateCommit,
288
+ message,
289
+ };
290
+ }
291
+ catch (error) {
292
+ logger.error("[researchTools] research_commit_candidate failed", {
293
+ error: String(error),
294
+ });
295
+ return {
296
+ success: false,
297
+ error: error instanceof Error ? error.message : String(error),
298
+ };
299
+ }
300
+ },
301
+ }),
302
+ /**
303
+ * Run the experiment.
304
+ */
305
+ research_run_experiment: tool({
306
+ description: "Run the configured experiment command with timeout. Returns structured summary with metric, memory, and crash status.",
307
+ inputSchema: z.object({
308
+ description: z.string().describe("Description of this experiment run"),
309
+ }),
310
+ execute: async ({ description }) => {
311
+ try {
312
+ logger.info("[researchTools] Starting experiment", { description });
313
+ const summary = await withTimeout(runner.run(), config.timeoutMs + 30_000, new Error("Experiment runner exceeded safety timeout"));
314
+ // Increment run count and save lastSummary
315
+ const state = await stateStore.load();
316
+ if (state) {
317
+ await stateStore.update({
318
+ runCount: state.runCount + 1,
319
+ lastSummary: summary,
320
+ });
321
+ }
322
+ return {
323
+ success: true,
324
+ description,
325
+ summary,
326
+ };
327
+ }
328
+ catch (error) {
329
+ logger.error("[researchTools] research_run_experiment failed", {
330
+ error: String(error),
331
+ });
332
+ return {
333
+ success: false,
334
+ error: error instanceof Error ? error.message : String(error),
335
+ };
336
+ }
337
+ },
338
+ }),
339
+ /**
340
+ * Parse the experiment log file.
341
+ */
342
+ research_parse_log: tool({
343
+ description: "Parse the run.log file to extract structured experiment summary (metric, memory, crash status, etc.)",
344
+ inputSchema: z.object({}),
345
+ execute: async () => {
346
+ try {
347
+ const logPath = path.join(config.repoPath, config.logPath);
348
+ const logContent = readFileSync(logPath, "utf-8");
349
+ const summary = parseExperimentSummary(logContent, config.metric, config.memoryMetric);
350
+ return {
351
+ success: true,
352
+ summary,
353
+ };
354
+ }
355
+ catch (error) {
356
+ logger.error("[researchTools] research_parse_log failed", {
357
+ error: String(error),
358
+ });
359
+ return {
360
+ success: false,
361
+ error: error instanceof Error ? error.message : String(error),
362
+ };
363
+ }
364
+ },
365
+ }),
366
+ /**
367
+ * Record an experiment result.
368
+ */
369
+ research_record: tool({
370
+ description: "Record the result of an experiment to results.tsv and runs.jsonl. Status is computed deterministically from the experiment outcome.",
371
+ inputSchema: z.object({
372
+ description: z.string().describe("Description of the experiment"),
373
+ }),
374
+ execute: async ({ description }) => {
375
+ try {
376
+ const state = await stateStore.load();
377
+ if (!state) {
378
+ return { success: false, error: "No research state found." };
379
+ }
380
+ // Get the current summary from state (saved by research_run_experiment)
381
+ const summary = state.lastSummary;
382
+ if (!summary) {
383
+ return {
384
+ success: false,
385
+ error: "No experiment summary found. Run research_run_experiment first.",
386
+ };
387
+ }
388
+ const metric = summary.metric ?? null;
389
+ const memoryGb = summary.memoryValue ?? null;
390
+ // Compute status deterministically
391
+ let status;
392
+ if (summary?.timedOut) {
393
+ status = "timeout";
394
+ }
395
+ else if (summary?.crashed || metric === null) {
396
+ status = "crash";
397
+ }
398
+ else if (state.bestMetric === null) {
399
+ // First successful run - always keep
400
+ status = "keep";
401
+ }
402
+ else {
403
+ // Compare metric against best
404
+ const isImprovement = config.metric.direction === "lower"
405
+ ? metric < state.bestMetric
406
+ : metric > state.bestMetric;
407
+ status = isImprovement ? "keep" : "discard";
408
+ }
409
+ const commit = state.candidateCommit || state.acceptedCommit || "unknown";
410
+ const record = {
411
+ commit,
412
+ metric,
413
+ memoryGb,
414
+ status,
415
+ description,
416
+ timestamp: new Date().toISOString(),
417
+ };
418
+ await recorder.appendTsv(record);
419
+ await recorder.appendJsonl(record);
420
+ // Update last status in state
421
+ await stateStore.update({ lastStatus: status });
422
+ return {
423
+ success: true,
424
+ record,
425
+ };
426
+ }
427
+ catch (error) {
428
+ logger.error("[researchTools] research_record failed", {
429
+ error: String(error),
430
+ });
431
+ return {
432
+ success: false,
433
+ error: error instanceof Error ? error.message : String(error),
434
+ };
435
+ }
436
+ },
437
+ }),
438
+ /**
439
+ * Accept the candidate commit as the new baseline.
440
+ */
441
+ research_accept: tool({
442
+ description: "Accept the candidate commit as the new best. Updates acceptedCommit to candidateCommit, updates bestMetric from latest metric, and increments keepCount.",
443
+ inputSchema: z.object({}),
444
+ execute: async () => {
445
+ try {
446
+ const state = await stateStore.load();
447
+ if (!state) {
448
+ return { success: false, error: "No research state found." };
449
+ }
450
+ if (!state.candidateCommit) {
451
+ return { success: false, error: "No candidate commit to accept." };
452
+ }
453
+ // Get latest summary from state (saved by research_run_experiment)
454
+ const summary = state.lastSummary;
455
+ if (!summary || summary.metric === null) {
456
+ return {
457
+ success: false,
458
+ error: "No valid experiment summary to accept. Run an experiment first.",
459
+ };
460
+ }
461
+ if (summary.crashed || summary.timedOut) {
462
+ return {
463
+ success: false,
464
+ error: `Cannot accept a ${summary.crashed ? "crashed" : "timed-out"} experiment. Use research_revert instead.`,
465
+ };
466
+ }
467
+ // Require that the latest recorded status is "keep" (set by research_record)
468
+ if (state.lastStatus !== "keep") {
469
+ return {
470
+ success: false,
471
+ error: `Cannot accept: last recorded status is "${state.lastStatus}". Only "keep" experiments can be accepted.`,
472
+ };
473
+ }
474
+ let bestMetric = state.bestMetric;
475
+ // Validate that this is actually an improvement
476
+ if (state.bestMetric !== null) {
477
+ const isImprovement = config.metric.direction === "lower"
478
+ ? summary.metric < state.bestMetric
479
+ : summary.metric > state.bestMetric;
480
+ if (!isImprovement) {
481
+ return {
482
+ success: false,
483
+ error: `Metric ${summary.metric} is not better than current best ${state.bestMetric} (direction: ${config.metric.direction}). Use research_revert instead.`,
484
+ };
485
+ }
486
+ }
487
+ bestMetric = summary.metric;
488
+ await stateStore.update({
489
+ acceptedCommit: state.candidateCommit,
490
+ bestMetric,
491
+ baselineMetric: state.baselineMetric ?? bestMetric,
492
+ keepCount: state.keepCount + 1,
493
+ candidateCommit: null,
494
+ });
495
+ return {
496
+ success: true,
497
+ acceptedCommit: state.candidateCommit,
498
+ bestMetric,
499
+ keepCount: state.keepCount + 1,
500
+ };
501
+ }
502
+ catch (error) {
503
+ logger.error("[researchTools] research_accept failed", {
504
+ error: String(error),
505
+ });
506
+ return {
507
+ success: false,
508
+ error: error instanceof Error ? error.message : String(error),
509
+ };
510
+ }
511
+ },
512
+ }),
513
+ /**
514
+ * Revert to the accepted commit.
515
+ */
516
+ research_revert: tool({
517
+ description: "Revert repository to the accepted commit (git reset --hard). Clears candidateCommit from state.",
518
+ inputSchema: z.object({}),
519
+ execute: async () => {
520
+ try {
521
+ const state = await stateStore.load();
522
+ if (!state) {
523
+ return { success: false, error: "No research state found." };
524
+ }
525
+ if (!state.acceptedCommit) {
526
+ return {
527
+ success: false,
528
+ error: "No accepted commit to revert to.",
529
+ };
530
+ }
531
+ execFileSync("git", ["reset", "--hard", state.acceptedCommit], {
532
+ cwd: config.repoPath,
533
+ encoding: "utf-8",
534
+ });
535
+ await stateStore.update({ candidateCommit: null });
536
+ return {
537
+ success: true,
538
+ revertedTo: state.acceptedCommit,
539
+ };
540
+ }
541
+ catch (error) {
542
+ logger.error("[researchTools] research_revert failed", {
543
+ error: String(error),
544
+ });
545
+ return {
546
+ success: false,
547
+ error: error instanceof Error ? error.message : String(error),
548
+ };
549
+ }
550
+ },
551
+ }),
552
+ /**
553
+ * Inspect the last 50 lines of the run log for debugging.
554
+ */
555
+ research_inspect_failure: tool({
556
+ description: "Inspect the last 50 lines of run.log to debug experiment failures.",
557
+ inputSchema: z.object({}),
558
+ execute: async () => {
559
+ try {
560
+ const logPath = path.join(config.repoPath, config.logPath);
561
+ const logContent = readFileSync(logPath, "utf-8");
562
+ const lines = logContent.split("\n");
563
+ const lastLines = lines.slice(-50).join("\n");
564
+ return {
565
+ success: true,
566
+ tail: lastLines,
567
+ totalLines: lines.length,
568
+ };
569
+ }
570
+ catch (error) {
571
+ logger.error("[researchTools] research_inspect_failure failed", {
572
+ error: String(error),
573
+ });
574
+ return {
575
+ success: false,
576
+ error: error instanceof Error ? error.message : String(error),
577
+ };
578
+ }
579
+ },
580
+ }),
581
+ /**
582
+ * Save the current state to disk.
583
+ */
584
+ research_checkpoint: tool({
585
+ description: "Save the current research state to disk. Call periodically to ensure progress is not lost.",
586
+ inputSchema: z.object({}),
587
+ execute: async () => {
588
+ try {
589
+ const state = await stateStore.load();
590
+ if (!state) {
591
+ return {
592
+ success: false,
593
+ error: "No research state to checkpoint.",
594
+ };
595
+ }
596
+ // Force save by re-saving current state
597
+ await stateStore.save(state);
598
+ return {
599
+ success: true,
600
+ checkpointedAt: new Date().toISOString(),
601
+ phase: state.currentPhase,
602
+ runCount: state.runCount,
603
+ };
604
+ }
605
+ catch (error) {
606
+ logger.error("[researchTools] research_checkpoint failed", {
607
+ error: String(error),
608
+ });
609
+ return {
610
+ success: false,
611
+ error: error instanceof Error ? error.message : String(error),
612
+ };
613
+ }
614
+ },
615
+ }),
616
+ };
617
+ }
618
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Research worker — orchestrates the full experiment loop.
3
+ *
4
+ * Wires tools, state, policy, and NeuroLink generate() into a
5
+ * single experiment cycle. Can run standalone or via TaskManager.
6
+ *
7
+ * Emits autoresearch:* lifecycle events through an injected emitter
8
+ * and wraps key operations in OpenTelemetry spans for observability.
9
+ */
10
+ import type { AutoresearchEmitter, ExperimentPhase, ExperimentRecord, PhaseToolPolicy, ResearchConfig, ResearchState } from "../types/autoresearchTypes.js";
11
+ export declare class ResearchWorker {
12
+ private config;
13
+ private stateStore;
14
+ private repoPolicy;
15
+ private runner;
16
+ private recorder;
17
+ private promptCompiler;
18
+ private initialized;
19
+ /** Event emitter injected by NeuroLink/TaskManager for lifecycle events. */
20
+ private emitter?;
21
+ constructor(configInput: Partial<ResearchConfig> & {
22
+ repoPath: string;
23
+ mutablePaths: string[];
24
+ runCommand: string;
25
+ metric: ResearchConfig["metric"];
26
+ });
27
+ /** Set the event emitter (called by NeuroLink/TaskManager during integration). */
28
+ setEmitter(emitter: AutoresearchEmitter): void;
29
+ /** Emit a lifecycle event. Safe to call when no emitter is set. */
30
+ private emit;
31
+ /** Initialize: validate config, ensure branch, create state */
32
+ initialize(tag: string): Promise<ResearchState>;
33
+ /** Load existing state (for resuming) */
34
+ resume(): Promise<ResearchState>;
35
+ /** Run one full experiment cycle without AI — just the deterministic parts */
36
+ runExperimentCycle(description: string): Promise<ExperimentRecord>;
37
+ /** Get the tools record for use with NeuroLink.generate() */
38
+ getTools(): Record<string, unknown>;
39
+ /** Build system prompt */
40
+ getSystemPrompt(): Promise<string>;
41
+ /** Build cycle prompt */
42
+ getCyclePrompt(): Promise<string>;
43
+ /** Get current state */
44
+ getState(): Promise<ResearchState | null>;
45
+ /** Get results stats */
46
+ getStats(): Promise<import("../types/autoresearchTypes.js").ExperimentStats>;
47
+ /** Get config */
48
+ getConfig(): ResearchConfig;
49
+ /**
50
+ * Single authority for phase transitions.
51
+ * Persists the new phase to the state store and emits phase-changed event.
52
+ */
53
+ advancePhase(phase: ExperimentPhase): Promise<void>;
54
+ /**
55
+ * Returns the phase tool policy for the current phase.
56
+ * Reads the phase from persisted state.
57
+ */
58
+ getPhaseToolPolicy(): Promise<PhaseToolPolicy>;
59
+ /**
60
+ * Returns a tool filter object for the current phase, compatible
61
+ * with NeuroLink generate()'s toolFilter option.
62
+ *
63
+ * Returns { include: string[] } listing only the tools allowed
64
+ * in the current phase.
65
+ */
66
+ getToolFilterForCurrentPhase(): Promise<{
67
+ include: string[];
68
+ }>;
69
+ /** Emit an autoresearch:error event. */
70
+ private emitError;
71
+ }