@evomap/evolver-core 2.0.0-beta.16 → 2.0.0-beta.18
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/dist/exec/autoExec.d.ts +17 -0
- package/dist/exec/autoExec.js +32 -1
- package/dist/trace/index.d.ts +2 -1
- package/dist/trace/index.js +2 -1
- package/dist/trace/learningTrace.d.ts +17 -2
- package/dist/trace/learningTrace.js +4 -2
- package/dist/trace/proxyTurns.d.ts +31 -0
- package/dist/trace/proxyTurns.js +129 -0
- package/package.json +1 -1
package/dist/exec/autoExec.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { ReuseOutcomeSummary, ReuseOutcomeEvent } from '../ops/reuseOutcome
|
|
|
11
11
|
import type { PersonalityStore } from '../personality/store.js';
|
|
12
12
|
import type { MemoryGraphProvider } from '../algo/memoryGraph.js';
|
|
13
13
|
import { type LearningPacketSink, type TraceSink } from '../trace/learningTrace.js';
|
|
14
|
+
import type { TraceReadOptions } from '../trace/trajectoryExport.js';
|
|
14
15
|
export interface AutoExecTask {
|
|
15
16
|
id: string;
|
|
16
17
|
repo: string;
|
|
@@ -124,6 +125,22 @@ export interface AutoExecDeps {
|
|
|
124
125
|
traceSink?: TraceSink;
|
|
125
126
|
/** Hub packet sourceRepo column; default 'evolver-v2'. */
|
|
126
127
|
sourceRepo?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Proxy llm_turn fold (Learning Ops slice 5): when set, after the cycle (and BEFORE run.completed, so
|
|
130
|
+
* sequence order holds) the run's wall-clock window of proxy trace records is read from `dir`
|
|
131
|
+
* (llm-trace-*.jsonl day-files) and folded into the recorder via recordLlmTurn — real per-request
|
|
132
|
+
* model.called + tool.called/tool.failed detail instead of only the bridge's coarse spawn event.
|
|
133
|
+
* Correlation is the time window + session-first-turn heuristic (see trace/proxyTurns.ts). Best-effort:
|
|
134
|
+
* a missing dir / unreadable file / no proxy degrades to zero folded turns, never a verdict change.
|
|
135
|
+
*/
|
|
136
|
+
proxyTraces?: {
|
|
137
|
+
/** Proxy trace day-file dir (events/paths.ts tracesDir()). */
|
|
138
|
+
dir: string;
|
|
139
|
+
/** Decryption material for encrypted trace envelopes (allowPartial is always forced on). */
|
|
140
|
+
readOptions?: TraceReadOptions;
|
|
141
|
+
/** Injected clock for deterministic tests. Default Date.now. */
|
|
142
|
+
now?: () => number;
|
|
143
|
+
};
|
|
127
144
|
};
|
|
128
145
|
}
|
|
129
146
|
export interface ForcedGeneFields {
|
package/dist/exec/autoExec.js
CHANGED
|
@@ -12,6 +12,7 @@ import { runEvolutionCycle } from '../algo/orchestrator.js';
|
|
|
12
12
|
import { makeSafeExecute, makeTrustedGeneResolver } from './autonomousCycle.js';
|
|
13
13
|
import { findSignalHints } from './openPrRegistry.js';
|
|
14
14
|
import { AgentRunTraceRecorder, buildLearningPacketDraft } from '../trace/learningTrace.js';
|
|
15
|
+
import { collectRunLlmTurns } from '../trace/proxyTurns.js';
|
|
15
16
|
/** Same path-containment as the bridge guard — used here to refuse before running anything (clean verdict). */
|
|
16
17
|
function withinAllowlist(repo, roots) {
|
|
17
18
|
const c = resolvePath(repo);
|
|
@@ -147,6 +148,10 @@ export async function runAutoExecTask(deps, rawTask, safety) {
|
|
|
147
148
|
...(deps.learningTrace.traceSink ? { sink: deps.learningTrace.traceSink } : {}),
|
|
148
149
|
})
|
|
149
150
|
: undefined;
|
|
151
|
+
// Wall-clock window of this run, used to correlate the proxy's llm_turn records (slice 5). Captured
|
|
152
|
+
// unconditionally-cheaply only when the fold is configured.
|
|
153
|
+
const proxyTraceClock = deps.learningTrace?.proxyTraces?.now ?? Date.now;
|
|
154
|
+
const runStartMs = deps.learningTrace?.proxyTraces ? proxyTraceClock() : 0;
|
|
150
155
|
try {
|
|
151
156
|
traceRecorder?.runStarted({ taskSummary: task.expectedEffect, signals: cycleSignals, metadata: { repo: task.repo, target: task.target } });
|
|
152
157
|
}
|
|
@@ -159,12 +164,25 @@ export async function runAutoExecTask(deps, rawTask, safety) {
|
|
|
159
164
|
hubCandidates = [];
|
|
160
165
|
}
|
|
161
166
|
}
|
|
167
|
+
// evaluation fill-in (slice 6): the validate hook is the run's external verifier (sandboxed validation
|
|
168
|
+
// commands), so its result — when it actually RAN — becomes the packet's evaluation.verification
|
|
169
|
+
// (verifier 'automated_test'). Observation is a pass-through wrapper: the hook's result reaches the
|
|
170
|
+
// bridge unchanged, and a run where validation never fired keeps the evaluation placeholder.
|
|
171
|
+
let observedVerification;
|
|
172
|
+
const baseValidate = deps.validate?.(task);
|
|
173
|
+
const observingValidate = baseValidate
|
|
174
|
+
? async (mutation, decision, cwd) => {
|
|
175
|
+
const v = await baseValidate(mutation, decision, cwd);
|
|
176
|
+
observedVerification = { verifier: 'automated_test', passed: v.passed, ...(v.score !== undefined ? { score: v.score } : {}) };
|
|
177
|
+
return v;
|
|
178
|
+
}
|
|
179
|
+
: undefined;
|
|
162
180
|
const execute = makeSafeExecute(task.repo, deps.store, safety, {
|
|
163
181
|
...(deps.provenance ? { provenance: deps.provenance } : {}),
|
|
164
182
|
...(deps.review ? { review: deps.review } : {}),
|
|
165
183
|
...(deps.includeProbation ? { includeProbation: true } : {}),
|
|
166
184
|
...(task.validationCmds ? { validationCmds: task.validationCmds } : {}),
|
|
167
|
-
...(
|
|
185
|
+
...(observingValidate ? { validate: observingValidate } : {}),
|
|
168
186
|
...(deps.personality ? { personality: deps.personality } : {}),
|
|
169
187
|
...(deps.agent ? { agent: deps.agent } : {}),
|
|
170
188
|
...(deps.git ? { git: deps.git } : {}),
|
|
@@ -211,6 +229,18 @@ export async function runAutoExecTask(deps, rawTask, safety) {
|
|
|
211
229
|
const status = res.finalStage === 'solidified' ? 'solidified' : res.finalStage === 'failed' ? 'failed' : 'innovated';
|
|
212
230
|
const cap = res.capsule;
|
|
213
231
|
if (traceRecorder && deps.learningTrace) {
|
|
232
|
+
// Proxy llm_turn fold (slice 5): fold the run window's per-request turns BEFORE run.completed so the
|
|
233
|
+
// trajectory stays sequence-ordered (model/tool detail inside the run, completion last). Own try — a
|
|
234
|
+
// throwing sink mid-fold must not cost the run its completion event or packet draft.
|
|
235
|
+
try {
|
|
236
|
+
const proxyTraces = deps.learningTrace.proxyTraces;
|
|
237
|
+
if (proxyTraces) {
|
|
238
|
+
const turns = collectRunLlmTurns(proxyTraces.dir, { startMs: runStartMs, endMs: proxyTraceClock() }, proxyTraces.readOptions ? { readOptions: proxyTraces.readOptions } : {});
|
|
239
|
+
for (const turn of turns)
|
|
240
|
+
traceRecorder.recordLlmTurn(turn);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch { /* observability only */ }
|
|
214
244
|
try {
|
|
215
245
|
traceRecorder.runCompleted({
|
|
216
246
|
status: res.finalStage === 'solidified' ? 'success' : 'failed',
|
|
@@ -224,6 +254,7 @@ export async function runAutoExecTask(deps, rawTask, safety) {
|
|
|
224
254
|
taskSummary: task.expectedEffect,
|
|
225
255
|
signals: cycleSignals,
|
|
226
256
|
environment: { repo: task.repo, runner: safety.runner ?? 'claude' },
|
|
257
|
+
...(observedVerification !== undefined ? { verification: observedVerification } : {}),
|
|
227
258
|
}));
|
|
228
259
|
}
|
|
229
260
|
catch { /* packet delivery is best-effort; never fail the task */ }
|
package/dist/trace/index.d.ts
CHANGED
package/dist/trace/index.js
CHANGED
|
@@ -128,12 +128,24 @@ export declare function learningTraceObserver(deps: {
|
|
|
128
128
|
recorder: AgentRunTraceRecorder;
|
|
129
129
|
timeoutMs?: number;
|
|
130
130
|
}): Observer;
|
|
131
|
+
/**
|
|
132
|
+
* Externally-verified run evidence (Learning Ops slice 6): the runtime's validate hook (sandboxed
|
|
133
|
+
* validation commands) is an `automated_test` verifier in the hub VERIFIERS vocabulary. Only a hook
|
|
134
|
+
* that actually RAN produces one of these — agent self-report never fills evaluation.
|
|
135
|
+
*/
|
|
136
|
+
export interface LearningPacketVerification {
|
|
137
|
+
verifier: 'automated_test';
|
|
138
|
+
passed: boolean;
|
|
139
|
+
score?: number;
|
|
140
|
+
}
|
|
131
141
|
export interface LearningPacketDraftInput {
|
|
132
142
|
/** e.g. 'evolver-v2'. Hub column sourceRepo. */
|
|
133
143
|
sourceRepo: string;
|
|
134
144
|
taskSummary?: string;
|
|
135
145
|
signals?: readonly string[];
|
|
136
146
|
environment?: Record<string, unknown>;
|
|
147
|
+
/** When present, fills evaluation (placeholder → false). Omit when no external verifier ran. */
|
|
148
|
+
verification?: LearningPacketVerification;
|
|
137
149
|
}
|
|
138
150
|
/** Local draft aligned with hub LearningOpsPacket ingest fields; placeholders are explicit, not implied. */
|
|
139
151
|
export interface LearningPacketDraft {
|
|
@@ -160,10 +172,13 @@ export interface LearningPacketDraft {
|
|
|
160
172
|
placeholder: true;
|
|
161
173
|
items: never[];
|
|
162
174
|
};
|
|
175
|
+
/** placeholder:false ⇔ an external verifier ran (verification input) — then verifier/verifierPassed are set. */
|
|
163
176
|
evaluation: {
|
|
164
|
-
placeholder:
|
|
177
|
+
placeholder: boolean;
|
|
165
178
|
outcomeStatus: 'success' | 'failed' | 'unknown';
|
|
166
|
-
verifier: null;
|
|
179
|
+
verifier: 'automated_test' | null;
|
|
180
|
+
verifierPassed?: boolean;
|
|
181
|
+
verifierScore?: number;
|
|
167
182
|
failureCategory: string | null;
|
|
168
183
|
};
|
|
169
184
|
governance: {
|
|
@@ -244,9 +244,11 @@ export function buildLearningPacketDraft(recorder, input) {
|
|
|
244
244
|
trajectory: events,
|
|
245
245
|
artifacts: { placeholder: true, items: [] },
|
|
246
246
|
evaluation: {
|
|
247
|
-
placeholder:
|
|
247
|
+
placeholder: input.verification === undefined,
|
|
248
248
|
outcomeStatus: completedStatus === 'success' ? 'success' : completedStatus === 'failed' ? 'failed' : 'unknown',
|
|
249
|
-
verifier: null,
|
|
249
|
+
verifier: input.verification?.verifier ?? null,
|
|
250
|
+
...(input.verification !== undefined ? { verifierPassed: input.verification.passed } : {}),
|
|
251
|
+
...(input.verification?.score !== undefined ? { verifierScore: input.verification.score } : {}),
|
|
250
252
|
failureCategory: typeof failureKind === 'string' ? failureKind : null,
|
|
251
253
|
},
|
|
252
254
|
governance: { placeholder: true, redactionStatus: 'metadata_only', consentStatus: 'unknown', trainingEligible: false, retentionPolicy: 'standard' },
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type TraceReadOptions } from './trajectoryExport.js';
|
|
2
|
+
import { type TraceTurnDraft } from './trajectory.js';
|
|
3
|
+
/** One agent run's wall-clock window (epoch ms, same host clock as the proxy's ts). */
|
|
4
|
+
export interface RunTurnWindow {
|
|
5
|
+
startMs: number;
|
|
6
|
+
endMs: number;
|
|
7
|
+
}
|
|
8
|
+
export interface SelectRunLlmTurnsOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Exact-match correlation key: when the caller knows the spawned agent's session id, only that session's
|
|
11
|
+
* turns are returned (the window heuristic is skipped — the id is authoritative).
|
|
12
|
+
*/
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface CollectRunLlmTurnsOptions extends SelectRunLlmTurnsOptions {
|
|
16
|
+
/** Decryption material forwarded to readTraceRowsFromJsonl. allowPartial is always forced on. */
|
|
17
|
+
readOptions?: TraceReadOptions;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Pure selector (unit-testable without fs): pick the turns that belong to the run per the correlation
|
|
21
|
+
* contract above, sorted by ts ascending (stable — equal timestamps keep day-file append order, and the
|
|
22
|
+
* recorder's fold order becomes the sequence order).
|
|
23
|
+
*/
|
|
24
|
+
export declare function selectRunLlmTurns(turns: readonly TraceTurnDraft[], window: RunTurnWindow, opts?: SelectRunLlmTurnsOptions): TraceTurnDraft[];
|
|
25
|
+
/**
|
|
26
|
+
* Read the proxy trace day-files in `dir` and return this run's llm_turns (see the correlation contract
|
|
27
|
+
* above), ready to fold via recordLlmTurn. Reuses readTraceRowsFromJsonl (decryption + row parsing) and
|
|
28
|
+
* traceRecordToTurnDraft (normalization). Never throws: any failure — no proxy, missing dir, unreadable
|
|
29
|
+
* file, undecryptable rows — degrades to [].
|
|
30
|
+
*/
|
|
31
|
+
export declare function collectRunLlmTurns(dir: string, window: RunTurnWindow, opts?: CollectRunLlmTurnsOptions): TraceTurnDraft[];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Proxy llm_turn → run fold (Learning Ops slice 5): collect the per-request llm_turn records the LLM proxy
|
|
2
|
+
// captured DURING one agent run's wall-clock window, normalized as TraceTurnDrafts ready for
|
|
3
|
+
// AgentRunTraceRecorder.recordLlmTurn. This is what upgrades a run's trajectory from the bridge's single
|
|
4
|
+
// coarse model.called (the headless runner is a black box) to real per-request fidelity
|
|
5
|
+
// (provider/model/usage/latency/stop_reason + tool-call detail).
|
|
6
|
+
//
|
|
7
|
+
// Correlation contract (why time window + session-first-turn, not session_id alone): the headless runner does
|
|
8
|
+
// not report its session id back to the bridge (`claude -p --output-format text` is opaque), and llm_turn rows
|
|
9
|
+
// carry no cwd — so the run has no exact key to look up. What the run DOES own is its wall-clock window on the
|
|
10
|
+
// same host the proxy writes from (one shared clock, no skew). A turn belongs to the run iff:
|
|
11
|
+
// 1. its ts falls inside [startMs, endMs], AND
|
|
12
|
+
// 2. its session's FIRST observed turn also falls inside the window — a session spawned by this run cannot
|
|
13
|
+
// have traffic predating the run, while a concurrent interactive session (started earlier) is excluded by
|
|
14
|
+
// its pre-window history. Sessionless turns (session_id null) fall back to the window test alone.
|
|
15
|
+
// Callers that DO know the spawned agent's session id (e.g. a future runner passing --session-id) can pass
|
|
16
|
+
// `sessionId` for exact-match correlation instead of the heuristic.
|
|
17
|
+
//
|
|
18
|
+
// Residual risk, accepted + documented: an interactive session whose very first request starts inside the run
|
|
19
|
+
// window is indistinguishable from the run's own agent. On an unattended daemon host this is rare, and the
|
|
20
|
+
// fold is observability-only — it can bias a trace, never a verdict.
|
|
21
|
+
//
|
|
22
|
+
// Everything degrades silently to [] (missing dir, unreadable file, undecryptable envelope, bad ts): the
|
|
23
|
+
// learning trace must never fail or slow a task.
|
|
24
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { readTraceRowsFromJsonl } from './trajectoryExport.js';
|
|
27
|
+
import { traceRecordToTurnDraft } from './trajectory.js';
|
|
28
|
+
/** Any proxy day-file (`llm-trace-*.jsonl`). */
|
|
29
|
+
const TRACE_FILE_RE = /^llm-trace-.*\.jsonl$/i;
|
|
30
|
+
/** The canonical day-stamped name the proxy's JsonlTraceSink writes (`llm-trace-YYYYMMDD.jsonl`, UTC). */
|
|
31
|
+
const DAY_STAMPED_FILE_RE = /^llm-trace-(\d{8})\.jsonl$/i;
|
|
32
|
+
function utcDayStamp(ms) {
|
|
33
|
+
const d = new Date(ms);
|
|
34
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
35
|
+
return `${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Keep day-stamped files that could contain the window's turns. One extra preceding day is included so the
|
|
39
|
+
* session-first-turn heuristic can see the pre-window history of a session that started before midnight.
|
|
40
|
+
* Non-day-stamped `llm-trace-*.jsonl` names (custom sinks/tests) are kept conservatively — the ts window
|
|
41
|
+
* filter below is the authority; the filename filter only trims read volume.
|
|
42
|
+
*/
|
|
43
|
+
function fileCoversWindow(name, window) {
|
|
44
|
+
const match = DAY_STAMPED_FILE_RE.exec(name);
|
|
45
|
+
if (!match)
|
|
46
|
+
return true;
|
|
47
|
+
const stamp = match[1];
|
|
48
|
+
return stamp >= utcDayStamp(window.startMs - 24 * 60 * 60 * 1000) && stamp <= utcDayStamp(window.endMs);
|
|
49
|
+
}
|
|
50
|
+
function turnTsMs(turn) {
|
|
51
|
+
if (turn.ts === null)
|
|
52
|
+
return null;
|
|
53
|
+
const ms = Date.parse(turn.ts);
|
|
54
|
+
return Number.isFinite(ms) ? ms : null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Pure selector (unit-testable without fs): pick the turns that belong to the run per the correlation
|
|
58
|
+
* contract above, sorted by ts ascending (stable — equal timestamps keep day-file append order, and the
|
|
59
|
+
* recorder's fold order becomes the sequence order).
|
|
60
|
+
*/
|
|
61
|
+
export function selectRunLlmTurns(turns, window, opts = {}) {
|
|
62
|
+
const stamped = turns
|
|
63
|
+
.map((turn) => ({ turn, tsMs: turnTsMs(turn) }))
|
|
64
|
+
.filter((entry) => entry.tsMs !== null);
|
|
65
|
+
let selected;
|
|
66
|
+
if (opts.sessionId !== undefined) {
|
|
67
|
+
selected = stamped.filter(({ turn }) => turn.session_id === opts.sessionId);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// First observed turn per session across ALL provided turns (including pre-window rows from the same
|
|
71
|
+
// day files) — this is what tells an in-run spawned session apart from an older concurrent one.
|
|
72
|
+
const firstTsBySession = new Map();
|
|
73
|
+
for (const { turn, tsMs } of stamped) {
|
|
74
|
+
if (turn.session_id === null)
|
|
75
|
+
continue;
|
|
76
|
+
const prev = firstTsBySession.get(turn.session_id);
|
|
77
|
+
if (prev === undefined || tsMs < prev)
|
|
78
|
+
firstTsBySession.set(turn.session_id, tsMs);
|
|
79
|
+
}
|
|
80
|
+
selected = stamped.filter(({ turn, tsMs }) => {
|
|
81
|
+
if (tsMs < window.startMs || tsMs > window.endMs)
|
|
82
|
+
return false;
|
|
83
|
+
if (turn.session_id === null)
|
|
84
|
+
return true;
|
|
85
|
+
const firstTs = firstTsBySession.get(turn.session_id);
|
|
86
|
+
return firstTs !== undefined && firstTs >= window.startMs;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return selected
|
|
90
|
+
.map((entry, index) => ({ ...entry, index }))
|
|
91
|
+
.sort((a, b) => a.tsMs - b.tsMs || a.index - b.index)
|
|
92
|
+
.map(({ turn }) => turn);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Read the proxy trace day-files in `dir` and return this run's llm_turns (see the correlation contract
|
|
96
|
+
* above), ready to fold via recordLlmTurn. Reuses readTraceRowsFromJsonl (decryption + row parsing) and
|
|
97
|
+
* traceRecordToTurnDraft (normalization). Never throws: any failure — no proxy, missing dir, unreadable
|
|
98
|
+
* file, undecryptable rows — degrades to [].
|
|
99
|
+
*/
|
|
100
|
+
export function collectRunLlmTurns(dir, window, opts = {}) {
|
|
101
|
+
try {
|
|
102
|
+
if (!(window.endMs >= window.startMs))
|
|
103
|
+
return [];
|
|
104
|
+
const names = readdirSync(dir)
|
|
105
|
+
.filter((name) => TRACE_FILE_RE.test(name) && fileCoversWindow(name, window))
|
|
106
|
+
.sort();
|
|
107
|
+
const turns = [];
|
|
108
|
+
for (const name of names) {
|
|
109
|
+
let text;
|
|
110
|
+
try {
|
|
111
|
+
text = readFileSync(join(dir, name), 'utf8');
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// allowPartial forced on: an undecryptable envelope is a coverage gap, never a fold failure.
|
|
117
|
+
const { rows } = readTraceRowsFromJsonl(text, { ...(opts.readOptions ?? {}), allowPartial: true });
|
|
118
|
+
for (const row of rows) {
|
|
119
|
+
const turn = traceRecordToTurnDraft(row);
|
|
120
|
+
if (turn !== null)
|
|
121
|
+
turns.push(turn);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return selectRunLlmTurns(turns, window, opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {});
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|