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
4 changes: 2 additions & 2 deletions packages/coding-agent/src/core/evolution-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,8 +770,8 @@ class CodingAgentSnapshotSource implements EvolutionSnapshotSource {
promptVersions: {
reflector: "reflector-v2",
curator: "curator-v1",
skillCurator: "skill-curator-v1",
skillSynthesizer: "skill-synthesizer-v1",
skillCurator: "skill-curator-v2",
skillSynthesizer: "skill-synthesizer-v2",
validator: "validator-v1",
writer: "writer-v1",
},
Expand Down
7 changes: 4 additions & 3 deletions packages/coding-agent/test/evolution-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1061,9 +1061,10 @@ describe("coding-agent evolution runtime", () => {
'"objective":"classify-operational-patterns-and-produce-non-executable-skill-candidates"',
);
expect(prompt).not.toContain(traceId);
return fauxAssistantMessage(fauxToolCall("submit_evolution_response", { decisions: [] }), {
stopReason: "toolUse",
});
return fauxAssistantMessage(
fauxToolCall("submit_evolution_response", { decisions: [], proceduralInformationLosses: [] }),
{ stopReason: "toolUse" },
);
},
]);
const runtime = await createCodingAgentEvolutionRuntime({
Expand Down
23 changes: 23 additions & 0 deletions packages/evolution/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,29 @@ export interface Lesson {
readonly actionableTakeaway: string;
}

export type ProceduralInformationLossKind =
| "action-order"
| "decision-branch"
| "failure-signal"
| "mitigation-or-recovery"
| "tool-or-environment-dependency"
| "verification-action";

export interface ProceduralInformationLoss {
readonly missing: readonly ProceduralInformationLossKind[];
readonly supportingEvidenceIds: readonly string[];
readonly explanation: string;
}

export interface ProceduralInformationLossFinding {
readonly artifactKind: "procedural-information-loss";
readonly metadata: EvolutionArtifactMetadata;
readonly findingId: string;
readonly detectedBy: "skill-curator";
readonly sourceLessonIds: readonly string[];
readonly loss: ProceduralInformationLoss;
}

export type SkillCandidateOperation = "CREATE" | "MERGE" | "RETIRE" | "UPDATE";

export type SkillabilityClassification =
Expand Down
3 changes: 3 additions & 0 deletions packages/evolution/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export {
type MutationWriteResult,
type MutationWrittenLedgerEvent,
type OrchestrationSourceTarget,
type ProceduralInformationLoss,
type ProceduralInformationLossFinding,
type ProceduralInformationLossKind,
type ProposalDispositionLedgerEvent,
type ProposalRolledBackLedgerEvent,
type ProposalValidationResult,
Expand Down
13 changes: 12 additions & 1 deletion packages/evolution/src/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,11 +704,22 @@ export class EvolutionPipeline implements EvolutionStageExecutor {
curation.deferredFindings.map((finding) => finding.findingId),
"deferred skill finding ID",
);
assertUniqueIds(
curation.proceduralInformationLossFindings.map((finding) => finding.findingId),
"procedural information loss finding ID",
);
return {
artifacts: [...curation.candidates, ...curation.deferredFindings],
artifacts: [
...curation.candidates,
...curation.deferredFindings,
...curation.proceduralInformationLossFindings,
],
state: {
candidateIds: curation.candidates.map((candidate) => candidate.candidateId),
deferredFindingIds: curation.deferredFindings.map((finding) => finding.findingId),
proceduralInformationLossFindingIds: curation.proceduralInformationLossFindings.map(
(finding) => finding.findingId,
),
selectedMemoryProposalIds: selectedMemoryProposals.map((scored) => scored.proposal.proposalId),
},
directive: selectedMemoryProposals.length === 0 ? "complete-no-op" : "continue",
Expand Down
3 changes: 3 additions & 0 deletions packages/evolution/src/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
Lesson,
MutationBatch,
MutationWriteReceipt,
ProceduralInformationLossFinding,
ReflectionNode,
ScoredProposal,
SkillCandidate,
Expand All @@ -54,6 +55,7 @@ export type EvolutionArtifact =
| InstructionSnapshot
| Lesson
| MutationBatch
| ProceduralInformationLossFinding
| MutationWriteReceipt
| ReflectionNode
| ScoredProposal
Expand Down Expand Up @@ -248,6 +250,7 @@ export interface SkillCuratorInput {
export interface SkillCurationResult {
readonly candidates: readonly SkillCandidate[];
readonly deferredFindings: readonly DeferredSkillCandidateFinding[];
readonly proceduralInformationLossFindings: readonly ProceduralInformationLossFinding[];
}

export interface SkillCurator {
Expand Down
6 changes: 6 additions & 0 deletions packages/evolution/src/skill-curator-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export class SkillCuratorToolRegistry {
readonly #maxToolCalls: number;
readonly #maxReferencesPerCall: number;
readonly #recoverableDenials: SkillCuratorRecoverableToolDenial[] = [];
readonly #inspectedEvidenceIds = new Set<string>();
readonly #inspectedSkillIds = new Set<string>();
readonly evidence: EvolutionModelReferenceCatalog<SkillCuratorEvidenceBinding>;
readonly skills: EvolutionModelReferenceCatalog<EvolutionSkillCatalogEntry>;
Expand Down Expand Up @@ -246,6 +247,10 @@ export class SkillCuratorToolRegistry {
return entry.ref;
}

wasEvidenceInspected(evidenceId: string): boolean {
return this.#inspectedEvidenceIds.has(evidenceId);
}

wasSkillInspected(skillId: string): boolean {
return this.#inspectedSkillIds.has(skillId);
}
Expand Down Expand Up @@ -275,6 +280,7 @@ export class SkillCuratorToolRegistry {
const context = [...contextBySource.values()].sort(
(left, right) => left.evidence.order.traceSequence - right.evidence.order.traceSequence,
);
for (const binding of selected) this.#inspectedEvidenceIds.add(binding.evidence.evidenceId);
return {
output: {
evidence: context.map((binding) => ({
Expand Down
134 changes: 130 additions & 4 deletions packages/evolution/src/skill-curator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import type {
EvolutionSkillCatalogEntry,
JsonObject,
Lesson,
ProceduralInformationLoss,
ProceduralInformationLossFinding,
ProceduralInformationLossKind,
ScoredProposal,
SkillabilityClassification,
SkillCandidate,
Expand Down Expand Up @@ -48,8 +51,17 @@ const MEMORY_RELATIONSHIPS = new Set<SkillMemoryRelationshipClassification>([
"supersedes",
]);
const OPERATIONS = new Set<SkillCandidateOperation>(["CREATE", "MERGE", "RETIRE", "UPDATE"]);
const PROCEDURAL_INFORMATION_LOSS_KINDS = new Set<ProceduralInformationLossKind>([
"action-order",
"decision-branch",
"failure-signal",
"mitigation-or-recovery",
"tool-or-environment-dependency",
"verification-action",
]);

interface SkillCuratorSchemaReferences {
readonly evidenceRefs: readonly string[];
readonly lessonRefs: readonly string[];
readonly memoryProposalRefs: readonly string[];
readonly skillRefs: readonly string[];
Expand Down Expand Up @@ -80,9 +92,16 @@ interface DeferredFindingDraft {
readonly reason: string;
}

interface ProceduralInformationLossDraft {
readonly lossKey: string;
readonly lessonIds: readonly string[];
readonly loss: ProceduralInformationLoss;
}

interface ParsedSkillCuratorOutput {
readonly candidates: readonly CandidateDraft[];
readonly deferredFindings: readonly DeferredFindingDraft[];
readonly proceduralInformationLosses: readonly ProceduralInformationLossDraft[];
}

interface EvidenceReferences {
Expand Down Expand Up @@ -203,6 +222,20 @@ export function createSkillCuratorOutputSchema(references: SkillCuratorSchemaRef
skillability,
memoryRelationship: relationship,
};
const proceduralInformationLoss = schema(
{
lossKey: { type: "string", minLength: 1 },
lessonRefs: referenceArraySchema(references.lessonRefs, 1),
missing: {
type: "array",
minItems: 1,
items: { type: "string", enum: [...PROCEDURAL_INFORMATION_LOSS_KINDS] },
},
supportingEvidenceRefs: referenceArraySchema(references.evidenceRefs, 1),
explanation: { type: "string", minLength: 1 },
},
["lossKey", "lessonRefs", "missing", "supportingEvidenceRefs", "explanation"],
);
return schema(
{
decisions: {
Expand Down Expand Up @@ -248,8 +281,9 @@ export function createSkillCuratorOutputSchema(references: SkillCuratorSchemaRef
],
},
},
proceduralInformationLosses: { type: "array", items: proceduralInformationLoss },
},
["decisions"],
["decisions", "proceduralInformationLosses"],
);
}

Expand Down Expand Up @@ -447,6 +481,53 @@ function existingSkills(value: unknown, registry: SkillCuratorToolRegistry): rea
return selected;
}

function proceduralInformationLossDraft(
value: unknown,
lessons: EvolutionModelReferenceCatalog<Lesson>,
registry: SkillCuratorToolRegistry,
): ProceduralInformationLossDraft {
if (!isRecord(value)) throw new Error("proceduralInformationLosses entries must be objects");
assertOnlyFields(
value,
["lossKey", "lessonRefs", "missing", "supportingEvidenceRefs", "explanation"],
"proceduralInformationLoss",
);
const lossKey = requiredString(value, "lossKey");
if (!DECISION_KEY_PATTERN.test(lossKey)) throw new Error(`Invalid procedural information loss key "${lossKey}"`);
const selectedLessons = referenceArray(value.lessonRefs, "lessonRefs", lessons, { allowEmpty: false });
const missing = stringArray(value.missing, "missing", false);
for (const item of missing) {
if (!PROCEDURAL_INFORMATION_LOSS_KINDS.has(item as ProceduralInformationLossKind)) {
throw new Error(`Unsupported missing procedural information kind "${item}"`);
}
}
const evidence = referenceArray(value.supportingEvidenceRefs, "supportingEvidenceRefs", registry.evidence, {
allowEmpty: false,
});
const lessonEvidenceIds = new Set(
selectedLessons.flatMap((lesson) => [...lesson.evidenceIds, ...lesson.counterevidenceIds]),
);
for (const binding of evidence) {
if (!lessonEvidenceIds.has(binding.evidence.evidenceId)) {
throw new Error(
`Procedural information loss evidence "${binding.evidence.evidenceId}" is not cited by its lessons`,
);
}
if (!registry.wasEvidenceInspected(binding.evidence.evidenceId)) {
throw new Error(`Procedural information loss evidence "${binding.evidence.evidenceId}" must be inspected`);
}
}
return {
lossKey,
lessonIds: selectedLessons.map((lesson) => lesson.lessonId),
loss: {
missing: missing as readonly ProceduralInformationLossKind[],
supportingEvidenceIds: evidence.map((binding) => binding.evidence.evidenceId),
explanation: requiredString(value, "explanation"),
},
};
}

function candidateDraft(
decisionKey: string,
lessonIds: readonly string[],
Expand Down Expand Up @@ -522,8 +603,11 @@ function parseOutput(
proposals: EvolutionModelReferenceCatalog<ScoredProposal>,
registry: SkillCuratorToolRegistry,
): ParsedSkillCuratorOutput {
assertOnlyFields(output, ["decisions"], "Skill Curator response");
assertOnlyFields(output, ["decisions", "proceduralInformationLosses"], "Skill Curator response");
if (!Array.isArray(output.decisions)) throw new Error("Skill Curator decisions must be an array");
if (!Array.isArray(output.proceduralInformationLosses)) {
throw new Error("Skill Curator proceduralInformationLosses must be an array");
}
const seenKeys = new Set<string>();
const seenCandidateNames = new Set<string>();
const candidates: CandidateDraft[] = [];
Expand Down Expand Up @@ -586,7 +670,16 @@ function parseOutput(
seenCandidateNames.add(normalizedName);
candidates.push(draft);
}
return { candidates, deferredFindings };
const seenLossKeys = new Set<string>();
const proceduralInformationLosses = output.proceduralInformationLosses.map((value) => {
const loss = proceduralInformationLossDraft(value, lessons, registry);
if (seenLossKeys.has(loss.lossKey)) {
throw new Error(`Duplicate procedural information loss key "${loss.lossKey}"`);
}
seenLossKeys.add(loss.lossKey);
return loss;
});
return { candidates, deferredFindings, proceduralInformationLosses };
}

function taskLineageIds(evidence: readonly MaterializedEvidence[]): readonly string[] {
Expand Down Expand Up @@ -749,7 +842,33 @@ function resultsFromDrafts(
...content,
};
});
return { candidates, deferredFindings };
const proceduralInformationLossFindings = parsed.proceduralInformationLosses.map(
(draft): ProceduralInformationLossFinding => {
const content = {
artifactKind: "procedural-information-loss" as const,
findingId: stableId("procedural-information-loss", {
idempotencyKey: input.idempotencyKey,
lossKey: draft.lossKey,
draft,
}),
detectedBy: "skill-curator" as const,
sourceLessonIds: draft.lessonIds,
loss: draft.loss,
};
return {
metadata: createEvolutionArtifactMetadata({
artifactId: content.findingId,
producer,
sourceIds: unique([...content.sourceLessonIds, ...content.loss.supportingEvidenceIds]),
scope: input.input.metadata.scope,
createdAt: input.input.metadata.createdAt,
content,
}),
...content,
};
},
);
return { candidates, deferredFindings, proceduralInformationLossFindings };
}

function modelInput(
Expand Down Expand Up @@ -783,6 +902,12 @@ function modelInput(
workspaceTarget: jsonObject(workspace(input)),
globalEvolvedSkillTargetPermitted: false,
proceduralObservations: [],
reportProceduralInformationLoss: {
when: "A lesson preserves a conclusion but omits material procedural information that cited source evidence contains, preventing or weakening a candidate.",
requireSourceEvidenceInspection: true,
missingKinds: [...PROCEDURAL_INFORMATION_LOSS_KINDS],
doNotReportAbsentSourceInformation: true,
},
...(responseCorrection === null
? {}
: {
Expand Down Expand Up @@ -885,6 +1010,7 @@ export class StructuredSkillCurator implements SkillCurator {
: { maxReferencesPerCall: this.#maxReferencesPerToolCall }),
});
const outputSchema = createSkillCuratorOutputSchema({
evidenceRefs: registry.evidence.references,
lessonRefs: lessonCatalog.references,
memoryProposalRefs: proposalCatalog.references,
skillRefs: registry.skills.references,
Expand Down
Loading
Loading