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
11 changes: 10 additions & 1 deletion packages/coding-agent/src/core/evolution-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
PollingEvolutionWorkSource,
RecursiveReflector,
StructuredDeltaCurator,
StructuredSkillCurator,
type SuccessfulMaterialization,
selectEvolutionEvidence,
} from "#evolution";
Expand Down Expand Up @@ -341,7 +342,12 @@ export class CodingAgentEvolutionModelExecutor implements EvolutionModelExecutor
}
if (lastCorrection !== null) {
throw new EvolutionExecutionError(`Evolution model did not submit a valid response: ${lastCorrection}`, {
reasonCode: request.stage === "curator" ? "curation-invalid-response" : "reflection-invalid-response",
reasonCode:
request.stage === "curator"
? "curation-invalid-response"
: request.stage === "skill-curator"
? "skill-curation-invalid-response"
: "reflection-invalid-response",
retryable: false,
diagnostics: { lastCorrection },
});
Expand Down Expand Up @@ -756,6 +762,7 @@ class CodingAgentSnapshotSource implements EvolutionSnapshotSource {
promptVersions: {
reflector: "reflector-v2",
curator: "curator-v1",
skillCurator: "skill-curator-v1",
validator: "validator-v1",
writer: "writer-v1",
},
Expand Down Expand Up @@ -803,12 +810,14 @@ export async function createCodingAgentEvolutionRuntime(
const model = new CodingAgentEvolutionModelExecutor(options.session, options.modelRuntime, priority);
const reflector = new RecursiveReflector({ version: VERSION, artifacts: persistence, ledger: persistence });
const curator = new StructuredDeltaCurator({ version: VERSION, ledger: persistence });
const skillCurator = new StructuredSkillCurator({ version: VERSION, artifacts: persistence, ledger: persistence });
const ranker = new DeterministicProposalRanker({ version: VERSION });
const pipeline = new EvolutionPipeline({
version: VERSION,
model,
reflector,
curator,
skillCurator,
validator: ranker,
scorer: ranker,
targets,
Expand Down
15 changes: 14 additions & 1 deletion packages/coding-agent/test/evolution-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,8 @@ describe("coding-agent evolution runtime", () => {
["reflector", "json-text", "reflection-invalid-response"],
["curator", "response-tool", "curation-invalid-response"],
["curator", "json-text", "curation-invalid-response"],
["skill-curator", "response-tool", "skill-curation-invalid-response"],
["skill-curator", "json-text", "skill-curation-invalid-response"],
] as const)("classifies exhausted %s %s corrections", async (stage, submissionMethod, expectedReasonCode) => {
const harness = await createHarness();
harnesses.push(harness);
Expand Down Expand Up @@ -1052,6 +1054,17 @@ describe("coding-agent evolution runtime", () => {
stopReason: "toolUse",
});
},
(context) => {
const prompt = String(context.messages[0]?.content ?? "");
prompts.push(prompt);
expect(prompt).toContain(
'"objective":"classify-operational-patterns-and-produce-non-executable-skill-candidates"',
);
expect(prompt).not.toContain(traceId);
return fauxAssistantMessage(fauxToolCall("submit_evolution_response", { decisions: [] }), {
stopReason: "toolUse",
});
},
]);
const runtime = await createCodingAgentEvolutionRuntime({
agentDir,
Expand All @@ -1072,7 +1085,7 @@ describe("coding-agent evolution runtime", () => {
},
{ timeout: 5_000, interval: 20 },
);
expect(prompts).toHaveLength(2);
expect(prompts).toHaveLength(3);
expect(existsSync(join(agentDir, "evolution-failures", started.job.jobId))).toBe(false);
} finally {
await runtime.stop();
Expand Down
112 changes: 112 additions & 0 deletions packages/evolution/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export interface EvolutionTokenMeasurement {
export interface EvolutionPromptVersions {
readonly reflector: string;
readonly curator: string;
readonly skillCurator: string;
readonly validator: string;
readonly writer: string;
}
Expand Down Expand Up @@ -420,6 +421,102 @@ export interface Lesson {
readonly actionableTakeaway: string;
}

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

export type SkillabilityClassification =
| "conditional-procedure"
| "ordered-workflow"
| "reusable-operational-guideline"
| "setup-tool-sequence"
| "troubleshooting-recovery"
| "verification-routine";

export type SkillMemoryRelationshipClassification =
| "complements"
| "conflicts"
| "fallback-until-activation"
| "independent"
| "redundant"
| "supersedes";

export interface SkillCandidateWorkspaceScope {
readonly repositoryId: string;
readonly directoryId?: string;
readonly rootPath: string;
readonly repositoryRevision: string;
readonly evolutionScope: EvolutionScope;
}

export interface SkillCandidateMemoryRelationship {
readonly classification: SkillMemoryRelationshipClassification;
readonly memoryProposalIds: readonly string[];
readonly rationale: string;
}

export interface SkillCandidateProcedureStep {
readonly sequence: number;
readonly action: string;
readonly condition: string | null;
}

export interface SkillCandidateFailureHandling {
readonly signal: string;
readonly mitigation: string;
readonly recovery: string;
}

export interface SkillCandidate {
readonly artifactKind: "skill-candidate";
readonly metadata: EvolutionArtifactMetadata;
readonly candidateId: string;
readonly operation: SkillCandidateOperation;
readonly workspace: SkillCandidateWorkspaceScope;
readonly proposedName: string;
readonly existingSkillIds: readonly string[];
readonly sourceLessonIds: readonly string[];
readonly sourceProceduralObservationIds: readonly string[];
readonly supportingEvidenceIds: readonly string[];
readonly contradictingEvidenceIds: readonly string[];
readonly supportingTraceIds: readonly string[];
readonly contradictingTraceIds: readonly string[];
readonly taskLineageIds: readonly string[];
readonly skillability: {
readonly classification: SkillabilityClassification;
readonly rationale: string;
};
readonly trigger: {
readonly description: string;
readonly positiveExamples: readonly string[];
readonly negativeExamples: readonly string[];
};
readonly preconditions: readonly string[];
readonly procedure: {
readonly steps: readonly SkillCandidateProcedureStep[];
readonly requiredTools: readonly string[];
readonly environment: readonly string[];
};
readonly failureHandling: readonly SkillCandidateFailureHandling[];
readonly applicabilityBoundaries: readonly string[];
readonly verificationContract: {
readonly checks: readonly string[];
readonly completionCondition: string;
};
readonly memoryRelationship: SkillCandidateMemoryRelationship;
}

export interface DeferredSkillCandidateFinding {
readonly artifactKind: "skill-candidate-finding";
readonly metadata: EvolutionArtifactMetadata;
readonly findingId: string;
readonly disposition: "deferred";
readonly workspace: SkillCandidateWorkspaceScope;
readonly sourceLessonIds: readonly string[];
readonly existingSkillIds: readonly string[];
readonly skillability: SkillCandidate["skillability"];
readonly memoryRelationship: SkillCandidateMemoryRelationship;
readonly reason: string;
}

export interface EvolutionMemoryTarget {
readonly kind: "evolution-memory";
readonly expectedHarnessVersion: HarnessVersion;
Expand Down Expand Up @@ -672,6 +769,7 @@ export type EvolutionJobStage =
| "reflecting"
| "curating"
| "validating"
| "skill-curating"
| "writing"
| "activating"
| "completed";
Expand All @@ -694,6 +792,7 @@ export type EvolutionFailureReasonCode =
| "model-call-failed"
| "pipeline-invariant"
| "curation-invalid-response"
| "skill-curation-invalid-response"
| "reflection-budget-exhausted"
| "reflection-invalid-response"
| "shutdown"
Expand Down Expand Up @@ -882,6 +981,18 @@ export interface CuratorToolCallLedgerEvent extends EvolutionLedgerEventBase {
readonly denialCode: string | null;
}

export interface SkillCuratorToolCallLedgerEvent extends EvolutionLedgerEventBase {
readonly type: "skill-curator-tool-call";
readonly toolCallId: string;
readonly toolName: "inspect_skills" | "inspect_source_evidence";
readonly requestDigest: string;
readonly requestedReferences: readonly string[];
readonly resolvedEvidenceIds: readonly string[];
readonly resolvedSkillIds: readonly string[];
readonly resultDigest: string | null;
readonly denialCode: string | null;
}

export interface ReflectorModelCallLedgerEvent extends EvolutionLedgerEventBase {
readonly type: "reflector-model-call";
readonly modelCallId: string;
Expand Down Expand Up @@ -922,5 +1033,6 @@ export type EvolutionLedgerEvent =
| ReflectionLeafRetryPendingLedgerEvent
| ReflectionNodeCheckpointedLedgerEvent
| CuratorToolCallLedgerEvent
| SkillCuratorToolCallLedgerEvent
| ReflectorModelCallLedgerEvent
| ReflectorToolCallLedgerEvent;
3 changes: 2 additions & 1 deletion packages/evolution/src/control-plane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ const NEXT_STAGE: Readonly<Record<ActiveEvolutionStage, EvolutionJobStage>> = {
"counter-slot": "reflecting",
reflecting: "curating",
curating: "validating",
validating: "writing",
validating: "skill-curating",
"skill-curating": "writing",
writing: "activating",
activating: "completed",
};
Expand Down
1 change: 1 addition & 0 deletions packages/evolution/src/filesystem-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const JOB_STAGES = new Set([
"reflecting",
"curating",
"validating",
"skill-curating",
"writing",
"activating",
"completed",
Expand Down
26 changes: 26 additions & 0 deletions packages/evolution/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export {
type CounterSignal,
type CounterSignalSource,
type CuratorToolCallLedgerEvent,
type DeferredSkillCandidateFinding,
EVOLUTION_MEMORY_ITEM_SCHEMA_VERSION,
EVOLUTION_SCHEMA_VERSION,
type EvidenceCorpusDiagnostic,
Expand Down Expand Up @@ -116,6 +117,15 @@ export {
type ReflectorToolCallLedgerEvent,
type ScoredProposal,
type ScoredProposalDisposition,
type SkillabilityClassification,
type SkillCandidate,
type SkillCandidateFailureHandling,
type SkillCandidateMemoryRelationship,
type SkillCandidateOperation,
type SkillCandidateProcedureStep,
type SkillCandidateWorkspaceScope,
type SkillCuratorToolCallLedgerEvent,
type SkillMemoryRelationshipClassification,
type SkillTarget,
type ValidationIssue,
type ValidationReport,
Expand Down Expand Up @@ -217,6 +227,9 @@ export type {
ReflectionResult,
Reflector,
ReflectorInput,
SkillCurationResult,
SkillCurator,
SkillCuratorInput,
SuccessfulMaterialization,
} from "./ports.ts";
export {
Expand Down Expand Up @@ -250,6 +263,19 @@ export {
ReflectorToolRegistry,
type ReflectorToolRegistryOptions,
} from "./reflector-tools.ts";
export {
createSkillCuratorOutputSchema,
StructuredSkillCurator,
type StructuredSkillCuratorDependencies,
type StructuredSkillCuratorOptions,
} from "./skill-curator.ts";
export {
type SkillCuratorEvidenceBinding,
type SkillCuratorRecoverableToolDenial,
type SkillCuratorToolAudit,
SkillCuratorToolRegistry,
type SkillCuratorToolRegistryOptions,
} from "./skill-curator-tools.ts";
export {
EvolutionTargetAdapterRegistry,
type EvolutionTargetAdapterRegistryOptions,
Expand Down
12 changes: 11 additions & 1 deletion packages/evolution/src/model-references.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import type { EvolutionReferenceNormalization, JsonObject } from "./contracts.ts";

export type EvolutionModelReferenceKind = "document" | "evidence" | "instruction" | "lesson" | "memory" | "slice";
export type EvolutionModelReferenceKind =
| "document"
| "evidence"
| "instruction"
| "lesson"
| "memory"
| "proposal"
| "skill"
| "slice";

const REFERENCE_PREFIXES: Readonly<Record<EvolutionModelReferenceKind, string>> = {
document: "D",
evidence: "E",
instruction: "I",
lesson: "L",
memory: "M",
proposal: "P",
skill: "K",
slice: "S",
};

Expand Down
48 changes: 47 additions & 1 deletion packages/evolution/src/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import type {
ProposalValidator,
ReflectionResult,
Reflector,
SkillCurator,
} from "./ports.ts";
import { computeEvolutionContentDigest, createEvolutionArtifactMetadata } from "./provenance.ts";
import type { EvolutionTargetAdapterRegistry } from "./targets.ts";
Expand All @@ -49,6 +50,7 @@ export interface EvolutionPipelineDependencies {
readonly model: EvolutionModelExecutor;
readonly reflector: Reflector;
readonly curator: Curator;
readonly skillCurator: SkillCurator;
readonly validator: ProposalValidator;
readonly scorer: ProposalScorer;
readonly targets: EvolutionTargetAdapterRegistry;
Expand Down Expand Up @@ -665,7 +667,51 @@ export class EvolutionPipeline implements EvolutionStageExecutor {
acceptedProposalIds,
selectedProposalIds,
},
directive: selectedProposalIds.length === 0 ? "complete-no-op" : "continue",
directive: "continue",
};
}
case "skill-curating": {
const { corpus, reflection } = this.#pipelineInputs(context);
if (reflection === null) {
throw new EvolutionExecutionError("Evidence coverage is missing from reflection output", {
reasonCode: "artifact-corrupt",
retryable: false,
});
}
const selectedMemoryProposals = context.priorArtifacts
.filter(isScoredProposal)
.filter((item) => item.disposition === "selected");
let curation: Awaited<ReturnType<SkillCurator["curate"]>>;
try {
curation = await this.#dependencies.skillCurator.curate({
jobId: context.job.jobId,
input: context.input,
corpus,
reflection,
selectedMemoryProposals,
model: this.#dependencies.model,
idempotencyKey: context.idempotencyKey,
signal: context.signal,
});
} catch (error) {
stageFailure(error, "Skill Curator model stage failed", "model-call-failed", true);
}
assertUniqueIds(
curation.candidates.map((candidate) => candidate.candidateId),
"skill candidate ID",
);
assertUniqueIds(
curation.deferredFindings.map((finding) => finding.findingId),
"deferred skill finding ID",
);
return {
artifacts: [...curation.candidates, ...curation.deferredFindings],
state: {
candidateIds: curation.candidates.map((candidate) => candidate.candidateId),
deferredFindingIds: curation.deferredFindings.map((finding) => finding.findingId),
selectedMemoryProposalIds: selectedMemoryProposals.map((scored) => scored.proposal.proposalId),
},
directive: selectedMemoryProposals.length === 0 ? "complete-no-op" : "continue",
};
}
case "writing": {
Expand Down
Loading
Loading