Skip to content

feat(oxide-code): add tool system with bash tool and agent loop - #3

Merged
hakula139 merged 9 commits into
mainfrom
feat/tool-system
Apr 3, 2026
Merged

feat(oxide-code): add tool system with bash tool and agent loop#3
hakula139 merged 9 commits into
mainfrom
feat/tool-system

Conversation

@hakula139

@hakula139 hakula139 commented Apr 2, 2026

Copy link
Copy Markdown
Owner

Summary

Add the tool execution framework and agent loop, turning the REPL from a single-shot chat into an autonomous agent that can call tools and act on results. The agent streams responses, detects tool-use content blocks, dispatches to the appropriate tool, feeds the result back, and loops until the LLM produces a text-only response.

  • Add tool.rs with Tool trait (object-safe via Pin<Box<dyn Future>>), ToolDefinition, ToolOutput, and ToolRegistry
  • Add tool/bash.rs with shell execution via tokio::process::Command, configurable timeout, and head+tail output truncation
  • Add is_error field to ContentBlock::ToolResult for error signaling back to the LLM
  • Add tools parameter to Anthropic API request via CreateMessageRequest and stream_message
  • Replace send_and_print with stream_response (accumulates text + tool-use blocks from SSE) and agent_turn (tool dispatch loop with 25-round safety limit)
  • Remove stale #[expect(dead_code)] on ContentBlockInfo and Delta now that stream_response uses both types

Design Decisions

  • Pin<Box<dyn Future>> over async_trait / enum dispatch: Keeps the Tool trait object-safe for Box<dyn Tool> without adding a dependency. New tools are purely additive — implement the trait and register.
  • Head+tail output truncation: Inspired by OpenCode's strategy — keeps the first 50KB and last 50KB instead of just truncating at the start. Error messages and summaries at the end of output are often the most valuable context for the LLM.
  • BlockAccumulator state machine: Tracks in-flight content blocks during SSE streaming. Text deltas are printed in real-time; InputJsonDelta fragments are buffered and parsed on ContentBlockStop.

Changes

File Description
Cargo.toml Add tokio process and time features; bump MSRV to 1.91
crates/oxide-code/src/tool.rs Tool trait, ToolDefinition, ToolOutput, ToolRegistry
crates/oxide-code/src/tool/bash.rs BashTool — shell execution, timeout, head+tail truncation
crates/oxide-code/src/message.rs Add is_error field to ContentBlock::ToolResult; generic is_default helper
crates/oxide-code/src/client/anthropic.rs Add tools to API request; simplify empty-to-None with then_some; strengthen SSE parser tests with field-level assertions
crates/oxide-code/src/config/oauth.rs Tighten expiresAt test assertion to time-bounded range
crates/oxide-code/src/main.rs Agent loop (agent_turn, stream_response, BlockAccumulator), tool display helpers
CLAUDE.md Update crate structure diagram; add file-splitting and assertion quality guidelines

Test plan

  • cargo build compiles cleanly
  • cargo clippy --all-targets -- -D warnings — zero warnings
  • cargo test — 43 tests pass (including multi-byte truncation boundary test)
  • cargo llvm-cov --ignore-filename-regex 'main\.rs'tool.rs 100%, tool/bash.rs 95%, message.rs 98%
  • Smoke test: agent executes bash tool calls and loops correctly

Introduce the tool execution framework and wire it into an agent loop,
turning the REPL from a single-shot chat into an autonomous agent that
can call tools and act on results.

- Add `tool.rs` with `Tool` trait (object-safe via `Pin<Box<dyn Future>>`),
  `ToolDefinition`, `ToolOutput`, and `ToolRegistry`
- Add `tool/bash.rs` with shell execution via `tokio::process::Command`,
  configurable timeout, and head+tail output truncation (inspired by
  OpenCode's strategy of preserving both the beginning and end of output)
- Add `is_error` field to `ContentBlock::ToolResult` for error signaling
- Add `tools` parameter to Anthropic API request and `stream_message`
- Replace `send_and_print` with `stream_response` (accumulates both text
  and tool-use content blocks from SSE) and `agent_turn` (detects tool
  calls, dispatches, feeds results back, loops until text-only response)
- Remove stale `#[expect(dead_code)]` on `ContentBlockInfo` and `Delta`
  now that `stream_response` uses both types
- Update CLAUDE.md crate structure and module organization guidelines
@hakula139 hakula139 added the enhancement New feature or request label Apr 2, 2026
@hakula139 hakula139 self-assigned this Apr 2, 2026
- Bump rust-version to 1.91 (floor_char_boundary, let chains)
- Guard truncate_output against barely-over-limit content where the
  separator would make the result longer than the original
- Replace is_false with generic is_default<T: Default + PartialEq>
  (adopted from kiln) for skip_serializing_if on boolean fields
- Reorder test sections in message.rs to mirror production code order
  (ContentBlock before Message)
- Reorder functions in main.rs: stream_response before display helpers,
  matching call order within agent_turn
- Update docs/roadmap.md to reflect shipped tool system
Replace weak assertions that would pass even with buggy code:

- Truncation tests: use asymmetric sentinels (HEAD/TAIL markers)
  instead of uniform data that makes starts_with/ends_with
  unfalsifiable. Add multi-byte boundary test for floor_char_boundary.
- SSE parser tests: verify deserialized field values instead of
  matching only the enum variant with wildcard (..) patterns.
- message.rs: verify all ToolResult fields on deserialization, not
  just is_error.
- oauth.rs: replace vacuous expiresAt > 1000 with tight time-bounded
  range derived from now_millis + expires_in.
- bash execute tests: assert exact output structure instead of loose
  contains() checks that miss ordering and separator bugs.

Add assertion quality guideline to CLAUDE.md testing conventions.
…eout message

- Consume tool_uses vec instead of borrowing, eliminating unnecessary
  id.clone() and input.clone() on each iteration.
- Use milliseconds in timeout error message so sub-second timeouts
  report accurately (was "0s" for 100ms).
Replace legacy claude-sonnet-4-20250514 with claude-sonnet-4-6.
- Merge definitions_includes_all_tools and
  definitions_serializes_input_schema into a single test.
- Remove execute_echo, fully covered by run_valid_command.
Use writeln! for exit code and append \n to "(no output)" so
all execute() output ends with a trailing newline regardless of
which branch appends last. Use indoc! for multiline test assertions.
Keep comments that explain why or clarify non-obvious logic.
@hakula139
hakula139 merged commit 255de87 into main Apr 3, 2026
1 check passed
@hakula139
hakula139 deleted the feat/tool-system branch April 3, 2026 03:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant