feat(FN-4219): complete Steps 5-7 — docs, changeset, and verification fixes
Fusion-Task-Id: FN-4219 Fusion-Task-Lineage: 8d9a9ba6-6729-4376-b935-549e28a7fa35
This commit is contained in:
5
.changeset/FN-4219-experiment-executor.md
Normal file
5
.changeset/FN-4219-experiment-executor.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add experiment executor runtime (init/run/log lifecycle, METRIC parser, async benchmark runner, keep/revert git policy) for upstream pi-autoresearch parity. Additive only; existing research subsystem unchanged.
|
||||||
74
docs/research/experiment-executor.md
Normal file
74
docs/research/experiment-executor.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Experiment Executor (`@fusion/engine`)
|
||||||
|
|
||||||
|
`ExperimentExecutor` provides engine-side parity with pi-autoresearch's init/run/log loop.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
- `initExperiment(input)`
|
||||||
|
- Creates a new active session and appends a `config` record.
|
||||||
|
- If an active/finalizing session with the same `name` + `projectId` exists, starts a new segment instead.
|
||||||
|
- `runExperiment(input, opts?)`
|
||||||
|
- Runs benchmark command asynchronously, parses `METRIC` lines, and returns a transient run result.
|
||||||
|
- Does not persist run records.
|
||||||
|
- `logExperiment(input)`
|
||||||
|
- Appends a persisted `run` record with selected outcome.
|
||||||
|
- `keep` commits git changes.
|
||||||
|
- `discard` / `checks_failed` / `errored` can revert to a baseline commit.
|
||||||
|
- `getStatus(sessionId)` returns current session status, runs in segment, active handles, and limits.
|
||||||
|
- `cancel(runHandle)` aborts an in-flight benchmark run.
|
||||||
|
|
||||||
|
## Store Composition (FN-4218)
|
||||||
|
|
||||||
|
Executor uses `ExperimentSessionStore` for:
|
||||||
|
- session creation/reuse (`createSession`, `startNewSegment`)
|
||||||
|
- record append (`appendRecord`)
|
||||||
|
- best/kept pointers (`setBestRun`, `recordKept`)
|
||||||
|
- run payload commit patching (`updateRecordPayload` additive method)
|
||||||
|
|
||||||
|
## METRIC Grammar
|
||||||
|
|
||||||
|
Parser accepts:
|
||||||
|
|
||||||
|
```regex
|
||||||
|
^METRIC\s+([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*(?:\(([^)]+)\))?\s*$
|
||||||
|
```
|
||||||
|
|
||||||
|
- first valid metric = primary
|
||||||
|
- later metrics = secondary
|
||||||
|
- dedup by metric name (last-write-wins)
|
||||||
|
- denylist: `__proto__`, `constructor`, `prototype`
|
||||||
|
- non-finite values are ignored with warnings
|
||||||
|
|
||||||
|
## Benchmark Execution Contract
|
||||||
|
|
||||||
|
`runBenchmark()` uses non-blocking child process execution (`spawn`, async path):
|
||||||
|
- default timeout: 10 minutes
|
||||||
|
- default max buffer: 10MB
|
||||||
|
- supports `AbortSignal`
|
||||||
|
- throttled progress callback (<= every 500ms)
|
||||||
|
- when stdout exceeds buffer, full output is written to temp file and returned stdout is truncated to last 64KB
|
||||||
|
|
||||||
|
## Keep/Revert Git Policy
|
||||||
|
|
||||||
|
Preserved artifacts on revert:
|
||||||
|
- `autoresearch.jsonl`
|
||||||
|
- `autoresearch.md`
|
||||||
|
- `autoresearch.ideas.md`
|
||||||
|
- `autoresearch.checks.sh`
|
||||||
|
- `autoresearch.config.json`
|
||||||
|
- `autoresearch.hooks/`
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- `keep`: stage all + commit (`experiment(<session>): keep <run>` message default)
|
||||||
|
- discard/check failures/errors: reset hard to baseline while preserving autoresearch artifacts via stash roundtrip
|
||||||
|
|
||||||
|
## Error Taxonomy
|
||||||
|
|
||||||
|
- `ExperimentMaxIterationsError` — run attempted after reaching max iterations.
|
||||||
|
- `ExperimentGitNotConfiguredError` — keep/revert path requested without configured `GitOps`.
|
||||||
|
- `ExperimentRevertConflictError` — stash-pop conflict while restoring preserved artifacts after revert.
|
||||||
|
|
||||||
|
## Follow-ups
|
||||||
|
|
||||||
|
- FN-4221: CLI/dashboard/pi-extension wiring.
|
||||||
|
- FN-4222: finalize workflow (branch/finalization orchestration).
|
||||||
@@ -81,6 +81,11 @@ Rules:
|
|||||||
| Baseline/current best pointers | `baselineRunId`, `bestRunId` |
|
| Baseline/current best pointers | `baselineRunId`, `bestRunId` |
|
||||||
| Finalization summary | `finalize` record payload + session status/finalizedAt |
|
| Finalization summary | `finalize` record payload + session status/finalizedAt |
|
||||||
|
|
||||||
|
## Store API Notes
|
||||||
|
|
||||||
|
- `recordKept(sessionId, runRecordId)` is idempotent and only appends missing run IDs.
|
||||||
|
- `updateRecordPayload(recordId, patch)` applies additive payload patches for existing records (used by executor to backfill run commit SHA after keep commits).
|
||||||
|
|
||||||
## Follow-ups
|
## Follow-ups
|
||||||
|
|
||||||
- **FN-4219**: executor/orchestrator loop (`init/run/log`) and runtime integration.
|
- **FN-4219**: executor/orchestrator loop (`init/run/log`) and runtime integration.
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ const baseSession: ExperimentSession = {
|
|||||||
currentSegment: 1,
|
currentSegment: 1,
|
||||||
maxIterations: 10,
|
maxIterations: 10,
|
||||||
tags: [],
|
tags: [],
|
||||||
bestRunId: null,
|
bestRunId: undefined,
|
||||||
baselineCommit: null,
|
|
||||||
keptRunIds: [],
|
keptRunIds: [],
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
@@ -33,8 +32,9 @@ const baseRunRecord: ExperimentSessionRecord = {
|
|||||||
id: "EXPR-001",
|
id: "EXPR-001",
|
||||||
sessionId: "EXP-001",
|
sessionId: "EXP-001",
|
||||||
segment: 1,
|
segment: 1,
|
||||||
|
seq: 1,
|
||||||
type: "run",
|
type: "run",
|
||||||
payload: { status: "keep", secondaryMetrics: [] },
|
payload: { status: "keep", primaryMetric: 0.91, secondaryMetrics: [] },
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,6 @@ export async function runBenchmark(
|
|||||||
let truncatedTempFile: string | undefined;
|
let truncatedTempFile: string | undefined;
|
||||||
let progressStdoutChunk = "";
|
let progressStdoutChunk = "";
|
||||||
let progressStderrChunk = "";
|
let progressStderrChunk = "";
|
||||||
let finished = false;
|
|
||||||
|
|
||||||
const emitProgress = () => {
|
const emitProgress = () => {
|
||||||
if (!opts.onProgress) {
|
if (!opts.onProgress) {
|
||||||
progressStdoutChunk = "";
|
progressStdoutChunk = "";
|
||||||
@@ -121,7 +119,6 @@ export async function runBenchmark(
|
|||||||
child.on("error", (error) => {
|
child.on("error", (error) => {
|
||||||
cleanup();
|
cleanup();
|
||||||
if ((error as NodeJS.ErrnoException).name === "AbortError") {
|
if ((error as NodeJS.ErrnoException).name === "AbortError") {
|
||||||
finished = true;
|
|
||||||
resolve({
|
resolve({
|
||||||
exitCode: 1,
|
exitCode: 1,
|
||||||
stdout: truncated ? (stdoutFull + stdoutTail).slice(-STDOUT_TAIL_BYTES) : stdoutFull,
|
stdout: truncated ? (stdoutFull + stdoutTail).slice(-STDOUT_TAIL_BYTES) : stdoutFull,
|
||||||
@@ -157,7 +154,6 @@ export async function runBenchmark(
|
|||||||
const effectiveStdout = truncated
|
const effectiveStdout = truncated
|
||||||
? (stdoutFull + stdoutTail).slice(-STDOUT_TAIL_BYTES)
|
? (stdoutFull + stdoutTail).slice(-STDOUT_TAIL_BYTES)
|
||||||
: stdoutFull;
|
: stdoutFull;
|
||||||
finished = true;
|
|
||||||
resolve({
|
resolve({
|
||||||
exitCode: code ?? (signal ? 1 : 0),
|
exitCode: code ?? (signal ? 1 : 0),
|
||||||
stdout: effectiveStdout,
|
stdout: effectiveStdout,
|
||||||
|
|||||||
Reference in New Issue
Block a user