Initial CodeCraft: web-based coding harness for AI models
Rust backend (axum/tokio): agent loop with streaming, tools (fs, bash, web_fetch, todo, ask_user, subagents, skills, craft snapshots/compare), permissions with modes and rules, Anthropic + OpenAI-compatible providers, MCP client (stdio/HTTP, elicitation), SQLite sessions, usage statistics, compaction, commented TOML config editable via toml_edit. React PWA: chat with live streaming, permission/question cards, modes (build/plan/craft), settings for providers/models/prices/MCP/permissions, raw config editor, statistics dashboard, i18n (en/de). Docker setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
f3a2fe3816
62 files changed
+21835
No files matched your search
@@ -0,0 +1,175 @@
|
||||
//! 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."
|
||||
.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)"}
|
||||
},
|
||||
"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})))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user