Skip to content

feat: add iii-agent worker - #13

Merged
rohitg00 merged 9 commits into
mainfrom
feat/agent
Apr 22, 2026
Merged

feat: add iii-agent worker#13
rohitg00 merged 9 commits into
mainfrom
feat/agent

Conversation

@rohitg00

@rohitg00 rohitg00 commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Chat orchestrator for the iii console with 7 functions
  • Dynamically discovers functions from all connected workers via list_functions()
  • LLM-powered (Claude Haiku) with ReAct loop, tool name sanitization, INCLUDED_PREFIXES filter
  • All through iii primitives: State, Streams, Cron, Subscribe, HTTP triggers
  • 21 tests, 0 warnings

Functions

Function Description
agent::chat Synchronous chat with LLM + function calling
agent::chat_stream Streaming chat via iii Streams
agent::discover List available functions
agent::plan Generate execution plan without executing
agent::session_create Create chat session
agent::session_history Get conversation history
agent::session_cleanup Remove expired sessions (cron)

Test plan

  • cargo test — 21 tests passing
  • cargo check — 0 warnings
  • E2E tested with console chat bar, Haiku model, live function calling

Summary by CodeRabbit

Release Notes

  • New Features

    • Added an LLM-powered agent with multi-turn chat and streaming support
    • Added function discovery for dynamic capability detection
    • Added planning functionality for task orchestration
    • Added session management for conversation persistence
  • Documentation

    • Added README with setup and usage instructions
    • Added technical specification document
  • Configuration

    • Added YAML-based configuration supporting Anthropic model selection, token limits, and session management parameters

@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@rohitg00 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 36 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 52 minutes and 36 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ba9d1d3-e604-4fa5-a6f2-11eafe06e743

📥 Commits

Reviewing files that changed from the base of the PR and between 7de94fa and 11e9b94.

📒 Files selected for processing (10)
  • agent/.gitignore
  • agent/Cargo.toml
  • agent/README.md
  • agent/src/config.rs
  • agent/src/discovery.rs
  • agent/src/functions/chat.rs
  • agent/src/functions/chat_stream.rs
  • agent/src/functions/plan.rs
  • agent/src/functions/session.rs
  • agent/src/main.rs
📝 Walkthrough

Walkthrough

A complete new iii-agent Rust package is introduced, consisting of CLI infrastructure, configuration loading, LLM integration with Anthropic's Messages API, tool discovery from remote workers, and multiple request handlers (chat, streaming chat, planning, session management) that orchestrate conversations between users, the LLM, and external worker functions via state and trigger mechanisms.

Changes

