mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 14:16:39 +01:00
## Thinking Path > - Paperclip manages AI agents that do work for users. > - Product E2E journeys check user outcomes on real deployments. > - Native task threads can render saved final replies from a settled run. > - The new QA Plan and Stop checks looked only for agent comments. > - Both flows reached the requested terminal state and showed the correct reply, but that storage assumption rejected them. > - This PR checks the reloaded visible reply and binds it to the final successful run. Explicit presentation decisions remain authoritative. ## Linked Issues or Issue Description Refs #18222 and #18244. **What happened?** The deployed Plan journey completed all three native runs, including revision and approval. Stop completed native cancellation, resource release and a new direction. Their final-answer checks failed because the comments endpoint did not contain the rendered native reply. The original failed grades remain unchanged. **Expected behavior** Require exactly one correct visible final answer after reload, attributed to the final successful owned run. Reject an earlier run, progress text, hidden content, duplicate answers or a cancelled run. If its selected completion comment is projected, require that comment and matching author/run/text. Otherwise require a single durable final assistant message and its settled native final-response anchor. **Steps to reproduce** Run the Plan or Stop journey on a native deployment whose task thread renders the settled run before a completion comment is projected. ## What Changed - Add a narrow reply observer over the existing task-thread hooks. It reloads the page and refreshes saved run/comment metadata while waiting. - Bind native replies to a validated durable final assistant event and the successful run's settled final-response anchor. Require the selected comment when projected; support pending or suppressed projection without accepting a summary fallback. Accept payload and item-normalized finals, rejecting contradictory fields. - Use the observer for Plan completion and Stop's new direction. Keep existing run counts, deployment binding, cancellation and artifact checks. - Add negative grading tests, include the helper in the definition hash, and document its evidence contract. Classic protocol comment matchers are unchanged. ## Verification - 63 focused tests passed across native reply evidence, agent evidence and journey contracts. - Product E2E TypeScript passed. The reply observer uses the remaining journey deadline and backs off polling. - Live Plan and Stop attempts exposed this grading failure. Their original outcomes remain failed; fresh qualification is required after merge. - Full current-head CI passed at `40840ed742977853630d733292461ad7003fc7fb`; Greptile and Paperclip Review both scored 5/5, with zero unresolved review threads. ## Risks The observer depends on existing task-thread test hooks and run anchors. Missing or changed hooks fail closed. It reads rendered user-facing text; a finish summary alone cannot satisfy it. Native event provenance, run ownership and exact reply equality are required. No backend, model, provider, budget or cancellation policy changes are included. ## Model Used OpenAI Codex, GPT-6 family. The exact serving model ID and context window are not exposed by this session. Used reasoning, source editing, shell execution and test inspection. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
76 lines
5.5 KiB
TypeScript
76 lines
5.5 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { taskReplyBelongsToRun, uniqueBoundTaskReply, type TaskReplyObservation } from "./native-task-reply.js";
|
|
|
|
const run = { id: "final-run", companyId: "owned-company", agentId: "owned-agent", status: "succeeded", runtimeMode: "native", resultJson: {} };
|
|
const observed: TaskReplyObservation = { body: "The exact final reply", anchor: "final-run:turn", settled: true, nativeFinal: true, visible: true };
|
|
function events() {
|
|
return [{ seq: 1, runId: run.id, companyId: run.companyId, agentId: run.agentId, eventType: "item.completed", protocolSchemaVersion: 1,
|
|
sourceEventId: "event", sourceInstanceId: "instance", sourceSeq: 1,
|
|
payload: { prpEvent: { schema: "paperclip.prp.event.v1", schemaVersion: 1, runId: run.id, sourceKind: "runner", eventType: "item.completed",
|
|
sourceEventId: "event", sourceInstanceId: "instance", sourceSeq: 1, payload: { kind: "agentMessage", channel: "final", text: observed.body } } } }];
|
|
}
|
|
|
|
it("binds a settled native final reply to its actual run without inventing a comment", () => {
|
|
expect(taskReplyBelongsToRun(observed, run, "owned-agent", [], events())).toBe(true);
|
|
for (const change of [{ anchor: "earlier-run:turn" }, { anchor: "final-run:turnish" }, { settled: false }, { nativeFinal: false }, { visible: false }, { body: "" }])
|
|
expect(taskReplyBelongsToRun({ ...observed, ...change }, run, "owned-agent", [], events())).toBe(false);
|
|
for (const change of [{ agentId: "other-agent" }, { status: "cancelled" }, { runtimeMode: "legacy" }])
|
|
expect(taskReplyBelongsToRun(observed, { ...run, ...change }, "owned-agent", [], events())).toBe(false);
|
|
});
|
|
|
|
it("requires one exact visible answer and rejects duplicates, stale answers and extra text", () => {
|
|
expect(uniqueBoundTaskReply([observed], run, "owned-agent", [], observed.body, events())).toEqual(observed);
|
|
for (const all of [[], [observed, observed], [{ ...observed, body: observed.body + " extra" }],
|
|
[{ ...observed, anchor: "old:turn" }], [observed, { ...observed, anchor: "old:turn" }]])
|
|
expect(uniqueBoundTaskReply(all, run, "owned-agent", [], observed.body, events())).toBeNull();
|
|
});
|
|
|
|
it("accepts item-normalized final messages but rejects contradictory payload fields", () => {
|
|
const copy: any[] = events();
|
|
const payload = copy[0].payload.prpEvent.payload;
|
|
payload.item = { type: payload.kind, channel: payload.channel, text: payload.text };
|
|
delete payload.kind; delete payload.channel; delete payload.text;
|
|
expect(taskReplyBelongsToRun(observed, run, "owned-agent", [], copy)).toBe(true);
|
|
for (const change of [{ kind: "tool_call" }, { channel: "commentary" }, { text: "other reply" }]) {
|
|
expect(taskReplyBelongsToRun(observed, run, "owned-agent", [], [{ ...copy[0], payload: { prpEvent: {
|
|
...copy[0].payload.prpEvent, payload: { ...payload, ...change },
|
|
} } }])).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("rejects a rendered finish summary with no durable final assistant message", () => {
|
|
const summaryOnly = { ...run, resultJson: { nativeResult: { summary: observed.body } } };
|
|
expect(taskReplyBelongsToRun(observed, summaryOnly, "owned-agent", [])).toBe(false);
|
|
const changes = [
|
|
(e: ReturnType<typeof events>[number]) => { e.runId = "other"; },
|
|
(e: ReturnType<typeof events>[number]) => { e.companyId = "other"; },
|
|
(e: ReturnType<typeof events>[number]) => { e.agentId = "other"; },
|
|
(e: ReturnType<typeof events>[number]) => { e.protocolSchemaVersion = 99; },
|
|
(e: ReturnType<typeof events>[number]) => { e.payload.prpEvent.sourceKind = "control_plane"; },
|
|
(e: ReturnType<typeof events>[number]) => { e.payload.prpEvent.payload.channel = "commentary"; },
|
|
(e: ReturnType<typeof events>[number]) => { e.payload.prpEvent.payload.text = "different reply"; },
|
|
(e: ReturnType<typeof events>[number]) => { e.sourceSeq = 2; },
|
|
];
|
|
for (const change of changes) { const copy = events(); change(copy[0]!); expect(taskReplyBelongsToRun(observed, run, "owned-agent", [], copy)).toBe(false); }
|
|
expect(taskReplyBelongsToRun(observed, run, "owned-agent", [], [...events(), ...events()])).toBe(false);
|
|
});
|
|
|
|
it("requires the selected, attributed completion comment when a presentation decision exists", () => {
|
|
const published = { ...run, resultJson: { presentationDecision: { commentId: "selected" } } };
|
|
const comment = { id: "selected", authorAgentId: "owned-agent", createdByRunId: "final-run", body: observed.body };
|
|
const bubble = { ...observed, anchor: "selected", nativeFinal: false, settled: false };
|
|
expect(taskReplyBelongsToRun(bubble, published, "owned-agent", [comment])).toBe(true);
|
|
expect(taskReplyBelongsToRun(observed, published, "owned-agent", [comment])).toBe(false);
|
|
for (const change of [{ id: "other" }, { authorAgentId: "other-agent" }, { createdByRunId: "earlier-run" }, { body: "wrong reply" }])
|
|
expect(taskReplyBelongsToRun(bubble, published, "owned-agent", [{ ...comment, ...change }])).toBe(false);
|
|
expect(taskReplyBelongsToRun(bubble, published, "owned-agent", [])).toBe(false);
|
|
});
|
|
|
|
it("accepts a proven native final before the selected comment is projected, and when comment creation is suppressed", () => {
|
|
for (const presentationDecision of [{ commentId: "not-yet-projected", commentAction: "create" }, { commentId: null, commentAction: "none" }]) {
|
|
const pendingProjection = { ...run, resultJson: { presentationDecision } };
|
|
expect(taskReplyBelongsToRun(observed, pendingProjection, "owned-agent", [], events())).toBe(true);
|
|
expect(taskReplyBelongsToRun(observed, pendingProjection, "owned-agent", [])).toBe(false);
|
|
}
|
|
});
|