Skip to content

feat: add SharedState for sub-agent communication#35

Merged
yuanhao merged 13 commits into
mainfrom
feat/shared-state
Apr 27, 2026
Merged

feat: add SharedState for sub-agent communication#35
yuanhao merged 13 commits into
mainfrom
feat/shared-state

Conversation

@yuanhao

@yuanhao yuanhao commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds SharedState — a shared key-value store (Arc<RwLock<HashMap<String, String>>>) that sub-agents can read/write via an injected shared_state tool
  • Opt-in via .with_shared_state(state) on SubAgentTool — existing behavior unchanged
  • Core agent loop untouched (no changes to types.rs, agent_loop.rs, agent.rs)
  • Closes SubAgentTool: support shared REPL state across calls (RLM primitive) #34

New files

  • src/shared_state.rsSharedState type with 10MB default cap, get/set/remove/keys/summary
  • src/tools/shared_state_tool.rsSharedStateTool implementing AgentTool (get/set/list/remove)
  • tests/shared_state_test.rs — 5 integration tests with MockProvider

Modified files

  • src/sub_agent.rsshared_state field + builder, injects tool + state summary into prompt
  • src/tools/mod.rs — register module
  • src/lib.rs — register module + re-export

Usage

let state = SharedState::new();
state.set("ci_log", large_log_text).await.unwrap();

let analyzer = SubAgentTool::new("analyzer", provider.clone())
    .with_shared_state(state.clone());  // opt-in

// Sub-agent calls shared_state(action="get", key="ci_log") to read
// Parent reads back: state.get("summary").await

Test plan

  • cargo fmt -- --check
  • cargo clippy --all-targets (with -Dwarnings)
  • cargo test — 187 tests pass, 0 failures
  • Unit tests for SharedState (get/set/remove/keys/capacity)
  • Unit tests for SharedStateTool (all actions + error cases)
  • Integration: parent stores → sub-agent reads
  • Integration: sub-agent writes → parent reads back
  • Integration: two parallel sub-agents share state
  • Backward compat: SubAgentTool without shared_state works as before

🤖 Generated with Claude Code

yuanhao and others added 8 commits April 27, 2026 08:15
Add a shared key-value store that sub-agents can read/write via an
injected tool, so large artifacts (CI logs, code, traces) are stored
once and referenced by name instead of re-pasted into every prompt.

- SharedState: Arc<RwLock<HashMap<String, String>>> with 10MB default cap
- SharedStateTool: AgentTool impl with get/set/list/remove actions
- SubAgentTool: opt-in .with_shared_state(state) builder injects tool
  and state summary into sub-agent system prompt
- Core agent loop untouched (no changes to types.rs, agent_loop.rs, agent.rs)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add CLAUDE.md section covering SharedState architecture and usage.
Add examples/shared_state.rs demonstrating parallel CI log analysis
where 3 sub-agents share a single artifact via shared state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add examples/code_review.rs — interactive code review agent that users
run on their own files. 3 parallel sub-agents (bugs, quality, docs)
share the source code via SharedState.

Update docs site:
- concepts/sub-agents.md: new Shared State section with usage examples
- reference/tools.md: document SharedStateTool

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up on_update callback so sub-agent text deltas stream to stderr
in real-time with [bugs]/[quality]/[docs] prefixes. Users see all 3
reviewers working in parallel instead of waiting in silence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Buffer per-agent text deltas and flush on newlines so parallel
sub-agent output doesn't interleave mid-line. Tool call events
flush the buffer and print on their own line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `turn_delay` to `AgentLoopConfig` and `SubAgentTool` for throttling
API calls between turns — needed for OAuth tokens with strict rate limits.

Add `examples/rlm.rs` demonstrating true recursive language model:
lead_analyst discovers files autonomously and delegates to file_analyst
sub-agents, communicating through SharedState.

Fix SubAgentTool silently swallowing provider errors by checking
StopReason::Error before extracting text. Use Debug format for transport
errors to preserve the full error chain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ok RLM example

Refactor SharedState to use a `SharedStateBackend` trait with two
implementations: `MemoryBackend` (default, same behavior) and
`FileBackend` (each key stored as a file for persistence and debugging).
Custom backends (Redis, SQLite, etc.) can implement the trait.

Add `with_model_config()` to SubAgentTool so sub-agents can use any
provider (OpenAI, xAI, Groq, etc.) — previously hardcoded to None,
silently breaking non-Anthropic sub-agents.

Switch RLM example to Grok (grok-4-1-fast-reasoning) via xAI API,
removing Anthropic OAuth rate-limit workarounds. Fix shared state
summary format from ambiguous "9B" to "9 bytes".

