symfonic.kernel.tool_round¶
tool_round ¶
Executing one round's calls, and saying so as it goes.
Split out of :mod:symfonic.kernel.runner when that module reached its size
budget. The concept is a real one rather than a place to put lines: a round
is the unit that runs a batch of reserved calls and emits a tool_call
before each and a tool_result after it, so a consumer of the event stream
sees the same interleaving the tool port saw.
The outcomes come back in request order. record_tools and the post-tool
rung both join results to calls positionally, so a round that returned them
in completion order would attach a result to the wrong call and nothing
downstream could tell.
And it is where a precondition gets to object. plan.bindings.tool_preconditions are checks a capability contributed, and this is the one
point the kernel dispatches a call -- so a check here judges the call that is
about to run: after reserve_requests assigned its id and after every
pre-tool stage has amended it. Judging earlier would judge the call the
model asked for, and an amendment could then walk a refused call past the rule
that refused it.
Three answers, not two. A check may object, in which case the port is asked for
a refusal and the tool is never invoked; it may admit; or it may declare that
the rules themselves could not be reached. The third admits and reports that
no decision was made, so a stage event says the check did not happen -- a
degradation that looked like an admission would be a gate reporting success for
a question it never asked.
A check that raises does not get the third answer. An enforcement fault
must not become a permission: a malformed predicate, an evaluator that threw, a
state the rule cannot read -- each is a gate that failed while a rule existed,
and admitting on them turns a broken gate into an open one. The narrow case the
third answer covers is that the store of rules was unreachable, which the
check declares with PreconditionVerdict.unavailable; only the check knows
which of the two happened, so only the check may say.
run_tool_round
async
¶
run_tool_round(plan: Any, requests: Sequence[Any], *, emitter: Any, publish: Callable[[Any], Any], lifecycle: Any, outcomes: list[Any]) -> AsyncIterator[Any]
Run requests in order, yielding each call and its result.
outcomes is filled rather than returned because this is a generator:
the caller needs the results after the stream is drained, and an async
generator cannot hand back a value the way a coroutine can.