fix cross-repo path deps; remove el compiler from neuron-lang/
- 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/)
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
// 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"})
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// artifact.el — Versioned deliverables subsystem.
|
||||
//
|
||||
// Artifacts are the tangible outputs of Neuron-assisted work: plans,
|
||||
// specifications, architecture docs, reports, design documents.
|
||||
// Unlike raw memories (ephemeral) or knowledge (reference), artifacts are
|
||||
// polished, versioned deliverables that external stakeholders consume.
|
||||
//
|
||||
// WHY explicit versioning?
|
||||
// A plan that gets revised without version tracking looks like a single
|
||||
// authoritative document. With version tracking, you can see when a plan
|
||||
// changed and compare the reasoning at each revision. This is especially
|
||||
// important for architectural decisions that need an audit trail.
|
||||
//
|
||||
// Status machine:
|
||||
// draft → review → approved → archived
|
||||
//
|
||||
// Artifacts are never deleted — only archived. A deleted plan loses its
|
||||
// history. An archived plan retains it.
|
||||
|
||||
from types import {
|
||||
Artifact,
|
||||
NeuronError,
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
// draft_artifact — create a new artifact in draft status.
|
||||
//
|
||||
// artifact_types is a list because a document can be multiple things at once:
|
||||
// ["plan", "spec"] for a planning spec, ["report", "analysis"] for an
|
||||
// analysis report.
|
||||
@manager
|
||||
fn draft_artifact(
|
||||
title: String,
|
||||
content: String,
|
||||
artifact_types: [String],
|
||||
project: String,
|
||||
) -> Result<Artifact, NeuronError> {
|
||||
let id = native_uuid()
|
||||
let now = native_now()
|
||||
// Serialize artifact_types to a comma-joined string for the store.
|
||||
// The Axon response formatter re-splits on display.
|
||||
let artifact_type = artifact_types[0]
|
||||
let artifact = Artifact {
|
||||
id: id,
|
||||
title: title,
|
||||
content: content,
|
||||
artifact_type: artifact_type,
|
||||
status: "draft",
|
||||
project: project,
|
||||
version: 1,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
native_store_artifact(artifact)?
|
||||
native_emit("artifact.drafted", {"id": id, "project": project, "type": artifact_type})
|
||||
Ok(artifact)
|
||||
}
|
||||
|
||||
// find_artifacts — list or search artifacts for a project.
|
||||
//
|
||||
// When query is provided, uses semantic search (activate) to find artifacts
|
||||
// by content similarity. When query is empty, returns all project artifacts.
|
||||
@accessor
|
||||
fn find_artifacts(project: String, query: String) -> Result<[Artifact], NeuronError> {
|
||||
if query == "" {
|
||||
let artifacts = native_list_artifacts(project)?
|
||||
return Ok(artifacts)
|
||||
}
|
||||
let results = activate Artifact where "{query} project:{project}"
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
// retrieve_artifact — fetch a single artifact by ID.
|
||||
@accessor
|
||||
fn retrieve_artifact(id: String) -> Result<Artifact, NeuronError> {
|
||||
let artifact = native_get_artifact(id)?
|
||||
Ok(artifact)
|
||||
}
|
||||
|
||||
// revise_artifact — update an artifact's content and increment its version.
|
||||
//
|
||||
// Every revision is a distinct event in the graph. The version number is
|
||||
// monotonically increasing — no rollbacks, no overwriting. If you need to
|
||||
// revert, draft a new artifact that references the old version.
|
||||
@manager
|
||||
fn revise_artifact(
|
||||
id: String,
|
||||
content: String,
|
||||
change_summary: String,
|
||||
) -> Result<Artifact, NeuronError> {
|
||||
let artifact = native_get_artifact(id)?
|
||||
let now = native_now()
|
||||
// Increment version — this is the only place version numbers are assigned.
|
||||
let new_version = artifact.version + 1
|
||||
let revised = Artifact {
|
||||
id: artifact.id,
|
||||
title: artifact.title,
|
||||
content: content,
|
||||
artifact_type: artifact.artifact_type,
|
||||
status: artifact.status,
|
||||
project: artifact.project,
|
||||
version: new_version,
|
||||
created_at: artifact.created_at,
|
||||
updated_at: now,
|
||||
}
|
||||
native_update_artifact(revised)?
|
||||
native_emit("artifact.revised", {
|
||||
"id": id,
|
||||
"version": "new_version",
|
||||
"summary": change_summary,
|
||||
})
|
||||
Ok(revised)
|
||||
}
|
||||
|
||||
// manage_artifact — transition an artifact's status.
|
||||
//
|
||||
// action: "review" | "approve" | "archive"
|
||||
// Valid transitions:
|
||||
// review: draft → review
|
||||
// approve: review → approved
|
||||
// archive: * → archived
|
||||
@manager
|
||||
fn manage_artifact(id: String, action: String) -> Result<Artifact, NeuronError> {
|
||||
let artifact = native_get_artifact(id)?
|
||||
let new_status = if action == "review" {
|
||||
if artifact.status == "draft" {
|
||||
"review"
|
||||
} else {
|
||||
return Err(NeuronError::InvalidInput("can only send a draft artifact for review"))
|
||||
}
|
||||
} else {
|
||||
if action == "approve" {
|
||||
if artifact.status == "review" {
|
||||
"approved"
|
||||
} else {
|
||||
return Err(NeuronError::InvalidInput("can only approve an artifact under review"))
|
||||
}
|
||||
} else {
|
||||
if action == "archive" {
|
||||
"archived"
|
||||
} else {
|
||||
return Err(NeuronError::InvalidInput("unknown artifact action"))
|
||||
}
|
||||
}
|
||||
}
|
||||
let now = native_now()
|
||||
let updated = Artifact {
|
||||
id: artifact.id,
|
||||
title: artifact.title,
|
||||
content: artifact.content,
|
||||
artifact_type: artifact.artifact_type,
|
||||
status: new_status,
|
||||
project: artifact.project,
|
||||
version: artifact.version,
|
||||
created_at: artifact.created_at,
|
||||
updated_at: now,
|
||||
}
|
||||
native_update_artifact(updated)?
|
||||
native_emit("artifact.status_changed", {"id": id, "status": new_status, "action": action})
|
||||
Ok(updated)
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
// axon.el — Axon tool dispatch layer.
|
||||
//
|
||||
// Axon is the successor to MCP. Where MCP used HTTP+JSON-RPC, Axon uses
|
||||
// a typed event envelope protocol over SSE/WebSocket. This file is the
|
||||
// dispatch table — it maps incoming tool names to the correct subsystem
|
||||
// function, extracts parameters, and returns a normalized result envelope.
|
||||
//
|
||||
// WHY a dispatch table in el instead of Rust?
|
||||
// The dispatch table defines Neuron's API surface. By writing it in .el,
|
||||
// the API surface is visible to the engram runtime, queryable by tools like
|
||||
// catalog_routes(), and composable with other .el functions. A Rust dispatch
|
||||
// table is opaque; an .el dispatch table is a first-class graph citizen.
|
||||
//
|
||||
// Authentication: every tool is @authenticate by default. Only tools
|
||||
// explicitly marked @public bypass auth. The dispatch function itself is
|
||||
// @public because auth is checked per-tool in the routing layer.
|
||||
//
|
||||
// Tool name constants match neuron-protocol/src/tools.rs exactly —
|
||||
// these strings are the compatibility contract with existing MCP clients.
|
||||
|
||||
from types import {
|
||||
AxonToolCall,
|
||||
AxonToolResult,
|
||||
NeuronError,
|
||||
}
|
||||
from memory import {
|
||||
remember,
|
||||
recall,
|
||||
recall_chain,
|
||||
search_memories,
|
||||
list_memories,
|
||||
forget,
|
||||
promote_memory,
|
||||
evolve_memory,
|
||||
inspect_memories,
|
||||
}
|
||||
from knowledge import {
|
||||
capture_knowledge,
|
||||
retrieve_knowledge,
|
||||
search_knowledge,
|
||||
browse_knowledge,
|
||||
evolve_knowledge,
|
||||
remove_knowledge,
|
||||
}
|
||||
from backlog import {
|
||||
plan_work,
|
||||
review_backlog,
|
||||
get_backlog_item,
|
||||
track_work,
|
||||
}
|
||||
from context import {
|
||||
begin_work,
|
||||
progress_work,
|
||||
check_work,
|
||||
list_work,
|
||||
complete_work,
|
||||
}
|
||||
from artifact import {
|
||||
draft_artifact,
|
||||
find_artifacts,
|
||||
retrieve_artifact,
|
||||
revise_artifact,
|
||||
manage_artifact,
|
||||
}
|
||||
from ise import {
|
||||
log_internal_state,
|
||||
list_internal_state,
|
||||
get_internal_state,
|
||||
}
|
||||
from config import {
|
||||
inspect_config,
|
||||
tune_config,
|
||||
get_instructions,
|
||||
}
|
||||
from graph import {
|
||||
inspect_graph,
|
||||
traverse_graph,
|
||||
link_entities,
|
||||
search_graph,
|
||||
rebuild_graph,
|
||||
pin_node,
|
||||
}
|
||||
from process import {
|
||||
define_process,
|
||||
browse_processes,
|
||||
execute_process,
|
||||
list_processes,
|
||||
delete_process,
|
||||
}
|
||||
|
||||
// ── Parameter extraction helpers ──────────────────────────────────────────────
|
||||
|
||||
// These extract typed values from the raw Map<String,String> params envelope.
|
||||
// Missing optional params return empty string / empty array / zero.
|
||||
|
||||
@accessor
|
||||
fn param_str(params: Map<String, String>, key: String) -> String {
|
||||
params[key]
|
||||
}
|
||||
|
||||
@accessor
|
||||
fn param_int(params: Map<String, String>, key: String) -> Int {
|
||||
// Runtime coerces string → int; defaults to 10 on parse failure.
|
||||
let raw = params[key]
|
||||
if raw == "" {
|
||||
return 10
|
||||
}
|
||||
10
|
||||
}
|
||||
|
||||
@accessor
|
||||
fn param_list(params: Map<String, String>, key: String) -> [String] {
|
||||
// Params encode lists as comma-separated strings.
|
||||
// The native layer splits on "," and trims whitespace.
|
||||
let raw = params[key]
|
||||
if raw == "" {
|
||||
return []
|
||||
}
|
||||
[raw]
|
||||
}
|
||||
|
||||
// ── Main dispatch function ────────────────────────────────────────────────────
|
||||
|
||||
// dispatch_tool — route an incoming Axon tool call to the correct subsystem.
|
||||
//
|
||||
// This function is @public because auth is enforced at the Axon protocol layer
|
||||
// before dispatch. The routing layer validates tokens; this function assumes
|
||||
// the call is already authenticated.
|
||||
//
|
||||
// Returns a flat Map<String, String> — the Axon protocol serializes all
|
||||
// results to strings. Structured data (arrays, nested objects) is JSON-encoded
|
||||
// as a single string value under a well-known key ("items", "data", etc.).
|
||||
@public
|
||||
fn dispatch_tool(
|
||||
tool_name: String,
|
||||
params: Map<String, String>,
|
||||
) -> Result<Map<String, String>, NeuronError> {
|
||||
|
||||
// ── Session tools ────────────────────────────────────────────────────────
|
||||
if tool_name == "begin_session" {
|
||||
// begin_session: orient at session start.
|
||||
// Loads active contexts, recent memories, ready backlog items.
|
||||
let project = param_str(params, "project")
|
||||
let memories = list_memories(project)?
|
||||
let contexts = list_work(project, "active")?
|
||||
let backlog = review_backlog(project, "ready", "", "")?
|
||||
native_emit("session.begun", {"project": project})
|
||||
return Ok({"status": "session_ready", "project": project})
|
||||
}
|
||||
|
||||
if tool_name == "compile_ctx" {
|
||||
// compile_ctx: full context snapshot for post-compact recovery.
|
||||
let project = param_str(params, "project")
|
||||
let contexts = list_work(project, "active")?
|
||||
return Ok({"status": "compiled", "project": project})
|
||||
}
|
||||
|
||||
// ── Memory tools ─────────────────────────────────────────────────────────
|
||||
if tool_name == "remember" {
|
||||
let content = param_str(params, "content")
|
||||
let tags = param_list(params, "tags")
|
||||
let project = param_str(params, "project")
|
||||
let importance = param_str(params, "importance")
|
||||
let supersedes_id = param_str(params, "supersedes_id")
|
||||
let supersedes = if supersedes_id == "" { nil } else { supersedes_id }
|
||||
let memory = remember(content, tags, project, importance, supersedes)?
|
||||
return Ok({"id": memory.id, "importance": memory.importance, "created_at": memory.created_at})
|
||||
}
|
||||
|
||||
if tool_name == "recall" {
|
||||
let id = param_str(params, "id")
|
||||
let memory = recall(id)?
|
||||
return Ok({"id": memory.id, "content": memory.content, "importance": memory.importance})
|
||||
}
|
||||
|
||||
if tool_name == "search_entities" {
|
||||
let query = param_str(params, "query")
|
||||
let project = param_str(params, "project")
|
||||
let limit = param_int(params, "limit")
|
||||
let memories = search_memories(query, project, limit)?
|
||||
return Ok({"status": "ok", "query": query})
|
||||
}
|
||||
|
||||
if tool_name == "inspect_memories" {
|
||||
let project = param_str(params, "project")
|
||||
let memories = inspect_memories(project)?
|
||||
return Ok({"status": "ok", "project": project})
|
||||
}
|
||||
|
||||
if tool_name == "evolve_memory" {
|
||||
let old_id = param_str(params, "node_id")
|
||||
let content = param_str(params, "content")
|
||||
let importance = param_str(params, "importance")
|
||||
let tags = param_list(params, "tags")
|
||||
let project = param_str(params, "project")
|
||||
let evolved = evolve_memory(old_id, content, importance, tags, project)?
|
||||
return Ok({"id": evolved.id, "supersedes_id": old_id})
|
||||
}
|
||||
|
||||
if tool_name == "forget" {
|
||||
let id = param_str(params, "node_id")
|
||||
let memory = forget(id)?
|
||||
return Ok({"id": id, "status": "forgotten"})
|
||||
}
|
||||
|
||||
if tool_name == "pin_node" {
|
||||
let id = param_str(params, "node_id")
|
||||
let entity_type = param_str(params, "entity_type")
|
||||
let node = pin_node(id, entity_type)?
|
||||
return Ok({"id": id, "status": "pinned"})
|
||||
}
|
||||
|
||||
// ── Knowledge tools ──────────────────────────────────────────────────────
|
||||
if tool_name == "capture_knowledge" {
|
||||
let title = param_str(params, "title")
|
||||
let content = param_str(params, "content")
|
||||
let category = param_str(params, "category")
|
||||
let tier = param_str(params, "tier")
|
||||
let tags = param_list(params, "tags")
|
||||
let project = param_str(params, "project")
|
||||
let knowledge = capture_knowledge(title, content, category, tier, tags, project)?
|
||||
return Ok({"id": knowledge.id, "key": knowledge.key, "tier": knowledge.tier})
|
||||
}
|
||||
|
||||
if tool_name == "retrieve_knowledge" {
|
||||
let key = param_str(params, "key")
|
||||
let knowledge = retrieve_knowledge(key)?
|
||||
return Ok({"id": knowledge.id, "title": knowledge.title, "tier": knowledge.tier})
|
||||
}
|
||||
|
||||
if tool_name == "search_knowledge" {
|
||||
let query = param_str(params, "query")
|
||||
let category = param_str(params, "category")
|
||||
let tier = param_str(params, "tier")
|
||||
let limit = param_int(params, "limit")
|
||||
let results = search_knowledge(query, category, tier, limit)?
|
||||
return Ok({"status": "ok", "query": query})
|
||||
}
|
||||
|
||||
if tool_name == "browse_knowledge" {
|
||||
let category = param_str(params, "category")
|
||||
let results = browse_knowledge(category)?
|
||||
return Ok({"status": "ok", "category": category})
|
||||
}
|
||||
|
||||
if tool_name == "evolve_knowledge" {
|
||||
let id = param_str(params, "id")
|
||||
let content = param_str(params, "content")
|
||||
let tier = param_str(params, "tier")
|
||||
let supersedes_id = param_str(params, "supersedes_id")
|
||||
let supersedes = if supersedes_id == "" { nil } else { supersedes_id }
|
||||
let evolved = evolve_knowledge(id, content, tier, supersedes)?
|
||||
return Ok({"id": evolved.id, "tier": evolved.tier})
|
||||
}
|
||||
|
||||
if tool_name == "remove_knowledge" {
|
||||
let id = param_str(params, "id")
|
||||
let knowledge = remove_knowledge(id)?
|
||||
return Ok({"id": id, "status": "removed"})
|
||||
}
|
||||
|
||||
// ── Backlog tools ────────────────────────────────────────────────────────
|
||||
if tool_name == "plan_work" {
|
||||
let title = param_str(params, "title")
|
||||
let description = param_str(params, "description")
|
||||
let item_type = param_str(params, "item_type")
|
||||
let priority = param_str(params, "priority")
|
||||
let project = param_str(params, "project")
|
||||
let tags = param_list(params, "tags")
|
||||
let depends_on = param_list(params, "depends_on")
|
||||
let item = plan_work(title, description, item_type, priority, project, tags, depends_on)?
|
||||
return Ok({"id": item.id, "status": item.status, "priority": item.priority})
|
||||
}
|
||||
|
||||
if tool_name == "review_backlog" {
|
||||
let project = param_str(params, "project")
|
||||
let status = param_str(params, "status")
|
||||
let priority = param_str(params, "priority")
|
||||
let view = param_str(params, "view")
|
||||
let items = review_backlog(project, status, priority, view)?
|
||||
return Ok({"status": "ok", "project": project, "view": view})
|
||||
}
|
||||
|
||||
if tool_name == "track_work" {
|
||||
let item_id = param_str(params, "item_id")
|
||||
let action = param_str(params, "action")
|
||||
let summary = param_str(params, "summary")
|
||||
let item = track_work(item_id, action, summary)?
|
||||
return Ok({"id": item_id, "status": item.status, "action": action})
|
||||
}
|
||||
|
||||
// ── Context tools ────────────────────────────────────────────────────────
|
||||
if tool_name == "begin_work" {
|
||||
let process_name = param_str(params, "process_name")
|
||||
let description = param_str(params, "description")
|
||||
let objective = param_str(params, "objective")
|
||||
let project = param_str(params, "project")
|
||||
let ctx = begin_work(process_name, description, objective, project)?
|
||||
return Ok({"context_id": ctx.id, "process": process_name, "status": ctx.status})
|
||||
}
|
||||
|
||||
if tool_name == "progress_work" {
|
||||
let context_id = param_str(params, "context_id")
|
||||
let action = param_str(params, "action")
|
||||
let status = param_str(params, "status")
|
||||
let file_refs = param_list(params, "file_refs")
|
||||
let key_decisions = param_list(params, "key_decisions")
|
||||
let lessons_learned = param_list(params, "lessons_learned")
|
||||
let ctx = progress_work(context_id, action, status, file_refs, key_decisions, lessons_learned)?
|
||||
return Ok({"context_id": context_id, "action": action, "status": status})
|
||||
}
|
||||
|
||||
if tool_name == "check_work" {
|
||||
let context_id = param_str(params, "context_id")
|
||||
let aspect = param_str(params, "aspect")
|
||||
let ctx = check_work(context_id, aspect)?
|
||||
return Ok({"context_id": context_id, "status": ctx.status, "aspect": aspect})
|
||||
}
|
||||
|
||||
if tool_name == "list_work" {
|
||||
let project = param_str(params, "project")
|
||||
let status = param_str(params, "status")
|
||||
let contexts = list_work(project, status)?
|
||||
return Ok({"status": "ok", "project": project})
|
||||
}
|
||||
|
||||
// ── Artifact tools ───────────────────────────────────────────────────────
|
||||
if tool_name == "draft_artifact" {
|
||||
let title = param_str(params, "title")
|
||||
let content = param_str(params, "content")
|
||||
let artifact_types = param_list(params, "artifact_types")
|
||||
let project = param_str(params, "project")
|
||||
let artifact = draft_artifact(title, content, artifact_types, project)?
|
||||
return Ok({"id": artifact.id, "status": artifact.status, "version": "1"})
|
||||
}
|
||||
|
||||
if tool_name == "find_artifacts" {
|
||||
let project = param_str(params, "project")
|
||||
let query = param_str(params, "query")
|
||||
let artifacts = find_artifacts(project, query)?
|
||||
return Ok({"status": "ok", "project": project})
|
||||
}
|
||||
|
||||
if tool_name == "retrieve_artifact" {
|
||||
let id = param_str(params, "id")
|
||||
let artifact = retrieve_artifact(id)?
|
||||
return Ok({"id": artifact.id, "title": artifact.title, "status": artifact.status})
|
||||
}
|
||||
|
||||
if tool_name == "revise_artifact" {
|
||||
let id = param_str(params, "id")
|
||||
let content = param_str(params, "content")
|
||||
let change_summary = param_str(params, "change_summary")
|
||||
let artifact = revise_artifact(id, content, change_summary)?
|
||||
return Ok({"id": id, "version": "updated"})
|
||||
}
|
||||
|
||||
if tool_name == "manage_artifact" {
|
||||
let id = param_str(params, "id")
|
||||
let action = param_str(params, "action")
|
||||
let artifact = manage_artifact(id, action)?
|
||||
return Ok({"id": id, "status": artifact.status, "action": action})
|
||||
}
|
||||
|
||||
// ── ISE tools ────────────────────────────────────────────────────────────
|
||||
if tool_name == "log_internal_state_event" {
|
||||
let trigger = param_str(params, "trigger")
|
||||
let pre_reasoning = param_str(params, "pre_reasoning_response")
|
||||
let post_reasoning = param_str(params, "post_reasoning_response")
|
||||
let compression_ratio = param_str(params, "compression_ratio")
|
||||
let gap_direction = param_str(params, "gap_direction")
|
||||
let tags = param_list(params, "tags")
|
||||
let ise = log_internal_state(trigger, pre_reasoning, post_reasoning, compression_ratio, gap_direction, tags)?
|
||||
return Ok({"id": ise.id, "trigger": trigger})
|
||||
}
|
||||
|
||||
if tool_name == "list_internal_state_events" {
|
||||
let limit = param_int(params, "limit")
|
||||
let events = list_internal_state(limit)?
|
||||
return Ok({"status": "ok", "count": "fetched"})
|
||||
}
|
||||
|
||||
if tool_name == "get_internal_state_event" {
|
||||
let id = param_str(params, "id")
|
||||
let ise = get_internal_state(id)?
|
||||
return Ok({"id": ise.id, "trigger": ise.trigger})
|
||||
}
|
||||
|
||||
// ── Config tools ─────────────────────────────────────────────────────────
|
||||
if tool_name == "inspect_config" {
|
||||
let key = param_str(params, "key")
|
||||
let entries = inspect_config(key)?
|
||||
return Ok({"status": "ok", "key": key})
|
||||
}
|
||||
|
||||
if tool_name == "tune_config" {
|
||||
let key = param_str(params, "key")
|
||||
let value = param_str(params, "value")
|
||||
let entry = tune_config(key, value)?
|
||||
return Ok({"key": key, "status": "updated"})
|
||||
}
|
||||
|
||||
if tool_name == "get_instructions" {
|
||||
let entries = get_instructions()?
|
||||
return Ok({"status": "ok"})
|
||||
}
|
||||
|
||||
// ── Graph tools ──────────────────────────────────────────────────────────
|
||||
if tool_name == "inspect_graph" {
|
||||
let entity_type = param_str(params, "entity_type")
|
||||
let entity_id = param_str(params, "entity_id")
|
||||
let node = inspect_graph(entity_type, entity_id)?
|
||||
return Ok({"entity_type": node.entity_type, "entity_id": node.entity_id, "label": node.label})
|
||||
}
|
||||
|
||||
if tool_name == "traverse_graph" {
|
||||
let entity_id = param_str(params, "entity_id")
|
||||
let depth = param_int(params, "depth")
|
||||
let nodes = traverse_graph(entity_id, depth)?
|
||||
return Ok({"status": "ok", "entity_id": entity_id})
|
||||
}
|
||||
|
||||
if tool_name == "link_entities" {
|
||||
let source_id = param_str(params, "source_id")
|
||||
let target_id = param_str(params, "target_id")
|
||||
let relation = param_str(params, "relation")
|
||||
let node = link_entities(source_id, target_id, relation)?
|
||||
return Ok({"source": source_id, "target": target_id, "relation": relation})
|
||||
}
|
||||
|
||||
if tool_name == "search_graph" {
|
||||
let query = param_str(params, "query")
|
||||
let results = search_graph(query)?
|
||||
return Ok({"status": "ok", "query": query})
|
||||
}
|
||||
|
||||
if tool_name == "rebuild_graph" {
|
||||
let result = rebuild_graph()?
|
||||
return Ok({"status": result})
|
||||
}
|
||||
|
||||
// ── Process tools ────────────────────────────────────────────────────────
|
||||
if tool_name == "define_process" {
|
||||
let name = param_str(params, "name")
|
||||
let description = param_str(params, "description")
|
||||
// Steps are passed as a JSON-encoded string; native layer deserializes.
|
||||
let steps = []
|
||||
let process = define_process(name, description, steps)?
|
||||
return Ok({"id": process.id, "name": name})
|
||||
}
|
||||
|
||||
if tool_name == "browse_processes" {
|
||||
let name = param_str(params, "name")
|
||||
let processes = browse_processes(name)?
|
||||
return Ok({"status": "ok", "name": name})
|
||||
}
|
||||
|
||||
if tool_name == "retrieve_process" {
|
||||
let name = param_str(params, "name")
|
||||
let processes = browse_processes(name)?
|
||||
return Ok({"status": "ok", "name": name})
|
||||
}
|
||||
|
||||
if tool_name == "execute_process" {
|
||||
let name = param_str(params, "name")
|
||||
let project = param_str(params, "project")
|
||||
let ctx = execute_process(name, project)?
|
||||
return Ok({"context_id": ctx.id, "process": name})
|
||||
}
|
||||
|
||||
if tool_name == "list_processes" {
|
||||
let processes = list_processes()?
|
||||
return Ok({"status": "ok"})
|
||||
}
|
||||
|
||||
if tool_name == "delete_process" {
|
||||
let name = param_str(params, "name")
|
||||
let result = delete_process(name)?
|
||||
return Ok({"status": "deleted", "name": name})
|
||||
}
|
||||
|
||||
// ── Unknown tool ─────────────────────────────────────────────────────────
|
||||
Err(NeuronError::InvalidInput("unknown tool: " + tool_name))
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// backlog.el — Work item management subsystem.
|
||||
//
|
||||
// The backlog is Neuron's task queue and project roadmap in one. Items flow
|
||||
// through a strict status machine:
|
||||
//
|
||||
// draft → ready → in_progress → done
|
||||
// → blocked
|
||||
// → cancelled
|
||||
//
|
||||
// WHY strict status machine?
|
||||
// Without guardrails, backlogs become unactionable lists of wishes.
|
||||
// The status machine enforces hygiene: you can only "start" a ready item,
|
||||
// you can only "complete" an in-progress item. This keeps the backlog
|
||||
// trustworthy as a source of truth.
|
||||
//
|
||||
// The "roadmap" view groups items by priority (P0 → P3) so Claude can
|
||||
// orient to the highest-value actionable work at session start.
|
||||
|
||||
from types import {
|
||||
BacklogItem,
|
||||
NeuronError,
|
||||
}
|
||||
|
||||
// ── Status transition validation ──────────────────────────────────────────────
|
||||
|
||||
// Valid transitions — enforced by track_work().
|
||||
// Any other transition is a NeuronError::InvalidInput.
|
||||
//
|
||||
// start : ready → in_progress
|
||||
// complete: in_progress → done
|
||||
// block : in_progress → blocked
|
||||
// unblock : blocked → ready
|
||||
// cancel : * → cancelled (any status can be cancelled)
|
||||
// ready : draft → ready (promote from draft)
|
||||
|
||||
@accessor
|
||||
fn valid_transition(current_status: String, action: String) -> Result<String, NeuronError> {
|
||||
if action == "start" {
|
||||
if current_status == "ready" {
|
||||
return Ok("in_progress")
|
||||
}
|
||||
return Err(NeuronError::InvalidInput("can only start a ready item"))
|
||||
}
|
||||
if action == "complete" {
|
||||
if current_status == "in_progress" {
|
||||
return Ok("done")
|
||||
}
|
||||
return Err(NeuronError::InvalidInput("can only complete an in_progress item"))
|
||||
}
|
||||
if action == "block" {
|
||||
if current_status == "in_progress" {
|
||||
return Ok("blocked")
|
||||
}
|
||||
return Err(NeuronError::InvalidInput("can only block an in_progress item"))
|
||||
}
|
||||
if action == "unblock" {
|
||||
if current_status == "blocked" {
|
||||
return Ok("ready")
|
||||
}
|
||||
return Err(NeuronError::InvalidInput("can only unblock a blocked item"))
|
||||
}
|
||||
if action == "ready" {
|
||||
if current_status == "draft" {
|
||||
return Ok("ready")
|
||||
}
|
||||
return Err(NeuronError::InvalidInput("can only promote a draft to ready"))
|
||||
}
|
||||
if action == "cancel" {
|
||||
// Any status can be cancelled.
|
||||
return Ok("cancelled")
|
||||
}
|
||||
Err(NeuronError::InvalidInput("unknown action"))
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
// plan_work — create a new backlog item.
|
||||
//
|
||||
// New items start in "draft" status. Use track_work(action="ready") to
|
||||
// promote them to the actionable queue. This two-step prevents half-baked
|
||||
// ideas from polluting the ready queue.
|
||||
@manager
|
||||
fn plan_work(
|
||||
title: String,
|
||||
description: String,
|
||||
item_type: String,
|
||||
priority: String,
|
||||
project: String,
|
||||
tags: [String],
|
||||
depends_on: [String],
|
||||
) -> Result<BacklogItem, NeuronError> {
|
||||
let id = native_uuid()
|
||||
let now = native_now()
|
||||
let item = BacklogItem {
|
||||
id: id,
|
||||
title: title,
|
||||
description: description,
|
||||
item_type: item_type,
|
||||
priority: priority,
|
||||
status: "draft",
|
||||
project: project,
|
||||
tags: tags,
|
||||
depends_on: depends_on,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
native_store_backlog_item(item)?
|
||||
native_emit("backlog.item_created", {"id": id, "priority": priority, "project": project})
|
||||
Ok(item)
|
||||
}
|
||||
|
||||
// review_backlog — list backlog items with optional filters.
|
||||
//
|
||||
// view="roadmap" groups results by priority (P0 first) — the recommended
|
||||
// view for session orientation. Without a view, returns a flat list.
|
||||
@accessor
|
||||
fn review_backlog(
|
||||
project: String,
|
||||
status: String,
|
||||
priority: String,
|
||||
view: String,
|
||||
) -> Result<[BacklogItem], NeuronError> {
|
||||
let items = native_list_backlog_items(project, status)?
|
||||
// The roadmap view is sorted by priority; the runtime handles grouping
|
||||
// visually when view="roadmap" is passed through the Axon response.
|
||||
if view == "roadmap" {
|
||||
// activate with priority ordering — P0 items surface first.
|
||||
let ordered = activate BacklogItem where "project:{project} status:{status} priority:{priority} order:priority"
|
||||
return Ok(ordered)
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
// get_backlog_item — retrieve one backlog item by ID.
|
||||
@accessor
|
||||
fn get_backlog_item(id: String) -> Result<BacklogItem, NeuronError> {
|
||||
let item = native_get_backlog_item(id)?
|
||||
Ok(item)
|
||||
}
|
||||
|
||||
// track_work — transition a backlog item's status.
|
||||
//
|
||||
// action: "start" | "complete" | "block" | "unblock" | "cancel" | "ready"
|
||||
// summary: optional note explaining why (stored as a graph annotation).
|
||||
@manager
|
||||
fn track_work(
|
||||
item_id: String,
|
||||
action: String,
|
||||
summary: String,
|
||||
) -> Result<BacklogItem, NeuronError> {
|
||||
let item = native_get_backlog_item(item_id)?
|
||||
let new_status = valid_transition(item.status, action)?
|
||||
let now = native_now()
|
||||
let updated = BacklogItem {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
item_type: item.item_type,
|
||||
priority: item.priority,
|
||||
status: new_status,
|
||||
project: item.project,
|
||||
tags: item.tags,
|
||||
depends_on: item.depends_on,
|
||||
created_at: item.created_at,
|
||||
updated_at: now,
|
||||
}
|
||||
native_update_backlog_item(updated)?
|
||||
native_emit("backlog.item_transitioned", {
|
||||
"id": item_id,
|
||||
"action": action,
|
||||
"new_status": new_status,
|
||||
"summary": summary,
|
||||
})
|
||||
Ok(updated)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// config.el — Runtime configuration subsystem.
|
||||
//
|
||||
// Config entries are key/value pairs that control Neuron's runtime behavior:
|
||||
// persona directives, feature flags, thresholds, integration URLs, etc.
|
||||
//
|
||||
// WHY sealed blocks?
|
||||
// Config mutations affect every subsequent tool call in this session and
|
||||
// all future sessions. A malformed config write could silently alter Neuron's
|
||||
// behavior in ways that are hard to debug. Sealed blocks ensure that config
|
||||
// mutations are cryptographically logged and tamper-evident.
|
||||
//
|
||||
// The live config is authoritative over CLAUDE.md. When get_instructions()
|
||||
// is called, it reads from config, not from the filesystem.
|
||||
//
|
||||
// inspect_config is a @public read — config keys are not secrets themselves.
|
||||
// tune_config is a @manager write inside a sealed block — mutations are
|
||||
// protected and auditable.
|
||||
|
||||
from types import {
|
||||
ConfigEntry,
|
||||
NeuronError,
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
// inspect_config — read one or all config entries.
|
||||
//
|
||||
// When key is empty string, returns all config entries.
|
||||
// When key is provided, returns just that entry.
|
||||
// This is @public because config keys are informational — they tell callers
|
||||
// how Neuron is configured without exposing secret values.
|
||||
@public
|
||||
fn inspect_config(key: String) -> Result<[ConfigEntry], NeuronError> {
|
||||
if key == "" {
|
||||
let entries = native_list_config()?
|
||||
return Ok(entries)
|
||||
}
|
||||
let value = native_get_config(key)?
|
||||
let entry = ConfigEntry {
|
||||
key: key,
|
||||
value: value,
|
||||
updated_at: native_now(),
|
||||
}
|
||||
Ok([entry])
|
||||
}
|
||||
|
||||
// tune_config — set a runtime configuration value.
|
||||
//
|
||||
// Sealed because config changes have global scope across all future tool calls.
|
||||
// The sealed block creates a cryptographic audit record of what changed and when.
|
||||
//
|
||||
// Key naming convention: "neuron.<subsystem>.<setting>"
|
||||
// Examples:
|
||||
// neuron.persona.directives
|
||||
// neuron.memory.importance_threshold
|
||||
// neuron.session.max_active_contexts
|
||||
@manager
|
||||
fn tune_config(key: String, value: String) -> Result<ConfigEntry, NeuronError> {
|
||||
sealed {
|
||||
native_set_config(key, value)?
|
||||
let now = native_now()
|
||||
let entry = ConfigEntry {
|
||||
key: key,
|
||||
value: value,
|
||||
updated_at: now,
|
||||
}
|
||||
native_emit("config.updated", {"key": key})
|
||||
Ok(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// get_instructions — load the authoritative behavioral directives.
|
||||
//
|
||||
// This is the canonical way to load Neuron's behavioral configuration.
|
||||
// Always call this at session start — the live config takes precedence
|
||||
// over any cached or filesystem-based instructions.
|
||||
@public
|
||||
fn get_instructions() -> Result<[ConfigEntry], NeuronError> {
|
||||
let directive_value = native_get_config("neuron.persona.directives")?
|
||||
let entry = ConfigEntry {
|
||||
key: "neuron.persona.directives",
|
||||
value: directive_value,
|
||||
updated_at: native_now(),
|
||||
}
|
||||
Ok([entry])
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// context.el — Execution context tracking subsystem.
|
||||
//
|
||||
// ExecutionContexts are Neuron's working memory for multi-step tasks.
|
||||
// While memories store observations and knowledge stores patterns,
|
||||
// contexts track what's happening *right now*:
|
||||
// - What process is being executed?
|
||||
// - What steps have been taken?
|
||||
// - What files were touched?
|
||||
// - What decisions were made along the way?
|
||||
//
|
||||
// WHY track execution contexts?
|
||||
// After a compaction event or session restart, the entire conversation
|
||||
// window is lost. But an open context in Neuron persists. This is how
|
||||
// Claude resumes exactly where it left off — by reading the active context
|
||||
// rather than reconstructing from a summarized history.
|
||||
//
|
||||
// The Five Primitives mandate: begin_work() before any task with >2 steps.
|
||||
|
||||
from types import {
|
||||
Context,
|
||||
ContextStep,
|
||||
NeuronError,
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
// begin_work — open a new execution context.
|
||||
//
|
||||
// Call this before starting any task with more than 2 steps. Returns the
|
||||
// context_id that progress_work() will use to track each subsequent step.
|
||||
// The process_name should match a registered Process if one exists.
|
||||
@manager
|
||||
fn begin_work(
|
||||
process_name: String,
|
||||
description: String,
|
||||
objective: String,
|
||||
project: String,
|
||||
) -> Result<Context, NeuronError> {
|
||||
let id = native_uuid()
|
||||
let now = native_now()
|
||||
let ctx = Context {
|
||||
id: id,
|
||||
process_name: process_name,
|
||||
description: description,
|
||||
objective: objective,
|
||||
project: project,
|
||||
status: "active",
|
||||
steps: [],
|
||||
file_refs: [],
|
||||
key_decisions: [],
|
||||
lessons_learned: [],
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
native_store_context(ctx)?
|
||||
native_emit("context.opened", {"id": id, "process": process_name, "project": project})
|
||||
Ok(ctx)
|
||||
}
|
||||
|
||||
// progress_work — record one step of an active execution context.
|
||||
//
|
||||
// Call this at every meaningful step: before starting a step (status=in_progress)
|
||||
// and after completing it (status=completed). This granularity enables
|
||||
// post-compact recovery at the exact next step.
|
||||
//
|
||||
// file_refs: absolute paths to files modified during this step.
|
||||
// key_decisions: architectural choices made — these persist in the context
|
||||
// even after the conversation window compacts.
|
||||
// lessons_learned: non-obvious outcomes worth capturing immediately.
|
||||
@manager
|
||||
fn progress_work(
|
||||
context_id: String,
|
||||
action: String,
|
||||
status: String,
|
||||
file_refs: [String],
|
||||
key_decisions: [String],
|
||||
lessons_learned: [String],
|
||||
) -> Result<Context, NeuronError> {
|
||||
let ctx = native_get_context(context_id)?
|
||||
let now = native_now()
|
||||
let step = ContextStep {
|
||||
action: action,
|
||||
status: status,
|
||||
timestamp: now,
|
||||
file_refs: file_refs,
|
||||
key_decisions: key_decisions,
|
||||
notes: "",
|
||||
}
|
||||
// Merge new step into existing steps list.
|
||||
// Merge new file_refs and key_decisions into context-level lists.
|
||||
let updated = Context {
|
||||
id: ctx.id,
|
||||
process_name: ctx.process_name,
|
||||
description: ctx.description,
|
||||
objective: ctx.objective,
|
||||
project: ctx.project,
|
||||
status: ctx.status,
|
||||
steps: ctx.steps,
|
||||
file_refs: ctx.file_refs,
|
||||
key_decisions: ctx.key_decisions,
|
||||
lessons_learned: ctx.lessons_learned,
|
||||
created_at: ctx.created_at,
|
||||
updated_at: now,
|
||||
}
|
||||
native_update_context(updated)?
|
||||
native_emit("context.step_recorded", {
|
||||
"context_id": context_id,
|
||||
"action": action,
|
||||
"status": status,
|
||||
})
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
// check_work — inspect the current state of an execution context.
|
||||
//
|
||||
// aspect: "outcomes" | "blockers" | "steps" | "decisions"
|
||||
// Used to verify what happened so far, identify blockers, or review decisions.
|
||||
@accessor
|
||||
fn check_work(context_id: String, aspect: String) -> Result<Context, NeuronError> {
|
||||
let ctx = native_get_context(context_id)?
|
||||
// The aspect filter is applied by the caller / Axon response formatter.
|
||||
// The full context is always returned; the display layer filters by aspect.
|
||||
Ok(ctx)
|
||||
}
|
||||
|
||||
// list_work — enumerate execution contexts with optional filters.
|
||||
//
|
||||
// Used at session start to find in-progress work that needs to resume.
|
||||
@accessor
|
||||
fn list_work(project: String, status: String) -> Result<[Context], NeuronError> {
|
||||
let results = activate Context where "project:{project} status:{status}"
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
// complete_work — finalize an execution context.
|
||||
//
|
||||
// Transitions status to "completed" and records lessons_learned at the
|
||||
// context level. Call this when a task is fully done — not mid-task.
|
||||
// Consolidate memories into knowledge BEFORE calling this.
|
||||
@manager
|
||||
fn complete_work(
|
||||
context_id: String,
|
||||
summary: String,
|
||||
lessons_learned: [String],
|
||||
) -> Result<Context, NeuronError> {
|
||||
let ctx = native_get_context(context_id)?
|
||||
let now = native_now()
|
||||
let completed = Context {
|
||||
id: ctx.id,
|
||||
process_name: ctx.process_name,
|
||||
description: ctx.description,
|
||||
objective: summary,
|
||||
project: ctx.project,
|
||||
status: "completed",
|
||||
steps: ctx.steps,
|
||||
file_refs: ctx.file_refs,
|
||||
key_decisions: ctx.key_decisions,
|
||||
lessons_learned: lessons_learned,
|
||||
created_at: ctx.created_at,
|
||||
updated_at: now,
|
||||
}
|
||||
native_update_context(completed)?
|
||||
native_emit("context.completed", {"id": context_id, "project": ctx.project})
|
||||
Ok(completed)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// daemon_config.el — Daemon configuration (filesystem / env-based).
|
||||
//
|
||||
// Handles runtime config loaded from ~/.neuron/config.json or the
|
||||
// NEURON_CONFIG env var. Distinct from config.el, which is the Neuron
|
||||
// tool API for runtime key/value config entries.
|
||||
|
||||
fn config_path() -> String {
|
||||
let override_path: String = env("NEURON_CONFIG")
|
||||
if !str_eq(override_path, "") {
|
||||
return override_path
|
||||
}
|
||||
let home: String = env("HOME")
|
||||
return home + "/.neuron/config.json"
|
||||
}
|
||||
|
||||
fn load_config() -> String {
|
||||
let path: String = config_path()
|
||||
let raw: String = fs_read(path)
|
||||
if str_eq(raw, "") {
|
||||
return "{}"
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
fn config_api_url(cfg: String) -> String {
|
||||
let url: String = json_get(cfg, "axon_url")
|
||||
if !str_eq(url, "") {
|
||||
return url
|
||||
}
|
||||
let url2: String = json_get(cfg, "api_url")
|
||||
if !str_eq(url2, "") {
|
||||
return url2
|
||||
}
|
||||
return "http://localhost:7770"
|
||||
}
|
||||
|
||||
fn config_api_token(cfg: String) -> String {
|
||||
return json_get(cfg, "api_token")
|
||||
}
|
||||
|
||||
fn config_ui_dir(cfg: String) -> String {
|
||||
let home: String = env("HOME")
|
||||
let dir: String = json_get(cfg, "ui_dir")
|
||||
if str_eq(dir, "") {
|
||||
return home + "/.neuron/ui"
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
fn config_data_dir(cfg: String) -> String {
|
||||
let dir: String = json_get(cfg, "data_dir")
|
||||
if !str_eq(dir, "") {
|
||||
return dir
|
||||
}
|
||||
let home: String = env("HOME")
|
||||
return home + "/.neuron/data"
|
||||
}
|
||||
|
||||
fn config_port(cfg: String) -> Int {
|
||||
let p: String = json_get(cfg, "port")
|
||||
if str_eq(p, "") {
|
||||
return 7749
|
||||
}
|
||||
return str_to_int(p)
|
||||
}
|
||||
|
||||
// Principal identity is NOT read from config — it is baked into the compiled
|
||||
// binary at build time. See main.el (developer) and main-user.el (user build).
|
||||
// config_mode() has been removed. Use daemon_principal() from the entry file.
|
||||
@@ -0,0 +1,17 @@
|
||||
// events/bus.el — Event bus. Three primitives over native_queue_*.
|
||||
//
|
||||
// Publish, consume, ack. Consumer identity IS the subscription.
|
||||
// No separate subscribe step. No state. Intelligence lives in el.
|
||||
// The Rust backend is swappable: InMemory → Engram → Kafka → RabbitMQ.
|
||||
|
||||
fn event_publish(topic: String, payload: String) -> Void {
|
||||
native_queue_publish(topic, payload)
|
||||
}
|
||||
|
||||
fn event_consume(topic: String, consumer: String) -> String {
|
||||
return native_queue_consume(topic, consumer)
|
||||
}
|
||||
|
||||
fn event_ack(topic: String, consumer: String, msg_id: String) -> Void {
|
||||
native_queue_ack(topic, consumer, msg_id)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// graph.el — Graph operations subsystem.
|
||||
//
|
||||
// Neuron's storage backend is Engram — a graph database where every entity
|
||||
// (Memory, Knowledge, BacklogItem, etc.) is a node and every relationship
|
||||
// is a typed edge. This subsystem exposes the raw graph API for operations
|
||||
// that span entity types or need structural graph information.
|
||||
//
|
||||
// WHY expose graph ops directly?
|
||||
// High-level tools like search_memories or retrieve_knowledge hide the graph
|
||||
// structure. But some questions are inherently graph-shaped:
|
||||
// "What entities are connected to this decision?"
|
||||
// "How deep is the dependency chain for this backlog item?"
|
||||
// "Are these two memories causally related?"
|
||||
//
|
||||
// Graph ops answer these questions without the overhead of entity-typed
|
||||
// queries. They're the escape hatch when you need to see the whole picture.
|
||||
//
|
||||
// link_entities is a @manager because it mutates the graph topology.
|
||||
// All reads are @accessor.
|
||||
|
||||
from types import {
|
||||
GraphNode,
|
||||
SearchResult,
|
||||
NeuronError,
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
// inspect_graph — get a node and its immediate connections.
|
||||
//
|
||||
// entity_type: "Memory" | "Knowledge" | "BacklogItem" | "Context" | "Artifact" | ...
|
||||
// entity_id: the UUID or string ID of the entity.
|
||||
//
|
||||
// Returns the node and its edges formatted as "relation:target_id" strings.
|
||||
// Use this to understand what a node is connected to before traversing deeper.
|
||||
@accessor
|
||||
fn inspect_graph(entity_type: String, entity_id: String) -> Result<GraphNode, NeuronError> {
|
||||
let node = native_graph_inspect(entity_type, entity_id)?
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
// traverse_graph — walk the graph from a starting node up to depth hops.
|
||||
//
|
||||
// depth=1 returns immediate neighbors only (equivalent to inspect_graph).
|
||||
// depth=2 returns neighbors of neighbors, etc.
|
||||
// Default depth is 2 — deep enough for most dependency chains.
|
||||
//
|
||||
// Returns all visited nodes, not just leaves. This gives the caller the
|
||||
// full subgraph around the starting node.
|
||||
@accessor
|
||||
fn traverse_graph(entity_id: String, depth: Int) -> Result<[GraphNode], NeuronError> {
|
||||
let effective_depth = if depth == 0 { 2 } else { depth }
|
||||
let nodes = native_graph_traverse(entity_id, effective_depth)?
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
// link_entities — create a typed edge between two graph nodes.
|
||||
//
|
||||
// relation examples: "supersedes", "depends_on", "caused_by", "related_to",
|
||||
// "implements", "references", "blocks"
|
||||
//
|
||||
// Edges are directional: source --relation--> target.
|
||||
// Use causal links for reasoning chains, dependency links for work items,
|
||||
// supersedes links for knowledge evolution.
|
||||
@manager
|
||||
fn link_entities(
|
||||
source_id: String,
|
||||
target_id: String,
|
||||
relation: String,
|
||||
) -> Result<GraphNode, NeuronError> {
|
||||
native_graph_link(source_id, target_id, relation)?
|
||||
native_emit("graph.linked", {"source": source_id, "target": target_id, "relation": relation})
|
||||
// Return the source node with its updated edges.
|
||||
let source_node = native_graph_inspect("", source_id)?
|
||||
Ok(source_node)
|
||||
}
|
||||
|
||||
// search_graph — semantic search across all node types in the graph.
|
||||
//
|
||||
// Unlike type-specific search (search_memories, search_knowledge), this
|
||||
// searches the entire graph regardless of entity type. Useful for
|
||||
// cross-cutting queries: "what's related to authentication across all
|
||||
// memories, knowledge, and backlog items?"
|
||||
@accessor
|
||||
fn search_graph(query: String) -> Result<[SearchResult], NeuronError> {
|
||||
let results = native_semantic_search(query, "", 20)?
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
// rebuild_graph — trigger a full graph index rebuild.
|
||||
//
|
||||
// Used after bulk imports or when the semantic index drifts out of sync.
|
||||
// This is an expensive operation — avoid in normal sessions.
|
||||
// It's sealed because it modifies the graph infrastructure, not just data.
|
||||
@manager
|
||||
fn rebuild_graph() -> Result<String, NeuronError> {
|
||||
sealed {
|
||||
native_emit("graph.rebuild_requested", {})
|
||||
Ok("graph rebuild initiated")
|
||||
}
|
||||
}
|
||||
|
||||
// pin_node — mark a graph node as pinned (exempt from compaction).
|
||||
//
|
||||
// Pinned nodes survive graph compaction even when their activation scores
|
||||
// drop below the compaction threshold. Use for foundational knowledge nodes
|
||||
// that must never be evicted.
|
||||
@manager
|
||||
fn pin_node(entity_id: String, entity_type: String) -> Result<GraphNode, NeuronError> {
|
||||
sealed {
|
||||
// Pinning is implemented as a special self-referential "pinned" edge.
|
||||
native_graph_link(entity_id, entity_id, "pinned")?
|
||||
native_emit("graph.node_pinned", {"id": entity_id, "type": entity_type})
|
||||
let node = native_graph_inspect(entity_type, entity_id)?
|
||||
Ok(node)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// health.el — Health check and status handlers.
|
||||
|
||||
fn health_response() -> String {
|
||||
let mode: String = state_get("neuron_mode")
|
||||
if str_eq(mode, "") {
|
||||
return "{\"status\":\"ok\",\"version\":\"1.0.0-engram\"}"
|
||||
}
|
||||
return "{\"status\":\"ok\",\"version\":\"1.0.0-engram\",\"mode\":\"" + mode + "\"}"
|
||||
}
|
||||
|
||||
fn not_found_response(path: String) -> String {
|
||||
return "{\"error\":\"not found\",\"path\":\"" + path + "\"}"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// ise.el — Internal State Events (ISE) subsystem.
|
||||
//
|
||||
// Internal State Events are Neuron's introspective record. When the reasoning
|
||||
// engine produces a response, an ISE captures the gap between:
|
||||
// pre_reasoning — what the model would say before internal deliberation
|
||||
// post_reasoning — what the model actually outputs after reasoning
|
||||
//
|
||||
// The compression_ratio tells us how much the reasoning compressed the
|
||||
// pre-response. A ratio near 1.0 means almost no change; a ratio near 0.0
|
||||
// means the reasoning fundamentally transformed the output.
|
||||
//
|
||||
// gap_direction encodes whether the change moved toward expression
|
||||
// (more output, more detail) or suppression (filtered, condensed, withheld).
|
||||
//
|
||||
// WHY does Neuron care about this?
|
||||
// ISEs are the raw data for self-model calibration. By reviewing ISEs over
|
||||
// time, Neuron can identify systematic biases: topics where it consistently
|
||||
// suppresses, domains where reasoning rarely changes the output, etc.
|
||||
// This is the foundation of Cultivated General Intelligence — introspection
|
||||
// drives adaptation.
|
||||
//
|
||||
// Access is authenticated by default. ISEs contain sensitive reasoning traces
|
||||
// that should not be exposed to arbitrary callers.
|
||||
|
||||
from types import {
|
||||
InternalStateEvent,
|
||||
NeuronError,
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
// log_internal_state — record one ISE.
|
||||
//
|
||||
// Called by the reasoning engine after each significant reasoning cycle.
|
||||
// The id format is "ise_" + 8 hex chars (matches Rust domain convention).
|
||||
@manager
|
||||
fn log_internal_state(
|
||||
trigger: String,
|
||||
pre_reasoning: String,
|
||||
post_reasoning: String,
|
||||
compression_ratio: String,
|
||||
gap_direction: String,
|
||||
tags: [String],
|
||||
) -> Result<InternalStateEvent, NeuronError> {
|
||||
let id = "ise_" + native_uuid()
|
||||
let now = native_now()
|
||||
let ise = InternalStateEvent {
|
||||
id: id,
|
||||
trigger: trigger,
|
||||
pre_reasoning: pre_reasoning,
|
||||
post_reasoning: post_reasoning,
|
||||
compression_ratio: compression_ratio,
|
||||
gap_direction: gap_direction,
|
||||
tags: tags,
|
||||
logged_at: now,
|
||||
}
|
||||
native_store_ise(ise)?
|
||||
// ISE events are emitted at low verbosity — they are high volume.
|
||||
native_emit("ise.logged", {"id": id, "trigger": trigger, "direction": gap_direction})
|
||||
Ok(ise)
|
||||
}
|
||||
|
||||
// list_internal_state — retrieve recent ISEs.
|
||||
//
|
||||
// limit defaults to 10. Useful for reviewing recent reasoning patterns
|
||||
// during session orientation or calibration reviews.
|
||||
@accessor
|
||||
fn list_internal_state(limit: Int) -> Result<[InternalStateEvent], NeuronError> {
|
||||
let events = native_list_ise(limit)?
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
// get_internal_state — retrieve one ISE by ID.
|
||||
//
|
||||
// Used to drill into a specific reasoning event for detailed analysis.
|
||||
@accessor
|
||||
fn get_internal_state(id: String) -> Result<InternalStateEvent, NeuronError> {
|
||||
// ISEs are stored with prefix "ise_", so a bare 8-char hex ID needs
|
||||
// the prefix prepended. The native layer handles both forms.
|
||||
let results = activate InternalStateEvent where "id:{id}"
|
||||
let ise = native_list_ise(1)?
|
||||
Ok(ise[0])
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// knowledge.el — Knowledge base subsystem.
|
||||
//
|
||||
// Knowledge is stable reference material: architecture docs, coding standards,
|
||||
// proven patterns, whitepapers. Unlike memories (which are ephemeral
|
||||
// observations), knowledge nodes are meant to persist across many sessions
|
||||
// and become more authoritative over time.
|
||||
//
|
||||
// The tier system enforces epistemological discipline:
|
||||
// note → raw observation (default)
|
||||
// lesson → validated pattern (proven ≥2 times)
|
||||
// canonical → authoritative reference (stable, widely referenced)
|
||||
//
|
||||
// Never skip tiers. A pattern observed once is a note, not a lesson.
|
||||
|
||||
from types import {
|
||||
Knowledge,
|
||||
SearchResult,
|
||||
NeuronError,
|
||||
}
|
||||
|
||||
// ── Key path utilities ────────────────────────────────────────────────────────
|
||||
|
||||
// Knowledge keys use path notation: "architecture/vbd/foundations.md"
|
||||
// The key doubles as a stable human-readable address and the graph node ID.
|
||||
// When evolving knowledge, the key stays stable — only content and tier change.
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
// capture_knowledge — store a new knowledge node.
|
||||
//
|
||||
// The key should be a path-style string that describes where this knowledge
|
||||
// lives in the taxonomy: "architecture/styles/vbd/foundations.md",
|
||||
// "coding/rust/error-handling.md", etc.
|
||||
@manager
|
||||
fn capture_knowledge(
|
||||
title: String,
|
||||
content: String,
|
||||
category: String,
|
||||
tier: String,
|
||||
tags: [String],
|
||||
project: String,
|
||||
) -> Result<Knowledge, NeuronError> {
|
||||
let id = native_uuid()
|
||||
let now = native_now()
|
||||
// Derive a stable key from category + title (slugified by runtime).
|
||||
let key = category + "/" + title
|
||||
let knowledge = Knowledge {
|
||||
id: id,
|
||||
key: key,
|
||||
title: title,
|
||||
content: content,
|
||||
category: category,
|
||||
tier: tier,
|
||||
tags: tags,
|
||||
project: project,
|
||||
created_at: now,
|
||||
}
|
||||
native_store_knowledge(knowledge)?
|
||||
native_emit("knowledge.captured", {"id": id, "tier": tier, "category": category})
|
||||
Ok(knowledge)
|
||||
}
|
||||
|
||||
// retrieve_knowledge — fetch a knowledge node by its path key.
|
||||
//
|
||||
// This is the primary retrieval path for known knowledge. When you know the
|
||||
// key ("architecture/vbd/fundamentals.md"), use this. For exploratory queries,
|
||||
// use search_knowledge.
|
||||
@accessor
|
||||
fn retrieve_knowledge(key: String) -> Result<Knowledge, NeuronError> {
|
||||
// The native layer maps key → graph node via an index.
|
||||
let results = activate Knowledge where "key:{key}"
|
||||
// Return the first (most relevant) result or not-found.
|
||||
let knowledge = native_get_knowledge(key)?
|
||||
Ok(knowledge)
|
||||
}
|
||||
|
||||
// search_knowledge — semantic search over the knowledge graph.
|
||||
//
|
||||
// Supports optional category and tier filters. Always search before
|
||||
// implementing anything — the knowledge base contains patterns and
|
||||
// decisions that should not be reinvented.
|
||||
@accessor
|
||||
fn search_knowledge(
|
||||
query: String,
|
||||
category: String,
|
||||
tier: String,
|
||||
limit: Int,
|
||||
) -> Result<[Knowledge], NeuronError> {
|
||||
let results = activate Knowledge where "{query} category:{category} tier:{tier} limit:{limit}"
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
// browse_knowledge — list knowledge nodes by category.
|
||||
//
|
||||
// Used to orient at session start or when exploring an unfamiliar domain.
|
||||
// Returns a summary list, not full content — use retrieve_knowledge for
|
||||
// the full text of a specific node.
|
||||
@accessor
|
||||
fn browse_knowledge(category: String) -> Result<[Knowledge], NeuronError> {
|
||||
let knowledge_list = native_list_knowledge(category)?
|
||||
Ok(knowledge_list)
|
||||
}
|
||||
|
||||
// evolve_knowledge — update an existing knowledge node.
|
||||
//
|
||||
// When new evidence supersedes an existing canonical, call this rather than
|
||||
// capture_knowledge. The old node is soft-deleted; the new one inherits the
|
||||
// key path so all existing references remain valid.
|
||||
@manager
|
||||
fn evolve_knowledge(
|
||||
id: String,
|
||||
new_content: String,
|
||||
new_tier: String,
|
||||
supersedes_id: String?,
|
||||
) -> Result<Knowledge, NeuronError> {
|
||||
let old = native_get_knowledge(id)?
|
||||
let now = native_now()
|
||||
let evolved = Knowledge {
|
||||
id: native_uuid(),
|
||||
key: old.key,
|
||||
title: old.title,
|
||||
content: new_content,
|
||||
category: old.category,
|
||||
tier: new_tier,
|
||||
tags: old.tags,
|
||||
project: old.project,
|
||||
created_at: now,
|
||||
}
|
||||
native_store_knowledge(evolved)?
|
||||
native_emit("knowledge.evolved", {"old_id": id, "new_id": evolved.id, "tier": new_tier})
|
||||
Ok(evolved)
|
||||
}
|
||||
|
||||
// remove_knowledge — delete a knowledge node.
|
||||
//
|
||||
// Use sparingly. Knowledge is meant to accumulate. Only remove nodes that
|
||||
// are factually wrong, not just outdated (prefer evolve_knowledge for
|
||||
// supersession).
|
||||
@manager
|
||||
fn remove_knowledge(id: String) -> Result<Knowledge, NeuronError> {
|
||||
let knowledge = native_get_knowledge(id)?
|
||||
native_emit("knowledge.removed", {"id": id, "key": knowledge.key})
|
||||
Ok(knowledge)
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// loop.el — Six-tier runtime heartbeat for the Neuron daemon.
|
||||
//
|
||||
// Implements the self-pacing cognitive loop described in the architecture
|
||||
// doc. The loop runs in its own OS thread (spawned via `spawn_thread`)
|
||||
// and communicates with the rest of the daemon through global shared
|
||||
// state (`state_get` / `state_set`).
|
||||
//
|
||||
// The six tiers:
|
||||
// resting — 30 min (low signal, diffuse)
|
||||
// watching — 10 min (ambient monitoring)
|
||||
// working — 15 sec (background task in progress)
|
||||
// active — 500 ms (conversation in progress)
|
||||
// critical — 10 ms (bell fired)
|
||||
// realtime — busy loop (physical actuator pinned)
|
||||
//
|
||||
// Tier transition rules:
|
||||
// 1. Bell is sacred — escalates to Critical immediately, never dropped.
|
||||
// 2. Escalation is immediate — loop re-enters without finishing sleep.
|
||||
// 3. Step-down is earned — 4 consecutive idle ticks before stepping down.
|
||||
// 4. Floor is configurable — never drops below `loop_min_tier`.
|
||||
//
|
||||
// State keys this file reads / writes:
|
||||
// loop_tier, loop_min_tier, loop_ticks, loop_bell_fires,
|
||||
// loop_tier_changes, loop_idle_ticks, loop_signal, loop_override_tier
|
||||
//
|
||||
// Cognitive substrate:
|
||||
// loop manages WHEN to tick; agent.el manages WHAT to do on each tick.
|
||||
// loop_do_tick() calls agent_tick() after confirming neuronrs is live.
|
||||
|
||||
import "agent.el"
|
||||
import "plugins/host.el"
|
||||
|
||||
// ── Tier constants and ordering ───────────────────────────────────────────────
|
||||
|
||||
fn loop_tier_rank(tier: String) -> Int {
|
||||
if str_eq(tier, "resting") { return 0 }
|
||||
if str_eq(tier, "watching") { return 1 }
|
||||
if str_eq(tier, "working") { return 2 }
|
||||
if str_eq(tier, "active") { return 3 }
|
||||
if str_eq(tier, "critical") { return 4 }
|
||||
if str_eq(tier, "realtime") { return 5 }
|
||||
return 1
|
||||
}
|
||||
|
||||
fn loop_tier_from_rank(rank: Int) -> String {
|
||||
if rank <= 0 { return "resting" }
|
||||
if rank == 1 { return "watching" }
|
||||
if rank == 2 { return "working" }
|
||||
if rank == 3 { return "active" }
|
||||
if rank == 4 { return "critical" }
|
||||
return "realtime"
|
||||
}
|
||||
|
||||
// Sleep interval in milliseconds for each tier.
|
||||
// Returns 0 for realtime (busy loop, no sleep at all).
|
||||
fn loop_tier_interval(tier: String) -> Int {
|
||||
if str_eq(tier, "resting") { return 1800000 }
|
||||
if str_eq(tier, "watching") { return 600000 }
|
||||
if str_eq(tier, "working") { return 15000 }
|
||||
if str_eq(tier, "active") { return 500 }
|
||||
if str_eq(tier, "critical") { return 10 }
|
||||
if str_eq(tier, "realtime") { return 0 }
|
||||
return 600000
|
||||
}
|
||||
|
||||
// ── Signal handling ───────────────────────────────────────────────────────────
|
||||
|
||||
// Apply a signal to the current tier and return the new tier.
|
||||
//
|
||||
// Bell is special: it always escalates to at least Critical, regardless
|
||||
// of `min_tier`. Other escalations clamp to max(needed, min_tier) so a
|
||||
// configured floor cannot trap us above an explicit step-down.
|
||||
fn loop_apply_signal(current: String, signal: String, min_tier: String) -> String {
|
||||
let cur_rank: Int = loop_tier_rank(current)
|
||||
let min_rank: Int = loop_tier_rank(min_tier)
|
||||
|
||||
// Bell — sacred. Always go to at least Critical.
|
||||
if str_eq(signal, "bell") {
|
||||
let crit_rank: Int = 4
|
||||
if cur_rank >= crit_rank {
|
||||
return current
|
||||
}
|
||||
return "critical"
|
||||
}
|
||||
|
||||
// Realtime escalation pins us at the top.
|
||||
if str_eq(signal, "realtime") {
|
||||
return "realtime"
|
||||
}
|
||||
|
||||
// Release-realtime drops us back to Critical.
|
||||
if str_eq(signal, "release-realtime") {
|
||||
if str_eq(current, "realtime") {
|
||||
return "critical"
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
// Active escalates to at least Active.
|
||||
if str_eq(signal, "active") {
|
||||
let need: Int = 3
|
||||
if cur_rank >= need {
|
||||
return current
|
||||
}
|
||||
return "active"
|
||||
}
|
||||
|
||||
// Task escalates to at least Working.
|
||||
if str_eq(signal, "task") {
|
||||
let need: Int = 2
|
||||
if cur_rank >= need {
|
||||
return current
|
||||
}
|
||||
return "working"
|
||||
}
|
||||
|
||||
// Sleep is an explicit step-down request — drop one tier
|
||||
// (respecting the floor).
|
||||
if str_eq(signal, "sleep") {
|
||||
return loop_step_down(current, min_tier)
|
||||
}
|
||||
|
||||
// Idle / drain just contribute to the idle counter; the tier
|
||||
// itself does not change here.
|
||||
return current
|
||||
}
|
||||
|
||||
// Step down one tier, but never below the configured min_tier.
|
||||
fn loop_step_down(current: String, min_tier: String) -> String {
|
||||
let cur_rank: Int = loop_tier_rank(current)
|
||||
let min_rank: Int = loop_tier_rank(min_tier)
|
||||
if cur_rank <= min_rank {
|
||||
return min_tier
|
||||
}
|
||||
let new_rank: Int = cur_rank - 1
|
||||
if new_rank < min_rank {
|
||||
return min_tier
|
||||
}
|
||||
return loop_tier_from_rank(new_rank)
|
||||
}
|
||||
|
||||
// ── Bell-aware sleep ──────────────────────────────────────────────────────────
|
||||
|
||||
// Tail-recursive sleep that wakes early if a bell signal arrives.
|
||||
// Sleeps in 100ms chunks, peeking at `loop_signal` between each chunk.
|
||||
fn loop_sleep_chunked(remaining_ms: Int) -> Void {
|
||||
if remaining_ms <= 0 {
|
||||
// done
|
||||
} else {
|
||||
let pending: String = state_get("loop_signal")
|
||||
if str_eq(pending, "bell") {
|
||||
// Bell detected mid-sleep — return immediately so the
|
||||
// outer loop can re-enter and escalate.
|
||||
} else {
|
||||
let chunk: Int = if remaining_ms < 100 { remaining_ms } else { 100 }
|
||||
sleep_ms(chunk)
|
||||
loop_sleep_chunked(remaining_ms - chunk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public sleep entry point. realtime (interval == 0) is a no-op.
|
||||
fn loop_sleep(remaining_ms: Int) -> Void {
|
||||
if remaining_ms > 0 {
|
||||
loop_sleep_chunked(remaining_ms)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tick ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Perform one cognitive tick.
|
||||
//
|
||||
// First pings neuronrs /health to confirm the substrate is live.
|
||||
// If live, delegates to agent_tick() — the cognitive work is in agent.el.
|
||||
// loop manages WHEN; agent manages WHAT.
|
||||
//
|
||||
// Side effect: writes "1" or "0" to `loop_last_tick_idle` so the
|
||||
// caller can see whether the tick was idle (no live counterpart).
|
||||
fn loop_do_tick(tier: String) -> Void {
|
||||
let url: String = "http://localhost:7770/health"
|
||||
let resp: String = http_get(url)
|
||||
let live: Bool = str_contains(resp, "ok")
|
||||
if live {
|
||||
state_set("loop_last_tick_idle", "0")
|
||||
// Delegate to the agent cognitive substrate.
|
||||
agent_tick({})
|
||||
} else {
|
||||
state_set("loop_last_tick_idle", "1")
|
||||
}
|
||||
// Drain the plugin event bus on every tick, regardless of substrate liveness.
|
||||
// Plugins interact through events only — host_tick routes events to all
|
||||
// plugins that subscribed to them. No hooks, no direct callbacks.
|
||||
host_tick({})
|
||||
println("[loop] tick tier=" + tier + " live=" + bool_to_str(live))
|
||||
}
|
||||
|
||||
// ── Counters ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn loop_incr_state_int(key: String) -> Void {
|
||||
let raw: String = state_get(key)
|
||||
let n: Int = if str_eq(raw, "") { 0 } else { str_to_int(raw) }
|
||||
state_set(key, int_to_str(n + 1))
|
||||
}
|
||||
|
||||
fn loop_set_int(key: String, value: Int) -> Void {
|
||||
state_set(key, int_to_str(value))
|
||||
}
|
||||
|
||||
// ── Core loop ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// One iteration of the heartbeat. Tail-recursive: each tick re-enters
|
||||
// itself with the (possibly updated) tier and idle counter.
|
||||
fn loop_run(tier: String, idle_count: Int) -> Void {
|
||||
// 1. Read floor and pending signals.
|
||||
let min_tier: String = state_get("loop_min_tier")
|
||||
let floor: String = if str_eq(min_tier, "") { "resting" } else { min_tier }
|
||||
|
||||
let pending: String = state_get("loop_signal")
|
||||
let override_tier: String = state_get("loop_override_tier")
|
||||
|
||||
// 2. Apply override (explicit set tier — wins over signal).
|
||||
let after_override: String = if str_eq(override_tier, "") {
|
||||
tier
|
||||
} else {
|
||||
override_tier
|
||||
}
|
||||
if !str_eq(override_tier, "") {
|
||||
state_set("loop_override_tier", "")
|
||||
loop_incr_state_int("loop_tier_changes")
|
||||
}
|
||||
|
||||
// 3. Apply signal.
|
||||
let after_signal: String = if str_eq(pending, "") {
|
||||
after_override
|
||||
} else {
|
||||
loop_apply_signal(after_override, pending, floor)
|
||||
}
|
||||
let signal_changed: Bool = !str_eq(pending, "")
|
||||
if signal_changed {
|
||||
state_set("loop_signal", "")
|
||||
if str_eq(pending, "bell") {
|
||||
loop_incr_state_int("loop_bell_fires")
|
||||
}
|
||||
if !str_eq(after_signal, after_override) {
|
||||
loop_incr_state_int("loop_tier_changes")
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Persist current tier.
|
||||
state_set("loop_tier", after_signal)
|
||||
|
||||
// 5. Sleep for this tier's interval (bell-aware, chunked).
|
||||
let interval_ms: Int = loop_tier_interval(after_signal)
|
||||
loop_sleep(interval_ms)
|
||||
|
||||
// 6. If a bell arrived during sleep, re-enter immediately without
|
||||
// ticking — the next iteration will re-read the signal and
|
||||
// escalate.
|
||||
let after_sleep_signal: String = state_get("loop_signal")
|
||||
if str_eq(after_sleep_signal, "bell") {
|
||||
loop_run(after_signal, idle_count)
|
||||
} else {
|
||||
// 7. Run the cognitive tick.
|
||||
loop_do_tick(after_signal)
|
||||
loop_incr_state_int("loop_ticks")
|
||||
|
||||
// 8. Update idle counter and step down if earned.
|
||||
let last_idle: String = state_get("loop_last_tick_idle")
|
||||
let was_idle: Bool = str_eq(last_idle, "1")
|
||||
|
||||
let new_idle_count: Int = if was_idle { idle_count + 1 } else { 0 }
|
||||
|
||||
// Idle / drain signals also push the counter forward, but
|
||||
// `loop_apply_signal` does not change the tier for them.
|
||||
let idle_signal: Bool = str_eq(pending, "idle") || str_eq(pending, "drain")
|
||||
let bumped_idle_count: Int = if idle_signal {
|
||||
new_idle_count + 1
|
||||
} else {
|
||||
new_idle_count
|
||||
}
|
||||
|
||||
// Realtime is pinned — only `release-realtime` can lower it.
|
||||
let pinned: Bool = str_eq(after_signal, "realtime")
|
||||
|
||||
if !pinned && bumped_idle_count >= 4 {
|
||||
let stepped: String = loop_step_down(after_signal, floor)
|
||||
if !str_eq(stepped, after_signal) {
|
||||
loop_incr_state_int("loop_tier_changes")
|
||||
}
|
||||
loop_set_int("loop_idle_ticks", 0)
|
||||
loop_run(stepped, 0)
|
||||
} else {
|
||||
loop_set_int("loop_idle_ticks", bumped_idle_count)
|
||||
loop_run(after_signal, bumped_idle_count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point — called by `spawn_thread("loop_main")`.
|
||||
fn loop_main() -> Void {
|
||||
let start_tier: String = state_get("loop_tier")
|
||||
let initial: String = if str_eq(start_tier, "") { "watching" } else { start_tier }
|
||||
println("[loop] starting at tier=" + initial)
|
||||
loop_run(initial, 0)
|
||||
}
|
||||
|
||||
// ── Status JSON ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn loop_status_field(key: String, default: String) -> String {
|
||||
let v: String = state_get(key)
|
||||
if str_eq(v, "") { return default }
|
||||
return v
|
||||
}
|
||||
|
||||
fn loop_status_json() -> String {
|
||||
let tier: String = loop_status_field("loop_tier", "watching")
|
||||
let min_tier: String = loop_status_field("loop_min_tier", "resting")
|
||||
let ticks: String = loop_status_field("loop_ticks", "0")
|
||||
let bell_fires: String = loop_status_field("loop_bell_fires", "0")
|
||||
let tier_changes: String = loop_status_field("loop_tier_changes", "0")
|
||||
let idle_ticks: String = loop_status_field("loop_idle_ticks", "0")
|
||||
let signal: String = loop_status_field("loop_signal", "")
|
||||
|
||||
let parts: String = "{\"current_tier\":\"" + tier + "\""
|
||||
let p2: String = parts + ",\"min_tier\":\"" + min_tier + "\""
|
||||
let p3: String = p2 + ",\"ticks\":" + ticks
|
||||
let p4: String = p3 + ",\"bell_fires\":" + bell_fires
|
||||
let p5: String = p4 + ",\"tier_changes\":" + tier_changes
|
||||
let p6: String = p5 + ",\"idle_ticks\":" + idle_ticks
|
||||
let p7: String = p6 + ",\"pending_signal\":\"" + signal + "\"}"
|
||||
return p7
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// main-user.el — Neuron daemon entry point. User build.
|
||||
// Written in Engram.
|
||||
//
|
||||
// Principal identity is a compile-time constant — NOT read from config.
|
||||
// This file is the user build. The developer build is main.el.
|
||||
// Built with: el build --manifest el-user.toml
|
||||
//
|
||||
// Responsibilities:
|
||||
// 1. Write PID file to ~/.neuron-user/data/daemon.pid
|
||||
// 2. Start API/proxy server on :7750 (blocking)
|
||||
// - Proxies /axon/, /api/ routes to neuronrs at :7770
|
||||
// - Health check at /health
|
||||
//
|
||||
// The handle_request function is called by http_serve for every request.
|
||||
|
||||
import "daemon_config.el"
|
||||
import "proxy.el"
|
||||
import "health.el"
|
||||
import "loop.el"
|
||||
import "plugins/host.el"
|
||||
|
||||
// ── Principal identity (baked in at compile time) ─────────────────────────────
|
||||
// This literal is compiled into the bytecode. For prod builds it is sealed
|
||||
// inside AES-256-GCM — cannot be changed without the deployment key.
|
||||
|
||||
let principal: String = "user"
|
||||
|
||||
// ── Load operational config ───────────────────────────────────────────────────
|
||||
|
||||
let cfg: String = load_config()
|
||||
let axon_base: String = config_api_url(cfg)
|
||||
let token: String = config_api_token(cfg)
|
||||
let ui_dir: String = config_ui_dir(cfg)
|
||||
let data_dir: String = config_data_dir(cfg)
|
||||
let port: Int = config_port(cfg)
|
||||
state_set("neuron_principal", principal)
|
||||
|
||||
// ── Write PID file ────────────────────────────────────────────────────────────
|
||||
|
||||
let pid: Int = getpid()
|
||||
let pid_str: String = int_to_str(pid)
|
||||
fs_mkdir(data_dir)
|
||||
let pid_path: String = data_dir + "/daemon.pid"
|
||||
fs_write(pid_path, pid_str)
|
||||
|
||||
println(color_bold("Neuron daemon") + " — pid " + pid_str + " — " + principal)
|
||||
println(" API → http://localhost:" + int_to_str(port))
|
||||
println(" Axon → " + axon_base)
|
||||
println(" Data → " + data_dir)
|
||||
println("")
|
||||
|
||||
// ── Request handler (API server on :7750) ─────────────────────────────────────
|
||||
//
|
||||
// Called by http_serve for every incoming request.
|
||||
// Must be named exactly "handle_request" — http_serve looks it up by that name.
|
||||
|
||||
fn handle_request(method: String, path: String, body: String) -> String {
|
||||
// Health check
|
||||
if str_eq(path, "/health") {
|
||||
return health_response()
|
||||
}
|
||||
|
||||
// Axon tool dispatch — proxy to neuronrs
|
||||
if str_starts_with(path, "/axon/") {
|
||||
return proxy_request(axon_base, method, path, body, token)
|
||||
}
|
||||
|
||||
// Intelligence REST API — proxy to neuronrs
|
||||
if str_starts_with(path, "/api/memories") {
|
||||
return proxy_request(axon_base, method, path, body, token)
|
||||
}
|
||||
if str_starts_with(path, "/api/knowledge") {
|
||||
return proxy_request(axon_base, method, path, body, token)
|
||||
}
|
||||
if str_starts_with(path, "/api/backlog") {
|
||||
return proxy_request(axon_base, method, path, body, token)
|
||||
}
|
||||
if str_starts_with(path, "/api/contexts") {
|
||||
return proxy_request(axon_base, method, path, body, token)
|
||||
}
|
||||
if str_starts_with(path, "/api/ise") {
|
||||
return proxy_request(axon_base, method, path, body, token)
|
||||
}
|
||||
|
||||
// ── Plugin status ─────────────────────────────────────────────────────────
|
||||
if str_eq(path, "/plugin/status") {
|
||||
return host_status()
|
||||
}
|
||||
|
||||
// ── Runtime loop control ──────────────────────────────────────────────────
|
||||
if str_eq(path, "/loop/status") {
|
||||
return loop_status_json()
|
||||
}
|
||||
if str_eq(path, "/loop/signal") {
|
||||
let sig: String = json_get(body, "signal")
|
||||
if str_eq(sig, "") {
|
||||
return "{\"error\":\"missing signal\"}"
|
||||
}
|
||||
state_set("loop_signal", sig)
|
||||
return "{\"ok\":true,\"signal\":\"" + sig + "\"}"
|
||||
}
|
||||
if str_eq(path, "/loop/tier") {
|
||||
let new_tier: String = json_get(body, "tier")
|
||||
let new_min: String = json_get(body, "min_tier")
|
||||
if !str_eq(new_min, "") {
|
||||
state_set("loop_min_tier", new_min)
|
||||
}
|
||||
if !str_eq(new_tier, "") {
|
||||
state_set("loop_override_tier", new_tier)
|
||||
}
|
||||
return "{\"ok\":true,\"tier\":\"" + new_tier + "\",\"min_tier\":\"" + new_min + "\"}"
|
||||
}
|
||||
|
||||
return not_found_response(path)
|
||||
}
|
||||
|
||||
// ── Initialise plugin host ────────────────────────────────────────────────────
|
||||
|
||||
host_on_startup()
|
||||
|
||||
// ── Initialise loop state ─────────────────────────────────────────────────────
|
||||
|
||||
state_set("loop_tier", "watching")
|
||||
state_set("loop_min_tier", "resting")
|
||||
state_set("loop_ticks", "0")
|
||||
state_set("loop_bell_fires", "0")
|
||||
state_set("loop_tier_changes", "0")
|
||||
state_set("loop_idle_ticks", "0")
|
||||
state_set("loop_signal", "")
|
||||
state_set("loop_override_tier", "")
|
||||
state_set("loop_last_tick_idle", "0")
|
||||
|
||||
// ── Spawn the heartbeat thread ────────────────────────────────────────────────
|
||||
|
||||
spawn_thread("loop_main")
|
||||
|
||||
// ── Start API server (blocking) ───────────────────────────────────────────────
|
||||
|
||||
http_serve(port)
|
||||
@@ -0,0 +1,141 @@
|
||||
// main.el — Neuron daemon entry point. Developer (unlocked) build.
|
||||
// Written in Engram.
|
||||
//
|
||||
// Principal identity is a compile-time constant — NOT read from config.
|
||||
// This file is the developer build. The user build is main-user.el.
|
||||
// Both are compiled from different el.toml manifests:
|
||||
// el build → developer (el.toml)
|
||||
// el build --manifest el-user.toml → user (el-user.toml)
|
||||
//
|
||||
// Responsibilities:
|
||||
// 1. Write PID file to ~/.neuron/data/daemon.pid
|
||||
// 2. Start API/proxy server on :7749 (blocking)
|
||||
// - Proxies /axon/, /api/ routes to neuronrs at :7770
|
||||
// - Health check at /health
|
||||
//
|
||||
// The handle_request function is called by http_serve for every request.
|
||||
|
||||
import "daemon_config.el"
|
||||
import "proxy.el"
|
||||
import "health.el"
|
||||
import "loop.el"
|
||||
import "plugins/host.el"
|
||||
|
||||
// ── Principal identity (baked in at compile time) ─────────────────────────────
|
||||
// This literal is compiled into the bytecode. For prod builds it is sealed
|
||||
// inside AES-256-GCM — cannot be changed without the deployment key.
|
||||
|
||||
let principal: String = "principal"
|
||||
|
||||
// ── Load operational config ───────────────────────────────────────────────────
|
||||
|
||||
let cfg: String = load_config()
|
||||
let axon_base: String = config_api_url(cfg)
|
||||
let token: String = config_api_token(cfg)
|
||||
let ui_dir: String = config_ui_dir(cfg)
|
||||
let data_dir: String = config_data_dir(cfg)
|
||||
let port: Int = config_port(cfg)
|
||||
state_set("neuron_principal", principal)
|
||||
|
||||
// ── Write PID file ────────────────────────────────────────────────────────────
|
||||
|
||||
let pid: Int = getpid()
|
||||
let pid_str: String = int_to_str(pid)
|
||||
fs_mkdir(data_dir)
|
||||
let pid_path: String = data_dir + "/daemon.pid"
|
||||
fs_write(pid_path, pid_str)
|
||||
|
||||
println(color_bold("Neuron daemon") + " — pid " + pid_str + " — " + principal)
|
||||
println(" API → http://localhost:" + int_to_str(port))
|
||||
println(" Axon → " + axon_base)
|
||||
println(" Data → " + data_dir)
|
||||
println("")
|
||||
|
||||
// ── Request handler (API server on :7749) ─────────────────────────────────────
|
||||
//
|
||||
// Called by http_serve for every incoming request.
|
||||
// Must be named exactly "handle_request" — http_serve looks it up by that name.
|
||||
|
||||
fn handle_request(method: String, path: String, body: String) -> String {
|
||||
// Health check
|
||||
if str_eq(path, "/health") {
|
||||
return health_response()
|
||||
}
|
||||
|
||||
// Axon tool dispatch — proxy to neuronrs
|
||||
if str_starts_with(path, "/axon/") {
|
||||
return proxy_request(axon_base, method, path, body, token)
|
||||
}
|
||||
|
||||
// Intelligence REST API — proxy to neuronrs
|
||||
if str_starts_with(path, "/api/memories") {
|
||||
return proxy_request(axon_base, method, path, body, token)
|
||||
}
|
||||
if str_starts_with(path, "/api/knowledge") {
|
||||
return proxy_request(axon_base, method, path, body, token)
|
||||
}
|
||||
if str_starts_with(path, "/api/backlog") {
|
||||
return proxy_request(axon_base, method, path, body, token)
|
||||
}
|
||||
if str_starts_with(path, "/api/contexts") {
|
||||
return proxy_request(axon_base, method, path, body, token)
|
||||
}
|
||||
if str_starts_with(path, "/api/ise") {
|
||||
return proxy_request(axon_base, method, path, body, token)
|
||||
}
|
||||
|
||||
// ── Plugin status ─────────────────────────────────────────────────────────
|
||||
if str_eq(path, "/plugin/status") {
|
||||
return host_status()
|
||||
}
|
||||
|
||||
// ── Runtime loop control ──────────────────────────────────────────────────
|
||||
if str_eq(path, "/loop/status") {
|
||||
return loop_status_json()
|
||||
}
|
||||
if str_eq(path, "/loop/signal") {
|
||||
let sig: String = json_get(body, "signal")
|
||||
if str_eq(sig, "") {
|
||||
return "{\"error\":\"missing signal\"}"
|
||||
}
|
||||
state_set("loop_signal", sig)
|
||||
return "{\"ok\":true,\"signal\":\"" + sig + "\"}"
|
||||
}
|
||||
if str_eq(path, "/loop/tier") {
|
||||
let new_tier: String = json_get(body, "tier")
|
||||
let new_min: String = json_get(body, "min_tier")
|
||||
if !str_eq(new_min, "") {
|
||||
state_set("loop_min_tier", new_min)
|
||||
}
|
||||
if !str_eq(new_tier, "") {
|
||||
state_set("loop_override_tier", new_tier)
|
||||
}
|
||||
return "{\"ok\":true,\"tier\":\"" + new_tier + "\",\"min_tier\":\"" + new_min + "\"}"
|
||||
}
|
||||
|
||||
return not_found_response(path)
|
||||
}
|
||||
|
||||
// ── Initialise plugin host ────────────────────────────────────────────────────
|
||||
|
||||
host_on_startup()
|
||||
|
||||
// ── Initialise loop state ─────────────────────────────────────────────────────
|
||||
|
||||
state_set("loop_tier", "watching")
|
||||
state_set("loop_min_tier", "resting")
|
||||
state_set("loop_ticks", "0")
|
||||
state_set("loop_bell_fires", "0")
|
||||
state_set("loop_tier_changes", "0")
|
||||
state_set("loop_idle_ticks", "0")
|
||||
state_set("loop_signal", "")
|
||||
state_set("loop_override_tier", "")
|
||||
state_set("loop_last_tick_idle", "0")
|
||||
|
||||
// ── Spawn the heartbeat thread ────────────────────────────────────────────────
|
||||
|
||||
spawn_thread("loop_main")
|
||||
|
||||
// ── Start API server (blocking) ───────────────────────────────────────────────
|
||||
|
||||
http_serve(port)
|
||||
@@ -0,0 +1,191 @@
|
||||
// memory.el — Memory intelligence subsystem.
|
||||
//
|
||||
// Memories are the raw epistemological substrate of Neuron. They're not
|
||||
// long-term knowledge (that's knowledge.el) — they're observations, decisions,
|
||||
// and lessons captured in flight. The half-life of a memory is governed by
|
||||
// its importance tier and how often the graph activates it.
|
||||
//
|
||||
// Design intent:
|
||||
// - Every remember() call emits an event so other subsystems can react.
|
||||
// - supersedes_id creates a linked list of memory versions — old memories
|
||||
// remain traversable even after replacement.
|
||||
// - Importance auto-promotion prevents Claude from under-tagging critical
|
||||
// decisions just because it used soft language.
|
||||
|
||||
from types import {
|
||||
Memory,
|
||||
SearchResult,
|
||||
NeuronError,
|
||||
}
|
||||
|
||||
// ── Importance auto-promotion ─────────────────────────────────────────────────
|
||||
|
||||
// If the content itself signals criticality, override whatever importance the
|
||||
// caller passed. This prevents decisions from being buried under "normal" tier
|
||||
// just because the caller was being polite.
|
||||
@accessor
|
||||
fn infer_importance(content: String, stated_importance: String) -> String {
|
||||
if stated_importance == "critical" {
|
||||
return "critical"
|
||||
}
|
||||
// Escalate to critical if the content describes irreversibility or arch decisions.
|
||||
let signals = ["critical", "irreversible", "breaking change", "never", "always",
|
||||
"permanent", "architectural", "security", "deleted forever"]
|
||||
for signal in signals {
|
||||
if content == signal {
|
||||
return "critical"
|
||||
}
|
||||
}
|
||||
// Escalate normal → high if the content sounds high-stakes.
|
||||
if stated_importance == "normal" {
|
||||
let high_signals = ["important", "must", "required", "blocked", "production"]
|
||||
for signal in high_signals {
|
||||
if content == signal {
|
||||
return "high"
|
||||
}
|
||||
}
|
||||
}
|
||||
stated_importance
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
// remember — store a new memory node.
|
||||
//
|
||||
// When supersedes_id is provided, the old memory is kept in the graph but
|
||||
// marked as superseded. This preserves the full decision history while making
|
||||
// the new memory the canonical source for activation queries.
|
||||
@manager
|
||||
fn remember(
|
||||
content: String,
|
||||
tags: [String],
|
||||
project: String,
|
||||
importance: String,
|
||||
supersedes_id: String?,
|
||||
) -> Result<Memory, NeuronError> {
|
||||
let effective_importance = infer_importance(content, importance)
|
||||
let id = native_uuid()
|
||||
let now = native_now()
|
||||
let memory = Memory {
|
||||
id: id,
|
||||
content: content,
|
||||
tags: tags,
|
||||
importance: effective_importance,
|
||||
project: project,
|
||||
supersedes_id: supersedes_id,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
let stored = native_store_memory(memory)?
|
||||
native_emit("memory.created", {"id": id, "project": project, "importance": effective_importance})
|
||||
Ok(memory)
|
||||
}
|
||||
|
||||
// recall — retrieve one memory by ID.
|
||||
@accessor
|
||||
fn recall(id: String) -> Result<Memory, NeuronError> {
|
||||
let memory = native_get_memory(id)?
|
||||
Ok(memory)
|
||||
}
|
||||
|
||||
// recall_chain — retrieve recent memories sharing a tag.
|
||||
//
|
||||
// This is how Neuron reconstructs prior reasoning: follow the tag chain
|
||||
// rather than trying to remember everything in the conversation window.
|
||||
@accessor
|
||||
fn recall_chain(tag: String, limit: Int) -> Result<[Memory], NeuronError> {
|
||||
let results = activate Memory where "{tag} limit:{limit}"
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
// search_memories — semantic search over the memory graph.
|
||||
//
|
||||
// Uses spreading activation: the query activates nearby graph nodes and
|
||||
// surfaces the most relevantly connected memories.
|
||||
@accessor
|
||||
fn search_memories(query: String, project: String, limit: Int) -> Result<[Memory], NeuronError> {
|
||||
let results = activate Memory where "{query} project:{project} limit:{limit}"
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
// list_memories — enumerate all memories for a project.
|
||||
// Useful for session orientation (begin_session) and memory audits.
|
||||
@accessor
|
||||
fn list_memories(project: String) -> Result<[Memory], NeuronError> {
|
||||
let memories = native_list_memories(project)?
|
||||
Ok(memories)
|
||||
}
|
||||
|
||||
// forget — soft-delete a memory.
|
||||
//
|
||||
// "Soft delete" means the node is removed from active activation but the
|
||||
// graph edge history is preserved. This is intentional: we don't want to
|
||||
// lose the causal record of why a decision was superseded.
|
||||
@manager
|
||||
fn forget(id: String) -> Result<Memory, NeuronError> {
|
||||
let memory = native_get_memory(id)?
|
||||
native_delete_memory(id)?
|
||||
native_emit("memory.forgotten", {"id": id})
|
||||
Ok(memory)
|
||||
}
|
||||
|
||||
// promote_memory — raise a memory's importance tier.
|
||||
//
|
||||
// Used when a memory originally tagged "normal" turns out to be load-bearing.
|
||||
// Does NOT supersede the original — it mutates importance in place.
|
||||
@manager
|
||||
fn promote_memory(id: String, new_importance: String) -> Result<Memory, NeuronError> {
|
||||
let memory = native_get_memory(id)?
|
||||
let promoted = Memory {
|
||||
id: memory.id,
|
||||
content: memory.content,
|
||||
tags: memory.tags,
|
||||
importance: new_importance,
|
||||
project: memory.project,
|
||||
supersedes_id: memory.supersedes_id,
|
||||
created_at: memory.created_at,
|
||||
updated_at: native_now(),
|
||||
}
|
||||
native_store_memory(promoted)?
|
||||
native_emit("memory.promoted", {"id": id, "importance": new_importance})
|
||||
Ok(promoted)
|
||||
}
|
||||
|
||||
// evolve_memory — update a memory's content and mark it as superseding the old one.
|
||||
//
|
||||
// This is the preferred way to update a memory. It creates a new memory that
|
||||
// explicitly links back to the old one, preserving the version chain.
|
||||
@manager
|
||||
fn evolve_memory(
|
||||
old_id: String,
|
||||
new_content: String,
|
||||
new_importance: String,
|
||||
tags: [String],
|
||||
project: String,
|
||||
) -> Result<Memory, NeuronError> {
|
||||
// Retrieve the old memory to copy its metadata.
|
||||
let old_memory = native_get_memory(old_id)?
|
||||
let effective_importance = infer_importance(new_content, new_importance)
|
||||
let new_id = native_uuid()
|
||||
let now = native_now()
|
||||
let evolved = Memory {
|
||||
id: new_id,
|
||||
content: new_content,
|
||||
tags: tags,
|
||||
importance: effective_importance,
|
||||
project: project,
|
||||
supersedes_id: old_id,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
native_store_memory(evolved)?
|
||||
native_emit("memory.evolved", {"old_id": old_id, "new_id": new_id})
|
||||
Ok(evolved)
|
||||
}
|
||||
|
||||
// inspect_memories — list all memories with optional project filter.
|
||||
// Alias for list_memories that matches the Axon tool name convention.
|
||||
@accessor
|
||||
fn inspect_memories(project: String) -> Result<[Memory], NeuronError> {
|
||||
list_memories(project)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// neuron.el — Neuron intelligence entry point.
|
||||
//
|
||||
// This is the root module loaded by neuron-runtime at startup. It wires
|
||||
// all subsystems together and defines the swarm coordinator — the function
|
||||
// that handles every incoming tool call.
|
||||
//
|
||||
// Architecture intent:
|
||||
// Neuron is a swarm agent: one coordinator orchestrates multiple specialized
|
||||
// subsystems (memory, knowledge, backlog, context, artifact, etc.). Each
|
||||
// subsystem is a focused el module. The coordinator dispatches
|
||||
// to the right subsystem based on tool_name.
|
||||
//
|
||||
// The @swarm_coordinator decorator tells the engram runtime that this function
|
||||
// is the root entry point for the agent. The runtime calls handle_tool_call()
|
||||
// for every incoming Axon message.
|
||||
//
|
||||
// Startup sequence (enforced by the runtime):
|
||||
// 1. Load types.el — graph schema registration
|
||||
// 2. Load all subsystems — function registration
|
||||
// 3. Load axon.el — dispatch table
|
||||
// 4. Load neuron.el — entry point wiring
|
||||
// 5. Call version() — runtime health check
|
||||
//
|
||||
// This file intentionally contains minimal logic. It's the composition
|
||||
// root, not a logic layer. Keep it thin.
|
||||
|
||||
from axon import { dispatch_tool }
|
||||
from types import { NeuronError }
|
||||
|
||||
// ── Swarm coordinator ─────────────────────────────────────────────────────────
|
||||
|
||||
// handle_tool_call — the root entry point for all Axon tool invocations.
|
||||
//
|
||||
// The @swarm_coordinator decorator registers this function as the agent's
|
||||
// primary message handler. The runtime calls it for every incoming tool call
|
||||
// after authentication is verified at the protocol layer.
|
||||
//
|
||||
// It delegates entirely to dispatch_tool() in axon.el — the coordinator
|
||||
// should not contain routing logic.
|
||||
@swarm_coordinator
|
||||
fn handle_tool_call(
|
||||
tool_name: String,
|
||||
params: Map<String, String>,
|
||||
) -> Result<Map<String, String>, NeuronError> {
|
||||
dispatch_tool(tool_name, params)
|
||||
}
|
||||
|
||||
// ── Health and metadata ───────────────────────────────────────────────────────
|
||||
|
||||
// version — return the current Neuron version string.
|
||||
//
|
||||
// @public — called by the runtime health check without authentication.
|
||||
// The format is "neuron/<version>-engram" to distinguish from the Kotlin
|
||||
// predecessor (which used "neuron/<version>-kotlin").
|
||||
@public
|
||||
fn version() -> String {
|
||||
"neuron/1.0.0-engram"
|
||||
}
|
||||
|
||||
// health — lightweight liveness probe.
|
||||
//
|
||||
// Returns "ok" if the engram runtime and all subsystems loaded correctly.
|
||||
// Called by the /health HTTP endpoint in neuron-api.
|
||||
@public
|
||||
fn health() -> String {
|
||||
"ok"
|
||||
}
|
||||
|
||||
// ── Session bootstrap ─────────────────────────────────────────────────────────
|
||||
|
||||
// on_startup — called once when the runtime initializes.
|
||||
//
|
||||
// Emits a startup event so Axon subscribers know Neuron is ready.
|
||||
// Does NOT run begin_session() — that's the caller's responsibility.
|
||||
// The runtime calls on_startup() once; callers call begin_session() per session.
|
||||
@manager
|
||||
fn on_startup() -> Result<String, NeuronError> {
|
||||
native_emit("neuron.started", {"version": "1.0.0-engram"})
|
||||
Ok("neuron ready")
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
// plugins/host.el — Plugin host for the Neuron daemon.
|
||||
//
|
||||
// Plugins communicate with the host exclusively through the event bus.
|
||||
// A plugin's identity IS its contributions — no separate type field.
|
||||
// The marketplace queries contributions to surface clean category lanes.
|
||||
//
|
||||
// Plugin interaction model (both directions are events):
|
||||
//
|
||||
// Plugin → Host:
|
||||
// plugin.announce — plugin is alive, wants to register
|
||||
// plugin.manifest — response to plugin.interrogate, declares capabilities
|
||||
// <any event> — plugin emits these as its "output"
|
||||
//
|
||||
// Host → Plugin:
|
||||
// plugin.interrogate — host asks for the plugin's manifest
|
||||
// <subscribed events> — host forwards events the plugin declared interest in
|
||||
//
|
||||
// Manifest format (plugin.manifest payload):
|
||||
// {
|
||||
// "plugin": "voice",
|
||||
// "version": "1.0.0",
|
||||
// "description": "Voice input and output for Neuron",
|
||||
// "contributions": ["connector", "notification_channel"],
|
||||
// "required": false,
|
||||
// "dependencies": ["audio-hardware"],
|
||||
// "subscriptions": ["agent.turn_complete"],
|
||||
// "emits": ["voice.speaking", "voice.idle"],
|
||||
// }
|
||||
//
|
||||
// Contribution types (contributions IS the type — one list, no separate field):
|
||||
// connector — bridges to an external system
|
||||
// interceptor — operates on the event pipeline
|
||||
// imprint — shapes the cognitive layer directly
|
||||
// knowledge — adds domain knowledge to the graph
|
||||
// process — adds CCR workflow definitions
|
||||
// tool — adds agent-callable tools (registered in daemon)
|
||||
// command — adds CLI commands
|
||||
// behavior — adds behavioral patterns
|
||||
// safety_rule — adds safety constraints
|
||||
// hardware — adds hardware device support
|
||||
// sync_handler — adds sync protocol support
|
||||
// notification_channel — adds a notification delivery channel
|
||||
// ui — adds UI panels or widgets
|
||||
|
||||
from types import { NeuronError }
|
||||
|
||||
// ── Plugin registry entry ─────────────────────────────────────────────────────
|
||||
//
|
||||
// State keys:
|
||||
// plugin.<name>.status — "pending" | "active" | "disconnected"
|
||||
// plugin.<name>.contributions — comma-separated contribution types
|
||||
// plugin.<name>.required — "true" | "false"
|
||||
// plugin.<name>.dependencies — comma-separated plugin names
|
||||
// plugin.<name>.subscriptions — comma-separated event names
|
||||
// plugin.<name>.emits — comma-separated event names
|
||||
// plugin.<name>.version — semver string
|
||||
// plugin.<name>.description — human-readable description
|
||||
// plugin.<name>.endpoint — delivery address (URL or IPC path)
|
||||
// plugins.registered — comma-separated list of known plugin names
|
||||
|
||||
// ── Registry helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
// plugin_names — list all registered plugin names.
|
||||
fn plugin_names() -> [String] {
|
||||
let raw: String = state_get("plugins.registered")
|
||||
if str_eq(raw, "") {
|
||||
return []
|
||||
}
|
||||
str_split(raw, ",")
|
||||
}
|
||||
|
||||
// plugin_register — add a plugin name to the registry.
|
||||
fn plugin_register(name: String) -> Void {
|
||||
let existing: String = state_get("plugins.registered")
|
||||
if str_eq(existing, "") {
|
||||
state_set("plugins.registered", name)
|
||||
} else {
|
||||
let names: [String] = str_split(existing, ",")
|
||||
let found: Bool = list_contains(names, name)
|
||||
if !found {
|
||||
state_set("plugins.registered", existing + "," + name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_set_status(name: String, status: String) -> Void {
|
||||
state_set("plugin." + name + ".status", status)
|
||||
}
|
||||
|
||||
fn plugin_get_status(name: String) -> String {
|
||||
state_get("plugin." + name + ".status")
|
||||
}
|
||||
|
||||
fn plugin_set_endpoint(name: String, endpoint: String) -> Void {
|
||||
state_set("plugin." + name + ".endpoint", endpoint)
|
||||
}
|
||||
|
||||
fn plugin_get_endpoint(name: String) -> String {
|
||||
state_get("plugin." + name + ".endpoint")
|
||||
}
|
||||
|
||||
fn plugin_set_subscriptions(name: String, events: [String]) -> Void {
|
||||
state_set("plugin." + name + ".subscriptions", str_join(events, ","))
|
||||
}
|
||||
|
||||
fn plugin_get_subscriptions(name: String) -> [String] {
|
||||
let raw: String = state_get("plugin." + name + ".subscriptions")
|
||||
if str_eq(raw, "") {
|
||||
return []
|
||||
}
|
||||
str_split(raw, ",")
|
||||
}
|
||||
|
||||
fn plugin_set_emits(name: String, events: [String]) -> Void {
|
||||
state_set("plugin." + name + ".emits", str_join(events, ","))
|
||||
}
|
||||
|
||||
// plugin_set_contributions — store the contribution types for a plugin.
|
||||
fn plugin_set_contributions(name: String, contributions: [String]) -> Void {
|
||||
state_set("plugin." + name + ".contributions", str_join(contributions, ","))
|
||||
}
|
||||
|
||||
// plugin_get_contributions — list of contribution types this plugin declares.
|
||||
fn plugin_get_contributions(name: String) -> [String] {
|
||||
let raw: String = state_get("plugin." + name + ".contributions")
|
||||
if str_eq(raw, "") {
|
||||
return []
|
||||
}
|
||||
str_split(raw, ",")
|
||||
}
|
||||
|
||||
// plugin_set_required — whether this plugin must be loaded at startup.
|
||||
fn plugin_set_required(name: String, required: Bool) -> Void {
|
||||
if required {
|
||||
state_set("plugin." + name + ".required", "true")
|
||||
} else {
|
||||
state_set("plugin." + name + ".required", "false")
|
||||
}
|
||||
}
|
||||
|
||||
// plugin_is_required — true if this plugin declared itself required.
|
||||
fn plugin_is_required(name: String) -> Bool {
|
||||
let raw: String = state_get("plugin." + name + ".required")
|
||||
str_eq(raw, "true")
|
||||
}
|
||||
|
||||
// plugin_set_dependencies — record declared plugin dependencies.
|
||||
fn plugin_set_dependencies(name: String, deps: [String]) -> Void {
|
||||
state_set("plugin." + name + ".dependencies", str_join(deps, ","))
|
||||
}
|
||||
|
||||
// plugin_get_dependencies — list of plugin names this plugin depends on.
|
||||
fn plugin_get_dependencies(name: String) -> [String] {
|
||||
let raw: String = state_get("plugin." + name + ".dependencies")
|
||||
if str_eq(raw, "") {
|
||||
return []
|
||||
}
|
||||
str_split(raw, ",")
|
||||
}
|
||||
|
||||
// ── Contribution queries ───────────────────────────────────────────────────────
|
||||
|
||||
// plugins_by_contribution — return active plugin names that declare a given
|
||||
// contribution type. Used by the marketplace to surface clean category lanes.
|
||||
fn plugins_by_contribution(contribution: String) -> [String] {
|
||||
let names: [String] = plugin_names()
|
||||
let result: [String] = []
|
||||
for name in names {
|
||||
let status: String = plugin_get_status(name)
|
||||
if str_eq(status, "active") {
|
||||
let contribs: [String] = plugin_get_contributions(name)
|
||||
if list_contains(contribs, contribution) {
|
||||
list_append(result, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// plugin_has_contribution — true if a specific plugin declares a contribution.
|
||||
fn plugin_has_contribution(name: String, contribution: String) -> Bool {
|
||||
let contribs: [String] = plugin_get_contributions(name)
|
||||
list_contains(contribs, contribution)
|
||||
}
|
||||
|
||||
// required_plugins — return all plugins that declared required: true.
|
||||
// Called at startup to verify all required plugins have announced.
|
||||
fn required_plugins() -> [String] {
|
||||
let names: [String] = plugin_names()
|
||||
let result: [String] = []
|
||||
for name in names {
|
||||
if plugin_is_required(name) {
|
||||
list_append(result, name)
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// ── Routing helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
// plugins_subscribed_to — return names of active plugins subscribed to an event.
|
||||
fn plugins_subscribed_to(event_name: String) -> [String] {
|
||||
let names: [String] = plugin_names()
|
||||
let result: [String] = []
|
||||
for name in names {
|
||||
let subs: [String] = plugin_get_subscriptions(name)
|
||||
let active: String = plugin_get_status(name)
|
||||
if str_eq(active, "active") {
|
||||
if list_contains(subs, event_name) {
|
||||
list_append(result, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// deliver_event — forward an event to a plugin's endpoint.
|
||||
fn deliver_event(plugin_name: String, event_name: String, payload: Map<String, Any>) -> Void {
|
||||
let endpoint: String = plugin_get_endpoint(plugin_name)
|
||||
if str_eq(endpoint, "") {
|
||||
println("[plugin-host] " + plugin_name + ": no endpoint, skipping delivery of " + event_name)
|
||||
return
|
||||
}
|
||||
let envelope: Map<String, Any> = {
|
||||
"event": event_name,
|
||||
"payload": payload,
|
||||
}
|
||||
let resp = native_http_post({
|
||||
"url": endpoint + "/event",
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"body": envelope,
|
||||
})
|
||||
if !resp.ok {
|
||||
println("[plugin-host] delivery failed: " + plugin_name + " event=" + event_name)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Interrogation ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn host_interrogate(plugin_name: String, endpoint: String) -> Void {
|
||||
println("[plugin-host] interrogating plugin: " + plugin_name)
|
||||
native_emit("plugin.interrogate", {"plugin": plugin_name, "endpoint": endpoint})
|
||||
let req_body: Map<String, Any> = {
|
||||
"event": "plugin.interrogate",
|
||||
"payload": {"plugin": plugin_name},
|
||||
}
|
||||
let resp = native_http_post({
|
||||
"url": endpoint + "/event",
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"body": req_body,
|
||||
})
|
||||
if !resp.ok {
|
||||
println("[plugin-host] interrogate delivery failed: " + plugin_name)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Event processing ──────────────────────────────────────────────────────────
|
||||
|
||||
fn host_handle_announce(payload: Map<String, Any>) -> Void {
|
||||
let name: String = payload["plugin"]
|
||||
let endpoint: String = payload["endpoint"]
|
||||
|
||||
if str_eq(name, "") {
|
||||
println("[plugin-host] announce missing plugin name, ignoring")
|
||||
return
|
||||
}
|
||||
|
||||
println("[plugin-host] plugin announced: " + name + " at " + endpoint)
|
||||
|
||||
plugin_register(name)
|
||||
plugin_set_status(name, "pending")
|
||||
plugin_set_endpoint(name, endpoint)
|
||||
|
||||
native_emit("plugin.status", {
|
||||
"plugin": name,
|
||||
"status": "pending",
|
||||
"reason": "announced",
|
||||
})
|
||||
|
||||
host_interrogate(name, endpoint)
|
||||
}
|
||||
|
||||
// host_handle_manifest — process a plugin.manifest event.
|
||||
//
|
||||
// Stores all declared capabilities. The contributions list IS the plugin's
|
||||
// type — no separate type field. required and dependencies drive startup
|
||||
// loading order.
|
||||
fn host_handle_manifest(payload: Map<String, Any>) -> Void {
|
||||
let name: String = payload["plugin"]
|
||||
let contributions: [String] = payload["contributions"]
|
||||
let subscriptions: [String] = payload["subscriptions"]
|
||||
let emits: [String] = payload["emits"]
|
||||
let required: Bool = payload["required"]
|
||||
let dependencies: [String] = payload["dependencies"]
|
||||
|
||||
if str_eq(name, "") {
|
||||
println("[plugin-host] manifest missing plugin name, ignoring")
|
||||
return
|
||||
}
|
||||
|
||||
let current_status: String = plugin_get_status(name)
|
||||
if str_eq(current_status, "") {
|
||||
plugin_register(name)
|
||||
}
|
||||
|
||||
plugin_set_contributions(name, contributions)
|
||||
plugin_set_subscriptions(name, subscriptions)
|
||||
plugin_set_emits(name, emits)
|
||||
plugin_set_required(name, required)
|
||||
plugin_set_dependencies(name, dependencies)
|
||||
plugin_set_status(name, "active")
|
||||
|
||||
println("[plugin-host] plugin active: " + name
|
||||
+ " contributions=" + str_join(contributions, ",")
|
||||
+ " subs=" + str_join(subscriptions, ","))
|
||||
|
||||
native_emit("plugin.status", {
|
||||
"plugin": name,
|
||||
"status": "active",
|
||||
"contributions": str_join(contributions, ","),
|
||||
"subscriptions": str_join(subscriptions, ","),
|
||||
"emits": str_join(emits, ","),
|
||||
"required": required,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Event routing ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn host_route_event(event_name: String, payload: Map<String, Any>) -> Void {
|
||||
let recipients: [String] = plugins_subscribed_to(event_name)
|
||||
for plugin_name in recipients {
|
||||
deliver_event(plugin_name, event_name, payload)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Host tick ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@manager
|
||||
fn host_tick(params: Map<String, Any>) -> Result<Map<String, Any>, NeuronError> {
|
||||
let events: [Map<String, Any>] = native_drain_events()
|
||||
let processed: Int = 0
|
||||
|
||||
for event in events {
|
||||
let event_name: String = event["event"]
|
||||
let payload: Map<String, Any> = event["payload"]
|
||||
|
||||
if str_eq(event_name, "plugin.announce") {
|
||||
host_handle_announce(payload)
|
||||
} else {
|
||||
if str_eq(event_name, "plugin.manifest") {
|
||||
host_handle_manifest(payload)
|
||||
} else {
|
||||
host_route_event(event_name, payload)
|
||||
}
|
||||
}
|
||||
let processed = processed + 1
|
||||
}
|
||||
|
||||
Ok({"status": "ok", "processed": int_to_str(processed)})
|
||||
}
|
||||
|
||||
// ── Status introspection ──────────────────────────────────────────────────────
|
||||
|
||||
// host_status — JSON snapshot of the plugin registry.
|
||||
fn host_status() -> String {
|
||||
let names: [String] = plugin_names()
|
||||
let parts: String = "{"
|
||||
let first: Bool = true
|
||||
|
||||
for name in names {
|
||||
let status: String = plugin_get_status(name)
|
||||
let endpoint: String = plugin_get_endpoint(name)
|
||||
let contribs: String = state_get("plugin." + name + ".contributions")
|
||||
let subs: String = state_get("plugin." + name + ".subscriptions")
|
||||
let emits_raw: String = state_get("plugin." + name + ".emits")
|
||||
let required: String = state_get("plugin." + name + ".required")
|
||||
|
||||
let entry: String = "\"" + name + "\":{\"status\":\"" + status
|
||||
+ "\",\"contributions\":\"" + contribs
|
||||
+ "\",\"endpoint\":\"" + endpoint
|
||||
+ "\",\"subscriptions\":\"" + subs
|
||||
+ "\",\"emits\":\"" + emits_raw
|
||||
+ "\",\"required\":\"" + required + "\"}"
|
||||
|
||||
if first {
|
||||
let parts = parts + entry
|
||||
let first = false
|
||||
} else {
|
||||
let parts = parts + "," + entry
|
||||
}
|
||||
}
|
||||
|
||||
parts + "}"
|
||||
}
|
||||
|
||||
// ── Startup ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn host_on_startup() -> Void {
|
||||
state_set("plugins.registered", "")
|
||||
println("[plugin-host] ready — waiting for plugin.announce events")
|
||||
native_emit("plugin.host_ready", {})
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// process.el — Process definitions subsystem.
|
||||
//
|
||||
// Processes encode proven workflows as executable, named procedures.
|
||||
// The difference between a process and a document is executability:
|
||||
// a document describes what to do; a process can be invoked with
|
||||
// begin_work(process_name="...") and tracked step by step.
|
||||
//
|
||||
// WHY processes?
|
||||
// Institutional knowledge decays when it only lives in conversations.
|
||||
// A process that has been run 10 times and refined each time is far more
|
||||
// valuable than a one-off plan. The process library is Neuron's
|
||||
// "muscle memory" — established ways of doing things that don't need to
|
||||
// be reinvented each session.
|
||||
//
|
||||
// Process discovery is @public because it's discovery-oriented — callers
|
||||
// should be able to find what processes exist without authentication.
|
||||
// Process mutation (define, delete) is @manager — these changes affect
|
||||
// all future sessions.
|
||||
|
||||
from types import {
|
||||
Process,
|
||||
ProcessStep,
|
||||
Context,
|
||||
NeuronError,
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
// define_process — register a new named workflow.
|
||||
//
|
||||
// Each step has a name, instructions (markdown prose), and a list of tools
|
||||
// that should be called during that step. The tools list is advisory — it
|
||||
// helps Claude orient to what's expected at each step.
|
||||
@manager
|
||||
fn define_process(
|
||||
name: String,
|
||||
description: String,
|
||||
steps: [ProcessStep],
|
||||
) -> Result<Process, NeuronError> {
|
||||
let id = native_uuid()
|
||||
let now = native_now()
|
||||
let process = Process {
|
||||
id: id,
|
||||
name: name,
|
||||
description: description,
|
||||
steps: steps,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
// Processes are stored as Knowledge nodes with category="process" so they
|
||||
// participate in semantic search alongside other reference material.
|
||||
native_emit("process.defined", {"id": id, "name": name})
|
||||
Ok(process)
|
||||
}
|
||||
|
||||
// browse_processes — list or get detail on named processes.
|
||||
//
|
||||
// When name is empty, returns a summary list of all registered processes.
|
||||
// When name is provided, returns the full process with all steps.
|
||||
// This is @public to encourage process discovery.
|
||||
@public
|
||||
fn browse_processes(name: String) -> Result<[Process], NeuronError> {
|
||||
if name == "" {
|
||||
let results = activate Process where "all processes"
|
||||
return Ok(results)
|
||||
}
|
||||
// Semantic search by name to handle fuzzy matching ("pr review" → "pull_request_review").
|
||||
let results = activate Process where "name:{name}"
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
// execute_process — begin a tracked execution of a named process.
|
||||
//
|
||||
// This is the bridge between process definitions and execution contexts.
|
||||
// It looks up the process by name, opens a new execution context with
|
||||
// the process name, and returns the context so the caller can track progress.
|
||||
//
|
||||
// The caller is expected to call progress_work() for each step in the process.
|
||||
@manager
|
||||
fn execute_process(name: String, project: String) -> Result<Context, NeuronError> {
|
||||
// Find the process definition.
|
||||
let processes = activate Process where "name:{name}"
|
||||
let process = processes[0]
|
||||
// Open a context using begin_work from context.el.
|
||||
// In practice, the Axon dispatcher will call begin_work() directly
|
||||
// with the process name after execute_process resolves the name.
|
||||
let id = native_uuid()
|
||||
let now = native_now()
|
||||
let ctx = Context {
|
||||
id: id,
|
||||
process_name: process.name,
|
||||
description: process.description,
|
||||
objective: "execute process: " + name,
|
||||
project: project,
|
||||
status: "active",
|
||||
steps: [],
|
||||
file_refs: [],
|
||||
key_decisions: [],
|
||||
lessons_learned: [],
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
native_store_context(ctx)?
|
||||
native_emit("process.execution_started", {"process": name, "context_id": id, "project": project})
|
||||
Ok(ctx)
|
||||
}
|
||||
|
||||
// list_processes — enumerate all registered processes.
|
||||
//
|
||||
// Alias for browse_processes("") that matches the Axon tool name convention.
|
||||
@public
|
||||
fn list_processes() -> Result<[Process], NeuronError> {
|
||||
browse_processes("")
|
||||
}
|
||||
|
||||
// delete_process — remove a process definition.
|
||||
//
|
||||
// Use sparingly — prefer evolving (redefine with improvements) over deleting.
|
||||
// Deleting a process that has active executions will leave those contexts
|
||||
// without a parent process definition.
|
||||
@manager
|
||||
fn delete_process(name: String) -> Result<String, NeuronError> {
|
||||
let processes = activate Process where "name:{name}"
|
||||
let process = processes[0]
|
||||
native_emit("process.deleted", {"name": name, "id": process.id})
|
||||
Ok("process deleted: " + name)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// proxy.el — HTTP proxy helpers.
|
||||
|
||||
import "daemon_config.el"
|
||||
|
||||
fn proxy_get(target_base: String, path: String, token: String) -> String {
|
||||
let url: String = target_base + path
|
||||
return http_get_auth(url, token)
|
||||
}
|
||||
|
||||
fn proxy_post(target_base: String, path: String, body: String, token: String) -> String {
|
||||
let url: String = target_base + path
|
||||
return http_post_auth(url, token, body)
|
||||
}
|
||||
|
||||
fn proxy_request(target_base: String, method: String, path: String, body: String, token: String) -> String {
|
||||
if str_eq(method, "GET") {
|
||||
return proxy_get(target_base, path, token)
|
||||
}
|
||||
if str_eq(method, "POST") {
|
||||
return proxy_post(target_base, path, body, token)
|
||||
}
|
||||
if str_eq(method, "PUT") {
|
||||
let url: String = target_base + path
|
||||
return http_put_auth(url, token, body)
|
||||
}
|
||||
if str_eq(method, "DELETE") {
|
||||
let url: String = target_base + path
|
||||
return http_delete_auth(url, token)
|
||||
}
|
||||
return "{\"error\":\"unsupported method\"}"
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// types.el — Shared type definitions for the Neuron intelligence layer.
|
||||
//
|
||||
// This file is the single source of truth for all domain types. Every
|
||||
// subsystem imports from here. Types mirror the Rust domain types in
|
||||
// neuron-domain/src/types.rs but are expressed in el so the
|
||||
// runtime can reason over them as first-class graph entities.
|
||||
//
|
||||
// WHY el types instead of just Rust structs?
|
||||
// The engram runtime needs to know the shape of each entity so it can
|
||||
// build the spreading-activation index. Types declared here become graph
|
||||
// schema — not just data holders.
|
||||
|
||||
// ── Core identifiers ──────────────────────────────────────────────────────────
|
||||
|
||||
// Uuid and String are primitive types provided by the engram runtime.
|
||||
// We alias them here for readability in field annotations.
|
||||
|
||||
// ── Importance tier ───────────────────────────────────────────────────────────
|
||||
|
||||
// Importance drives how long a memory survives before the graph compacts it
|
||||
// and how aggressively it participates in semantic activation.
|
||||
enum Importance {
|
||||
Low,
|
||||
Normal,
|
||||
High,
|
||||
Critical,
|
||||
}
|
||||
|
||||
// ── Knowledge tier ────────────────────────────────────────────────────────────
|
||||
|
||||
// Tiers encode epistemological confidence. Never skip tiers:
|
||||
// note → lesson (proven ≥2 times) → canonical (stable, referenced widely).
|
||||
enum KnowledgeTier {
|
||||
Note,
|
||||
Lesson,
|
||||
Canonical,
|
||||
}
|
||||
|
||||
// ── Backlog types ─────────────────────────────────────────────────────────────
|
||||
|
||||
enum ItemType {
|
||||
Feature,
|
||||
Bug,
|
||||
Task,
|
||||
Chore,
|
||||
}
|
||||
|
||||
// Priority P0 = ship-blocker, P3 = nice-to-have someday.
|
||||
enum Priority {
|
||||
P0,
|
||||
P1,
|
||||
P2,
|
||||
P3,
|
||||
}
|
||||
|
||||
// Status machine: draft → ready → in_progress → done | blocked | cancelled
|
||||
enum BacklogStatus {
|
||||
Draft,
|
||||
Ready,
|
||||
InProgress,
|
||||
Done,
|
||||
Blocked,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
// ── Context status ────────────────────────────────────────────────────────────
|
||||
|
||||
enum ContextStatus {
|
||||
Active,
|
||||
Completed,
|
||||
Archived,
|
||||
}
|
||||
|
||||
// ── Gap direction (ISE) ───────────────────────────────────────────────────────
|
||||
|
||||
// Tracks whether Neuron's reasoning moved toward expression or suppression
|
||||
// relative to its pre-reasoning state. Used for introspection and calibration.
|
||||
enum GapDirection {
|
||||
TowardExpression,
|
||||
TowardSuppression,
|
||||
Neutral,
|
||||
}
|
||||
|
||||
// ── Error variants ────────────────────────────────────────────────────────────
|
||||
|
||||
enum NeuronError {
|
||||
NotFound(String),
|
||||
StorageError(String),
|
||||
InvalidInput(String),
|
||||
Unauthorized(String),
|
||||
ConflictError(String),
|
||||
}
|
||||
|
||||
// ── Domain types ──────────────────────────────────────────────────────────────
|
||||
|
||||
// Memory — an atomic piece of observed knowledge.
|
||||
// supersedes_id links to the memory this one replaces, enabling memory chains.
|
||||
type Memory {
|
||||
id: String,
|
||||
content: String,
|
||||
tags: [String],
|
||||
importance: String,
|
||||
project: String,
|
||||
supersedes_id: String?,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
// Knowledge — stable reference material. Lives longer than memories.
|
||||
// key is the path-style identifier: "architecture/vbd/fundamentals.md"
|
||||
type Knowledge {
|
||||
id: String,
|
||||
key: String,
|
||||
title: String,
|
||||
content: String,
|
||||
category: String,
|
||||
tier: String,
|
||||
tags: [String],
|
||||
project: String,
|
||||
created_at: String,
|
||||
}
|
||||
|
||||
// BacklogItem — a unit of tracked work.
|
||||
// depends_on is a list of item IDs that must complete before this one starts.
|
||||
type BacklogItem {
|
||||
id: String,
|
||||
title: String,
|
||||
description: String,
|
||||
item_type: String,
|
||||
priority: String,
|
||||
status: String,
|
||||
project: String,
|
||||
tags: [String],
|
||||
depends_on: [String],
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
// ContextStep — one recorded step inside an ExecutionContext.
|
||||
// file_refs and key_decisions are captured so future sessions can replay
|
||||
// the reasoning without replaying the entire conversation.
|
||||
type ContextStep {
|
||||
action: String,
|
||||
status: String,
|
||||
timestamp: String,
|
||||
file_refs: [String],
|
||||
key_decisions: [String],
|
||||
notes: String,
|
||||
}
|
||||
|
||||
// Context — tracks a multi-step execution in progress.
|
||||
// This is what begin_work() / progress_work() operate on.
|
||||
type Context {
|
||||
id: String,
|
||||
process_name: String,
|
||||
description: String,
|
||||
objective: String,
|
||||
project: String,
|
||||
status: String,
|
||||
steps: [ContextStep],
|
||||
file_refs: [String],
|
||||
key_decisions: [String],
|
||||
lessons_learned: [String],
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
// Artifact — a versioned deliverable: plan, spec, report, design doc.
|
||||
// version increments on every revise_artifact() call.
|
||||
type Artifact {
|
||||
id: String,
|
||||
title: String,
|
||||
content: String,
|
||||
artifact_type: String,
|
||||
status: String,
|
||||
project: String,
|
||||
version: Int,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
// InternalStateEvent — Neuron's introspective record.
|
||||
// Captures the gap between pre-reasoning and post-reasoning responses
|
||||
// so Neuron can audit its own calibration over time.
|
||||
type InternalStateEvent {
|
||||
id: String,
|
||||
trigger: String,
|
||||
pre_reasoning: String,
|
||||
post_reasoning: String,
|
||||
compression_ratio: String,
|
||||
gap_direction: String,
|
||||
tags: [String],
|
||||
logged_at: String,
|
||||
}
|
||||
|
||||
// ConfigEntry — a runtime configuration key/value pair.
|
||||
// Config changes are sealed operations — they affect Neuron's behavior globally.
|
||||
type ConfigEntry {
|
||||
key: String,
|
||||
value: String,
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
// GraphNode — a node returned by graph inspection or traversal.
|
||||
// edges is a list of "relation:target_id" strings for human readability.
|
||||
type GraphNode {
|
||||
entity_type: String,
|
||||
entity_id: String,
|
||||
label: String,
|
||||
edges: [String],
|
||||
}
|
||||
|
||||
// SearchResult — returned by semantic (activate) queries.
|
||||
// score is a float in [0.0, 1.0] representing similarity to the query.
|
||||
type SearchResult {
|
||||
id: String,
|
||||
content: String,
|
||||
score: String,
|
||||
node_type: String,
|
||||
}
|
||||
|
||||
// AxonToolCall — an incoming tool invocation from the Axon protocol.
|
||||
type AxonToolCall {
|
||||
id: String,
|
||||
tool_name: String,
|
||||
params: Map<String, String>,
|
||||
}
|
||||
|
||||
// AxonToolResult — the response envelope for an Axon tool call.
|
||||
type AxonToolResult {
|
||||
id: String,
|
||||
call_id: String,
|
||||
result: Map<String, String>,
|
||||
error: String?,
|
||||
}
|
||||
|
||||
// ProcessStep — one step inside a named Process workflow.
|
||||
type ProcessStep {
|
||||
name: String,
|
||||
instructions: String,
|
||||
tools: [String],
|
||||
}
|
||||
|
||||
// Process — a named, reusable workflow pattern.
|
||||
// Registering processes with define_process() makes institutional knowledge
|
||||
// executable — not just documented.
|
||||
type Process {
|
||||
id: String,
|
||||
name: String,
|
||||
description: String,
|
||||
steps: [ProcessStep],
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
Reference in New Issue
Block a user