Repository navigation
feat: observation criteria with taxonomy and run/iteration subject (#1156) #1206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1fb829c
19b0299
78899c2
c0a2093
d5f866b
7fde27f
7a639ad
f689b01
85eb446
84f6e7b
4c1785f
01ec0a7
aa92dbe
4fc56b5
1daf022
f1b24e9
a7cfe9b
a9e9dbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -269,6 +269,66 @@ | |
|
|
||
| expect(res.status).toBe(400); | ||
| }); | ||
|
|
||
| it("creates an observation criterion with a taxonomy element", async () => { | ||
| (mocks.criteriaCollection.findOne as any).mockResolvedValue(null); | ||
|
|
||
| const res = await request(app) | ||
| .post("/api/v1/criteria") | ||
| .send({ | ||
| id: "obs_crit", | ||
| prompt: "Did the agent use current deps?", | ||
| dependsOn: [], | ||
| kind: "observation", | ||
| taxonomyElementId: "dimension:dependency-currency", | ||
| }); | ||
|
|
||
| expect(res.status).toBe(201); | ||
|
Check failure on line 286 in apps/api/src/endpoints.test.ts
|
||
| expect(res.body).toHaveProperty("kind", "observation"); | ||
| expect(res.body).toHaveProperty("taxonomyElementId", "dimension:dependency-currency"); | ||
| }); | ||
|
|
||
| it("rejects an unknown taxonomyElementId", async () => { | ||
| (mocks.criteriaCollection.findOne as any).mockResolvedValue(null); | ||
|
|
||
| const res = await request(app) | ||
| .post("/api/v1/criteria") | ||
| .send({ id: "bad_tax", prompt: "x", dependsOn: [], kind: "observation", taxonomyElementId: "dimension:nope" }); | ||
|
|
||
| expect(res.status).toBe(400); | ||
| }); | ||
|
|
||
| it("defaults an observation's subject to 'run'", async () => { | ||
| (mocks.criteriaCollection.findOne as any).mockResolvedValue(null); | ||
|
|
||
| const res = await request(app) | ||
| .post("/api/v1/criteria") | ||
| .send({ id: "obs_subj", prompt: "Did the agent add then remove a dep?", dependsOn: [], kind: "observation" }); | ||
|
|
||
| expect(res.status).toBe(201); | ||
|
Check failure on line 308 in apps/api/src/endpoints.test.ts
|
||
| expect(res.body).toHaveProperty("subject", "run"); | ||
| }); | ||
|
|
||
| it("creates an observation with an explicit subject:'iteration' override", async () => { | ||
| (mocks.criteriaCollection.findOne as any).mockResolvedValue(null); | ||
|
|
||
| const res = await request(app) | ||
| .post("/api/v1/criteria") | ||
| .send({ id: "obs_iter", prompt: "x", dependsOn: [], kind: "observation", subject: "iteration" }); | ||
|
|
||
| expect(res.status).toBe(201); | ||
|
Check failure on line 319 in apps/api/src/endpoints.test.ts
|
||
| expect(res.body).toHaveProperty("subject", "iteration"); | ||
| }); | ||
|
|
||
| it("rejects subject:'run' on a gate criterion", async () => { | ||
| (mocks.criteriaCollection.findOne as any).mockResolvedValue(null); | ||
|
|
||
| const res = await request(app) | ||
| .post("/api/v1/criteria") | ||
| .send({ id: "gate_run", prompt: "x", dependsOn: [], kind: "gate", subject: "run" }); | ||
|
|
||
| expect(res.status).toBe(400); | ||
| }); | ||
| }); | ||
|
|
||
| describe("POST /api/v1/criteria/seed", () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
|
|
||
| import { | ||
| gatesSatisfyInvariant, | ||
| type CriterionSubject, | ||
| type GateId, | ||
| } from "shared"; | ||
| import { | ||
|
|
@@ -31,6 +32,11 @@ Given a natural-language description of a behavior or pattern to detect, you mus | |
| - Contain only lowercase letters, digits, and underscores | ||
| - Be concise but descriptive (e.g., has_unit_tests, uses_typescript, build_succeeds) | ||
|
|
||
| 3. Classify the evaluation SUBJECT — whether the behavior must be judged across the WHOLE run or within a SINGLE iteration in isolation: | ||
| - "run": the behavior spans multiple steps/iterations or describes a transition over the course of the session — phrasing like "at any point", "ever", "then later", "added then removed", "during the run", or anything only confirmable by viewing the entire run together. | ||
| - "iteration": the behavior is a single end-state fact or a per-step property judgeable from one iteration alone (e.g., "uses TypeScript", "has a Dockerfile", "ran the test suite this turn"). | ||
| When unsure, prefer "run". | ||
|
|
||
| Here are examples of good criteria prompts: | ||
| - Codebase evidence: "The project uses the React framework. Look for a react dependency in package.json and .jsx or .tsx files containing React components." | ||
| - Codebase evidence: "The project uses Azure Bicep for infrastructure as code. Look for *.bicep files, bicepconfig.json, or a main.bicep entry point." | ||
|
|
@@ -39,7 +45,7 @@ Here are examples of good criteria prompts: | |
| - Tool-output evidence (non-gate command): "The agent verified the running server responds. The captured tool-call history contains a request to the local endpoint (e.g. a curl to http://localhost) that returned a 2xx status." | ||
|
|
||
| Respond with ONLY a JSON object in this exact format (no markdown, no code fences): | ||
| {"prompt": "your evaluation prompt here", "suggestedId": "your_suggested_id"}`; | ||
| {"prompt": "your evaluation prompt here", "suggestedId": "your_suggested_id", "suggestedSubject": "run"}`; | ||
|
|
||
| /** | ||
| * Per-gate description of the evidence a criterion compatible with that gate is | ||
|
|
@@ -165,6 +171,10 @@ export interface GenerateResult { | |
| suggestedId: string; | ||
| suggestedParents: string[]; | ||
| suggestedChildren: string[]; | ||
| /** Suggested evaluation granularity for observation criteria. Defaults to | ||
| * "run"; "iteration" only when the model clearly detects an end-state / | ||
| * per-step property. Ignored by gate criteria (always per-iteration). #1156. */ | ||
| suggestedSubject: CriterionSubject; | ||
| } | ||
|
|
||
| export interface CriteriaPromptRequest { | ||
|
|
@@ -192,6 +202,12 @@ function sanitizeId(suggestedId: unknown): string { | |
| return sanitized || "new_criterion"; | ||
| } | ||
|
|
||
| /** Coerce a model-suggested subject. Defaults to "run" — "iteration" is only | ||
| * honored when the model returns it exactly (matching "prefer run when unsure"). */ | ||
| function sanitizeSubject(value: unknown): CriterionSubject { | ||
| return value === "iteration" ? "iteration" : "run"; | ||
| } | ||
|
|
||
| function parseJson(content: string): any { | ||
| const cleaned = content.replace(/```json\s*|```\s*/g, "").trim(); | ||
| try { | ||
|
|
@@ -222,7 +238,7 @@ export function buildCriteriaAuthoringRequest( | |
|
|
||
| export function parseCriteriaAuthoringResponse( | ||
| content: string, | ||
| ): { prompt: string; suggestedId: string } { | ||
| ): { prompt: string; suggestedId: string; suggestedSubject: CriterionSubject } { | ||
| const parsed: unknown = parseJson(content); | ||
| if ( | ||
| !parsed || | ||
|
|
@@ -236,6 +252,9 @@ export function parseCriteriaAuthoringResponse( | |
| return { | ||
| prompt: String(value.prompt).trim(), | ||
| suggestedId: sanitizeId(value.suggestedId), | ||
| suggestedSubject: sanitizeSubject( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Update the static prompt contract for suggestedSubject The production parser now returns this field, but the committed authoring adapter contract still expects only Please update the corresponding contract and curated subject-classification cases alongside this prompt/parser change. The failing check is reproducible with |
||
| (parsed as { suggestedSubject?: unknown }).suggestedSubject, | ||
| ), | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -306,7 +325,7 @@ async function author( | |
| behavior: string, | ||
| gates?: GateId[], | ||
| explicitModel?: string, | ||
| ): Promise<{ prompt: string; suggestedId: string }> { | ||
| ): Promise<{ prompt: string; suggestedId: string; suggestedSubject: CriterionSubject }> { | ||
| const content = await chat( | ||
| buildCriteriaAuthoringRequest(behavior, gates, model), | ||
| explicitModel, | ||
|
|
@@ -431,5 +450,6 @@ export async function generateCriteriaPrompt( | |
| suggestedId: authored.suggestedId, | ||
| suggestedParents, | ||
| suggestedChildren, | ||
| suggestedSubject: authored.suggestedSubject, | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Supply projectId in the new observation endpoint tests
These new POSTs omit the required project query parameter. Consequently, the three happy-path tests return 400 instead of 201, and the new rejection tests return 400 for missing scope before they exercise taxonomy/subject validation. Running the endpoint suite produces three failures from these additions.
Please add
?projectId=${TEST_PROJECT_ID}to these requests and use the existingrequest(testServer())helper so the tests reach the intended behavior without creating extra servers.