mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
## Thinking Path > - Paperclip manages agents and tasks. > - Runner reports durable run events through PRP. > - Runtime clients need a typed record of reported activity. > - The event must keep the normal run authority and read boundaries. > - This pull request adds the generic runtime activity event contract. > - Later PRs can publish and consume this contract without private plugin coupling. ## Linked Issues or Issue Description First feature prerequisite extracted from #18050. Runner publishing is #18050; the dependent consumer is #18131. Related independent test stability work: #18128. The parent search and review history is preserved in [the verification archive](https://github.com/paperclipai/paperclip/pull/18050#issuecomment-6489369676). **Subsystem affected** Runner event protocol, server run-event persistence and redaction, and UI event taxonomy. **Problem or motivation** Core needs a durable, typed activity record tied to the authenticated run. A text marker is not an event contract. **Proposed solution** Add `runtime.activity.recorded` as a P1 event with a closed `{ namespace, activityKey }` payload. Runner knows no plugin or panel identity. Run/session identity, sequence and emission time stay in the outer envelope; Core resolves company, task and agent through the authorized run binding. Use the existing authenticated persistence, acknowledgement, replay and redaction paths. ## What Changed - Define and validate the event in PRP v1, v2 and v3. Update the generated protocol artifacts and event catalog. - Preserve saved and live event redaction. Test persistence, replay deduplication and the existing run-read boundary. - Register the event in the UI taxonomy without changing panel display behavior. - Advance the source-bound Copilot identity to 78. Retain all published historical identity fixtures without changing their bytes. ## Verification - Root `pnpm typecheck` and `pnpm build`: passed for the revised activity contract. - Node protocol checks: 14 passed. Provider identity and registry tests: 185 passed. Server tests: 103 passed. UI event registry: five passed. - Maintained Grok and Copilot identity generator checks and `git diff --check`: passed. - The first activity-contract review scored 4/5 because the dependent producer and readers still used the earlier contract. Both dependent PRs are now updated and restacked; the finding is resolved and review is running again. - Initial extraction retained a consumer-only import in one server suite. That suite failed to load. The consumer case and import were moved out; the final three server files passed all 103 cases. - Earlier panel-specific head `6137c50bc` had green CI and standard/Apex 5/5. Those scores do not qualify this revised activity contract. Fresh CI and reviews are pending. Earlier combined results in the archive apply only to their recorded source revisions. ## Risks This change is additive. It adds no route, publisher, filesystem transport, provider sandbox grant, or panel display behavior. Activation hints grant no permission to read a plugin view. Provider identities bind the changed protocol source; historical identities remain decode-only. No schema migration or dependency change is required. ## Model Used OpenAI Codex, based on GPT-6, with reasoning, repository tools and code execution. The exact serving snapshot is not exposed. ## 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>
81 lines
3.9 KiB
TypeScript
81 lines
3.9 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
TASK_PROTOCOL_EVENT_SURFACE_REGISTRY,
|
|
TASK_PROTOCOL_REQUEST_SURFACE_REGISTRY,
|
|
TASK_PROTOCOL_RESULT_DISPOSITION_REGISTRY,
|
|
TASK_PROTOCOL_RUN_TERMINAL_REGISTRY,
|
|
TASK_PROTOCOL_TURN_TERMINAL_REGISTRY,
|
|
} from "./task-protocol-surfaces";
|
|
|
|
function schema(name: string): Record<string, unknown> {
|
|
const path = fileURLToPath(new URL(`../../../../packages/paperclip-runner/protocol/schemas/${name}`, import.meta.url));
|
|
return JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
}
|
|
|
|
function record(value: unknown): Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
? value as Record<string, unknown>
|
|
: {};
|
|
}
|
|
|
|
function enumStrings(value: unknown): string[] {
|
|
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : [];
|
|
}
|
|
|
|
describe("Paperclip task protocol surface registry", () => {
|
|
it("folds activation into the task panel instead of adding a chat message", () => {
|
|
expect(TASK_PROTOCOL_EVENT_SURFACE_REGISTRY["runtime.activity.recorded"]).toMatchObject({ disposition: "folded", surface: "task_panel" });
|
|
});
|
|
it("classifies every canonical event exactly once", () => {
|
|
const eventSchema = schema("event.schema.json");
|
|
const properties = record(eventSchema.properties);
|
|
const eventType = record(properties.eventType);
|
|
const canonical = enumStrings(eventType.enum).sort();
|
|
const registered = Object.keys(TASK_PROTOCOL_EVENT_SURFACE_REGISTRY).sort();
|
|
expect(registered).toEqual(canonical);
|
|
});
|
|
|
|
it("gives every inline event a Storybook verification surface", () => {
|
|
for (const [eventType, registration] of Object.entries(TASK_PROTOCOL_EVENT_SURFACE_REGISTRY)) {
|
|
if (registration.disposition !== "inline") continue;
|
|
expect(registration.story, eventType).toMatch(/^Task Page\/Runner Protocol\//);
|
|
}
|
|
});
|
|
|
|
it("classifies both runtime request types and all issue-thread request kinds", () => {
|
|
const requestSchema = schema("request.schema.json");
|
|
const alternatives = Array.isArray(requestSchema.oneOf) ? requestSchema.oneOf.map(record) : [];
|
|
const runtimeProperties = alternatives
|
|
.filter((candidate) => record(record(candidate.properties).requestKind).const === "runtime")
|
|
.map((candidate) => record(candidate.properties));
|
|
const issueProperties = record(alternatives.find((candidate) => record(record(candidate.properties).requestKind).const === "issue_thread")?.properties);
|
|
const expected = [...new Set([
|
|
...runtimeProperties.flatMap((properties) => {
|
|
const type = record(properties.type);
|
|
const kinds = typeof type.const === "string" ? [type.const] : enumStrings(type.enum);
|
|
return kinds.map((kind) => `runtime.${kind}`);
|
|
}),
|
|
...enumStrings(record(issueProperties.kind).enum).map((kind) => `issue_thread.${kind}`),
|
|
])].sort();
|
|
expect(Object.keys(TASK_PROTOCOL_REQUEST_SURFACE_REGISTRY).sort()).toEqual(expected);
|
|
});
|
|
|
|
it("covers result dispositions and both terminal state axes", () => {
|
|
const resultSchema = schema("result.schema.json");
|
|
const terminalSchema = schema("terminal.schema.json");
|
|
const resultProperties = record(resultSchema.properties);
|
|
const terminalProperties = record(terminalSchema.properties);
|
|
expect(Object.keys(TASK_PROTOCOL_RESULT_DISPOSITION_REGISTRY).sort()).toEqual(
|
|
enumStrings(record(resultProperties.reportedWorkDisposition).enum).sort(),
|
|
);
|
|
expect(Object.keys(TASK_PROTOCOL_TURN_TERMINAL_REGISTRY).sort()).toEqual(
|
|
enumStrings(record(terminalProperties.turnTerminalState).enum).sort(),
|
|
);
|
|
expect(Object.keys(TASK_PROTOCOL_RUN_TERMINAL_REGISTRY).sort()).toEqual(
|
|
enumStrings(record(terminalProperties.runTerminalState).enum).sort(),
|
|
);
|
|
});
|
|
});
|