Skip to content

fix(agent): restore the hook-free AgentRun integration surface - #2284

Open
gold-silver-copper wants to merge 3 commits into
mainfrom
fix/agent-run-integration-surface
Open

fix(agent): restore the hook-free AgentRun integration surface#2284
gold-silver-copper wants to merge 3 commits into
mainfrom
fix/agent-run-integration-surface

Conversation

@gold-silver-copper

Copy link
Copy Markdown
Contributor

Restore the focused low-level capabilities lost when Agent fields and raw
completion construction became private: callers manually driving AgentRun can
seed a run from durable agent policy, prepare one configured hook-free
request, pair its response and tool calls with the exact turn
metadata/implementation snapshot, and clone the agent's live ToolServerHandle
when rebuilding after a resume. AgentRunner remains the only
batteries-included runtime; this PR adds no second coordinator.

What's restored

In v0.40 a manual AgentRun driver could use an already-built Agent as the
configuration and tool source for its loop; v0.41 removed that when Agent
fields went private and the raw Completion/StreamingCompletion impls were
deleted. This PR restores exactly that capability, additively:

  • Agent::tool_server_handle() — owned clone of the live registry handle
    (the capability to use after a cross-process resume; observes later registry
    changes).
  • Agent::new_run(prompt) — a plain AgentRun seeded with the agent's
    durable run policy, constructed through the same build_agent_run path the
    runner uses, so the two cannot drift.
  • Agent::prepare_completion_request(prompt, history, &mut run) — one fully
    configured, hook-free provider request, fed directly from
    AgentRunStep::CallModel's fields. Reads the run's committed output-tool
    name and commits the freshly resolved name back (exactly as the classic
    driver does), so multi-turn Tool-mode pinning and run/request output policy
    stay paired. Impossible tool choices fail before any provider IO.
  • PreparedAgentRequest::into_parts() → the sendable request builder plus an
    opaque PreparedAgentTurn owning exactly two operations:
    • model_turn(response) supplies the exact executable/allowed tool-name
      sets captured at preparation (no more hand-ordering two same-typed sets);
    • execute_call(call, &mut context) honors preresolved_result, applies
      the standard clear→dispatch→accept context hygiene, and executes only
      through the registry snapshot pinned at preparation — a tool
      re-registered afterwards is unreachable, a tool registered afterwards is
      rejected.

The prepared pair is in-process state for one issued request — not
serializable, no durability claims. Durable state remains the AgentRun.
Hook-free means hook-free: no hooks, memory, telemetry spans, concurrency
policy, or passive dynamic context run on this surface, and the docs say so.

Docs and tests

  • examples/agent_run_stepping rewritten around the new surface, including a
    cross-process resume that finishes pending calls via the rebuilt agent's
    tool_server_handle().
  • run module docs gained the Agent ⇄ AgentRun ⇄ PreparedAgentRequest/Turn
    relationship diagram and the list of what manual driving makes the caller
    own; MIGRATING.md gained a matching "0.41 → next" section (DynamicTool
    remains the replacement for Box<dyn ToolDyn>).
  • 14 in-crate tests cover: agent-built tool access, configured preparation,
    exact name-set handoff, snapshot fidelity, seeded-run policy agreement
    (output mode/tool name, output validation, turn and retry budgets, pre-IO
    tool-choice validation), the hook-free boundary (plus a guard that the same
    hook still fires under AgentRunner), a full manual round trip,
    pre-resolved hygiene, and a cross-process resume guard.

Verified: cargo fmt, full rig-agent suite, clippy --all-targets --all-features -D warnings (rig-agent + example), rustdoc with warnings
denied, git diff --check.

Supersedes the direction of #2278 for fixing this regression (analysis:
https://gist.github.com/gold-silver-copper/5bdcbc6afbab072cdb93fb614b719fc5);
no AgentDriver/DriveStep/TurnTools and no second coordinator.
(on_reasoning_delta was already fixed by #2282.)

Restore the focused low-level capabilities lost when Agent fields and raw
completion construction became private: callers manually driving AgentRun can
seed a run from durable agent policy, prepare one configured hook-free
request, pair its response and tool calls with the exact turn
metadata/implementation snapshot, and clone the agent's live ToolServerHandle
when rebuilding after a resume. AgentRunner remains the only
batteries-included runtime; this PR adds no second coordinator.
…rface

- build_prepared_completion_request takes a named-field PreparedRequestInputs
  struct instead of 17 positional arguments, so same-typed parameters cannot
  be silently transposed at either call site.
- PreparedAgentTurn::execute_call dispatches through a new
  ToolRegistrySnapshot::execute that mirrors ToolServerHandle::execute,
  instead of hand-rolling a third copy of the clear/dispatch/accept sequence.
- Agent::new_run calls build_agent_run directly with defaults shared with
  AgentRunner::from_agent as named constants, instead of deep-cloning a
  throwaway runner.
- The example's resume loop holds one ToolContext for the whole run, matching
  Part 1's stated convention and the classic runner.
…view findings

- prepare_completion_request now advertises the run's tool choice (via a new
  pub(crate) AgentRun::tool_choice accessor), so an AgentRun::with_tool_choice
  override after new_run reaches the provider and the run never classifies
  calls under a policy the request didn't carry; covered by a new test.
- The #1928 output-tool read/commit pairing now lives once, in
  build_prepared_completion_request_for_run, shared by the classic driver and
  the manual surface; the committed name is borrowed, not cloned.
- PendingToolCall::result_content is a new public helper correlating an
  executed tool's output with its call (id, provider call id, tool name), so
  the cross-process-resume recipe no longer hand-copies correlation fields;
  the example, tests, and execute_call use it.
- ToolDispatch::publish owns the shared accept/return half of the dispatch
  context-hygiene contract for all three execute surfaces; the clear stays
  before dispatch, as the cancelled-dispatch hygiene tests pin.
- PreparedRequestInputs.chat_history is Cow, so the manual surface moves the
  owned history from CallModel instead of re-cloning it each turn; the
  snapshot dispatch takes owned args, dropping a per-call copy of the
  argument JSON.
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