synergyspec-selfevolving 2.1.6 → 2.1.8
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/commands/self-evolution.js +8 -7
- package/dist/core/self-evolution/edits-contract.d.ts +3 -0
- package/dist/core/self-evolution/edits-contract.js +8 -0
- package/dist/core/self-evolution/evolving-agent.d.ts +33 -4
- package/dist/core/self-evolution/evolving-agent.js +162 -13
- package/package.json +1 -1
|
@@ -423,13 +423,14 @@ function packageHostEdits(editsInput, allowedFiles, currentFiles, group, targetI
|
|
|
423
423
|
}
|
|
424
424
|
const validated = validateCandidateEdits(editsInput.edits, allowedFiles);
|
|
425
425
|
const oldByPath = new Map(currentFiles.map((f) => [f.relPath.replace(/\\/g, '/'), f.content]));
|
|
426
|
-
// No-op guard:
|
|
427
|
-
// nothing to evolve — surface it as a no-op
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
// No-op guard: package only edits whose content differs from the current
|
|
427
|
+
// file. If none remain there is nothing to evolve — surface it as a no-op
|
|
428
|
+
// (placeholder), like the agent path.
|
|
429
|
+
const changed = validated.filter((e) => (oldByPath.get(e.relPath) ?? '') !== e.content);
|
|
430
|
+
if (changed.length === 0) {
|
|
430
431
|
throw new EvolvingAgentNoOp();
|
|
431
432
|
}
|
|
432
|
-
const diffPatch =
|
|
433
|
+
const diffPatch = changed
|
|
433
434
|
.map((e) => renderUnifiedDiff(e.relPath, oldByPath.get(e.relPath) ?? '', e.content))
|
|
434
435
|
.join('\n');
|
|
435
436
|
const rationale = (editsInput.rationale ?? '').trim() ||
|
|
@@ -437,9 +438,9 @@ function packageHostEdits(editsInput, allowedFiles, currentFiles, group, targetI
|
|
|
437
438
|
return {
|
|
438
439
|
targetId,
|
|
439
440
|
diffPatch,
|
|
440
|
-
changedFiles:
|
|
441
|
+
changedFiles: changed.map((e) => e.relPath),
|
|
441
442
|
rationale,
|
|
442
|
-
edits:
|
|
443
|
+
edits: changed,
|
|
443
444
|
};
|
|
444
445
|
}
|
|
445
446
|
/**
|
|
@@ -53,6 +53,9 @@ export interface CanonicalProposeOutput {
|
|
|
53
53
|
* {@link EvolvingAgentOutputInvalid} for any shape / frozen / scope
|
|
54
54
|
* violation. Path traversal and absolute paths are rejected transitively: they
|
|
55
55
|
* can never be a member of `allowedFiles`, so they fail the scope check.
|
|
56
|
+
* Duplicate relPaths are rejected because full-file replacements have no
|
|
57
|
+
* stable sequencing semantics; allowing them would let a later entry silently
|
|
58
|
+
* override an earlier one.
|
|
56
59
|
*/
|
|
57
60
|
export declare function validateCandidateEdits(rawEdits: readonly unknown[], allowedFiles: readonly string[]): {
|
|
58
61
|
relPath: string;
|
|
@@ -58,6 +58,9 @@ export const CanonicalProposerInvocationError = EvolvingAgentInvocationError;
|
|
|
58
58
|
* {@link EvolvingAgentOutputInvalid} for any shape / frozen / scope
|
|
59
59
|
* violation. Path traversal and absolute paths are rejected transitively: they
|
|
60
60
|
* can never be a member of `allowedFiles`, so they fail the scope check.
|
|
61
|
+
* Duplicate relPaths are rejected because full-file replacements have no
|
|
62
|
+
* stable sequencing semantics; allowing them would let a later entry silently
|
|
63
|
+
* override an earlier one.
|
|
61
64
|
*/
|
|
62
65
|
export function validateCandidateEdits(rawEdits, allowedFiles) {
|
|
63
66
|
if (rawEdits.length === 0) {
|
|
@@ -66,6 +69,7 @@ export function validateCandidateEdits(rawEdits, allowedFiles) {
|
|
|
66
69
|
const allowed = new Set(allowedFiles.map((p) => p.replace(/\\/g, '/')));
|
|
67
70
|
const frozen = new Set(GATE_DEFINING_FILES.map((p) => p.replace(/\\/g, '/')));
|
|
68
71
|
const validated = [];
|
|
72
|
+
const seen = new Set();
|
|
69
73
|
for (const e of rawEdits) {
|
|
70
74
|
if (!e || typeof e !== 'object') {
|
|
71
75
|
throw new EvolvingAgentOutputInvalid('edit entry must be an object');
|
|
@@ -82,6 +86,10 @@ export function validateCandidateEdits(rawEdits, allowedFiles) {
|
|
|
82
86
|
if (!allowed.has(norm)) {
|
|
83
87
|
throw new EvolvingAgentOutputInvalid(`edit relPath "${relPath}" is outside the target's declared files`);
|
|
84
88
|
}
|
|
89
|
+
if (seen.has(norm)) {
|
|
90
|
+
throw new EvolvingAgentOutputInvalid(`edit relPath "${relPath}" appears more than once; combine changes into one full-file content edit`);
|
|
91
|
+
}
|
|
92
|
+
seen.add(norm);
|
|
85
93
|
validated.push({ relPath: norm, content });
|
|
86
94
|
}
|
|
87
95
|
return validated;
|
|
@@ -41,6 +41,7 @@ import { type EvidenceCheck } from './promote.js';
|
|
|
41
41
|
import { type TargetProtection } from './success-channel.js';
|
|
42
42
|
import { type RejectBufferEntry } from './policy/reject-buffer.js';
|
|
43
43
|
import { type PolicyPrediction, type PolicyLedgerEntry } from './policy/policy-store.js';
|
|
44
|
+
import { type DiffEdit } from './line-diff.js';
|
|
44
45
|
import { type DiagnosisGap } from './scope-gate.js';
|
|
45
46
|
import type { WeaknessClass, GapSeverity } from './reward-agent.js';
|
|
46
47
|
/** Default edit budget L: at most this many changed lines (added + removed). */
|
|
@@ -122,17 +123,40 @@ export interface EvolvingAgentRefusal {
|
|
|
122
123
|
kind: 'refusal';
|
|
123
124
|
reason: string;
|
|
124
125
|
}
|
|
126
|
+
/** Legacy full-file replacement, still accepted for compatibility and small files. */
|
|
127
|
+
export interface EvolvingAgentFullContentEdit {
|
|
128
|
+
relPath: string;
|
|
129
|
+
content: string;
|
|
130
|
+
}
|
|
131
|
+
/** Replace one exact existing text block with a new text block. */
|
|
132
|
+
export interface EvolvingAgentReplaceTextEdit {
|
|
133
|
+
relPath: string;
|
|
134
|
+
operation: 'replaceText';
|
|
135
|
+
find: string;
|
|
136
|
+
replace: string;
|
|
137
|
+
}
|
|
138
|
+
/** Insert text adjacent to one exact marker block. */
|
|
139
|
+
export interface EvolvingAgentInsertMarkerEdit {
|
|
140
|
+
relPath: string;
|
|
141
|
+
operation: 'insertAfter' | 'insertBefore';
|
|
142
|
+
marker: string;
|
|
143
|
+
insert: string;
|
|
144
|
+
}
|
|
145
|
+
export type EvolvingAgentEditInstruction = EvolvingAgentFullContentEdit | EvolvingAgentReplaceTextEdit | EvolvingAgentInsertMarkerEdit;
|
|
125
146
|
/** A concrete bounded edit with its checkable prediction. */
|
|
126
147
|
export interface EvolvingAgentEdit {
|
|
127
148
|
kind: 'edit';
|
|
128
149
|
rationale: string;
|
|
129
150
|
prediction: PolicyPrediction;
|
|
130
|
-
edits:
|
|
131
|
-
relPath: string;
|
|
132
|
-
content: string;
|
|
133
|
-
}[];
|
|
151
|
+
edits: EvolvingAgentEditInstruction[];
|
|
134
152
|
}
|
|
135
153
|
export type ParsedEvolvingAgentResponse = EvolvingAgentRefusal | EvolvingAgentEdit;
|
|
154
|
+
export interface ResolvedEvolvingAgentEdit {
|
|
155
|
+
kind: 'edit';
|
|
156
|
+
rationale: string;
|
|
157
|
+
prediction: PolicyPrediction;
|
|
158
|
+
edits: DiffEdit[];
|
|
159
|
+
}
|
|
136
160
|
/**
|
|
137
161
|
* Parse the model's single `json:patch` block. Accepts EITHER the refusal shape
|
|
138
162
|
* (`{edits: [], refusal: string}`) OR a concrete edit (`{rationale, prediction,
|
|
@@ -144,6 +168,11 @@ export type ParsedEvolvingAgentResponse = EvolvingAgentRefusal | EvolvingAgentEd
|
|
|
144
168
|
* them); this only enforces the SHAPE of the contract.
|
|
145
169
|
*/
|
|
146
170
|
export declare function parseEvolvingAgentResponse(text: string): ParsedEvolvingAgentResponse;
|
|
171
|
+
/**
|
|
172
|
+
* Expand model-facing compact edit operations into the full-file DiffEdit shape
|
|
173
|
+
* consumed by the existing static, budget, scope, and policy-promotion gates.
|
|
174
|
+
*/
|
|
175
|
+
export declare function resolveEvolvingAgentEditInstructions(candidate: EvolvingAgentEdit, currentFiles: readonly DiffEdit[]): ResolvedEvolvingAgentEdit;
|
|
147
176
|
export interface RunEvolvingAgentOptions {
|
|
148
177
|
repoRoot: string;
|
|
149
178
|
episodeId: string;
|
|
@@ -101,12 +101,21 @@ function preludeLines(editBudget) {
|
|
|
101
101
|
' "prediction": {"metric": "loss" | "passRate" | "healthPenalty",',
|
|
102
102
|
' "direction": "down" | "up",',
|
|
103
103
|
' "checkBy": "<one sentence: how a later episode settles this>"},',
|
|
104
|
-
' "edits": [{"relPath": "<one of the allowed files>",
|
|
104
|
+
' "edits": [{"relPath": "<one of the allowed files>",',
|
|
105
|
+
' "operation": "replaceText",',
|
|
106
|
+
' "find": "<exact existing text that occurs once>",',
|
|
107
|
+
' "replace": "<replacement text>"}]}',
|
|
105
108
|
'```',
|
|
106
109
|
'',
|
|
107
110
|
'Rules:',
|
|
108
111
|
'- Only edit files listed under "CANONICAL TARGET" below. Never invent paths.',
|
|
109
|
-
|
|
112
|
+
'- Prefer compact edit operations: `replaceText`, `insertAfter`, or `insertBefore`.',
|
|
113
|
+
'- `replaceText.find` and marker text must match exactly one location in the current file.',
|
|
114
|
+
'- For `insertAfter`/`insertBefore`, include a non-empty `marker` field and a string `insert` field.',
|
|
115
|
+
'- Empty `edits` is valid only with a non-empty `refusal`; never emit `{"edits":[]}` alone.',
|
|
116
|
+
'- Include needed newlines inside `replace`/`insert`; the runner will expand the operation locally.',
|
|
117
|
+
'- Legacy `content` full-file replacements are accepted only for small files or true rewrites.',
|
|
118
|
+
'- Do not re-emit a large canonical file just to make a small localized edit.',
|
|
110
119
|
'- You NEVER score and you NEVER touch the gate/oracle files.',
|
|
111
120
|
].join('\n');
|
|
112
121
|
}
|
|
@@ -246,20 +255,153 @@ export function parseEvolvingAgentResponse(text) {
|
|
|
246
255
|
// Empty edits with no refusal reason is a malformed no-op, not a refusal.
|
|
247
256
|
throw new EvolvingAgentNoOp();
|
|
248
257
|
}
|
|
249
|
-
|
|
258
|
+
if (Object.prototype.hasOwnProperty.call(o, 'refusal')) {
|
|
259
|
+
throw new EvolvingAgentOutputInvalid('refusal is only valid with empty edits; choose either a refusal or a concrete edit');
|
|
260
|
+
}
|
|
261
|
+
// Concrete-edit shape: validate prediction + edit instruction shapes.
|
|
250
262
|
const prediction = parsePrediction(o.prediction);
|
|
251
263
|
const edits = [];
|
|
252
264
|
for (const e of rawEdits) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
265
|
+
if (!e || typeof e !== 'object') {
|
|
266
|
+
throw new EvolvingAgentOutputInvalid('edit entry must be an object');
|
|
267
|
+
}
|
|
268
|
+
const entry = e;
|
|
269
|
+
const relPath = entry.relPath;
|
|
270
|
+
if (typeof relPath !== 'string') {
|
|
271
|
+
throw new EvolvingAgentOutputInvalid('edit must have string relPath');
|
|
272
|
+
}
|
|
273
|
+
const normalizedRelPath = relPath.replace(/\\/g, '/');
|
|
274
|
+
const hasContent = Object.prototype.hasOwnProperty.call(entry, 'content');
|
|
275
|
+
const operation = entry.operation;
|
|
276
|
+
if (hasContent && operation !== undefined) {
|
|
277
|
+
throw new EvolvingAgentOutputInvalid('edit must use either legacy `content` or a compact `operation`, not both');
|
|
278
|
+
}
|
|
279
|
+
if (hasContent) {
|
|
280
|
+
if (typeof entry.content !== 'string') {
|
|
281
|
+
throw new EvolvingAgentOutputInvalid('legacy edit content must be a string');
|
|
282
|
+
}
|
|
283
|
+
edits.push({ relPath: normalizedRelPath, content: entry.content });
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (operation === 'replaceText') {
|
|
287
|
+
if (typeof entry.find !== 'string' || entry.find.length === 0) {
|
|
288
|
+
throw new EvolvingAgentOutputInvalid('replaceText edit requires a non-empty string `find`');
|
|
289
|
+
}
|
|
290
|
+
if (typeof entry.replace !== 'string') {
|
|
291
|
+
throw new EvolvingAgentOutputInvalid('replaceText edit requires string `replace`');
|
|
292
|
+
}
|
|
293
|
+
edits.push({
|
|
294
|
+
relPath: normalizedRelPath,
|
|
295
|
+
operation,
|
|
296
|
+
find: entry.find,
|
|
297
|
+
replace: entry.replace,
|
|
298
|
+
});
|
|
299
|
+
continue;
|
|
257
300
|
}
|
|
258
|
-
|
|
301
|
+
if (operation === 'insertAfter' || operation === 'insertBefore') {
|
|
302
|
+
if (typeof entry.marker !== 'string' || entry.marker.length === 0) {
|
|
303
|
+
throw new EvolvingAgentOutputInvalid(`${operation} edit requires a non-empty string marker`);
|
|
304
|
+
}
|
|
305
|
+
if (typeof entry.insert !== 'string') {
|
|
306
|
+
throw new EvolvingAgentOutputInvalid(`${operation} edit requires string insert`);
|
|
307
|
+
}
|
|
308
|
+
edits.push({
|
|
309
|
+
relPath: normalizedRelPath,
|
|
310
|
+
operation,
|
|
311
|
+
marker: entry.marker,
|
|
312
|
+
insert: entry.insert,
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
throw new EvolvingAgentOutputInvalid("edit must have legacy string `content` or operation 'replaceText' | 'insertAfter' | 'insertBefore'");
|
|
259
317
|
}
|
|
260
318
|
const rationale = typeof o.rationale === 'string' ? o.rationale.trim() : '';
|
|
261
319
|
return { kind: 'edit', rationale, prediction, edits };
|
|
262
320
|
}
|
|
321
|
+
function findExactlyOnce(content, needle, description) {
|
|
322
|
+
let foundAt = -1;
|
|
323
|
+
let count = 0;
|
|
324
|
+
let searchFrom = 0;
|
|
325
|
+
while (true) {
|
|
326
|
+
const idx = content.indexOf(needle, searchFrom);
|
|
327
|
+
if (idx === -1)
|
|
328
|
+
break;
|
|
329
|
+
foundAt = idx;
|
|
330
|
+
count += 1;
|
|
331
|
+
if (count > 1)
|
|
332
|
+
break;
|
|
333
|
+
searchFrom = idx + 1;
|
|
334
|
+
}
|
|
335
|
+
if (count !== 1) {
|
|
336
|
+
throw new EvolvingAgentOutputInvalid(`${description} must match exactly one location in the current file, found ${count} (missing or ambiguous)`);
|
|
337
|
+
}
|
|
338
|
+
return foundAt;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Expand model-facing compact edit operations into the full-file DiffEdit shape
|
|
342
|
+
* consumed by the existing static, budget, scope, and policy-promotion gates.
|
|
343
|
+
*/
|
|
344
|
+
export function resolveEvolvingAgentEditInstructions(candidate, currentFiles) {
|
|
345
|
+
const currentByPath = new Map(currentFiles.map((f) => [f.relPath.replace(/\\/g, '/'), f.content]));
|
|
346
|
+
const proposedByPath = new Map();
|
|
347
|
+
const editModeByPath = new Map();
|
|
348
|
+
const order = [];
|
|
349
|
+
const rememberPath = (relPath) => {
|
|
350
|
+
if (!proposedByPath.has(relPath))
|
|
351
|
+
order.push(relPath);
|
|
352
|
+
};
|
|
353
|
+
for (const raw of candidate.edits) {
|
|
354
|
+
const relPath = raw.relPath.replace(/\\/g, '/');
|
|
355
|
+
const mode = 'content' in raw ? 'fullContent' : 'compact';
|
|
356
|
+
const previousMode = editModeByPath.get(relPath);
|
|
357
|
+
if (mode === 'fullContent' && previousMode !== undefined) {
|
|
358
|
+
throw new EvolvingAgentOutputInvalid(`legacy full-file content edit for "${relPath}" cannot be combined with other edits for the same file`);
|
|
359
|
+
}
|
|
360
|
+
if (mode === 'compact' && previousMode === 'fullContent') {
|
|
361
|
+
throw new EvolvingAgentOutputInvalid(`legacy full-file content edit for "${relPath}" cannot be combined with other edits for the same file`);
|
|
362
|
+
}
|
|
363
|
+
editModeByPath.set(relPath, mode);
|
|
364
|
+
if ('content' in raw) {
|
|
365
|
+
rememberPath(relPath);
|
|
366
|
+
proposedByPath.set(relPath, raw.content);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const current = proposedByPath.get(relPath) ?? currentByPath.get(relPath);
|
|
370
|
+
if (current === undefined) {
|
|
371
|
+
throw new EvolvingAgentOutputInvalid(`compact edit for "${relPath}" cannot be expanded because the current file was not loaded`);
|
|
372
|
+
}
|
|
373
|
+
rememberPath(relPath);
|
|
374
|
+
if (raw.operation === 'replaceText') {
|
|
375
|
+
const start = findExactlyOnce(current, raw.find, `replaceText.find for "${relPath}"`);
|
|
376
|
+
const content = current.slice(0, start) +
|
|
377
|
+
raw.replace +
|
|
378
|
+
current.slice(start + raw.find.length);
|
|
379
|
+
proposedByPath.set(relPath, content);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const markerStart = findExactlyOnce(current, raw.marker, `${raw.operation}.marker for "${relPath}"`);
|
|
383
|
+
const insertAt = raw.operation === 'insertAfter'
|
|
384
|
+
? markerStart + raw.marker.length
|
|
385
|
+
: markerStart;
|
|
386
|
+
proposedByPath.set(relPath, current.slice(0, insertAt) + raw.insert + current.slice(insertAt));
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
kind: 'edit',
|
|
390
|
+
rationale: candidate.rationale,
|
|
391
|
+
prediction: candidate.prediction,
|
|
392
|
+
edits: order.map((relPath) => ({ relPath, content: proposedByPath.get(relPath) ?? '' })),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function dropByteIdenticalEdits(candidate, currentFiles) {
|
|
396
|
+
const currentByPath = new Map(currentFiles.map((f) => [f.relPath.replace(/\\/g, '/'), f.content]));
|
|
397
|
+
return {
|
|
398
|
+
...candidate,
|
|
399
|
+
edits: candidate.edits.filter((edit) => {
|
|
400
|
+
const current = currentByPath.get(edit.relPath.replace(/\\/g, '/'));
|
|
401
|
+
return current === undefined || current !== edit.content;
|
|
402
|
+
}),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
263
405
|
function parsePrediction(raw) {
|
|
264
406
|
if (!raw || typeof raw !== 'object') {
|
|
265
407
|
throw new EvolvingAgentOutputInvalid('a concrete edit requires a `prediction` object {metric, direction, checkBy}');
|
|
@@ -475,6 +617,7 @@ export async function runEvolvingAgent(opts) {
|
|
|
475
617
|
: `${basePrompt}\n\n# PREVIOUS ATTEMPT WAS REJECTED\n${feedback}\n` +
|
|
476
618
|
'Re-emit EXACTLY ONE ```json:patch fenced block — either a refusal ' +
|
|
477
619
|
'({"edits": [], "refusal": string}) or a single bounded edit ' +
|
|
620
|
+
'using a compact operation or legacy full-file content ' +
|
|
478
621
|
'({"rationale", "prediction", "edits"}), staying inside the diagnosed ' +
|
|
479
622
|
'sections and within the changed-line budget.';
|
|
480
623
|
const run = await runHeadlessAgent(prompt, {
|
|
@@ -490,14 +633,19 @@ export async function runEvolvingAgent(opts) {
|
|
|
490
633
|
throw new EvolvingAgentInvocationError(run.stderr);
|
|
491
634
|
}
|
|
492
635
|
try {
|
|
493
|
-
const
|
|
494
|
-
if (
|
|
495
|
-
parsed =
|
|
636
|
+
const response = parseEvolvingAgentResponse(run.stdout);
|
|
637
|
+
if (response.kind === 'refusal') {
|
|
638
|
+
parsed = response;
|
|
496
639
|
break;
|
|
497
640
|
}
|
|
641
|
+
const resolved = resolveEvolvingAgentEditInstructions(response, currentFiles);
|
|
498
642
|
// Static-shape edit: validate scope-to-target + frozen freeze here so a
|
|
499
643
|
// bad path is a REPAIRABLE failure (the evolving agent's repair contract).
|
|
500
|
-
validateCandidateEdits(
|
|
644
|
+
validateCandidateEdits(resolved.edits, allowedFiles);
|
|
645
|
+
const candidate = dropByteIdenticalEdits(resolved, currentFiles);
|
|
646
|
+
if (candidate.edits.length === 0) {
|
|
647
|
+
throw new EvolvingAgentOutputInvalid('edit makes no changes against the current policy head - emit a refusal or a real bounded edit');
|
|
648
|
+
}
|
|
501
649
|
// ≤ L budget (repairable).
|
|
502
650
|
const changed = countChangedLines(candidate.edits, currentFiles);
|
|
503
651
|
if (changed > editBudget) {
|
|
@@ -537,7 +685,8 @@ export async function runEvolvingAgent(opts) {
|
|
|
537
685
|
break;
|
|
538
686
|
}
|
|
539
687
|
catch (err) {
|
|
540
|
-
if (err instanceof EvolvingAgentOutputInvalid
|
|
688
|
+
if ((err instanceof EvolvingAgentOutputInvalid || err instanceof EvolvingAgentNoOp) &&
|
|
689
|
+
attempt < maxRepairAttempts) {
|
|
541
690
|
feedback = gateFeedback(err.message);
|
|
542
691
|
continue;
|
|
543
692
|
}
|