Skip to content
This repository was archived by the owner on Nov 10, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/distill-receipt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 EvoMap

// Transport success is not a receipt that persist:true stored both artifacts.
export function outcomeOfDistill(result, usage) {
const data = result?.data;
if (!result?.ok) return { status: 'failed', error: String(result?.error), usage };
const hasId = asset => typeof asset?.asset_id === 'string' && asset.asset_id.trim().length > 0;
if (data?.ok !== true || data.status !== 'stored' || !hasId(data.gene) || !hasId(data.capsule) || data.quality?.ok === false) {
return { status: 'failed', error: String(data?.reason ?? `distill_not_persisted:${data?.status ?? 'unknown'}`), data, usage };
}
return { status: 'sent', data, usage };
}
3 changes: 2 additions & 1 deletion src/session-distill.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { outcomeOfDistill } from './distill-receipt.js';
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 EvoMap

Expand Down Expand Up @@ -230,7 +231,7 @@ export async function distillCandidate({ proxyFetch, llm, candidate, focus, time
if (!args) return { status: 'unparsed', usage };
if (cancel?.aborted || !isCurrent()) return { status: 'dropped', usage };
const result = await proxyFetch('POST', '/conversation/distill', distillBody(candidate, args, usage), signal);
return result?.ok ? { status: 'sent', usage, data: result.data } : { status: 'failed', error: String(result?.error) };
return outcomeOfDistill(result, usage);
} catch (error) {
return { status: 'failed', error: String(error?.message ?? error) };
}
Expand Down
32 changes: 32 additions & 0 deletions test/distill-receipt.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { outcomeOfDistill } from '../src/distill-receipt.js';

const stored = { ok: true, status: 'stored', gene: { asset_id: 'gene_x' }, capsule: { asset_id: 'capsule_x' }, quality: { ok: true } };
const usage = { input_tokens: 10, output_tokens: 20 };

test('only a stored receipt with both artifact identities is sent', () => {
const result = outcomeOfDistill({ ok: true, data: stored }, usage);
assert.equal(result.status, 'sent');
assert.equal(result.data, stored);
assert.equal(result.usage, usage);
// Older stored receipts need not contain the optional quality field.
assert.equal(outcomeOfDistill({ ok: true, data: { ...stored, quality: undefined } }, usage).status, 'sent');
});

test('HTTP success with rejected, draft, malformed or incomplete data is not persisted', () => {
for (const data of [undefined, {}, { id: 'legacy-without-proof' }, { ok: false, reason: 'quality_gate' },
{ ...stored, status: 'draft' }, { ...stored, status: 'rejected' }, { ...stored, quality: { ok: false } },
{ ...stored, gene: null }, { ...stored, capsule: null }, { ...stored, gene: { asset_id: ' ' } },
{ ...stored, capsule: { asset_id: 42 } }]) {
const outcome = outcomeOfDistill({ ok: true, data }, usage);
assert.equal(outcome.status, 'failed');
assert.equal(outcome.data, data);
assert.equal(outcome.usage, usage);
}
});

test('transport failure and business rejection preserve usage and the reason', () => {
assert.deepEqual(outcomeOfDistill({ ok: false, error: 'unreachable' }, usage), { status: 'failed', error: 'unreachable', usage });
assert.equal(outcomeOfDistill({ ok: true, data: { ok: false, reason: 'quality_gate' } }, usage).error, 'quality_gate');
});
23 changes: 21 additions & 2 deletions test/session-distill.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ test('a list the model wrote as one numbered paragraph becomes steps', () => {

test('a candidate is distilled through the host model and sent to the Proxy with its own cost', async () => {
const sent = [];
const proxyFetch = async (method, path, body) => { sent.push({ path, body }); return { ok: true, data: { id: 'gene_x' } }; };
const proxyFetch = async (method, path, body) => { sent.push({ path, body }); return { ok: true, data: { id: 'gene_x', ok: true, status: 'stored', gene: { asset_id: 'gene_x' }, capsule: { asset_id: 'capsule_x' } } }; };
const candidate = candidateOf({ prompt: 'add a retry', record: record(), model: MODEL, turn: 1 });
const llm = fakeLlm();
const outcome = await distillCandidate({ proxyFetch, llm, candidate, timeoutMs: 1_000 });
Expand Down Expand Up @@ -181,7 +181,11 @@ async function harness(t, prefix, answerOf) {
return { proxy, listeners, llm: ctx.llm };
}

const proxyAnswer = (assets = []) => (path) => (path === '/asset/fetch' ? { assets } : { recorded: true });
const proxyAnswer = (assets = []) => (path) => {
if (path === '/asset/fetch') return { assets };
if (path === '/conversation/distill') return { ok: true, status: 'stored', gene: { asset_id: 'gene_x' }, capsule: { asset_id: 'capsule_x' } };
return { recorded: true };
};

async function verifiedTurn(listeners, agent, turn = 1) {
await primedBy(listeners, agent, 'add a retry to the uploader', turn);
Expand Down Expand Up @@ -314,3 +318,18 @@ test("the next session reports a killed session's open strategies and distills i
assert.equal(report.outcome, 'failed');
assert.equal(llm.calls.length, 1);
});

test('an HTTP 200 rejection retains the candidate and its model usage for a retry', async (t) => {
t.after(freshHome('evolver-distill-receipt-'));
const candidate = candidateOf({ prompt: 'add a retry', record: record(), model: MODEL, turn: 1 });
rememberCandidate('receipt-rejected', candidate);
const outcome = await distillCandidate({
proxyFetch: async () => ({ ok: true, data: { ok: false, reason: 'quality_gate', status: 'rejected' } }),
llm: fakeLlm(), candidate, timeoutMs: 1_000,
});
assert.equal(outcome.status, 'failed');
assert.equal(outcome.error, 'quality_gate');
assert.ok(outcome.usage);
settleCandidate('receipt-rejected', candidate, { sent: outcome.status === 'sent' });
assert.equal(dueCandidates({ sessionId: 'receipt-rejected' })[0].candidate.attempts, 1);
});
Loading