Cohort / File(s) Summary
Project Setup & Build
agent/Cargo.toml, agent/build.rs, agent/config.yaml
Cargo workspace manifest for iii-agent binary with dependencies (tokio, serde, reqwest, tracing, clap, uuid); build script captures target triple; example config with Anthropic model, token limits, iteration caps, session TTL, and cron schedule.
Documentation
agent/README.md, agent/SPEC.md
README describes agent functionality (chat, discovery, planning, session lifecycle), setup prerequisites, CLI usage, and config keys; SPEC details worker responsibilities, function exports, state scopes, control flows, JSON-UI schema, streaming event structure, and discovery filters.
Configuration & Manifest
agent/src/config.rs, agent/src/manifest.rs
AgentConfig struct with YAML deserialization and defaults; load_config(path) function; ModuleManifest builder using compile-time macros for name, version, and supported targets.
Core Services
agent/src/llm.rs, agent/src/state.rs, agent/src/discovery.rs
LLM client for Anthropic Messages API with non-streaming and streaming (send_stream) support; helper extractors for text and tool uses; state manipulation helpers (get, set, delete, list); tool discovery that filters worker functions, sanitizes names, constructs tool definitions, and builds system prompts.
Function Handlers
agent/src/functions/mod.rs, agent/src/functions/chat.rs, agent/src/functions/chat_stream.rs, agent/src/functions/discover.rs, agent/src/functions/plan.rs, agent/src/functions/session.rs
Module declarations and handler builders: chat/streaming handlers iterate up to max iterations, executing tool calls with 30s timeout; discover returns available tools as JSON; plan generates JSON DAG from LLM output; session handlers create/retrieve/cleanup stored conversations with TTL-based expiration.
Application Entry Point
agent/src/main.rs
Tokio CLI app parsing config/URL/API key, registering worker with III engine, wiring function handlers and HTTP/cron triggers, caching discovered tools, and blocking on graceful shutdown signal.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Agent as iii-agent<br/>(HTTP Handler)
    participant Engine as III Engine<br/>(Trigger/State)
    participant LLM as Anthropic<br/>Messages API
    participant Workers as III Workers

    Client->>Agent: POST /chat<br/>(session_id, message)
    Agent->>Engine: state::get(agent:sessions:{id})
    Engine-->>Agent: conversation history
    
    Agent->>Engine: discover_tools()
    Engine->>Workers: list_functions()
    Workers-->>Engine: FunctionInfo[]
    Engine-->>Agent: tools[]
    
    Agent->>LLM: LlmRequest<br/>(messages, tools, system)
    LLM-->>Agent: LlmResponse<br/>(content, stop_reason)
    
    alt Tool Uses Present
        Agent->>Engine: trigger(function_id, input)<br/>×N (parallel)
        Engine->>Workers: execute function
        Workers-->>Engine: result
        Engine-->>Agent: tool results[]
        Agent->>Agent: Append results to messages
        Agent->>LLM: LlmRequest<br/>(updated messages)
        LLM-->>Agent: LlmResponse
    else No Tool Uses
        Agent->>Agent: Extract assistant text
    end
    
    Agent->>Engine: state::set(agent:sessions:{id}, updated_history)
    Engine-->>Agent: ok
    
    Agent-->>Client: 200 OK<br/>(parsed UI elements, token usage)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • sergiofilhowz

Poem

🐰 Whiskers twitching with delight,
A new agent hops into sight!
With LLM and tools to explore,
Conversations flow through the door.
Sessions saved, workflows planned—
Hop-hop-hop, the III expands! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.20% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add iii-agent worker' directly and clearly summarizes the main change: adding a new worker implementation for the iii agent system.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Chat orchestrator for the iii console. 7 functions:
- agent::chat — synchronous chat with LLM + dynamic function discovery
- agent::chat_stream — streaming chat via iii Streams
- agent::discover — list available functions
- agent::plan — generate execution plan without executing
- agent::session_create/history/cleanup — session management

Dynamically discovers functions from all connected workers via list_functions().
Uses Claude Haiku for fast responses. Tool names sanitized for Anthropic API.
Uses iii primitives: State (sessions, tool cache), Streams (events), Cron (cleanup), Subscribe (functions-available).
21 tests, 0 warnings.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (1)
agent/config.yaml (1)

1-1: Align default model with fallback default to avoid silent behavior shifts.

