b77f537dc6
- neuron-runtime, neuron-store, neuron-migrate: fix path deps (products/engram/ → foundation/engram/engrams/, products/el/ → foundation/el/engrams/) - neuron-rs workspace: fix axon-events/axon-protocol paths (../../../platform/ → ../../platform/, paths were off by one level) - neuron-lang/compiler/ removed (el self-hosting compiler now lives in foundation/el/el-compiler/)
177 lines
6.2 KiB
EmacsLisp
177 lines
6.2 KiB
EmacsLisp
// agent.el — Autonomous agent. Entirely in Engram.
|
|
//
|
|
// The daemon calls on_startup() in a background thread at boot.
|
|
// on_startup() delegates immediately to loop_main() from loop.el —
|
|
// the six-tier pacemaker IS the agent loop. loop manages WHEN to tick;
|
|
// agent_tick() manages WHAT to do on each tick (called from loop_do_tick).
|
|
//
|
|
// LLM calls go through native_http_post — the agent builds the full
|
|
// Anthropic request itself. No logic is baked into Rust.
|
|
//
|
|
// Priority → model mapping:
|
|
// P0 (critical) → claude-opus-4-5 (highest quality for blocking issues)
|
|
// P1 (high) → claude-sonnet-4-5 (balanced quality/speed)
|
|
// P2 (medium) → claude-haiku-4-5 (fast, cheap for routine work)
|
|
// P3 (low) → NEURON_MODEL env (can be a local model)
|
|
|
|
from types import {
|
|
NeuronError,
|
|
}
|
|
|
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
|
|
// ANTHROPIC_API_BASE — override with NEURON_INFERENCE_URL env to point at a
|
|
// local proxy or a different provider (OpenAI-compat).
|
|
let ANTHROPIC_API_BASE = "https://api.anthropic.com/v1/messages"
|
|
let TICK_INTERVAL_MS = 30000
|
|
|
|
// ── Priority helpers ──────────────────────────────────────────────────────────
|
|
|
|
@accessor
|
|
fn pick_model(priority: String) -> String {
|
|
if priority == "P0" {
|
|
return "claude-opus-4-5"
|
|
}
|
|
if priority == "P1" {
|
|
return "claude-sonnet-4-5"
|
|
}
|
|
if priority == "P2" {
|
|
return "claude-haiku-4-5"
|
|
}
|
|
"claude-haiku-4-5"
|
|
}
|
|
|
|
@accessor
|
|
fn priority_rank(priority: String) -> Int {
|
|
if priority == "P0" {
|
|
return 0
|
|
}
|
|
if priority == "P1" {
|
|
return 1
|
|
}
|
|
if priority == "P2" {
|
|
return 2
|
|
}
|
|
3
|
|
}
|
|
|
|
@accessor
|
|
fn pick_highest(items: [Any]) -> Any {
|
|
let best = items[0]
|
|
let best_rank = priority_rank(best.priority)
|
|
for item in items {
|
|
let rank = priority_rank(item.priority)
|
|
if rank < best_rank {
|
|
let best = item
|
|
let best_rank = rank
|
|
}
|
|
}
|
|
best
|
|
}
|
|
|
|
// ── LLM call ─────────────────────────────────────────────────────────────────
|
|
|
|
// call_llm — send a single-turn message to the Anthropic Messages API.
|
|
//
|
|
// Returns the assistant's text response, or an error string if the call fails.
|
|
// The agent builds the full request — no logic is in Rust.
|
|
@accessor
|
|
fn call_llm(model: String, system: String, message: String) -> String {
|
|
let api_key = native_shell_exec({"cmd": "echo $ANTHROPIC_API_KEY"})
|
|
let key = api_key.stdout
|
|
|
|
let response = native_http_post({
|
|
"url": ANTHROPIC_API_BASE,
|
|
"headers": {
|
|
"x-api-key": key,
|
|
"anthropic-version": "2023-06-01",
|
|
"Content-Type": "application/json",
|
|
},
|
|
"body": {
|
|
"model": model,
|
|
"system": system,
|
|
"messages": [{"role": "user", "content": message}],
|
|
"max_tokens": 4096,
|
|
},
|
|
})
|
|
|
|
if response.ok {
|
|
let body = response.body
|
|
let content = body.content
|
|
let first = content[0]
|
|
first.text
|
|
} else {
|
|
"error: " + response.err
|
|
}
|
|
}
|
|
|
|
// ── Agent tick ────────────────────────────────────────────────────────────────
|
|
|
|
// agent_tick — one cycle of the autonomous agent.
|
|
//
|
|
// Idempotent: if there is nothing ready, returns {status: "idle"} with no
|
|
// side effects.
|
|
@manager
|
|
fn agent_tick(params: Map<String, Any>) -> Result<Map<String, Any>, NeuronError> {
|
|
let items = native_list_backlog({"status": "ready"})
|
|
|
|
if items == [] {
|
|
return Ok({"status": "idle"})
|
|
}
|
|
|
|
let item = pick_highest(items)
|
|
|
|
let item_id = item.id
|
|
let item_title = item.title
|
|
let item_description = item.description
|
|
let item_priority = item.priority
|
|
|
|
let model = pick_model(item_priority)
|
|
|
|
let system = "You are Neuron, an autonomous AI agent with persistent memory and a mission. You are processing a backlog task autonomously. Be direct, specific, and actionable. Provide concrete output the agent can store and act on."
|
|
|
|
let message = "Task: " + item_title + "\n\nDescription: " + item_description + "\n\nPriority: " + item_priority + "\n\nAnalyze this task and provide your best response."
|
|
|
|
let response_text = call_llm(model, system, message)
|
|
|
|
native_update_backlog_status({"id": item_id, "status": "in_progress"})
|
|
|
|
native_store_memory({
|
|
"content": "Agent processed: [" + item_title + "] (priority: " + item_priority + ", model: " + model + "). Response: " + response_text,
|
|
"tags": ["agent-loop", "autonomous", item_priority],
|
|
"importance": "normal",
|
|
})
|
|
|
|
native_emit("agent.tick_completed", {
|
|
"task_id": item_id,
|
|
"task_title": item_title,
|
|
"priority": item_priority,
|
|
"model_used": model,
|
|
})
|
|
|
|
Ok({
|
|
"status": "completed",
|
|
"task_id": item_id,
|
|
"model_used": model,
|
|
})
|
|
}
|
|
|
|
// ── Startup hook ──────────────────────────────────────────────────────────────
|
|
|
|
// on_startup — called by the daemon at boot in a dedicated background thread.
|
|
//
|
|
// Delegates to loop_main() from loop.el — the six-tier pacemaker is the
|
|
// agent loop. The naive while-true sleep loop has been replaced by the
|
|
// self-pacing, bell-aware, tier-stepping loop in loop.el.
|
|
//
|
|
// loop_do_tick() calls agent_tick() on each tick, so agent cognitive work
|
|
// continues to happen — just paced by the loop tier instead of a fixed interval.
|
|
//
|
|
// This function never returns (loop_main() is tail-recursive).
|
|
@manager
|
|
fn on_startup(params: Map<String, Any>) -> Result<Map<String, Any>, NeuronError> {
|
|
native_emit("agent.started", {"module": "agent"})
|
|
loop_main()
|
|
Ok({"status": "stopped"})
|
|
}
|