Files
codecraft/server/src/tools/agent.rs
T
Richard ZinkandClaude Opus 5.5 49262c612c Users, file manager, image generation, budgets and agent workflow
- User management: local accounts (Argon2) and OpenID Connect, admin/user
  roles, per-user folders plus admin-shared folders, read-only session
  sharing, event filtering per user
- File manager with CodeMirror editor and git client (status, diff,
  stage, commit, branches, fetch/pull/push, log, clone/init)
- Image generation via fal.ai: generate_image tool and image page
- Budgets: monthly limits per provider, provider balances/costs
  (OpenRouter, Anthropic admin cost report, fal.ai), price discovery
- Subagents on other models, background tasks, concurrency limits per
  host/model/subagent
- MCP tools loaded on demand (mcp_search/mcp_call), read-only hints
- Permission mode "auto", safe read-only commands always allowed
- Persistent message queue with attachments, deletable
- Live reasoning incl. <think> tags, activity animations, CSS themes,
  mobile layout, statistics per user, upload size handling
- Docker: named volume for state, localhost-bound port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:54:08 +01:00

178 lines
7.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Tools that interact with the harness itself: todo list, questions to the
//! user and subagents.
use super::{Tool, ToolCtx, ToolOutput, arg_str};
use crate::types::Question;
use anyhow::{Result, bail};
use async_trait::async_trait;
use serde_json::{Value, json};
pub struct TodoWrite;
#[async_trait]
impl Tool for TodoWrite {
fn name(&self) -> &str {
"todo_write"
}
fn description(&self) -> String {
"Maintain a task list for the current work, shown live to the user. Send the complete \
list each time. Use it for multi-step tasks: exactly one item in_progress at a time, \
mark items completed as soon as they are done."
.into()
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"todos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"content": {"type": "string"},
"status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}
},
"required": ["content", "status"]
}
}
},
"required": ["todos"]
})
}
fn read_only(&self) -> bool {
true
}
async fn run(&self, input: Value, ctx: &ToolCtx) -> Result<ToolOutput> {
let todos = input.get("todos").cloned().unwrap_or(json!([]));
if !todos.is_array() {
bail!("todos must be an array");
}
ctx.app.set_todos(&ctx.session_id, todos.clone())?;
let open = todos.as_array().unwrap().iter().filter(|t| t["status"] != "completed").count();
Ok(ToolOutput::text(format!("Todo list updated ({open} open).")).with_meta(json!({"todos": todos})))
}
}
// ---------------------------------------------------------------------------
pub struct AskUser;
#[async_trait]
impl Tool for AskUser {
fn name(&self) -> &str {
"ask_user"
}
fn description(&self) -> String {
"Ask the user one or more questions and wait for the answers. Use this only when you are \
blocked on a decision that is genuinely the user's (requirements, preferences, choices \
between valid approaches) – not for things you can find out yourself. Offer 2-4 concrete \
options per question; the user can always answer with free text instead."
.into()
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"questions": {
"type": "array",
"minItems": 1,
"maxItems": 4,
"items": {
"type": "object",
"properties": {
"question": {"type": "string"},
"header": {"type": "string", "description": "Very short label (max 12 chars)"},
"multi_select": {"type": "boolean"},
"options": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {"type": "string"},
"description": {"type": "string"}
},
"required": ["label"]
}
}
},
"required": ["question"]
}
}
},
"required": ["questions"]
})
}
fn read_only(&self) -> bool {
true
}
async fn run(&self, input: Value, ctx: &ToolCtx) -> Result<ToolOutput> {
let questions: Vec<Question> = serde_json::from_value(input["questions"].clone())?;
if questions.is_empty() {
bail!("at least one question is required");
}
let answers = ctx
.app
.ask_user(&ctx.session_id, "agent", questions.clone(), ctx.cancel.clone())
.await?;
let Some(answers) = answers else {
return Ok(ToolOutput::error("The user dismissed the questions without answering."));
};
let mut out = String::from("The user answered:\n");
for q in &questions {
let a = answers.get(&q.question).map(|v| match v {
Value::String(s) => s.clone(),
Value::Array(a) => a.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>().join(", "),
other => other.to_string(),
});
out.push_str(&format!("- {}\n → {}\n", q.question, a.unwrap_or_else(|| "(no answer)".into())));
}
Ok(ToolOutput::text(out).with_meta(json!({"answers": answers})))
}
}
// ---------------------------------------------------------------------------
pub struct Task;
#[async_trait]
impl Tool for Task {
fn name(&self) -> &str {
"task"
}
fn description(&self) -> String {
"Launch a subagent to handle an independent, multi-step task in its own context (e.g. a \
broad codebase search, a focused review). The subagent only knows what you put in the \
prompt – write a complete, self-contained brief. Its final message is returned to you. \
Several task calls in one response run in parallel (subject to the configured limits). \
With background=true you continue working and receive the result later."
.into()
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"description": {"type": "string", "description": "Short (3-5 words) task description"},
"prompt": {"type": "string", "description": "The full brief for the subagent"},
"subagent_type": {"type": "string", "description": "Subagent to use (default: general)"},
"background": {"type": "boolean", "description": "Run in the background: returns immediately, the result arrives later as a message. Use for work you don't need right away (e.g. writing docs, summaries, translations)."}
},
"required": ["description", "prompt"]
})
}
fn read_only(&self) -> bool {
// Read-only agents (plan, explore) can spawn read-only subagents; the runner enforces it.
true
}
fn title(&self, input: &Value) -> String {
format!("Subagent: {}", input["description"].as_str().unwrap_or("task"))
}
async fn run(&self, input: Value, ctx: &ToolCtx) -> Result<ToolOutput> {
let prompt = arg_str(&input, "prompt")?.to_string();
let description = input["description"].as_str().unwrap_or("subagent").to_string();
let agent = input["subagent_type"].as_str().unwrap_or("general").to_string();
let (child_id, result) =
crate::runner::run_subagent(ctx.app.clone(), ctx, &agent, &description, prompt).await?;
Ok(ToolOutput::text(result).with_meta(json!({"session_id": child_id, "agent": agent})))
}
}