Spike: PTY-based Claude Code integration with multi-agent concurrency

Proves that spawning `claude -p` in a pseudo-terminal from Rust gets Max
subscription billing (apiKeySource: "none", rateLimitType: "five_hour")
instead of per-token API charges. Concurrent agents run in parallel PTY
sessions with session resumption via --resume for multi-turn conversations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-19 15:25:22 +00:00
parent 8f0bc971bf
commit 68a19c393e
17 changed files with 1159 additions and 22 deletions

View File

@@ -189,12 +189,55 @@ where
.clone()
.unwrap_or_else(|| "http://localhost:11434".to_string());
let is_claude = config.model.starts_with("claude-");
eprintln!("[chat] provider={} model={}", config.provider, config.model);
let is_claude_code = config.provider == "claude-code";
let is_claude = !is_claude_code && config.model.starts_with("claude-");
if !is_claude && config.provider.as_str() != "ollama" {
if !is_claude_code && !is_claude && config.provider.as_str() != "ollama" {
return Err(format!("Unsupported provider: {}", config.provider));
}
// Claude Code provider: bypasses our tool loop entirely.
// Claude Code has its own agent loop, tools, and context management.
// We just pipe the user message in and stream raw output back.
if is_claude_code {
use crate::llm::providers::claude_code::ClaudeCodeProvider;
let user_message = messages
.iter()
.rev()
.find(|m| m.role == Role::User)
.map(|m| m.content.clone())
.ok_or_else(|| "No user message found".to_string())?;
let project_root = state
.get_project_root()
.unwrap_or_else(|_| std::path::PathBuf::from("."));
let provider = ClaudeCodeProvider::new();
let response = provider
.chat_stream(
&user_message,
&project_root.to_string_lossy(),
&mut cancel_rx,
|token| on_token(token),
)
.await
.map_err(|e| format!("Claude Code Error: {e}"))?;
let assistant_msg = Message {
role: Role::Assistant,
content: response.content.unwrap_or_default(),
tool_calls: None,
tool_call_id: None,
};
let mut result = messages.clone();
result.push(assistant_msg);
on_update(&result);
return Ok(result);
}
let tool_defs = get_tool_definitions();
let tools = if config.enable_tools.unwrap_or(true) {
tool_defs.as_slice()