Skip to content
Merged
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
34 changes: 31 additions & 3 deletions docs/reference/trajectory-assessment.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ It does not change solver prompts, benchmark scoring, checkpoints or research bu
## Researcher synthesis in the existing reader

New registrations using the default configuration and CLI previews request
`research_synthesis: true` in their
`research_synthesis: true` and `research_synthesis_revision: 2` in their
frozen model configuration. Explicit analysis uses the same observer call to
produce `research-episodes-synthesis-v1`: the six episode judgments plus a
produce `research-episodes-synthesis-v2`: the six episode judgments plus a
source-bound `synthesis`. There is no extra per-finding model call. The ordinary
Goal view follows **results → research report → research history**. Native
measurements and unknown retention appear first; the saved report adds a concise
Expand Down Expand Up @@ -48,13 +48,41 @@ in the summary. Useful negative evidence and observed late gains remain visible.
Existing registered configurations without this field keep their original
prompt, analysis key, schema and saved results. They are not automatically
upgraded or rerun. For an explicitly requested synthesis of an older study,
register a new version with `config.research_synthesis: true`; keep the previous
register a new version with `config.research_synthesis: true` and
`config.research_synthesis_revision: 2`; keep the previous
pack and its failures. The Python preparation adapter also accepts
`research_synthesis=False` to reproduce the legacy method. To disable new
synthesis, register with that field false or an explicit legacy configuration;
no source history is deleted. A language change retains the existing independent
language-specific cache and execution boundary.

Revision 2 refines the existing report fields, without new verdicts or a second
analysis call. It asks the observer to reconcile completed comparisons and
follow-up decisions before recommending work, preserve paired gains and
regressions, and distinguish configured limits from actual completed work.
Controls and predictions should explain whether the intervention engaged and
which missing observations would prevent interpretation. Equal seeds or time
limits do not alone prove matched work: distinguish fixed-work from wall-clock
comparisons and check the relevant coupling, allowing the intended state/path
divergence. A justified repeat with contemporary controls can still be useful.
These are model reasoning instructions; the validator does not certify that the
answer followed them or that a proposed experiment is scientifically sufficient.

New default producer requests persist revision 2. Previously stored synthesis
configurations that omit the revision retain the exact v1 prompt, key and
language caches; reading or retrying them does not upgrade the method. Explicit
custom curator configurations also remain v1 when the revision is omitted.
To use revised guidance on an existing case, register a successor pack and
explicitly run that new analysis in the same workbench. Keep both reports and
their separate failures and usage. No new configuration screen, automatic
generation or experiment execution is introduced.

新版指引要求先核对已完成比较和后续取舍,再提出下一步;把配置上限、实际完成工作及
干预是否生效分开,并保留配对改善/回退和局部结果未转移到原生测评的边界。相同种子
或墙钟期限不等于工作量匹配;应核对相关耦合,也允许机制本身造成后续路径分歧。
新生产请求保存第2版;缺少版本字段的旧配置保持原提示、标识和语言缓存。接续分析须
另行明确生成,原报告、失败与用量保留。引用资格和指引文字均不构成科学有效性证明。

Curators must admit relevant prior feedback into each episode that needs it.
A source existing elsewhere in the pack does not authorize a metric to cite it.
When prior context or submitted-code excerpts are missing, revise the frozen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def prepare(
{
"input": json.loads(input_path.read_text(encoding="utf-8")),
"config": {"model": model, "effort": effort, "service_tier": service_tier,
**({"research_synthesis": True} if research_synthesis else {})},
**({"research_synthesis": True, "research_synthesis_revision": 2} if research_synthesis else {})},
},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from ...file_lock import exclusive_file_lock
from .trajectory_assessment import _digest, _write, execute, read_job, request_cancel

DEFAULT_CONFIG = {"model": "gpt-6.1-sol", "effort": "max", "service_tier": "fast", "research_synthesis": True}
DEFAULT_CONFIG = {"model": "gpt-6.1-sol", "effort": "max", "service_tier": "fast", "research_synthesis": True,
"research_synthesis_revision": 2}