Line 1 uses Sonnet, but fallback defaults in agent/src/config.rs use Haiku. When config loading fails, model behavior changes unexpectedly (quality/cost/latency). Consider keeping one canonical default across both paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent/config.yaml` at line 1, The default Anthropic model in
agent/config.yaml ("anthropic_model") is different from the fallback used in
agent/src/config.rs, causing unexpected behavior when config loading fails; make
them identical by either updating agent/config.yaml's anthropic_model value to
match the fallback constant (e.g., the string in DEFAULT_ANTHROPIC_MODEL or the
fallback used in load_config()/Config::default()) or by changing the fallback
constant in agent/src/config.rs to the canonical model you want; ensure the same
exact model identifier string is used in both places so there is one canonical
default across config file and code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@agent/README.md`:
- Around line 40-42: Replace the inline secret in the example command so users
don't paste real keys; update the README example to show setting
ANTHROPIC_API_KEY in a separate step (e.g., export ANTHROPIC_API_KEY="sk-..." or
sourcing from an env file / .env) and then running the iii-agent binary
(./target/release/iii-agent --url ws://127.0.0.1:49134 --config ./config.yaml),
or alternatively show a placeholder like ANTHROPIC_API_KEY=<REDACTED> and a
preceding export instruction; ensure the example references the same flags
(--url, --config) but removes inline secret disclosure.

In `@agent/src/config.rs`:
- Around line 51-55: The load_config function currently deserializes YAML into
AgentConfig but doesn't validate semantic bounds; after calling
serde_yaml::from_str in load_config, add explicit runtime checks on AgentConfig
fields (e.g., ensure max_tokens > 0, max_iterations > 0, non-empty/valid cron
string, and any other domain constraints on AgentConfig) and return a
descriptive Err when any check fails so startup fails fast with a clear message;
implement these checks inside load_config immediately after deserialization and
reference the AgentConfig instance to produce context-rich errors.

In `@agent/src/discovery.rs`:
- Around line 6-9: The current hardcoded INCLUDED_PREFIXES constant makes valid
worker functions undiscoverable; replace it with a configurable exclusion-based
approach: remove or deprecate INCLUDED_PREFIXES and introduce a configurable
EXCLUDED_PREFIXES (or load prefixes from config/env) so discovery only filters
known-internal namespaces instead of allowing only six prefixes; update the
discovery logic that references INCLUDED_PREFIXES to instead consult the new
EXCLUDED_PREFIXES (or config-driven list) and accept any worker whose namespace
does not match an excluded prefix, and provide a sensible default for
EXCLUDED_PREFIXES containing only internal prefixes so behavior is unchanged
unless configured.

In `@agent/src/functions/chat_stream.rs`:
- Around line 303-326: The current load_history and save_history read the
session record via state_get but save_history overwrites the entire record with
a bare array, dropping metadata like created_at; change save_history so it
preserves the stored session object shape instead of writing the array directly:
call state_get(iii, "agent:sessions", session_id) to obtain the existing record
(or create a new object if none), set its "value" field to
serde_json::to_value(messages) (or json!([]) fallback), preserve or set
created_at if missing, then call state_set(iii, "agent:sessions", session_id,
&record) to write the full object back; keep load_history reading
val.get("value") as before but do not rely on save_history producing a bare
Vec<Message>.
- Around line 39-52: The code uses payload.get("session_id") to build
stream_group which allows empty session IDs and causes all requests to share
"agent:events:"; change the logic around the session_id binding so that if
payload.get("session_id") is absent or empty you generate a unique ID (e.g.,
Uuid::new_v4().to_string()) before computing stream_group; update the variable
created at session_id and ensure stream_group = format!("agent:events:{}",
session_id) uses that generated or provided ID (references: session_id variable,
stream_group).

In `@agent/src/functions/chat.rs`:
- Around line 188-211: The current load_history and save_history replace the
entire session record with a bare Vec<Message>, which drops fields like
created_at; update them to deserialize the stored value into a SessionState
struct (e.g., struct SessionState { created_at: String, messages: Vec<Message>
}), have load_history extract and return session.messages (or empty vec if
missing), and have save_history load the existing SessionState (or create a new
one with created_at set if missing), set its messages = messages, then serialize
and persist the full SessionState via state::state_set so other fields are
preserved for session_cleanup and session_history.

In `@agent/src/functions/plan.rs`:
- Around line 41-42: The planner currently uses
discovery::build_capabilities_summary(&tools) which composes capabilities from
ToolDef.name (sanitized names like eval__metrics) causing generated
steps[*].function_id to be invalid; update the logic so
build_capabilities_summary (or the call site in the planner where tools is
passed) uses each tool's original function_id (e.g., eval::metrics) instead of
ToolDef.name, or alternatively post-process the model's returned Plan in the
function that calls discovery::discover_tools (and/or the planner function) to
normalize/translate any sanitized names back to the original function_id before
returning the plan to executors; reference ToolDef.name, ToolDef.function_id (or
the field that holds the original id), discovery::discover_tools,
discovery::build_capabilities_summary, and ensure steps[*].function_id matches
those original function_ids.

In `@agent/src/functions/session.rs`:
- Around line 73-78: The cleanup handler currently hardcodes a 24-hour TTL in
build_cleanup_handler; change build_cleanup_handler to accept the configured
session_ttl_hours (pass the value from your config/manifest into
build_cleanup_handler) and replace all literal `24` comparisons inside the
returned closure (and the other occurrences around the second block at lines
103-107) with comparisons against that injected session_ttl_hours value (use the
same time-unit semantics as the config and the same identifier
`session_ttl_hours` inside the closure) so the handler honors the configured
TTL.

In `@agent/src/llm.rs`:
- Around line 109-113: The reqwest client is created with Client::new() (in the
new(api_key: String) constructor) which leaves no timeout and can hang; replace
Client::new() with a configured client from
Client::builder().timeout(Duration::from_secs(30)) (or read timeout from
config/env) and return that as the client field in new; also add the necessary
std::time::Duration import and ensure any per-request builders (where you call
client.post/... or methods around the client) do not override this with infinite
timeouts.
- Around line 168-193: The SSE parsing closure over byte_stream (the
byte_stream.map that builds event_stream) currently parses each TCP chunk
independently and drops JSON that spans chunks; fix by implementing buffering of
partial lines inside that closure (or replace with an eventsource_stream parser)
so chunks are appended to a persistent buffer, then split on '\n' to extract
complete "data: " lines, keep any trailing partial line in the buffer for the
next chunk, parse those complete lines into StreamEvent (the
serde_json::from_str::<StreamEvent>) and surface parse errors instead of
silently ignoring them; update the closure that produces event_stream (and the
subsequent flat_map) to use this buffered approach or swap in eventsource_stream
to ensure tool calls and text_delta events aren’t lost.

---

Nitpick comments:
In `@agent/config.yaml`:
- Line 1: The default Anthropic model in agent/config.yaml ("anthropic_model")
is different from the fallback used in agent/src/config.rs, causing unexpected
behavior when config loading fails; make them identical by either updating
agent/config.yaml's anthropic_model value to match the fallback constant (e.g.,
the string in DEFAULT_ANTHROPIC_MODEL or the fallback used in
load_config()/Config::default()) or by changing the fallback constant in
agent/src/config.rs to the canonical model you want; ensure the same exact model
identifier string is used in both places so there is one canonical default
across config file and code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba16c178-3335-4cfb-94af-476c03e6b491

