mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 06:42:06 +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>
146 lines
9.0 KiB
TypeScript
146 lines
9.0 KiB
TypeScript
/**
|
|
* Exhaustive product disposition for Paperclip Runner Protocol surfaces.
|
|
*
|
|
* This is intentionally separate from the renderer: adding a protocol event
|
|
* must be a conscious product decision even when the decision is to fold it
|
|
* into an existing turn or keep it in DevTools. The contract test compares
|
|
* these keys with the canonical JSON schemas.
|
|
*/
|
|
export type TaskProtocolDisposition = "inline" | "folded" | "debug-only";
|
|
|
|
export interface TaskProtocolSurfaceRegistration {
|
|
disposition: TaskProtocolDisposition;
|
|
surface: string;
|
|
story: string | null;
|
|
rationale: string;
|
|
}
|
|
|
|
const inline = (surface: string, story: string, rationale: string): TaskProtocolSurfaceRegistration => ({
|
|
disposition: "inline",
|
|
surface,
|
|
story,
|
|
rationale,
|
|
});
|
|
|
|
const folded = (surface: string, rationale: string): TaskProtocolSurfaceRegistration => ({
|
|
disposition: "folded",
|
|
surface,
|
|
story: null,
|
|
rationale,
|
|
});
|
|
|
|
const debugOnly = (surface: string, rationale: string): TaskProtocolSurfaceRegistration => ({
|
|
disposition: "debug-only",
|
|
surface,
|
|
story: null,
|
|
rationale,
|
|
});
|
|
|
|
const providerEvents = [
|
|
"plan.updated",
|
|
"tool.execution.started", "tool.execution.progressed", "tool.execution.completed",
|
|
"research.started", "research.progressed", "research.completed",
|
|
"delegation.started", "delegation.updated", "delegation.completed",
|
|
"model.route.changed", "model.verification.updated", "context.compacted",
|
|
"artifact.viewed", "artifact.generated", "review.mode.changed",
|
|
"hook.started", "hook.completed", "memory.citation.referenced",
|
|
"safety.review.started", "safety.review.completed", "terminal.input.sent",
|
|
"wait.started", "wait.completed", "provider.notice.recorded",
|
|
] as const;
|
|
|
|
const runnerLifecycleEvents = [
|
|
"runner.connected", "runner.reconnected", "runner.reconciled", "runner.disconnected",
|
|
"runner.draining", "runner.backpressure", "runner.suspending", "runner.suspended", "runner.stopped",
|
|
"runtime.phase.changed", "workspace.ready",
|
|
"harness.starting", "harness.ready", "harness.exited",
|
|
"session.starting", "session.started", "session.resuming", "session.resumed",
|
|
"session.reconciled", "session.updated", "session.closed",
|
|
"turn.submitted", "turn.accepted", "turn.started", "turn.completed",
|
|
"run.attached", "run.detached",
|
|
] as const;
|
|
|
|
const diagnosticEvents = [
|
|
"runner.diagnostic", "harness.diagnostic", "session.failed", "turn.failed",
|
|
"turn.interrupted", "turn.cancelled", "item.failed",
|
|
] as const;
|
|
|
|
const itemEvents = ["item.started", "item.delta", "item.completed", "usage.reported"] as const;
|
|
|
|
const mcpLifecycleEvents = [
|
|
"mcp_app.discovered", "mcp_app.resource.resolved", "mcp_app.initializing", "mcp_app.ready",
|
|
"mcp_app.host_context.changed", "mcp_app.failed", "mcp_app.teardown",
|
|
] as const;
|
|
|
|
const interactionLifecycleEvents = [
|
|
"interaction.request.proposed", "interaction.request.materialized", "interaction.request.rejected",
|
|
"interaction.response.progressed", "interaction.response.resolved", "interaction.response.delivered",
|
|
] as const;
|
|
|
|
const governanceEvents = [
|
|
"attention.request.proposed", "attention.request.routed", "attention.request.resolved",
|
|
"attention.request.expired", "attention.request.superseded", "work.assessment.recorded",
|
|
"issue.status.decision.recorded", "issue.status.decision.applied", "issue.status.decision.rejected",
|
|
"issue.status.decision.superseded",
|
|
] as const;
|
|
|
|
export const TASK_PROTOCOL_EVENT_SURFACE_REGISTRY: Readonly<Record<string, TaskProtocolSurfaceRegistration>> = Object.freeze({
|
|
...Object.fromEntries(providerEvents.map((eventType) => [eventType, inline("provider_activity", "Task Page/Runner Protocol/Provider semantics", "Provider-neutral semantic activity is readable in the task turn.")])),
|
|
"workspace.change.updated": inline("workspace_change", "Task Page/Runner Protocol/Workspace changes", "In-progress workspace changes update one diff card."),
|
|
"workspace.diff.recorded": inline("workspace_change", "Task Page/Runner Protocol/Workspace changes", "The runner-verified final diff replaces the in-progress revision."),
|
|
"workspace.file.referenced": inline("workspace_file", "Task Page/Runner Protocol/File references", "Verified references open a bounded preview and the production file viewer."),
|
|
"semantic_tool.input": folded("tool", "Semantic operations reuse the production tool row."),
|
|
"semantic_tool.result": folded("tool", "Semantic operation outcomes update the matching tool row."),
|
|
"semantic_tool.reconciled": folded("tool", "Recovered semantic operation outcomes update the matching tool row without duplicating it."),
|
|
"mcp_app.tool_input": folded("tool", "MCP inputs reuse the production tool row."),
|
|
"mcp_app.tool_result": folded("tool", "MCP outcomes update the matching tool row."),
|
|
"mcp_app.action.requested": folded("interaction", "Action requests materialize as authoritative issue-thread interactions."),
|
|
"mcp_app.action.resolved": folded("interaction", "Action resolution is shown on the authoritative interaction card."),
|
|
"runtime_request.created": inline("runtime_request", "Task Page/Runner Protocol/Runtime requests", "Pending provider runtime requests remain visible and actionable when a resolver is available."),
|
|
"runtime_request.resolved": folded("runtime_request", "Resolution updates the existing request card."),
|
|
"runtime_request.expired": folded("runtime_request", "Expiry updates the existing request card."),
|
|
"runtime_request.cancelled": folded("runtime_request", "Cancellation updates the existing request card."),
|
|
...Object.fromEntries(interactionLifecycleEvents.map((eventType) => [eventType, folded("interaction", "The control-plane interaction record is canonical and owns rendering." )])),
|
|
"run.result.proposed": inline("run_result", "Task Page/Runner Protocol/Results and terminal states", "Structured completion, evidence, verification, blockers, and artifacts remain inspectable."),
|
|
"run.result.accepted": folded("run_result", "Acceptance updates the existing result without duplicating it."),
|
|
"run.result.rejected": folded("run_result", "Rejected results remain part of run governance rather than a second completion card."),
|
|
"run.terminal": inline("run_terminal", "Task Page/Runner Protocol/Results and terminal states", "Terminal outcome and stop reason remain visible."),
|
|
...Object.fromEntries(runnerLifecycleEvents.map((eventType) => [eventType, folded("turn", "Routine lifecycle transitions are summarized by the run turn and composer state.")])),
|
|
...Object.fromEntries(diagnosticEvents.map((eventType) => [eventType, folded("system_notice", "Actionable failure text is folded into the run or system notice.")])),
|
|
...Object.fromEntries(itemEvents.map((eventType) => [eventType, folded("conversation_or_tool", "Item payloads normalize into messages, reasoning, tools, diffs, or usage." )])),
|
|
"runtime.activity.recorded": folded("task_panel", "The activated plugin view appears in the task side panel."),
|
|
"auth.credentials.updated": debugOnly("run_debug", "Credential updates notify the host and remain in run details."),
|
|
"sandbox.metric": debugOnly("run_debug", "Sandbox telemetry is available in run details, not the primary task thread."),
|
|
...Object.fromEntries(mcpLifecycleEvents.map((eventType) => [eventType, debugOnly("run_debug", "MCP application transport lifecycle remains in run details unless it produces an actionable failure.")])),
|
|
...Object.fromEntries(governanceEvents.map((eventType) => [eventType, folded("governance", "Authoritative task status, attention, and interaction records own this state." )])),
|
|
});
|
|
|
|
export const TASK_PROTOCOL_REQUEST_SURFACE_REGISTRY = Object.freeze({
|
|
"runtime.permission": inline("runtime_request", "Task Page/Runner Protocol/Runtime requests", "Permission choices render on the runtime request card."),
|
|
"runtime.input": inline("runtime_request", "Task Page/Runner Protocol/Runtime requests", "Structured runtime input renders on the runtime request card."),
|
|
"issue_thread.request_confirmation": inline("interaction", "Task Page/Runner Protocol/Interactions", "Uses the production confirmation card."),
|
|
"issue_thread.request_checkbox_confirmation": inline("interaction", "Task Page/Runner Protocol/Interactions", "Uses the production bounded checkbox card."),
|
|
"issue_thread.request_item_verdicts": inline("interaction", "Task Page/Runner Protocol/Interactions", "Uses the production per-item verdict card."),
|
|
"issue_thread.ask_user_questions": inline("interaction", "Task Page/Runner Protocol/Interactions", "Uses the production typed question controls."),
|
|
"issue_thread.suggest_tasks": inline("interaction", "Task Page/Runner Protocol/Interactions", "Uses the production task suggestion tree."),
|
|
});
|
|
|
|
export const TASK_PROTOCOL_RESULT_DISPOSITION_REGISTRY = Object.freeze({
|
|
done: "run_result",
|
|
blocked: "run_result",
|
|
needs_review: "run_result",
|
|
yielded: "run_result",
|
|
} as const);
|
|
|
|
export const TASK_PROTOCOL_TURN_TERMINAL_REGISTRY = Object.freeze({
|
|
completed: "run_terminal",
|
|
failed: "run_terminal",
|
|
interrupted: "run_terminal",
|
|
cancelled: "run_terminal",
|
|
} as const);
|
|
|
|
export const TASK_PROTOCOL_RUN_TERMINAL_REGISTRY = Object.freeze({
|
|
succeeded: "run_terminal",
|
|
failed: "run_terminal",
|
|
cancelled: "run_terminal",
|
|
} as const);
|