mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
fix(runner): preserve canonical reasoning delta privacy
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a589381f90
commit
a978d1bc54
2 files changed
+49
-3
No files matched your search
@@ -32,6 +32,7 @@ import type {
|
||||
PrpTerminalState,
|
||||
} from "../protocol/replay-contract.js";
|
||||
import { executeNativeSession } from "../native-session-runtime.js";
|
||||
import { redactCapabilityEvidenceData } from "./evidence-redaction.js";
|
||||
import { NativeSessionCloseUnrecoverableError } from "../contracts/native-session-backend.js";
|
||||
import { parsePaperclipQuestionSet } from "../contracts/question-set.js";
|
||||
import { ACPX_CREDENTIAL_BINDING_ENV, createAcpxCredentialBinding, createAcpxSidecarHostEnvironment } from "../drivers/acpx/environment.js";
|
||||
@@ -1922,6 +1923,35 @@ it("binds a durable semantic result to the active provider turn", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["summary", "detail"])("preserves canonical reasoning %s deltas without publishing assistant text", (channel) => {
|
||||
// Exact shape emitted by the Rust ACPX projector, with synthetic private text.
|
||||
const payload = { provider: "acpx", itemId: "reason-1", kind: "reasoning", channel,
|
||||
providerMethod: "runtime.event", text: "PRIVATE_REASONING_SENTINEL" };
|
||||
const method = runnerdCanonicalNotificationMethod("item.delta", payload);
|
||||
expect(method).toBe(channel === "detail" ? "item/reasoning/textDelta" : "item/reasoning/summaryTextDelta");
|
||||
const params = rehydrateRunnerdDeltaNotification(payload, "provider-thread", "provider-turn");
|
||||
expect(params).toMatchObject({ threadId: "provider-thread", turnId: "provider-turn", itemId: "reason-1" });
|
||||
const evidence = redactCapabilityEvidenceData("provider_event", { method, params });
|
||||
expect(evidence).toEqual({ event: "reasoning_delta" });
|
||||
expect(JSON.stringify(evidence)).not.toContain("PRIVATE_REASONING_SENTINEL");
|
||||
});
|
||||
|
||||
it("classifies each coalesced canonical delta without mixing reasoning into assistant output", () => {
|
||||
const notifications = expandRunnerdCanonicalNotifications("item/agentMessage/delta", {
|
||||
coalescedCount: 3,
|
||||
events: [
|
||||
{ kind: "reasoning", channel: "summary", text: "PRIVATE_REASONING_SENTINEL" },
|
||||
{ kind: "agentMessage", channel: "progress", text: "Visible progress" },
|
||||
{ kind: "reasoning", channel: "detail", text: "PRIVATE_DETAIL_SENTINEL" },
|
||||
],
|
||||
}, "item.delta");
|
||||
expect(notifications.map((entry) => entry.method)).toEqual([
|
||||
"item/reasoning/summaryTextDelta", "item/agentMessage/delta", "item/reasoning/textDelta",
|
||||
]);
|
||||
expect(notifications.filter((entry) => entry.method === "item/agentMessage/delta").map((entry) => entry.params.text))
|
||||
.toEqual(["Visible progress"]);
|
||||
});
|
||||
|
||||
it("restores provider identity and streamed text from a canonical delta", () => {
|
||||
expect(rehydrateRunnerdDeltaNotification({ text: "Reading Gmail", itemId: "message-1", turnId: "controller-turn" }, "root-thread", "provider-turn"))
|
||||
.toMatchObject({ threadId: "root-thread", turnId: "provider-turn", delta: "Reading Gmail", itemId: "message-1" });
|
||||
|
||||
@@ -1325,10 +1325,21 @@ export function unseenRunnerdCommittedEvents<
|
||||
export function expandRunnerdCanonicalNotifications(
|
||||
method: string,
|
||||
input: unknown,
|
||||
eventType?: string,
|
||||
): Array<{ method: string; params: Record<string, unknown> }> {
|
||||
const payload = record(input);
|
||||
if (!Array.isArray(payload.events)) return [{ method, params: payload }];
|
||||
return payload.events.map((event) => ({ method, params: record(event) }));
|
||||
const events = Array.isArray(payload.events) ? payload.events : [payload];
|
||||
return events.map((event) => {
|
||||
const params = record(event);
|
||||
// A coalesced envelope can contain both reasoning and visible messages.
|
||||
// Classify each payload after expansion, never using the envelope's kind.
|
||||
return {
|
||||
method: eventType === "item.delta"
|
||||
? runnerdCanonicalNotificationMethod(eventType, params) ?? method
|
||||
: method,
|
||||
params,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function runnerdCanonicalNotificationMethod(
|
||||
@@ -1341,6 +1352,11 @@ export function runnerdCanonicalNotificationMethod(
|
||||
if (eventType === "session.goal.snapshot" && payload.goal === null) {
|
||||
return undefined;
|
||||
}
|
||||
if (eventType === "item.delta" && payload.kind === "reasoning") {
|
||||
return payload.channel === "detail"
|
||||
? "item/reasoning/textDelta"
|
||||
: "item/reasoning/summaryTextDelta";
|
||||
}
|
||||
return (
|
||||
{
|
||||
"turn.started": "turn/started",
|
||||
@@ -6007,7 +6023,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
: event.eventType === "provider.event"
|
||||
? unwrapRunnerdProviderNotifications(eventPayload)
|
||||
: canonicalMethod
|
||||
? expandRunnerdCanonicalNotifications(canonicalMethod, eventPayload)
|
||||
? expandRunnerdCanonicalNotifications(canonicalMethod, eventPayload, event.eventType)
|
||||
: [];
|
||||
for (const payload of notifications) {
|
||||
const method = payload.method;
|
||||
|
||||
Reference in new issue
Block a user