Skip to content

Commit 1af57f6

Browse files
committed
Merge branch 'main' into posthog-code/web-host-cloud-tasks-auth
2 parents 2483a98 + eea5948 commit 1af57f6

92 files changed

Lines changed: 6592 additions & 940 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pr-approval-agent.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ jobs:
6767
ANTHROPIC_API_KEY: ${{ secrets.STAMPHOG_ANTHROPIC_API_KEY }}
6868
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_TOKEN }}
6969
GH_TOKEN: ${{ steps.app-token.outputs.token }}
70+
# Set both to route through the ai-gateway (gateway.py overrides
71+
# ANTHROPIC_* at runtime). Unset means direct Anthropic.
72+
AI_GATEWAY_URL: ${{ secrets.STAMPHOG_AI_GATEWAY_URL }}
73+
AI_GATEWAY_API_KEY: ${{ secrets.STAMPHOG_AI_GATEWAY_API_KEY }}
7074
run: |
7175
uv run tools/pr-approval-agent/review_pr.py \
7276
${{ github.event.pull_request.number }} \

packages/core/src/autoresearch/autoresearch.test.ts

Lines changed: 161 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ function reportText(value: number, summary = "tweak"): string {
142142
return `Done.\n\`\`\`autoresearch\nmetric: ${value}\nsummary: ${summary}\n\`\`\``;
143143
}
144144

145+
function researchText(
146+
summary = "Mapped the execution path",
147+
finding = "The metric is computed in the workspace server.",
148+
nextStep = "Trace the benchmark command",
149+
): string {
150+
return `Investigated.\n\`\`\`autoresearch\ntype: research\nsummary: ${summary}\nfinding: ${finding}\nnext: ${nextStep}\n\`\`\``;
151+
}
152+
145153
/** A session `model` config option (and optional `thought_level`). */
146154
function modelConfig(
147155
currentValue: string,
@@ -202,6 +210,14 @@ function completeTurn(text: string, taskRunId = TASK_RUN_ID): void {
202210
});
203211
}
204212

