Plumb toolChoice from an agent option to the wire — pi-ai already carries the field
#7090
Replies: 1 comment
|
The field is genuinely absent end to end, and your four hops are the right four. Two corrections: the typing wrinkle does not hold against the pinned pi-ai, and the blast radius is wider than the pi-ai adapter if the DeepSeek route has to work too. Where it actually breaks
The durability half is right, and the seam is sharper than "the request type"The loop builds each request as Where I have to disagree — the typing wrinkleAgainst
(The So "the simple entry is declared without a tool-choice field" does not hold for this version, and nothing needs to swap to the full entry on that account. What is true, and is the real block for your use case, is that the neutral value set is narrower than the wire: The route question is load-bearingIf the deployment runs the Why it is missing rather than broken
Not verified here: the captured-body |
Uh oh!
There was an error while loading. Please reload this page.
Headline
toolChoiceis a field the SDK already carries and maps onto the wire; dsh drops it in fourhops, so no agent option can require a tool call. Please plumb the existing field from an
agent option through to the request body, and record it in the durable request header so a
resumed session rebuilds the identical request.
This asks for no new capability. The OpenAI-compatible completions entry that dsh already
calls forwards a tool choice today — dsh simply never sets one, and the allow-list in the
adapter forwards only what it names.
Why it matters
A delegation agent whose rule is "nothing is claimed to work until the simulator ran it" has
to be able to compel the call that runs the simulator. Without a wire-level requirement the
rule rests entirely on persona prose, which a model can ignore — and here one did: a model
answered with a confidently fabricated component value,
finish_reason: stop, and no toolcall, twice. Prompt wording cannot distinguish "I ran it" from "I made it up"; a required tool
call can.
Evidence that the field is already carried (the negative control is clean)
The dependency was driven against a local mock endpoint over the OpenAI-compatible completions
protocol, one tool offered, a prompt telling the model to call it. Lower-cased key list of the
captured body, unrelated keys only:
A is the negative control: the field is absent unless asked for, so an assertion on it cannot
pass vacuously.
The four hops that drop it, in order
only provider, model and a token cap.
is also why the requirement cannot ride the durable request header today, which is the
next section.
therefore forwards only what it names.
The durability constraint — why this is one change and not two
The option and the durable header are one decision. Adding the field to the request type
without adding it to the call configuration compiles, works in the happy path, and silently
loses the requirement on resume: the session restores a request that no longer compels
anything, and the failure looks like a model ignoring an instruction rather than a field that
was dropped. The header the session logs must therefore carry the requirement, so a resumed
session rebuilds the identical request — and a change to it should be a logged header change,
so "when was this agent compelled?" is answerable from the log.
The typing wrinkle that must not be papered over
The protocol's simple stream entry — the one dsh calls — is declared without a
tool-choice field, while its implementation forwards one into the full stream entry, which is
declared with it and sets the body field. So the behaviour is real but only half-declared.
dsh calls the simple entry on purpose: it is the entry that maps the harness's reasoning level
onto the protocol's effort. The fix therefore belongs in the options that the simple entry
forwards, pinned by a test that asserts the body — not in a swap to the full entry, which
would trade this bug for a reasoning-level one.
The shape that works, as shipped locally
Four hops plus one agent option (
toolChoice: 'none' | 'auto' | 'required' | {function name})carried on the subagent tool row; the requirement lands in the child's call configuration and
in the durable request header; a compelled turn that answers in prose fails with a typed,
non-retryable error handed to the delegating agent instead of being accepted as success. It
has run in this deployment for several days over the free tier.
How it is verified here — keyless, at the highest seam
The mock HTTP endpoint above is already in daily use by the adapter spec, so every assertion
runs against it with a stubbed key and no network:
and that assertion must be watched failing before the implementation lands.
tool choice; a route whose protocol cannot carry it is refused before any request exists,
by a typed error naming the protocol, with the mock endpoint receiving nothing.
typed content failure; with the requirement absent the identical completion stays a success.
That pair is what proves the failure is caused by the requirement and not by the fixture.
Local commits on the deployment branch, each with its specs:
5676b24(carry a per-agent toolchoice to the request header and the wire),
e7a3c4d(a prose answer fails the compelledturn),
3039b1f(refuse a tool choice the route's protocol cannot carry) and9da8f42(pinthe per-protocol capability table). Specs:
packages/subagent/tool-subagent/tests/tool-choice-wire.spec.ts,packages/llm/llm/tests/tool-choice-capability.spec.ts,packages/llm/llm-pi-ai/tests/tool-choice-capability.spec.ts.Adjacent, and deliberately NOT part of this request
anthropic-messagesspells the same idea differently:anywhere the completions protocolsays
required, and a flat{type: 'tool', name}where ours is{type: 'function', function: {name}}. Plumbing the field through exposes that gap ratherthan creating it, so the local change declares per protocol and per value what an
implementation forwards and refuses a declared kind the route cannot carry — safe, and a
failure that names the protocol instead of a malformed body at a paid endpoint.
The translation itself (widen the declared carrying set, map the two spellings, assert the
captured body at the same seam) is a separate change, tracked locally on its own ticket.
It is noted here only so a reviewer is not surprised by the refusal; bundling it would widen
this review for no gain, because accepting plumbing should not require deciding a
protocol-vocabulary question.
Out of scope for this request
All reactions