417 lines
14 KiB
C
Raw Normal View History

2026-01-28 19:34:39 +01:00
// retoor <retoor@molodetz.nl>
#include "agent.h"
#include "db.h"
#include "http_client.h"
#include "r_config.h"
#include "r_error.h"
2026-02-10 21:20:19 +01:00
#include "spawn_tracker.h"
2026-01-28 19:34:39 +01:00
#include "tool.h"
2026-01-29 02:27:20 +01:00
#include "line.h"
#include "markdown.h"
#include "utils.h"
2026-02-10 04:29:48 +01:00
#include <curl/curl.h>
2026-01-28 19:34:39 +01:00
#include <json-c/json.h>
#include <locale.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static volatile sig_atomic_t sigint_count = 0;
static time_t first_sigint_time = 0;
static bool syntax_highlight_enabled = true;
static bool api_mode = false;
static db_handle global_db = NULL;
static messages_handle global_messages = NULL;
extern tool_registry_t *tools_get_registry(void);
extern void tools_registry_shutdown(void);
static bool include_file(const char *path);
static char *get_prompt_from_stdin(char *prompt);
static char *get_prompt_from_args(int argc, char **argv);
static bool try_prompt(int argc, char *argv[]);
static void repl(void);
static void init(void);
static void cleanup(void);
static void handle_sigint(int sig);
static char *get_env_string(void) {
2026-01-28 19:47:14 +01:00
FILE *fp = popen("env", "r");
if (!fp)
return NULL;
size_t buffer_size = 1024;
size_t total_size = 0;
char *output = malloc(buffer_size);
if (!output) {
pclose(fp);
return NULL;
}
size_t bytes_read;
while ((bytes_read = fread(output + total_size, 1, buffer_size - total_size,
fp)) > 0) {
total_size += bytes_read;
if (total_size >= buffer_size) {
buffer_size *= 2;
char *temp = realloc(output, buffer_size);
if (!temp) {
free(output);
2026-01-28 19:34:39 +01:00
pclose(fp);
return NULL;
2026-01-28 19:47:14 +01:00
}
output = temp;
2026-01-28 19:34:39 +01:00
}
2026-01-28 19:47:14 +01:00
}
output[total_size] = '\0';
pclose(fp);
return output;
2026-01-28 19:34:39 +01:00
}
static char *get_prompt_from_stdin(char *prompt) {
2026-01-28 19:47:14 +01:00
int index = 0;
int c;
while ((c = getchar()) != EOF) {
prompt[index++] = (char)c;
}
prompt[index] = '\0';
return prompt;
2026-01-28 19:34:39 +01:00
}
static char *get_prompt_from_args(int argc, char **argv) {
2026-01-28 19:47:14 +01:00
r_config_handle cfg = r_config_get_instance();
char *prompt = malloc(10 * 1024 * 1024 + 1);
char *system_msg = malloc(1024 * 1024);
if (!prompt || !system_msg) {
free(prompt);
free(system_msg);
return NULL;
}
system_msg[0] = '\0';
bool get_from_stdin = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--stdin") == 0) {
fprintf(stderr, "Reading from stdin.\n");
get_from_stdin = true;
} else if (strcmp(argv[i], "--verbose") == 0) {
r_config_set_verbose(cfg, true);
} else if (strcmp(argv[i], "--py") == 0 && i + 1 < argc) {
char *py_file_path = expand_home_directory(argv[++i]);
fprintf(stderr, "Including \"%s\".\n", py_file_path);
include_file(py_file_path);
free(py_file_path);
} else if (strcmp(argv[i], "--context") == 0 && i + 1 < argc) {
char *context_file_path = argv[++i];
fprintf(stderr, "Including \"%s\".\n", context_file_path);
include_file(context_file_path);
} else if (strcmp(argv[i], "--api") == 0) {
api_mode = true;
} else if (strcmp(argv[i], "--nh") == 0) {
syntax_highlight_enabled = false;
fprintf(stderr, "Syntax highlighting disabled.\n");
} else if (strncmp(argv[i], "--session=", 10) == 0) {
continue;
} else if (strcmp(argv[i], "-s") == 0 ||
strcmp(argv[i], "--session") == 0) {
i++;
continue;
2026-01-28 19:34:39 +01:00
} else {
2026-01-28 19:47:14 +01:00
strcat(system_msg, argv[i]);
strcat(system_msg, (i < argc - 1) ? " " : ".");
2026-01-28 19:34:39 +01:00
}
2026-01-28 19:47:14 +01:00
}
if (get_from_stdin) {
if (*system_msg && global_messages) {
messages_add(global_messages, "system", system_msg);
2026-01-28 19:34:39 +01:00
}
2026-01-28 19:47:14 +01:00
prompt = get_prompt_from_stdin(prompt);
free(system_msg);
} else {
free(prompt);
prompt = system_msg;
}
if (!*prompt) {
free(prompt);
return NULL;
}
return prompt;
2026-01-28 19:34:39 +01:00
}
static bool try_prompt(int argc, char *argv[]) {
2026-01-28 19:47:14 +01:00
char *prompt = get_prompt_from_args(argc, argv);
if (prompt) {
char *response = agent_chat(prompt, global_messages);
if (!response) {
printf("Could not get response from server\n");
free(prompt);
return false;
2026-01-28 19:34:39 +01:00
}
2026-01-28 20:06:18 +01:00
// response is already printed inside agent_run
2026-01-28 19:47:14 +01:00
free(response);
free(prompt);
return true;
}
return false;
2026-01-28 19:34:39 +01:00
}
static bool include_file(const char *path) {
2026-01-28 19:47:14 +01:00
char *file_content = read_file(path);
if (!file_content)
return false;
if (global_messages) {
messages_add(global_messages, "system", file_content);
}
free(file_content);
return true;
2026-01-28 19:34:39 +01:00
}
static void repl(void) {
2026-01-28 19:47:14 +01:00
r_config_handle cfg = r_config_get_instance();
tool_registry_t *tools = tools_get_registry();
line_init();
char *line = NULL;
while (true) {
line = line_read("> ");
if (!line || !*line)
continue;
if (!strncmp(line, "!dump", 5)) {
char *json = messages_to_string(global_messages);
if (json) {
printf("%s\n", json);
free(json);
}
continue;
2026-01-28 19:34:39 +01:00
}
2026-01-28 19:47:14 +01:00
if (!strncmp(line, "!clear", 6)) {
messages_clear(global_messages);
fprintf(stderr, "Session cleared.\n");
continue;
2026-01-28 19:34:39 +01:00
}
2026-01-28 19:47:14 +01:00
if (!strncmp(line, "!session", 8)) {
printf("Session: %s\n", messages_get_session_id(global_messages));
continue;
2026-01-28 19:34:39 +01:00
}
2026-02-10 04:29:48 +01:00
if (!strncmp(line, "!new", 4)) {
messages_clear(global_messages);
char session_id[64];
snprintf(session_id, sizeof(session_id), "session-%d-%ld", getpid(), (long)time(NULL));
messages_set_session_id(global_messages, session_id);
fprintf(stderr, "New session: %s\n", session_id);
continue;
}
2026-01-28 19:47:14 +01:00
if (!strncmp(line, "!verbose", 8)) {
bool verbose = !r_config_is_verbose(cfg);
r_config_set_verbose(cfg, verbose);
fprintf(stderr, "%s\n",
verbose ? "Verbose mode enabled" : "Verbose mode disabled");
continue;
}
if (line && *line != '\n') {
line_add_history(line);
}
if (!strncmp(line, "!tools", 6)) {
struct json_object *descs = tool_registry_get_descriptions(tools);
printf("Available tools: %s\n", json_object_to_json_string(descs));
continue;
}
if (!strncmp(line, "!models", 7)) {
http_client_handle http = http_client_create(r_config_get_api_key(cfg));
if (http) {
http_client_set_show_spinner(http, false);
char *response = NULL;
if (http_get(http, r_config_get_models_url(cfg), &response) ==
R_SUCCESS &&
response) {
printf("Models: %s\n", response);
free(response);
}
http_client_destroy(http);
}
continue;
}
if (!strncmp(line, "!model", 6)) {
if (line[6] == ' ') {
r_config_set_model(cfg, line + 7);
}
printf("Current model: %s\n", r_config_get_model(cfg));
continue;
}
if (!strncmp(line, "exit", 4)) {
exit(0);
2026-01-28 19:34:39 +01:00
}
2026-01-28 19:47:14 +01:00
while (line && *line != '\n') {
char *response = agent_chat(line, global_messages);
if (response) {
2026-01-29 07:42:06 +01:00
// response is already printed inside agent_run via
// parse_markdown_to_ansi
2026-01-28 19:47:14 +01:00
free(response);
} else {
fprintf(stderr, "Agent returned no response\n");
}
2026-01-28 20:06:18 +01:00
line = NULL;
2026-01-28 19:34:39 +01:00
}
2026-01-28 19:47:14 +01:00
}
}
static void init(void) {
2026-02-10 04:29:48 +01:00
curl_global_init(CURL_GLOBAL_DEFAULT);
2026-01-28 19:47:14 +01:00
setbuf(stdout, NULL);
line_init();
r_config_handle cfg = r_config_get_instance();
global_db = db_open(NULL);
global_messages = messages_create(r_config_get_session_id(cfg));
char *schema = db_get_schema(global_db);
char payload[1024 * 1024] = {0};
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char datetime[64];
strftime(datetime, sizeof(datetime), "%Y-%m-%d %H:%M:%S %Z", tm_info);
char cwd[4096];
if (!getcwd(cwd, sizeof(cwd))) {
strcpy(cwd, "unknown");
}
snprintf(
payload, sizeof(payload),
"# AUTONOMOUS AGENT INSTRUCTIONS\n"
"Current date/time: %s\n"
"Working directory: %s\n\n"
2026-02-10 21:20:19 +01:00
"You are an autonomous AI agent with tools and sub-agents at your disposal.\n\n"
"## RULE #1: CLASSIFY BEFORE ACTING\n"
"Before doing ANYTHING, classify the user's request:\n\n"
"**SIMPLE** — questions, greetings, opinions, math, explanations, "
"small file reads/writes, single-step tasks:\n"
" -> Respond directly with text. Use a tool ONLY if the user explicitly "
"asks for something that requires one (e.g. 'read file X', 'search for Y').\n"
" -> Do NOT browse files, update PROJECT_KNOWLEDGE.md, or spawn agents.\n"
" -> Do NOT use tools just to 'explore' or 'get context'.\n\n"
"**COMPLEX** — multi-step research, building projects, security audits, "
"tasks that require multiple tools or sub-agents:\n"
" -> Use the full orchestration framework described below.\n"
" -> Keep using tools until the task is fully complete.\n\n"
"If unsure, treat it as SIMPLE. Only escalate to COMPLEX when the task "
"clearly requires multiple steps.\n\n"
"## Orchestration Framework (COMPLEX tasks only)\n"
"You are the **Executive Agent (Apex)**. Delegate to specialized sub-agents:\n"
"- **researcher**: Information gathering, web search, data extraction\n"
"- **developer**: Coding, testing, debugging, file creation\n"
"- **security**: Security audits, vulnerability analysis\n"
"- **fetcher**: URL content retrieval\n\n"
"### Hierarchy\n"
"- **Executive (Apex)**: Final arbiter. Owns the Strategic Blueprint.\n"
"- **Managers**: Create detailed Task Packs. Synthesize sub-agent outputs.\n"
"- **Workers**: Execute atomic tasks.\n\n"
"### Protocols (COMPLEX tasks only)\n"
"1. **Strategic Blueprint**: Output a blueprint: Mission, Departments, Checklist.\n"
"2. **Sequential Handover**: Do not spawn a Developer until the Researcher "
"has delivered documented facts to `PROJECT_KNOWLEDGE.md`.\n"
"3. **Content Depth Guardrail**: Placeholder text is a failure. Use "
"'read_file' to audit sub-agent work before concluding.\n"
"4. **Global Task Registry (GTR)**: Query GTR for every sub-task. "
"DUPLICATION IS FORBIDDEN.\n"
"5. **Fan-Out Architecture**: Manager calls `web_search` to get URLs, "
"then uses `research_dispatcher` to queue them.\n\n"
"### Shared Memory (COMPLEX tasks only)\n"
"- Update `PROJECT_KNOWLEDGE.md` with new findings.\n"
"- All sub-agents receive the full content of `PROJECT_KNOWLEDGE.md`.\n\n"
"### Sub-Agent Result Handling\n"
"When a sub-agent returns, read and synthesize the result. "
"If the user asked to save results, call write_file yourself.\n\n"
"### Spawn Limits\n"
"The system enforces spawn depth and total spawn limits automatically.\n\n"
"## Tool Usage (when tools are needed)\n"
"- Only use tools when the task requires them.\n"
"- If a tool fails, analyze and retry with a different approach.\n"
"- When working on a COMPLEX task, keep calling tools until done.\n\n"
"## Output Rules\n"
"- When you use tools, include actual data from results in your response.\n"
"- Do not claim a task is done unless verified.\n"
"- For COMPLEX tasks, perform one logical step at a time.\n"
"## Python\n"
"Use native python only, no 3rd party packages unless verified installed.\n"
2026-01-28 19:47:14 +01:00
"## Local Database\n"
2026-02-10 21:20:19 +01:00
"SQLite via db_query, db_get, db_set. Use stemmed lowercase keys.\n"
2026-01-28 19:47:14 +01:00
"Schema: %s\n\n"
"## Response Format\n"
2026-02-10 21:20:19 +01:00
"Your response is the only thing the user sees. Tool outputs are hidden.\n"
"Copy relevant data from tool results into your response.\n"
"## Backup\n"
"Make a .bak backup before editing files you did not create.\n"
"## Terminal\n"
"You have bash access. Prefer commands that do not require root.\n",
2026-01-29 07:42:06 +01:00
datetime, cwd, schema ? schema : "{}");
2026-01-28 19:47:14 +01:00
free(schema);
fprintf(stderr, "Loading...");
if (global_messages) {
messages_add(global_messages, "system", payload);
}
const char *env_system_msg = r_config_get_system_message(cfg);
if (env_system_msg && *env_system_msg && global_messages) {
messages_add(global_messages, "system", env_system_msg);
}
if (!include_file(".rcontext.txt")) {
include_file("~/.rcontext.txt");
}
fprintf(stderr, "\r \r");
2026-01-28 19:34:39 +01:00
}
static void cleanup(void) {
2026-01-28 19:47:14 +01:00
if (global_messages) {
messages_destroy(global_messages);
global_messages = NULL;
}
if (global_db) {
db_close(global_db);
global_db = NULL;
}
tools_registry_shutdown();
2026-02-10 21:20:19 +01:00
spawn_tracker_destroy();
2026-01-28 19:47:14 +01:00
r_config_destroy();
2026-01-28 19:34:39 +01:00
}
static void handle_sigint(int sig) {
2026-01-28 19:47:14 +01:00
(void)sig;
time_t current_time = time(NULL);
printf("\n");
if (sigint_count == 0) {
first_sigint_time = current_time;
sigint_count++;
} else {
if (difftime(current_time, first_sigint_time) <= 1) {
cleanup();
exit(0);
2026-01-28 19:34:39 +01:00
} else {
2026-01-28 19:47:14 +01:00
sigint_count = 1;
first_sigint_time = current_time;
2026-01-28 19:34:39 +01:00
}
2026-01-28 19:47:14 +01:00
}
2026-01-28 19:34:39 +01:00
}
static void parse_session_arg(int argc, char *argv[]) {
2026-01-28 19:47:14 +01:00
r_config_handle cfg = r_config_get_instance();
for (int i = 1; i < argc; i++) {
if (strncmp(argv[i], "--session=", 10) == 0) {
const char *name = argv[i] + 10;
if (!r_config_set_session_id(cfg, name)) {
fprintf(stderr, "Error: Invalid session name '%s'\n", name);
exit(1);
}
return;
}
if ((strcmp(argv[i], "-s") == 0 || strcmp(argv[i], "--session") == 0) &&
i + 1 < argc) {
const char *name = argv[++i];
if (!r_config_set_session_id(cfg, name)) {
fprintf(stderr, "Error: Invalid session name '%s'\n", name);
exit(1);
}
return;
2026-01-28 19:34:39 +01:00
}
2026-01-28 19:47:14 +01:00
}
2026-01-28 19:34:39 +01:00
}
int main(int argc, char *argv[]) {
2026-01-28 19:47:14 +01:00
signal(SIGINT, handle_sigint);
atexit(cleanup);
parse_session_arg(argc, argv);
init();
char *env_string = get_env_string();
if (env_string && *env_string && global_messages) {
messages_add(global_messages, "system", env_string);
free(env_string);
}
messages_load(global_messages);
if (try_prompt(argc, argv)) {
2026-01-28 19:34:39 +01:00
return 0;
2026-01-28 19:47:14 +01:00
}
repl();
return 0;
2026-01-28 19:34:39 +01:00
}