Update docs: sub-agents configuration table, multi-provider section,
SubAgentTool API reference, nesting supported note, examples list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show commented-out FileBackend alternative alongside the default
in-memory SharedState so users can easily switch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@yuanhao

yuanhao commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator Author

Code review

Found 5 issues:

  1. CLAUDE.md describes SharedState with outdated type (CLAUDE.md says "SharedState is a shared key-value store (Arc<RwLock<HashMap<String, String>>>)") but the implementation now uses Arc<dyn SharedStateBackend> with pluggable backends. The architecture description is factually wrong.

yoagent/CLAUDE.md

Lines 84 to 86 in 3312908

`SharedState` is a shared key-value store (`Arc<RwLock<HashMap<String, String>>>`) for sub-agent communication. It lets a parent store large artifacts once and have multiple sub-agents read/write by reference — no re-pasting into prompts.

  1. FileBackend key collision and broken keys() round-tripkey_to_path() sanitizes non-alphanumeric chars to _, so distinct keys like "summary:src/main.rs" and "summary_src_main.rs" map to the same file, causing silent overwrites. Additionally, keys() returns sanitized filenames, so key.strip_prefix("summary:") in rlm.rs will never match when using FileBackend.

yoagent/src/shared_state.rs

Lines 220 to 233 in 3312908

/// Replaces path separators and other unsafe chars with underscores.
fn key_to_path(&self, key: &str) -> PathBuf {
let safe: String = key
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' {
c
} else {
'_'
}
})
.collect();
self.dir.join(safe)
}

  1. docs/reference/configuration.md drops compaction_strategy from struct listing — The diff replaces compaction_strategy with turn_delay, but both fields exist in the real AgentLoopConfig struct. The docs now omit a public field entirely.

pub on_error: Option<OnErrorFn>,
pub input_filters: Vec<Arc<dyn InputFilter>>,
pub turn_delay: Option<Duration>,
}

  1. src/sub_agent.rs module doc still says "no nesting" — Line 10 states "sub-agents are not given other SubAgentTools" but examples/rlm.rs nests SubAgentTools, and docs/concepts/sub-agents.md was updated to say "Nesting supported". The source code comment contradicts both.

//! - **Context isolation**: each invocation starts a fresh conversation
//! - **Depth limiting**: sub-agents are not given other SubAgentTools (static, no runtime counter)
//! - **Cancellation propagation**: the parent's cancel token is forwarded

  1. FileBackend I/O errors silently swallowedSharedState::get() uses .ok().flatten(), remove() uses unwrap_or(false), keys() uses unwrap_or_default(). Any filesystem error (permission denied, disk full) is indistinguishable from "key not found" to the caller and to sub-agents.

yoagent/src/shared_state.rs

Lines 341 to 368 in 3312908

/// Get a value by key. Returns `None` if the key doesn't exist.
pub async fn get(&self, key: &str) -> Option<String> {
self.backend.get(key).await.ok().flatten()
}
/// Store a value. Returns `Err(CapacityError)` if the backend rejects it.
pub async fn set(&self, key: &str, value: String) -> Result<(), SharedStateError> {
self.backend.set(key, value).await
}
/// Remove a key. Returns `true` if the key existed.
pub async fn remove(&self, key: &str) -> bool {
self.backend.remove(key).await.unwrap_or(false)
}
/// List all keys (sorted).
pub async fn keys(&self) -> Vec<String> {
self.backend.keys().await.unwrap_or_default()
}
/// Human-readable summary of stored variables (key names + byte sizes).
/// Suitable for injecting into a system prompt.
pub async fn summary(&self) -> String {
self.backend
.summary()
.await
.unwrap_or_else(|_| "(error reading state)".to_string())
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

yuanhao and others added 5 commits April 27, 2026 15:19
- Update CLAUDE.md to reflect Arc<dyn SharedStateBackend> instead of HashMap
- Switch FileBackend to percent-encoding for reversible key mapping
- Restore compaction_strategy field in configuration.md
- Fix sub_agent.rs doc: nesting is supported, not prevented
- Log I/O errors in SharedState public API instead of silent swallowing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add SharedState API section to api.md (constructors, methods, backends, trait)
- Add FileBackend and custom backend docs to sub-agents.md
- Add turn_delay to agent-loop.md struct and field table
- Fix duplicate compaction_strategy in configuration.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@yuanhao yuanhao merged commit 1c792d1 into main Apr 27, 2026
1 check passed
@yuanhao yuanhao deleted the feat/shared-state branch April 27, 2026 13:35
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.

SubAgentTool: support shared REPL state across calls (RLM primitive)

1 participant