feat(tool,runner): async tools — pause/resume, typed tool.Func, pre-1.0 API cleanups#147
feat(tool,runner): async tools — pause/resume, typed tool.Func, pre-1.0 API cleanups#147weeco wants to merge 6 commits into
Conversation
This comment has been minimized.
This comment has been minimized.
eefb172 to
3bb7ea3
Compare
…pause/resume
Tools (and interceptors) can pause an invocation — for external jobs,
human approval, or user input — and resume it later, across process
boundaries.
- tool.Tool is now Name/Description/InputSchema + Execute(ctx, Call)
returning Execution{Output, Await, Actions}. The typed path is
tool.Func[In, Out] with schema inference; results built via
Done/Pending/NeedInput.
- Await{Reason, Resume, ...} describes a pause; the closed Reason/Resume
table is validated by the registry. Pauses persist as typed
session.PendingToolCall records (kvstore included: new proto map
fields, opaque JSON values carrying their own schema_version).
- Runner gains Resume/Progress/Cancel with hash-based resume receipts:
duplicate submissions are acknowledged, conflicting ones rejected
(ErrResumeConflict).
- Re-entry routes through the agent's tool interceptor chain
(LLMAgent.ExecuteToolResume), so an approval interceptor consumes the
decision before the tool runs — denied approvals never execute the
tool. funcTool never re-runs the typed function on re-entry. Chained
pauses (approve → external work) keep the call resumable.
- llmagent persists tool response parts in request order while
streaming events in completion order.
- Examples migrated to the new API; task build:examples wired into CI.
Resume previously returned a lazy iter.Seq2 — calling it and dropping the return value compiled cleanly and did nothing. Now validation, authorization, receipts, mutation, and the session save all happen before Resume returns; the stream replays mutation-phase events and then runs the agent continuation (re-acquiring the session lock). All-rejected batches fail eagerly. Progress returns its single ToolProgressEvent directly; Cancel reports ResumeOperationCancel to the ResumeAuthorizer.
Export SpecProvider{ToolSpec() Spec} and Unwrapper{Unwrap() Tool};
SpecOf follows Unwrap chains so decorators preserve the wrapped tool's
async hints. AsyncSpec enforcement moves from funcTool into the
registry, covering every Tool implementation. agenttool declares
AsyncHandoff.
Execution.Actions was write-only: builtins emitted ActionArtifact but nothing read it. llmagent now yields a ToolArtifactEvent per artifact action; adapters and applications own persistence.
- runner.Result → runner.Resumption (collided with tool.Result[T]). - tool.Registry is a concrete struct; Execute/ExecuteAll deleted (they passed a zero InvocationInfo and swallowed pauses); NewRegistry is variadic; MCP accepts a narrow ToolRegistry interface. - Removed Call.Args, Await.ExpiresAt (use Timeout), write-only metadata bags, and unimplemented coalescing fields. Await.Resume may be left empty — Normalize defaults it from Reason; interceptor-created pauses are normalized and validated too. - Run's userID is used: message-resume goes through the ResumeAuthorizer. ToolType constants; deterministic PendingCalls ordering.
0e2ebd3 to
9c07425
Compare
- Validate JSON at trust boundaries (tool output, resume output, progress payloads; new ErrInvalidResumePayload). Async-hint separator normalized; stale tool/doc.go rewritten; crash recovery warns loudly when it strands a pause. - fakellm works under the agent loop: ThenRespondText wires streaming, the echo fallback carries its message, conversation keys are stable across turns; llmagent sets Request.Metadata[session_id]. - Review findings: agenttool re-entry resolves with the resume payload instead of re-running the child agent; xid session IDs; SpecOf is cycle-bounded; a2a empty-reasoning filter restored; shared SummarizePendingCall projection; Await metadata cloned into pending state. - API trims for a leaner surface: PendingReentry and Spec.OutputSchema removed. Resume on an agent without re-entry support fails eagerly (ErrReentryNotSupported) instead of silently recording the payload; tool.Definition treats a provided Spec as the source of truth.
9c07425 to
aaf4796
Compare
|
@claude Review this, specifically whether API and usage for users is good |
|
Claude finished @weeco's task in 4m 1s —— View job API & Usage Review — async tools / typed
|
Async tools for the agent runtime: tools (and interceptors) can now pause an invocation — for external jobs, human approval, or user input — and the runner can resume it later, across process boundaries. Ships with the typed
tool.Funcpath and a set of pre-1.0 API cleanups.Builds on #142 (sealed
Part).Why
Previously every tool had to return its final result inside one
Executecall. That ruled out deployments, batch jobs, human approval gates, MCP elicitation — anything that takes minutes or needs a human in the middle. The SDK had wire format for "paused" but no mechanism to produce, persist, or resume a pause.The API
Sync tool (the 90% path)
Schema is inferred from
WeatherInput. No JSON plumbing, noDefinition()boilerplate.Async tool (external job)
The invocation ends with
FinishReasonPaused; the pending call is persisted on the session (session.PendingToolCall, kvstore included) and surfaced onInvocationEndEvent.PendingCalls.Resuming (e.g. from a webhook handler)
Resumemutates eagerly — by the time it returns, the session is updated and saved; dropping the stream skips only the model continuation. It is idempotent for at-least-once delivery: duplicate submissions with the same payload are acknowledged, different payloads conflict (Stripe-style).Runner.Progressrecords non-terminal updates;Runner.Cancelaborts.Human-in-the-loop approval (interceptor)
Re-entry goes through the interceptor chain, so denial means the tool never runs. Chained pauses work: approve → tool itself returns
Pending→ resume again with the external result (covered by an end-to-end test).Breaking changes (pre-1.0, intentional)
tool.Toolis nowName()/Description()/InputSchema()+Execute(ctx, Call) (Execution, error); errors returned fromExecutebecome model-visible tool errors.tool.Registryis a concrete struct;Execute/ExecuteAllremoved (useRun(...).Response());NewRegistry()is variadic. MCP accepts a narrowmcp.ToolRegistryinterface.Runner.Resume/Cancelreturn(iter.Seq2[agent.Event, error], error);Progressreturns(agent.ToolProgressEvent, error).agent.ToolInterceptorreturnstool.Execution;ToolCallInfogainedResume.Call.Args,Await.ExpiresAt(useTimeout),Execution/Action/Config.Metadata(write-only),Spec.OutputSchema(advisory-only),tool.PendingReentry(the typed path never re-runs the function; implementTooldirectly for re-entry logic), coalescing fields.Await.Resumemay be left empty (defaults fromReason).kvstorepersistsPendingToolCalls/ResumeReceipts(new proto fields 4/5, backward compatible).Known gaps (deliberate, follow-ups)
InvocationEndEvent.PendingCallsbut don't yet mapToolProgressEvent/ToolArtifactEventto their wire formats.session.Store).