0xPlaygrounds / 0xPlaygrounds/rig
feat: add code-mode-ready tool lifecycle, nested dispatch, and rich outputs
- Dominant language
- Rust
- Stars
- 8.6k
- Forks
- 959
- Avg merge
- 4h 32m
- Merged PRs (30d)
- 117
Description
- [x] I have looked for existing issues (including closed) about this
## Feature Request
Add the framework primitives needed for a fully integrated code-mode tool: run-scoped tool lifecycle, context-preserving nested tool dispatch, structured rich outputs, parent/child metadata, and richer tool definitions.
This is a follow-up to #1439. I built a Monty/Python code-mode integration against `rig-core` 0.39 and then checked the current `main` API at `56e15e162c342f62662e49fc1fb98b62e97ffa69`. Recent work such as #1536 and the structured `ToolReturn` / `ToolExecutionResult` APIs has solved part of the problem, but several framework boundaries still force a code-mode adapter to bypass Rig or emulate behavior locally.
## Motivation
A code-mode tool presents one model-facing `run_code` function while exposing the agent's other tools as functions inside a sandbox. A generated program can call multiple tools concurrently, chain results, branch, filter, and aggregate without one model round trip per tool call.
A production implementation needs the nested calls to behave like ordinary Rig calls:
- use the same policies, hooks, structured failures, tracing, cancellation, and runtime context;
- preserve rich tool results such as images without string conventions;
- scope a persistent REPL to one agent run/conversation;
- expose parent/child call metadata;
- render accurate signatures, including return schemas and sequential/parallel behavior.
The current APIs make the basic execution loop possible, but not this integration.
## Limitations encountered
### 1. No run-scoped lifecycle for stateful tools
A persistent code-mode REPL must belong to one agent run or conversation. A registered tool instance is stored behind the agent/tool server and can be shared by agent clones and successive runs. There is no equivalent of a `for_run` tool/toolset factory that creates and disposes run-local tool state.
`ToolCallExtensions` now allows caller-supplied typed context, which is useful, but Rig does not automatically provide the tool with the runner's `RunId`, conversation identity, outer tool-call IDs, cancellation/deadline, or a lifecycle boundary. The practical workarounds are either rebuilding the agent/tool per request or sharing one REPL and risking cross-run state leakage.
### 2. Nested calls cannot re-enter the complete Rig execution pipeline
A code-mode adapter currently has two choices:
1. call wrapped `ToolDyn` values directly; or
2. call `ToolServerHandle::call_tool_structured`.
The first bypasses Rig hooks, policies, tool-server lookup, structured outcomes, and normal tracing. The second preserves structured dispatch but does not itself emit the agent runner's `StepEvent::ToolCall` / `StepEvent::ToolResult` hook chain, because those events are driven around top-level calls by the runner.
There is no public call-scoped executor that a composite tool can use to invoke another tool with:
- inherited `ToolCallExtensions`;
- hook/policy execution;
- parent and child internal call IDs;
- recursion protection (so `run_code` cannot invoke itself accidentally);
- normal structured results and tracing.
### 3. Dynamic tool output is still a string at the model boundary
`ToolDyn::call_structured` returns `ToolExecutionResult`, but its model-facing payload remains a `String`. Rich results are encoded through conventions parsed by `ToolResultContent::from_tool_output`.
This creates several problems for code mode:
- image results require magic JSON (`type` / `data` / `mimeType`);
- ordinary JSON objects containing reserved `response` or `parts` keys can be reinterpreted as multimodal output;
- print-plus-image results require another magic hybrid envelope;
- `ToolResultContent` currently supports only text and image, not documents/audio/video;
- typed nested results must be decoded from strings before they can be used by sandbox code.
A structured dynamic output channel such as `OneOrMany` (or a more general `ToolOutput`) would avoid guessing and reserved-key collisions.
### 4. Nested metadata is not first-class conversation metadata
`ToolResultExtensions` is a strong host-only metadata mechanism and reaches `StepEvent::ToolResult`. However:
- direct nested calls do not produce those runner events;
- `completion::message::ToolResult` has no extensions/metadata field;
- there is no parent-child relationship between the outer `run_code` call and nested calls;
- a composite tool cannot obtain the outer provider/internal call IDs through its execution context.
A local adapter can keep its own trace buffer, but it cannot produce Rig-correlated nested spans/history equivalent to normal tool calls.
### 5. `ToolDefinition` lacks code-mode-relevant metadata
The provider-facing definition has only `name`, `description`, and input `parameters`. Code mode also needs:
- a return JSON schema to generate useful function return types rather than `Any`;
- an execution policy (`sequential` versus parallel-safe) for barriers and durable runtimes;
- tool kind/control metadata so framework tools stay native rather than being folded into the sandbox;
- deferred/native fallback markers for tool search and provider-native tools;
- arbitrary metadata for selectors such as `code_mode = true`.
Without these, wrappers cannot faithfully compose tool catalogs or safely choose concurrency.
### 6. No bounded "model retry this tool call" result contract
Sandbox syntax/runtime/type failures should normally be returned to the model with a bounded retry budget so it can repair generated code. Structured `ToolFailure` can classify an error and hooks can steer flow, but there is no direct equivalent of a retryable tool result that requests regenerated tool arguments while counting against a tool-specific retry budget.
### 7. No wrapper-toolset transformation API
A complete implementation should transform an assembled toolset from:
```text
[tool_a, tool_b, tool_c]
```
into:
```text
[run_code] // with tool_a/tool_b/tool_c callable inside it
```
while leaving selected control/native tools visible. Today applications must manually construct a second vector of wrapped tools and make sure the originals are not also registered. This makes dynamic tools, MCP refresh, tool search, and provider-native fallbacks difficult to compose.
## Proposal
This likely fits as several composable primitives rather than one code-mode-specific API:
1. **Run-scoped tool factories/toolsets**
- Construct a tool/toolset for a `RunContext` and dispose it after the run.
- Provide stable run ID, optional conversation/session identity, cancellation/deadline, and outer call IDs.
2. **A nested/scoped tool executor**
- Public handle available through tool-call context.
- Executes another registered tool through lookup, extensions, hooks/policies, tracing, and structured outcomes.
- Generates child internal IDs and carries `parent_internal_call_id`.
- Supports allowlists and recursion guards.
3. **Structured model-facing tool output**
- Let dynamic tools return typed content parts directly instead of only strings.
- Preserve existing string APIs as compatibility conveniences.
- Expand rich tool-result content consistently across streaming/non-streaming providers.
4. **Nested metadata/correlation**
- Parent/child IDs on tool call/result events and tracing.
- A documented way to retain selected result extensions in run history when desired, while keeping sensitive metadata host-only by default.
5. **Richer tool definitions**
- Optional return schema, metadata, execution policy, tool kind, deferred/native markers.
6. **Retryable tool outcomes**
- A bounded repair/retry action for valid tool names whose execution indicates that the model should regenerate arguments/code.
7. **Toolset wrapper/transform API**
- Transform the final assembled tool catalog and dispatch selected calls through a wrapper without manually duplicating registration.
The nested executor and lifecycle pieces should remain runtime/language agnostic so JavaScript, Monty/Python, Lua, shell, workflow, and composite tools can share them.
## Alternatives
### Call `ToolDyn` directly
This works for a prototype and supports parallel calls, but bypasses the runner's hooks, policies, IDs, tracing, and structured dispatch.
### Call `ToolServerHandle::call_tool_structured`
Better than direct dispatch, but it still does not reproduce the runner-managed hook/event pipeline or parent-child correlation.
### Rebuild the agent/tool per request
Provides state isolation, but is expensive and prevents a reusable configured agent. It also does not solve nested hooks, rich outputs, or metadata.
### Encode everything in JSON strings
This is backward compatible but ambiguous. It creates reserved-key collisions and cannot losslessly carry richer model content or host-only metadata.
### Implement code mode entirely inside Rig
That could solve one runtime, but the underlying lifecycle, nested execution, rich-output, and correlation requirements are useful to any composite tool or workflow engine. Exposing those primitives keeps #1439 modular and language/runtime agnostic.
## Related issues/work
- #1439 — code mode proposal
- #1536 / its merged PR — per-call runtime extensions
- #1650 — multimodal tool-result consistency
- #2094 — structured tool failures and call-scoped state ergonomics
Contributor guide
Research direction
The issue describes framework primitives for a code-mode tool. Start by examining the existing tool execution pipeline in rig-core, particularly ToolDyn, ToolServerHandle, and ToolExecutionResult. Look at the merged PR #1536 for call-scoped extensions and #1650 for multimodal results. Understand how hooks, policies, and tracing currently work. The goal is to design APIs for run-scoped lifecycle, nested dispatch, and structured outputs, not to implement a specific runtime.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- ai-infra-agents, backend-api-design, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100