From 7fdbee36e05f7874236f604d6d2aabdbe95070da Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Fri, 11 Sep 2026 11:06:03 -0700 Subject: [PATCH] fix(interface): bound a turn prompt as content, not as metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `prompt` was `boundedStringSchema` — CONTRACT_MAX_STRING_LENGTH, 16,384 characters. That is the bound for identifiers and accounting fields. A prompt is what an agent is asked to do: its size is set by the work, not by the protocol. Held to the metadata bound it silently capped what a manager could delegate. Measured 2026-09-11 in a live run already carrying both prior bound fixes (agent-provider-tangle 1.1.10, agent-runtime 0.214.0): two of ten child deaths were `Too big: expected string to have <=16384 characters` at path ["prompt"], refusing a director that was handing an independent-checker child a 27 KB patch so it could rebuild and re-verify a measured result. This is the third time agent-authored content has been held to the metadata bound — tool output twice (#311, #312), now the prompt. The distinguishing test is worth stating once: if an agent or a tool produced it, it is content; if it names, routes, or accounts for the turn, it is metadata. `context` keeps the metadata bound on exactly that reading. Moved at all four sites: AgentTurnInput, the runtime-control prompt command, portable context continuation, and interactive session control. The interactive site kept its non-empty requirement as a refinement, because the content schema is a custom schema and carries no `.min`. Truncation is deliberately NOT used here, unlike tool output: a silently shortened instruction is worse than a refused one. Only the ceiling moves, to where the producer's own limits already sit. Tests: three added to the turn-input bounds case, failing on the previous code. agent-interface suite 546 pass. `src/certified-context.test.ts` has one pre-existing failure that reproduces identically on unmodified main. Closes #313. Co-Authored-By: Claude Opus 5 (1M context) --- packages/agent-interface/package.json | 2 +- .../src/environment-interactive-control.ts | 9 ++++++++- packages/agent-interface/src/environment-runtime.ts | 7 ++++++- packages/agent-interface/src/leaf-modules.test.ts | 12 ++++++++++++ .../src/portable-context-continuation.ts | 6 ++++-- packages/agent-interface/src/runtime-control.ts | 3 ++- 6 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/agent-interface/package.json b/packages/agent-interface/package.json index d51b6bc1..a5b4ae93 100644 --- a/packages/agent-interface/package.json +++ b/packages/agent-interface/package.json @@ -1,6 +1,6 @@ { "name": "@tangle-network/agent-interface", - "version": "2.6.1", + "version": "2.7.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/packages/agent-interface/src/environment-interactive-control.ts b/packages/agent-interface/src/environment-interactive-control.ts index 5290d549..feb36805 100644 --- a/packages/agent-interface/src/environment-interactive-control.ts +++ b/packages/agent-interface/src/environment-interactive-control.ts @@ -8,6 +8,7 @@ import { } from "./agent-execution-preparation-receipt.js"; import { boundedIdentifierSchema, + boundedEventContentStringSchema, boundedStringSchema, } from "./contract-limits.js"; import { @@ -546,7 +547,13 @@ const AgentInteractiveSessionPromptCommandMaterialSchema = z operationId: boundedIdentifierSchema, ref: AgentInteractiveSessionRefSchema, control: AgentInteractiveSessionControlClaimSchema, - prompt: boundedStringSchema.min(1), + // Content, not metadata: see environment-runtime.ts. + // Content, not metadata: see environment-runtime.ts. `boundedEventContentStringSchema` is a + // custom schema and carries no `.min`, so the non-empty requirement is kept as a refinement — + // an interactive prompt with nothing in it is still refused. + prompt: boundedEventContentStringSchema.refine((value) => value.length >= 1, { + message: "interactive prompt must not be empty", + }), }) .superRefine((command, refinement) => { if (!agentInteractiveSessionControlClaimMatchesRef(command.ref, command.control)) { diff --git a/packages/agent-interface/src/environment-runtime.ts b/packages/agent-interface/src/environment-runtime.ts index 82cdd91e..de7b7d77 100644 --- a/packages/agent-interface/src/environment-runtime.ts +++ b/packages/agent-interface/src/environment-runtime.ts @@ -55,7 +55,12 @@ export interface AgentTurnInput { } export const AgentTurnInputSchema = z.strictObject({ - prompt: boundedStringSchema.optional(), + // A prompt is what the agent is asked to do, so it is CONTENT: its size is set by the work, + // not by the protocol. Held to the metadata bound it capped a manager's brief at 16,384 + // characters, which refused a director handing a checker a 27 KB patch to re-verify a measured + // result (agent-sdk#313). Refusal is still right here — a silently shortened instruction is + // worse than none — so only the ceiling moves. + prompt: boundedEventContentStringSchema.optional(), parts: z.array(InputPartSchema).max(CONTRACT_MAX_ARRAY_LENGTH).optional(), sessionId: boundedIdentifierSchema.optional(), model: boundedIdentifierSchema.optional(), diff --git a/packages/agent-interface/src/leaf-modules.test.ts b/packages/agent-interface/src/leaf-modules.test.ts index fecb5671..99730a5d 100644 --- a/packages/agent-interface/src/leaf-modules.test.ts +++ b/packages/agent-interface/src/leaf-modules.test.ts @@ -555,6 +555,18 @@ describe("interface split leaf modules", () => { expect(() => AgentTurnInputSchema.parse({ prompt: "x", context: { huge: "x".repeat(CONTRACT_MAX_STRING_LENGTH + 1) } }), ).toThrow(); + // A prompt is CONTENT: its size is set by the work, not by the protocol. Held to the metadata + // bound it capped a manager's brief at 16,384 characters, which refused a director handing a + // checker a 27 KB patch to re-verify a measured result (agent-sdk#313). `context` above keeps + // the metadata bound, because that is material describing the turn rather than the ask itself. + expect(() => + AgentTurnInputSchema.parse({ prompt: "x".repeat(CONTRACT_MAX_STRING_LENGTH + 1) }), + ).not.toThrow(); + expect(() => AgentTurnInputSchema.parse({ prompt: "x".repeat(200_000) })).not.toThrow(); + // The content bound still governs it, so an unbounded prompt is still refused. + expect(() => + AgentTurnInputSchema.parse({ prompt: "x".repeat(2 * 1024 * 1024) }), + ).toThrow(); for (const usageMode of ["delta", "cumulative"]) { expect(AgentTurnResultSchema.parse({ text: "x", success: true, diff --git a/packages/agent-interface/src/portable-context-continuation.ts b/packages/agent-interface/src/portable-context-continuation.ts index a341b784..817395c7 100644 --- a/packages/agent-interface/src/portable-context-continuation.ts +++ b/packages/agent-interface/src/portable-context-continuation.ts @@ -5,7 +5,8 @@ import { type AgentExactRunControlRef, } from "./runtime-control.js"; import { idSchema, InputPartSchema, jsonRecordSchema, sha256DigestSchema, wireDigest } from "./portable-context-shared.js"; -import { boundedStringSchema, CONTRACT_MAX_ARRAY_LENGTH } from "./contract-limits.js"; +import { boundedEventContentStringSchema, + boundedStringSchema, CONTRACT_MAX_ARRAY_LENGTH } from "./contract-limits.js"; export const NativeContextBoundarySchema = z .discriminatedUnion("kind", [ @@ -78,7 +79,8 @@ export interface NativeContextContinuationTurn { } export const NativeContextContinuationTurnSchema = z.strictObject({ - prompt: boundedStringSchema.optional(), + // Content, not metadata: see environment-runtime.ts. + prompt: boundedEventContentStringSchema.optional(), parts: z.array(InputPartSchema).max(CONTRACT_MAX_ARRAY_LENGTH).optional(), model: idSchema.optional(), context: jsonRecordSchema.optional(), diff --git a/packages/agent-interface/src/runtime-control.ts b/packages/agent-interface/src/runtime-control.ts index 21ba2829..06465fa1 100644 --- a/packages/agent-interface/src/runtime-control.ts +++ b/packages/agent-interface/src/runtime-control.ts @@ -399,7 +399,8 @@ const partSchema = z.discriminatedUnion("type", [ z.strictObject({ ...partBase, type: z.literal("subtask"), - prompt: boundedStringSchema, + // Content, not metadata: see environment-runtime.ts. + prompt: boundedEventContentStringSchema, description: boundedStringSchema, agent: stableIdSchema, }),