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
5 changes: 3 additions & 2 deletions packages/coding-agent/test/suite/subagent-startup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,14 @@ describe("asynchronous subagent startup", () => {
gates.get("second")!.resolve(fauxAssistantMessage("second done"));
await waitForStatus(controller, "second", "idle");
await providerStarts.get("third")!.promise;
expect(started).toEqual(["first", "second", "third"]);
expect(started.slice(0, 2).sort()).toEqual(["first", "second"]);
expect(started[2]).toBe("third");
expect(controller.snapshot().agents.find((agent) => agent.name === "fourth")?.status).toBe("queued");

gates.get("first")!.resolve(fauxAssistantMessage("first done"));
await waitForStatus(controller, "first", "idle");
await providerStarts.get("fourth")!.promise;
expect(started).toEqual(["first", "second", "third", "fourth"]);
expect(started[3]).toBe("fourth");
} finally {
for (const [name, gate] of gates) gate.resolve(fauxAssistantMessage(`${name} done`));
}
Expand Down
31 changes: 27 additions & 4 deletions packages/evolution/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,21 +488,42 @@ export interface ValidationIssue {

export interface ProposalValidationResult {
readonly proposalId: string;
readonly disposition: "accepted" | "deferred" | "rejected";
readonly disposition: "accepted" | "deferred" | "quarantined" | "rejected";
readonly reasonCodes: readonly string[];
readonly issues: readonly ValidationIssue[];
}

export interface ValidationReport {
readonly metadata: EvolutionArtifactMetadata;
readonly validatorVersion: string;
readonly assurance: "provenance-recurrence-and-structure-only";
readonly proposalResults: readonly ProposalValidationResult[];
}

export type ScoredProposalDisposition = "deferred" | "rejected" | "selected" | "superseded";
export interface EvidenceRankingScoreComponents {
readonly recurrence: number;
readonly provenanceIntegrity: number;
readonly sourceSignalStrength: number;
readonly scopeCoverage: number;
readonly curatorConfidence: number;
readonly proposalCompleteness: number;
readonly contradictionPenalty: number;
}

export type ScoredProposalDisposition =
| "deferred"
| "quarantined"
| "rejected"
| "selected"
| "superseded"
| "unselected";

export interface ScoredProposal {
readonly metadata: EvolutionArtifactMetadata;
readonly proposal: EvolutionProposal;
readonly score: number | null;
readonly scorerVersion: string;
readonly evidenceRankingScore: number | null;
readonly scoreComponents: EvidenceRankingScoreComponents | null;
readonly disposition: ScoredProposalDisposition;
readonly reasonCodes: readonly string[];
}
Expand Down Expand Up @@ -658,9 +679,11 @@ export type EvolutionProposalLedgerDisposition =
| "applied"
| "deferred"
| "failed"
| "quarantined"
| "rejected"
| "rolled-back"
| "superseded";
| "superseded"
| "unselected";

interface EvolutionLedgerEventBase {
readonly metadata: EvolutionArtifactMetadata;
Expand Down
8 changes: 8 additions & 0 deletions packages/evolution/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {
type EvidenceCoverageDisposition,
type EvidenceCoverageEntry,
type EvidenceCoverageMap,
type EvidenceRankingScoreComponents,
type EvidenceSemanticUnit,
type EvidenceSemanticUnitKind,
type EvidenceSlice,
Expand Down Expand Up @@ -168,6 +169,13 @@ export type {
ReflectorInput,
SuccessfulMaterialization,
} from "./ports.ts";
export {
DeterministicProposalRanker,
type DeterministicProposalRankerDependencies,
type DeterministicProposalRankerOptions,
EVIDENCE_RANKING_ASSURANCE,
EVIDENCE_RANKING_SCORER_VERSION,
} from "./proposal-ranking.ts";
export {
type CreateEvolutionArtifactMetadataOptions,
computeEvolutionContentDigest,
Expand Down
33 changes: 18 additions & 15 deletions packages/evolution/src/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function isProposal(artifact: EvolutionArtifact): artifact is EvolutionProposal
}

function isScoredProposal(artifact: EvolutionArtifact): artifact is ScoredProposal {
return "proposal" in artifact && "score" in artifact;
return "proposal" in artifact && "evidenceRankingScore" in artifact;
}

function isMutationBatch(artifact: EvolutionArtifact): artifact is MutationBatch {
Expand Down Expand Up @@ -165,6 +165,12 @@ function assertScoredProposals(
retryable: false,
});
}
if (scoredProposals.filter((scored) => scored.disposition === "selected").length > 5) {
throw new EvolutionExecutionError("Scorer selected more than five proposal groups", {
reasonCode: "pipeline-invariant",
retryable: false,
});
}
}

function assertValidationReport(
Expand All @@ -186,15 +192,18 @@ function assertValidationReport(
retryable: false,
});
}
const accepted = new Set(
validation.proposalResults
.filter((result) => result.disposition === "accepted")
.map((result) => result.proposalId),
);
const validationById = new Map(validation.proposalResults.map((result) => [result.proposalId, result]));
if (
scoredProposals.some((scored) => scored.disposition === "selected" && !accepted.has(scored.proposal.proposalId))
scoredProposals.some((scored) => {
const validationResult = validationById.get(scored.proposal.proposalId);
if (validationResult === undefined) return true;
if (validationResult.disposition === "accepted") {
return !["selected", "superseded", "unselected"].includes(scored.disposition);
}
return scored.disposition !== validationResult.disposition;
})
) {
throw new EvolutionExecutionError("Scorer selected a proposal that deterministic validation did not accept", {
throw new EvolutionExecutionError("Scorer disposition does not match deterministic validation", {
reasonCode: "pipeline-invariant",
retryable: false,
});
Expand Down Expand Up @@ -543,12 +552,6 @@ export class EvolutionPipeline implements EvolutionStageExecutor {
const selectedProposalIds = scoredProposals
.filter((scored) => scored.disposition === "selected")
.map((scored) => scored.proposal.proposalId);
if (acceptedProposalIds.length > 0 && selectedProposalIds.length === 0) {
throw new EvolutionExecutionError("Accepted proposals produced no writable selection", {
reasonCode: "validation-rejected",
retryable: false,
});
}
const ledgerEvents = scoredProposals.flatMap((scored): EvolutionLedgerEvent[] => {
const disposition: EvolutionProposalLedgerDisposition | null =
scored.disposition === "selected" ? null : scored.disposition;
Expand All @@ -563,7 +566,7 @@ export class EvolutionPipeline implements EvolutionStageExecutor {
acceptedProposalIds,
selectedProposalIds,
},
directive: acceptedProposalIds.length === 0 ? "complete-no-op" : "continue",
directive: selectedProposalIds.length === 0 ? "complete-no-op" : "continue",
};
}
case "writing": {
Expand Down
Loading