Skip to content

feat(runtime): tell the model its limits, before the turn and at them - #122

Merged
DaXu999 merged 6 commits into
mainfrom
feat/limit-notice
Nov 9, 2026
Merged

DaXu999 merged 6 commits into
mainfrom
feat/limit-notice

Conversation

@DaXu999

@DaXu999 DaXu999 commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

Why

Since #118 a turn has no limit unless the host sets one. When the host does, the model learned of it only by running into it: a call past maxToolCalls came back denied with "The runtime reached its maximum number of tool calls", which did not say the budget was gone for the turn, and the standard loop kept calling the model until a step limit or the deadline, each step billed. The model was never told its budget in advance, and nothing told it the turn was draining at its deadline.

What

  • The model is told its limits up front. New describeLimits in @aicoo/sharedos-runtime renders the effective limits as plain sentences, and the default seat instructions in @aicoo/sharedos-adapters append them after the host guidance and the reach. The MCP seat text says the budget covers calls to this server's tools, since the harness's own tools never reach SharedOS. RuntimeTurnRequest.options carries the limits the turn runs under.
  • A reached limit says so, truthfully on every path. tool_call_limit_exceeded: "This turn's budget of N tool calls is used up. No further call to these tools will run this turn; finish with what you have." step_limit_exceeded names the step past budget. turn_draining: "The turn is ending at its deadline and takes no new calls to these tools; finish with what you have.", without "at its deadline" when an audit outage drains it. When the catalogue offers sharedos.escalate, each adds ", except asking for a human with sharedos.escalate".
  • The model decides when to stop. No forced closing step: a steps-only budget is described as steps, never as refused, and the text tells the model to keep its last step for the answer.
  • Conformance regenerated: only the prompt-set hashes moved, no cell changed. Docs: errors, host integration, MCP toolshare, architecture, quickstart, adapters README; docs/api regenerated; CHANGELOG entry under Unreleased.

23 files: 1,107 lines added, 136 removed. Of those, regenerated docs/api is 174 added and 93 removed, and tests are 553 added and 3 removed.

What a host has to update

  • Nothing changes about when a turn ends. A host with its own driver may call describeLimits(request.options, { escalation: escalationOffered(request.tools) }) to tell its model the same.
  • Tests that assert the old refusal messages need the new texts, listed in docs/errors.md.

Related

Follow-up to #118, as announced there. Sibling of #120 and #121 off main.

Checks

  • pnpm check is green: format, typecheck, vitest 1209, test:release 7/7, docs:api:check and conformance:check (172 pass, 20 not applicable, 12 not implemented, manifest up to date).
  • Reviewed by a second agent; the wording cases it found false on some path are fixed, with a test for each.

A model was not told its budget in advance, and a refusal at the limit did
not say that later calls would be refused too, so it was left to work that
out. This changes what the model is told and nothing about when a turn ends.

Before the turn, describeLimits renders the limits the host set: the tool
calls the turn may make, its steps (each tool call and the final answer),
and its deadline as a length of time, with the closing advice to finish with
what it has once a budget is spent. Unset limits say nothing. The executor
now hands the runtime a request whose options are the limits it enforces,
the request's own or the executor's default, so a driver that sees only the
request can tell its model. Every seat appends the text after the reach; the
MCP harness runtime leaves out the step budget, since none binds it.

At the limit, tool_call_limit_exceeded and step_limit_exceeded name the
budget and say no further tool call will run this turn, and turn_draining
says the turn is ending at its deadline, or just ending on an audit outage.
The codes are unchanged.

The conformance prompt set hashes move because the scripted columns set
limits and their seats are now told them; no cell changed.
…ery path

The step refusal claimed no further tool call would run, but it refuses only
a call that names a step past the budget; a later call inside it still runs.
It now says which step the call named, that it did not run, and that no call
past the budget will.

Asking for a human ends the turn without passing through the budgets, so on
a turn that offers sharedos.escalate the call-budget refusal, the deadline
drain and describeLimits name it as the exception. An audit outage drain
does not, since the ask could not be recorded.

describeLimits says a spent call budget refuses every further call only when
maxToolCalls is set. For steps it says that a turn whose last step is a tool
call ends without an answer, so keep the last step for the answer. Over MCP
the budget is named as calls to this server's tools, since the harness's own
tools count against nothing.

errors.md now separates the model-facing step refusal from the standard
loop's turn-ending text. The conformance prompt set hashes move again with
the new wording; no cell changed.
A harness over MCP has tools of its own that never reach SharedOS and keep
running after the budget is spent, so "No further tool call will run this
turn" was false there. The call-budget refusal now says "No further call to
these tools will run this turn", and both drain refusals say the turn "takes
no new calls to these tools". The escalation clause and the closing advice
are unchanged, and in the standard loop every tool the model has is one of
these, so the words stay true on both paths.
@DaXu999
DaXu999 merged commit 6640db9 into main Nov 9, 2026
2 checks passed
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