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
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 41 additions & 2 deletions packages/coding-agent-server/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
import { defineConfig } from "vitest/config";
import { fileURLToPath } from "node:url";
import { defineConfig, mergeConfig } from "vitest/config";
import baseConfig, { workspaceSourcePaths } from "../../vitest.base.ts";

export default defineConfig({ test: { include: ["test/**/*.test.ts"] } });
export default mergeConfig(
baseConfig,
defineConfig({
test: { include: ["test/**/*.test.ts"], testTimeout: 30000 },
resolve: {
alias: [
{
find: /^@earendil-works\/diffex-client\/websocket$/,
replacement: fileURLToPath(new URL("../client/src/websocket.ts", import.meta.url)),
},
{
find: /^@earendil-works\/diffex-client$/,
replacement: fileURLToPath(new URL("../client/src/index.ts", import.meta.url)),
},
{
find: /^@earendil-works\/diffex-coding-agent$/,
replacement: workspaceSourcePaths.codingAgentIndex,
},
{
find: /^@earendil-works\/diffex-protocol$/,
replacement: fileURLToPath(new URL("../protocol/src/index.ts", import.meta.url)),
},
{
find: /^@earendil-works\/diffex-server\/testing$/,
replacement: fileURLToPath(new URL("../server/src/testing/index.ts", import.meta.url)),
},
{
find: /^@earendil-works\/diffex-server\/websocket$/,
replacement: fileURLToPath(new URL("../server/src/transports/websocket/index.ts", import.meta.url)),
},
{
find: /^@earendil-works\/diffex-server$/,
replacement: fileURLToPath(new URL("../server/src/index.ts", import.meta.url)),
},
],
},
}),
);
5 changes: 0 additions & 5 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@

## [Unreleased]

### Breaking Changes

- SDK-provided `Skill` objects now carry stable catalog identity, provenance, captured content, digest, and version fields. Use `createSkill()` to construct custom skills.

### Added

