Accept story 34: Per-Project Agent Configuration and Role Definitions

Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.

- AgentConfig expanded with structured fields, validated at startup (panics
  on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-19 18:46:14 +00:00
parent f9fc2472fd
commit 6d57b06636
12 changed files with 1013 additions and 214 deletions

View File

@@ -44,6 +44,8 @@ fn remove_port_file(path: &Path) {
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let app_state = Arc::new(SessionState::default());
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
*app_state.project_root.lock().unwrap() = Some(cwd.clone());
let store = Arc::new(
JsonFileStore::from_path(PathBuf::from("store.json")).map_err(std::io::Error::other)?,
);
@@ -69,7 +71,10 @@ async fn main() -> Result<(), std::io::Error> {
println!("\x1b[96;1mFrontend:\x1b[0m \x1b[94mhttp://{addr}\x1b[0m");
println!("\x1b[92;1mOpenAPI Docs:\x1b[0m \x1b[94mhttp://{addr}/docs\x1b[0m");
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
// Validate agent config at startup — panic on invalid project.toml.
config::ProjectConfig::load(&cwd)
.unwrap_or_else(|e| panic!("Invalid project.toml: {e}"));
let port_file = write_port_file(&cwd, port);
let result = Server::new(TcpListener::bind(&addr)).run(app).await;
@@ -100,6 +105,28 @@ mod tests {
assert_eq!(parse_port(Some("not_a_number".to_string())), 3001);
}
#[test]
#[should_panic(expected = "Invalid project.toml: Duplicate agent name")]
fn panics_on_duplicate_agent_names() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".story_kit");
std::fs::create_dir_all(&sk).unwrap();
std::fs::write(
sk.join("project.toml"),
r#"
[[agent]]
name = "coder"
[[agent]]
name = "coder"
"#,
)
.unwrap();
config::ProjectConfig::load(tmp.path())
.unwrap_or_else(|e| panic!("Invalid project.toml: {e}"));
}
#[test]
fn write_and_remove_port_file() {
let tmp = tempfile::tempdir().unwrap();