mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-12 18:06:11 +02:00
Allow explicit absent continuation in provider completion tools
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
304af99040
commit
88fbc2c702
5 files changed
+67
-3
No files matched your search
@@ -66,6 +66,12 @@ effects, audit, retries, conflicts, redaction, continuation, and terminal
|
||||
decisions. The production adapter stays App-owned and invokes Paperclip's real
|
||||
route/service authorities; it does not copy those rules into this package.
|
||||
|
||||
Provider-facing `paperclip_finish` accepts an omitted or `null` continuation for
|
||||
`done`, `completed`, and `needs_review`. This supports gateways that require all
|
||||
declared tool properties to be present. Normalization removes only `null`;
|
||||
non-yielding tool calls still reject a continuation object, and `yielded` still
|
||||
requires a complete `response_wake` object.
|
||||
|
||||
## Quick start
|
||||
|
||||
Native provider debug-trace correlation uses an incremental index owned by its transport. Pending
|
||||
|
||||
@@ -131,6 +131,19 @@ describe("provider-neutral completion result schema", () => {
|
||||
expect(validate(response)).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["done", "needs_review", "completed"])("accepts explicit no-continuation at the provider boundary for %s", (disposition) => {
|
||||
const providerValidate = new Ajv2020({ allErrors: true, strict: false })
|
||||
.compile(PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA);
|
||||
const response = {
|
||||
...structuredClone(baseResult),
|
||||
reportedWorkDisposition: disposition,
|
||||
continuation: null,
|
||||
};
|
||||
expect(providerValidate(response)).toBe(true);
|
||||
// The canonical output remains strict; normalization removes only null.
|
||||
expect(validate(response)).toBe(false);
|
||||
});
|
||||
|
||||
it("exposes concrete completion fields while retaining response-wake validation", () => {
|
||||
// The live Codex code-mode renderer reduced a conditional-only root allOf
|
||||
// to `args: unknown`. Keep this tool object-shaped for provider discovery.
|
||||
@@ -154,6 +167,7 @@ describe("provider-neutral completion result schema", () => {
|
||||
};
|
||||
expect(providerValidate(yielded)).toBe(true);
|
||||
expect(providerValidate({ ...yielded, continuation: undefined })).toBe(false);
|
||||
expect(providerValidate({ ...yielded, continuation: null })).toBe(false);
|
||||
expect(providerValidate({ ...yielded, continuation: { kind: "response_wake" } })).toBe(false);
|
||||
expect(providerValidate({
|
||||
...yielded, continuation: { ...yielded.continuation, kind: "same_agent" },
|
||||
|
||||
@@ -355,15 +355,23 @@ export const PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA = {
|
||||
reportedWorkDisposition: { enum: ["done", "needs_review", "yielded", "completed"] },
|
||||
verification: providerVerificationCompatibilitySchema,
|
||||
attentionRequests: providerAttentionCompatibilitySchema,
|
||||
continuation: responseWakeContinuationSchema,
|
||||
// Responses-compatible gateways can require every declared property in
|
||||
// tool calls. Give non-yielding results an explicit absence value instead
|
||||
// of forcing callers to invent a response-wake continuation.
|
||||
continuation: {
|
||||
...responseWakeContinuationSchema,
|
||||
type: ["object", "null"],
|
||||
description:
|
||||
"Use null or omit this field for done, completed, or needs_review. Only yielded requires a response-wake object with kind, summary, and idempotencyKey.",
|
||||
},
|
||||
},
|
||||
// Keep the provider-facing root a concrete object. Codex code-mode renders a
|
||||
// root allOf containing only an if/then constraint as `args: unknown`, hiding
|
||||
// every required field from the model. This equivalent direct conditional
|
||||
// preserves validation without obscuring the object-shaped tool signature.
|
||||
if: { properties: { reportedWorkDisposition: { const: "yielded" } }, required: ["reportedWorkDisposition"] },
|
||||
then: { required: ["continuation"] },
|
||||
else: { not: { required: ["continuation"] } },
|
||||
then: { required: ["continuation"], properties: { continuation: { type: "object" } } },
|
||||
else: { properties: { continuation: { type: "null" } } },
|
||||
} as const;
|
||||
|
||||
export const PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA = {
|
||||
|
||||
@@ -6,6 +6,36 @@ import {
|
||||
import { validatePrpStructuredRunResult } from "./replay-contract.js";
|
||||
|
||||
describe("normalizePrpResultSignals", () => {
|
||||
it("normalizes an explicit null continuation without weakening completion rules", () => {
|
||||
const providerResult = {
|
||||
reportedWorkDisposition: "done",
|
||||
summary: "Renamed the task to bobathan.",
|
||||
completionClaim: {
|
||||
contractRevision: "1",
|
||||
objectiveSatisfied: true,
|
||||
criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: [] }],
|
||||
remainingWork: [],
|
||||
},
|
||||
evidence: [],
|
||||
verification: [],
|
||||
continuation: null,
|
||||
};
|
||||
const validated = validatePrpStructuredRunResult(providerResult);
|
||||
expect(validated).toMatchObject({ ok: true, result: { reportedWorkDisposition: "done" } });
|
||||
if (validated.ok) expect(validated.result).not.toHaveProperty("continuation");
|
||||
expect(providerResult).toHaveProperty("continuation", null);
|
||||
expect(validatePrpStructuredRunResult({
|
||||
...providerResult,
|
||||
reportedWorkDisposition: "yielded",
|
||||
})).toMatchObject({ ok: false });
|
||||
expect(normalizeLegacyPrpStructuredRunResult({
|
||||
...providerResult,
|
||||
continuation: { kind: "response_wake", summary: "Contradictory wait.", idempotencyKey: "wait-1" },
|
||||
})).toHaveProperty("continuation", {
|
||||
kind: "response_wake", summary: "Contradictory wait.", idempotencyKey: "wait-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts legacy completion aliases as one canonical PRP result", () => {
|
||||
const legacy = {
|
||||
schema: "paperclip.run_result.v1",
|
||||
|
||||
@@ -119,6 +119,12 @@ export function normalizeLegacyPrpStructuredRunResult(value: unknown): unknown {
|
||||
const source = record(value);
|
||||
if (Object.keys(source).length === 0) return value;
|
||||
const normalized: Record<string, unknown> = { ...source };
|
||||
// Some provider transports require all tool properties to be present. Null
|
||||
// explicitly means no continuation; preserve every non-null value so strict
|
||||
// canonical validation still rejects contradictory or malformed waits.
|
||||
if (normalized.continuation === null) {
|
||||
delete normalized.continuation;
|
||||
}
|
||||
// Provider tool-callers do not consistently echo constant discriminator
|
||||
// fields even when they are present in the advertised JSON schema. The
|
||||
// transport already selected the semantic-result tool, so adding its
|
||||
|
||||
Reference in new issue
Block a user