0xPlaygrounds / 0xPlaygrounds/rig

feat: add code-mode-ready tool lifecycle, nested dispatch, and rich outputs

Open
#2,095 1 comment 0 reactions 0 assignees View on GitHub
feat
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.