class PackUnavailable(ValueError):
Expand Down
23 changes: 19 additions & 4 deletions loopx/control_plane/capabilities/trajectory_assessment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,15 @@ Answer what was learned about the original objective, the important turning poin
The summary is a concise account of this declared sample, not the whole run. Return up to three findings, ordered by research value. For each, separate the directly observed result from your interpretation and concrete competing explanations. Preserve useful negative results and real late gains as positive signals without claiming their mechanism is proven. Empty findings are valid when evidence is insufficient; explain the gap in the summary.
Return up to two next_experiments in priority order, only when justified by this evidence. State the question, why it matters now, the smallest intervention, controls including relevant checkpoint/input and comparable budget, a predicted distinguishing observation and a result that would disconfirm the interpretation. Advice is a proposal, not execution or authorization. Avoid generic advice that cannot distinguish the supplied explanations.
Every summary, finding and next experiment must name the supplied episode_ids it draws on and cite the exact supporting source lines within those episodes' source_ids. Cite observations supporting the inference, not a future result. Cross-episode synthesis may combine the named episodes' evidence, but does not expand any individual metric's citation scope. Global sources outside those named episodes are not permitted. Do not infer causality from temporal adjacency, local proxy gains, a test count or a model verdict. Missing observations remain explicit limitations. Keep technical identifiers, numeric values, units, failures and uncertainty faithful to the sources.`;
// The revision is persisted in new requests. Omission replays the exact v1
// instructions and identity, including language-specific historical jobs.
const SYNTHESIS_DECISION_INSTRUCTION = `${SYNTHESIS_INSTRUCTION}
Before proposing work, reconcile applicable completed comparisons, follow-up outcomes and the decision that retained or rejected the candidate. A missing excerpt does not establish that an experiment never happened. Prefer the unresolved condition or counterexample; justify a repeat when contemporary controls, noise or recovery make it informative.
Make the comparison legible: identify the baseline and candidate, input selection, actual budget and validity. Keep paired improvements, regressions and ties together when supplied; do not add overlapping cohorts or compare differences from different baselines as if paired. Preserve local-to-native transfer failures and distinguish best, final and retained results.
For a resource-limit explanation, distinguish a configured ceiling from attempted, completed and effective work. Ask whether the changed limit could actually bind in the observed execution. A run that ended before the old deadline is not positive evidence that relaxing that deadline helped; it can be an unexposed control. Missing phase-entry, stopping-reason or completed-work evidence remains unknown, not proof of either no effect or more exploration.
Use the existing controls, prediction and disconfirming_result fields to make the proposed test interpretable. Name only the operational checks needed to tell whether its intervention engaged and its comparison held, such as phase entry, actual exit reason and completed/effective work. Make predictions conditional on those checks; a failed manipulation is not a refutation of the mechanism.
Equal seeds or wall-clock limits alone do not establish matched random draws, proposal coverage or completed work. Where relevant, distinguish a fixed-work comparison from an end-to-end wall-clock comparison, and say how actual coupling will be checked. Identify intended post-intervention state/path divergence separately from a confound; do not demand identical later trajectories when changing those trajectories is the mechanism. Sampled proposals, accepted intermediate moves and direct final improvements are different observations; no direct update does not exclude an indirect path effect.
Keep findings and proposals concise and specific to the cited evidence. These are reasoning instructions, not extra measurements, automatic scientific qualification or permission to execute a test.`;
const groundedSchema = {
episode_ids: {type: 'array', minItems: 1, items: {type: 'string'}},
citations: {type: 'array', minItems: 1, items: citationSchema},
Expand All @@ -220,7 +229,12 @@ export function prepareTrajectoryAssessment(params: Record<string, unknown>) {
throw new EffectRuntimeRequestError('research_synthesis must be boolean');
}
const synthesis = config.research_synthesis === true;
const method = synthesis ? SYNTHESIS_METHOD : TRAJECTORY_METHOD;
const synthesisRevision = config.research_synthesis_revision;
if (synthesisRevision !== undefined && (synthesisRevision !== 2 || !synthesis)) {
throw new EffectRuntimeRequestError('research_synthesis_revision requires enabled synthesis and supported revision 2');
}
const synthesisInstruction = synthesisRevision === 2 ? SYNTHESIS_DECISION_INSTRUCTION : SYNTHESIS_INSTRUCTION;
const method = synthesis ? synthesisRevision === 2 ? 'research-episodes-synthesis-v2' : SYNTHESIS_METHOD : TRAJECTORY_METHOD;
if (config.output_language !== undefined && config.output_language !== 'zh' && config.output_language !== 'en') {
throw new EffectRuntimeRequestError('output_language must be zh or en');
}
Expand All @@ -232,7 +246,8 @@ export function prepareTrajectoryAssessment(params: Record<string, unknown>) {
// Only analysis identity; provider configuration/execution is owned by its adapter.
// Omission preserves the identity of already registered, language-unspecified artifacts.
const configIdentity = {model: str(config.model, 'model'), effort: str(config.effort, 'effort'), service_tier: str(config.service_tier, 'service_tier'),
...(language ? {output_language: language} : {}), ...(synthesis ? {research_synthesis: true} : {})};
...(language ? {output_language: language} : {}), ...(synthesis ? {research_synthesis: true} : {}),
...(synthesisRevision === 2 ? {research_synthesis_revision: synthesisRevision} : {})};
const numberedSources = input.sources.map(s => ({...s, text: s.text.split('\n').map((line, n) => `${s.first_line+n}: ${line}`).join('\n')}));
const languageInstruction = language ? `\n\nOutput language: ${language === 'zh' ? 'Simplified Chinese (简体中文)' : 'English'}. Write every reason, limitation and episode question in this language, regardless of the evidence language.
Return episode_questions exactly once per supplied episode: episode_id unchanged and question as a concise faithful rendering of its research question. Preserve uncertainty, negation, comparisons, numbers and units. Do not add conclusions to questions. These are analytical renderings, not replacements for original records.
Expand All @@ -252,7 +267,7 @@ Do not translate source identities, references, timestamps, statuses, measuremen
const output_schema = synthesis ? {...assessmentSchema,
required: [...assessmentSchema.required, 'synthesis'], properties: {...assessmentSchema.properties, synthesis: synthesisSchema}} : assessmentSchema;
const analysis_key = createHash('sha256').update(JSON.stringify({method, rubric: RUBRIC, input, config: configIdentity,
...(synthesis ? {synthesis_instruction: SYNTHESIS_INSTRUCTION} : {}),
...(synthesis ? {synthesis_instruction: synthesisInstruction} : {}),
...(native ? {native_context_instruction: NATIVE_CONTEXT_INSTRUCTION} : {}),
...(language ? {language_instruction: languageInstruction} : {}),
...(display ? {display_method: display.schema_version, display_instruction: displayInstruction + literalValueInstruction + contextInstruction + quantityInstruction} : {})})).digest('hex');
Expand All @@ -266,7 +281,7 @@ Do not translate source identities, references, timestamps, statuses, measuremen
...(display ? {display_request: {schema_version: display.schema_version, snapshot_key: display.snapshot_key,
source_id: display.source_id, source_namespace: display.source_namespace, eligible: display.fields.length}} : {}),
...(measured ? {native_outcomes: measured} : {}),
prompt: `${RUBRIC}${languageInstruction}${displayInstruction}${literalValueInstruction}${contextInstruction}${quantityInstruction}${synthesis ? SYNTHESIS_INSTRUCTION : ''}${native ? NATIVE_CONTEXT_INSTRUCTION : ''}\n\nEvidence pack:\n${JSON.stringify({...input, sources: promptSources})}${native ? `\n\nNative outcome context:\n${JSON.stringify(native)}` : ''}${display ? `\n\ndisplay_fields:\n${JSON.stringify(display.requests.map(field => ({field_id: field.field_id, text: field.protected.masked,
prompt: `${RUBRIC}${languageInstruction}${displayInstruction}${literalValueInstruction}${contextInstruction}${quantityInstruction}${synthesis ? synthesisInstruction : ''}${native ? NATIVE_CONTEXT_INSTRUCTION : ''}\n\nEvidence pack:\n${JSON.stringify({...input, sources: promptSources})}${native ? `\n\nNative outcome context:\n${JSON.stringify(native)}` : ''}${display ? `\n\ndisplay_fields:\n${JSON.stringify(display.requests.map(field => ({field_id: field.field_id, text: field.protected.masked,
...(field.context_citations ? {context_citations: field.context_citations} : {}),
...(display.literalContext ? {literals: field.protected.literals} : {}),
...(display.literalValues ? {explanation_numbers: field.protected.explanation_numbers} : {})})))}` : ''}`,
Expand Down
29 changes: 29 additions & 0 deletions tests/capabilities/test_trajectory_assessment_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,35 @@ def upgraded_method(record, output_language=None):
inspect_pack(**scope)


def test_revised_producer_preserves_frozen_legacy_job_and_requires_new_execution(pack):
from loopx.capabilities.benchmark_toolkit.trajectory_assessment import prepare

scope, operation, current, input_path, sources, provider = pack
assert current["config"]["research_synthesis_revision"] == 2
prepared = prepare(input_path, model="gpt-6.1-sol", effort="max", service_tier="fast")
assert prepared["analysis_key"] == current["analysis_key"]
legacy = register_pack(**scope, input_path=input_path, source_root=sources,
title="Frozen legacy analysis", config={key: value for key, value in current["config"].items()
if key != "research_synthesis_revision"})
legacy_operation = {**scope, "pack_id": legacy["pack_id"], "analysis_key": legacy["analysis_key"]}
execute_pack(**legacy_operation, codex=provider)
old = inspect_pack(**scope, pack_id=legacy["pack_id"])
before = {path: path.read_bytes() for path in scope["runtime_root"].glob("**/jobs/*/*") if path.is_file()}
assert current["analysis_key"] != legacy["analysis_key"]
pending = inspect_pack(**scope, pack_id=current["pack_id"])
assert pending["job"] is None and pending["result"] is None
execute_pack(**operation, codex=provider)
new = inspect_pack(**scope, pack_id=current["pack_id"])
assert new["result"]["method_version"] == "research-episodes-synthesis-v2"
assert new["result"]["metrics"] == old["result"]["metrics"]
assert inspect_pack(**scope, pack_id=legacy["pack_id"]) == old
assert all(path.read_bytes() == content for path, content in before.items())
# Both frozen versions still use the same literal, source-qualified reader.
for op in [legacy_operation, operation]:
assert read_citation(**op, source_id="source", first_line=2, last_line=2)["text"] == (
"The native measurement did not improve utility · 原始测量。")


def _usage_fixture():
def counts(i, c, o):
return dict(input_tokens=i, cached_input_tokens=c, output_tokens=o)
Expand Down
32 changes: 30 additions & 2 deletions tests/control_plane_ts/trajectory_assessment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,11 +496,12 @@ test('artifact digests retain bare lowercase syntax, original bytes and error se
}
});

test('research synthesis is grounded across named episodes without widening metric evidence', () => {
for (const revision of [undefined, 2]) test(`research synthesis revision ${revision ?? 1} preserves episode citation boundaries`, () => {
const x = input();
x.sources.push({id: 'other', artifact: 'other.txt', artifact_sha256: 'b'.repeat(64), first_line: 4, text: 'A competing explanation remains open'});
x.episodes[1]!.source_ids = ['other'];
const p = prepareTrajectoryAssessment({input: x, config: {...config, research_synthesis: true, output_language: 'zh'}});
const p = prepareTrajectoryAssessment({input: x, config: {...config, research_synthesis: true, output_language: 'zh',
...(revision === 2 ? {research_synthesis_revision: revision} : {})}});
const citation = {source_id: 's', first_line: 10, last_line: 10};
const synthesis = {
summary: {text: 'Negative evidence narrowed the question; the mechanism remains unresolved.', episode_ids: ['e1'], citations: [citation]},
Expand Down Expand Up @@ -544,3 +545,30 @@ test('synthesis off preserves legacy prompt, schema, key and qualified output',
assert.equal(qualifyTrajectoryAssessment({...original, output: output()}).synthesis, undefined);
assert.throws(() => prepareTrajectoryAssessment({input: input(), config: {...config, research_synthesis: 'yes'}}), /must be boolean/);
});

test('synthesis revision separates new guidance while frozen and disabled requests keep their identities', () => {
const legacy = prepareTrajectoryAssessment({input: input(), config: {...config, research_synthesis: true}});
// Frozen public fixture identities from before the revision: upgrading the
// producer must not strand saved jobs, including language-specific requests.
assert.equal(prepared().analysis_key, 'bafc1cd4c93f25a274a6f6789eda79f3a1f9e197ff21a651bfd7f677f36cb607');
assert.equal(legacy.analysis_key, '96c07478771c8bdb4db5e79999a31a25f8acc86cbc182bc8cb09ad52ac8a7a91');
const next = prepareTrajectoryAssessment({input: input(), config: {...legacy.config, research_synthesis_revision: 2}});
assert.equal(next.method_version, 'research-episodes-synthesis-v2');
assert.notEqual(next.analysis_key, legacy.analysis_key);
assert.deepEqual(next.input, legacy.input);
assert.deepEqual(next.output_schema, legacy.output_schema, 'No new result fields or scientific qualification gate');
assert.equal(next.prompt.split('\n\nEvidence pack:\n')[1], legacy.prompt.split('\n\nEvidence pack:\n')[1]);
const raw = {...output(), synthesis: {summary: {text: 'The mechanism is unresolved.', episode_ids: ['e1'],
citations: [{source_id: 's', first_line: 10, last_line: 10}]}, findings: [], next_experiments: []}};
assert.deepEqual(qualifyTrajectoryAssessment({...next, output: raw}).metrics,
qualifyTrajectoryAssessment({...legacy, output: raw}).metrics);
assert.equal(compareTrajectoryAssessments({...next, previous: {...legacy, output: raw}}).can_reuse, false,
'A guidance revision needs an explicit new analysis; do not relabel a saved judgment');
assert.deepEqual(prepareTrajectoryAssessment({input: input(), config: legacy.config}), legacy);
for (const revision of [null, true, '2', 0, 1, 3]) {
assert.throws(() => prepareTrajectoryAssessment({input: input(), config: {...config,
research_synthesis: true, research_synthesis_revision: revision}}), /supported revision/);
}
assert.throws(() => prepareTrajectoryAssessment({input: input(), config: {...config,
research_synthesis: false, research_synthesis_revision: 2}}), /enabled synthesis/);
});
Loading