📥 Commits

Reviewing files that changed from the base of the PR and between e664259 and 7de94fa.

📒 Files selected for processing (17)
  • agent/Cargo.toml
  • agent/README.md
  • agent/SPEC.md
  • agent/build.rs
  • agent/config.yaml
  • agent/src/config.rs
  • agent/src/discovery.rs
  • agent/src/functions/chat.rs
  • agent/src/functions/chat_stream.rs
  • agent/src/functions/discover.rs
  • agent/src/functions/mod.rs
  • agent/src/functions/plan.rs
  • agent/src/functions/session.rs
  • agent/src/llm.rs
  • agent/src/main.rs
  • agent/src/manifest.rs
  • agent/src/state.rs

Comment thread agent/README.md
Comment thread agent/src/config.rs
Comment thread agent/src/discovery.rs Outdated
Comment thread agent/src/functions/chat_stream.rs
Comment thread agent/src/functions/chat_stream.rs
Comment thread agent/src/functions/chat.rs
Comment thread agent/src/functions/plan.rs Outdated
Comment thread agent/src/functions/session.rs
Comment thread agent/src/llm.rs
Comment on lines +109 to +113
pub fn new(api_key: String) -> Self {
Self {
client: Client::new(),
api_key,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the full llm.rs file to understand the structure
wc -l agent/src/llm.rs

Repository: iii-hq/workers

Length of output: 78


🏁 Script executed:

# Read the file to see the code structure and verify the line numbers
cat -n agent/src/llm.rs

Repository: iii-hq/workers

Length of output: 7722


🏁 Script executed:

# Check if reqwest is in Cargo.toml to understand version/features
grep -A 5 "reqwest" Cargo.toml | head -20

Repository: iii-hq/workers

Length of output: 101


🏁 Script executed:

# Check for Cargo.toml in agent directory
find agent -name "Cargo.toml"

Repository: iii-hq/workers

Length of output: 74


🏁 Script executed:

# Let's also check the repository structure to understand dependencies
ls -la | grep -E "Cargo|package"

Repository: iii-hq/workers

Length of output: 40


🌐 Web query:

reqwest::Client default timeout behavior http requests

💡 Result:

The default timeout behavior for reqwest::Client (async) created via Client::new or ClientBuilder::new.build is no timeout (None). A timeout must be explicitly set using ClientBuilder::timeout or RequestBuilder::timeout. For the blocking client (reqwest::blocking::Client), the default is 30 seconds, which can be disabled by setting .timeout(None). This applies to the latest version as of 2026-04-14. The async client intentionally has no default to avoid interrupting long-running streaming responses, while users can wrap futures in tokio::time::timeout if needed. Per-request timeouts override client-level settings.

Citations:


Set explicit request timeouts for Anthropic calls.

reqwest::Client::new() has no default timeout and will hang indefinitely on network stalls or upstream delays (lines 125, 152), pinning worker resources and degrading availability.

Suggested patch
+use std::time::Duration;
+
 pub struct LlmClient {
     client: Client,
     api_key: String,
 }
@@
     pub async fn send(&self, request: &LlmRequest) -> Result<LlmResponse> {
         let response = self
             .client
             .post(ANTHROPIC_API_URL)
+            .timeout(Duration::from_secs(60))
             .header("x-api-key", &self.api_key)
             .header("anthropic-version", ANTHROPIC_VERSION)
             .header("content-type", "application/json")
             .json(request)
             .send()
             .await?;
@@
         let response = self
             .client
             .post(ANTHROPIC_API_URL)
+            .timeout(Duration::from_secs(60))
             .header("x-api-key", &self.api_key)
             .header("anthropic-version", ANTHROPIC_VERSION)
             .header("content-type", "application/json")
             .json(&stream_request)
             .send()
             .await?;

Also applies to: 122-131, 150-158

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent/src/llm.rs` around lines 109 - 113, The reqwest client is created with
Client::new() (in the new(api_key: String) constructor) which leaves no timeout
and can hang; replace Client::new() with a configured client from
Client::builder().timeout(Duration::from_secs(30)) (or read timeout from
config/env) and return that as the client field in new; also add the necessary
std::time::Duration import and ensure any per-request builders (where you call
client.post/... or methods around the client) do not override this with infinite
timeouts.

Comment thread agent/src/llm.rs
Comment on lines +168 to +193
let event_stream = byte_stream.map(|chunk_result| {
let chunk = chunk_result.map_err(|e| anyhow!("stream read error: {}", e))?;
let text = String::from_utf8_lossy(&chunk);
let mut events = Vec::new();

for line in text.lines() {
if let Some(data) = line.strip_prefix("data: ") {
if data == "[DONE]" {
continue;
}
if let Ok(event) = serde_json::from_str::<StreamEvent>(data) {
events.push(event);
}
}
}

Ok(events)
});

Ok(event_stream.flat_map(|result| {
let items: Vec<Result<StreamEvent>> = match result {
Ok(events) => events.into_iter().map(Ok).collect(),
Err(e) => vec![Err(e)],
};
futures_util::stream::iter(items)
}))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the file and check its structure
find . -type f -name "llm.rs" | head -20

Repository: iii-hq/workers

Length of output: 76


🏁 Script executed:

# Check the specific lines in question
if [ -f "agent/src/llm.rs" ]; then
  wc -l agent/src/llm.rs
fi

Repository: iii-hq/workers

Length of output: 78


🏁 Script executed:

# Read the relevant section of the file
if [ -f "agent/src/llm.rs" ]; then
  sed -n '160,200p' agent/src/llm.rs
fi

Repository: iii-hq/workers

Length of output: 1534


🏁 Script executed:

# Look for StreamEvent definition and context
if [ -f "agent/src/llm.rs" ]; then
  head -100 agent/src/llm.rs
fi

Repository: iii-hq/workers

Length of output: 2454


🏁 Script executed:

# Check Cargo.toml for dependencies, specifically eventsource_stream
find . -name "Cargo.toml" -type f | head -5

Repository: iii-hq/workers

Length of output: 123


🏁 Script executed:

# Check agent/Cargo.toml for dependencies
cat agent/Cargo.toml

Repository: iii-hq/workers

Length of output: 824


🏁 Script executed:

# Search for any existing eventsource or similar crates in the codebase
rg "eventsource" --type toml

Repository: iii-hq/workers

Length of output: 40


🏁 Script executed:

# Look at the full context of the stream parsing function
sed -n '130,220p' agent/src/llm.rs

Repository: iii-hq/workers

Length of output: 3002


🏁 Script executed:

# Check if there are other parts of the codebase that use this stream
rg "event_stream\|StreamEvent" agent/src/

Repository: iii-hq/workers

Length of output: 40


🏁 Script executed:

# Check how the stream is consumed in the codebase
rg "send_stream" agent/src/ -A 5

Repository: iii-hq/workers

Length of output: 871


🏁 Script executed:

# Look for any buffering or frame handling logic elsewhere
rg "lines()\|chunk\|frame" agent/src/ -B 2 -A 2

Repository: iii-hq/workers

Length of output: 40


🏁 Script executed:

# Check what version of reqwest bytes_stream returns and if there's documentation
rg "bytes_stream" . --type rust

Repository: iii-hq/workers

Length of output: 127


🏁 Script executed:

# Look for tests that might show the expected behavior
find . -name "*test*" -type f | head -20

Repository: iii-hq/workers

Length of output: 97


🏁 Script executed:

# Look at the chat_stream consumer to understand impact
cat -n agent/src/functions/chat_stream.rs | head -100

Repository: iii-hq/workers

Length of output: 3560


🏁 Script executed:

# Check if there are any integration tests or examples that show streaming
find . -type f -name "*.rs" | xargs grep -l "send_stream" | head -10

Repository: iii-hq/workers

Length of output: 113


🏁 Script executed:

# Verify the specific concern: can JSON split across chunks?
# Look at how reqwest structures the stream
cat > /tmp/analyze_chunk_split.py << 'EOF'
# Check if a JSON event can reasonably split across TCP/HTTP chunks
# Typical chunks are 4-16KB, but SSE can have smaller message sizes

# Anthropic's streaming format is Server-Sent Events (SSE)
# Each event looks like:
# data: {"type": "content_block_delta", "delta": {"type": "text_delta", "text": "hello"}}
# 
# If the text content is long, a single event can easily exceed typical frame sizes
# and get split across multiple TCP packets/chunks

print("Analysis: SSE events with long content can exceed chunk boundaries")
print("Example: A text_delta with 1000+ character text would span chunks")
print("Risk: Current code processes chunk-by-chunk, can miss split events")
EOF
python /tmp/analyze_chunk_split.py

Repository: iii-hq/workers

Length of output: 257


🏁 Script executed:

# Check if there's any buffer handling in the streaming logic
sed -n '1,220p' agent/src/llm.rs | grep -i "buffer\|accumulate\|vec\|collect"

Repository: iii-hq/workers

Length of output: 518


🏁 Script executed:

# One more check: look at the actual stream consumption to see if tool_use loss would be critical
sed -n '1,150p' agent/src/functions/chat_stream.rs

Repository: iii-hq/workers

Length of output: 4623


🏁 Script executed:

# Verify: can we see where tool_use events are actually processed?
sed -n '100,200p' agent/src/functions/chat_stream.rs

Repository: iii-hq/workers

Length of output: 3573


🏁 Script executed:

# Check if there's any accumulation or buffering of partial data anywhere
rg "content_block\|tool_use\|delta" agent/src/functions/chat_stream.rs -B 2 -A 2

Repository: iii-hq/workers

Length of output: 40


SSE stream parsing loses events when JSON frames split across chunks, silently dropping tool calls and truncating text.

The code processes each TCP chunk independently and attempts to parse complete JSON lines. When a JSON event spans multiple chunks (common with longer content), both chunks fail to parse individually—the silent if let Ok(event) discards them without error. This breaks tool invocation: collected_tool_uses will be missing entries, preventing function execution. Similarly, text content gets truncated when deltas are lost.

Example: A content_block_delta event with long text_delta can exceed typical chunk sizes and split across two TCP frames. The first frame ends mid-JSON; the second frame has only the tail. Neither parses, both drop silently.

Accumulate frames into a proper SSE parser (e.g., eventsource_stream crate) or buffer incomplete lines across chunks before parsing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent/src/llm.rs` around lines 168 - 193, The SSE parsing closure over
byte_stream (the byte_stream.map that builds event_stream) currently parses each
TCP chunk independently and drops JSON that spans chunks; fix by implementing
buffering of partial lines inside that closure (or replace with an
eventsource_stream parser) so chunks are appended to a persistent buffer, then
split on '\n' to extract complete "data: " lines, keep any trailing partial line
in the buffer for the next chunk, parse those complete lines into StreamEvent
(the serde_json::from_str::<StreamEvent>) and surface parse errors instead of
silently ignoring them; update the closure that produces event_stream (and the
subsequent flat_map) to use this buffered approach or swap in eventsource_stream
to ensure tool calls and text_delta events aren’t lost.

- Drop features = ["otel"] (OTel always-on in 0.11.0)
- Add metadata: None to RegisterTriggerInput literals (new required field)
- 21 tests pass
@rohitg00

Copy link
Copy Markdown
Contributor Author

Heads up — main is being bumped to iii-sdk =0.11.3 in #33. When that lands, please rebase and bump this worker's pin (currently 0.11.0) to =0.11.3. Two 0.11.x deltas that may bite:

  • iii-sdk no longer exposes an otel cargo feature — OTel is always-on. Drop features = ["otel"] if you use it.
  • WorkerMetadata gained an isolation field. If you construct it as a struct literal, add ..Default::default() (or fill the field). iii-lsp hit this; see the fix in chore: bump iii-sdk to 0.11.3 across all workers #33.
  • register_function(msg, handler) (two-arg) is now register_function_with(msg, handler); register_function is single-arg via IntoFunctionRegistration. image-resize hit this; see chore: bump iii-sdk to 0.11.3 across all workers #33.

Release notes: https://github.com/iii-hq/iii/releases/tag/iii/v0.11.3

…y allowlist, planner ids

- chat.rs + chat_stream.rs: preserve the {created_at, messages} envelope
  instead of overwriting the session record with a bare messages array.
  save_history now reads the existing created_at (or mints a fresh RFC3339
  one) and writes the full envelope; load_history unwraps either shape
  (envelope or legacy bare array) so old sessions still open.
- chat_stream.rs: mint a fresh uuid session_id when the caller omits one
  so concurrent session-less requests don't collide on the shared
  agent:events: stream group.
- config.rs: validate() after deserialization — reject empty
  anthropic_model / cron string and zero max_tokens / max_iterations /
  session_ttl_hours at startup instead of breaking behaviour silently.
- session.rs: build_cleanup_handler now takes ttl_hours and honours
  config.session_ttl_hours instead of the hardcoded 24h.
- discovery.rs: flip from a hardcoded include-list of six worker
  namespaces to an exclude-list of infrastructure prefixes (agent::,
  engine::, state::, stream::, iii.). New worker namespaces are
  auto-discoverable without a code change. Add discover_tools_with(iii,
  excluded) + DEFAULT_EXCLUDED_PREFIXES so deployments can tighten.
- discovery.rs: add build_planner_capabilities(iii) that emits engine
  function_ids (eval::metrics) instead of sanitized tool names
  (eval__metrics). plan.rs uses it, so the LLM fills steps[*].function_id
  with ids downstream executors can actually invoke.
- README.md: replace 'ANTHROPIC_API_KEY=sk-ant-... ./iii-agent ...' with
  an export loaded from the keychain so the literal key doesn't land in
  shell history or ps output.
@rohitg00
rohitg00 merged commit bf91f45 into main Apr 22, 2026
5 checks passed
@rohitg00
rohitg00 deleted the feat/agent branch April 22, 2026 23:20
@coderabbitai coderabbitai Bot mentioned this pull request May 4, 2026
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