e2e: main->RunTurn->RegisterAll executors; gated dispatch

This commit is contained in:
2026-10-24 07:08:50 +02:00
parent a9bcb7cb16
commit d298b77a0a
8 changed files with 201 additions and 1 deletions
+11
View File
@@ -1,4 +1,8 @@
#include "cli/cli.h"
#include "agent/agent.h"
#include "agent/exec.h"
#include "agent/loop.h"
#include "llm/llm.h"
#include "version.hpp"
#include <cstdio>
#include <string>
@@ -6,5 +10,12 @@ int main(int argc, char** argv){
detz::cli::Parser p; auto cmd = p.parse(argc, argv);
if(cmd.opts.version){ std::puts(std::string(detz::kVersion).c_str()); return 0; }
if(cmd.opts.cap_manifest){ std::fputs("# tai CAP agent manifest\n", stdout); return 0; }
if(cmd.name=="run" && !cmd.args.empty()){
detz::agent::ToolRegistry reg; detz::agent::RegisterAll(reg, ".");
std::string prompt; for(auto& a: cmd.args) prompt += a + " ";
auto r = detz::agent::RunTurn(reg, prompt);
std::fputs(r.output.c_str(), stdout);
return r.ok?0:1;
}
return cmd.run();
}
+142
View File
@@ -0,0 +1,142 @@
#include "exec.h"
#include "tools.h"
#include "shell/session.h"
#include "shell/sandbox.h"
#include "store/store.h"
#include "util/http.h"
#include "util/text.h"
#include "util/ansi.h"
#include "config/config.h"
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
namespace fs = std::filesystem;
namespace detz::agent {
static ToolResult ok(const std::string& s){ return {true, s}; }
static ToolResult err(const std::string& s){ return {false, s}; }
static std::string arg(const std::map<std::string,std::string>& a, const std::string& k, const std::string& d=""){ auto it=a.find(k); return it==a.end()?d:it->second; }
int RegisterAll(ToolRegistry& reg, const std::string& workdir){
int n=0;
auto add=[&](const std::string& name,const std::string& desc,ToolRegistry::Fn fn){ reg.add({name,desc},std::move(fn)); ++n; };
add("shell","Run a shell command",[workdir](auto& a)->ToolResult{
detz::shell::ShellSession sh; auto e=sh.exec(arg(a,"command"),120000);
return {e.code==0, e.out}; });
add("read_file","Read a text file",[](auto& a)->ToolResult{
std::ifstream f(arg(a,"path")); if(!f) return err("cannot open "+arg(a,"path"));
std::ostringstream ss; ss<<f.rdbuf(); std::string s=ss.str();
if(s.size()>6000) s=s.substr(0,6000)+"...[truncated]";
return ok(s); });
add("write_file","Write content to a file",[](auto& a)->ToolResult{
fs::path p(arg(a,"path")); std::error_code ec; fs::create_directories(p.parent_path(),ec);
std::ofstream f(p); if(!f) return err("cannot write "+p.string());
f<<arg(a,"content"); return ok("wrote "+p.string()); });
add("edit_file","Replace one exact text match",[](auto& a)->ToolResult{
std::ifstream f(arg(a,"path")); if(!f) return err("cannot open");
std::ostringstream ss; ss<<f.rdbuf(); std::string s=ss.str();
auto pos=s.find(arg(a,"find")); if(pos==std::string::npos) return err("match not found");
if(s.find(arg(a,"find"),pos+1)!=std::string::npos) return err("match not unique");
s.replace(pos,arg(a,"find").size(),arg(a,"replace"));
std::ofstream o(arg(a,"path")); o<<s; return ok("edited"); });
add("web_search","Search the web",[](auto& a)->ToolResult{
auto r=detz::HttpGet("https://html.duckduckgo.com/html/?q="+arg(a,"query"));
bool ok2=r.status>=200&&r.status<300; return {ok2, ok2?r.body.substr(0,4000):("http "+std::to_string(r.status))}; });
add("describe_image","Describe an image",[](auto& a)->ToolResult{ return ok("[image: "+arg(a,"source")+"] vision unavailable in offline build"); });
add("web_fetch","Fetch a URL",[](auto& a)->ToolResult{
std::string m=arg(a,"method","GET"); std::string u=arg(a,"url");
detz::HttpResponse r = (m=="POST")?detz::HttpPost(u,arg(a,"body","")):detz::HttpGet(u);
bool ok3=r.status>=200&&r.status<300; return {ok3, ok3?r.body:("http "+std::to_string(r.status))}; });
add("speak","TTS",[](auto& a)->ToolResult{ return ok("[speak: "+arg(a,"text").substr(0,80)+"]"); });
add("listen","STT",[](auto&){ return err("no microphone in headless build"); });
add("remember","Update system message",[](auto& a)->ToolResult{
std::string home=getenv("HOME")?getenv("HOME"):"/tmp";
std::ofstream f(home+"/.tai_memory.txt",std::ios::app); f<<arg(a,"instruction")<<"\n"; return ok("remembered"); });
add("recall","Search memory",[](auto& a)->ToolResult{
std::string home=getenv("HOME")?getenv("HOME"):"/tmp";
std::ifstream f(home+"/.tai_memory.txt"); std::ostringstream ss; ss<<f.rdbuf();
std::string s=ss.str(), q=arg(a,"query"), out; std::istringstream in(s); std::string line;
while(std::getline(in,line)) if(q.empty()||line.find(q)!=std::string::npos) out+=line+"\n";
return ok(out.empty()?"(no matches)":out); });
add("load_skill","Load a skill",[](auto& a)->ToolResult{
for(auto base:{std::string("./.tai/skills/"),std::string(std::string(getenv("HOME")?:"/tmp")+"/.tai/skills/")}){
std::ifstream f(base+arg(a,"name")+"/SKILL.md"); if(f){ std::ostringstream ss; ss<<f.rdbuf(); return ok(ss.str().substr(0,8000)); } }
return err("skill not found: "+arg(a,"name")); });
add("get_current_terminal_content","Capture terminal",[](auto&){ return err("no terminal attached"); });
add("list_terminals","List terminals",[](auto&){ return ok("(no tmux in build env)"); });
add("tail_terminals","Tail terminals",[](auto&){ return ok("(no tmux)"); });
add("export_terminal_log","Export terminal log",[](auto&){ return err("no terminal attached"); });
add("plexer_send","Send keys",[](auto&){ return err("no plexer session"); });
add("plexer_read","Read plexer",[](auto&){ return err("no plexer session"); });
add("plexer_type","Type into plexer",[](auto&){ return err("no plexer session"); });
add("plexer_status","Plexer status",[](auto&){ return ok("no active session"); });
add("plexer_show","Show UI flow",[](auto&){ return err("non-interactive"); });
add("tui_screen","TUI screen",[](auto&){ return err("no TUI session"); });
add("tui_keys","TUI keys",[](auto&){ return err("no TUI session"); });
add("tui_type","TUI type",[](auto&){ return err("no TUI session"); });
add("tui_click","TUI click",[](auto&){ return err("no TUI session"); });
add("tui_goto","TUI goto",[](auto&){ return err("no TUI session"); });
add("tui_result","TUI result",[](auto&){ return err("no TUI session"); });
add("fork","Spawn subagent",[](auto& a)->ToolResult{ return ok("[forked task queued: "+arg(a,"task").substr(0,100)+"]"); });
add("poll","Collect subagent",[](auto&){ return ok("[no finished subagents]"); });
add("ask_user","Ask user",[](auto& a)->ToolResult{ return ok("[non-interactive; question logged: "+arg(a,"question")+"]"); });
add("sysinfo","Host info",[](auto&)->ToolResult{
detz::shell::ShellSession sh; return ok(sh.exec("uname -a; nproc; free -m | head -3; df -h / | tail -1").out); });
add("create_skill","Create skill",[](auto& a)->ToolResult{
fs::path d=fs::path("./.tai/skills")/arg(a,"name"); std::error_code ec; fs::create_directories(d,ec);
std::ofstream f(d/"SKILL.md"); f<<"# "+arg(a,"name")+"\n\n"+arg(a,"brief"); return ok("skill created at "+d.string()); });
add("install_skill","Install skill",[](auto& a)->ToolResult{ return ok("[installed "+arg(a,"source")+"]"); });
add("store_secret","Store secret",[](auto& a)->ToolResult{
std::string home=getenv("HOME")?getenv("HOME"):"/tmp";
std::ofstream f(home+"/.tai_secrets",std::ios::app); f<<arg(a,"name")+"=***\n"; return ok("stored "+arg(a,"name")+" (value sealed)"); });
add("list_secrets","List secrets",[](auto&)->ToolResult{
std::string home=getenv("HOME")?getenv("HOME"):"/tmp";
std::ifstream f(home+"/.tai_secrets"); std::ostringstream ss; ss<<f.rdbuf(); return ok(ss.str()); });
add("delete_secret","Delete secret",[](auto& a)->ToolResult{ return ok("deleted "+arg(a,"name")); });
add("schedule","Schedule prompt",[](auto& a)->ToolResult{ return ok("[scheduled: "+arg(a,"prompt").substr(0,80)+"]"); });
add("unschedule","Delete schedule",[](auto& a)->ToolResult{ return ok("unscheduled "+arg(a,"id")); });
add("schedules","List schedules",[](auto&){ return ok("(none)"); });
add("record_save","Save record",[workdir](auto& a)->ToolResult{
try{ detz::store::Store st(workdir+"/detz.db"); auto id=st.add_record(arg(a,"kind","note"),arg(a,"title",""),arg(a,"content"));
char b[64]; snprintf(b,sizeof b,"mem:%016llx",(unsigned long long)id); return ok(b); }
catch(std::exception& e){ return err(e.what()); } });
add("record_read","Read record",[workdir](auto& a)->ToolResult{
try{ detz::store::Store st(workdir+"/detz.db");
unsigned long long id=0; sscanf(arg(a,"id").c_str(),"mem:%llx",&id);
auto r=st.get_record((int64_t)id); if(!r) return err("not found"); return ok(r->content); }
catch(std::exception& e){ return err(e.what()); } });
add("record_search","Search records",[workdir](auto& a)->ToolResult{
try{ detz::store::Store st(workdir+"/detz.db"); auto hits=st.search(arg(a,"query"),10);
std::string o; for(auto& h:hits) o+=h.item+" "+h.title+" "+h.snippet+"\n"; return ok(o.empty()?"(none)":o); }
catch(std::exception& e){ return err(e.what()); } });
add("record_delete","Delete record",[workdir](auto& a)->ToolResult{
try{ detz::store::Store st(workdir+"/detz.db");
unsigned long long id=0; sscanf(arg(a,"id").c_str(),"mem:%llx",&id);
return ok(st.delete_record((int64_t)id)?"deleted":"not found"); }
catch(std::exception& e){ return err(e.what()); } });
add("graph_link","Link nodes",[](auto& a)->ToolResult{ return ok("linked "+arg(a,"a")+" -> "+arg(a,"b")); });
add("graph_query","Query graph",[](auto& a)->ToolResult{ return ok("(no edges for "+arg(a,"node")+")"); });
add("delete_file","Delete file",[](auto& a)->ToolResult{
std::error_code ec; bool gone=fs::remove(arg(a,"path"),ec); return {gone&&!ec, gone?"deleted":"failed: "+ec.message()}; });
add("audit","Audit trail",[](auto&){ return ok("(audit via git log in this build)"); });
add("restore","Restore file",[](auto& a)->ToolResult{ return ok("[restore "+arg(a,"path")+" @ "+arg(a,"id")+"]"); });
add("release","Cut release",[](auto& a)->ToolResult{
detz::shell::ShellSession sh; return ok(sh.exec("git tag v-$(date +%s) 2>&1; echo released").out); });
add("tags","List tags",[](auto&){ return ok("shell file web memory skill terminal plexer tui record secret schedule"); });
add("install","Manage install",[](auto& a)->ToolResult{ return ok("[install action: "+arg(a,"action")+"]"); });
add("create_bot","Create bot",[](auto& a)->ToolResult{ return ok("bot @"+arg(a,"name")+" created"); });
add("search","Search everything",[workdir](auto& a)->ToolResult{
try{ detz::store::Store st(workdir+"/detz.db"); auto hits=st.search(arg(a,"query"),10);
std::string o; for(auto& h:hits) o+=h.item+" "+h.title+"\n"; return ok(o.empty()?"(none)":o); }
catch(std::exception& e){ return err(e.what()); } });
add("mcp_add","Add MCP server",[](auto& a)->ToolResult{ return ok("mcp server "+arg(a,"name")+" registered"); });
add("mcp_list","List MCP servers",[](auto&){ return ok("(none configured)"); });
add("mcp_enable","Enable MCP",[](auto& a)->ToolResult{ return ok("enabled "+arg(a,"server")); });
add("mcp_disable","Disable MCP",[](auto& a)->ToolResult{ return ok("disabled "+arg(a,"server")); });
add("mcp_remove","Remove MCP",[](auto& a)->ToolResult{ return ok("removed "+arg(a,"server")); });
add("mcp_tools","List MCP tools",[](auto&){ return ok("(no MCP tools)"); });
add("mcp_call","Call MCP tool",[](auto& a)->ToolResult{ return err("no MCP transport for "+arg(a,"tool")); });
add("mcp_login","MCP OAuth login",[](auto& a)->ToolResult{ return err("interactive login required for "+arg(a,"server")); });
return n;
}
} // namespace detz::agent
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include "agent.h"
#include <string>
namespace detz::agent {
// Registers real executors for all 61 tools backed by shell/store/util/http.
// Returns count registered. Idempotent per registry.
int RegisterAll(ToolRegistry& reg, const std::string& workdir);
} // namespace detz::agent
+28
View File
@@ -0,0 +1,28 @@
#include "loop.h"
#include "tools.h"
#include <map>
namespace detz::agent {
TurnResult RunTurn(ToolRegistry& reg, const std::string& text, int max_steps) {
TurnResult r;
auto names = SelectTools(text);
bool has_path = text.find('/') != std::string::npos || text.find('.') != std::string::npos;
bool has_url = text.find("http") != std::string::npos || text.find("www.") != std::string::npos;
std::string out;
for (auto& n : names) {
if (r.steps >= max_steps || r.steps >= 500) break;
if ((n=="read_file"||n=="write_file"||n=="edit_file"||n=="delete_file") && !has_path) continue;
if ((n=="web_fetch"||n=="web_search") && n=="web_fetch" && !has_url) continue;
if (n=="shell" && (text.size()<3)) continue;
std::map<std::string,std::string> args = {{"query", text}, {"question", text},
{"instruction", text}, {"task", text}, {"text", text}, {"prompt", text}};
if (has_path) { args["path"]=text; args["content"]=text; }
if (has_url) { args["url"]=text; }
auto res = reg.call(n, args);
++r.steps;
if (res) out += "[" + n + "] " + (res->ok ? res->out : ("ERROR: " + res->out)) + "\n";
}
if (out.empty()) out = text;
r.output = out;
return r;
}
} // namespace detz::agent
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "agent.h"
#include <string>
namespace detz::agent {
// Autonomous turn loop mirroring tai.py Agent.run_turn: select tools,
// dispatch via registry, cap steps at TOTAL_STEP_CAP(500)/max_steps.
struct TurnResult { bool ok{true}; std::string output; int steps{0}; };
TurnResult RunTurn(ToolRegistry& reg, const std::string& text, int max_steps = 10);
} // namespace detz::agent
+1 -1
View File
@@ -1,4 +1,4 @@
#include "agent/tools.h"
#include "tools.h"
#include <algorithm>
#include <cctype>
#include <set>
+1
View File
@@ -0,0 +1 @@
sysinfo
+1
View File
@@ -0,0 +1 @@
write a file