@juspay/neurolink 9.52.0 → 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.
- package/CHANGELOG.md +6 -0
- package/README.md +19 -0
- package/dist/agent/directTools.d.ts +2 -2
- package/dist/auth/errors.d.ts +1 -1
- package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
- package/dist/auth/providers/BaseAuthProvider.d.ts +1 -1
- package/dist/autoresearch/config.d.ts +11 -0
- package/dist/autoresearch/config.js +108 -0
- package/dist/autoresearch/errors.d.ts +40 -0
- package/dist/autoresearch/errors.js +20 -0
- package/dist/autoresearch/index.d.ts +23 -0
- package/dist/autoresearch/index.js +34 -0
- package/dist/autoresearch/phasePolicy.d.ts +9 -0
- package/dist/autoresearch/phasePolicy.js +69 -0
- package/dist/autoresearch/promptCompiler.d.ts +15 -0
- package/dist/autoresearch/promptCompiler.js +120 -0
- package/dist/autoresearch/repoPolicy.d.ts +32 -0
- package/dist/autoresearch/repoPolicy.js +128 -0
- package/dist/autoresearch/resultRecorder.d.ts +20 -0
- package/dist/autoresearch/resultRecorder.js +130 -0
- package/dist/autoresearch/runner.d.ts +10 -0
- package/dist/autoresearch/runner.js +102 -0
- package/dist/autoresearch/stateStore.d.ts +12 -0
- package/dist/autoresearch/stateStore.js +163 -0
- package/dist/autoresearch/summaryParser.d.ts +16 -0
- package/dist/autoresearch/summaryParser.js +94 -0
- package/dist/autoresearch/tools.d.ts +257 -0
- package/dist/autoresearch/tools.js +617 -0
- package/dist/autoresearch/worker.d.ts +71 -0
- package/dist/autoresearch/worker.js +417 -0
- package/dist/browser/neurolink.min.js +340 -326
- package/dist/cli/commands/autoresearch.d.ts +41 -0
- package/dist/cli/commands/autoresearch.js +487 -0
- package/dist/cli/commands/config.d.ts +1 -1
- package/dist/cli/commands/task.d.ts +2 -0
- package/dist/cli/commands/task.js +32 -3
- package/dist/cli/parser.js +4 -1
- package/dist/core/baseProvider.js +18 -0
- package/dist/evaluation/errors/EvaluationError.d.ts +1 -1
- package/dist/lib/agent/directTools.d.ts +2 -2
- package/dist/lib/auth/errors.d.ts +1 -1
- package/dist/lib/auth/middleware/AuthMiddleware.d.ts +1 -1
- package/dist/lib/auth/providers/BaseAuthProvider.d.ts +1 -1
- package/dist/lib/autoresearch/config.d.ts +11 -0
- package/dist/lib/autoresearch/config.js +109 -0
- package/dist/lib/autoresearch/errors.d.ts +40 -0
- package/dist/lib/autoresearch/errors.js +21 -0
- package/dist/lib/autoresearch/index.d.ts +23 -0
- package/dist/lib/autoresearch/index.js +35 -0
- package/dist/lib/autoresearch/phasePolicy.d.ts +9 -0
- package/dist/lib/autoresearch/phasePolicy.js +70 -0
- package/dist/lib/autoresearch/promptCompiler.d.ts +15 -0
- package/dist/lib/autoresearch/promptCompiler.js +121 -0
- package/dist/lib/autoresearch/repoPolicy.d.ts +32 -0
- package/dist/lib/autoresearch/repoPolicy.js +129 -0
- package/dist/lib/autoresearch/resultRecorder.d.ts +20 -0
- package/dist/lib/autoresearch/resultRecorder.js +131 -0
- package/dist/lib/autoresearch/runner.d.ts +10 -0
- package/dist/lib/autoresearch/runner.js +103 -0
- package/dist/lib/autoresearch/stateStore.d.ts +12 -0
- package/dist/lib/autoresearch/stateStore.js +164 -0
- package/dist/lib/autoresearch/summaryParser.d.ts +16 -0
- package/dist/lib/autoresearch/summaryParser.js +95 -0
- package/dist/lib/autoresearch/tools.d.ts +257 -0
- package/dist/lib/autoresearch/tools.js +618 -0
- package/dist/lib/autoresearch/worker.d.ts +71 -0
- package/dist/lib/autoresearch/worker.js +418 -0
- package/dist/lib/core/baseProvider.js +18 -0
- package/dist/lib/evaluation/errors/EvaluationError.d.ts +1 -1
- package/dist/lib/files/fileTools.d.ts +1 -1
- package/dist/lib/neurolink.js +22 -2
- package/dist/lib/providers/litellm.js +2 -2
- package/dist/lib/providers/openRouter.js +2 -2
- package/dist/lib/providers/openaiCompatible.js +3 -1
- package/dist/lib/tasks/autoresearchTaskExecutor.d.ts +32 -0
- package/dist/lib/tasks/autoresearchTaskExecutor.js +303 -0
- package/dist/lib/tasks/errors.d.ts +3 -1
- package/dist/lib/tasks/errors.js +1 -0
- package/dist/lib/tasks/taskExecutor.d.ts +4 -2
- package/dist/lib/tasks/taskExecutor.js +8 -1
- package/dist/lib/tasks/taskManager.js +27 -3
- package/dist/lib/tasks/tools/taskTools.d.ts +1 -1
- package/dist/lib/telemetry/attributes.d.ts +15 -0
- package/dist/lib/telemetry/attributes.js +16 -0
- package/dist/lib/telemetry/tracers.d.ts +1 -0
- package/dist/lib/telemetry/tracers.js +1 -0
- package/dist/lib/types/autoresearchTypes.d.ts +194 -0
- package/dist/lib/types/autoresearchTypes.js +18 -0
- package/dist/lib/types/common.d.ts +11 -0
- package/dist/lib/types/index.d.ts +16 -14
- package/dist/lib/types/index.js +21 -17
- package/dist/lib/types/taskTypes.d.ts +38 -0
- package/dist/lib/workflow/config.d.ts +3 -3
- package/dist/neurolink.js +22 -2
- package/dist/providers/litellm.js +2 -2
- package/dist/providers/openRouter.js +2 -2
- package/dist/providers/openaiCompatible.js +3 -1
- package/dist/rag/errors/RAGError.d.ts +1 -1
- package/dist/tasks/autoresearchTaskExecutor.d.ts +32 -0
- package/dist/tasks/autoresearchTaskExecutor.js +302 -0
- package/dist/tasks/errors.d.ts +3 -1
- package/dist/tasks/errors.js +1 -0
- package/dist/tasks/taskExecutor.d.ts +4 -2
- package/dist/tasks/taskExecutor.js +8 -1
- package/dist/tasks/taskManager.js +27 -3
- package/dist/tasks/tools/taskTools.d.ts +1 -1
- package/dist/telemetry/attributes.d.ts +15 -0
- package/dist/telemetry/attributes.js +16 -0
- package/dist/telemetry/tracers.d.ts +1 -0
- package/dist/telemetry/tracers.js +1 -0
- package/dist/types/autoresearchTypes.d.ts +194 -0
- package/dist/types/autoresearchTypes.js +17 -0
- package/dist/types/common.d.ts +11 -0
- package/dist/types/index.d.ts +16 -14
- package/dist/types/index.js +21 -17
- package/dist/types/taskTypes.d.ts +38 -0
- package/package.json +1 -1
|
@@ -0,0 +1,418 @@
|
|
|
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 { execFileSync } from "node:child_process";
|
|
11
|
+
import { ATTR } from "../telemetry/attributes.js";
|
|
12
|
+
import { tracers } from "../telemetry/tracers.js";
|
|
13
|
+
import { withSpan } from "../telemetry/withSpan.js";
|
|
14
|
+
import { withTimeout } from "../utils/errorHandling.js";
|
|
15
|
+
import { logger } from "../utils/logger.js";
|
|
16
|
+
import { resolveConfig, validateConfig } from "./config.js";
|
|
17
|
+
import { AutoresearchError } from "./errors.js";
|
|
18
|
+
import { getPhaseToolPolicy } from "./phasePolicy.js";
|
|
19
|
+
import { PromptCompiler } from "./promptCompiler.js";
|
|
20
|
+
import { RepoPolicy } from "./repoPolicy.js";
|
|
21
|
+
import { ResultRecorder } from "./resultRecorder.js";
|
|
22
|
+
import { ExperimentRunner } from "./runner.js";
|
|
23
|
+
import { ResearchStateStore } from "./stateStore.js";
|
|
24
|
+
import { createResearchTools } from "./tools.js";
|
|
25
|
+
function isBetter(candidate, best, direction) {
|
|
26
|
+
return direction === "lower" ? candidate < best : candidate > best;
|
|
27
|
+
}
|
|
28
|
+
function decideOutcome(metric, crashed, timedOut, bestMetric, direction) {
|
|
29
|
+
if (timedOut) {
|
|
30
|
+
return "timeout";
|
|
31
|
+
}
|
|
32
|
+
if (crashed || metric === null) {
|
|
33
|
+
return "crash";
|
|
34
|
+
}
|
|
35
|
+
if (bestMetric === null) {
|
|
36
|
+
return "keep";
|
|
37
|
+
} // First run is baseline
|
|
38
|
+
return isBetter(metric, bestMetric, direction) ? "keep" : "discard";
|
|
39
|
+
}
|
|
40
|
+
export class ResearchWorker {
|
|
41
|
+
config;
|
|
42
|
+
stateStore;
|
|
43
|
+
repoPolicy;
|
|
44
|
+
runner;
|
|
45
|
+
recorder;
|
|
46
|
+
promptCompiler;
|
|
47
|
+
initialized = false;
|
|
48
|
+
/** Event emitter injected by NeuroLink/TaskManager for lifecycle events. */
|
|
49
|
+
emitter;
|
|
50
|
+
constructor(configInput) {
|
|
51
|
+
this.config = resolveConfig(configInput);
|
|
52
|
+
this.stateStore = new ResearchStateStore(this.config.repoPath, this.config.statePath);
|
|
53
|
+
this.repoPolicy = new RepoPolicy(this.config);
|
|
54
|
+
this.runner = new ExperimentRunner(this.config);
|
|
55
|
+
this.recorder = new ResultRecorder(this.config);
|
|
56
|
+
this.promptCompiler = new PromptCompiler(this.config);
|
|
57
|
+
}
|
|
58
|
+
// ── Emitter integration ──────────────────────────────────
|
|
59
|
+
/** Set the event emitter (called by NeuroLink/TaskManager during integration). */
|
|
60
|
+
setEmitter(emitter) {
|
|
61
|
+
this.emitter = emitter;
|
|
62
|
+
}
|
|
63
|
+
/** Emit a lifecycle event. Safe to call when no emitter is set. */
|
|
64
|
+
emit(event, ...args) {
|
|
65
|
+
this.emitter?.emit(event, ...args);
|
|
66
|
+
}
|
|
67
|
+
// ── Lifecycle ────────────────────────────────────────────
|
|
68
|
+
/** Initialize: validate config, ensure branch, create state */
|
|
69
|
+
async initialize(tag) {
|
|
70
|
+
return withSpan({
|
|
71
|
+
name: "autoresearch.initialize",
|
|
72
|
+
tracer: tracers.autoresearch,
|
|
73
|
+
attributes: {
|
|
74
|
+
[ATTR.AR_TAG]: tag,
|
|
75
|
+
[ATTR.AR_BRANCH]: `${this.config.branchPrefix}${tag}`,
|
|
76
|
+
},
|
|
77
|
+
}, async (span) => {
|
|
78
|
+
validateConfig(this.config);
|
|
79
|
+
const branch = `${this.config.branchPrefix}${tag}`;
|
|
80
|
+
// Create branch if it doesn't exist
|
|
81
|
+
try {
|
|
82
|
+
const currentBranch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
83
|
+
cwd: this.config.repoPath,
|
|
84
|
+
encoding: "utf-8",
|
|
85
|
+
}).trim();
|
|
86
|
+
if (currentBranch !== branch) {
|
|
87
|
+
try {
|
|
88
|
+
execFileSync("git", ["checkout", "-b", branch], {
|
|
89
|
+
cwd: this.config.repoPath,
|
|
90
|
+
stdio: "ignore",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Branch may already exist
|
|
95
|
+
execFileSync("git", ["checkout", branch], {
|
|
96
|
+
cwd: this.config.repoPath,
|
|
97
|
+
stdio: "ignore",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
this.emitError(tag, "BRANCH_ERROR", `Failed to setup branch ${branch}`);
|
|
104
|
+
throw AutoresearchError.create("BRANCH_ERROR", `Failed to setup branch ${branch}`, {
|
|
105
|
+
cause: error instanceof Error ? error : undefined,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Initialize state
|
|
109
|
+
const state = await this.stateStore.initialize(tag, branch);
|
|
110
|
+
// Ensure results file exists
|
|
111
|
+
await this.recorder.ensureResultsFile();
|
|
112
|
+
this.initialized = true;
|
|
113
|
+
span.setAttribute(ATTR.AR_PHASE, state.currentPhase);
|
|
114
|
+
logger.info("[Autoresearch] Worker initialized", { tag, branch });
|
|
115
|
+
this.emit("autoresearch:initialized", {
|
|
116
|
+
tag,
|
|
117
|
+
branch,
|
|
118
|
+
config: {
|
|
119
|
+
repoPath: this.config.repoPath,
|
|
120
|
+
runCommand: this.config.runCommand,
|
|
121
|
+
metric: this.config.metric,
|
|
122
|
+
timeoutMs: this.config.timeoutMs,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
return state;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Load existing state (for resuming) */
|
|
129
|
+
async resume() {
|
|
130
|
+
return withSpan({
|
|
131
|
+
name: "autoresearch.resume",
|
|
132
|
+
tracer: tracers.autoresearch,
|
|
133
|
+
}, async (span) => {
|
|
134
|
+
const state = await this.stateStore.load();
|
|
135
|
+
if (!state) {
|
|
136
|
+
throw AutoresearchError.create("STATE_NOT_FOUND", "No state file found. Run initialize() first.");
|
|
137
|
+
}
|
|
138
|
+
validateConfig(this.config);
|
|
139
|
+
// Ensure we're on the correct branch (may have changed after restart)
|
|
140
|
+
try {
|
|
141
|
+
const currentBranch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: this.config.repoPath, encoding: "utf-8" }).trim();
|
|
142
|
+
if (currentBranch !== state.branch) {
|
|
143
|
+
execFileSync("git", ["checkout", state.branch], {
|
|
144
|
+
cwd: this.config.repoPath,
|
|
145
|
+
stdio: "ignore",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (branchErr) {
|
|
150
|
+
logger.error("[Autoresearch] Failed to restore branch", {
|
|
151
|
+
expected: state.branch,
|
|
152
|
+
error: branchErr instanceof Error
|
|
153
|
+
? branchErr.message
|
|
154
|
+
: String(branchErr),
|
|
155
|
+
});
|
|
156
|
+
throw AutoresearchError.create("BRANCH_ERROR", `Failed to checkout branch ${state.branch} during resume`, { cause: branchErr instanceof Error ? branchErr : undefined });
|
|
157
|
+
}
|
|
158
|
+
this.initialized = true;
|
|
159
|
+
span.setAttribute(ATTR.AR_TAG, state.tag);
|
|
160
|
+
span.setAttribute(ATTR.AR_BRANCH, state.branch);
|
|
161
|
+
span.setAttribute(ATTR.AR_RUN_COUNT, state.runCount);
|
|
162
|
+
span.setAttribute(ATTR.AR_PHASE, state.currentPhase);
|
|
163
|
+
this.emit("autoresearch:resumed", {
|
|
164
|
+
tag: state.tag,
|
|
165
|
+
branch: state.branch,
|
|
166
|
+
runCount: state.runCount,
|
|
167
|
+
currentPhase: state.currentPhase,
|
|
168
|
+
});
|
|
169
|
+
return state;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/** Run one full experiment cycle without AI — just the deterministic parts */
|
|
173
|
+
async runExperimentCycle(description) {
|
|
174
|
+
if (!this.initialized) {
|
|
175
|
+
throw AutoresearchError.create("WORKER_NOT_INITIALIZED", "Call initialize() or resume() first");
|
|
176
|
+
}
|
|
177
|
+
const state = await this.stateStore.load();
|
|
178
|
+
if (!state) {
|
|
179
|
+
throw AutoresearchError.create("STATE_NOT_FOUND", "State file missing");
|
|
180
|
+
}
|
|
181
|
+
return withSpan({
|
|
182
|
+
name: "autoresearch.experiment_cycle",
|
|
183
|
+
tracer: tracers.autoresearch,
|
|
184
|
+
attributes: {
|
|
185
|
+
[ATTR.AR_TAG]: state.tag,
|
|
186
|
+
[ATTR.AR_RUN_COUNT]: state.runCount,
|
|
187
|
+
[ATTR.AR_DESCRIPTION]: description,
|
|
188
|
+
},
|
|
189
|
+
}, async (span) => {
|
|
190
|
+
const cycleStart = Date.now();
|
|
191
|
+
this.emit("autoresearch:experiment-started", {
|
|
192
|
+
tag: state.tag,
|
|
193
|
+
runCount: state.runCount,
|
|
194
|
+
description,
|
|
195
|
+
});
|
|
196
|
+
await this.advancePhase("run");
|
|
197
|
+
// Run the experiment
|
|
198
|
+
logger.info("[Autoresearch] Running experiment", {
|
|
199
|
+
runCount: state.runCount,
|
|
200
|
+
description,
|
|
201
|
+
});
|
|
202
|
+
const summary = await withSpan({
|
|
203
|
+
name: "autoresearch.experiment_run",
|
|
204
|
+
tracer: tracers.autoresearch,
|
|
205
|
+
attributes: { [ATTR.AR_TAG]: state.tag },
|
|
206
|
+
}, async () => {
|
|
207
|
+
return withTimeout(this.runner.run(), this.config.timeoutMs + 30_000, new Error("Experiment runner exceeded safety timeout"));
|
|
208
|
+
});
|
|
209
|
+
await this.advancePhase("evaluate");
|
|
210
|
+
// Deterministic decision
|
|
211
|
+
const status = decideOutcome(summary.metric, summary.crashed, summary.timedOut, state.bestMetric, this.config.metric.direction);
|
|
212
|
+
// Get commit hash
|
|
213
|
+
const commit = this.repoPolicy.getHeadCommit() || "unknown";
|
|
214
|
+
// Build record
|
|
215
|
+
const record = {
|
|
216
|
+
commit,
|
|
217
|
+
metric: summary.metric,
|
|
218
|
+
memoryGb: summary.memoryValue,
|
|
219
|
+
status,
|
|
220
|
+
description,
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
};
|
|
223
|
+
// Record result
|
|
224
|
+
await this.recorder.appendTsv(record);
|
|
225
|
+
await this.recorder.appendJsonl(record);
|
|
226
|
+
await this.advancePhase("accept_or_revert");
|
|
227
|
+
// Update state based on outcome
|
|
228
|
+
if (status === "keep") {
|
|
229
|
+
await this.stateStore.update({
|
|
230
|
+
acceptedCommit: commit,
|
|
231
|
+
bestMetric: summary.metric,
|
|
232
|
+
baselineMetric: state.baselineMetric ?? summary.metric,
|
|
233
|
+
keepCount: state.keepCount + 1,
|
|
234
|
+
runCount: state.runCount + 1,
|
|
235
|
+
lastStatus: status,
|
|
236
|
+
candidateCommit: null,
|
|
237
|
+
});
|
|
238
|
+
// Emit metric-improved if this beats a previous best
|
|
239
|
+
if (summary.metric !== null &&
|
|
240
|
+
state.bestMetric !== null &&
|
|
241
|
+
isBetter(summary.metric, state.bestMetric, this.config.metric.direction)) {
|
|
242
|
+
this.emit("autoresearch:metric-improved", {
|
|
243
|
+
tag: state.tag,
|
|
244
|
+
previousBest: state.bestMetric,
|
|
245
|
+
newBest: summary.metric,
|
|
246
|
+
commit,
|
|
247
|
+
direction: this.config.metric.direction,
|
|
248
|
+
runCount: state.runCount + 1,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
// Revert on discard/crash/timeout
|
|
254
|
+
if (state.acceptedCommit) {
|
|
255
|
+
this.emit("autoresearch:revert", {
|
|
256
|
+
tag: state.tag,
|
|
257
|
+
targetCommit: state.acceptedCommit,
|
|
258
|
+
reason: status,
|
|
259
|
+
runCount: state.runCount,
|
|
260
|
+
});
|
|
261
|
+
try {
|
|
262
|
+
execFileSync("git", ["reset", "--hard", state.acceptedCommit], {
|
|
263
|
+
cwd: this.config.repoPath,
|
|
264
|
+
stdio: "ignore",
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
269
|
+
logger.error("[Autoresearch] Revert failed — state NOT updated", {
|
|
270
|
+
error: errorMsg,
|
|
271
|
+
});
|
|
272
|
+
this.emit("autoresearch:revert-failed", {
|
|
273
|
+
tag: state.tag,
|
|
274
|
+
targetCommit: state.acceptedCommit,
|
|
275
|
+
error: errorMsg,
|
|
276
|
+
runCount: state.runCount,
|
|
277
|
+
});
|
|
278
|
+
this.emitError(state.tag, "REVERT_FAILED", `Failed to revert to ${state.acceptedCommit}. Manual intervention required.`, state.currentPhase, state.runCount);
|
|
279
|
+
// Do NOT advance state — repo is in unknown state
|
|
280
|
+
throw AutoresearchError.create("REVERT_FAILED", `Failed to revert to ${state.acceptedCommit}. Manual intervention required.`, {
|
|
281
|
+
cause: error instanceof Error ? error : undefined,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Only reach here if revert succeeded (or no acceptedCommit to revert to)
|
|
286
|
+
// currentPhase advancement happens unconditionally below via advancePhase("propose")
|
|
287
|
+
await this.stateStore.update({
|
|
288
|
+
runCount: state.runCount + 1,
|
|
289
|
+
lastStatus: status,
|
|
290
|
+
candidateCommit: null,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const durationMs = Date.now() - cycleStart;
|
|
294
|
+
// Set span attributes for the completed cycle
|
|
295
|
+
span.setAttribute(ATTR.AR_STATUS, status);
|
|
296
|
+
if (summary.metric !== null) {
|
|
297
|
+
span.setAttribute(ATTR.AR_METRIC, summary.metric);
|
|
298
|
+
}
|
|
299
|
+
span.setAttribute(ATTR.AR_COMMIT, commit);
|
|
300
|
+
span.setAttribute(ATTR.AR_DURATION_MS, durationMs);
|
|
301
|
+
logger.info("[Autoresearch] Experiment complete", {
|
|
302
|
+
status,
|
|
303
|
+
metric: summary.metric,
|
|
304
|
+
runCount: state.runCount + 1,
|
|
305
|
+
});
|
|
306
|
+
this.emit("autoresearch:experiment-completed", {
|
|
307
|
+
tag: state.tag,
|
|
308
|
+
runCount: state.runCount + 1,
|
|
309
|
+
status,
|
|
310
|
+
metric: summary.metric,
|
|
311
|
+
commit,
|
|
312
|
+
description,
|
|
313
|
+
durationMs,
|
|
314
|
+
});
|
|
315
|
+
// Emit state-updated with final state snapshot
|
|
316
|
+
const updatedState = await this.stateStore.load();
|
|
317
|
+
if (updatedState) {
|
|
318
|
+
this.emit("autoresearch:state-updated", {
|
|
319
|
+
tag: updatedState.tag,
|
|
320
|
+
phase: updatedState.currentPhase,
|
|
321
|
+
runCount: updatedState.runCount,
|
|
322
|
+
keepCount: updatedState.keepCount,
|
|
323
|
+
bestMetric: updatedState.bestMetric,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
await this.advancePhase("propose");
|
|
327
|
+
return record;
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** Get the tools record for use with NeuroLink.generate() */
|
|
331
|
+
getTools() {
|
|
332
|
+
return createResearchTools({
|
|
333
|
+
config: this.config,
|
|
334
|
+
stateStore: this.stateStore,
|
|
335
|
+
repoPolicy: this.repoPolicy,
|
|
336
|
+
runner: this.runner,
|
|
337
|
+
recorder: this.recorder,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/** Build system prompt */
|
|
341
|
+
async getSystemPrompt() {
|
|
342
|
+
return this.promptCompiler.buildSystemPrompt();
|
|
343
|
+
}
|
|
344
|
+
/** Build cycle prompt */
|
|
345
|
+
async getCyclePrompt() {
|
|
346
|
+
const state = await this.stateStore.load();
|
|
347
|
+
if (!state) {
|
|
348
|
+
throw AutoresearchError.create("STATE_NOT_FOUND", "No state");
|
|
349
|
+
}
|
|
350
|
+
const results = await this.recorder.readAll();
|
|
351
|
+
return this.promptCompiler.buildCyclePrompt(state, results);
|
|
352
|
+
}
|
|
353
|
+
/** Get current state */
|
|
354
|
+
async getState() {
|
|
355
|
+
return this.stateStore.load();
|
|
356
|
+
}
|
|
357
|
+
/** Get results stats */
|
|
358
|
+
async getStats() {
|
|
359
|
+
return this.recorder.getStats();
|
|
360
|
+
}
|
|
361
|
+
/** Get config */
|
|
362
|
+
getConfig() {
|
|
363
|
+
return this.config;
|
|
364
|
+
}
|
|
365
|
+
// ── Phase management (Phase 1b/1c) ──────────────────────
|
|
366
|
+
/**
|
|
367
|
+
* Single authority for phase transitions.
|
|
368
|
+
* Persists the new phase to the state store and emits phase-changed event.
|
|
369
|
+
*/
|
|
370
|
+
async advancePhase(phase) {
|
|
371
|
+
const currentState = await this.stateStore.load();
|
|
372
|
+
const fromPhase = currentState?.currentPhase ?? "bootstrap";
|
|
373
|
+
await this.stateStore.update({ currentPhase: phase });
|
|
374
|
+
logger.debug("[Autoresearch] Phase advanced", { phase });
|
|
375
|
+
if (fromPhase !== phase) {
|
|
376
|
+
this.emit("autoresearch:phase-changed", {
|
|
377
|
+
from: fromPhase,
|
|
378
|
+
to: phase,
|
|
379
|
+
runCount: currentState?.runCount ?? 0,
|
|
380
|
+
tag: currentState?.tag ?? "",
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Returns the phase tool policy for the current phase.
|
|
386
|
+
* Reads the phase from persisted state.
|
|
387
|
+
*/
|
|
388
|
+
async getPhaseToolPolicy() {
|
|
389
|
+
const state = await this.stateStore.load();
|
|
390
|
+
if (!state) {
|
|
391
|
+
throw AutoresearchError.create("STATE_NOT_FOUND", "No state for getPhaseToolPolicy");
|
|
392
|
+
}
|
|
393
|
+
return getPhaseToolPolicy(state.currentPhase);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Returns a tool filter object for the current phase, compatible
|
|
397
|
+
* with NeuroLink generate()'s toolFilter option.
|
|
398
|
+
*
|
|
399
|
+
* Returns { include: string[] } listing only the tools allowed
|
|
400
|
+
* in the current phase.
|
|
401
|
+
*/
|
|
402
|
+
async getToolFilterForCurrentPhase() {
|
|
403
|
+
const policy = await this.getPhaseToolPolicy();
|
|
404
|
+
return { include: [...policy.activeTools] };
|
|
405
|
+
}
|
|
406
|
+
// ── Private helpers ──────────────────────────────────────
|
|
407
|
+
/** Emit an autoresearch:error event. */
|
|
408
|
+
emitError(tag, code, message, phase, runCount) {
|
|
409
|
+
this.emit("autoresearch:error", {
|
|
410
|
+
tag,
|
|
411
|
+
error: message,
|
|
412
|
+
code,
|
|
413
|
+
phase,
|
|
414
|
+
runCount,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -776,6 +776,24 @@ export class BaseProvider {
|
|
|
776
776
|
},
|
|
777
777
|
responseTime: 0, // BaseProvider doesn't track response time directly
|
|
778
778
|
toolsUsed: result.toolsUsed || [],
|
|
779
|
+
// Map toolExecutions from EnhancedGenerateResult shape to TextGenerationResult shape
|
|
780
|
+
// Preserve original timing/status fields when present, fall back to safe defaults
|
|
781
|
+
toolExecutions: result.toolExecutions?.map((te) => {
|
|
782
|
+
const t = te;
|
|
783
|
+
return {
|
|
784
|
+
// Spread original fields first so normalized fields take precedence
|
|
785
|
+
...te,
|
|
786
|
+
toolName: te.name,
|
|
787
|
+
executionTime: typeof t.executionTime === "number"
|
|
788
|
+
? t.executionTime
|
|
789
|
+
: typeof t.duration === "number"
|
|
790
|
+
? t.duration
|
|
791
|
+
: 0,
|
|
792
|
+
success: typeof t.success === "boolean"
|
|
793
|
+
? t.success
|
|
794
|
+
: t.status === undefined || t.status === "success",
|
|
795
|
+
};
|
|
796
|
+
}),
|
|
779
797
|
enhancedWithTools: !!(result.toolsUsed && result.toolsUsed.length > 0),
|
|
780
798
|
analytics: result.analytics,
|
|
781
799
|
evaluation: result.evaluation,
|
|
@@ -69,7 +69,7 @@ export declare const evaluationErrors: {
|
|
|
69
69
|
/** Rate limit hit during evaluation */
|
|
70
70
|
readonly RATE_LIMIT_ERROR: "RATE_LIMIT_ERROR";
|
|
71
71
|
};
|
|
72
|
-
create: (code: "
|
|
72
|
+
create: (code: "CONFIGURATION_ERROR" | "PROVIDER_ERROR" | "EVALUATION_FAILED" | "PARSE_ERROR" | "STRATEGY_NOT_FOUND" | "CUSTOM_EVALUATOR_ERROR" | "BATCH_EVALUATION_ERROR" | "AGGREGATION_ERROR" | "REGISTRY_ERROR" | "MAX_RETRIES_EXCEEDED" | "TIMEOUT_ERROR" | "RATE_LIMIT_ERROR", message: string, options?: {
|
|
73
73
|
retryable?: boolean;
|
|
74
74
|
details?: Record<string, unknown>;
|
|
75
75
|
cause?: Error;
|
|
@@ -173,7 +173,7 @@ export declare function createFileTools(registry: FileReferenceRegistry): {
|
|
|
173
173
|
} | undefined;
|
|
174
174
|
columns?: string[] | undefined;
|
|
175
175
|
entry_path?: string | undefined;
|
|
176
|
-
format?: "
|
|
176
|
+
format?: "text" | "detailed" | "summary" | undefined;
|
|
177
177
|
}, {
|
|
178
178
|
success: false;
|
|
179
179
|
error: string | undefined;
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -28,9 +28,9 @@ import { getContextOverflowProvider, isContextOverflowError, parseProviderOverfl
|
|
|
28
28
|
import { ContextBudgetExceededError } from "./context/errors.js";
|
|
29
29
|
import { repairToolPairs } from "./context/toolPairRepair.js";
|
|
30
30
|
import { SYSTEM_LIMITS } from "./core/constants.js";
|
|
31
|
-
import { createToolEventPayload } from "./core/toolEvents.js";
|
|
32
31
|
import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
|
|
33
32
|
import { AIProviderFactory } from "./core/factory.js";
|
|
33
|
+
import { createToolEventPayload } from "./core/toolEvents.js";
|
|
34
34
|
import { ProviderRegistry } from "./factories/providerRegistry.js";
|
|
35
35
|
import { FileReferenceRegistry } from "./files/fileReferenceRegistry.js";
|
|
36
36
|
import { createFileTools } from "./files/fileTools.js";
|
|
@@ -2705,6 +2705,7 @@ Current user's request: ${currentInput}`;
|
|
|
2705
2705
|
region: options.region,
|
|
2706
2706
|
tts: options.tts,
|
|
2707
2707
|
fileRegistry: this.fileRegistry,
|
|
2708
|
+
timeout: options.timeout,
|
|
2708
2709
|
abortSignal: options.abortSignal,
|
|
2709
2710
|
skipToolPromptInjection: options.skipToolPromptInjection,
|
|
2710
2711
|
middleware: options.middleware,
|
|
@@ -4108,7 +4109,26 @@ Current user's request: ${currentInput}`;
|
|
|
4108
4109
|
responseTime,
|
|
4109
4110
|
finishReason: result.finishReason,
|
|
4110
4111
|
toolsUsed: result.toolsUsed || [],
|
|
4111
|
-
|
|
4112
|
+
// Map toolExecutions from EnhancedGenerateResult shape ({name,input,output})
|
|
4113
|
+
// to TextGenerationResult shape ({toolName,executionTime,success}).
|
|
4114
|
+
// Preserve original timing/status when present, fall back to safe defaults.
|
|
4115
|
+
toolExecutions: result.toolExecutions?.map((te) => {
|
|
4116
|
+
const t = te;
|
|
4117
|
+
return {
|
|
4118
|
+
// Spread original fields first so normalized fields take precedence
|
|
4119
|
+
...te,
|
|
4120
|
+
toolName: te.name,
|
|
4121
|
+
executionTime: typeof t.executionTime === "number"
|
|
4122
|
+
? t.executionTime
|
|
4123
|
+
: typeof t.duration === "number"
|
|
4124
|
+
? t.duration
|
|
4125
|
+
: 0,
|
|
4126
|
+
success: typeof t.success === "boolean"
|
|
4127
|
+
? t.success
|
|
4128
|
+
: t.status === "success",
|
|
4129
|
+
};
|
|
4130
|
+
}),
|
|
4131
|
+
enhancedWithTools: !!result.toolExecutions?.length,
|
|
4112
4132
|
analytics: result.analytics,
|
|
4113
4133
|
evaluation: result.evaluation,
|
|
4114
4134
|
audio: result.audio,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
2
2
|
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
3
|
-
import { NoOutputGeneratedError, Output, streamText, } from "ai";
|
|
3
|
+
import { NoOutputGeneratedError, Output, stepCountIs, streamText, } from "ai";
|
|
4
4
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
5
5
|
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
|
|
6
6
|
import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
|
|
@@ -165,7 +165,7 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
165
165
|
Object.keys(tools).length > 0 && {
|
|
166
166
|
tools,
|
|
167
167
|
toolChoice: resolveToolChoice(options, tools, shouldUseTools),
|
|
168
|
-
|
|
168
|
+
stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
|
|
169
169
|
}),
|
|
170
170
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
171
171
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
2
|
-
import { NoOutputGeneratedError, Output, streamText, } from "ai";
|
|
2
|
+
import { NoOutputGeneratedError, Output, stepCountIs, streamText, } from "ai";
|
|
3
3
|
import { AIProviderName } from "../constants/enums.js";
|
|
4
4
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
5
5
|
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
|
|
@@ -248,7 +248,7 @@ export class OpenRouterProvider extends BaseProvider {
|
|
|
248
248
|
Object.keys(tools).length > 0 && {
|
|
249
249
|
tools,
|
|
250
250
|
toolChoice: resolveToolChoice(options, tools, shouldUseTools),
|
|
251
|
-
|
|
251
|
+
stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
|
|
252
252
|
}),
|
|
253
253
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
254
254
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
2
|
-
import { NoOutputGeneratedError, streamText, } from "ai";
|
|
2
|
+
import { NoOutputGeneratedError, stepCountIs, streamText, } from "ai";
|
|
3
3
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
4
|
+
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
|
|
4
5
|
import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
|
|
5
6
|
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
6
7
|
import { logger } from "../utils/logger.js";
|
|
@@ -191,6 +192,7 @@ export class OpenAICompatibleProvider extends BaseProvider {
|
|
|
191
192
|
: {}),
|
|
192
193
|
tools,
|
|
193
194
|
toolChoice: resolveToolChoice(options, tools, shouldUseTools),
|
|
195
|
+
stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
|
|
194
196
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
195
197
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
196
198
|
onStepFinish: (event) => {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autoresearch task executor — bridges TaskManager with the
|
|
3
|
+
* autoresearch experiment loop.
|
|
4
|
+
*
|
|
5
|
+
* Each tick:
|
|
6
|
+
* 1. Loads/creates a ResearchWorker for the task's tag
|
|
7
|
+
* 2. Gets the phase-appropriate tool filter
|
|
8
|
+
* 3. Calls NeuroLink.generate() with research tools + prompt
|
|
9
|
+
* 4. Advances phase based on which tools the AI called
|
|
10
|
+
* 5. Returns a TaskRunResult
|
|
11
|
+
*
|
|
12
|
+
* Workers are cached by tag to avoid re-initialization on each tick.
|
|
13
|
+
* Forwards the NeuroLink emitter to each worker for lifecycle events.
|
|
14
|
+
*/
|
|
15
|
+
import type { AutoresearchEmitter } from "../types/autoresearchTypes.js";
|
|
16
|
+
import type { NeuroLinkExecutable, Task, TaskRunResult } from "../types/taskTypes.js";
|
|
17
|
+
/**
|
|
18
|
+
* Clear all cached workers. Called by TaskManager.shutdown().
|
|
19
|
+
*/
|
|
20
|
+
export declare function clearWorkerCache(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Execute one autoresearch tick for a task.
|
|
23
|
+
*
|
|
24
|
+
* Returns a TaskRunResult-shaped object.
|
|
25
|
+
* If the task is missing autoresearch config, returns an error result
|
|
26
|
+
* instead of throwing (so the scheduler can record the failure).
|
|
27
|
+
*
|
|
28
|
+
* @param emitter - Optional emitter to forward autoresearch lifecycle events
|
|
29
|
+
*/
|
|
30
|
+
export declare function executeAutoresearchTick(task: Task & {
|
|
31
|
+
autoresearch?: unknown;
|
|
32
|
+
}, neurolink: NeuroLinkExecutable, emitter?: AutoresearchEmitter): Promise<TaskRunResult>;
|