Skip to content

fix(providers): require an interface that bounds a prompt as content - #320

Merged
drewstone merged 1 commit into
mainfrom
fix/provider-requires-interface-2.8
Sep 11, 2026
Merged

fix(providers): require an interface that bounds a prompt as content#320
drewstone merged 1 commit into
mainfrom
fix/provider-requires-interface-2.8

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

The defect

@tangle-network/agent-provider-tangle is the code that validates a turn, so the
@tangle-network/agent-interface copy it resolves decides what a turn may contain — not the copy
the consumer installed at its own top level. It declared ^2.6.1, which still admits the release
whose bug it was shipped to escape.

agent-interface 2.7.0 (#314) moved every prompt field off the 16 KiB metadata bound
(CONTRACT_MAX_STRING_LENGTH) onto the 1 MiB content bound (boundedEventContentStringSchema),
because a manager's brief could not fit and every oversized turn died at zero iterations. 2.8.0
(#315) made the contract bounds public.

Measured 2026-09-11 in the Discovery Lab: a consumer whose lockfile had already resolved 2.6.1 for
the provider, and which then raised its own top-level agent-interface to 2.8.0, ended up with two
copies
— agent-runtime on 2.8.0, the provider on 2.6.1. Because the provider validates, children
kept dying on the 16 KiB bound with a ZodError at path: ["prompt"] on a stack whose package.json
said the bug was fixed: 14 of 34 children of one run and 8 of 52 of another, every one at
iterations 0, surfaced as "retained provider execution requires reconciliation before replacement".
Confirmed by reading the pnpm store links: the provider's own
node_modules/@tangle-network/agent-interface pointed at 2.6.1 on agent-runtime 0.217.1 +
agent-provider-tangle 1.1.11 + agent-interface 2.8.0. The published latest, 1.2.0, still declares
^2.6.1.

Ranges changed, and the ones deliberately left

Each package was decided on whether its own validation depends on the 2.7.0/2.8.0 change.

Package Range Why
agent-provider-tangle ^2.6.1^2.8.0 promptFromTurnInput parses every turn with AgentTurnInputSchema, sendPrompt parses every interactive command with AgentInteractiveSessionPromptCommandSchema — both are 2.7.0 sites. It also imports CONTRACT_MAX_STRING_LENGTH, CONTRACT_MAX_IDENTIFIER_LENGTH and CONTRACT_MAX_ARRAY_LENGTH, which only became public in 2.8.0, so the floor is the later release.
agent-provider-cli-bridge ^2.4.0^2.7.0 prepareCliBridgeRun parses every turn with AgentTurnInputSchema. Same exposure, same failure mode. It imports none of the constants 2.8.0 exported, so its floor is where the bound it needs actually changed.
agent-core ^2.0.0 unchanged Consumes interaction schemas and stream event types. The interaction-answer bound in interaction-answer-validation.ts stayed on CONTRACT_MAX_STRING_LENGTH in 2.7.0 by design; nothing it validates moved.
agent-provider-e2b, agent-provider-daytona ^2.4.0 unchanged Type-only imports from agent-interface/environment-provider. They validate no turn.
agent-provider-computesdk ^2.4.0 unchanged Parses WorkspaceRequestSchema only.
agent-provider-testkit ^2.4.0 unchanged Its interactive conformance builds prompt commands, but asserts digest replay rather than size, and its prompts are metadata-sized. A prompt digest is content-addressed and identical across the bound change, so a testkit on an older copy cannot disagree with a provider on a newer one.
agent-trace-contract n/a No agent-interface dependency.

Making the split detectable

packages/agent-provider-tangle/src/producer-consumer-bounds.test.ts (#315) cannot see this. It
reads the shipped Sandbox dist and compares it against contract constants it imports, and
pnpm-workspace.yaml pins overrides["@tangle-network/agent-interface"]: workspace:*, so inside
this repo it always links the workspace copy and never sees a consumer's older resolution.

packages/agent-provider-tangle/src/resolved-interface-floor.test.ts checks the other half.
Behaviour first, then the versions:

  1. promptFromTurnInput accepts a prompt one character over the metadata bound — the exact call
    that killed the children above.
  2. AgentInteractiveSessionPromptCommandSchema reports no issue at path: ["prompt"] for an
    oversized prompt. The command is otherwise invalid on purpose; the assertion is only that its
    length is not one of the reasons.
  3. The resolved copy exports the contract bounds producer-consumer-bounds.test.ts imports by
    name. On an older copy that file does not fail, it fails to load, which is how the seam it
    guards silently stops being checked.
  4. The agent-interface this package resolves is at or above the floor the package declares. It is
    found by walking the node_modules chain, because agent-interface's exports map has no
    ./package.json subpath and no require condition, so require.resolve cannot reach it.
  5. The declared floor is at or above the release its validation needs. Without this, cases 1–3 pass
    for the wrong reason in this repo: the override means a range lowered back to ^2.4.0 would
    still resolve something new enough, and only a consumer would find out.

It imports agent-interface as a namespace on purpose: a named import of a constant an old
resolution does not export fails at link time with a SyntaxError naming no package, no version and
no reason, and this file exists to diagnose exactly that resolution.

Proof, not assertion

End to end, with the published artifacts and no override. npm installing the published
@tangle-network/agent-provider-tangle@1.2.0 beside @tangle-network/agent-interface@2.6.1 lets npm
dedupe onto the single 2.6.1 copy, because ^2.6.1 admits it. Calling the provider's own
dist/tangle-prompt.js with a 20,000-character prompt:

provider version   : 1.2.0
provider declares  : ^2.6.1
provider resolves  : 2.6.1
20,000-char prompt : REFUSED [{"code":"too_big","maximum":16384,"path":["prompt"]}]

The same package built from this branch, in the same tree, forces npm to nest its own copy:

provider version   : 1.2.0
provider declares  : ^2.8.0
provider resolves  : 2.9.0   (nested under the provider)
20,000-char prompt : ACCEPTED (unchanged)

The new check against a deliberately old resolution. A scratch copy of the package with
@tangle-network/agent-interface pinned to 2.6.1 while the manifest still declares ^2.8.0 fails
4 of 5 cases, each with its own reason, including the production error verbatim:

× accepts a turn prompt larger than the metadata bound
    ... still holds a turn prompt to the 16384-character metadata bound ...
    thrown: too_big, maximum 16384, path ["prompt"]
× does not refuse an interactive prompt for its length
× exports the contract bounds this package validates its own limits against
    ... does not export CONTRACT_MAX_STRING_LENGTH, which became public in 2.8.0 ...
× resolves an agent-interface at or above the range this package declares
    ... resolves 2.6.1, below its declared floor 2.8.0 ...

Against the published 2.9.0 in the same scratch project: 5 of 5 pass. Lowering the declared range
back to ^2.6.1 while resolving 2.9.0 fails only case 5, which is the regression this repo's
override would otherwise hide.

Checks, run locally

Serialized with --fileParallelism=false.

Suite Result
agent-provider-tangle (vitest run src) 285 pass in 20 files, up from 280 in 19 on the base
agent-provider-cli-bridge 214 pass in 10 files
agent-provider-testkit 27 pass in 3 files
agent-core 506 pass in 22 files
agent-trace-contract 182 pass in 4 files
agent-provider-computesdk / daytona / e2b 6 / 4 / 4 pass
agent-interface 554 of 555 — src/certified-context.test.ts fails 1 of 12 identically on unmodified origin/main in this worktree, checked by stashing and rerunning
pnpm -r build, pnpm -r check-types pass
pnpm check:package-artifacts pass — 9 isolated packages, 31 exports, 62 export targets
pnpm check:control-artifacts pass — 183 tests over the packed interface, testkit and tangle contracts
pnpm install --frozen-lockfile pass, pnpm-lock.yaml unchanged (the workspace override records workspace:*, so raising a published range does not move it)

Release

Through changesets, not by hand: .changeset/provider-requires-content-bound-prompt.md marks both
providers patch. changeset status --since=origin/main confirms
@tangle-network/agent-provider-tangle 1.2.0 → 1.2.1 and
@tangle-network/agent-provider-cli-bridge 1.0.2 → 1.0.3. The Release workflow versions and
publishes on merge; nothing version-bearing is edited in this PR.

🤖 Generated with Claude Code

`@tangle-network/agent-provider-tangle` is the code that VALIDATES a turn, so the
agent-interface copy IT resolves decides what a turn may contain — not the copy the
consumer installed at its own top level. It declared `^2.6.1`, which still admits the
release whose bug it was shipped to escape.

agent-interface 2.7.0 (#314) moved every `prompt` field off the 16 KiB metadata bound
(`CONTRACT_MAX_STRING_LENGTH`) onto the 1 MiB content bound, and 2.8.0 (#315) made the
contract bounds public. Measured 2026-09-11: a consumer whose lockfile already held 2.6.1
for this package, and which then raised its own top-level agent-interface to 2.8.0, ended
up with two copies — agent-runtime on 2.8.0, the provider on 2.6.1. Because the provider
validates, children kept dying on the 16 KiB bound with a ZodError at `path: ["prompt"]`
on a stack whose package.json said the bug was fixed: 14 of 34 children of one run and 8
of 52 of another, every one at iterations 0, surfaced as "retained provider execution
requires reconciliation before replacement".

Ranges raised, each decided on whether the package's own validation depends on the change:

  agent-provider-tangle      ^2.6.1 -> ^2.8.0  parses every turn with AgentTurnInputSchema
                                               and every interactive command with
                                               AgentInteractiveSessionPromptCommandSchema;
                                               also imports the bounds 2.8.0 made public.
  agent-provider-cli-bridge  ^2.4.0 -> ^2.7.0  prepareCliBridgeRun parses every turn with
                                               AgentTurnInputSchema. Same exposure, and
                                               2.7.0 is where the bound it needs arrived.

Left alone, each checked rather than assumed:

  agent-core           ^2.0.0  interaction schemas and stream event types only; the
                               interaction-answer bound did not move in 2.7.0.
  agent-provider-{e2b,daytona} ^2.4.0  type-only imports; validate no turn.
  agent-provider-computesdk    ^2.4.0  parses WorkspaceRequestSchema only.
  agent-provider-testkit       ^2.4.0  builds short conformance prompts and asserts digest
                               replay, not size; a prompt digest is content-addressed and
                               identical across the bound change.
  agent-trace-contract         no agent-interface dependency.

`producer-consumer-bounds.test.ts` (#315) cannot see this split: it reads the shipped
Sandbox dist and compares it against contract constants it imports, and the workspace pins
`overrides["@tangle-network/agent-interface"]: workspace:*`, so in this repo it always
links the workspace copy. `resolved-interface-floor.test.ts` checks the other half —
behaviour first, then the versions:

  - `promptFromTurnInput` accepts a prompt one character over the metadata bound;
  - `AgentInteractiveSessionPromptCommandSchema` reports no issue at `path: ["prompt"]`
    for an oversized prompt;
  - the resolved copy exports the contract bounds `producer-consumer-bounds.test.ts`
    imports by name — on an older copy that file does not fail, it fails to LOAD;
  - the agent-interface this package resolves is at or above its declared floor;
  - the declared floor is at or above the release the validation needs, which is what the
    workspace override would otherwise hide.

It imports agent-interface as a namespace on purpose: a named import of a constant an old
resolution does not export fails at link time with a SyntaxError naming no package, no
version and no reason, and this file exists to diagnose exactly that resolution.

Proven against a deliberately old resolution rather than only asserted. The published
provider 1.2.0 installed beside agent-interface 2.6.1, with no override, shares the 2.6.1
copy and refuses a 20,000-character prompt with `too_big maximum 16384 path ["prompt"]`;
this branch's tarball, in the same tree, makes npm nest 2.9.0 under the provider and the
same prompt is accepted unchanged. The new test file, run against an installed 2.6.1,
fails 4 of its 5 cases with those messages, and passes 5 of 5 against the published 2.9.0.

Tests, serialized (`--fileParallelism=false`): agent-provider-tangle 285 pass in 20 files,
up from 280 in 19; cli-bridge 214; testkit 27; agent-core 506; trace-contract 182;
computesdk 6; daytona 4; e2b 4. agent-interface 554 of 555 — `certified-context.test.ts`
fails 1 of 12 identically on unmodified origin/main in this worktree. `pnpm -r check-types`,
`pnpm -r build`, `pnpm check:package-artifacts` (9 packages, 31 exports, 62 export targets)
and `pnpm check:control-artifacts` (183 tests) all pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@drewstone
drewstone merged commit ba7a885 into main Sep 11, 2026
2 checks passed
@drewstone
drewstone deleted the fix/provider-requires-interface-2.8 branch September 11, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant