mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
Project Runner PRP events into task chat
This commit is contained in:
1 parent
00b9012a28
commit
d2c709ed19
6 files changed
+83
-5
No files matched your search
@@ -1316,18 +1316,32 @@ class CodexHarnessSession implements HarnessSession {
|
||||
if (!isSemanticTool(tool)) {
|
||||
const admitted = this.#dynamicTools.some((candidate) => candidate.name === tool);
|
||||
if (admitted && this.#dynamicToolHandler !== undefined) {
|
||||
this.#emit("item.started", {
|
||||
kind: "dynamicToolCall",
|
||||
item: { type: "tool_use", id: callId, name: tool, input: request.params.arguments },
|
||||
}, { turnId, itemId: callId });
|
||||
try {
|
||||
return dynamicToolResponse(await this.#dynamicToolHandler({
|
||||
const result = await this.#dynamicToolHandler({
|
||||
tool,
|
||||
callId,
|
||||
threadId,
|
||||
turnId,
|
||||
arguments: request.params.arguments,
|
||||
}));
|
||||
});
|
||||
this.#emit("item.completed", {
|
||||
kind: "dynamicToolCall",
|
||||
item: { type: "tool_result", id: callId, tool_use_id: callId, result },
|
||||
}, { turnId, itemId: callId });
|
||||
return dynamicToolResponse(result);
|
||||
} catch (error) {
|
||||
const message = boundedText(error instanceof Error ? error.message : error);
|
||||
this.#emit("item.completed", {
|
||||
kind: "dynamicToolCall",
|
||||
item: { type: "tool_result", id: callId, tool_use_id: callId, error: message, is_error: true },
|
||||
}, { turnId, itemId: callId });
|
||||
return {
|
||||
success: false,
|
||||
contentItems: [{ type: "inputText", text: boundedText(error instanceof Error ? error.message : error) }],
|
||||
contentItems: [{ type: "inputText", text: message }],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16171,6 +16171,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
leaseOwner: runOptions.nativeLeaseOwner,
|
||||
backend: options.nativeSessionBackendFactory?.(nativeExecution),
|
||||
useRunnerd: agent.adapterType === "paperclip_runner",
|
||||
onLog,
|
||||
onSpawn: async (meta) => {
|
||||
await persistRunProcessMetadata(run.id, meta);
|
||||
},
|
||||
|
||||
@@ -247,6 +247,7 @@ export async function executePaperclipNativeSession(input: {
|
||||
/** Test seam at the provider boundary; production always uses the package Codex backend. */
|
||||
backend?: NativeSessionBackend;
|
||||
useRunnerd?: boolean;
|
||||
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||
}): Promise<AdapterExecutionResult> {
|
||||
const leaseOwner = input.leaseOwner ?? `${input.runnerInstanceId}:${randomUUID()}`;
|
||||
const leaseNow = new Date();
|
||||
@@ -293,6 +294,10 @@ export async function executePaperclipNativeSession(input: {
|
||||
completionContractSha256: input.execution.completionContract.sha256,
|
||||
sourceInstanceId: input.runnerInstanceId,
|
||||
controlPlaneSourceInstanceId: controlPlaneInstanceId,
|
||||
}, {
|
||||
onCommittedEvent: input.onLog
|
||||
? async (event) => input.onLog!("stdout", `${JSON.stringify({ type: "paperclip.prp.event", event })}\n`)
|
||||
: undefined,
|
||||
});
|
||||
let native: Awaited<ReturnType<typeof executeNativeSession>>;
|
||||
try {
|
||||
|
||||
@@ -66,10 +66,14 @@ export class PaperclipControlPlanePort implements ControlPlanePort {
|
||||
readonly #db: Db;
|
||||
readonly #binding: PaperclipControlPlaneBinding;
|
||||
#sessionId: string | null = null;
|
||||
readonly #onCommittedEvent?: (event: PrpEvent) => Promise<void>;
|
||||
|
||||
constructor(db: Db, binding: PaperclipControlPlaneBinding) {
|
||||
constructor(db: Db, binding: PaperclipControlPlaneBinding, options: {
|
||||
onCommittedEvent?: (event: PrpEvent) => Promise<void>;
|
||||
} = {}) {
|
||||
this.#db = db;
|
||||
this.#binding = structuredClone(binding);
|
||||
this.#onCommittedEvent = options.onCommittedEvent;
|
||||
}
|
||||
|
||||
async openRun(input: OpenControlPlaneRunInput): Promise<void> {
|
||||
@@ -190,6 +194,7 @@ export class PaperclipControlPlanePort implements ControlPlanePort {
|
||||
canonicalPayload: event as unknown as Record<string, unknown>,
|
||||
},
|
||||
});
|
||||
if (persisted.disposition === "committed") await this.#onCommittedEvent?.(event);
|
||||
return {
|
||||
cursor: persisted.row.seq,
|
||||
highestContiguousSourceSeq: persisted.highestContiguousSourceSeq,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { paperclipRunnerUIAdapter } from "./index";
|
||||
|
||||
describe("paperclip runner transcript projection", () => {
|
||||
it("renders committed PRP semantic tool items with the existing chat parts", () => {
|
||||
const started = paperclipRunnerUIAdapter.parseStdoutLine(JSON.stringify({
|
||||
type: "paperclip.prp.event",
|
||||
event: {
|
||||
eventType: "item.started",
|
||||
payload: { item: { type: "tool_use", id: "call-1", name: "get_task_context", input: {} } },
|
||||
},
|
||||
}), "2026-08-21T12:00:00.000Z");
|
||||
const completed = paperclipRunnerUIAdapter.parseStdoutLine(JSON.stringify({
|
||||
type: "paperclip.prp.event",
|
||||
event: {
|
||||
eventType: "item.completed",
|
||||
payload: { item: { type: "tool_result", id: "call-1", tool_use_id: "call-1", result: { ok: true } } },
|
||||
},
|
||||
}), "2026-08-21T12:00:01.000Z");
|
||||
expect(started).toEqual([expect.objectContaining({ kind: "tool_call", name: "get_task_context", toolUseId: "call-1" })]);
|
||||
expect(completed).toEqual([expect.objectContaining({ kind: "tool_result", toolUseId: "call-1", isError: false })]);
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,40 @@ import type { UIAdapterModule } from "../types";
|
||||
import { parseCodexStdoutLine, buildPaperclipRunnerConfig } from "@paperclipai/adapter-codex-local/ui";
|
||||
import { CodexLocalConfigFields } from "../codex-local/config-fields";
|
||||
|
||||
function parsePaperclipRunnerStdoutLine(line: string, ts: string) {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(line);
|
||||
} catch {
|
||||
return parseCodexStdoutLine(line, ts);
|
||||
}
|
||||
if (typeof parsed !== "object" || parsed === null || (parsed as { type?: unknown }).type !== "paperclip.prp.event") {
|
||||
return parseCodexStdoutLine(line, ts);
|
||||
}
|
||||
const event = (parsed as { event?: unknown }).event;
|
||||
if (typeof event !== "object" || event === null) return [];
|
||||
const candidate = event as { eventType?: unknown; payload?: unknown };
|
||||
const eventType = typeof candidate.eventType === "string" ? candidate.eventType : "";
|
||||
const payload = typeof candidate.payload === "object" && candidate.payload !== null
|
||||
? candidate.payload as Record<string, unknown>
|
||||
: {};
|
||||
if ((eventType === "item.started" || eventType === "item.completed") && payload.item) {
|
||||
return parseCodexStdoutLine(JSON.stringify({ type: eventType, item: payload.item }), ts);
|
||||
}
|
||||
if (eventType === "turn.started") return parseCodexStdoutLine(JSON.stringify({ type: "turn.started" }), ts);
|
||||
if (eventType === "turn.completed") {
|
||||
return parseCodexStdoutLine(JSON.stringify({ type: "turn.completed", result: payload.result ?? "" }), ts);
|
||||
}
|
||||
if (eventType === "harness.diagnostic") {
|
||||
return [{ kind: "system" as const, ts, text: String(payload.message ?? payload.code ?? "Runner diagnostic") }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export const paperclipRunnerUIAdapter: UIAdapterModule = {
|
||||
type: "paperclip_runner",
|
||||
label: "Paperclip Runner",
|
||||
parseStdoutLine: parseCodexStdoutLine,
|
||||
parseStdoutLine: parsePaperclipRunnerStdoutLine,
|
||||
ConfigFields: CodexLocalConfigFields,
|
||||
buildAdapterConfig: buildPaperclipRunnerConfig,
|
||||
};
|
||||
Reference in new issue
Block a user