213+
function streamTurnText(text: string, taskRunId = TASK_RUN_ID): void {
214+
const events = sessionStore.getState().sessions[taskRunId]?.events ?? [];
215+
sessionStoreSetters.updateSession(taskRunId, {
216+
isPromptPending: true,
217+
events: [...events, agentChunkEvent(text)],
218+
});
219+
}
220+
205221
function runTurn(text: string, taskRunId = TASK_RUN_ID): void {
206222
beginTurn(taskRunId);
207223
completeTurn(text, taskRunId);
@@ -251,6 +267,7 @@ function makeRun(
251267
phase: null,
252268
originalModel: null,
253269
originalEffort: null,
270+
researchFindings: [],
254271
iterations: [],
255272
startedAt: 1_000,
256273
endedAt: null,
@@ -350,7 +367,7 @@ describe("AutoresearchService", () => {
350367
});
351368

352369
describe("registerRun", () => {
353-
it("registers without sending the kickoff rode the task's initial prompt", () => {
370+
it("registers without sending because the kickoff rode the task initial prompt", () => {
354371
const run = service.registerRun(baseConfig);
355372

356373
expect(run.status).toBe("running");
@@ -367,13 +384,145 @@ describe("AutoresearchService", () => {
367384
expect(sentPrompts[0].prompt).toContain("iteration 2");
368385
});
369386

387+
it("tracks reports from an initial prompt already in progress", () => {
388+
beginTurn();
389+
service.registerRun(baseConfig);
390+
391+
streamTurnText(`${reportText(10, "baseline")}\nIteration 2 starts now.`);
392+
streamTurnText(
393+
`${reportText(12, "iteration 2")}\nIteration 3 starts now.`,
394+
);
395+
streamTurnText(
396+
`${reportText(14, "iteration 3")}\nIteration 4 starts now.`,
397+
);
398+
399+
expect(activeRun().iterations).toEqual([
400+
expect.objectContaining({ index: 1, value: 10 }),
401+
expect.objectContaining({ index: 2, value: 12 }),
402+
expect.objectContaining({ index: 3, value: 14 }),
403+
]);
404+
expect(sentPrompts).toHaveLength(0);
405+
406+
completeTurn("Continuing after the reported iterations.");
407+
408+
expect(activeRun().iterations).toHaveLength(3);
409+
expect(sentPrompts).toHaveLength(1);
410+
expect(sentPrompts[0].prompt).toContain("iteration 4");
411+
});
412+
413+
it("recovers when an active prompt was incorrectly marked handled", () => {
414+
beginTurn();
415+
const run = service.registerRun(baseConfig);
416+
const internals = service as unknown as {
417+
promptCursor: Map<string, number>;
418+
};
419+
internals.promptCursor.set(run.id, 1);
420+
421+
streamTurnText(`${reportText(10, "baseline")}\nIteration 2 starts now.`);
422+
423+
expect(activeRun().iterations).toEqual([
424+
expect.objectContaining({ index: 1, value: 10 }),
425+
]);
426+
});
427+
370428
it("shares the one-live-run-per-task guard with startRun", () => {
371429
service.registerRun(baseConfig);
372430
expect(() => service.startRun(baseConfig)).toThrow(/already running/);
373431
});
374432
});
375433

376434
describe("iteration loop", () => {
435+
it("records a metric block after the next iteration starts", () => {
436+
service.startRun(baseConfig);
437+
beginTurn();
438+
streamTurnText(`${reportText(10, "baseline")}\nIteration 2 starts now.`);
439+
440+
expect(activeRun().iterations).toEqual([
441+
expect.objectContaining({ index: 1, value: 10, summary: "baseline" }),
442+
]);
443+
expect(sentPrompts).toHaveLength(1);
444+
445+
streamTurnText("Continuing to inspect the next change.");
446+
expect(activeRun().iterations).toHaveLength(1);
447+
448+
completeTurn("Finished the turn.");
449+
expect(activeRun().iterations).toHaveLength(1);
450+
expect(sentPrompts).toHaveLength(2);
451+
expect(sentPrompts.at(-1)?.prompt).toContain(
452+
"Then make the next focused change",
453+
);
454+
});
455+
456+
it("keeps the tail report provisional until the turn ends", () => {
457+
service.startRun({ ...baseConfig, maxIterations: 1 });
458+
beginTurn();
459+
streamTurnText(reportText(10, "draft baseline"));
460+
461+
expect(activeRun().iterations).toHaveLength(0);
462+
expect(activeRun().status).toBe("running");
463+
464+
streamTurnText(reportText(12, "corrected baseline"));
465+
expect(activeRun().iterations).toHaveLength(0);
466+
467+
completeTurn("Finalized the corrected report.");
468+
469+
expect(activeRun().iterations).toEqual([
470+
expect.objectContaining({
471+
index: 1,
472+
value: 12,
473+
summary: "corrected baseline",
474+
}),
475+
]);
476+
expect(activeRun().status).toBe("completed");
477+
expect(activeRun().endReason).toBe("max-iterations");
478+
});
479+
480+
it("records prebaseline research without consuming an iteration", () => {
481+
service.startRun(baseConfig);
482+
runTurn(researchText());
483+
484+
const researchingRun = activeRun();
485+
expect(researchingRun.researchFindings).toEqual([
486+
expect.objectContaining({
487+
index: 1,
488+
summary: "Mapped the execution path",
489+
finding: "The metric is computed in the workspace server.",
490+
nextStep: "Trace the benchmark command",
491+
}),
492+
]);
493+
expect(researchingRun.iterations).toHaveLength(0);
494+
expect(sentPrompts.at(-1)?.prompt).toContain(
495+
"Continue investigating the codebase or establish the baseline measurement",
496+
);
497+
498+
runTurn(reportText(10, "baseline"));
499+
500+
expect(activeRun().iterations).toEqual([
501+
expect.objectContaining({ index: 1, value: 10, summary: "baseline" }),
502+
]);
503+
});
504+
505+
it("does not accept research checkpoints after the baseline", async () => {
506+
service.startRun(baseConfig);
507+
runTurn(reportText(10, "baseline"));
508+
runTurn(researchText());
509+
await passReminderGrace();
510+
511+
expect(activeRun().researchFindings).toHaveLength(0);
512+
expect(activeRun().iterations).toHaveLength(1);
513+
expect(sentPrompts.at(-1)?.prompt).toContain("did not include");
514+
});
515+
516+
it("prefers a metric when a prebaseline reply contains both report types", () => {
517+
service.startRun(baseConfig);
518+
runTurn(`${researchText()}\n${reportText(10, "baseline")}`);
519+
520+
expect(activeRun().researchFindings).toHaveLength(0);
521+
expect(activeRun().iterations).toEqual([
522+
expect.objectContaining({ index: 1, value: 10, summary: "baseline" }),
523+
]);
524+
});
525+
377526
it("records an iteration and sends a continuation prompt", () => {
378527
service.startRun(baseConfig);
379528
runTurn(reportText(10, "baseline"));
@@ -534,7 +683,7 @@ describe("AutoresearchService", () => {
534683
);
535684
expect(activeRun().metricUnit).toBe("kB");
536685

537-
// First unit wins, like the name — a stable unit keeps values readable.
686+
// First unit wins, like the name. A stable unit keeps values readable.
538687
runTurn("```autoresearch\nmetric: 400000\nunit: bytes\n```");
539688
expect(activeRun().metricUnit).toBe("kB");
540689
});
@@ -572,7 +721,7 @@ describe("AutoresearchService", () => {
572721
expect(modelSwitches).toEqual([
573722
{ taskId: TASK_ID, model: "claude-opus-4-8" },
574723
]);
575-
expect(sentPrompts.at(-1)?.prompt).toContain("implementation phase");
724+
expect(sentPrompts.at(-1)?.prompt).toContain("Implementation phase");
576725
expect(sentPrompts.at(-1)?.prompt).toContain(
577726
"Do NOT run the measurement",
578727
);
@@ -610,7 +759,7 @@ describe("AutoresearchService", () => {
610759

611760
expect(activeRun().iterations).toHaveLength(2);
612761
expect(activeRun().phase).toBe("implement");
613-
expect(sentPrompts.at(-1)?.prompt).toContain("implementation phase");
762+
expect(sentPrompts.at(-1)?.prompt).toContain("Implementation phase");
614763
});
615764

616765
it("still fails a measure turn that never reports", async () => {
@@ -988,6 +1137,9 @@ describe("AutoresearchService", () => {
9881137
bestValue: 10,
9891138
delta: null,
9901139
summary: "baseline",
1140+
hypothesis: null,
1141+
plan: null,
1142+
approach: null,
9911143
at: 1_000,
9921144
},
9931145
],
@@ -1024,7 +1176,7 @@ describe("AutoresearchService", () => {
10241176
]);
10251177
await service.rehydrate();
10261178

1027-
// No session exists for task-9 yet — recovery asks the host to
1179+
// No session exists for task 9 yet. Recovery asks the host to
10281180
// reconnect it.
10291181
await passRecoveryDelay();
10301182
expect(reconnectCalls).toEqual(["task-9"]);
@@ -1074,7 +1226,7 @@ describe("AutoresearchService", () => {
10741226
// The in-memory run (with its recorded iteration) wins over the row.
10751227
expect(state.runs[live.id]?.iterations).toHaveLength(1);
10761228
expect(state.runs["ar-past"]?.status).toBe("completed");
1077-
// The live run stays the active one — it started later.
1229+
// The live run stays active because it started later.
10781230
expect(state.activeRunIdByTask[TASK_ID]).toBe(live.id);
10791231
});
10801232

@@ -1203,7 +1355,9 @@ describe("AutoresearchService", () => {
12031355

12041356
// No phase alternation: the next prompt is a plain continuation.
12051357
expect(activeRun().phase).toBeNull();
1206-
expect(sentPrompts.at(-1)?.prompt).toContain("Continue:");
1358+
expect(sentPrompts.at(-1)?.prompt).toContain(
1359+
"Then make the next focused change",
1360+
);
12071361
});
12081362
});
12091363

0 commit comments

Comments
 (0)