@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.
- package/CHANGELOG.md +12 -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 -324
- 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/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/parser.js +4 -1
- package/dist/core/baseProvider.js +18 -0
- package/dist/core/factory.d.ts +2 -2
- package/dist/core/factory.js +4 -4
- package/dist/evaluation/errors/EvaluationError.d.ts +1 -1
- package/dist/factories/providerFactory.d.ts +4 -4
- package/dist/factories/providerFactory.js +20 -7
- package/dist/factories/providerRegistry.d.ts +5 -0
- package/dist/factories/providerRegistry.js +45 -26
- 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/core/factory.d.ts +2 -2
- package/dist/lib/core/factory.js +4 -4
- package/dist/lib/evaluation/errors/EvaluationError.d.ts +1 -1
- package/dist/lib/factories/providerFactory.d.ts +4 -4
- package/dist/lib/factories/providerFactory.js +20 -7
- package/dist/lib/factories/providerRegistry.d.ts +5 -0
- package/dist/lib/factories/providerRegistry.js +45 -26
- package/dist/lib/files/fileTools.d.ts +1 -1
- package/dist/lib/neurolink.d.ts +21 -0
- package/dist/lib/neurolink.js +91 -8
- package/dist/lib/providers/amazonBedrock.d.ts +6 -1
- package/dist/lib/providers/amazonBedrock.js +14 -2
- package/dist/lib/providers/amazonSagemaker.d.ts +7 -1
- package/dist/lib/providers/amazonSagemaker.js +21 -3
- package/dist/lib/providers/anthropic.d.ts +4 -1
- package/dist/lib/providers/anthropic.js +18 -5
- package/dist/lib/providers/azureOpenai.d.ts +2 -1
- package/dist/lib/providers/azureOpenai.js +10 -5
- package/dist/lib/providers/googleAiStudio.d.ts +4 -1
- package/dist/lib/providers/googleAiStudio.js +6 -7
- package/dist/lib/providers/googleVertex.d.ts +3 -1
- package/dist/lib/providers/googleVertex.js +96 -17
- package/dist/lib/providers/huggingFace.d.ts +2 -1
- package/dist/lib/providers/huggingFace.js +4 -4
- package/dist/lib/providers/litellm.d.ts +5 -1
- package/dist/lib/providers/litellm.js +16 -11
- package/dist/lib/providers/mistral.d.ts +2 -1
- package/dist/lib/providers/mistral.js +2 -2
- package/dist/lib/providers/ollama.d.ts +3 -1
- package/dist/lib/providers/ollama.js +2 -2
- package/dist/lib/providers/openAI.d.ts +5 -1
- package/dist/lib/providers/openAI.js +15 -5
- package/dist/lib/providers/openRouter.d.ts +5 -1
- package/dist/lib/providers/openRouter.js +19 -7
- package/dist/lib/providers/openaiCompatible.d.ts +4 -1
- package/dist/lib/providers/openaiCompatible.js +18 -4
- 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/configTypes.d.ts +7 -0
- package/dist/lib/types/generateTypes.d.ts +13 -0
- package/dist/lib/types/index.d.ts +16 -14
- package/dist/lib/types/index.js +21 -17
- package/dist/lib/types/providers.d.ts +75 -0
- package/dist/lib/types/streamTypes.d.ts +7 -1
- package/dist/lib/types/taskTypes.d.ts +38 -0
- package/dist/lib/workflow/config.d.ts +3 -3
- package/dist/neurolink.d.ts +21 -0
- package/dist/neurolink.js +91 -8
- package/dist/providers/amazonBedrock.d.ts +6 -1
- package/dist/providers/amazonBedrock.js +14 -2
- package/dist/providers/amazonSagemaker.d.ts +7 -1
- package/dist/providers/amazonSagemaker.js +21 -3
- package/dist/providers/anthropic.d.ts +4 -1
- package/dist/providers/anthropic.js +18 -5
- package/dist/providers/azureOpenai.d.ts +2 -1
- package/dist/providers/azureOpenai.js +10 -5
- package/dist/providers/googleAiStudio.d.ts +4 -1
- package/dist/providers/googleAiStudio.js +6 -7
- package/dist/providers/googleVertex.d.ts +3 -1
- package/dist/providers/googleVertex.js +96 -17
- package/dist/providers/huggingFace.d.ts +2 -1
- package/dist/providers/huggingFace.js +4 -4
- package/dist/providers/litellm.d.ts +5 -1
- package/dist/providers/litellm.js +16 -11
- package/dist/providers/mistral.d.ts +2 -1
- package/dist/providers/mistral.js +2 -2
- package/dist/providers/ollama.d.ts +3 -1
- package/dist/providers/ollama.js +2 -2
- package/dist/providers/openAI.d.ts +5 -1
- package/dist/providers/openAI.js +15 -5
- package/dist/providers/openRouter.d.ts +5 -1
- package/dist/providers/openRouter.js +19 -7
- package/dist/providers/openaiCompatible.d.ts +4 -1
- package/dist/providers/openaiCompatible.js +18 -4
- 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/configTypes.d.ts +7 -0
- package/dist/types/generateTypes.d.ts +13 -0
- package/dist/types/index.d.ts +16 -14
- package/dist/types/index.js +21 -17
- package/dist/types/providers.d.ts +75 -0
- package/dist/types/streamTypes.d.ts +7 -1
- package/dist/types/taskTypes.d.ts +38 -0
- package/package.json +3 -2
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-based tool access policy for the autoresearch experiment loop.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExperimentPhase, PhaseToolPolicy } from "../types/autoresearchTypes.js";
|
|
5
|
+
export declare function getPhaseToolPolicy(phase: ExperimentPhase): PhaseToolPolicy;
|
|
6
|
+
/**
|
|
7
|
+
* Returns all research tool names across all phases.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getAllResearchToolNames(): string[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-based tool access policy for the autoresearch experiment loop.
|
|
3
|
+
*/
|
|
4
|
+
const PHASE_POLICIES = {
|
|
5
|
+
bootstrap: {
|
|
6
|
+
activeTools: [
|
|
7
|
+
"research_get_context",
|
|
8
|
+
"research_read_file",
|
|
9
|
+
"research_checkpoint",
|
|
10
|
+
],
|
|
11
|
+
forcedTool: "research_get_context",
|
|
12
|
+
},
|
|
13
|
+
baseline: {
|
|
14
|
+
activeTools: [
|
|
15
|
+
"research_run_experiment",
|
|
16
|
+
"research_parse_log",
|
|
17
|
+
"research_record",
|
|
18
|
+
"research_accept",
|
|
19
|
+
"research_checkpoint",
|
|
20
|
+
],
|
|
21
|
+
forcedTool: "research_run_experiment",
|
|
22
|
+
},
|
|
23
|
+
propose: {
|
|
24
|
+
activeTools: ["research_get_context", "research_read_file"],
|
|
25
|
+
forcedTool: "research_get_context",
|
|
26
|
+
},
|
|
27
|
+
edit: {
|
|
28
|
+
activeTools: [
|
|
29
|
+
"research_read_file",
|
|
30
|
+
"research_write_candidate",
|
|
31
|
+
"research_diff",
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
commit: {
|
|
35
|
+
activeTools: ["research_commit_candidate"],
|
|
36
|
+
forcedTool: "research_commit_candidate",
|
|
37
|
+
},
|
|
38
|
+
run: {
|
|
39
|
+
activeTools: ["research_run_experiment"],
|
|
40
|
+
forcedTool: "research_run_experiment",
|
|
41
|
+
},
|
|
42
|
+
evaluate: {
|
|
43
|
+
activeTools: ["research_parse_log", "research_inspect_failure"],
|
|
44
|
+
forcedTool: "research_parse_log",
|
|
45
|
+
},
|
|
46
|
+
record: {
|
|
47
|
+
activeTools: ["research_record", "research_checkpoint"],
|
|
48
|
+
forcedTool: "research_record",
|
|
49
|
+
},
|
|
50
|
+
accept_or_revert: {
|
|
51
|
+
activeTools: ["research_accept", "research_revert", "research_checkpoint"],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
export function getPhaseToolPolicy(phase) {
|
|
55
|
+
const p = PHASE_POLICIES[phase];
|
|
56
|
+
return { activeTools: [...p.activeTools], forcedTool: p.forcedTool };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Returns all research tool names across all phases.
|
|
60
|
+
*/
|
|
61
|
+
export function getAllResearchToolNames() {
|
|
62
|
+
const names = new Set();
|
|
63
|
+
for (const policy of Object.values(PHASE_POLICIES)) {
|
|
64
|
+
for (const tool of policy.activeTools) {
|
|
65
|
+
names.add(tool);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...names];
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=phasePolicy.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt compilation for the autoresearch worker.
|
|
3
|
+
*
|
|
4
|
+
* Reads program.md and combines it with current state and recent
|
|
5
|
+
* results to build the system prompt and per-cycle prompt.
|
|
6
|
+
*/
|
|
7
|
+
import type { ResearchConfig, ResearchState, ExperimentRecord } from "../types/autoresearchTypes.js";
|
|
8
|
+
export declare class PromptCompiler {
|
|
9
|
+
private config;
|
|
10
|
+
constructor(config: ResearchConfig);
|
|
11
|
+
/** Reads program.md and builds the system prompt */
|
|
12
|
+
buildSystemPrompt(): Promise<string>;
|
|
13
|
+
/** Builds the per-cycle prompt with current state + recent results */
|
|
14
|
+
buildCyclePrompt(state: ResearchState, recentResults: ExperimentRecord[]): Promise<string>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt compilation for the autoresearch worker.
|
|
3
|
+
*
|
|
4
|
+
* Reads program.md and combines it with current state and recent
|
|
5
|
+
* results to build the system prompt and per-cycle prompt.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, statSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { logger } from "../utils/logger.js";
|
|
10
|
+
import { getPhaseToolPolicy } from "./phasePolicy.js";
|
|
11
|
+
export class PromptCompiler {
|
|
12
|
+
config;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
}
|
|
16
|
+
/** Reads program.md and builds the system prompt */
|
|
17
|
+
async buildSystemPrompt() {
|
|
18
|
+
const MAX_PROGRAM_BYTES = 1_048_576; // 1 MB
|
|
19
|
+
let programContent = "";
|
|
20
|
+
try {
|
|
21
|
+
const programPath = path.join(this.config.repoPath, this.config.programPath);
|
|
22
|
+
const stat = statSync(programPath);
|
|
23
|
+
if (stat.size > MAX_PROGRAM_BYTES) {
|
|
24
|
+
logger.warn("[Autoresearch] program.md exceeds 1MB, truncating");
|
|
25
|
+
programContent = readFileSync(programPath, "utf-8").slice(0, MAX_PROGRAM_BYTES);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
programContent = readFileSync(programPath, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
logger.warn("[Autoresearch] program.md not found, using minimal system prompt");
|
|
33
|
+
}
|
|
34
|
+
const toolList = [
|
|
35
|
+
"research_get_context — Get current research state (branch, metric, recent results)",
|
|
36
|
+
"research_read_file — Read a file from the repo (must be in allowed paths)",
|
|
37
|
+
"research_write_candidate — Write to a mutable file (only allowed paths)",
|
|
38
|
+
"research_diff — Show git diff for mutable files",
|
|
39
|
+
"research_commit_candidate — Commit staged mutable files",
|
|
40
|
+
"research_run_experiment — Run the experiment command with timeout",
|
|
41
|
+
"research_parse_log — Parse the experiment log for metrics",
|
|
42
|
+
"research_record — Record experiment result (keep/discard/crash/timeout)",
|
|
43
|
+
"research_accept — Accept candidate (update best metric, advance branch)",
|
|
44
|
+
"research_revert — Revert to last accepted commit",
|
|
45
|
+
"research_inspect_failure — Read last 50 lines of run.log for crash diagnosis",
|
|
46
|
+
"research_checkpoint — Save current state to disk",
|
|
47
|
+
];
|
|
48
|
+
return [
|
|
49
|
+
"You are an autonomous research agent running experiments in a loop.",
|
|
50
|
+
"",
|
|
51
|
+
programContent ? "## Research Program" : "",
|
|
52
|
+
programContent,
|
|
53
|
+
"",
|
|
54
|
+
"## Available Tools",
|
|
55
|
+
...toolList.map((t) => `- ${t}`),
|
|
56
|
+
"",
|
|
57
|
+
"## Constraints",
|
|
58
|
+
`- You may ONLY modify these files: ${this.config.mutablePaths.join(", ")}`,
|
|
59
|
+
`- You must NEVER modify: ${this.config.immutablePaths.join(", ")}`,
|
|
60
|
+
`- The primary metric is: ${this.config.metric.name} (${this.config.metric.direction} is better)`,
|
|
61
|
+
`- Experiment timeout: ${Math.round(this.config.timeoutMs / 1000)} seconds`,
|
|
62
|
+
"",
|
|
63
|
+
"## Workflow",
|
|
64
|
+
"1. Call research_get_context to understand current state",
|
|
65
|
+
"2. Read relevant files to understand the code",
|
|
66
|
+
"3. Propose and implement a single experiment change",
|
|
67
|
+
"4. Commit the change",
|
|
68
|
+
"5. Run the experiment",
|
|
69
|
+
"6. Parse the results",
|
|
70
|
+
"7. Record the outcome",
|
|
71
|
+
"8. Accept (if improved) or revert (if not)",
|
|
72
|
+
"",
|
|
73
|
+
"NEVER STOP. Continue proposing experiments indefinitely.",
|
|
74
|
+
"Prefer simplicity — deleting code is better than adding complexity.",
|
|
75
|
+
]
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.join("\n");
|
|
78
|
+
}
|
|
79
|
+
/** Builds the per-cycle prompt with current state + recent results */
|
|
80
|
+
async buildCyclePrompt(state, recentResults) {
|
|
81
|
+
const parts = [
|
|
82
|
+
`## Current Research State`,
|
|
83
|
+
`- Branch: ${state.branch}`,
|
|
84
|
+
`- Run count: ${state.runCount}`,
|
|
85
|
+
`- Keep count: ${state.keepCount}`,
|
|
86
|
+
`- Best ${this.config.metric.name}: ${state.bestMetric ?? "no baseline yet"}`,
|
|
87
|
+
`- Last status: ${state.lastStatus ?? "none"}`,
|
|
88
|
+
`- Current phase: ${state.currentPhase}`,
|
|
89
|
+
`- Accepted commit: ${state.acceptedCommit ?? "none"}`,
|
|
90
|
+
];
|
|
91
|
+
if (recentResults.length > 0) {
|
|
92
|
+
parts.push("");
|
|
93
|
+
parts.push("## Recent Results (last 10)");
|
|
94
|
+
parts.push(`commit\t${this.config.metric.name}\tstatus\tdescription`);
|
|
95
|
+
for (const r of recentResults.slice(-10)) {
|
|
96
|
+
const metricStr = r.metric !== null && Number.isFinite(r.metric)
|
|
97
|
+
? r.metric.toFixed(6)
|
|
98
|
+
: "N/A";
|
|
99
|
+
parts.push(`${r.commit}\t${metricStr}\t${r.status}\t${r.description}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
parts.push("");
|
|
103
|
+
// Phase-aware first action suggestion (avoid biasing accept_or_revert toward accept)
|
|
104
|
+
const phasePolicy = getPhaseToolPolicy(state.currentPhase);
|
|
105
|
+
let firstAction;
|
|
106
|
+
if (phasePolicy.forcedTool) {
|
|
107
|
+
firstAction = phasePolicy.forcedTool;
|
|
108
|
+
}
|
|
109
|
+
else if (state.currentPhase === "accept_or_revert") {
|
|
110
|
+
// Don't bias toward accept or revert — the deterministic policy decides.
|
|
111
|
+
// Use checkpoint (neutral) since accept/revert are both in activeTools.
|
|
112
|
+
firstAction = "research_checkpoint";
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
firstAction = phasePolicy.activeTools[0] || "research_get_context";
|
|
116
|
+
}
|
|
117
|
+
parts.push(`Continue the experiment loop. Start by calling ${firstAction}.`);
|
|
118
|
+
return parts.join("\n");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=promptCompiler.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository policy enforcement for autoresearch.
|
|
3
|
+
*
|
|
4
|
+
* Controls which files can be read/written and validates
|
|
5
|
+
* git operations against the research branch.
|
|
6
|
+
*/
|
|
7
|
+
import type { ResearchConfig } from "../types/autoresearchTypes.js";
|
|
8
|
+
export declare class RepoPolicy {
|
|
9
|
+
private config;
|
|
10
|
+
private resolvedMutablePaths;
|
|
11
|
+
private resolvedImmutablePaths;
|
|
12
|
+
constructor(config: ResearchConfig);
|
|
13
|
+
/** Returns true if resolved path is inside repoPath (handles prefix collision) */
|
|
14
|
+
private isInsideRepo;
|
|
15
|
+
/** Returns true if path is within mutablePaths and NOT in immutablePaths */
|
|
16
|
+
isWriteAllowed(filePath: string): boolean;
|
|
17
|
+
/** Returns true if path is in immutablePaths */
|
|
18
|
+
isProtected(filePath: string): boolean;
|
|
19
|
+
/** Returns true if path is readable (mutable, immutable, or program path) */
|
|
20
|
+
isReadAllowed(filePath: string): boolean;
|
|
21
|
+
/** Validates staged files are all in mutablePaths and on the right branch */
|
|
22
|
+
validateCommit(expectedBranch: string): Promise<{
|
|
23
|
+
valid: boolean;
|
|
24
|
+
violations: string[];
|
|
25
|
+
}>;
|
|
26
|
+
/** Returns list of staged file paths. Throws on git failure. */
|
|
27
|
+
getStagedFiles(): Promise<string[]>;
|
|
28
|
+
/** Returns current git branch */
|
|
29
|
+
getCurrentBranch(): string;
|
|
30
|
+
/** Returns short commit hash */
|
|
31
|
+
getHeadCommit(): string;
|
|
32
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository policy enforcement for autoresearch.
|
|
3
|
+
*
|
|
4
|
+
* Controls which files can be read/written and validates
|
|
5
|
+
* git operations against the research branch.
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { logger } from "../utils/logger.js";
|
|
10
|
+
export class RepoPolicy {
|
|
11
|
+
config;
|
|
12
|
+
resolvedMutablePaths;
|
|
13
|
+
resolvedImmutablePaths;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.resolvedMutablePaths = config.mutablePaths.map((p) => path.resolve(config.repoPath, p));
|
|
17
|
+
this.resolvedImmutablePaths = config.immutablePaths.map((p) => path.resolve(config.repoPath, p));
|
|
18
|
+
}
|
|
19
|
+
/** Returns true if resolved path is inside repoPath (handles prefix collision) */
|
|
20
|
+
isInsideRepo(resolved) {
|
|
21
|
+
const rel = path.relative(this.config.repoPath, resolved);
|
|
22
|
+
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
23
|
+
}
|
|
24
|
+
/** Returns true if path is within mutablePaths and NOT in immutablePaths */
|
|
25
|
+
isWriteAllowed(filePath) {
|
|
26
|
+
const resolved = path.resolve(this.config.repoPath, filePath);
|
|
27
|
+
// Must be inside repoPath
|
|
28
|
+
if (!this.isInsideRepo(resolved)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
// Immutable paths always deny, even if they're children of a mutable parent
|
|
32
|
+
if (this.isProtected(filePath)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return this.resolvedMutablePaths.some((mp) => resolved === mp || resolved.startsWith(mp + path.sep));
|
|
36
|
+
}
|
|
37
|
+
/** Returns true if path is in immutablePaths */
|
|
38
|
+
isProtected(filePath) {
|
|
39
|
+
const resolved = path.resolve(this.config.repoPath, filePath);
|
|
40
|
+
return this.resolvedImmutablePaths.some((ip) => resolved === ip || resolved.startsWith(ip + path.sep));
|
|
41
|
+
}
|
|
42
|
+
/** Returns true if path is readable (mutable, immutable, or program path) */
|
|
43
|
+
isReadAllowed(filePath) {
|
|
44
|
+
const resolved = path.resolve(this.config.repoPath, filePath);
|
|
45
|
+
if (!this.isInsideRepo(resolved)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const programResolved = path.resolve(this.config.repoPath, this.config.programPath);
|
|
49
|
+
return (this.isWriteAllowed(filePath) ||
|
|
50
|
+
this.isProtected(filePath) ||
|
|
51
|
+
resolved === programResolved);
|
|
52
|
+
}
|
|
53
|
+
/** Validates staged files are all in mutablePaths and on the right branch */
|
|
54
|
+
async validateCommit(expectedBranch) {
|
|
55
|
+
const violations = [];
|
|
56
|
+
const staged = await this.getStagedFiles();
|
|
57
|
+
for (const file of staged) {
|
|
58
|
+
const resolved = path.resolve(this.config.repoPath, file);
|
|
59
|
+
// Block results files
|
|
60
|
+
if (file === this.config.resultsPath) {
|
|
61
|
+
violations.push(`Results file staged: ${file}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Block state files
|
|
65
|
+
const stateDir = path.dirname(this.config.statePath);
|
|
66
|
+
if (file === this.config.statePath ||
|
|
67
|
+
(stateDir !== "." && file.startsWith(stateDir + path.sep))) {
|
|
68
|
+
violations.push(`State file staged: ${file}`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// Block immutable files (even if under a mutable parent)
|
|
72
|
+
if (this.resolvedImmutablePaths.some((ip) => resolved === ip || resolved.startsWith(ip + path.sep))) {
|
|
73
|
+
violations.push(`Immutable file staged: ${file}`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Block non-mutable files
|
|
77
|
+
if (!this.resolvedMutablePaths.some((mp) => resolved === mp || resolved.startsWith(mp + path.sep))) {
|
|
78
|
+
violations.push(`Non-mutable file staged: ${file}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Verify branch
|
|
82
|
+
const currentBranch = this.getCurrentBranch();
|
|
83
|
+
if (currentBranch !== expectedBranch) {
|
|
84
|
+
violations.push(`Wrong branch: expected ${expectedBranch}, got ${currentBranch}`);
|
|
85
|
+
}
|
|
86
|
+
return { valid: violations.length === 0, violations };
|
|
87
|
+
}
|
|
88
|
+
/** Returns list of staged file paths. Throws on git failure. */
|
|
89
|
+
async getStagedFiles() {
|
|
90
|
+
const output = execFileSync("git", ["diff", "--cached", "--name-only"], {
|
|
91
|
+
cwd: this.config.repoPath,
|
|
92
|
+
encoding: "utf-8",
|
|
93
|
+
});
|
|
94
|
+
return output.trim().split("\n").filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
/** Returns current git branch */
|
|
97
|
+
getCurrentBranch() {
|
|
98
|
+
try {
|
|
99
|
+
return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
100
|
+
cwd: this.config.repoPath,
|
|
101
|
+
encoding: "utf-8",
|
|
102
|
+
}).trim();
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
logger.warn("[Autoresearch] getCurrentBranch failed", {
|
|
106
|
+
repoPath: this.config.repoPath,
|
|
107
|
+
error: err instanceof Error ? err.message : String(err),
|
|
108
|
+
});
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Returns short commit hash */
|
|
113
|
+
getHeadCommit() {
|
|
114
|
+
try {
|
|
115
|
+
return execFileSync("git", ["rev-parse", "--short=7", "HEAD"], {
|
|
116
|
+
cwd: this.config.repoPath,
|
|
117
|
+
encoding: "utf-8",
|
|
118
|
+
}).trim();
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
logger.warn("[Autoresearch] getHeadCommit failed", {
|
|
122
|
+
repoPath: this.config.repoPath,
|
|
123
|
+
error: err instanceof Error ? err.message : String(err),
|
|
124
|
+
});
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=repoPolicy.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experiment result recording — TSV + optional JSONL.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExperimentRecord, ExperimentStats, ResearchConfig } from "../types/autoresearchTypes.js";
|
|
5
|
+
export declare class ResultRecorder {
|
|
6
|
+
private config;
|
|
7
|
+
private tsvPath;
|
|
8
|
+
private jsonlPath;
|
|
9
|
+
constructor(config: ResearchConfig);
|
|
10
|
+
/** Creates results.tsv with header if it doesn't exist */
|
|
11
|
+
ensureResultsFile(): Promise<void>;
|
|
12
|
+
/** Appends one TSV row to results.tsv */
|
|
13
|
+
appendTsv(record: ExperimentRecord): Promise<void>;
|
|
14
|
+
/** Appends one JSON line to runs.jsonl */
|
|
15
|
+
appendJsonl(record: ExperimentRecord): Promise<void>;
|
|
16
|
+
/** Reads all records from results.tsv */
|
|
17
|
+
readAll(): Promise<ExperimentRecord[]>;
|
|
18
|
+
/** Returns summary stats */
|
|
19
|
+
getStats(): Promise<ExperimentStats>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experiment result recording — TSV + optional JSONL.
|
|
3
|
+
*/
|
|
4
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
import { AutoresearchError } from "./errors.js";
|
|
8
|
+
export class ResultRecorder {
|
|
9
|
+
config;
|
|
10
|
+
tsvPath;
|
|
11
|
+
jsonlPath;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.tsvPath = path.join(config.repoPath, config.resultsPath);
|
|
15
|
+
this.jsonlPath = path.join(config.repoPath, ".autoresearch", "runs.jsonl");
|
|
16
|
+
}
|
|
17
|
+
/** Creates results.tsv with header if it doesn't exist */
|
|
18
|
+
async ensureResultsFile() {
|
|
19
|
+
if (!existsSync(this.tsvPath)) {
|
|
20
|
+
try {
|
|
21
|
+
// Ensure parent directory exists (handles custom resultsPath like "artifacts/results.tsv")
|
|
22
|
+
const dir = path.dirname(this.tsvPath);
|
|
23
|
+
if (!existsSync(dir)) {
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
// Use the actual metric name in header
|
|
27
|
+
const header = `commit\t${this.config.metric.name}\tmemory_gb\tstatus\tdescription`;
|
|
28
|
+
writeFileSync(this.tsvPath, header + "\n", "utf-8");
|
|
29
|
+
logger.info("[Autoresearch] Created results file", {
|
|
30
|
+
path: this.tsvPath,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw AutoresearchError.create("RESULTS_WRITE_FAILED", `Failed to create results file: ${this.tsvPath}`, {
|
|
35
|
+
cause: error instanceof Error ? error : undefined,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Appends one TSV row to results.tsv */
|
|
41
|
+
async appendTsv(record) {
|
|
42
|
+
await this.ensureResultsFile();
|
|
43
|
+
const metricStr = record.metric !== null ? record.metric.toFixed(6) : "N/A";
|
|
44
|
+
const memoryStr = record.memoryGb !== null ? record.memoryGb.toFixed(1) : "N/A";
|
|
45
|
+
const safeDescription = record.description.replace(/[\t\n\r]/g, " ").trim();
|
|
46
|
+
const line = `${record.commit}\t${metricStr}\t${memoryStr}\t${record.status}\t${safeDescription}`;
|
|
47
|
+
try {
|
|
48
|
+
appendFileSync(this.tsvPath, line + "\n", "utf-8");
|
|
49
|
+
logger.debug("[Autoresearch] Appended TSV record", {
|
|
50
|
+
commit: record.commit,
|
|
51
|
+
status: record.status,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
throw AutoresearchError.create("RESULTS_WRITE_FAILED", `Failed to append to results file`, {
|
|
56
|
+
cause: error instanceof Error ? error : undefined,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Appends one JSON line to runs.jsonl */
|
|
61
|
+
async appendJsonl(record) {
|
|
62
|
+
try {
|
|
63
|
+
const dir = path.dirname(this.jsonlPath);
|
|
64
|
+
if (!existsSync(dir)) {
|
|
65
|
+
mkdirSync(dir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
appendFileSync(this.jsonlPath, JSON.stringify(record) + "\n", "utf-8");
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
// JSONL is optional — log warning but don't throw
|
|
71
|
+
logger.warn("[Autoresearch] Failed to append JSONL audit entry", {
|
|
72
|
+
error: error instanceof Error ? error.message : String(error),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Reads all records from results.tsv */
|
|
77
|
+
async readAll() {
|
|
78
|
+
if (!existsSync(this.tsvPath)) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const content = readFileSync(this.tsvPath, "utf-8");
|
|
83
|
+
const lines = content.trim().split("\n");
|
|
84
|
+
if (lines.length <= 1) {
|
|
85
|
+
return [];
|
|
86
|
+
} // Header only
|
|
87
|
+
return lines.slice(1).map((line) => {
|
|
88
|
+
const [commit, metricStr, memoryStr, status, ...descParts] = line.split("\t");
|
|
89
|
+
return {
|
|
90
|
+
commit: commit || "",
|
|
91
|
+
metric: metricStr && metricStr !== "N/A" ? parseFloat(metricStr) : null,
|
|
92
|
+
memoryGb: memoryStr && memoryStr !== "N/A" ? parseFloat(memoryStr) : null,
|
|
93
|
+
status: (status || "crash"),
|
|
94
|
+
description: descParts.join("\t"),
|
|
95
|
+
timestamp: new Date().toISOString(), // Not stored in TSV, use current
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Returns summary stats */
|
|
104
|
+
async getStats() {
|
|
105
|
+
const records = await this.readAll();
|
|
106
|
+
const keeps = records.filter((r) => r.status === "keep");
|
|
107
|
+
const bestKeep = keeps.reduce((best, r) => {
|
|
108
|
+
if (r.metric === null) {
|
|
109
|
+
return best;
|
|
110
|
+
}
|
|
111
|
+
if (best === null || best.metric === null) {
|
|
112
|
+
return r;
|
|
113
|
+
}
|
|
114
|
+
if (this.config.metric.direction === "lower") {
|
|
115
|
+
return r.metric < best.metric ? r : best;
|
|
116
|
+
}
|
|
117
|
+
return r.metric > best.metric ? r : best;
|
|
118
|
+
}, null);
|
|
119
|
+
return {
|
|
120
|
+
total: records.length,
|
|
121
|
+
keepCount: keeps.length,
|
|
122
|
+
discardCount: records.filter((r) => r.status === "discard").length,
|
|
123
|
+
crashCount: records.filter((r) => r.status === "crash").length,
|
|
124
|
+
timeoutCount: records.filter((r) => r.status === "timeout").length,
|
|
125
|
+
keepRate: records.length > 0 ? keeps.length / records.length : 0,
|
|
126
|
+
bestMetric: bestKeep?.metric ?? null,
|
|
127
|
+
bestCommit: bestKeep?.commit ?? null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=resultRecorder.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experiment runner — spawn, timeout, capture.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExperimentSummary, ResearchConfig } from "../types/autoresearchTypes.js";
|
|
5
|
+
export declare class ExperimentRunner {
|
|
6
|
+
private config;
|
|
7
|
+
constructor(config: ResearchConfig);
|
|
8
|
+
/** Runs the experiment with hard timeout, returns summary */
|
|
9
|
+
run(): Promise<ExperimentSummary>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experiment runner — spawn, timeout, capture.
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { writeFileSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { logger } from "../utils/logger.js";
|
|
8
|
+
import { parseExperimentSummary } from "./summaryParser.js";
|
|
9
|
+
export class ExperimentRunner {
|
|
10
|
+
config;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
}
|
|
14
|
+
/** Runs the experiment with hard timeout, returns summary */
|
|
15
|
+
async run() {
|
|
16
|
+
const logPath = path.join(this.config.repoPath, this.config.logPath);
|
|
17
|
+
// Redact potential inline env vars or tokens before logging
|
|
18
|
+
const redactedCmd = this.config.runCommand
|
|
19
|
+
.replace(/[A-Z_]+=\S+\s/g, (m) => m.split("=")[0] + "=*** ")
|
|
20
|
+
.replace(/--(?:token|key|secret|password)\s+\S+/gi, (m) => m.split(/\s+/)[0] + " ***");
|
|
21
|
+
logger.info("[Autoresearch] Starting experiment", {
|
|
22
|
+
command: redactedCmd,
|
|
23
|
+
timeoutMs: this.config.timeoutMs,
|
|
24
|
+
});
|
|
25
|
+
// eslint-disable-next-line no-useless-assignment -- catch block assigns on spawn failure
|
|
26
|
+
let logContent = "";
|
|
27
|
+
let timedOut = false;
|
|
28
|
+
let exitCode = 0;
|
|
29
|
+
try {
|
|
30
|
+
logContent = await new Promise((resolve, reject) => {
|
|
31
|
+
let output = "";
|
|
32
|
+
const proc = spawn(this.config.runCommand, {
|
|
33
|
+
shell: true,
|
|
34
|
+
cwd: this.config.repoPath,
|
|
35
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
36
|
+
});
|
|
37
|
+
// Capture stdout and stderr
|
|
38
|
+
proc.stdout?.on("data", (chunk) => {
|
|
39
|
+
output += chunk.toString();
|
|
40
|
+
});
|
|
41
|
+
proc.stderr?.on("data", (chunk) => {
|
|
42
|
+
output += chunk.toString();
|
|
43
|
+
});
|
|
44
|
+
// Hard timeout
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
timedOut = true;
|
|
47
|
+
try {
|
|
48
|
+
proc.kill("SIGKILL");
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Process may have already exited
|
|
52
|
+
}
|
|
53
|
+
}, this.config.timeoutMs);
|
|
54
|
+
proc.on("close", (code, signal) => {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
// Signal-terminated (SIGKILL, SIGSEGV, etc.) = crash unless we timed it out
|
|
57
|
+
if (signal && !timedOut) {
|
|
58
|
+
exitCode = 1;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
exitCode = code ?? 0;
|
|
62
|
+
}
|
|
63
|
+
logger.debug("[Autoresearch] Experiment process exited", {
|
|
64
|
+
code,
|
|
65
|
+
signal,
|
|
66
|
+
exitCode,
|
|
67
|
+
timedOut,
|
|
68
|
+
});
|
|
69
|
+
resolve(output);
|
|
70
|
+
});
|
|
71
|
+
proc.on("error", (error) => {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
reject(error);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Spawn failure — treat as crash with non-zero exit code
|
|
79
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
80
|
+
logContent = `SPAWN ERROR: ${errorMsg}\nFAIL`;
|
|
81
|
+
exitCode = 1;
|
|
82
|
+
logger.error("[Autoresearch] Experiment spawn failed", {
|
|
83
|
+
error: errorMsg,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Write log to file
|
|
87
|
+
try {
|
|
88
|
+
writeFileSync(logPath, logContent, "utf-8");
|
|
89
|
+
}
|
|
90
|
+
catch (writeError) {
|
|
91
|
+
logger.warn("[Autoresearch] Failed to write run.log", {
|
|
92
|
+
error: writeError instanceof Error ? writeError.message : String(writeError),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Parse summary with exit code and timeout info
|
|
96
|
+
const summary = parseExperimentSummary(logContent, this.config.metric, this.config.memoryMetric, {
|
|
97
|
+
timedOut,
|
|
98
|
+
exitCode,
|
|
99
|
+
});
|
|
100
|
+
return summary;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Research state persistence — file-backed JSON store.
|
|
3
|
+
*/
|
|
4
|
+
import type { ResearchState } from "../types/autoresearchTypes.js";
|
|
5
|
+
export declare class ResearchStateStore {
|
|
6
|
+
private filePath;
|
|
7
|
+
constructor(repoPath: string, statePath: string);
|
|
8
|
+
load(): Promise<ResearchState | null>;
|
|
9
|
+
save(state: ResearchState): Promise<void>;
|
|
10
|
+
initialize(tag: string, branch: string): Promise<ResearchState>;
|
|
11
|
+
update(patch: Partial<ResearchState>): Promise<ResearchState>;
|
|
12
|
+
}
|