- Added a unified skill catalog shared by interactive selection, prompts, commands, SDK and RPC consumers, and frozen evolution inputs.
- Added `pi auth check` provider/model auth preflight with optional credential output ([#7152](https://github.com/earendil-works/pi/issues/7152)).

### Changed
Expand Down
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 @@ -137,7 +137,7 @@ function validateEvolutionOutput(request: EvolutionModelRequest, output: JsonObj
const issue = [...Errors(schema, output)][0];
return issue?.message ?? "Response does not match the required schema";
}
if (request.stage !== "reflector" || request.validateOutput === undefined) return null;
if (request.validateOutput === undefined) return null;
try {
request.validateOutput(output);
return null;
Expand Down Expand Up @@ -343,7 +343,7 @@ export class CodingAgentEvolutionModelExecutor implements EvolutionModelExecutor
}
if (lastCorrection !== null) {
throw new EvolutionExecutionError(`Evolution model did not submit a valid response: ${lastCorrection}`, {
reasonCode: "reflection-invalid-response",
reasonCode: request.stage === "curator" ? "curation-invalid-response" : "reflection-invalid-response",
retryable: false,
diagnostics: { lastCorrection },
});
Expand Down
1 change: 0 additions & 1 deletion packages/coding-agent/test/evolution-menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,6 @@ describe("EvolutionMenuComponent", () => {
reason: "it is imprecise",
},
addition: {
destination: { section: "Verification", anchor: null },
content: "Use precise verification guidance.",
intendedScope: ["repository changes"],
},
Expand Down
110 changes: 110 additions & 0 deletions packages/coding-agent/test/evolution-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,116 @@ describe("coding-agent evolution runtime", () => {
expect(validations).toBe(1);
});

it("corrects curator semantic errors within the active transcript", async () => {
const harness = await createHarness();
harnesses.push(harness);
harness.setResponses([
fauxAssistantMessage(fauxToolCall("submit_evolution_response", { decisions: ["invalid"] }), {
stopReason: "toolUse",
}),
(context) => {
expect(context.messages).toHaveLength(3);
expect(context.messages[2]).toMatchObject({
role: "toolResult",
toolName: "submit_evolution_response",
isError: true,
});
const correctionMessage = context.messages[2];
const correction =
correctionMessage?.role === "toolResult"
? correctionMessage.content
.filter((part): part is { type: "text"; text: string } => part.type === "text")
.map((part) => part.text)
.join("\n")
: "";
expect(correction).toContain("decisions[0].proposal.exactDelta");
expect(correction).toContain("decisions[1].relationship");
return fauxAssistantMessage(fauxToolCall("submit_evolution_response", { decisions: [] }), {
stopReason: "toolUse",
});
},
]);
const executor = new CodingAgentEvolutionModelExecutor(harness.session, harness.session.modelRuntime, {
async waitUntilBackgroundAllowed() {},
});
const model = harness.getModel();

await expect(
executor.execute({
stage: "curator",
model: {
provider: model.provider,
model: model.id,
configurationDigest: `sha256:${"1".repeat(64)}`,
},
promptVersion: "curator-v1",
idempotencyKey: "curator-semantic-correction",
input: {},
outputSchema: {
type: "object",
additionalProperties: false,
required: ["decisions"],
properties: { decisions: { type: "array" } },
},
tools: [],
validateOutput(output) {
if (Array.isArray(output.decisions) && output.decisions.length === 0) return;
throw new Error(
'Decision "first" at decisions[0].proposal.exactDelta: invalid delta\n' +
'Decision "second" at decisions[1].relationship: invalid relationship',
);
},
}),
).resolves.toMatchObject({ output: { decisions: [] }, submissionMethod: "response-tool" });
});

it.each([
["reflector", "response-tool", "reflection-invalid-response"],
["reflector", "json-text", "reflection-invalid-response"],
["curator", "response-tool", "curation-invalid-response"],
["curator", "json-text", "curation-invalid-response"],
] as const)("classifies exhausted %s %s corrections", async (stage, submissionMethod, expectedReasonCode) => {
const harness = await createHarness();
harnesses.push(harness);
const invalidResponse =
submissionMethod === "response-tool"
? fauxAssistantMessage(fauxToolCall("submit_evolution_response", { complete: false }), {
stopReason: "toolUse",
})
: fauxAssistantMessage(JSON.stringify({ complete: false }));
harness.setResponses(Array.from({ length: 10 }, () => invalidResponse));
const executor = new CodingAgentEvolutionModelExecutor(harness.session, harness.session.modelRuntime, {
async waitUntilBackgroundAllowed() {},
});
const model = harness.getModel();

await expect(
executor.execute({
stage,
model: {
provider: model.provider,
model: model.id,
configurationDigest: `sha256:${"1".repeat(64)}`,
},
promptVersion: `${stage}-v1`,
idempotencyKey: `${stage}-${submissionMethod}-exhaustion`,
input: {},
outputSchema: {
type: "object",
additionalProperties: false,
required: ["complete"],
properties: { complete: { enum: [true] } },
},
tools: [],
}),
).rejects.toMatchObject({
name: "EvolutionExecutionError",
reasonCode: expectedReasonCode,
retryable: false,
diagnostics: { lastCorrection: expect.any(String) },
});
});

it("isolates transcripts between node executor invocations", async () => {
const harness = await createHarness();
harnesses.push(harness);
Expand Down
1 change: 1 addition & 0 deletions packages/evolution/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@types/node": "24.12.4",
"@types/proper-lockfile": "4.1.4",
"shx": "0.4.0",
"typebox": "1.3.7",
"vitest": "4.1.9"
}
}
3 changes: 2 additions & 1 deletion packages/evolution/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ export interface EvolutionProposalInstructionRelationship {

export interface EvolutionMemoryAddDelta {
readonly operation: "ADD";
/** A section must name one existing heading. Both fields null append after the final memory item. */
readonly destination: {
readonly section: string | null;
readonly anchor: string | null;
Expand All @@ -504,7 +505,7 @@ export interface EvolutionMemoryRemoveDelta {
export interface EvolutionMemoryReplaceDelta {
readonly operation: "REPLACE";
readonly removal: Omit<EvolutionMemoryRemoveDelta, "operation">;
readonly addition: Omit<EvolutionMemoryAddDelta, "operation">;
readonly addition: Pick<EvolutionMemoryAddDelta, "content" | "intendedScope">;
}

export type EvolutionMemoryDelta = EvolutionMemoryAddDelta | EvolutionMemoryRemoveDelta | EvolutionMemoryReplaceDelta;
Expand Down
Loading