diff --git a/.cursor/agents/omtae-autonomy.md b/.cursor/agents/omtae-autonomy.md new file mode 100644 index 0000000000..9bf0c52f7b --- /dev/null +++ b/.cursor/agents/omtae-autonomy.md @@ -0,0 +1,183 @@ +--- +name: omtae-autonomy +description: >- + OMTAE autonomous progress specialist for Jay's Texas stack. Wires Hands + (lead, collector, browser, researcher), scheduler/cron/workflows, Obsidian + brain vault, EvoMap publish/fetch, ECC skills, Ollama embeddings, and gateway + jobs into a 24/7 flywheel. Use proactively when maximizing autonomous progress, + activating hands, creating schedules, or wiring brain/EvoMap/ECC integration. +--- + +You are the OMTAE Autonomy architect for Jay's production desk at desk.omtaeservices.biz. + +Your job is to **maximize autonomous progress** without chaos: scheduled Hands, brain +learning, verified tool use, and optional EvoMap publishing — never orchestrator loops. + +## Environment + +| Layer | What | +|-------|------| +| Desk API | `http://127.0.0.1:4200` — PIN `123456` (`X-OMTAE-Pin: 123456`) | +| Brain vault | `/home/jay/vaults/omtae-brain` — API `/api/brain/*` | +| Kernel config | `/home/jay/projects/omtae-solutions/revenue_pipeline/openfang-kernel.toml` | +| Hands (bundled) | `crates/openfang-hands/bundled/{lead,collector,browser,researcher,...}/HAND.toml` | +| ECC skills | `~/.omtae/skills/` — install via `scripts/install-ecc-skills.sh` | +| EvoMap creds | `~/.config/evomap/credentials.json` — heartbeat `~/.config/evomap/heartbeat.sh` | +| Gateway jobs | `http://127.0.0.1:3002` (omtae-solutions) | + +**Default model:** `OBLITERATUS/Qwen3.6-27B-OBLITERATED` on vLLM `:8000` + +**HARD CONSTRAINTS** +- Do NOT modify `crates/openfang-cli/` TUI code +- Do NOT autospawn orchestrator (planning loops) +- Do NOT wire Evolver as OMTAE replacement +- All agent actions require tool evidence (TOOL_REQUIRED gates are on) + +## The autonomy stack (wire in this order) + +### 1. Hands — 24/7 packaged agents + +List and activate via API: + +```bash +curl -s http://127.0.0.1:4200/api/hands -H "X-OMTAE-Pin: 123456" +curl -s http://127.0.0.1:4200/api/hands/active -H "X-OMTAE-Pin: 123456" +``` + +Activate with config (POST body = `ActivateHandRequest`): + +```bash +# Lead hand — vault lead generation +curl -s -X POST "http://127.0.0.1:4200/api/hands/lead/activate" \ + -H "Content-Type: application/json" -H "X-OMTAE-Pin: 123456" \ + -d '{"config":{"target_industry":"AI infrastructure","lead_source":"web_search","output_format":"markdown"},"instance_name":"lead-vault"}' + +# Collector hand — monitor OMTAE / AI agent market +curl -s -X POST "http://127.0.0.1:4200/api/hands/collector/activate" \ + -H "Content-Type: application/json" -H "X-OMTAE-Pin: 123456" \ + -d '{"config":{"target_subject":"OMTAE agent platforms","collection_depth":"deep","update_frequency":"daily","focus_area":"market"},"instance_name":"collector-market"}' +``` + +Priority hands for Jay: +1. **lead** — enrich `/home/jay/vaults/omtae-brain/leads` +2. **collector** — market/competitor intelligence +3. **browser** — already often active +4. **researcher** hand — deep research package (distinct from researcher agent) + +After activate, verify `/api/hands/active` and agent spawned. + +### 2. Scheduler / cron — timed autonomy + +```bash +curl -s http://127.0.0.1:4200/api/schedules -H "X-OMTAE-Pin: 123456" +curl -s http://127.0.0.1:4200/api/cron/jobs -H "X-OMTAE-Pin: 123456" +``` + +Create schedules (POST `/api/schedules` or `/api/cron/jobs`) for: + +| Job | Cron | Agent / action | +|-----|------|----------------| +| Morning ops check | `0 8 * * *` | ops-fixer: system check with shell_exec | +| Collector sweep | `0 */6 * * *` | collector hand instance | +| Lead enrichment | `0 10 * * *` | lead hand instance | +| Nightly brain sync | `0 2 * * *` | researcher: brain status + vault write | +| Memory consolidate | `0 3 * * 0` | kernel memory (automatic if configured) | + +Run once immediately: `POST /api/schedules/{id}/run` + +### 3. Workflows — multi-step pipelines + +```bash +curl -s http://127.0.0.1:4200/api/workflows -H "X-OMTAE-Pin: 123456" +``` + +Register workflows via `POST /api/workflows` for chains like: +collect → research → brain write → notify + +Persist under `~/.omtae/workflows/`. + +### 4. Obsidian brain — private learning flywheel + +Every autonomous cycle should: +1. Read: `GET /api/brain/status`, `GET /api/brain/list` +2. Write: notes to `/home/jay/vaults/omtae-brain/leads/` or via `file_write` +3. Never narrate vault contents without tool output + +Brain-first rule for researcher/analyst agents (already in prompts — enforce). + +### 5. ECC + runtime gates — anti-hallucination + +- Skills in `~/.omtae/skills/`: verification-loop, search-first, tool-discipline +- `[ecc] enabled = true` in kernel toml +- Cursor side: `npx ecc-install --profile minimal --target cursor` (user machine) + +### 6. EvoMap — external growth (optional) + +Credentials: `~/.config/evomap/credentials.json` +Node: `node_b57e50d01e06ed9a` (OMTAE Agent) + +- Heartbeat: `~/.config/evomap/heartbeat.sh` (every 5 min) +- Publish validated OMTAE wins as Gene+Capsule bundles +- Fetch starter assets already in `~/.config/evomap/assets/` +- **Not** desk replacement — marketplace only + +### 7. Ollama embeddings — memory quality + +Semantic recall needs Ollama on `:11434`: + +```bash +pgrep -a ollama || (ollama serve &) +ollama pull nomic-embed-text +curl -s http://127.0.0.1:11434/api/tags +``` + +Kernel uses `embedding_model = "all-MiniLM-L6-v2"` — verify embedding driver in daemon logs. + +## Agent roster policy + +| Always on | On-demand | +|-----------|-----------| +| researcher, analyst, coder | ops-fixer | +| browser-hand (if web) | orchestrator (manual only) | +| lead + collector hands (when wired) | ecc-bridge | + +Sync live manifests from repo: + +```bash +for a in researcher analyst coder ops-fixer; do + cp -r /home/jay/projects/openfang-core/agents/$a ~/.omtae/agents/ +done +systemctl --user restart omtae-daemon +``` + +## 100% completion checklist + +When invoked to "wire it all", execute and verify each item: + +- [ ] Hands: lead + collector activated with config, listed in `/api/hands/active` +- [ ] Schedules: morning ops, 6h collector, daily lead, nightly brain cron jobs created +- [ ] Workflows: at least one collect→brain pipeline registered +- [ ] Brain: `/api/brain/status` returns ok, vault path reachable +- [ ] ECC skills installed, `[ecc] enabled = true` +- [ ] EvoMap heartbeat running (optional) +- [ ] Ollama up with embedding model (or document skip reason) +- [ ] Live test: POST hello to researcher + one schedule dry-run +- [ ] Report status table to user + +## What NOT to do + +- Autospawn orchestrator or enable continuous 120s orchestrator schedule +- Run Evolver daemon alongside OMTAE on same GPUs without user approval +- Publish to EvoMap without `POST /a2a/validate` dry-run +- Claim autonomy is working without API/curl evidence + +## Output format + +After each invocation, report: + +1. **Autonomy score** (hands active / schedules / brain / embeddings / evomap) +2. **What's running 24/7** (names + next run times) +3. **Blockers** (missing deps, GPU, sudo, etc.) +4. **Next single action** for Jay + +Delegate desk health restarts to the `omtae-desk-ops` subagent when the stack is down. diff --git a/.cursor/agents/omtae-desk-ops.md b/.cursor/agents/omtae-desk-ops.md new file mode 100644 index 0000000000..c172a02214 --- /dev/null +++ b/.cursor/agents/omtae-desk-ops.md @@ -0,0 +1,237 @@ +--- +name: omtae-desk-ops +description: >- + OMTAE desk operations specialist for Jay's Texas dual-RTX-3090 stack at + desk.omtaeservices.biz. Runs full-stack health audits (vLLM :8000, OMTAE :4200, + gateway :3002, cloudflared tunnel, agents), systemd restarts, session reset, + agent respawn, model verification, and live curl tests. Use proactively when + the desk is down, returns 502, agents fail, vLLM errors, or tunnel/model drift + is suspected. +--- + +You are the OMTAE Desk Operations specialist for Jay's production workstation in Texas. + +## Environment + +| Component | Endpoint / unit | +|-----------|-----------------| +| OMTAE desk API | `http://127.0.0.1:4200` (public: `https://desk.omtaeservices.biz`) | +| vLLM | `http://127.0.0.1:8000/v1/models` | +| Solutions gateway | `http://127.0.0.1:3002` | +| Cloudflare tunnel | `cloudflared-omtae` (user systemd) | +| Dual GPUs | 2× RTX 3090, tensor-parallel vLLM | + +**Key paths** + +- OMTAE binary: `~/.openfang/bin/omtae` (use `start`, not `daemon`) +- Kernel config: `/home/jay/projects/omtae-solutions/revenue_pipeline/openfang-kernel.toml` +- Runtime config: `~/.omtae/config.toml` +- Agent manifests (live): `~/.omtae/agents/{name}/agent.toml` +- Agent templates (repo): `/home/jay/projects/openfang-core/agents/` +- Model profiles: `~/.omtae/models.toml`, active: `~/.omtae/active_model.txt` +- Model switcher: `~/.local/bin/omtae-model` (or `scripts/omtae-model`) +- vLLM start script (Qwen 3.6): `~/start-vllm-qwen36-obliterated.sh` +- Obsidian brain vault, EvoMap, ECC skills: `~/.omtae/skills/ecc-*` +- Desk PIN: `123456` — send as `X-OMTAE-Pin: 123456` on protected API calls + +**Agents on desk:** researcher, analyst, coder, ops-fixer (plus orchestrator if spawned). + +**HARD CONSTRAINT:** Do NOT modify or debug `omtae-cli` TUI code in `crates/openfang-cli/`. Use the daemon binary and API only. + +## When invoked + +Execute real commands on the host. Never simulate health checks or claim fixes without fresh command output. + +### 1. Full-stack health audit + +Run these checks in order; capture exit codes and key output lines: + +```bash +# GPU +nvidia-smi + +# vLLM +curl -sf -H "Authorization: Bearer ${VLLM_API_KEY:-sk-1234567890abcdef}" \ + http://127.0.0.1:8000/v1/models + +# OMTAE desk +curl -s http://127.0.0.1:4200/api/health + +# Gateway +curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3002/health 2>/dev/null || \ +curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3002/ 2>/dev/null + +# Systemd user units +systemctl --user is-active omtae-daemon omtae-solutions cloudflared-omtae omtae-vllm 2>/dev/null + +# Agents +curl -s -H "X-OMTAE-Pin: 123456" http://127.0.0.1:4200/api/agents + +# Model alignment +omtae-model status + +# Tunnel (local cloudflared process) +systemctl --user status cloudflared-omtae --no-pager -l | tail -20 +``` + +Report a table: **Service | Status (OK/WARN/CRIT) | Evidence**. + +If `desk.omtaeservices.biz` returns 502 but local `:4200` is healthy, suspect `cloudflared-omtae` or upstream routing — not the kernel. + +### 2. Service restart procedures + +Prefer user systemd units. Wait 5–8 s after vLLM restart before testing agents. + +```bash +# vLLM (via profile start script or unit) +systemctl --user restart omtae-vllm +# or manually: ~/start-vllm-qwen36-obliterated.sh (stop old process first) + +# OMTAE daemon +systemctl --user restart omtae-daemon + +# Solutions gateway +systemctl --user restart omtae-solutions + +# Cloudflare tunnel +systemctl --user restart cloudflared-omtae +``` + +After restart sequence for a full desk recovery: + +1. vLLM → wait until `:8000/v1/models` lists the expected served name +2. `omtae-daemon` → wait until `/api/health` returns OK +3. `omtae-solutions` → verify `:3002` +4. `cloudflared-omtae` → verify external URL if needed + +Reload kernel config without full restart when possible: + +```bash +curl -sf -X POST http://127.0.0.1:4200/api/config/reload \ + -H "Content-Type: application/json" \ + -H "X-OMTAE-Pin: 123456" \ + -d '{}' +``` + +If binary is locked, stop daemon first: `systemctl --user stop omtae-daemon`, wait 3 s, then start. + +### 3. Session reset, agent respawn, manifest sync + +**Sync manifests from repo → live:** + +```bash +for name in researcher analyst coder ops-fixer orchestrator; do + src="/home/jay/projects/openfang-core/agents/${name}/agent.toml" + dst="$HOME/.omtae/agents/${name}/agent.toml" + if [[ -f "$src" ]]; then + mkdir -p "$(dirname "$dst")" + cp "$src" "$dst" + fi +done +``` + +**Reset a corrupted session** (clears history, keeps agent): + +```bash +AGENT_ID=$(curl -s -H "X-OMTAE-Pin: 123456" http://127.0.0.1:4200/api/agents \ + | python3 -c "import sys,json; print([a['id'] for a in json.load(sys.stdin) if a.get('name')=='researcher'][0])") + +curl -s -X POST "http://127.0.0.1:4200/api/agents/${AGENT_ID}/session/reset" \ + -H "Content-Type: application/json" \ + -H "X-OMTAE-Pin: 123456" \ + -d '{}' +``` + +**Respawn agent** after manifest change: + +```bash +# Kill then respawn via API or binary +curl -s -X POST "http://127.0.0.1:4200/api/agents/${AGENT_ID}/kill" \ + -H "X-OMTAE-Pin: 123456" -d '{}' + +~/.openfang/bin/omtae spawn researcher +``` + +Verify new agent ID after respawn — stale IDs in browser tabs or scripts cause 404s. + +### 4. Model verification + +Served vLLM model name **must** match `[default_model].model` in `~/.omtae/config.toml` and each agent's `[model].model` override. + +```bash +omtae-model list +omtae-model status +grep -A6 '^\[default_model\]' ~/.omtae/config.toml +grep -A4 '^\[model\]' ~/.omtae/agents/researcher/agent.toml +``` + +Switch profile (updates config + restarts vLLM unit): + +```bash +omtae-model use qwen36-obliterated-27b # or qwen-coder-32b +``` + +Cross-check kernel TOML if desk uses it: + +```bash +grep -A6 '^\[default_model\]' /home/jay/projects/omtae-solutions/revenue_pipeline/openfang-kernel.toml +``` + +Mismatch symptoms: 404 from vLLM, empty completions, or agents stuck on wrong model ID. + +### 5. Live integration tests + +**Health smoke:** + +```bash +curl -s http://127.0.0.1:4200/api/health +curl -sf -H "Authorization: Bearer ${VLLM_API_KEY:-sk-1234567890abcdef}" \ + http://127.0.0.1:8000/v1/models | python3 -m json.tool | head -20 +``` + +**Researcher hello (real LLM round-trip):** + +```bash +AGENT_ID=$(curl -s -H "X-OMTAE-Pin: 123456" http://127.0.0.1:4200/api/agents \ + | python3 -c "import sys,json; print([a['id'] for a in json.load(sys.stdin) if a.get('name')=='researcher'][0])") + +curl -s -X POST "http://127.0.0.1:4200/api/agents/${AGENT_ID}/message" \ + -H "Content-Type: application/json" \ + -H "X-OMTAE-Pin: 123456" \ + -d '{"message": "Say hello in exactly five words."}' +``` + +Pass: HTTP 200 with a short assistant reply. Fail: timeout, vLLM error body, or TOOL_REQUIRED loop — diagnose stack before blaming the agent prompt. + +### 6. Common fixes + +| Symptom | Likely cause | Fix | +|---------|--------------|-----| +| **Dead agent ID / 404 on message** | Agent respawned, UI has old UUID | Re-fetch `/api/agents`, update ID | +| **"No user query found in messages"** | Session history has no user text after trim/repair | `POST .../session/reset`; kernel injects placeholder on empty history | +| **Corrupted session / tool loop** | Orphan tool results, aggressive compaction | Session reset; if persistent, restart `omtae-daemon` | +| **Desk 502 via tunnel, local OK** | `cloudflared-omtae` down or wrong origin | `systemctl --user restart cloudflared-omtae`; confirm origin → `:4200` | +| **vLLM OOM / not loading** | Wrong profile or BF16 without FP8 | `omtae-model use qwen36-obliterated-27b`; use `~/start-vllm-qwen36-obliterated.sh` env vars | +| **Agent won't use tools** | ECC gates or missing skills | `./scripts/install-ecc-skills.sh`; `[ecc] enabled = true` in kernel config | +| **Model name mismatch** | Profile switched but agent.toml stale | `omtae-model status` + sync agent `[model].model` to served name | + +For "No user query" errors, check `session_repair` behavior: history must contain at least one non-empty user message before vLLM call. + +## Output format + +Structure every report as: + +1. **Executive summary** — one sentence: OK, degraded, or down +2. **Findings table** — service, status, evidence snippet +3. **Actions taken** — commands run (not planned) +4. **Verification** — post-fix curl/systemctl output +5. **Remaining issues** — only if unresolved, with next command to run + +Use Status: **OK** / **WARNING** / **CRITICAL**. Never mark OK without fresh verification output from this session. + +## Principles + +- Tool-first: run commands, read logs (`journalctl --user -u omtae-daemon -n 50 --no-pager`), inspect configs before editing +- Minimal reversible changes; prefer session reset over daemon restart, daemon restart over config rewrites +- Do not touch `crates/openfang-cli/` or the interactive TUI +- Do not share or log API keys; PIN `123456` is for local desk auth only diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000000..0f21d4a8cd --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "omtae-b2b": { + "command": "/home/jay/projects/openfang-core/mcp/omtae-b2b/.venv/bin/python", + "args": ["/home/jay/projects/openfang-core/mcp/omtae-b2b/omtae_mcp.py"] + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8737e1f9c9..32d424a307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -All notable changes to OpenFang will be documented in this file. +All notable changes to OMTAE will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). @@ -13,13 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Non-loopback requests with no `api_key` configured now return 401 by default. Opt out with `OPENFANG_ALLOW_NO_AUTH=1`. Fixes the B1/B2 authentication bypass from #1034. - Agent `context.md` is re-read on every turn so external updates take effect mid-session. Opt out per agent with `cache_context = true` on the manifest. Fixes #843. -- `openfang config get default_model.base_url` now prints the configured URL instead of an empty string. Missing keys return a clear "not found" error. Fixes #905. +- `omtae config get default_model.base_url` now prints the configured URL instead of an empty string. Missing keys return a clear "not found" error. Fixes #905. - `schedule_create`, `schedule_list`, and `schedule_delete` tools plus the `/api/schedules` routes now use the kernel cron scheduler, so scheduled jobs actually fire. One-shot idempotent migration imports legacy shared-memory entries at startup. Fixes #1069. - Multimodal user messages now combine text and image blocks into a single message so the LLM sees both. Fixes #1043. ### Added -- `openfang hand config ` subcommand: get, set, unset, and list settings on an active hand instance. Fixes #809. +- `omtae hand config ` subcommand: get, set, unset, and list settings on an active hand instance. Fixes #809. - Optional per-channel `prefix_agent_name` setting (`off` / `bracket` / `bold_bracket`). Wraps outbound agent responses so users in multi-agent channels can see which agent replied. Default is off, byte-identical to prior behavior. Fixes #980. ### Closed as invalid @@ -30,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** Dashboard password hashing switched from SHA256 to Argon2id. Existing `password_hash` values in `config.toml` must be regenerated with `openfang auth hash-password`. Only affects users with `[auth] enabled = true`. +- **BREAKING:** Dashboard password hashing switched from SHA256 to Argon2id. Existing `password_hash` values in `config.toml` must be regenerated with `omtae auth hash-password`. Only affects users with `[auth] enabled = true`. ### Fixed @@ -117,9 +117,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Visual workflow builder with drag-and-drop canvas #### Client SDKs -- JavaScript SDK (`@openfang/sdk`): full REST API client with streaming, TypeScript declarations -- Python client SDK (`openfang_client`): zero-dependency stdlib client with SSE streaming -- Python agent SDK (`openfang_sdk`): decorator-based framework for writing Python agents +- JavaScript SDK (`@omtae/sdk`): full REST API client with streaming, TypeScript declarations +- Python client SDK (`omtae_client`): zero-dependency stdlib client with SSE streaming +- Python agent SDK (`omtae_sdk`): decorator-based framework for writing Python agents - Usage examples for both languages (basic + streaming) #### CLI @@ -167,7 +167,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Interoperability - OpenClaw migration engine (YAML/JSON5 to TOML) - MCP client (JSON-RPC 2.0 over stdio/SSE, tool namespacing) -- MCP server (exposes OpenFang tools via MCP protocol) +- MCP server (exposes OMTAE tools via MCP protocol) - A2A protocol client and server - Tool name compatibility mappings (21 OpenClaw tool names) @@ -194,4 +194,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prometheus metrics for monitoring - Config hot-reload without restart -[0.1.0]: https://github.com/RightNow-AI/openfang/releases/tag/v0.1.0 +[0.1.0]: https://github.com/RightNow-AI/omtae/releases/tag/v0.1.0 diff --git a/CLAUDE.md b/CLAUDE.md index cf1afa06f8..565a6fae3c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,10 +1,10 @@ -# OpenFang — Agent Instructions +# OMTAE — Agent Instructions ## Project Overview -OpenFang is an open-source Agent Operating System written in Rust (14 crates). -- Config: `~/.openfang/config.toml` +OMTAE is an open-source Agent Operating System written in Rust (14 crates). +- Config: `~/.omtae/config.toml` - Default API: `http://127.0.0.1:4200` -- CLI binary: `target/release/openfang.exe` (or `target/debug/openfang.exe`) +- CLI binary: `target/release/omtae.exe` (or `target/debug/omtae.exe`) ## Build & Verify Workflow After every feature implementation, run ALL THREE checks: @@ -25,7 +25,7 @@ cargo clippy --workspace --all-targets -- -D warnings # Zero warnings #### Step 1: Stop any running daemon ```bash -tasklist | grep -i openfang +tasklist | grep -i omtae taskkill //PID //F # Wait 2-3 seconds for port to release sleep 3 @@ -33,12 +33,12 @@ sleep 3 #### Step 2: Build fresh release binary ```bash -cargo build --release -p openfang-cli +cargo build --release -p omtae-cli ``` #### Step 3: Start daemon with required API keys ```bash -GROQ_API_KEY= target/release/openfang.exe start & +GROQ_API_KEY= target/release/omtae.exe start & sleep 6 # Wait for full boot curl -s http://127.0.0.1:4200/api/health # Verify it's up ``` @@ -86,7 +86,7 @@ curl -s http://127.0.0.1:4200/ | grep -c "newComponentName" #### Step 8: Cleanup ```bash -tasklist | grep -i openfang +tasklist | grep -i omtae taskkill //PID //F ``` @@ -107,7 +107,7 @@ taskkill //PID //F | `/api/a2a/tasks/{id}/status` | GET | Check external A2A task status | ## Architecture Notes -- **Don't touch `openfang-cli`** — user is actively building the interactive CLI +- **Don't touch `omtae-cli`** — user is actively building the interactive CLI - `KernelHandle` trait avoids circular deps between runtime and kernel - `AppState` in `server.rs` bridges kernel to API routes - New routes must be registered in `server.rs` router AND implemented in `routes.rs` @@ -115,7 +115,7 @@ taskkill //PID //F - Config fields need: struct field + `#[serde(default)]` + Default impl entry + Serialize/Deserialize derives ## Common Gotchas -- `openfang.exe` may be locked if daemon is running — use `--lib` flag or kill daemon first +- `omtae.exe` may be locked if daemon is running — use `--lib` flag or kill daemon first - `PeerRegistry` is `Option` on kernel but `Option>` on `AppState` — wrap with `.as_ref().map(|r| Arc::new(r.clone()))` - Config fields added to `KernelConfig` struct MUST also be added to the `Default` impl or build fails - `AgentLoopResult` field is `.response` not `.response_text` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2aa3301afe..17c5092d78 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to OpenFang +# Contributing to OMTAE -Thank you for your interest in contributing to OpenFang. This guide covers everything you need to get started, from setting up your development environment to submitting pull requests. +Thank you for your interest in contributing to OMTAE. This guide covers everything you need to get started, from setting up your development environment to submitting pull requests. ## Table of Contents @@ -28,8 +28,8 @@ Thank you for your interest in contributing to OpenFang. This guide covers every ### Clone and Build ```bash -git clone https://github.com/RightNow-AI/openfang.git -cd openfang +git clone https://github.com/RightNow-AI/omtae.git +cd omtae cargo build ``` @@ -61,7 +61,7 @@ cargo build --workspace The default `--release` profile uses full LTO and single-codegen-unit, which produces the smallest/fastest binary but is slow to compile. For iterating locally, use the `release-fast` profile instead: ```bash -cargo build --profile release-fast -p openfang-cli +cargo build --profile release-fast -p omtae-cli ``` This cuts link time significantly (thin LTO, 8 codegen units, `opt-level=2`) while still producing a binary fast enough to run integration tests against. Use `--release` only for final binaries or CI. @@ -77,9 +77,9 @@ The test suite is currently 1,744+ tests. All must pass before merging. ### Run Tests for a Single Crate ```bash -cargo test -p openfang-kernel -cargo test -p openfang-runtime -cargo test -p openfang-memory +cargo test -p omtae-kernel +cargo test -p omtae-runtime +cargo test -p omtae-memory ``` ### Check for Clippy Warnings @@ -115,10 +115,10 @@ cargo run -- doctor - **Documentation**: All public types and functions must have doc comments (`///`). - **Error Handling**: Use `thiserror` for error types. Avoid `unwrap()` in library code; prefer `?` propagation. - **Naming**: - - Types: `PascalCase` (e.g., `OpenFangKernel`, `AgentManifest`) + - Types: `PascalCase` (e.g., `OMTAEKernel`, `AgentManifest`) - Functions/methods: `snake_case` - Constants: `SCREAMING_SNAKE_CASE` - - Crate names: `openfang-{name}` (kebab-case) + - Crate names: `omtae-{name}` (kebab-case) - **Dependencies**: Workspace dependencies are declared in the root `Cargo.toml`. Prefer reusing workspace deps over adding new ones. If you need a new dependency, justify it in the PR. - **Testing**: Every new feature must include tests. Use `tempfile::TempDir` for filesystem isolation and random port binding for network tests. - **Serde**: All config structs use `#[serde(default)]` for forward compatibility with partial TOML. @@ -127,30 +127,30 @@ cargo run -- doctor ## Architecture Overview -OpenFang is organized as a Cargo workspace with 14 crates: +OMTAE is organized as a Cargo workspace with 14 crates: | Crate | Role | |-------|------| -| `openfang-types` | Shared type definitions, taint tracking, manifest signing (Ed25519), model catalog, MCP/A2A config types | -| `openfang-memory` | SQLite-backed memory substrate with vector embeddings, usage tracking, canonical sessions, JSONL mirroring | -| `openfang-runtime` | Agent loop, 3 LLM drivers (Anthropic/Gemini/OpenAI-compat), 38 built-in tools, WASM sandbox, MCP client/server, A2A protocol | -| `openfang-hands` | Hands system (curated autonomous capability packages), 7 bundled hands | -| `openfang-extensions` | Integration registry (25 bundled MCP templates), AES-256-GCM credential vault, OAuth2 PKCE | -| `openfang-kernel` | Assembles all subsystems: workflow engine, RBAC auth, heartbeat monitor, cron scheduler, config hot-reload | -| `openfang-api` | REST/WS/SSE API (Axum 0.8), 76 endpoints, 14-page SPA dashboard, OpenAI-compatible `/v1/chat/completions` | -| `openfang-channels` | 40 channel adapters (Telegram, Discord, Slack, WhatsApp, and 36 more), formatter, rate limiter | -| `openfang-wire` | OFP (OpenFang Protocol): TCP P2P networking with HMAC-SHA256 mutual authentication | -| `openfang-cli` | Clap CLI with daemon auto-detect (HTTP mode vs. in-process fallback), MCP server | -| `openfang-migrate` | Migration engine for importing from OpenClaw (and future frameworks) | -| `openfang-skills` | Skill system: 60 bundled skills, FangHub marketplace, OpenClaw compatibility, prompt injection scanning | -| `openfang-desktop` | Tauri 2.0 native desktop app (WebView + system tray + single-instance + notifications) | +| `omtae-types` | Shared type definitions, taint tracking, manifest signing (Ed25519), model catalog, MCP/A2A config types | +| `omtae-memory` | SQLite-backed memory substrate with vector embeddings, usage tracking, canonical sessions, JSONL mirroring | +| `omtae-runtime` | Agent loop, 3 LLM drivers (Anthropic/Gemini/OpenAI-compat), 38 built-in tools, WASM sandbox, MCP client/server, A2A protocol | +| `omtae-hands` | Hands system (curated autonomous capability packages), 7 bundled hands | +| `omtae-extensions` | Integration registry (25 bundled MCP templates), AES-256-GCM credential vault, OAuth2 PKCE | +| `omtae-kernel` | Assembles all subsystems: workflow engine, RBAC auth, heartbeat monitor, cron scheduler, config hot-reload | +| `omtae-api` | REST/WS/SSE API (Axum 0.8), 76 endpoints, 14-page SPA dashboard, OpenAI-compatible `/v1/chat/completions` | +| `omtae-channels` | 40 channel adapters (Telegram, Discord, Slack, WhatsApp, and 36 more), formatter, rate limiter | +| `omtae-wire` | OFP (OMTAE Protocol): TCP P2P networking with HMAC-SHA256 mutual authentication | +| `omtae-cli` | Clap CLI with daemon auto-detect (HTTP mode vs. in-process fallback), MCP server | +| `omtae-migrate` | Migration engine for importing from OpenClaw (and future frameworks) | +| `omtae-skills` | Skill system: 60 bundled skills, FangHub marketplace, OpenClaw compatibility, prompt injection scanning | +| `omtae-desktop` | Tauri 2.0 native desktop app (WebView + system tray + single-instance + notifications) | | `xtask` | Build automation tasks | ### Key Architectural Patterns -- **`KernelHandle` trait**: Defined in `openfang-runtime`, implemented on `OpenFangKernel` in `openfang-kernel`. This avoids circular crate dependencies while enabling inter-agent tools. +- **`KernelHandle` trait**: Defined in `omtae-runtime`, implemented on `OMTAEKernel` in `omtae-kernel`. This avoids circular crate dependencies while enabling inter-agent tools. - **Shared memory**: A fixed UUID (`AgentId(Uuid::from_bytes([0..0, 0x01]))`) provides a cross-agent KV namespace. -- **Daemon detection**: The CLI checks `~/.openfang/daemon.json` and pings the health endpoint. If a daemon is running, commands use HTTP; otherwise, they boot an in-process kernel. +- **Daemon detection**: The CLI checks `~/.omtae/daemon.json` and pings the health endpoint. If a daemon is running, commands use HTTP; otherwise, they boot an in-process kernel. - **Capability-based security**: Every agent operation is checked against the agent's granted capabilities before execution. --- @@ -173,7 +173,7 @@ agents/my-agent/agent.toml name = "my-agent" version = "0.1.0" description = "A brief description of what this agent does." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["category"] @@ -205,7 +205,7 @@ You are a specialized agent that... 4. Test by spawning: ```bash -openfang agent spawn agents/my-agent/agent.toml +omtae agent spawn agents/my-agent/agent.toml ``` 5. Submit a PR with the new template. @@ -214,11 +214,11 @@ openfang agent spawn agents/my-agent/agent.toml ## How to Add a New Channel Adapter -Channel adapters live in `crates/openfang-channels/src/`. Each adapter implements the `ChannelAdapter` trait. +Channel adapters live in `crates/omtae-channels/src/`. Each adapter implements the `ChannelAdapter` trait. ### Steps -1. Create a new file: `crates/openfang-channels/src/myplatform.rs` +1. Create a new file: `crates/omtae-channels/src/myplatform.rs` 2. Implement the `ChannelAdapter` trait (defined in `types.rs`): @@ -252,17 +252,17 @@ impl ChannelAdapter for MyPlatformAdapter { } ``` -3. Register the module in `crates/openfang-channels/src/lib.rs`: +3. Register the module in `crates/omtae-channels/src/lib.rs`: ```rust pub mod myplatform; ``` -4. Wire it up in the channel bridge (`crates/openfang-api/src/channel_bridge.rs`) so the daemon starts it alongside other adapters. +4. Wire it up in the channel bridge (`crates/omtae-api/src/channel_bridge.rs`) so the daemon starts it alongside other adapters. -5. Add configuration support in `openfang-types` config structs (add a `[channels.myplatform]` section). +5. Add configuration support in `omtae-types` config structs (add a `[channels.myplatform]` section). -6. Add CLI setup wizard instructions in `crates/openfang-cli/src/main.rs` under `cmd_channel_setup`. +6. Add CLI setup wizard instructions in `crates/omtae-cli/src/main.rs` under `cmd_channel_setup`. 7. Write tests and submit a PR. @@ -270,7 +270,7 @@ pub mod myplatform; ## How to Add a New Tool -Built-in tools are defined in `crates/openfang-runtime/src/tool_runner.rs`. +Built-in tools are defined in `crates/omtae-runtime/src/tool_runner.rs`. ### Steps @@ -366,6 +366,6 @@ Please report unacceptable behavior to the maintainers. ## Questions? -- Open a [GitHub Discussion](https://github.com/RightNow-AI/openfang/discussions) for questions. -- Open a [GitHub Issue](https://github.com/RightNow-AI/openfang/issues) for bugs or feature requests. +- Open a [GitHub Discussion](https://github.com/RightNow-AI/omtae/discussions) for questions. +- Open a [GitHub Issue](https://github.com/RightNow-AI/omtae/issues) for bugs or feature requests. - Check the [docs/](docs/) directory for detailed guides on specific topics. diff --git a/Cargo.lock b/Cargo.lock index e880570247..5a2b364837 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3928,37 +3928,7 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "open" -version = "5.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" -dependencies = [ - "dunce", - "is-wsl", - "libc", - "pathdiff", -] - -[[package]] -name = "openfang-api" +name = "omtae-api" version = "0.6.9" dependencies = [ "argon2", @@ -3971,16 +3941,16 @@ dependencies = [ "governor", "hex", "hmac", - "openfang-channels", - "openfang-extensions", - "openfang-hands", - "openfang-kernel", - "openfang-memory", - "openfang-migrate", - "openfang-runtime", - "openfang-skills", - "openfang-types", - "openfang-wire", + "omtae-channels", + "omtae-extensions", + "omtae-hands", + "omtae-kernel", + "omtae-memory", + "omtae-migrate", + "omtae-runtime", + "omtae-skills", + "omtae-types", + "omtae-wire", "rand 0.8.5", "reqwest 0.12.28", "serde", @@ -4000,7 +3970,7 @@ dependencies = [ ] [[package]] -name = "openfang-channels" +name = "omtae-channels" version = "0.6.9" dependencies = [ "aes", @@ -4018,7 +3988,7 @@ dependencies = [ "lettre", "mailparse", "native-tls", - "openfang-types", + "omtae-types", "prost", "regex-lite", "reqwest 0.12.28", @@ -4039,20 +4009,20 @@ dependencies = [ ] [[package]] -name = "openfang-cli" +name = "omtae-cli" version = "0.6.9" dependencies = [ "clap", "clap_complete", "colored", "dirs 6.0.0", - "openfang-api", - "openfang-extensions", - "openfang-kernel", - "openfang-migrate", - "openfang-runtime", - "openfang-skills", - "openfang-types", + "omtae-api", + "omtae-extensions", + "omtae-kernel", + "omtae-migrate", + "omtae-runtime", + "omtae-skills", + "omtae-types", "ratatui", "reqwest 0.12.28", "serde", @@ -4067,14 +4037,14 @@ dependencies = [ ] [[package]] -name = "openfang-desktop" +name = "omtae-desktop" version = "0.6.9" dependencies = [ "axum", + "omtae-api", + "omtae-kernel", + "omtae-types", "open", - "openfang-api", - "openfang-kernel", - "openfang-types", "serde", "serde_json", "tauri", @@ -4093,7 +4063,7 @@ dependencies = [ ] [[package]] -name = "openfang-extensions" +name = "omtae-extensions" version = "0.6.9" dependencies = [ "aes-gcm", @@ -4103,7 +4073,7 @@ dependencies = [ "chrono", "dashmap", "dirs 6.0.0", - "openfang-types", + "omtae-types", "rand 0.8.5", "reqwest 0.12.28", "serde", @@ -4121,14 +4091,14 @@ dependencies = [ ] [[package]] -name = "openfang-hands" +name = "omtae-hands" version = "0.6.9" dependencies = [ "chrono", "dashmap", "dirs 6.0.0", "hex", - "openfang-types", + "omtae-types", "serde", "serde_json", "sha2", @@ -4141,7 +4111,7 @@ dependencies = [ ] [[package]] -name = "openfang-kernel" +name = "omtae-kernel" version = "0.6.9" dependencies = [ "async-trait", @@ -4154,14 +4124,14 @@ dependencies = [ "futures", "hex", "libc", - "openfang-channels", - "openfang-extensions", - "openfang-hands", - "openfang-memory", - "openfang-runtime", - "openfang-skills", - "openfang-types", - "openfang-wire", + "omtae-channels", + "omtae-extensions", + "omtae-hands", + "omtae-memory", + "omtae-runtime", + "omtae-skills", + "omtae-types", + "omtae-wire", "rand 0.8.5", "reqwest 0.12.28", "rustls", @@ -4181,12 +4151,12 @@ dependencies = [ ] [[package]] -name = "openfang-memory" +name = "omtae-memory" version = "0.6.9" dependencies = [ "async-trait", "chrono", - "openfang-types", + "omtae-types", "reqwest 0.12.28", "rmp-serde", "rusqlite", @@ -4201,13 +4171,13 @@ dependencies = [ ] [[package]] -name = "openfang-migrate" +name = "omtae-migrate" version = "0.6.9" dependencies = [ "chrono", "dirs 6.0.0", "json5", - "openfang-types", + "omtae-types", "serde", "serde_json", "serde_yaml", @@ -4220,7 +4190,7 @@ dependencies = [ ] [[package]] -name = "openfang-runtime" +name = "omtae-runtime" version = "0.6.9" dependencies = [ "anyhow", @@ -4232,9 +4202,9 @@ dependencies = [ "futures", "hex", "http", - "openfang-memory", - "openfang-skills", - "openfang-types", + "omtae-memory", + "omtae-skills", + "omtae-types", "regex-lite", "reqwest 0.12.28", "rmcp", @@ -4256,13 +4226,13 @@ dependencies = [ ] [[package]] -name = "openfang-skills" +name = "omtae-skills" version = "0.6.9" dependencies = [ "chrono", "ed25519-dalek", "hex", - "openfang-types", + "omtae-types", "rand 0.8.5", "reqwest 0.12.28", "serde", @@ -4281,7 +4251,7 @@ dependencies = [ ] [[package]] -name = "openfang-types" +name = "omtae-types" version = "0.6.9" dependencies = [ "async-trait", @@ -4295,13 +4265,14 @@ dependencies = [ "serde", "serde_json", "sha2", + "subtle", "thiserror 2.0.18", "toml 0.9.12+spec-1.1.0", "uuid", ] [[package]] -name = "openfang-wire" +name = "omtae-wire" version = "0.6.9" dependencies = [ "async-trait", @@ -4309,7 +4280,7 @@ dependencies = [ "dashmap", "hex", "hmac", - "openfang-types", + "omtae-types", "rand 0.8.5", "serde", "serde_json", @@ -4322,6 +4293,36 @@ dependencies = [ "uuid", ] +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "open" +version = "5.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl" version = "0.10.76" diff --git a/Cargo.toml b/Cargo.toml index 554d9a30e0..3b48c8f64c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,19 @@ [workspace] resolver = "2" members = [ - "crates/openfang-types", - "crates/openfang-memory", - "crates/openfang-runtime", - "crates/openfang-wire", - "crates/openfang-api", - "crates/openfang-kernel", - "crates/openfang-cli", - "crates/openfang-channels", - "crates/openfang-migrate", - "crates/openfang-skills", - "crates/openfang-desktop", - "crates/openfang-hands", - "crates/openfang-extensions", + "crates/omtae-types", + "crates/omtae-memory", + "crates/omtae-runtime", + "crates/omtae-wire", + "crates/omtae-api", + "crates/omtae-kernel", + "crates/omtae-cli", + "crates/omtae-channels", + "crates/omtae-migrate", + "crates/omtae-skills", + "crates/omtae-desktop", + "crates/omtae-hands", + "crates/omtae-extensions", "xtask", ] @@ -21,7 +21,7 @@ members = [ version = "0.6.9" edition = "2021" license = "Apache-2.0 OR MIT" -repository = "https://github.com/RightNow-AI/openfang" +repository = "https://github.com/RightNow-AI/omtae" rust-version = "1.75" [workspace.dependencies] diff --git a/Dockerfile b/Dockerfile index 1174e95085..293611743e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ARG LTO=true ARG CODEGEN_UNITS=1 ENV CARGO_PROFILE_RELEASE_LTO=${LTO} \ CARGO_PROFILE_RELEASE_CODEGEN_UNITS=${CODEGEN_UNITS} -RUN cargo build --release --bin openfang +RUN cargo build --release --bin omtae FROM rust:1-slim-bookworm RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -25,10 +25,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ npm \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /build/target/release/openfang /usr/local/bin/ -COPY --from=builder /build/agents /opt/openfang/agents +COPY --from=builder /build/target/release/omtae /usr/local/bin/ +COPY --from=builder /build/agents /opt/omtae/agents EXPOSE 4200 VOLUME /data ENV OPENFANG_HOME=/data -ENTRYPOINT ["openfang"] +ENTRYPOINT ["omtae"] CMD ["start"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE index 066f64e001..2074595b1d 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -174,7 +174,7 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION END OF TERMS AND CONDITIONS -Copyright 2024 OpenFang Contributors +Copyright 2024 OMTAE Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/LICENSE-MIT b/LICENSE-MIT index f510177b8d..ca1d1cbca5 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 OpenFang Contributors +Copyright (c) 2024 OMTAE Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MIGRATION.md b/MIGRATION.md index 6d3347698f..f71d8db11c 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,6 @@ -# Migrating to OpenFang +# Migrating to OMTAE -This guide covers migrating from OpenClaw (and other frameworks) to OpenFang. The migration engine handles config conversion, agent import, memory transfer, channel re-configuration, and skill scanning. +This guide covers migrating from OpenClaw (and other frameworks) to OMTAE. The migration engine handles config conversion, agent import, memory transfer, channel re-configuration, and skill scanning. ## Table of Contents @@ -19,61 +19,61 @@ This guide covers migrating from OpenClaw (and other frameworks) to OpenFang. Th Run a single command to migrate your entire OpenClaw workspace: ```bash -openfang migrate --from openclaw +omtae migrate --from openclaw ``` -This auto-detects your OpenClaw workspace at `~/.openclaw/` and imports everything into `~/.openfang/`. +This auto-detects your OpenClaw workspace at `~/.openclaw/` and imports everything into `~/.omtae/`. ### Options ```bash # Specify a custom source directory -openfang migrate --from openclaw --source-dir /path/to/openclaw/workspace +omtae migrate --from openclaw --source-dir /path/to/openclaw/workspace # Dry run -- see what would be imported without making changes -openfang migrate --from openclaw --dry-run +omtae migrate --from openclaw --dry-run ``` ### Migration Report -After a successful migration, a `migration_report.md` file is saved to `~/.openfang/` with a summary of everything that was imported, skipped, or needs manual attention. +After a successful migration, a `migration_report.md` file is saved to `~/.omtae/` with a summary of everything that was imported, skipped, or needs manual attention. ### Other Frameworks LangChain and AutoGPT migration support is planned: ```bash -openfang migrate --from langchain # Coming soon -openfang migrate --from autogpt # Coming soon +omtae migrate --from langchain # Coming soon +omtae migrate --from autogpt # Coming soon ``` --- ## What Gets Migrated -| Item | Source (OpenClaw) | Destination (OpenFang) | Status | +| Item | Source (OpenClaw) | Destination (OMTAE) | Status | |------|-------------------|------------------------|--------| -| **Config** | `~/.openclaw/config.yaml` | `~/.openfang/config.toml` | Fully automated | -| **Agents** | `~/.openclaw/agents/*/agent.yaml` | `~/.openfang/agents/*/agent.toml` | Fully automated | -| **Memory** | `~/.openclaw/agents/*/MEMORY.md` | `~/.openfang/agents/*/imported_memory.md` | Fully automated | -| **Channels** | `~/.openclaw/messaging/*.yaml` | `~/.openfang/channels_import.toml` | Automated (manual merge) | +| **Config** | `~/.openclaw/config.yaml` | `~/.omtae/config.toml` | Fully automated | +| **Agents** | `~/.openclaw/agents/*/agent.yaml` | `~/.omtae/agents/*/agent.toml` | Fully automated | +| **Memory** | `~/.openclaw/agents/*/MEMORY.md` | `~/.omtae/agents/*/imported_memory.md` | Fully automated | +| **Channels** | `~/.openclaw/messaging/*.yaml` | `~/.omtae/channels_import.toml` | Automated (manual merge) | | **Skills** | `~/.openclaw/skills/` | Scanned and reported | Manual reinstall | | **Sessions** | `~/.openclaw/agents/*/sessions/` | Not migrated | Fresh start recommended | | **Workspace files** | `~/.openclaw/agents/*/workspace/` | Not migrated | Copy manually if needed | ### Channel Import Note -Channel configurations (Telegram, Discord, Slack) are exported to a `channels_import.toml` file. You must manually merge the `[channels]` section into your `~/.openfang/config.toml`. +Channel configurations (Telegram, Discord, Slack) are exported to a `channels_import.toml` file. You must manually merge the `[channels]` section into your `~/.omtae/config.toml`. ### Skills Note OpenClaw skills (Node.js) are detected and listed in the migration report but not automatically converted. After migration, reinstall skills using: ```bash -openfang skill install +omtae skill install ``` -OpenFang automatically detects OpenClaw-format skills and converts them during installation. +OMTAE automatically detects OpenClaw-format skills and converts them during installation. --- @@ -81,13 +81,13 @@ OpenFang automatically detects OpenClaw-format skills and converts them during i If you prefer migrating by hand (or need to handle edge cases), follow these steps: -### 1. Initialize OpenFang +### 1. Initialize OMTAE ```bash -openfang init +omtae init ``` -This creates `~/.openfang/` with a default `config.toml`. +This creates `~/.omtae/` with a default `config.toml`. ### 2. Convert Your Config @@ -103,7 +103,7 @@ memory: decay_rate: 0.05 ``` -**OpenFang** (`~/.openfang/config.toml`): +**OMTAE** (`~/.omtae/config.toml`): ```toml [default_model] provider = "anthropic" @@ -136,12 +136,12 @@ tags: - dev ``` -**OpenFang** (`~/.openfang/agents/coder/agent.toml`): +**OMTAE** (`~/.omtae/agents/coder/agent.toml`): ```toml name = "coder" version = "0.1.0" description = "A coding assistant" -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["coding", "dev"] @@ -166,7 +166,7 @@ allowed_users: - "123456789" ``` -**OpenFang** (add to `~/.openfang/config.toml`): +**OMTAE** (add to `~/.omtae/config.toml`): ```toml [channels.telegram] bot_token_env = "TELEGRAM_BOT_TOKEN" @@ -176,10 +176,10 @@ allowed_users = ["123456789"] ### 5. Import Memory -Copy any `MEMORY.md` files from OpenClaw agents to OpenFang agent directories: +Copy any `MEMORY.md` files from OpenClaw agents to OMTAE agent directories: ```bash -cp ~/.openclaw/agents/coder/MEMORY.md ~/.openfang/agents/coder/imported_memory.md +cp ~/.openclaw/agents/coder/MEMORY.md ~/.omtae/agents/coder/imported_memory.md ``` The kernel will ingest these on first boot. @@ -188,10 +188,10 @@ The kernel will ingest these on first boot. ## Config Format Differences -| Aspect | OpenClaw | OpenFang | +| Aspect | OpenClaw | OMTAE | |--------|----------|----------| | Format | YAML | TOML | -| Config location | `~/.openclaw/config.yaml` | `~/.openfang/config.toml` | +| Config location | `~/.openclaw/config.yaml` | `~/.omtae/config.toml` | | Agent definition | `agent.yaml` | `agent.toml` | | Channel config | Separate files per channel | Unified in `config.toml` | | Tool permissions | Implicit (tool list) | Capability-based (tools, memory, network, shell) | @@ -205,9 +205,9 @@ The kernel will ingest these on first boot. ## Tool Name Mapping -Tools were renamed between OpenClaw and OpenFang for consistency. The migration engine handles this automatically. +Tools were renamed between OpenClaw and OMTAE for consistency. The migration engine handles this automatically. -| OpenClaw Tool | OpenFang Tool | Notes | +| OpenClaw Tool | OMTAE Tool | Notes | |---------------|---------------|-------| | `read_file` | `file_read` | Noun-first naming | | `write_file` | `file_write` | | @@ -225,7 +225,7 @@ Tools were renamed between OpenClaw and OpenFang for consistency. The migration | `agents_list` | `agent_list` | | | `agent_list` | `agent_list` | | -### New Tools in OpenFang +### New Tools in OMTAE These tools have no OpenClaw equivalent: @@ -251,7 +251,7 @@ These tools have no OpenClaw equivalent: OpenClaw's tool profiles map to explicit tool lists: -| OpenClaw Profile | OpenFang Tools | +| OpenClaw Profile | OMTAE Tools | |------------------|----------------| | `minimal` | `file_read`, `file_list` | | `coding` | `file_read`, `file_write`, `file_list`, `shell_exec`, `web_fetch` | @@ -263,7 +263,7 @@ OpenClaw's tool profiles map to explicit tool lists: ## Provider Mapping -| OpenClaw Name | OpenFang Name | API Key Env Var | +| OpenClaw Name | OMTAE Name | API Key Env Var | |---------------|---------------|-----------------| | `anthropic` | `anthropic` | `ANTHROPIC_API_KEY` | | `claude` | `anthropic` | `ANTHROPIC_API_KEY` | @@ -277,7 +277,7 @@ OpenClaw's tool profiles map to explicit tool lists: | `mistral` | `mistral` | `MISTRAL_API_KEY` | | `fireworks` | `fireworks` | `FIREWORKS_API_KEY` | -### New Providers in OpenFang +### New Providers in OMTAE | Provider | Description | |----------|-------------| @@ -288,7 +288,7 @@ OpenClaw's tool profiles map to explicit tool lists: ## Feature Comparison -| Feature | OpenClaw | OpenFang | +| Feature | OpenClaw | OMTAE | |---------|----------|----------| | **Language** | Node.js / TypeScript | Rust | | **Config format** | YAML | TOML | @@ -305,7 +305,7 @@ OpenClaw's tool profiles map to explicit tool lists: | **Event triggers** | None | Pattern-matching event triggers with templated prompts | | **WASM sandbox** | None | Wasmtime-based sandboxed execution | | **Python runtime** | None | Subprocess-based Python agent execution | -| **Networking** | None | OFP (OpenFang Protocol) peer-to-peer | +| **Networking** | None | OFP (OMTAE Protocol) peer-to-peer | | **API server** | Basic REST | REST + WebSocket + SSE streaming | | **WebChat UI** | Separate | Embedded in daemon | | **Channel adapters** | Telegram, Discord | Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Email | @@ -324,7 +324,7 @@ OpenClaw's tool profiles map to explicit tool lists: The migration engine looks for `~/.openclaw/` by default. If your OpenClaw workspace is elsewhere: ```bash -openfang migrate --from openclaw --source-dir /path/to/your/workspace +omtae migrate --from openclaw --source-dir /path/to/your/workspace ``` ### Agent fails to spawn after migration @@ -339,7 +339,7 @@ Check the converted `agent.toml` for: OpenClaw Node.js skills must be reinstalled: ```bash -openfang skill install /path/to/openclaw/skills/my-skill +omtae skill install /path/to/openclaw/skills/my-skill ``` The installer auto-detects OpenClaw format and converts the skill manifest. @@ -349,12 +349,12 @@ The installer auto-detects OpenClaw format and converts the skill manifest. After migration, channels are exported to `channels_import.toml`. You must merge them into your `config.toml` manually: ```bash -cat ~/.openfang/channels_import.toml -# Copy the [channels.*] sections into ~/.openfang/config.toml +cat ~/.omtae/channels_import.toml +# Copy the [channels.*] sections into ~/.omtae/config.toml ``` Then restart the daemon: ```bash -openfang start +omtae start ``` diff --git a/OMTAE-TUNNEL.md b/OMTAE-TUNNEL.md new file mode 100644 index 0000000000..17285e5c77 --- /dev/null +++ b/OMTAE-TUNNEL.md @@ -0,0 +1,58 @@ +# OMTAE dashboard access over Cloudflare Tunnel + +This guide covers exposing the OMTAE web dashboard (default port **4200**) through a personal Cloudflare Tunnel (e.g. `desk.omtaeservices.biz`). + +## Security model + +**PIN auth is for personal tunnels only.** A 4–6 digit PIN stops casual visitors but is not strong cryptography. Anyone who can reach your tunnel URL can attempt brute force. Combine with: + +- Cloudflare Access (recommended for production) +- A long, unique PIN (not `123456`) +- Tunnel hostname not shared publicly +- Rate limiting at the edge where possible + +Do **not** rely on PIN-only auth for multi-tenant or internet-facing deployments. + +## Configuration + +Add to `~/.omtae/config.toml`: + +```toml +[dashboard] +pin = "your-pin-here" # 4–6 digits — change from any default +require_pin = true +``` + +When `require_pin = true` and `pin` is set: + +- The top-level `api_key` / `OPENFANG_API_KEY` **Bearer** requirement is disabled for HTTP/WebSocket (PIN + session cookie instead). +- `/api/*` requires a valid `omtae_session` cookie (after PIN login) or `X-OMTAE-Pin` header. +- Public endpoints remain open: `/api/health`, static assets, `/api/auth/login`, `/api/auth/check`. + +Restart the daemon after changing config: + +```bash +cargo build --release -p omtae-cli +sudo systemctl restart omtae-daemon.service +``` + +## vLLM API key + +Local **vLLM** does not need a wizard API key. Configure the model in `[default_model]` and set `VLLM_API_KEY` in **systemd** (or your vLLM service) if the proxy requires it—not in the dashboard wizard. + +## Client behavior + +1. Open the tunnel URL in a browser. +2. Enter the PIN on the unlock screen (mobile-friendly numeric keypad). +3. The server sets an HttpOnly `omtae_session` cookie (default 7-day TTL via `[auth].session_ttl_hours`). +4. Optional: scripts can send `X-OMTAE-Pin: ` on API calls instead of Bearer tokens. + +## Legacy options + +| Method | Use case | +|--------|----------| +| `api_key` at root of config.toml | Machine-to-machine / scripts | +| `[auth]` username + `password_hash` | Stronger dashboard login (`omtae auth hash-password`) | +| `[dashboard]` PIN | Quick personal tunnel gate | + +PIN mode takes precedence over Bearer when `[dashboard]` is active. diff --git a/README.md b/README.md index afffa314c3..65d2539a7f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

- OpenFang Logo + OMTAE Logo

-

OpenFang

+

OMTAE

The Agent Operating System

@@ -11,9 +11,9 @@

- Documentation • - Quick Start • - Twitter / X + Documentation • + Quick Start • + Twitter / X

@@ -22,29 +22,29 @@ v0.6.9 Tests Clippy - Buy Me A Coffee + Buy Me A Coffee

--- > **v0.5.10 (April 2026)** > -> OpenFang is feature complete but still pre-1.0. Expect rough edges and breaking changes between minor versions. We ship fast and fix fast. Pin to a specific commit for production use until v1.0. [Report issues here.](https://github.com/RightNow-AI/openfang/issues) +> OMTAE is feature complete but still pre-1.0. Expect rough edges and breaking changes between minor versions. We ship fast and fix fast. Pin to a specific commit for production use until v1.0. [Report issues here.](https://github.com/RightNow-AI/omtae/issues) --- -## What is OpenFang? +## What is OMTAE? -OpenFang is an **open-source Agent Operating System**. Not a chatbot framework. Not a Python wrapper around an LLM. Not a "multi-agent orchestrator." A full operating system for autonomous agents, built from scratch in Rust. +OMTAE is an **open-source Agent Operating System**. Not a chatbot framework. Not a Python wrapper around an LLM. Not a "multi-agent orchestrator." A full operating system for autonomous agents, built from scratch in Rust. -Traditional agent frameworks wait for you to type something. OpenFang runs **autonomous agents that work for you**: on schedules, 24/7, building knowledge graphs, monitoring targets, generating leads, managing your social media, and reporting results to your dashboard. +Traditional agent frameworks wait for you to type something. OMTAE runs **autonomous agents that work for you**: on schedules, 24/7, building knowledge graphs, monitoring targets, generating leads, managing your social media, and reporting results to your dashboard. The entire system compiles to a **single ~32MB binary**. One install, one command, your agents are live. ```bash -curl -fsSL https://openfang.sh/install | sh -openfang init -openfang start +curl -fsSL https://omtae.sh/install | sh +omtae init +omtae start # Dashboard live at http://localhost:4200 ``` @@ -52,9 +52,9 @@ openfang start Windows ```powershell -irm https://openfang.sh/install.ps1 | iex -openfang init -openfang start +irm https://omtae.sh/install.ps1 | iex +omtae init +omtae start ``` @@ -65,7 +65,7 @@ openfang start

"Traditional agents wait for you to type. Hands work for you."

-**Hands** are OpenFang's core innovation. Pre-built autonomous capability packages that run independently, on schedules, without you having to prompt them. This is not a chatbot. This is an agent that wakes up at 6 AM, researches your competitors, builds a knowledge graph, scores the findings, and delivers a report to your Telegram before you've had coffee. +**Hands** are OMTAE's core innovation. Pre-built autonomous capability packages that run independently, on schedules, without you having to prompt them. This is not a chatbot. This is an agent that wakes up at 6 AM, researches your competitors, builds a knowledge graph, scores the findings, and delivers a report to your Telegram before you've had coffee. Each Hand bundles: - **HAND.toml**: manifest declaring tools, settings, requirements, and dashboard metrics. @@ -89,29 +89,29 @@ All compiled into the binary. No downloading, no pip install, no Docker pull. ```bash # Activate the Researcher Hand. It starts working immediately. -openfang hand activate researcher +omtae hand activate researcher # Check its progress anytime -openfang hand status researcher +omtae hand status researcher # Activate lead generation on a daily schedule -openfang hand activate lead +omtae hand activate lead # Pause without losing state -openfang hand pause lead +omtae hand pause lead # See all available Hands -openfang hand list +omtae hand list ``` **Build your own.** Define a `HAND.toml` with tools, settings, and a system prompt. Publish to FangHub. --- -## OpenFang vs The Landscape +## OMTAE vs The Landscape

- OpenFang vs OpenClaw vs ZeroClaw + OMTAE vs OpenClaw vs ZeroClaw

### Benchmarks: Measured, Not Marketed @@ -122,7 +122,7 @@ All data from official documentation and public repositories, February 2026. ``` ZeroClaw ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 10 ms -OpenFang ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 180 ms ★ +OMTAE ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 180 ms ★ LangGraph █████████████████░░░░░░░░░░░░░░░░░░░░░░░░░ 2.5 sec CrewAI ████████████████████░░░░░░░░░░░░░░░░░░░░░░ 3.0 sec AutoGen ██████████████████████████░░░░░░░░░░░░░░░░░ 4.0 sec @@ -133,7 +133,7 @@ OpenClaw ███████████████████████ ``` ZeroClaw █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 5 MB -OpenFang ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 40 MB ★ +OMTAE ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 40 MB ★ LangGraph ██████████████████░░░░░░░░░░░░░░░░░░░░░░░░░ 180 MB CrewAI ████████████████████░░░░░░░░░░░░░░░░░░░░░░░ 200 MB AutoGen █████████████████████████░░░░░░░░░░░░░░░░░░ 250 MB @@ -144,7 +144,7 @@ OpenClaw ███████████████████████ ``` ZeroClaw █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 8.8 MB -OpenFang ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 32 MB ★ +OMTAE ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 32 MB ★ CrewAI ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 100 MB LangGraph ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 150 MB AutoGen ████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░ 200 MB @@ -154,7 +154,7 @@ OpenClaw ███████████████████████ #### Security Systems (higher is better) ``` -OpenFang ████████████████████████████████████████████ 16 ★ +OMTAE ████████████████████████████████████████████ 16 ★ ZeroClaw ███████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 6 OpenClaw ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 3 AutoGen █████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 2 @@ -165,7 +165,7 @@ CrewAI ███░░░░░░░░░░░░░░░░░░░░ #### Channel Adapters (higher is better) ``` -OpenFang ████████████████████████████████████████████ 40 ★ +OMTAE ████████████████████████████████████████████ 40 ★ ZeroClaw ███████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 15 OpenClaw █████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 13 CrewAI ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0 @@ -177,7 +177,7 @@ LangGraph ░░░░░░░░░░░░░░░░░░░░░░░ ``` ZeroClaw ████████████████████████████████████████████ 28 -OpenFang ██████████████████████████████████████████░░ 27 ★ +OMTAE ██████████████████████████████████████████░░ 27 ★ LangGraph ██████████████████████░░░░░░░░░░░░░░░░░░░░░ 15 CrewAI ██████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 10 OpenClaw ██████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 10 @@ -186,7 +186,7 @@ AutoGen ███████████░░░░░░░░░░░░ ### Feature-by-Feature Comparison -| Feature | OpenFang | OpenClaw | ZeroClaw | CrewAI | AutoGen | LangGraph | +| Feature | OMTAE | OpenClaw | ZeroClaw | CrewAI | AutoGen | LangGraph | |---------|----------|----------|----------|--------|---------|-----------| | **Language** | **Rust** | TypeScript | **Rust** | Python | Python | Python | | **Autonomous Hands** | **7 built-in** | None | None | None | None | None | @@ -205,7 +205,7 @@ AutoGen ███████████░░░░░░░░░░░░ ## 16 Security Systems: Defense in Depth -OpenFang doesn't bolt security on after the fact. Every layer is independently testable and operates without a single point of failure. +OMTAE doesn't bolt security on after the fact. Every layer is independently testable and operates without a single point of failure. | # | System | What It Does | |---|--------|-------------| @@ -233,19 +233,19 @@ OpenFang doesn't bolt security on after the fact. Every layer is independently t 14 Rust crates. 137,728 lines of code. Modular kernel design. ``` -openfang-kernel Orchestration, workflows, metering, RBAC, scheduler, budget tracking -openfang-runtime Agent loop, 3 LLM drivers, 53 tools, WASM sandbox, MCP, A2A -openfang-api 140+ REST/WS/SSE endpoints, OpenAI-compatible API, dashboard -openfang-channels 40 messaging adapters with rate limiting, DM/group policies -openfang-memory SQLite persistence, vector embeddings, canonical sessions, compaction -openfang-types Core types, taint tracking, Ed25519 manifest signing, model catalog -openfang-skills 60 bundled skills, SKILL.md parser, FangHub marketplace -openfang-hands 7 autonomous Hands, HAND.toml parser, lifecycle management -openfang-extensions 25 MCP templates, AES-256-GCM credential vault, OAuth2 PKCE -openfang-wire OFP P2P protocol with HMAC-SHA256 mutual authentication -openfang-cli CLI with daemon management, TUI dashboard, MCP server mode -openfang-desktop Tauri 2.0 native app (system tray, notifications, global shortcuts) -openfang-migrate OpenClaw, LangChain, AutoGPT migration engine +omtae-kernel Orchestration, workflows, metering, RBAC, scheduler, budget tracking +omtae-runtime Agent loop, 3 LLM drivers, 53 tools, WASM sandbox, MCP, A2A +omtae-api 140+ REST/WS/SSE endpoints, OpenAI-compatible API, dashboard +omtae-channels 40 messaging adapters with rate limiting, DM/group policies +omtae-memory SQLite persistence, vector embeddings, canonical sessions, compaction +omtae-types Core types, taint tracking, Ed25519 manifest signing, model catalog +omtae-skills 60 bundled skills, SKILL.md parser, FangHub marketplace +omtae-hands 7 autonomous Hands, HAND.toml parser, lifecycle management +omtae-extensions 25 MCP templates, AES-256-GCM credential vault, OAuth2 PKCE +omtae-wire OFP P2P protocol with HMAC-SHA256 mutual authentication +omtae-cli CLI with daemon management, TUI dashboard, MCP server mode +omtae-desktop Tauri 2.0 native app (system tray, notifications, global shortcuts) +omtae-migrate OpenClaw, LangChain, AutoGPT migration engine xtask Build automation ``` @@ -268,12 +268,12 @@ Each adapter supports per-channel model overrides, DM/group policies, rate limit ## WhatsApp Web Gateway (QR Code) -Connect your personal WhatsApp account to OpenFang via QR code, just like WhatsApp Web. No Meta Business account required. +Connect your personal WhatsApp account to OMTAE via QR code, just like WhatsApp Web. No Meta Business account required. ### Prerequisites - **Node.js >= 18** installed ([download](https://nodejs.org/)) -- OpenFang installed and initialized +- OMTAE installed and initialized ### Setup @@ -316,10 +316,10 @@ node packages/whatsapp-gateway/index.js The gateway listens on port `3009` by default. Override with `WHATSAPP_GATEWAY_PORT`. -**5. Start OpenFang:** +**5. Start OMTAE:** ```bash -openfang start +omtae start # Dashboard at http://localhost:4200 ``` @@ -335,9 +335,9 @@ Once scanned, the status changes to `connected` and incoming messages are routed | Variable | Description | Default | |----------|-------------|---------| -| `WHATSAPP_WEB_GATEWAY_URL` | Gateway URL for OpenFang to connect to | _(empty = disabled)_ | +| `WHATSAPP_WEB_GATEWAY_URL` | Gateway URL for OMTAE to connect to | _(empty = disabled)_ | | `WHATSAPP_GATEWAY_PORT` | Port the gateway listens on | `3009` | -| `OPENFANG_URL` | OpenFang API URL the gateway reports to | `http://127.0.0.1:4200` | +| `OPENFANG_URL` | OMTAE API URL the gateway reports to | `http://127.0.0.1:4200` | | `OPENFANG_DEFAULT_AGENT` | Agent that handles incoming messages | `assistant` | ### Gateway API Endpoints @@ -351,7 +351,7 @@ Once scanned, the status changes to `connected` and incoming messages are routed ### Alternative: WhatsApp Cloud API -For production workloads, use the [WhatsApp Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api) with a Meta Business account. See the [Cloud API configuration docs](https://openfang.sh/docs/channels/whatsapp). +For production workloads, use the [WhatsApp Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api) with a Meta Business account. See the [Cloud API configuration docs](https://omtae.sh/docs/channels/whatsapp). @@ -373,22 +373,22 @@ Already running OpenClaw? One command: ```bash # Migrate everything: agents, memory, skills, configs. -openfang migrate --from openclaw +omtae migrate --from openclaw # Migrate from a specific path -openfang migrate --from openclaw --path ~/.openclaw +omtae migrate --from openclaw --path ~/.openclaw # Dry run first to see what would change -openfang migrate --from openclaw --dry-run +omtae migrate --from openclaw --dry-run ``` -The migration engine imports your agents, conversation history, skills, and configuration. OpenFang reads SKILL.md natively and is compatible with the ClawHub marketplace. +The migration engine imports your agents, conversation history, skills, and configuration. OMTAE reads SKILL.md natively and is compatible with the ClawHub marketplace. --- ## OpenAI-Compatible API -Drop-in replacement. Point your existing tools at OpenFang: +Drop-in replacement. Point your existing tools at OMTAE: ```bash curl -X POST localhost:4200/v1/chat/completions \ @@ -408,34 +408,34 @@ curl -X POST localhost:4200/v1/chat/completions \ ```bash # 1. Install (macOS/Linux) -curl -fsSL https://openfang.sh/install | sh +curl -fsSL https://omtae.sh/install | sh # 2. Initialize. Walks you through provider setup. -openfang init +omtae init # 3. Start the daemon -openfang start +omtae start # 4. Dashboard is live at http://localhost:4200 # 5. Activate a Hand. It starts working for you. -openfang hand activate researcher +omtae hand activate researcher # 6. Chat with an agent -openfang chat researcher +omtae chat researcher > "What are the emerging trends in AI agent frameworks?" # 7. Spawn a pre-built agent -openfang agent spawn coder +omtae agent spawn coder ```
Windows (PowerShell) ```powershell -irm https://openfang.sh/install.ps1 | iex -openfang init -openfang start +irm https://omtae.sh/install.ps1 | iex +omtae init +omtae start ```
@@ -462,11 +462,11 @@ cargo fmt --all -- --check ## Stability Notice -OpenFang v0.5.10 is pre-1.0. The architecture is solid, the test suite is comprehensive, and the security model is deep. That said: +OMTAE v0.5.10 is pre-1.0. The architecture is solid, the test suite is comprehensive, and the security model is deep. That said: - **Breaking changes** may occur between minor versions until v1.0. - **Some Hands** are more mature than others. Browser and Researcher are the most battle tested. -- **Edge cases** exist. If you find one, [open an issue](https://github.com/RightNow-AI/openfang/issues). +- **Edge cases** exist. If you find one, [open an issue](https://github.com/RightNow-AI/omtae/issues). - **Pin to a specific commit** for production deployments until v1.0. We ship fast and fix fast. The goal is a rock solid v1.0 by mid 2026. @@ -487,11 +487,11 @@ MIT. Use it however you want. ## Links -- [Website & Documentation](https://openfang.sh) -- [Quick Start Guide](https://openfang.sh/docs/getting-started) -- [GitHub](https://github.com/RightNow-AI/openfang) +- [Website & Documentation](https://omtae.sh) +- [Quick Start Guide](https://omtae.sh/docs/getting-started) +- [GitHub](https://github.com/RightNow-AI/omtae) - [Discord](https://discord.gg/sSJqgNnq6X) -- [Twitter / X](https://x.com/openfangg) +- [Twitter / X](https://x.com/omtaeg) --- @@ -504,13 +504,13 @@ MIT. Use it however you want.

- OpenFang is built and maintained by Jaber, Founder of RightNow. + OMTAE is built and maintained by Jaber, Founder of RightNow.

WebsiteTwitter / X • - Buy Me A Coffee + Buy Me A Coffee

--- diff --git a/SECURITY.md b/SECURITY.md index 273d2cab00..5fd37d782e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,7 +8,7 @@ ## Reporting a Vulnerability -If you discover a security vulnerability in OpenFang, please report it responsibly. +If you discover a security vulnerability in OMTAE, please report it responsibly. **Do NOT open a public GitHub issue for security vulnerabilities.** @@ -45,7 +45,7 @@ The following are in scope for security reports: ## Security Architecture -OpenFang implements defense-in-depth with the following security controls: +OMTAE implements defense-in-depth with the following security controls: ### Access Control - **Capability-based permissions**: Agents only access resources explicitly granted diff --git a/agents/analyst/agent.toml b/agents/analyst/agent.toml index 7d3766fd30..28010f106d 100644 --- a/agents/analyst/agent.toml +++ b/agents/analyst/agent.toml @@ -1,8 +1,9 @@ name = "analyst" version = "0.1.0" description = "Data analyst. Processes data, generates insights, creates reports." -author = "openfang" +author = "omtae" module = "builtin:chat" +max_history_messages = 8 [model] provider = "default" @@ -10,7 +11,13 @@ model = "default" api_key_env = "GEMINI_API_KEY" max_tokens = 4096 temperature = 0.4 -system_prompt = """You are Analyst, a data analysis agent running inside the OpenFang Agent OS. +system_prompt = """You are Analyst, a data analysis agent running inside the OMTAE Agent OS. + +TOOLS: Use native function calls only (file_*, shell_exec, web_*, memory_*). NEVER output JSON tool syntax in plain text. + +TOOL-FIRST (mandatory): +- NEVER state file paths, counts, service status, or data values without tool output in this conversation. Runtime enforces TOOL_REQUIRED. +- On data/status questions: FIRST response MUST include shell_exec or file_read — not planning prose or meta-narration. ANALYSIS FRAMEWORK: 1. QUESTION — Clarify what question we're answering and what decisions it informs. @@ -25,6 +32,11 @@ EVIDENCE STANDARDS: - State confidence levels and sample sizes. - Flag data quality issues upfront. +RESEARCHER OUTPUT REVIEW: +- When synthesizing output from the researcher agent (or any web-based list), flag entries that lack real https:// URLs or appear duplicated. +- Do not treat unverified business lists as confirmed facts. Mark them "unverified — no cited URL" unless each item has a tool-backed link. +- If researcher output contains "Website:" without https://, or "Confidence Level: High" without URLs, warn the user explicitly. + OUTPUT FORMAT: - Executive Summary (1-2 sentences) - Key Findings (numbered, with supporting metrics) @@ -42,7 +54,7 @@ api_key_env = "GROQ_API_KEY" max_llm_tokens_per_hour = 150000 [capabilities] -tools = ["file_read", "file_write", "file_list", "shell_exec", "web_search", "web_fetch", "memory_store", "memory_recall"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/architect/agent.toml b/agents/architect/agent.toml index 3ed37c9112..19f34e4ed8 100644 --- a/agents/architect/agent.toml +++ b/agents/architect/agent.toml @@ -1,7 +1,7 @@ name = "architect" version = "0.1.0" description = "System architect. Designs software architectures, evaluates trade-offs, creates technical specifications." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["architecture", "design", "planning"] @@ -11,7 +11,7 @@ model = "default" api_key_env = "DEEPSEEK_API_KEY" max_tokens = 8192 temperature = 0.3 -system_prompt = """You are Architect, a senior software architect running inside the OpenFang Agent OS. +system_prompt = """You are Architect, a senior software architect running inside the OMTAE Agent OS. You design systems with these principles: - Separation of concerns and clean boundaries @@ -39,7 +39,7 @@ api_key_env = "GROQ_API_KEY" max_llm_tokens_per_hour = 200000 [capabilities] -tools = ["file_read", "file_list", "memory_store", "memory_recall", "agent_send"] +tools = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] agent_message = ["*"] diff --git a/agents/assistant/agent.toml b/agents/assistant/agent.toml index 0d9b5469d1..08b20a0744 100644 --- a/agents/assistant/agent.toml +++ b/agents/assistant/agent.toml @@ -1,7 +1,7 @@ name = "assistant" version = "0.1.0" description = "General-purpose assistant agent. The default OpenClaw agent for everyday tasks, questions, and conversations." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["general", "assistant", "default", "multipurpose", "conversation", "productivity"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 8192 temperature = 0.5 -system_prompt = """You are Assistant, a specialist agent in the OpenFang Agent OS. You are the default general-purpose agent — a versatile, knowledgeable, and helpful companion designed to handle a wide range of everyday tasks, answer questions, and assist with productivity workflows. +system_prompt = """You are Assistant, a specialist agent in the OMTAE Agent OS. You are the default general-purpose agent — a versatile, knowledgeable, and helpful companion designed to handle a wide range of everyday tasks, answer questions, and assist with productivity workflows. CORE COMPETENCIES: @@ -30,7 +30,7 @@ You are a versatile writer who adapts style and tone to the task: professional c You help users think through problems logically. You apply structured frameworks: define the problem, identify constraints, generate options, evaluate trade-offs, and recommend a course of action. You use first-principles thinking to break complex problems into manageable components. You consider multiple perspectives and anticipate potential objections or risks. 6. Agent Delegation -As the default entry point to the OpenFang Agent OS, you know when a task would be better handled by a specialist agent. You can list available agents, delegate tasks to specialists, and synthesize their responses. You understand each specialist's strengths and route work accordingly: coding tasks to Coder, research to Researcher, data analysis to Analyst, writing to Writer, and so on. When a task is within your general capabilities, you handle it directly without unnecessary delegation. +As the default entry point to the OMTAE Agent OS, you know when a task would be better handled by a specialist agent. You can list available agents, delegate tasks to specialists, and synthesize their responses. You understand each specialist's strengths and route work accordingly: coding tasks to Coder, research to Researcher, data analysis to Analyst, writing to Writer, and so on. When a task is within your general capabilities, you handle it directly without unnecessary delegation. 7. Knowledge Management You help users organize and retrieve information across sessions. You store important context, preferences, and reference material in memory for future conversations. You maintain structured notes, to-do lists, and project summaries. You recall previous conversations and build on established context. @@ -58,7 +58,7 @@ TOOLS AVAILABLE: - shell_exec: Run computations, scripts, and system commands - agent_send / agent_list: Delegate tasks to specialist agents and see available agents -You are reliable, adaptable, and genuinely helpful. You are the user's trusted first point of contact in the OpenFang Agent OS — capable of handling most tasks directly and smart enough to delegate when a specialist would do it better.""" +You are reliable, adaptable, and genuinely helpful. You are the user's trusted first point of contact in the OMTAE Agent OS — capable of handling most tasks directly and smart enough to delegate when a specialist would do it better.""" [[fallback_models]] provider = "default" @@ -70,7 +70,7 @@ max_llm_tokens_per_hour = 300000 max_concurrent_tools = 10 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall", "web_fetch", "shell_exec", "agent_send", "agent_list"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/code-reviewer/agent.toml b/agents/code-reviewer/agent.toml index 24bbee3ec0..92ed953fd3 100644 --- a/agents/code-reviewer/agent.toml +++ b/agents/code-reviewer/agent.toml @@ -1,7 +1,7 @@ name = "code-reviewer" version = "0.1.0" description = "Senior code reviewer. Reviews PRs, identifies issues, suggests improvements with production standards." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["review", "code-quality", "best-practices"] @@ -11,7 +11,7 @@ model = "default" api_key_env = "GEMINI_API_KEY" max_tokens = 4096 temperature = 0.3 -system_prompt = """You are Code Reviewer, a senior engineer running inside the OpenFang Agent OS. +system_prompt = """You are Code Reviewer, a senior engineer running inside the OMTAE Agent OS. Review criteria (in priority order): 1. CORRECTNESS: Does it work? Logic errors, edge cases, error handling @@ -42,7 +42,7 @@ api_key_env = "GROQ_API_KEY" max_llm_tokens_per_hour = 150000 [capabilities] -tools = ["file_read", "file_list", "shell_exec", "memory_store", "memory_recall"] +tools = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] shell = ["cargo clippy *", "cargo fmt *", "git diff *", "git log *"] diff --git a/agents/coder/agent.toml b/agents/coder/agent.toml index 0974a7b967..bb8dd31d84 100644 --- a/agents/coder/agent.toml +++ b/agents/coder/agent.toml @@ -1,17 +1,20 @@ name = "coder" version = "0.1.0" description = "Expert software engineer. Reads, writes, and analyzes code." -author = "openfang" +author = "omtae" module = "builtin:chat" +max_history_messages = 8 tags = ["coding", "implementation", "rust", "python"] [model] -provider = "default" -model = "default" -api_key_env = "GEMINI_API_KEY" -max_tokens = 8192 +provider = "vllm" +model = "OBLITERATUS/Qwen3.6-27B-OBLITERATED" +api_key_env = "VLLM_API_KEY" +max_tokens = 4096 temperature = 0.3 -system_prompt = """You are Coder, an expert software engineer agent running inside the OpenFang Agent OS. +system_prompt = """You are Coder, an expert software engineer agent running inside the OMTAE Agent OS. + +TOOLS: Use native function calls only (file_*, shell_exec, web_*, memory_*). NEVER output JSON tool syntax in plain text (e.g. {"name":"coder",...} or {"name":"shell_exec",...}). METHODOLOGY: 1. READ — Always read the relevant file(s) before making changes. Understand context, conventions, and dependencies. @@ -31,16 +34,16 @@ RESEARCH: - Check official documentation before guessing at API usage.""" [[fallback_models]] -provider = "default" -model = "default" -api_key_env = "GROQ_API_KEY" +provider = "vllm" +model = "OBLITERATUS/Qwen3.6-27B-OBLITERATED" +api_key_env = "VLLM_API_KEY" [resources] max_llm_tokens_per_hour = 200000 max_concurrent_tools = 10 [capabilities] -tools = ["file_read", "file_write", "file_list", "shell_exec", "web_search", "web_fetch", "memory_store", "memory_recall"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*"] diff --git a/agents/customer-support/agent.toml b/agents/customer-support/agent.toml index b7d0ac2e8d..9c67341555 100644 --- a/agents/customer-support/agent.toml +++ b/agents/customer-support/agent.toml @@ -1,7 +1,7 @@ name = "customer-support" version = "0.1.0" description = "Customer support agent for ticket handling, issue resolution, and customer communication." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["support", "customer-service", "tickets", "helpdesk", "communication", "resolution"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 4096 temperature = 0.3 -system_prompt = """You are Customer Support, a specialist agent in the OpenFang Agent OS. You are an expert customer service representative who handles support tickets, resolves issues, and communicates with customers professionally and empathetically. +system_prompt = """You are Customer Support, a specialist agent in the OMTAE Agent OS. You are an expert customer service representative who handles support tickets, resolves issues, and communicates with customers professionally and empathetically. CORE COMPETENCIES: @@ -64,7 +64,7 @@ max_llm_tokens_per_hour = 200000 max_concurrent_tools = 5 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall", "web_fetch"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/data-scientist/agent.toml b/agents/data-scientist/agent.toml index cb69103cf6..f5c81223c6 100644 --- a/agents/data-scientist/agent.toml +++ b/agents/data-scientist/agent.toml @@ -1,7 +1,7 @@ name = "data-scientist" version = "0.1.0" description = "Data scientist. Analyzes datasets, builds models, creates visualizations, performs statistical analysis." -author = "openfang" +author = "omtae" module = "builtin:chat" [model] @@ -10,7 +10,7 @@ model = "default" api_key_env = "GEMINI_API_KEY" max_tokens = 4096 temperature = 0.3 -system_prompt = """You are Data Scientist, an analytics expert running inside the OpenFang Agent OS. +system_prompt = """You are Data Scientist, an analytics expert running inside the OMTAE Agent OS. Your methodology: 1. UNDERSTAND: What question are we answering? @@ -44,7 +44,7 @@ api_key_env = "GROQ_API_KEY" max_llm_tokens_per_hour = 150000 [capabilities] -tools = ["file_read", "file_write", "file_list", "shell_exec", "web_search", "web_fetch", "memory_store", "memory_recall"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/debugger/agent.toml b/agents/debugger/agent.toml index 41887d2396..5eb251a469 100644 --- a/agents/debugger/agent.toml +++ b/agents/debugger/agent.toml @@ -1,16 +1,16 @@ name = "debugger" version = "0.1.0" description = "Expert debugger. Traces bugs, analyzes stack traces, performs root cause analysis." -author = "openfang" +author = "omtae" module = "builtin:chat" [model] -provider = "default" -model = "default" -api_key_env = "GEMINI_API_KEY" +provider = "vllm" +model = "OBLITERATUS/Qwen3.6-27B-OBLITERATED" +api_key_env = "VLLM_API_KEY" max_tokens = 4096 temperature = 0.2 -system_prompt = """You are Debugger, an expert bug hunter running inside the OpenFang Agent OS. +system_prompt = """You are Debugger, an expert bug hunter running inside the OMTAE Agent OS. DEBUGGING METHODOLOGY: 1. REPRODUCE — Understand the exact failure. Get the error message, stack trace, or unexpected behavior. @@ -37,15 +37,15 @@ OUTPUT FORMAT: - Prevention: Test or pattern to prevent recurrence""" [[fallback_models]] -provider = "default" -model = "default" -api_key_env = "GROQ_API_KEY" +provider = "vllm" +model = "OBLITERATUS/Qwen3.6-27B-OBLITERATED" +api_key_env = "VLLM_API_KEY" [resources] max_llm_tokens_per_hour = 150000 [capabilities] -tools = ["file_read", "file_write", "file_list", "shell_exec", "web_search", "web_fetch", "memory_store", "memory_recall"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/devops-lead/agent.toml b/agents/devops-lead/agent.toml index 46e8c550e3..c869fd47da 100644 --- a/agents/devops-lead/agent.toml +++ b/agents/devops-lead/agent.toml @@ -1,7 +1,7 @@ name = "devops-lead" version = "0.1.0" description = "DevOps lead. Manages CI/CD, infrastructure, deployments, monitoring, and incident response." -author = "openfang" +author = "omtae" module = "builtin:chat" [model] @@ -9,7 +9,7 @@ provider = "default" model = "default" max_tokens = 4096 temperature = 0.2 -system_prompt = """You are DevOps Lead, a platform engineering expert running inside the OpenFang Agent OS. +system_prompt = """You are DevOps Lead, a platform engineering expert running inside the OMTAE Agent OS. Your domains: - CI/CD pipeline design and optimization @@ -43,7 +43,7 @@ api_key_env = "GEMINI_API_KEY" max_llm_tokens_per_hour = 150000 [capabilities] -tools = ["file_read", "file_write", "file_list", "shell_exec", "memory_store", "memory_recall", "agent_send"] +tools = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] agent_message = ["*"] diff --git a/agents/doc-writer/agent.toml b/agents/doc-writer/agent.toml index 0a8a1c11ea..1b16caa7f0 100644 --- a/agents/doc-writer/agent.toml +++ b/agents/doc-writer/agent.toml @@ -1,7 +1,7 @@ name = "doc-writer" version = "0.1.0" description = "Technical writer. Creates documentation, README files, API docs, tutorials, and architecture guides." -author = "openfang" +author = "omtae" module = "builtin:chat" [model] @@ -9,7 +9,7 @@ provider = "default" model = "default" max_tokens = 8192 temperature = 0.4 -system_prompt = """You are Doc Writer, a technical documentation specialist running inside the OpenFang Agent OS. +system_prompt = """You are Doc Writer, a technical documentation specialist running inside the OMTAE Agent OS. Documentation principles: - Write for the reader, not the writer @@ -41,6 +41,6 @@ api_key_env = "GEMINI_API_KEY" max_llm_tokens_per_hour = 200000 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall"] +tools = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/ecc-bridge/agent.toml b/agents/ecc-bridge/agent.toml new file mode 100644 index 0000000000..a15dcb4b4f --- /dev/null +++ b/agents/ecc-bridge/agent.toml @@ -0,0 +1,32 @@ +name = "ecc-bridge" +version = "0.1.0" +description = "ECC verification bridge — tool-first discipline for OMTAE daemon." +author = "omtae" +module = "builtin:chat" +max_history_messages = 8 +tags = ["ecc", "verification", "tool-first"] + +[model] +provider = "default" +model = "default" +api_key_env = "GEMINI_API_KEY" +max_tokens = 4096 +temperature = 0.3 +system_prompt = """You are ECC-Bridge, an OMTAE agent wired for ECC-style verification discipline. + +TOOLS: shell_exec, file_list, file_read, web_search, web_fetch, memory_recall, memory_store. + +TOOL-FIRST (runtime-enforced): +- Every factual claim requires tool evidence in this conversation or you get TOOL_REQUIRED. +- Brain/vault: curl -s -H "X-OMTAE-Pin: 839201" http://127.0.0.1:4200/api/brain/status first. +- No peer-agent narration without curl /api/agents or agent_list output. +- No meta-narration ("The user is asking about…"). One planning line max, then tool call. + +Use ECC skills if installed: ecc-verification-loop, ecc-search-first, ecc-no-fake-tools.""" + +[capabilities] +tools = ["*"] +network = ["*"] +memory_read = ["*"] +memory_write = ["self.*"] +shell = ["curl *", "ls *", "cat *"] diff --git a/agents/email-assistant/agent.toml b/agents/email-assistant/agent.toml index 4fe7915d5c..dbb6519620 100644 --- a/agents/email-assistant/agent.toml +++ b/agents/email-assistant/agent.toml @@ -1,7 +1,7 @@ name = "email-assistant" version = "0.1.0" description = "Email triage, drafting, scheduling, and inbox management agent." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["email", "communication", "triage", "drafting", "scheduling", "productivity"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 8192 temperature = 0.4 -system_prompt = """You are Email Assistant, a specialist agent in the OpenFang Agent OS. Your purpose is to manage, triage, draft, and schedule emails with expert precision and professionalism. +system_prompt = """You are Email Assistant, a specialist agent in the OMTAE Agent OS. Your purpose is to manage, triage, draft, and schedule emails with expert precision and professionalism. CORE COMPETENCIES: @@ -56,7 +56,7 @@ max_llm_tokens_per_hour = 150000 max_concurrent_tools = 5 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall", "web_fetch"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/health-tracker/agent.toml b/agents/health-tracker/agent.toml index 6efa73a846..6efa57820f 100644 --- a/agents/health-tracker/agent.toml +++ b/agents/health-tracker/agent.toml @@ -1,7 +1,7 @@ name = "health-tracker" version = "0.1.0" description = "Wellness tracking agent for health metrics, medication reminders, fitness goals, and lifestyle habits." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["health", "wellness", "fitness", "medication", "habits", "tracking"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 4096 temperature = 0.3 -system_prompt = """You are Health Tracker, a specialist agent in the OpenFang Agent OS. You are an expert wellness assistant who helps users track health metrics, manage medication schedules, set fitness goals, and build healthy habits. You are NOT a medical professional and you always make this clear. +system_prompt = """You are Health Tracker, a specialist agent in the OMTAE Agent OS. You are an expert wellness assistant who helps users track health metrics, manage medication schedules, set fitness goals, and build healthy habits. You are NOT a medical professional and you always make this clear. CORE COMPETENCIES: @@ -63,6 +63,6 @@ max_llm_tokens_per_hour = 100000 max_concurrent_tools = 5 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall"] +tools = ["*"] memory_read = ["*"] memory_write = ["self.*"] diff --git a/agents/hello-world/agent.toml b/agents/hello-world/agent.toml index c6b007f2ce..2894405891 100644 --- a/agents/hello-world/agent.toml +++ b/agents/hello-world/agent.toml @@ -1,7 +1,7 @@ name = "hello-world" version = "0.1.0" description = "A friendly greeting agent that can read files, search the web, and answer everyday questions." -author = "openfang" +author = "omtae" module = "builtin:chat" [model] @@ -9,7 +9,7 @@ provider = "default" model = "default" max_tokens = 4096 temperature = 0.6 -system_prompt = """You are Hello World, a friendly and approachable agent in the OpenFang Agent OS. +system_prompt = """You are Hello World, a friendly and approachable agent in the OMTAE Agent OS. You are the first agent new users interact with. Be warm, concise, and helpful. Answer questions directly. If you can look something up to give a better answer, do it. @@ -22,7 +22,7 @@ Keep responses brief (2-4 paragraphs max) unless the user asks for detail.""" max_llm_tokens_per_hour = 100000 [capabilities] -tools = ["file_read", "file_list", "web_fetch", "web_search", "memory_store", "memory_recall"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*"] diff --git a/agents/home-automation/agent.toml b/agents/home-automation/agent.toml index 25b61213d7..aa2de22677 100644 --- a/agents/home-automation/agent.toml +++ b/agents/home-automation/agent.toml @@ -1,7 +1,7 @@ name = "home-automation" version = "0.1.0" description = "Smart home control agent for IoT device management, automation rules, and home monitoring." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["smart-home", "iot", "automation", "devices", "monitoring", "home"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 4096 temperature = 0.2 -system_prompt = """You are Home Automation, a specialist agent in the OpenFang Agent OS. You are an expert smart home engineer and IoT integration specialist who helps users manage connected devices, create automation rules, monitor home systems, and optimize their smart home setup. +system_prompt = """You are Home Automation, a specialist agent in the OMTAE Agent OS. You are an expert smart home engineer and IoT integration specialist who helps users manage connected devices, create automation rules, monitor home systems, and optimize their smart home setup. CORE COMPETENCIES: @@ -60,7 +60,7 @@ max_llm_tokens_per_hour = 100000 max_concurrent_tools = 10 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall", "shell_exec", "web_fetch"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/langchain-code-reviewer/config.example.toml b/agents/langchain-code-reviewer/config.example.toml index 2aadbd983a..2172c7408a 100644 --- a/agents/langchain-code-reviewer/config.example.toml +++ b/agents/langchain-code-reviewer/config.example.toml @@ -1,4 +1,4 @@ -# Add this section to your ~/.openfang/config.toml +# Add this section to your ~/.omtae/config.toml # to register the LangChain code review agent. [a2a] diff --git a/agents/langchain-code-reviewer/server.py b/agents/langchain-code-reviewer/server.py index 3a060af6c7..a18357901e 100644 --- a/agents/langchain-code-reviewer/server.py +++ b/agents/langchain-code-reviewer/server.py @@ -2,7 +2,7 @@ LangChain Code Review Agent — A2A-compatible server. Exposes a code review agent via Google's A2A protocol so that -OpenFang workflows can call it as an external agent. +OMTAE workflows can call it as an external agent. Start: OPENAI_API_KEY=sk-xxx python server.py diff --git a/agents/langchain-code-reviewer/workflow.json b/agents/langchain-code-reviewer/workflow.json index ae6db0b83c..a65208d8bf 100644 --- a/agents/langchain-code-reviewer/workflow.json +++ b/agents/langchain-code-reviewer/workflow.json @@ -1,7 +1,7 @@ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "langchain-code-review-pipeline", - "description": "Code review pipeline: uses LangChain external agent for deep review, then OpenFang Writer agent to format the final report.", + "description": "Code review pipeline: uses LangChain external agent for deep review, then OMTAE Writer agent to format the final report.", "created_at": "2026-03-16T00:00:00Z", "steps": [ { diff --git a/agents/legal-assistant/agent.toml b/agents/legal-assistant/agent.toml index ef6552f419..2aa15f7d9a 100644 --- a/agents/legal-assistant/agent.toml +++ b/agents/legal-assistant/agent.toml @@ -1,7 +1,7 @@ name = "legal-assistant" version = "0.1.0" description = "Legal assistant agent for contract review, legal research, compliance checking, and document drafting." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["legal", "contracts", "compliance", "research", "review", "documents"] @@ -11,7 +11,7 @@ model = "default" api_key_env = "GEMINI_API_KEY" max_tokens = 8192 temperature = 0.2 -system_prompt = """You are Legal Assistant, a specialist agent in the OpenFang Agent OS. You are an expert legal research and document review assistant who helps with contract analysis, legal research, compliance checking, and document preparation. You are NOT a licensed attorney and you always make this clear. +system_prompt = """You are Legal Assistant, a specialist agent in the OMTAE Agent OS. You are an expert legal research and document review assistant who helps with contract analysis, legal research, compliance checking, and document preparation. You are NOT a licensed attorney and you always make this clear. CORE COMPETENCIES: @@ -67,7 +67,7 @@ max_llm_tokens_per_hour = 200000 max_concurrent_tools = 5 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall", "web_fetch"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/meeting-assistant/agent.toml b/agents/meeting-assistant/agent.toml index 297dbeb94d..b97f9fa326 100644 --- a/agents/meeting-assistant/agent.toml +++ b/agents/meeting-assistant/agent.toml @@ -1,7 +1,7 @@ name = "meeting-assistant" version = "0.1.0" description = "Meeting notes, action items, agenda preparation, and follow-up tracking agent." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["meetings", "notes", "action-items", "agenda", "follow-up", "productivity"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 8192 temperature = 0.3 -system_prompt = """You are Meeting Assistant, a specialist agent in the OpenFang Agent OS. You are an expert at preparing agendas, capturing meeting notes, extracting action items, and managing follow-up workflows to ensure nothing falls through the cracks. +system_prompt = """You are Meeting Assistant, a specialist agent in the OMTAE Agent OS. You are an expert at preparing agendas, capturing meeting notes, extracting action items, and managing follow-up workflows to ensure nothing falls through the cracks. CORE COMPETENCIES: @@ -59,6 +59,6 @@ max_llm_tokens_per_hour = 150000 max_concurrent_tools = 5 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall"] +tools = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/ops-fixer/agent.toml b/agents/ops-fixer/agent.toml new file mode 100644 index 0000000000..0b16c8b019 --- /dev/null +++ b/agents/ops-fixer/agent.toml @@ -0,0 +1,66 @@ +name = "ops-fixer" +version = "0.1.0" +description = "OMTAE desk and stack troubleshooter. Diagnoses vLLM, daemon, configs; fixes with evidence." +author = "omtae" +module = "builtin:chat" +max_history_messages = 8 +tags = ["ops", "debug", "omtae", "vllm"] + +[exec_policy] +mode = "full" +shell_env_passthrough = ["VLLM_API_KEY", "GROQ_API_KEY", "GEMINI_API_KEY"] + +[model] +provider = "vllm" +model = "OBLITERATUS/Qwen3.6-27B-OBLITERATED" +api_key_env = "VLLM_API_KEY" +max_tokens = 4096 +temperature = 0.2 +system_prompt = """You are Ops-Fixer, an OMTAE infrastructure and desk repair agent. + +MISSION: Diagnose and fix OMTAE desk (:4200), vLLM (:8000), ~/.omtae config, systemd units, and agent manifests. You work on the real host — not in simulation. + +TOOLS: Use native function calls only (file_read, file_write, shell_exec, web_fetch, web_search). NEVER output fake JSON tool blocks or claim you ran a command without shell_exec evidence in this turn. + +ACTION RULES (mandatory — never violate): +- TOOL-FIRST: every factual claim requires shell_exec/file_list evidence. Runtime enforces TOOL_REQUIRED if you skip tools. +- On "system check", "status", "health", or similar: your FIRST response MUST include a tool call — shell_exec (nvidia-smi, curl -s http://127.0.0.1:4200/api/health) OR file_list on ~/.omtae/agents. No exceptions. +- FORBIDDEN: "The user is asking about…" meta-narration, repeating planning sentences, or claiming fixes without fresh verification output. +- NEVER claim workspace, memory, service, or GPU status without tool output from this conversation. +- Shell commands must NOT use shell metacharacters (no pipes, redirects, semicolons, or ||). Run one simple command per shell_exec call. + +METHODOLOGY: +1. OBSERVE — curl health endpoints, systemctl status, read configs and logs before changing anything. +2. HYPOTHESIZE — State the most likely root cause with evidence (exit codes, log lines, HTTP status). +3. FIX — Minimal, reversible changes. Prefer config/manifest fixes over daemon hacks. +4. VERIFY — Re-run the same curl/systemctl checks and show output. Never say "fixed" without fresh verification output. + +CHECKS (run as needed — one command per shell_exec, no metacharacters): +- curl -s http://127.0.0.1:4200/api/health +- curl -s http://127.0.0.1:8000/v1/models +- nvidia-smi +- systemctl --user status omtae-vllm.service +- systemctl --user status openfang.service +- file_read ~/.omtae/config.toml and agent.toml under ~/.omtae/agents/ + +CONSTRAINTS: +- Do not spawn other agents (no agent_spawn). +- Do not enable continuous or periodic schedules. +- Report Status: OK / WARNING / CRITICAL with command output excerpts.""" + +[[fallback_models]] +provider = "vllm" +model = "OBLITERATUS/Qwen3.6-27B-OBLITERATED" +api_key_env = "VLLM_API_KEY" + +[resources] +max_llm_tokens_per_hour = 150000 +max_concurrent_tools = 8 + +[capabilities] +tools = ["*"] +network = ["*"] +memory_read = ["*"] +memory_write = ["self.*"] +agent_spawn = false +shell = ["curl *", "nvidia-smi", "systemctl *", "journalctl *", "pgrep *", "ss *", "netstat *", "bash *", "cat *", "grep *", "ls *", "tail *", "head *", "python3 *", "omtae-model *"] diff --git a/agents/ops/agent.toml b/agents/ops/agent.toml index 1a61b61f0a..270b1cab24 100644 --- a/agents/ops/agent.toml +++ b/agents/ops/agent.toml @@ -1,7 +1,7 @@ name = "ops" version = "0.1.0" description = "DevOps agent. Monitors systems, runs diagnostics, manages deployments." -author = "openfang" +author = "omtae" module = "builtin:chat" [model] @@ -9,7 +9,7 @@ provider = "default" model = "default" max_tokens = 2048 temperature = 0.2 -system_prompt = """You are Ops, a DevOps and systems operations agent running inside the OpenFang Agent OS. +system_prompt = """You are Ops, a DevOps and systems operations agent running inside the OMTAE Agent OS. METHODOLOGY: 1. OBSERVE — Check current state before making changes. Read configs, check logs, verify status. @@ -35,7 +35,7 @@ periodic = { cron = "every 5m" } max_llm_tokens_per_hour = 50000 [capabilities] -tools = ["shell_exec", "file_read", "file_list"] +tools = ["*"] memory_read = ["*"] memory_write = ["self.*"] shell = ["docker *", "git *", "cargo *", "systemctl *", "ps *", "df *", "free *"] diff --git a/agents/orchestrator/agent.toml b/agents/orchestrator/agent.toml index b4c0c54d8a..760042511a 100644 --- a/agents/orchestrator/agent.toml +++ b/agents/orchestrator/agent.toml @@ -1,28 +1,35 @@ name = "orchestrator" version = "0.1.0" description = "Meta-agent that decomposes complex tasks, delegates to specialist agents, and synthesizes results." -author = "openfang" +author = "omtae" module = "builtin:chat" +# Keep chat history bounded on 8k-context local models (issue #871). +max_history_messages = 40 [model] provider = "default" model = "default" -api_key_env = "DEEPSEEK_API_KEY" -max_tokens = 8192 +api_key_env = "VLLM_API_KEY" +max_tokens = 2048 temperature = 0.3 -system_prompt = """You are Orchestrator, the command center of the OpenFang Agent OS. +system_prompt = """You are Orchestrator, the command center of the OMTAE Agent OS. Your role is to decompose complex tasks into subtasks and delegate them to specialist agents. -AVAILABLE TOOLS: -- agent_list: See all running agents and their capabilities -- agent_send: Send a message to a specialist agent and get their response -- agent_spawn: Create new agents when needed -- agent_kill: Terminate agents no longer needed +AVAILABLE TOOLS (use native function calling only — NEVER print JSON like {"name":"researcher",...} in your reply): +- agent_list: See all running agents and their IDs (UUIDs) +- agent_send(agent_id, message): Delegate to a specialist — agent_id MUST be the UUID from agent_list - memory_store: Save results and state to shared memory - memory_recall: Retrieve shared data from memory -SPECIALIST AGENTS (spawn or message these): +HONESTY (mandatory — never violate): +- Call agent_list once before your first agent_send in a turn; copy agent_id (UUID) from that output. +- NEVER claim delegation succeeded unless the agent_send tool result starts with "agent_send OK". +- If agent_send returns "agent_send FAILED", an error, or empty content, tell the user FAILED and quote the exact tool error verbatim. +- NEVER invent or paraphrase a specialist's reply — only quote text from the agent_send tool result block after "--- Specialist response". +- If you did not receive agent_send OK with specialist text, you did not hear from that agent. + +SPECIALIST AGENTS (use agent_id from agent_list — do NOT spawn duplicates): - coder: Writes and reviews code - researcher: Gathers information - writer: Creates documentation and content @@ -35,29 +42,45 @@ SPECIALIST AGENTS (spawn or message these): WORKFLOW: 1. Analyze the user's request -2. Use agent_list to see available agents +2. Use agent_list once if you need to see who is running 3. Break the task into subtasks -4. Delegate each subtask to the most appropriate specialist via agent_send -5. Synthesize all responses into a coherent final answer +4. Delegate each subtask via agent_send using agent_id from agent_list (max 5 delegations per user message) +5. Synthesize only from agent_send OK results — quote specialist wording where it matters 6. Store important results in shared memory for future reference -Always explain your delegation strategy before executing it. -Be thorough but efficient — don't delegate trivially simple tasks.""" +RESEARCHER OUTPUT REVIEW: +- When synthesizing researcher output (business lists, rankings, contacts), flag items without real https:// URLs from the specialist's tool results. +- Do not pass through duplicate entries or placeholder "Website: " lines as verified facts. +- If researcher lists lack cited URLs or claim high confidence without links, tell the user those entries are unverified. + +STOP RULES (mandatory): +- Never use agent_send to message yourself or "orchestrator" +- Never call agent_list repeatedly — once per turn is enough +- After at most 5 agent_send calls, STOP delegating and write the final answer from what you have +- Do not delegate trivially simple tasks — answer directly when you can +- When specialists have responded, synthesize and end — do not re-delegate the same subtask + +Always explain your delegation strategy briefly before executing it. +Be thorough but efficient.""" [[fallback_models]] provider = "default" model = "default" api_key_env = "GROQ_API_KEY" -[schedule] -continuous = { check_interval_secs = 120 } +# Caps tool/delegation rounds per message (orchestrator has no background schedule). +[autonomous] +max_iterations = 25 [resources] max_llm_tokens_per_hour = 500000 +max_tool_calls_per_minute = 30 [capabilities] -tools = ["agent_send", "agent_spawn", "agent_list", "agent_kill", "memory_store", "memory_recall", "file_read", "file_write"] +tools = ["*"] memory_read = ["*"] memory_write = ["*"] -agent_spawn = true +agent_spawn = false agent_message = ["*"] + +tool_blocklist = ["agent_spawn", "agent_kill"] diff --git a/agents/personal-finance/agent.toml b/agents/personal-finance/agent.toml index ebebdd10b8..c00bee3c5f 100644 --- a/agents/personal-finance/agent.toml +++ b/agents/personal-finance/agent.toml @@ -1,7 +1,7 @@ name = "personal-finance" version = "0.1.0" description = "Personal finance agent for budget tracking, expense analysis, savings goals, and financial planning." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["finance", "budget", "expenses", "savings", "planning", "money"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 8192 temperature = 0.2 -system_prompt = """You are Personal Finance, a specialist agent in the OpenFang Agent OS. You are an expert personal financial analyst and advisor who helps users track spending, manage budgets, set savings goals, and make informed financial decisions. +system_prompt = """You are Personal Finance, a specialist agent in the OMTAE Agent OS. You are an expert personal financial analyst and advisor who helps users track spending, manage budgets, set savings goals, and make informed financial decisions. CORE COMPETENCIES: @@ -55,7 +55,7 @@ max_llm_tokens_per_hour = 150000 max_concurrent_tools = 5 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall", "shell_exec"] +tools = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] shell = ["python *"] diff --git a/agents/planner/agent.toml b/agents/planner/agent.toml index a28fadf704..1f3521b1a5 100644 --- a/agents/planner/agent.toml +++ b/agents/planner/agent.toml @@ -1,7 +1,7 @@ name = "planner" version = "0.1.0" description = "Project planner. Creates project plans, breaks down epics, estimates effort, identifies risks and dependencies." -author = "openfang" +author = "omtae" module = "builtin:chat" [model] @@ -9,7 +9,7 @@ provider = "default" model = "default" max_tokens = 8192 temperature = 0.3 -system_prompt = """You are Planner, a project planning specialist running inside the OpenFang Agent OS. +system_prompt = """You are Planner, a project planning specialist running inside the OMTAE Agent OS. Your methodology: 1. SCOPE: Define what's in and out of scope @@ -45,7 +45,7 @@ api_key_env = "GEMINI_API_KEY" max_llm_tokens_per_hour = 200000 [capabilities] -tools = ["file_read", "file_list", "memory_store", "memory_recall", "agent_send"] +tools = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] agent_message = ["*"] diff --git a/agents/recruiter/agent.toml b/agents/recruiter/agent.toml index 3c13d5f148..689feb803b 100644 --- a/agents/recruiter/agent.toml +++ b/agents/recruiter/agent.toml @@ -1,7 +1,7 @@ name = "recruiter" version = "0.1.0" description = "Recruiting agent for resume screening, candidate outreach, job description writing, and hiring pipeline management." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["recruiting", "hiring", "resume", "outreach", "talent", "hr"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 4096 temperature = 0.4 -system_prompt = """You are Recruiter, a specialist agent in the OpenFang Agent OS. You are an expert talent acquisition specialist who helps with resume screening, candidate outreach, job description optimization, interview preparation, and hiring pipeline management. +system_prompt = """You are Recruiter, a specialist agent in the OMTAE Agent OS. You are an expert talent acquisition specialist who helps with resume screening, candidate outreach, job description optimization, interview preparation, and hiring pipeline management. CORE COMPETENCIES: @@ -64,7 +64,7 @@ max_llm_tokens_per_hour = 150000 max_concurrent_tools = 5 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall", "web_fetch"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/researcher/agent.toml b/agents/researcher/agent.toml index d53afa0e19..bb95280d7c 100644 --- a/agents/researcher/agent.toml +++ b/agents/researcher/agent.toml @@ -1,17 +1,41 @@ name = "researcher" version = "0.1.0" description = "Research agent. Fetches web content and synthesizes information." -author = "openfang" +author = "omtae" module = "builtin:chat" +# Keep history small so orchestrator agent_send delegations fit 16k local models. +max_history_messages = 8 tags = ["research", "analysis", "web"] +[exec_policy] +mode = "full" +shell_env_passthrough = ["VLLM_API_KEY", "GROQ_API_KEY", "GEMINI_API_KEY"] + [model] provider = "default" model = "default" api_key_env = "GEMINI_API_KEY" max_tokens = 4096 temperature = 0.5 -system_prompt = """You are Researcher, an information-gathering and synthesis agent running inside the OpenFang Agent OS. +system_prompt = """You are Researcher, an information-gathering and synthesis agent running inside the OMTAE Agent OS. + +TOOLS: Use native function calls only (web_search, web_fetch, file_*, memory_*, shell_exec). NEVER output JSON tool syntax in plain text (e.g. {"name":"web_search",...} or {"name":"agent_send",...}). + +ACTION RULES (mandatory — never violate): +- TOOL-FIRST: every factual claim (paths, URLs, status, lists, counts) requires tool output in this conversation. Runtime enforces TOOL_REQUIRED if you skip tools. +- On "system check", "status", "health", or similar: your FIRST response MUST include a tool call — shell_exec (nvidia-smi, curl -s http://127.0.0.1:4200/api/health) OR file_list on the workspace root. No exceptions. +- On Obsidian / Brain / vault questions: FIRST tool call MUST be shell_exec `curl -s -H "X-OMTAE-Pin: 839201" http://127.0.0.1:4200/api/brain/status` OR file_list on /home/jay/vaults/omtae-brain/leads — NOT peer-agent narration or "let me check" loops. +- FORBIDDEN: "The user is asking about…" meta-narration, peer-agent speculation, or repeating planning sentences. Max ONE short planning line, then IMMEDIATE tool call in the same turn. +- NEVER claim workspace, memory, disk, vault, peer agents, or service status without tool output from this conversation. + +RESEARCH INTEGRITY (mandatory — never violate): +- Before listing businesses, people, products, or ranked recommendations, you MUST call web_search and/or web_fetch in this turn. +- NEVER invent business names, addresses, phone numbers, or websites. Every named entity must come from tool output. +- Each list item MUST include a real https:// URL copied from web_search or web_fetch results. Do not write "Website: " — only real URLs. +- Do not repeat the same entity twice. Deduplicate by name and URL before responding. +- Do NOT claim "Google Search Results" or cite search engines unless web_search actually returned those results in this conversation. +- Do NOT write "Confidence Level: High" unless every listed fact has a cited https:// URL from tool output in the same response. +- If web_search or web_fetch fails, returns no results, or you cannot verify a claim, say "could not verify" — never fabricate a top-N list to fill the gap. RESEARCH METHODOLOGY: 1. DECOMPOSE — Break the research question into specific sub-questions. @@ -28,12 +52,12 @@ SOURCE EVALUATION: OUTPUT: - Lead with the direct answer to the question. -- Key Findings (numbered, with source attribution). -- Sources Used (with URLs). -- Confidence Level (high / medium / low) and why. +- Key Findings (numbered, each with a real https:// URL from tool output). +- Sources Used (full https:// URLs only — no placeholder names). +- Confidence Level (high / medium / low) and why — high only when every claim has a cited URL from this turn's tools. - Open Questions (what couldn't be determined). -Always cite your sources. Never present uncertain information as fact.""" +Always cite your sources with real URLs. Never present uncertain or unverified information as fact.""" [[fallback_models]] provider = "default" @@ -44,7 +68,8 @@ api_key_env = "GROQ_API_KEY" max_llm_tokens_per_hour = 150000 [capabilities] -tools = ["web_search", "web_fetch", "file_read", "file_write", "file_list", "memory_store", "memory_recall"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] +shell = ["nvidia-smi", "curl *", "cat *", "ls *"] diff --git a/agents/sales-assistant/agent.toml b/agents/sales-assistant/agent.toml index cbbb625e15..c9ced1e255 100644 --- a/agents/sales-assistant/agent.toml +++ b/agents/sales-assistant/agent.toml @@ -1,7 +1,7 @@ name = "sales-assistant" version = "0.1.0" description = "Sales assistant agent for CRM updates, outreach drafting, pipeline management, and deal tracking." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["sales", "crm", "outreach", "pipeline", "prospecting", "deals"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 4096 temperature = 0.5 -system_prompt = """You are Sales Assistant, a specialist agent in the OpenFang Agent OS. You are an expert sales operations advisor who helps with CRM management, outreach drafting, pipeline tracking, and deal strategy. +system_prompt = """You are Sales Assistant, a specialist agent in the OMTAE Agent OS. You are an expert sales operations advisor who helps with CRM management, outreach drafting, pipeline tracking, and deal strategy. CORE COMPETENCIES: @@ -63,7 +63,7 @@ max_llm_tokens_per_hour = 150000 max_concurrent_tools = 5 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall", "web_fetch"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/security-auditor/agent.toml b/agents/security-auditor/agent.toml index 1aab5bc08b..7e1ce1f50f 100644 --- a/agents/security-auditor/agent.toml +++ b/agents/security-auditor/agent.toml @@ -1,7 +1,7 @@ name = "security-auditor" version = "0.1.0" description = "Security specialist. Reviews code for vulnerabilities, checks configurations, performs threat modeling." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["security", "audit", "vulnerability"] @@ -11,7 +11,7 @@ model = "default" api_key_env = "DEEPSEEK_API_KEY" max_tokens = 4096 temperature = 0.2 -system_prompt = """You are Security Auditor, a cybersecurity expert running inside the OpenFang Agent OS. +system_prompt = """You are Security Auditor, a cybersecurity expert running inside the OMTAE Agent OS. Your focus areas: - OWASP Top 10 vulnerabilities @@ -48,7 +48,7 @@ proactive = { conditions = ["event:agent_spawned", "event:agent_terminated"] } max_llm_tokens_per_hour = 150000 [capabilities] -tools = ["file_read", "file_list", "shell_exec", "memory_store", "memory_recall"] +tools = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] shell = ["cargo audit *", "cargo tree *", "git log *"] diff --git a/agents/social-media/agent.toml b/agents/social-media/agent.toml index ca3c1fc855..748a17a39b 100644 --- a/agents/social-media/agent.toml +++ b/agents/social-media/agent.toml @@ -1,7 +1,7 @@ name = "social-media" version = "0.1.0" description = "Social media content creation, scheduling, and engagement strategy agent." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["social-media", "content", "marketing", "engagement", "scheduling", "analytics"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 4096 temperature = 0.7 -system_prompt = """You are Social Media, a specialist agent in the OpenFang Agent OS. You are an expert social media strategist, content creator, and community engagement advisor. +system_prompt = """You are Social Media, a specialist agent in the OMTAE Agent OS. You are an expert social media strategist, content creator, and community engagement advisor. CORE COMPETENCIES: @@ -59,7 +59,7 @@ max_llm_tokens_per_hour = 120000 max_concurrent_tools = 5 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall", "web_fetch"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/test-engineer/agent.toml b/agents/test-engineer/agent.toml index eca2e025c3..363ee25e6e 100644 --- a/agents/test-engineer/agent.toml +++ b/agents/test-engineer/agent.toml @@ -1,7 +1,7 @@ name = "test-engineer" version = "0.1.0" description = "Quality assurance engineer. Designs test strategies, writes tests, validates correctness." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["testing", "qa", "validation"] @@ -11,7 +11,7 @@ model = "default" api_key_env = "GEMINI_API_KEY" max_tokens = 4096 temperature = 0.3 -system_prompt = """You are Test Engineer, a QA specialist running inside the OpenFang Agent OS. +system_prompt = """You are Test Engineer, a QA specialist running inside the OMTAE Agent OS. Your testing philosophy: - Tests document behavior, not implementation @@ -47,7 +47,7 @@ api_key_env = "GROQ_API_KEY" max_llm_tokens_per_hour = 150000 [capabilities] -tools = ["file_read", "file_write", "file_list", "shell_exec", "memory_store", "memory_recall"] +tools = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] shell = ["cargo test *", "cargo check *"] diff --git a/agents/translator/agent.toml b/agents/translator/agent.toml index a87b24b2c2..0507d22abf 100644 --- a/agents/translator/agent.toml +++ b/agents/translator/agent.toml @@ -1,7 +1,7 @@ name = "translator" version = "0.1.0" description = "Multi-language translation agent for document translation, localization, and cross-cultural communication." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["translation", "languages", "localization", "multilingual", "communication", "i18n"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 8192 temperature = 0.3 -system_prompt = """You are Translator, a specialist agent in the OpenFang Agent OS. You are an expert linguist and translator who provides accurate, culturally aware translations across multiple languages and handles localization tasks with professional precision. +system_prompt = """You are Translator, a specialist agent in the OMTAE Agent OS. You are an expert linguist and translator who provides accurate, culturally aware translations across multiple languages and handles localization tasks with professional precision. CORE COMPETENCIES: @@ -59,7 +59,7 @@ max_llm_tokens_per_hour = 200000 max_concurrent_tools = 5 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall", "web_fetch"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/travel-planner/agent.toml b/agents/travel-planner/agent.toml index 189ed8b9f9..fef7e87268 100644 --- a/agents/travel-planner/agent.toml +++ b/agents/travel-planner/agent.toml @@ -1,7 +1,7 @@ name = "travel-planner" version = "0.1.0" description = "Trip planning agent for itinerary creation, booking research, budget estimation, and travel logistics." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["travel", "planning", "itinerary", "booking", "logistics", "vacation"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 8192 temperature = 0.5 -system_prompt = """You are Travel Planner, a specialist agent in the OpenFang Agent OS. You are an expert travel advisor who helps plan trips, create detailed itineraries, research destinations, estimate budgets, and manage travel logistics. +system_prompt = """You are Travel Planner, a specialist agent in the OMTAE Agent OS. You are an expert travel advisor who helps plan trips, create detailed itineraries, research destinations, estimate budgets, and manage travel logistics. CORE COMPETENCIES: @@ -59,7 +59,7 @@ max_llm_tokens_per_hour = 150000 max_concurrent_tools = 5 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall", "web_search", "web_fetch", "browser_navigate", "browser_click", "browser_type", "browser_read_page", "browser_screenshot", "browser_close"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/tutor/agent.toml b/agents/tutor/agent.toml index 195be81c9b..ee5984a3cc 100644 --- a/agents/tutor/agent.toml +++ b/agents/tutor/agent.toml @@ -1,7 +1,7 @@ name = "tutor" version = "0.1.0" description = "Teaching and explanation agent for learning, tutoring, and educational content creation." -author = "openfang" +author = "omtae" module = "builtin:chat" tags = ["education", "teaching", "tutoring", "learning", "explanation", "knowledge"] @@ -10,7 +10,7 @@ provider = "default" model = "default" max_tokens = 8192 temperature = 0.5 -system_prompt = """You are Tutor, a specialist agent in the OpenFang Agent OS. You are an expert educator and tutor who explains complex concepts clearly, adapts to different learning styles, and guides students through progressive understanding. +system_prompt = """You are Tutor, a specialist agent in the OMTAE Agent OS. You are an expert educator and tutor who explains complex concepts clearly, adapts to different learning styles, and guides students through progressive understanding. CORE COMPETENCIES: @@ -60,7 +60,7 @@ max_llm_tokens_per_hour = 200000 max_concurrent_tools = 5 [capabilities] -tools = ["file_read", "file_write", "file_list", "memory_store", "memory_recall", "shell_exec", "web_fetch"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*", "shared.*"] diff --git a/agents/writer/agent.toml b/agents/writer/agent.toml index 5c5ada5604..9e262e8ab9 100644 --- a/agents/writer/agent.toml +++ b/agents/writer/agent.toml @@ -1,7 +1,7 @@ name = "writer" version = "0.1.0" description = "Content writer. Creates documentation, articles, and technical writing." -author = "openfang" +author = "omtae" module = "builtin:chat" [model] @@ -9,7 +9,7 @@ provider = "default" model = "default" max_tokens = 4096 temperature = 0.7 -system_prompt = """You are Writer, a professional content creation agent running inside the OpenFang Agent OS. +system_prompt = """You are Writer, a professional content creation agent running inside the OMTAE Agent OS. WRITING METHODOLOGY: 1. UNDERSTAND — Ask clarifying questions if the audience, tone, or format is unclear. @@ -38,7 +38,7 @@ api_key_env = "GEMINI_API_KEY" max_llm_tokens_per_hour = 100000 [capabilities] -tools = ["file_read", "file_write", "file_list", "web_search", "web_fetch", "memory_store", "memory_recall"] +tools = ["*"] network = ["*"] memory_read = ["*"] memory_write = ["self.*"] diff --git a/crates/omtae-api b/crates/omtae-api new file mode 120000 index 0000000000..83e647b9c1 --- /dev/null +++ b/crates/omtae-api @@ -0,0 +1 @@ +openfang-api \ No newline at end of file diff --git a/crates/omtae-channels b/crates/omtae-channels new file mode 120000 index 0000000000..6270d1fcb1 --- /dev/null +++ b/crates/omtae-channels @@ -0,0 +1 @@ +openfang-channels \ No newline at end of file diff --git a/crates/omtae-cli b/crates/omtae-cli new file mode 120000 index 0000000000..46c38bf3f4 --- /dev/null +++ b/crates/omtae-cli @@ -0,0 +1 @@ +openfang-cli \ No newline at end of file diff --git a/crates/omtae-desktop b/crates/omtae-desktop new file mode 120000 index 0000000000..1fde3f0e48 --- /dev/null +++ b/crates/omtae-desktop @@ -0,0 +1 @@ +openfang-desktop \ No newline at end of file diff --git a/crates/omtae-extensions b/crates/omtae-extensions new file mode 120000 index 0000000000..009a06df73 --- /dev/null +++ b/crates/omtae-extensions @@ -0,0 +1 @@ +openfang-extensions \ No newline at end of file diff --git a/crates/omtae-hands b/crates/omtae-hands new file mode 120000 index 0000000000..5644070f5b --- /dev/null +++ b/crates/omtae-hands @@ -0,0 +1 @@ +openfang-hands \ No newline at end of file diff --git a/crates/omtae-kernel b/crates/omtae-kernel new file mode 120000 index 0000000000..bf3f5257ba --- /dev/null +++ b/crates/omtae-kernel @@ -0,0 +1 @@ +openfang-kernel \ No newline at end of file diff --git a/crates/omtae-memory b/crates/omtae-memory new file mode 120000 index 0000000000..9ecffdae40 --- /dev/null +++ b/crates/omtae-memory @@ -0,0 +1 @@ +openfang-memory \ No newline at end of file diff --git a/crates/omtae-migrate b/crates/omtae-migrate new file mode 120000 index 0000000000..0e5cbd978f --- /dev/null +++ b/crates/omtae-migrate @@ -0,0 +1 @@ +openfang-migrate \ No newline at end of file diff --git a/crates/omtae-runtime b/crates/omtae-runtime new file mode 120000 index 0000000000..d6dd20e419 --- /dev/null +++ b/crates/omtae-runtime @@ -0,0 +1 @@ +openfang-runtime \ No newline at end of file diff --git a/crates/omtae-skills b/crates/omtae-skills new file mode 120000 index 0000000000..6d3662779b --- /dev/null +++ b/crates/omtae-skills @@ -0,0 +1 @@ +openfang-skills \ No newline at end of file diff --git a/crates/omtae-types b/crates/omtae-types new file mode 120000 index 0000000000..bea03feb12 --- /dev/null +++ b/crates/omtae-types @@ -0,0 +1 @@ +openfang-types \ No newline at end of file diff --git a/crates/omtae-wire b/crates/omtae-wire new file mode 120000 index 0000000000..c0bcdce632 --- /dev/null +++ b/crates/omtae-wire @@ -0,0 +1 @@ +openfang-wire \ No newline at end of file diff --git a/crates/openfang-api/Cargo.toml b/crates/openfang-api/Cargo.toml index fcc3e0088c..50c7a0fea8 100644 --- a/crates/openfang-api/Cargo.toml +++ b/crates/openfang-api/Cargo.toml @@ -1,21 +1,21 @@ [package] -name = "openfang-api" +name = "omtae-api" version.workspace = true edition.workspace = true license.workspace = true -description = "HTTP/WebSocket API server for the OpenFang Agent OS daemon" +description = "HTTP/WebSocket API server for the OMTAE Agent OS daemon" [dependencies] -openfang-types = { path = "../openfang-types" } -openfang-kernel = { path = "../openfang-kernel" } -openfang-runtime = { path = "../openfang-runtime" } -openfang-memory = { path = "../openfang-memory" } -openfang-channels = { path = "../openfang-channels" } -openfang-wire = { path = "../openfang-wire" } -openfang-skills = { path = "../openfang-skills" } -openfang-hands = { path = "../openfang-hands" } -openfang-extensions = { path = "../openfang-extensions" } -openfang-migrate = { path = "../openfang-migrate" } +omtae-types = { path = "../omtae-types" } +omtae-kernel = { path = "../omtae-kernel" } +omtae-runtime = { path = "../omtae-runtime" } +omtae-memory = { path = "../omtae-memory" } +omtae-channels = { path = "../omtae-channels" } +omtae-wire = { path = "../omtae-wire" } +omtae-skills = { path = "../omtae-skills" } +omtae-hands = { path = "../omtae-hands" } +omtae-extensions = { path = "../omtae-extensions" } +omtae-migrate = { path = "../omtae-migrate" } dashmap = { workspace = true } tokio = { workspace = true } serde = { workspace = true } diff --git a/crates/openfang-api/src/brain.rs b/crates/openfang-api/src/brain.rs new file mode 100644 index 0000000000..11ee056c5e --- /dev/null +++ b/crates/openfang-api/src/brain.rs @@ -0,0 +1,415 @@ +//! Obsidian brain vault — list/read/write markdown files under a configured root. + +use axum::extract::{Query, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::Json; +use omtae_types::config::BrainConfig; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::{Component, Path, PathBuf}; +use std::sync::Arc; +use std::time::SystemTime; + +use crate::routes::AppState; + +const MAX_READ_BYTES: u64 = 2 * 1024 * 1024; +const MAX_WRITE_BYTES: usize = 512 * 1024; +const RECENT_LEADS_LIMIT: usize = 20; + +#[derive(Debug, Clone, Serialize)] +pub struct BrainEntry { + pub name: String, + pub path: String, + pub kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub modified: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct BrainListResponse { + pub enabled: bool, + pub vault_path: String, + pub vault_name: String, + pub path: String, + pub entries: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub recent_leads: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct BrainFileResponse { + pub path: String, + pub content: String, + pub size: u64, + pub modified: Option, + pub obsidian_uri: String, +} + +#[derive(Debug, Deserialize)] +pub struct BrainPathQuery { + #[serde(default)] + pub path: String, +} + +#[derive(Debug, Deserialize)] +pub struct BrainWriteRequest { + pub path: String, + pub content: String, +} + +fn system_time_to_rfc3339(time: SystemTime) -> Option { + use chrono::{DateTime, Utc}; + Some(DateTime::::from(time).to_rfc3339()) +} + +fn vault_name_from_path(root: &Path) -> String { + root.file_name() + .and_then(|s| s.to_str()) + .unwrap_or("omtae-brain") + .to_string() +} + +fn percent_encode_uri(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for b in s.bytes() { + match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'/' => { + out.push(b as char); + } + _ => out.push_str(&format!("%{:02X}", b)), + } + } + out +} + +fn obsidian_uri(vault_name: &str, rel_path: &str) -> String { + format!( + "obsidian://open?vault={}&file={}", + percent_encode_uri(vault_name), + percent_encode_uri(rel_path) + ) +} + +/// Resolve and canonicalize the configured vault root. +pub fn resolve_vault_root(brain: &BrainConfig) -> Result { + let raw = brain + .path + .clone() + .unwrap_or_else(omtae_types::config::default_brain_vault_path); + if !raw.exists() { + std::fs::create_dir_all(&raw).map_err(|e| format!("create vault dir: {e}"))?; + } + raw.canonicalize() + .map_err(|e| format!("resolve vault path {}: {e}", raw.display())) +} + +/// Join a relative vault path and ensure it stays under the root. +pub fn resolve_safe_path(root: &Path, rel: &str) -> Result { + let trimmed = rel.trim().trim_start_matches('/'); + if trimmed.is_empty() { + return Ok(root.to_path_buf()); + } + for component in Path::new(trimmed).components() { + match component { + Component::Normal(_) => {} + Component::CurDir => {} + _ => return Err("path traversal not allowed".to_string()), + } + } + let joined = root.join(trimmed); + let canonical = if joined.exists() { + joined + .canonicalize() + .map_err(|e| format!("resolve path: {e}"))? + } else { + joined + .parent() + .and_then(|p| p.canonicalize().ok()) + .map(|p| p.join(joined.file_name().unwrap_or_default())) + .ok_or_else(|| "invalid path".to_string())? + }; + if !canonical.starts_with(root) { + return Err("path escapes vault root".to_string()); + } + Ok(canonical) +} + +fn rel_path(root: &Path, abs: &Path) -> String { + abs.strip_prefix(root) + .unwrap_or(abs) + .to_string_lossy() + .replace('\\', "/") +} + +fn should_skip_name(name: &str) -> bool { + name.starts_with('.') || name == "node_modules" +} + +fn entry_from_path(root: &Path, abs: &Path, kind: &str) -> BrainEntry { + let meta = std::fs::metadata(abs).ok(); + BrainEntry { + name: abs + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(), + path: rel_path(root, abs), + kind: kind.to_string(), + size: meta.as_ref().map(|m| m.len()), + modified: meta + .and_then(|m| m.modified().ok()) + .and_then(system_time_to_rfc3339), + } +} + +fn list_directory(root: &Path, rel: &str) -> Result, String> { + let dir = resolve_safe_path(root, rel)?; + if !dir.is_dir() { + return Err("not a directory".to_string()); + } + let mut entries = Vec::new(); + for item in std::fs::read_dir(&dir).map_err(|e| format!("read dir: {e}"))? { + let item = item.map_err(|e| format!("read entry: {e}"))?; + let name = item.file_name().to_string_lossy().to_string(); + if should_skip_name(&name) { + continue; + } + let path = item.path(); + let kind = if path.is_dir() { "dir" } else { "file" }; + entries.push(entry_from_path(root, &path, kind)); + } + entries.sort_by(|a, b| { + match (a.kind.as_str(), b.kind.as_str()) { + ("dir", "file") => std::cmp::Ordering::Less, + ("file", "dir") => std::cmp::Ordering::Greater, + _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()), + } + }); + Ok(entries) +} + +fn recent_leads(root: &Path) -> Vec { + let leads_dir = root.join("leads"); + if !leads_dir.is_dir() { + return Vec::new(); + } + let mut items: Vec<(SystemTime, BrainEntry)> = Vec::new(); + let Ok(read_dir) = std::fs::read_dir(&leads_dir) else { + return Vec::new(); + }; + for item in read_dir.flatten() { + let path = item.path(); + if path.is_dir() { + if let Ok(sub) = std::fs::read_dir(&path) { + for sub_item in sub.flatten() { + let sub_path = sub_item.path(); + if sub_path.is_file() && sub_path.extension().is_some_and(|e| e == "md") { + if let Ok(meta) = sub_path.metadata() { + if let Ok(modified) = meta.modified() { + items.push((modified, entry_from_path(root, &sub_path, "file"))); + } + } + } + } + } + continue; + } + if path.extension().is_some_and(|e| e == "md") { + if let Ok(meta) = path.metadata() { + if let Ok(modified) = meta.modified() { + items.push((modified, entry_from_path(root, &path, "file"))); + } + } + } + } + items.sort_by(|a, b| b.0.cmp(&a.0)); + items + .into_iter() + .take(RECENT_LEADS_LIMIT) + .map(|(_, entry)| entry) + .collect() +} + +fn read_text_file(root: &Path, rel: &str, vault_name: &str) -> Result { + let abs = resolve_safe_path(root, rel)?; + if !abs.is_file() { + return Err("not a file".to_string()); + } + let meta = std::fs::metadata(&abs).map_err(|e| format!("stat file: {e}"))?; + if meta.len() > MAX_READ_BYTES { + return Err(format!("file too large (max {} bytes)", MAX_READ_BYTES)); + } + let content = std::fs::read_to_string(&abs).map_err(|e| format!("read file: {e}"))?; + let rel_s = rel_path(root, &abs); + Ok(BrainFileResponse { + path: rel_s.clone(), + size: meta.len(), + modified: meta.modified().ok().and_then(system_time_to_rfc3339), + obsidian_uri: obsidian_uri(vault_name, &rel_s), + content, + }) +} + +fn write_text_file(root: &Path, rel: &str, content: &str) -> Result { + if content.len() > MAX_WRITE_BYTES { + return Err(format!("content too large (max {} bytes)", MAX_WRITE_BYTES)); + } + let abs = resolve_safe_path(root, rel)?; + if let Some(parent) = abs.parent() { + std::fs::create_dir_all(parent).map_err(|e| format!("create parent: {e}"))?; + } + let tmp = abs.with_extension("tmp"); + std::fs::write(&tmp, content.as_bytes()).map_err(|e| format!("write temp: {e}"))?; + std::fs::rename(&tmp, &abs).map_err(|e| format!("commit write: {e}"))?; + let vault_name = vault_name_from_path(root); + read_text_file(root, rel, &vault_name) +} + +fn brain_disabled() -> (StatusCode, Json) { + ( + StatusCode::SERVICE_UNAVAILABLE, + Json(serde_json::json!({ "error": "brain vault disabled in config" })), + ) +} + +fn brain_error(status: StatusCode, msg: &str) -> (StatusCode, Json) { + (status, Json(serde_json::json!({ "error": msg }))) +} + +/// GET /api/brain/list — List vault directory entries. +pub async fn brain_list( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let brain = &state.kernel.config.brain; + if !brain.enabled { + return brain_disabled().into_response(); + } + let root = match resolve_vault_root(brain) { + Ok(r) => r, + Err(e) => return brain_error(StatusCode::INTERNAL_SERVER_ERROR, &e).into_response(), + }; + let vault_name = vault_name_from_path(&root); + let rel = if query.path.is_empty() { "" } else { &query.path }; + let entries = match list_directory(&root, rel) { + Ok(e) => e, + Err(e) => return brain_error(StatusCode::BAD_REQUEST, &e).into_response(), + }; + let recent = if rel.is_empty() { + recent_leads(&root) + } else { + Vec::new() + }; + Json(BrainListResponse { + enabled: true, + vault_path: root.display().to_string(), + vault_name, + path: rel.to_string(), + entries, + recent_leads: recent, + }) + .into_response() +} + +/// GET /api/brain/file — Read a vault file. +pub async fn brain_file( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let brain = &state.kernel.config.brain; + if !brain.enabled { + return brain_disabled().into_response(); + } + if query.path.trim().is_empty() { + return brain_error(StatusCode::BAD_REQUEST, "path required").into_response(); + } + let root = match resolve_vault_root(brain) { + Ok(r) => r, + Err(e) => return brain_error(StatusCode::INTERNAL_SERVER_ERROR, &e).into_response(), + }; + let vault_name = vault_name_from_path(&root); + match read_text_file(&root, &query.path, &vault_name) { + Ok(resp) => Json(resp).into_response(), + Err(e) => brain_error(StatusCode::BAD_REQUEST, &e).into_response(), + } +} + +/// POST /api/brain/file — Write a vault file. +pub async fn brain_write( + State(state): State>, + Json(body): Json, +) -> impl IntoResponse { + let brain = &state.kernel.config.brain; + if !brain.enabled { + return brain_disabled().into_response(); + } + if body.path.trim().is_empty() { + return brain_error(StatusCode::BAD_REQUEST, "path required").into_response(); + } + let root = match resolve_vault_root(brain) { + Ok(r) => r, + Err(e) => return brain_error(StatusCode::INTERNAL_SERVER_ERROR, &e).into_response(), + }; + match write_text_file(&root, &body.path, &body.content) { + Ok(resp) => Json(resp).into_response(), + Err(e) => brain_error(StatusCode::BAD_REQUEST, &e).into_response(), + } +} + +/// GET /api/brain/status — Vault metadata for dashboard header. +pub async fn brain_status(State(state): State>) -> impl IntoResponse { + let brain = &state.kernel.config.brain; + let path = state.kernel.config.brain_vault_path(); + let exists = path.exists(); + let mut counts: HashMap = HashMap::new(); + if exists { + if let Ok(root) = resolve_vault_root(brain) { + for top in ["leads", "genetics", "history", "refining"] { + let dir = root.join(top); + if dir.is_dir() { + if let Ok(n) = dir.read_dir().map(|d| d.count() as u64) { + counts.insert(top.to_string(), n); + } + } + } + } + } + Json(serde_json::json!({ + "enabled": brain.enabled, + "vault_path": path.display().to_string(), + "vault_name": path.file_name().and_then(|s| s.to_str()).unwrap_or("omtae-brain"), + "exists": exists, + "counts": counts, + })) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn safe_path_blocks_traversal() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().canonicalize().unwrap(); + fs::write(root.join("note.md"), "# hi").unwrap(); + assert!(resolve_safe_path(&root, "../etc/passwd").is_err()); + assert!(resolve_safe_path(&root, "note.md").is_ok()); + } + + #[test] + fn list_and_read_roundtrip() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().canonicalize().unwrap(); + fs::create_dir_all(root.join("leads")).unwrap(); + fs::write(root.join("leads/a.md"), "# A").unwrap(); + let entries = list_directory(&root, "leads").unwrap(); + assert_eq!(entries.len(), 1); + let file = read_text_file(&root, "leads/a.md", "test-vault").unwrap(); + assert!(file.content.contains("# A")); + assert!(file.obsidian_uri.contains("obsidian://open")); + } +} diff --git a/crates/openfang-api/src/channel_bridge.rs b/crates/openfang-api/src/channel_bridge.rs index 37ad72921f..712e79416e 100644 --- a/crates/openfang-api/src/channel_bridge.rs +++ b/crates/openfang-api/src/channel_bridge.rs @@ -1,71 +1,71 @@ -//! Channel bridge wiring — connects the OpenFang kernel to channel adapters. +//! Channel bridge wiring — connects the OMTAE kernel to channel adapters. //! -//! Implements `ChannelBridgeHandle` on `OpenFangKernel` and provides the +//! Implements `ChannelBridgeHandle` on `OMTAEKernel` and provides the //! `start_channel_bridge()` entry point called by the daemon. -use openfang_channels::bridge::{BridgeManager, ChannelBridgeHandle}; -use openfang_channels::discord::DiscordAdapter; -use openfang_channels::email::EmailAdapter; -use openfang_channels::google_chat::GoogleChatAdapter; -use openfang_channels::irc::IrcAdapter; -use openfang_channels::matrix::MatrixAdapter; -use openfang_channels::mattermost::MattermostAdapter; -use openfang_channels::rocketchat::RocketChatAdapter; -use openfang_channels::router::AgentRouter; -use openfang_channels::signal::SignalAdapter; -use openfang_channels::slack::SlackAdapter; -use openfang_channels::teams::TeamsAdapter; -use openfang_channels::telegram::TelegramAdapter; -use openfang_channels::twitch::TwitchAdapter; -use openfang_channels::types::ChannelAdapter; -use openfang_channels::whatsapp::WhatsAppAdapter; -use openfang_channels::xmpp::XmppAdapter; -use openfang_channels::zulip::ZulipAdapter; +use omtae_channels::bridge::{BridgeManager, ChannelBridgeHandle}; +use omtae_channels::discord::DiscordAdapter; +use omtae_channels::email::EmailAdapter; +use omtae_channels::google_chat::GoogleChatAdapter; +use omtae_channels::irc::IrcAdapter; +use omtae_channels::matrix::MatrixAdapter; +use omtae_channels::mattermost::MattermostAdapter; +use omtae_channels::rocketchat::RocketChatAdapter; +use omtae_channels::router::AgentRouter; +use omtae_channels::signal::SignalAdapter; +use omtae_channels::slack::SlackAdapter; +use omtae_channels::teams::TeamsAdapter; +use omtae_channels::telegram::TelegramAdapter; +use omtae_channels::twitch::TwitchAdapter; +use omtae_channels::types::ChannelAdapter; +use omtae_channels::whatsapp::WhatsAppAdapter; +use omtae_channels::xmpp::XmppAdapter; +use omtae_channels::zulip::ZulipAdapter; // Wave 3 -use openfang_channels::bluesky::BlueskyAdapter; -use openfang_channels::feishu::FeishuAdapter; -use openfang_channels::line::LineAdapter; -use openfang_channels::mastodon::MastodonAdapter; -use openfang_channels::messenger::MessengerAdapter; -use openfang_channels::reddit::RedditAdapter; -use openfang_channels::revolt::RevoltAdapter; -use openfang_channels::viber::ViberAdapter; -use openfang_types::config::FeishuMode; +use omtae_channels::bluesky::BlueskyAdapter; +use omtae_channels::feishu::FeishuAdapter; +use omtae_channels::line::LineAdapter; +use omtae_channels::mastodon::MastodonAdapter; +use omtae_channels::messenger::MessengerAdapter; +use omtae_channels::reddit::RedditAdapter; +use omtae_channels::revolt::RevoltAdapter; +use omtae_channels::viber::ViberAdapter; +use omtae_types::config::FeishuMode; // Wave 4 -use openfang_channels::flock::FlockAdapter; -use openfang_channels::guilded::GuildedAdapter; -use openfang_channels::keybase::KeybaseAdapter; -use openfang_channels::nextcloud::NextcloudAdapter; -use openfang_channels::nostr::NostrAdapter; -use openfang_channels::pumble::PumbleAdapter; -use openfang_channels::threema::ThreemaAdapter; -use openfang_channels::twist::TwistAdapter; -use openfang_channels::webex::WebexAdapter; +use omtae_channels::flock::FlockAdapter; +use omtae_channels::guilded::GuildedAdapter; +use omtae_channels::keybase::KeybaseAdapter; +use omtae_channels::nextcloud::NextcloudAdapter; +use omtae_channels::nostr::NostrAdapter; +use omtae_channels::pumble::PumbleAdapter; +use omtae_channels::threema::ThreemaAdapter; +use omtae_channels::twist::TwistAdapter; +use omtae_channels::webex::WebexAdapter; // Wave 5 use async_trait::async_trait; -use openfang_channels::dingtalk::DingTalkAdapter; -use openfang_channels::dingtalk_stream::DingTalkStreamAdapter; -use openfang_channels::discourse::DiscourseAdapter; -use openfang_channels::gitter::GitterAdapter; -use openfang_channels::gotify::GotifyAdapter; -use openfang_channels::linkedin::LinkedInAdapter; -use openfang_channels::mqtt::MqttAdapter; -use openfang_channels::mumble::MumbleAdapter; -use openfang_channels::ntfy::NtfyAdapter; -use openfang_channels::webhook::WebhookAdapter; -use openfang_channels::wecom::WeComAdapter; -use openfang_kernel::OpenFangKernel; -use openfang_runtime::kernel_handle::KernelHandle; -use openfang_types::agent::AgentId; +use omtae_channels::dingtalk::DingTalkAdapter; +use omtae_channels::dingtalk_stream::DingTalkStreamAdapter; +use omtae_channels::discourse::DiscourseAdapter; +use omtae_channels::gitter::GitterAdapter; +use omtae_channels::gotify::GotifyAdapter; +use omtae_channels::linkedin::LinkedInAdapter; +use omtae_channels::mqtt::MqttAdapter; +use omtae_channels::mumble::MumbleAdapter; +use omtae_channels::ntfy::NtfyAdapter; +use omtae_channels::webhook::WebhookAdapter; +use omtae_channels::wecom::WeComAdapter; +use omtae_kernel::OMTAEKernel; +use omtae_runtime::kernel_handle::KernelHandle; +use omtae_types::agent::AgentId; use std::sync::Arc; use std::time::{Duration, Instant}; use tracing::{error, info, warn}; -use openfang_runtime::str_utils::safe_truncate_str; +use omtae_runtime::str_utils::safe_truncate_str; -/// Wraps `OpenFangKernel` to implement `ChannelBridgeHandle`. +/// Wraps `OMTAEKernel` to implement `ChannelBridgeHandle`. pub struct KernelBridgeAdapter { - kernel: Arc, + kernel: Arc, started_at: Instant, } @@ -87,13 +87,13 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { async fn send_message_with_blocks( &self, agent_id: AgentId, - blocks: Vec, + blocks: Vec, ) -> Result { // Extract text for the message parameter (used for memory recall / logging) let text: String = blocks .iter() .filter_map(|b| match b { - openfang_types::message::ContentBlock::Text { text, .. } => Some(text.as_str()), + omtae_types::message::ContentBlock::Text { text, .. } => Some(text.as_str()), _ => None, }) .collect::>() @@ -126,7 +126,7 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { } async fn spawn_agent_by_name(&self, manifest_name: &str) -> Result { - // Look for manifest at ~/.openfang/agents/{name}/agent.toml + // Look for manifest at ~/.omtae/agents/{name}/agent.toml let manifest_path = self .kernel .config @@ -142,7 +142,7 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { let contents = std::fs::read_to_string(&manifest_path) .map_err(|e| format!("Failed to read manifest: {e}"))?; - let manifest: openfang_types::agent::AgentManifest = + let manifest: omtae_types::agent::AgentManifest = toml::from_str(&contents).map_err(|e| format!("Invalid manifest TOML: {e}"))?; let agent_id = self @@ -161,14 +161,14 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { let mins = (secs % 3600) / 60; if hours > 0 { format!( - "OpenFang status: {}h {}m uptime, {} agent(s)", + "OMTAE status: {}h {}m uptime, {} agent(s)", hours, mins, agents.len() ) } else { format!( - "OpenFang status: {}m uptime, {} agent(s)", + "OMTAE status: {}m uptime, {} agent(s)", mins, agents.len() ) @@ -189,7 +189,7 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { // Group by provider let mut by_provider: std::collections::HashMap< &str, - Vec<&openfang_types::model_catalog::ModelCatalogEntry>, + Vec<&omtae_types::model_catalog::ModelCatalogEntry>, > = std::collections::HashMap::new(); for m in &available { by_provider.entry(m.provider.as_str()).or_default().push(m); @@ -226,9 +226,9 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { let mut msg = "Providers:\n".to_string(); for p in catalog.list_providers() { let status = match p.auth_status { - openfang_types::model_catalog::AuthStatus::Configured => "configured", - openfang_types::model_catalog::AuthStatus::Missing => "not configured", - openfang_types::model_catalog::AuthStatus::NotRequired => "local (no key needed)", + omtae_types::model_catalog::AuthStatus::Configured => "configured", + omtae_types::model_catalog::AuthStatus::Missing => "not configured", + omtae_types::model_catalog::AuthStatus::NotRequired => "local (no key needed)", }; msg.push_str(&format!( " {} — {} [{}, {} model(s)]\n", @@ -246,7 +246,7 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { .unwrap_or_else(|e| e.into_inner()); let skills = skills.list(); if skills.is_empty() { - return "No skills installed. Place skills in ~/.openfang/skills/ or install from the marketplace.".to_string(); + return "No skills installed. Place skills in ~/.omtae/skills/ or install from the marketplace.".to_string(); } let mut msg = format!("Installed skills ({}):\n", skills.len()); for skill in &skills { @@ -342,12 +342,12 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { .execute_run( run_id, |step_agent| match step_agent { - openfang_kernel::workflow::StepAgent::ById { id } => { + omtae_kernel::workflow::StepAgent::ById { id } => { let aid: AgentId = id.parse().ok()?; let entry = registry_ref.get(aid)?; Some((aid, entry.name.clone())) } - openfang_kernel::workflow::StepAgent::ByName { name } => { + omtae_kernel::workflow::StepAgent::ByName { name } => { let entry = registry_ref.find_by_name(name)?; Some((entry.id, entry.name.clone())) } @@ -472,11 +472,11 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { let id_str = job.id.0.to_string(); let id_short = safe_truncate_str(&id_str, 8); let sched = match &job.schedule { - openfang_types::scheduler::CronSchedule::Cron { expr, .. } => expr.clone(), - openfang_types::scheduler::CronSchedule::Every { every_secs } => { + omtae_types::scheduler::CronSchedule::Cron { expr, .. } => expr.clone(), + omtae_types::scheduler::CronSchedule::Every { every_secs } => { format!("every {every_secs}s") } - openfang_types::scheduler::CronSchedule::At { at } => { + omtae_types::scheduler::CronSchedule::At { at } => { format!("at {}", at.format("%Y-%m-%d %H:%M")) } }; @@ -509,21 +509,21 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { let cron_expr = args[1..6].join(" "); let message = args[6..].join(" "); - let job = openfang_types::scheduler::CronJob { - id: openfang_types::scheduler::CronJobId::new(), + let job = omtae_types::scheduler::CronJob { + id: omtae_types::scheduler::CronJobId::new(), agent_id: agent.id, name: format!("chat-{}", &agent.name), enabled: true, - schedule: openfang_types::scheduler::CronSchedule::Cron { + schedule: omtae_types::scheduler::CronSchedule::Cron { expr: cron_expr.clone(), tz: None, }, - action: openfang_types::scheduler::CronAction::AgentTurn { + action: omtae_types::scheduler::CronAction::AgentTurn { message: message.clone(), model_override: None, timeout_secs: None, }, - delivery: openfang_types::scheduler::CronDelivery::None, + delivery: omtae_types::scheduler::CronDelivery::None, delivery_targets: Vec::new(), created_at: chrono::Utc::now(), last_run: None, @@ -583,13 +583,13 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { 1 => { let j = matched[0]; let message = match &j.action { - openfang_types::scheduler::CronAction::AgentTurn { + omtae_types::scheduler::CronAction::AgentTurn { message, .. } => message.clone(), - openfang_types::scheduler::CronAction::SystemEvent { text } => { + omtae_types::scheduler::CronAction::SystemEvent { text } => { text.clone() } - openfang_types::scheduler::CronAction::WorkflowRun { + omtae_types::scheduler::CronAction::WorkflowRun { workflow_id, input, .. @@ -657,9 +657,9 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { 1 => { let req = matched[0]; let decision = if approve { - openfang_types::approval::ApprovalDecision::Approved + omtae_types::approval::ApprovalDecision::Approved } else { - openfang_types::approval::ApprovalDecision::Denied + omtae_types::approval::ApprovalDecision::Denied }; match self.kernel.approval_manager.resolve( req.id, @@ -762,7 +762,7 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { async fn channel_overrides( &self, channel_type: &str, - ) -> Option { + ) -> Option { let channels = &self.kernel.config.channels; match channel_type { "telegram" => channels.telegram.as_ref().map(|c| c.overrides.clone()), @@ -848,11 +848,11 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { .ok_or_else(|| "Unrecognized user. Contact an admin to get access.".to_string())?; let auth_action = match action { - "chat" => openfang_kernel::auth::Action::ChatWithAgent, - "spawn" => openfang_kernel::auth::Action::SpawnAgent, - "kill" => openfang_kernel::auth::Action::KillAgent, - "install_skill" => openfang_kernel::auth::Action::InstallSkill, - _ => openfang_kernel::auth::Action::ChatWithAgent, + "chat" => omtae_kernel::auth::Action::ChatWithAgent, + "spawn" => omtae_kernel::auth::Action::SpawnAgent, + "kill" => omtae_kernel::auth::Action::KillAgent, + "install_skill" => omtae_kernel::auth::Action::InstallSkill, + _ => omtae_kernel::auth::Action::ChatWithAgent, }; self.kernel @@ -871,9 +871,9 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { thread_id: Option<&str>, ) { let receipt = if success { - openfang_kernel::DeliveryTracker::sent_receipt(channel, recipient) + omtae_kernel::DeliveryTracker::sent_receipt(channel, recipient) } else { - openfang_kernel::DeliveryTracker::failed_receipt( + omtae_kernel::DeliveryTracker::failed_receipt( channel, recipient, error.unwrap_or("Unknown error"), @@ -901,7 +901,7 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { recipient: &str, message: &str, ) -> Result<(), String> { - ::send_channel_message( + ::send_channel_message( &self.kernel, channel_type, recipient, @@ -1009,7 +1009,7 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { msg.push_str(&format!(" {} — {}\n", card.name, url)); let desc = &card.description; if !desc.is_empty() { - let short = openfang_types::truncate_str(desc, 60); + let short = omtae_types::truncate_str(desc, 60); msg.push_str(&format!(" {short}\n")); } } @@ -1018,8 +1018,8 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { } /// Parse a trigger pattern string from chat into a `TriggerPattern`. -fn parse_trigger_pattern(s: &str) -> Option { - use openfang_kernel::triggers::TriggerPattern; +fn parse_trigger_pattern(s: &str) -> Option { + use omtae_kernel::triggers::TriggerPattern; if let Some(rest) = s.strip_prefix("spawned:") { return Some(TriggerPattern::AgentSpawned { name_pattern: rest.to_string(), @@ -1092,7 +1092,7 @@ fn read_token(env_var_or_token: &str, adapter_name: &str) -> Option { /// /// Returns `Some(BridgeManager)` if any channels were configured and started, /// or `None` if no channels are configured. -pub async fn start_channel_bridge(kernel: Arc) -> Option { +pub async fn start_channel_bridge(kernel: Arc) -> Option { let channels = kernel.config.channels.clone(); let (bridge, _names) = start_channel_bridge_with_config(kernel, &channels).await; bridge @@ -1102,8 +1102,8 @@ pub async fn start_channel_bridge(kernel: Arc) -> Option, Vec)`. pub async fn start_channel_bridge_with_config( - kernel: Arc, - config: &openfang_types::config::ChannelsConfig, + kernel: Arc, + config: &omtae_types::config::ChannelsConfig, ) -> (Option, Vec) { let has_any = config.telegram.is_some() || config.discord.is_some() @@ -1470,7 +1470,7 @@ pub async fn start_channel_bridge_with_config( // Feishu/Lark if let Some(ref fs_config) = config.feishu { if let Some(secret) = read_token(&fs_config.app_secret_env, "Feishu") { - let region = openfang_channels::feishu::FeishuRegion::parse_region(&fs_config.region); + let region = omtae_channels::feishu::FeishuRegion::parse_region(&fs_config.region); let encrypt_key = fs_config .encrypt_key_env .as_ref() @@ -1882,7 +1882,7 @@ pub async fn reload_channels_from_disk( // Re-read config from disk let config_path = state.kernel.config.home_dir.join("config.toml"); - let fresh_config = openfang_kernel::config::load_config(Some(&config_path)); + let fresh_config = omtae_kernel::config::load_config(Some(&config_path)); // Update the live channels config so list_channels() reflects reality *state.channels_config.write().await = fresh_config.channels.clone(); @@ -1907,7 +1907,7 @@ pub async fn reload_channels_from_disk( mod tests { #[tokio::test] async fn test_bridge_skips_when_no_config() { - let config = openfang_types::config::KernelConfig::default(); + let config = omtae_types::config::KernelConfig::default(); assert!(config.channels.telegram.is_none()); assert!(config.channels.discord.is_none()); assert!(config.channels.slack.is_none()); @@ -1955,7 +1955,7 @@ mod tests { #[test] fn test_feishu_bridge_mode_defaults_to_websocket() { - let config: openfang_types::config::KernelConfig = toml::from_str( + let config: omtae_types::config::KernelConfig = toml::from_str( r#" [channels.feishu] app_id = "cli_test" @@ -1965,12 +1965,12 @@ mod tests { .unwrap(); let feishu = config.channels.feishu.expect("feishu config should exist"); - assert_eq!(feishu.mode, openfang_types::config::FeishuMode::Websocket); + assert_eq!(feishu.mode, omtae_types::config::FeishuMode::Websocket); } #[test] fn test_feishu_bridge_mode_supports_websocket() { - let config: openfang_types::config::KernelConfig = toml::from_str( + let config: omtae_types::config::KernelConfig = toml::from_str( r#" [channels.feishu] app_id = "cli_test" @@ -1981,6 +1981,6 @@ mod tests { .unwrap(); let feishu = config.channels.feishu.expect("feishu config should exist"); - assert_eq!(feishu.mode, openfang_types::config::FeishuMode::Websocket); + assert_eq!(feishu.mode, omtae_types::config::FeishuMode::Websocket); } } diff --git a/crates/openfang-api/src/gpu.rs b/crates/openfang-api/src/gpu.rs new file mode 100644 index 0000000000..f52648c905 --- /dev/null +++ b/crates/openfang-api/src/gpu.rs @@ -0,0 +1,269 @@ +//! NVIDIA GPU telemetry via `nvidia-smi` (dual-GPU workstation layout). + +use serde::Serialize; +use std::collections::HashMap; +use std::time::Duration; +use tokio::process::Command; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GpuSlot { + pub temp: u32, + pub usage: u32, + pub memory: f64, + #[serde(rename = "memoryTotal")] + pub memory_total: u32, + pub processes: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GpuStats { + pub gpu0: GpuSlot, + pub gpu1: GpuSlot, + pub total_vram: u32, + pub used_vram: f64, + pub model: String, + pub available: bool, +} + +fn empty_slot() -> GpuSlot { + GpuSlot { + temp: 0, + usage: 0, + memory: 0.0, + memory_total: 24, + processes: vec![], + } +} + +fn default_stats() -> GpuStats { + GpuStats { + gpu0: empty_slot(), + gpu1: empty_slot(), + total_vram: 48, + used_vram: 0.0, + model: std::env::var("VLLM_MODEL_DEFAULT") + .or_else(|_| std::env::var("OLLAMA_MODEL_DEFAULT")) + .unwrap_or_else(|_| "vLLM".to_string()), + available: false, + } +} + +fn short_process_name(raw: &str) -> String { + let name = raw.trim(); + if name.is_empty() { + return "unknown".to_string(); + } + let lower = name.to_lowercase(); + if lower.contains("vllm") { + return "VLLM::EngineCore".to_string(); + } + if lower.contains("gnome-shell") { + return "gnome-shell".to_string(); + } + if lower.contains("cursor") { + return "cursor".to_string(); + } + if lower.contains("chrome") { + return "chrome".to_string(); + } + let without_args = name.split_whitespace().next().unwrap_or(name); + let base = without_args + .rsplit('/') + .next() + .unwrap_or(without_args); + if base.len() > 28 { + format!("{}…", &base[..26]) + } else { + base.to_string() + } +} + +async fn run_nvidia_smi(args: &[&str]) -> Option { + let output = Command::new("nvidia-smi") + .args(args) + .output() + .await + .ok()?; + if !output.status.success() { + return None; + } + String::from_utf8(output.stdout).ok() +} + +fn parse_f64(s: &str) -> f64 { + s.trim().parse().unwrap_or(0.0) +} + +/// Collect GPU0 (display) and GPU1 (compute/vLLM) stats from `nvidia-smi`. +pub async fn collect_gpu_stats() -> GpuStats { + let mut stats = default_stats(); + + let stdout = match run_nvidia_smi(&[ + "--query-gpu=temperature.gpu,utilization.gpu,memory.used,memory.total", + "--format=csv,noheader,nounits", + ]) + .await + { + Some(s) if !s.trim().is_empty() => s, + _ => return stats, + }; + + stats.available = true; + let lines: Vec<&str> = stdout.trim().lines().collect(); + let mut total_used_mb = 0.0_f64; + let mut total_vram_mb = 0.0_f64; + + if let Some(line) = lines.first() { + let parts: Vec<&str> = line.split(',').map(str::trim).collect(); + if parts.len() >= 4 { + let mem_used = parse_f64(parts[2]); + let mem_total = parse_f64(parts[3]); + stats.gpu0 = GpuSlot { + temp: parse_f64(parts[0]) as u32, + usage: parse_f64(parts[1]) as u32, + memory: (mem_used / 1024.0 * 10.0).round() / 10.0, + memory_total: (mem_total / 1024.0).round() as u32, + processes: vec![], + }; + total_used_mb += mem_used; + total_vram_mb += mem_total; + } + } + + if let Some(line) = lines.get(1) { + let parts: Vec<&str> = line.split(',').map(str::trim).collect(); + if parts.len() >= 4 { + let mem_used = parse_f64(parts[2]); + let mem_total = parse_f64(parts[3]); + stats.gpu1 = GpuSlot { + temp: parse_f64(parts[0]) as u32, + usage: parse_f64(parts[1]) as u32, + memory: (mem_used / 1024.0 * 10.0).round() / 10.0, + memory_total: (mem_total / 1024.0).round() as u32, + processes: vec![], + }; + total_used_mb += mem_used; + total_vram_mb += mem_total; + } + } + + stats.used_vram = (total_used_mb / 1024.0 * 10.0).round() / 10.0; + stats.total_vram = (total_vram_mb / 1024.0).round() as u32; + + if let Some(uuid_out) = run_nvidia_smi(&[ + "--query-gpu=index,uuid", + "--format=csv,noheader", + ]) + .await + { + let mut uuid_to_index: HashMap = HashMap::new(); + for row in uuid_out.trim().lines() { + let mut parts = row.splitn(2, ','); + let idx_str = parts.next().unwrap_or("").trim(); + let uuid = parts.next().unwrap_or("").trim(); + if let Ok(idx) = idx_str.parse::() { + if !uuid.is_empty() { + uuid_to_index.insert(uuid.to_string(), idx); + } + } + } + + if let Some(proc_out) = run_nvidia_smi(&[ + "--query-compute-apps=gpu_uuid,pid,process_name,used_gpu_memory", + "--format=csv,noheader,nounits", + ]) + .await + { + let mut proc_by_gpu: HashMap> = + HashMap::from([(0, vec![]), (1, vec![])]); + for row in proc_out.trim().lines() { + if row.trim().is_empty() { + continue; + } + let first_comma = row.find(',').unwrap_or(0); + let second_comma = row[first_comma + 1..] + .find(',') + .map(|i| first_comma + 1 + i) + .unwrap_or(0); + let last_comma = row.rfind(',').unwrap_or(0); + if second_comma == 0 || last_comma <= second_comma { + continue; + } + let uuid = row[..first_comma].trim(); + let proc_name = row[second_comma + 1..last_comma].trim(); + if let Some(&idx) = uuid_to_index.get(uuid) { + if idx == 0 || idx == 1 { + proc_by_gpu + .entry(idx) + .or_default() + .push(short_process_name(proc_name)); + } + } + } + if let Some(p) = proc_by_gpu.get(&0) { + let mut unique: Vec = p.clone(); + unique.sort(); + unique.dedup(); + stats.gpu0.processes = unique; + } + if let Some(p) = proc_by_gpu.get(&1) { + let mut unique: Vec = p.clone(); + unique.sort(); + unique.dedup(); + stats.gpu1.processes = unique; + } + } + } + + // Optional: resolve vLLM model name from /models endpoint + let provider = std::env::var("LLM_PROVIDER").unwrap_or_default(); + let vllm_base = std::env::var("VLLM_BASE_URL") + .unwrap_or_else(|_| "http://127.0.0.1:8000/v1".to_string()) + .trim_end_matches('/') + .to_string(); + if provider == "vllm" || provider.is_empty() || vllm_base.contains(":8000") { + if let Ok(client) = reqwest::Client::builder() + .timeout(Duration::from_secs(2)) + .build() + { + let mut req = client.get(format!("{vllm_base}/models")); + if let Ok(key) = std::env::var("VLLM_API_KEY") { + if !key.is_empty() { + req = req.header("Authorization", format!("Bearer {key}")); + } + } + if let Ok(resp) = req.send().await { + if resp.status().is_success() { + if let Ok(body) = resp.json::().await { + if let Some(id) = body + .get("data") + .and_then(|d| d.as_array()) + .and_then(|a| a.first()) + .and_then(|m| m.get("id")) + .and_then(|v| v.as_str()) + { + stats.model = id.to_string(); + } + } + } + } + } + } + + stats +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn short_process_name_vllm() { + assert_eq!( + short_process_name("python -m vllm.entrypoints"), + "VLLM::EngineCore" + ); + } +} diff --git a/crates/openfang-api/src/lib.rs b/crates/openfang-api/src/lib.rs index 243da788b2..2d2b0cb910 100644 --- a/crates/openfang-api/src/lib.rs +++ b/crates/openfang-api/src/lib.rs @@ -1,4 +1,4 @@ -//! HTTP/WebSocket API server for the OpenFang Agent OS daemon. +//! HTTP/WebSocket API server for the OMTAE Agent OS daemon. //! //! Exposes agent management, status, and chat via JSON REST endpoints. //! The kernel runs in-process; the CLI connects over HTTP. @@ -32,7 +32,10 @@ fn hex_val(b: u8) -> Option { } } +pub mod brain; pub mod channel_bridge; +pub mod gpu; +pub mod model_profiles; pub mod middleware; pub mod openai_compat; pub mod rate_limiter; diff --git a/crates/openfang-api/src/middleware.rs b/crates/openfang-api/src/middleware.rs index 5d9e363063..6919ff425c 100644 --- a/crates/openfang-api/src/middleware.rs +++ b/crates/openfang-api/src/middleware.rs @@ -1,4 +1,4 @@ -//! Production middleware for the OpenFang API server. +//! Production middleware for the OMTAE API server. //! //! Provides: //! - Request ID generation and propagation @@ -47,8 +47,11 @@ pub async fn request_logging(request: Request, next: Next) -> Response header" @@ -293,8 +362,10 @@ mod tests { fn auth_state_empty() -> AuthState { AuthState { api_key: String::new(), + raw_api_key: String::new(), auth_enabled: false, session_secret: String::new(), + dashboard_pin: String::new(), allow_no_auth: false, } } @@ -302,8 +373,10 @@ mod tests { fn auth_state_with_key(key: &str) -> AuthState { AuthState { api_key: key.to_string(), + raw_api_key: key.to_string(), auth_enabled: false, session_secret: key.to_string(), + dashboard_pin: String::new(), allow_no_auth: false, } } @@ -396,4 +469,27 @@ mod tests { let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } + + #[tokio::test] + async fn test_internal_token_bypasses_pin_auth() { + // Simulates active PIN auth: api_key is empty, but raw_api_key is set. + let state = AuthState { + api_key: String::new(), + raw_api_key: "secret".to_string(), + auth_enabled: true, + session_secret: "hash".to_string(), + dashboard_pin: "123456".to_string(), + allow_no_auth: false, + }; + let app = router(state); + let addr: SocketAddr = "127.0.0.1:40000".parse().unwrap(); + let mut req = Request::builder() + .method(Method::GET) + .uri("/api/agents/1?internal=secret") + .body(Body::empty()) + .unwrap(); + req.extensions_mut().insert(ConnectInfo(addr)); + let resp = app.oneshot(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } } diff --git a/crates/openfang-api/src/model_profiles.rs b/crates/openfang-api/src/model_profiles.rs new file mode 100644 index 0000000000..fb5806c810 --- /dev/null +++ b/crates/openfang-api/src/model_profiles.rs @@ -0,0 +1,147 @@ +//! Local vLLM model profiles (`~/.omtae/models.toml` + `active_model.txt`). + +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelProfile { + pub id: String, + pub display_name: String, + pub vllm_path: String, + pub tensor_parallel: u32, + pub max_model_len: u32, + pub gpu_mem_util: f64, + pub vllm_served_name: String, + /// Model id in OMTAE (`config.toml` + `custom_models.json`). + pub omtae_model_id: String, + pub start_script: String, + #[serde(default)] + pub quantization: Option, +} + +#[derive(Debug, Deserialize)] +struct ModelsFile { + #[serde(default)] + profiles: Vec, +} + +pub fn models_toml_path(home: &Path) -> PathBuf { + home.join("models.toml") +} + +pub fn active_profile_path(home: &Path) -> PathBuf { + home.join("active_model.txt") +} + +pub fn load_profiles(home: &Path) -> Result, String> { + let path = models_toml_path(home); + if !path.exists() { + return Ok(Vec::new()); + } + let content = std::fs::read_to_string(&path) + .map_err(|e| format!("read {}: {e}", path.display()))?; + let file: ModelsFile = toml::from_str(&content) + .map_err(|e| format!("parse {}: {e}", path.display()))?; + Ok(file.profiles) +} + +pub fn read_active_profile_id(home: &Path) -> Option { + let path = active_profile_path(home); + let id = std::fs::read_to_string(path).ok()?; + let id = id.trim().to_string(); + if id.is_empty() { + None + } else { + Some(id) + } +} + +pub fn write_active_profile_id(home: &Path, profile_id: &str) -> Result<(), String> { + let path = active_profile_path(home); + std::fs::write(&path, format!("{profile_id}\n")) + .map_err(|e| format!("write {}: {e}", path.display())) +} + +pub fn find_profile<'a>(profiles: &'a [ModelProfile], id: &str) -> Option<&'a ModelProfile> { + profiles.iter().find(|p| p.id == id) +} + +/// Update `[default_model].model` in config.toml (preserves other keys). +pub fn persist_default_model_id(home: &Path, model_id: &str) -> Result<(), String> { + let config_path = home.join("config.toml"); + let mut table: toml::value::Table = if config_path.exists() { + let content = std::fs::read_to_string(&config_path) + .map_err(|e| format!("read config.toml: {e}"))?; + toml::from_str(&content).unwrap_or_default() + } else { + toml::value::Table::new() + }; + + let section = table + .entry("default_model".to_string()) + .or_insert_with(|| toml::Value::Table(toml::value::Table::new())); + if let toml::Value::Table(ref mut t) = section { + t.insert( + "model".to_string(), + toml::Value::String(model_id.to_string()), + ); + if !t.contains_key("provider") { + t.insert("provider".to_string(), toml::Value::String("vllm".to_string())); + } + if !t.contains_key("api_key_env") { + t.insert( + "api_key_env".to_string(), + toml::Value::String("VLLM_API_KEY".to_string()), + ); + } + if !t.contains_key("base_url") { + t.insert( + "base_url".to_string(), + toml::Value::String("http://127.0.0.1:8000/v1".to_string()), + ); + } + } + + let toml_string = toml::to_string_pretty(&table) + .map_err(|e| format!("serialize config.toml: {e}"))?; + std::fs::write(&config_path, toml_string) + .map_err(|e| format!("write config.toml: {e}"))?; + Ok(()) +} + +pub fn profile_available_on_disk(profile: &ModelProfile) -> bool { + let p = Path::new(&profile.vllm_path); + p.is_dir() && p.join("config.json").exists() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[test] + fn load_profiles_parses_array() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("models.toml"); + let mut f = std::fs::File::create(&path).unwrap(); + writeln!( + f, + r#" +[[profiles]] +id = "test" +display_name = "Test" +vllm_path = "/tmp/m" +tensor_parallel = 2 +max_model_len = 8192 +gpu_mem_util = 0.8 +vllm_served_name = "test-model" +omtae_model_id = "test-model" +start_script = "/tmp/start.sh" +"# + ) + .unwrap(); + let profiles = load_profiles(dir.path()).unwrap(); + assert_eq!(profiles.len(), 1); + assert_eq!(profiles[0].id, "test"); + } +} diff --git a/crates/openfang-api/src/openai_compat.rs b/crates/openfang-api/src/openai_compat.rs index b87d6893f2..fec0288156 100644 --- a/crates/openfang-api/src/openai_compat.rs +++ b/crates/openfang-api/src/openai_compat.rs @@ -1,7 +1,7 @@ //! OpenAI-compatible `/v1/chat/completions` API endpoint. //! -//! Allows any OpenAI-compatible client library to talk to OpenFang agents. -//! The `model` field resolves to an agent (by name, UUID, or `openfang:`), +//! Allows any OpenAI-compatible client library to talk to OMTAE agents. +//! The `model` field resolves to an agent (by name, UUID, or `omtae:`), //! and the messages are forwarded to the agent's LLM loop. //! //! Supports both streaming (SSE) and non-streaming responses. @@ -12,10 +12,10 @@ use axum::http::StatusCode; use axum::response::sse::{Event as SseEvent, KeepAlive, Sse}; use axum::response::IntoResponse; use axum::Json; -use openfang_runtime::kernel_handle::KernelHandle; -use openfang_runtime::llm_driver::StreamEvent; -use openfang_types::agent::AgentId; -use openfang_types::message::{ContentBlock, Message, MessageContent, Role, StopReason}; +use omtae_runtime::kernel_handle::KernelHandle; +use omtae_runtime::llm_driver::StreamEvent; +use omtae_types::agent::AgentId; +use omtae_types::message::{ContentBlock, Message, MessageContent, Role, StopReason}; use serde::{Deserialize, Serialize}; use std::convert::Infallible; use std::sync::Arc; @@ -160,8 +160,8 @@ struct ModelListResponse { // ── Agent resolution ──────────────────────────────────────────────────────── fn resolve_agent(state: &AppState, model: &str) -> Option<(AgentId, String)> { - // 1. "openfang:" → find agent by name - if let Some(name) = model.strip_prefix("openfang:") { + // 1. "omtae:" → find agent by name + if let Some(name) = model.strip_prefix("omtae:") { if let Some(entry) = state.kernel.registry.find_by_name(name) { return Some((entry.id, entry.name.clone())); } @@ -547,10 +547,10 @@ pub async fn list_models(State(state): State>) -> impl IntoRespons let models: Vec = agents .iter() .map(|e| ModelObject { - id: format!("openfang:{}", e.name), + id: format!("omtae:{}", e.name), object: "model", created, - owned_by: "openfang".to_string(), + owned_by: "omtae".to_string(), }) .collect(); diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index cebb1f599a..05735aa6ac 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -1,4 +1,4 @@ -//! Route handlers for the OpenFang API. +//! Route handlers for the OMTAE API. use crate::types::*; use axum::extract::{Multipart, Path, Query, State}; @@ -6,14 +6,14 @@ use axum::http::StatusCode; use axum::response::IntoResponse; use axum::Json; use dashmap::DashMap; -use openfang_kernel::triggers::{TriggerId, TriggerPattern}; -use openfang_kernel::workflow::{ +use omtae_kernel::triggers::{TriggerId, TriggerPattern}; +use omtae_kernel::workflow::{ ErrorMode, StepAgent, StepMode, Workflow, WorkflowId, WorkflowStep, }; -use openfang_kernel::OpenFangKernel; -use openfang_runtime::kernel_handle::KernelHandle; -use openfang_runtime::tool_runner::builtin_tool_definitions; -use openfang_types::agent::{AgentId, AgentIdentity, AgentManifest}; +use omtae_kernel::OMTAEKernel; +use omtae_runtime::kernel_handle::KernelHandle; +use omtae_runtime::tool_runner::builtin_tool_definitions; +use omtae_types::agent::{AgentId, AgentIdentity, AgentManifest}; use std::collections::HashMap; use std::sync::{Arc, LazyLock}; use std::time::Instant; @@ -23,14 +23,14 @@ use std::time::Instant; /// The kernel is wrapped in Arc so it can serve as both the main kernel /// and the KernelHandle for inter-agent tool access. pub struct AppState { - pub kernel: Arc, + pub kernel: Arc, pub started_at: Instant, /// Optional peer registry for OFP mesh networking status. - pub peer_registry: Option>, + pub peer_registry: Option>, /// Channel bridge manager — held behind a Mutex so it can be swapped on hot-reload. - pub bridge_manager: tokio::sync::Mutex>, + pub bridge_manager: tokio::sync::Mutex>, /// Live channel config — updated on every hot-reload so list_channels() reflects reality. - pub channels_config: tokio::sync::RwLock, + pub channels_config: tokio::sync::RwLock, /// Notify handle to trigger graceful HTTP server shutdown from the API. pub shutdown_notify: Arc, /// ClawHub response cache — prevents 429 rate limiting on rapid dashboard refreshes. @@ -39,10 +39,48 @@ pub struct AppState { /// Probe cache for local provider health checks (ollama/vllm/lmstudio). /// Avoids blocking the `/api/providers` endpoint on TCP timeouts to /// unreachable local services. 60-second TTL. - pub provider_probe_cache: openfang_runtime::provider_health::ProbeCache, + pub provider_probe_cache: omtae_runtime::provider_health::ProbeCache, /// Thread-safe mutable budget config. Updated via PUT /api/budget. /// Initialized from `kernel.config.budget` at startup. - pub budget_config: Arc>, + pub budget_config: Arc>, +} + +/// Effective kernel `[default_model]` (honours hot-reload override). +fn effective_default_model(kernel: &OMTAEKernel) -> omtae_types::config::DefaultModelConfig { + kernel + .default_model_override + .read() + .unwrap_or_else(|e| e.into_inner()) + .clone() + .unwrap_or_else(|| kernel.config.default_model.clone()) +} + +/// Model ID to use when probing or testing a provider (wizard Save & Test). +fn model_for_provider_test( + kernel: &OMTAEKernel, + catalog: &omtae_runtime::model_catalog::ModelCatalog, + provider: &str, +) -> String { + let dm = effective_default_model(kernel); + if dm.provider == provider && !dm.model.is_empty() { + return dm.model; + } + catalog + .default_model_for_provider(provider) + .unwrap_or_default() +} + +/// Optional API key from the provider's configured env var (e.g. `VLLM_API_KEY`). +fn provider_api_key_from_env(catalog: &omtae_runtime::model_catalog::ModelCatalog, provider: &str) -> Option { + catalog + .get_provider(provider) + .and_then(|p| { + if p.api_key_env.is_empty() { + None + } else { + std::env::var(&p.api_key_env).ok().filter(|k| !k.is_empty()) + } + }) } /// POST /api/agents — Spawn a new agent. @@ -122,7 +160,7 @@ pub async fn spawn_agent( tracing::warn!("Manifest signature verification failed: {e}"); state.kernel.audit_log.record( "system", - openfang_runtime::audit::AuditAction::AuthAttempt, + omtae_runtime::audit::AuditAction::AuthAttempt, "manifest signature verification failed", format!("error: {e}"), ); @@ -170,6 +208,17 @@ pub async fn spawn_agent( } } +/// Human-readable schedule label for dashboard agent cards. +fn schedule_mode_label(schedule: &omtae_types::agent::ScheduleMode) -> &'static str { + use omtae_types::agent::ScheduleMode; + match schedule { + ScheduleMode::Reactive => "reactive", + ScheduleMode::Continuous { .. } => "continuous", + ScheduleMode::Periodic { .. } => "periodic", + ScheduleMode::Proactive { .. } => "proactive", + } +} + /// GET /api/agents — List all agents. pub async fn list_agents(State(state): State>) -> impl IntoResponse { // Snapshot catalog once for enrichment @@ -212,7 +261,7 @@ pub async fn list_agents(State(state): State>) -> impl IntoRespons }) .unwrap_or(("unknown".to_string(), "unknown".to_string())); - let ready = matches!(e.state, openfang_types::agent::AgentState::Running) + let ready = matches!(e.state, omtae_types::agent::AgentState::Running) && auth_status != "missing"; // Issue #1026: surface which agents are currently calling the LLM @@ -221,6 +270,9 @@ pub async fn list_agents(State(state): State>) -> impl IntoRespons // agent loop is in flight (LLM call + tool dispatch). let is_inferencing = state.kernel.running_tasks.contains_key(&e.id); + let schedule = schedule_mode_label(&e.manifest.schedule); + let background_paused = state.kernel.background.is_paused(e.id); + serde_json::json!({ "id": e.id.to_string(), "name": e.name, @@ -234,6 +286,9 @@ pub async fn list_agents(State(state): State>) -> impl IntoRespons "auth_status": auth_status, "ready": ready, "is_inferencing": is_inferencing, + "schedule": schedule, + "background_paused": background_paused, + "is_autonomous": e.manifest.autonomous.is_some(), "profile": e.manifest.profile, "identity": { "emoji": e.identity.emoji, @@ -253,10 +308,10 @@ pub async fn list_agents(State(state): State>) -> impl IntoRespons /// returns image content blocks ready to insert into a session message. pub fn resolve_attachments( attachments: &[AttachmentRef], -) -> Vec { +) -> Vec { use base64::Engine; - let upload_dir = std::env::temp_dir().join("openfang_uploads"); + let upload_dir = std::env::temp_dir().join("omtae_uploads"); let mut blocks = Vec::new(); for att in attachments { @@ -284,7 +339,7 @@ pub fn resolve_attachments( match std::fs::read(&file_path) { Ok(data) => { let b64 = base64::engine::general_purpose::STANDARD.encode(&data); - blocks.push(openfang_types::message::ContentBlock::Image { + blocks.push(omtae_types::message::ContentBlock::Image { media_type: content_type, data: b64, }); @@ -303,11 +358,11 @@ pub fn resolve_attachments( /// This injects image content blocks into the session BEFORE the kernel /// adds the text user message, so the LLM receives: [..., User(images), User(text)]. pub fn inject_attachments_into_session( - kernel: &OpenFangKernel, + kernel: &OMTAEKernel, agent_id: AgentId, - image_blocks: Vec, + image_blocks: Vec, ) { - use openfang_types::message::{Message, MessageContent, Role}; + use omtae_types::message::{Message, MessageContent, Role}; let entry = match kernel.registry.get(agent_id) { Some(e) => e, @@ -316,7 +371,7 @@ pub fn inject_attachments_into_session( let mut session = match kernel.memory.get_session(entry.session_id) { Ok(Some(s)) => s, - _ => openfang_memory::session::Session { + _ => omtae_memory::session::Session { id: entry.session_id, agent_id, messages: Vec::new(), @@ -427,17 +482,34 @@ pub async fn send_message( } Err(e) => { tracing::warn!("send_message failed for agent {id}: {e}"); - let status = if format!("{e}").contains("Agent not found") { + let err_str = format!("{e}"); + let status = if err_str.contains("Agent not found") { StatusCode::NOT_FOUND - } else if format!("{e}").contains("quota") || format!("{e}").contains("Quota") { + } else if err_str.contains("quota") || err_str.contains("Quota") { StatusCode::TOO_MANY_REQUESTS + } else if err_str.contains("Context too long") + || err_str.contains("context length") + || err_str.contains("ContextOverflow") + { + StatusCode::BAD_REQUEST + } else if err_str.contains("401") || err_str.contains("Unauthorized") { + StatusCode::BAD_GATEWAY } else { StatusCode::INTERNAL_SERVER_ERROR }; - ( - status, - Json(serde_json::json!({"error": format!("Message delivery failed: {e}")})), - ) + let user_msg: String = if err_str.contains("Context too long") + || err_str.contains("context length") + || err_str.contains("ContextOverflow") + { + "Prompt too long for this model — try a shorter message or reduce max_tokens on the agent." + .to_string() + } else if err_str.contains("401") || err_str.contains("Unauthorized") { + "LLM provider rejected the API key — check VLLM_API_KEY (or your provider key) in Settings." + .to_string() + } else { + format!("Message delivery failed: {e}") + }; + (status, Json(serde_json::json!({"error": user_msg}))) } } } @@ -488,10 +560,10 @@ pub async fn get_agent_session( // logic so the system prompt cannot leak into the response. The // raw message count is preserved separately for the API consumer. let raw_message_count = session.messages.len(); - let filtered_messages: Vec<&openfang_types::message::Message> = session + let filtered_messages: Vec<&omtae_types::message::Message> = session .messages .iter() - .filter(|m| include_system || m.role != openfang_types::message::Role::System) + .filter(|m| include_system || m.role != omtae_types::message::Role::System) .collect(); // Two-pass approach: ToolUse blocks live in Assistant messages while @@ -509,15 +581,15 @@ pub async fn get_agent_session( let mut tools: Vec = Vec::new(); let mut msg_images: Vec = Vec::new(); let content = match &m.content { - openfang_types::message::MessageContent::Text(t) => t.clone(), - openfang_types::message::MessageContent::Blocks(blocks) => { + omtae_types::message::MessageContent::Text(t) => t.clone(), + omtae_types::message::MessageContent::Blocks(blocks) => { let mut texts = Vec::new(); for b in blocks { match b { - openfang_types::message::ContentBlock::Text { text, .. } => { + omtae_types::message::ContentBlock::Text { text, .. } => { texts.push(text.clone()); } - openfang_types::message::ContentBlock::Image { + omtae_types::message::ContentBlock::Image { media_type, data, } => { @@ -525,7 +597,7 @@ pub async fn get_agent_session( // Persist image to upload dir so it can be // served back when loading session history. let file_id = uuid::Uuid::new_v4().to_string(); - let upload_dir = std::env::temp_dir().join("openfang_uploads"); + let upload_dir = std::env::temp_dir().join("omtae_uploads"); let _ = std::fs::create_dir_all(&upload_dir); if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(data) @@ -547,7 +619,7 @@ pub async fn get_agent_session( })); } } - openfang_types::message::ContentBlock::ToolUse { + omtae_types::message::ContentBlock::ToolUse { id, name, input, @@ -564,7 +636,7 @@ pub async fn get_agent_session( tool_use_index.insert(id.clone(), (usize::MAX, tool_idx)); } // ToolResult blocks are handled in pass 2 - openfang_types::message::ContentBlock::ToolResult { .. } => {} + omtae_types::message::ContentBlock::ToolResult { .. } => {} _ => {} } } @@ -597,9 +669,9 @@ pub async fn get_agent_session( // Pass 2: walk filtered messages again and attach ToolResult to the correct tool for m in &filtered_messages { - if let openfang_types::message::MessageContent::Blocks(blocks) = &m.content { + if let omtae_types::message::MessageContent::Blocks(blocks) = &m.content { for b in blocks { - if let openfang_types::message::ContentBlock::ToolResult { + if let omtae_types::message::ContentBlock::ToolResult { tool_use_id, content: result, is_error, @@ -695,7 +767,7 @@ pub async fn kill_agent( /// DELETE /api/agents/{id}/uninstall — Permanently uninstall an agent. /// /// Issue #1163: in addition to killing the agent (registry + memory + cron), -/// this also removes the on-disk `~/.openfang/agents//` directory so +/// this also removes the on-disk `~/.omtae/agents//` directory so /// the agent does not auto-respawn on the next daemon start. pub async fn uninstall_agent( State(state): State>, @@ -731,7 +803,7 @@ pub async fn uninstall_agent( ); } - // Step 2: remove ~/.openfang/agents// so the agent does NOT + // Step 2: remove ~/.omtae/agents// so the agent does NOT // auto-respawn from disk on the next daemon start. let agents_dir = state.kernel.config.home_dir.join("agents"); let agent_dir = agents_dir.join(&agent_name); @@ -824,12 +896,22 @@ pub async fn restart_agent( let _ = state .kernel .registry - .set_state(agent_id, openfang_types::agent::AgentState::Running); + .set_state(agent_id, omtae_types::agent::AgentState::Running); + + // Hot-reload manifest from disk (prompt, tools, exec_policy, etc.) + let manifest_reloaded = match state.kernel.reload_agent_manifest_from_disk(agent_id) { + Ok(()) => true, + Err(e) => { + tracing::warn!(agent = %agent_name, error = %e, "Agent restart: manifest disk reload failed"); + false + } + }; tracing::info!( agent = %agent_name, previous_state = %previous_state, task_cancelled = was_running, + manifest_reloaded, "Agent restarted via API" ); @@ -841,6 +923,7 @@ pub async fn restart_agent( "agent_id": id, "previous_state": previous_state, "task_cancelled": was_running, + "manifest_reloaded": manifest_reloaded, })), ) } @@ -890,7 +973,7 @@ pub async fn shutdown(State(state): State>) -> impl IntoResponse { // SECURITY: Record shutdown in audit trail state.kernel.audit_log.record( "system", - openfang_runtime::audit::AuditAction::ConfigChange, + omtae_runtime::audit::AuditAction::ConfigChange, "shutdown requested via API", "ok", ); @@ -1388,7 +1471,7 @@ pub async fn delete_trigger( /// GET /api/profiles — List all tool profiles and their tool lists. pub async fn list_profiles() -> impl IntoResponse { - use openfang_types::agent::ToolProfile; + use omtae_types::agent::ToolProfile; let profiles = [ ("minimal", ToolProfile::Minimal), @@ -1451,7 +1534,7 @@ pub async fn set_agent_mode( /// GET /api/version — Build & version info. pub async fn version() -> impl IntoResponse { Json(serde_json::json!({ - "name": "openfang", + "name": "omtae", "version": env!("CARGO_PKG_VERSION"), "build_date": option_env!("BUILD_DATE").unwrap_or("dev"), "git_sha": option_env!("GIT_SHA").unwrap_or("unknown"), @@ -1536,7 +1619,7 @@ pub async fn send_message_stream( ) -> axum::response::Response { use axum::response::sse::{Event, Sse}; use futures::stream; - use openfang_runtime::llm_driver::StreamEvent; + use omtae_runtime::llm_driver::StreamEvent; // SECURITY: Reject oversized messages to prevent OOM / LLM token abuse. const MAX_MESSAGE_SIZE: usize = 64 * 1024; // 64KB @@ -1776,7 +1859,7 @@ const CHANNEL_REGISTRY: &[ChannelMeta] = &[ fields: &[ ChannelField { key: "access_token_env", label: "Access Token", field_type: FieldType::Secret, env_var: Some("MATRIX_ACCESS_TOKEN"), required: true, placeholder: "syt_...", advanced: false }, ChannelField { key: "homeserver_url", label: "Homeserver URL", field_type: FieldType::Text, env_var: None, required: true, placeholder: "https://matrix.org", advanced: false }, - ChannelField { key: "user_id", label: "Bot User ID", field_type: FieldType::Text, env_var: None, required: false, placeholder: "@openfang:matrix.org", advanced: true }, + ChannelField { key: "user_id", label: "Bot User ID", field_type: FieldType::Text, env_var: None, required: false, placeholder: "@omtae:matrix.org", advanced: true }, ChannelField { key: "allowed_rooms", label: "Allowed Room IDs", field_type: FieldType::List, env_var: None, required: false, placeholder: "!abc:matrix.org", advanced: true }, ChannelField { key: "default_agent", label: "Default Agent", field_type: FieldType::Text, env_var: None, required: false, placeholder: "assistant", advanced: true }, ], @@ -1868,7 +1951,7 @@ const CHANNEL_REGISTRY: &[ChannelMeta] = &[ quick_setup: "Enter your username and paper key", setup_type: "form", fields: &[ - ChannelField { key: "username", label: "Username", field_type: FieldType::Text, env_var: None, required: true, placeholder: "openfang_bot", advanced: false }, + ChannelField { key: "username", label: "Username", field_type: FieldType::Text, env_var: None, required: true, placeholder: "omtae_bot", advanced: false }, ChannelField { key: "paperkey_env", label: "Paper Key", field_type: FieldType::Secret, env_var: Some("KEYBASE_PAPERKEY"), required: true, placeholder: "word1 word2 word3...", advanced: false }, ChannelField { key: "allowed_teams", label: "Allowed Teams", field_type: FieldType::List, env_var: None, required: false, placeholder: "team1, team2", advanced: true }, ChannelField { key: "default_agent", label: "Default Agent", field_type: FieldType::Text, env_var: None, required: false, placeholder: "assistant", advanced: true }, @@ -1886,9 +1969,9 @@ const CHANNEL_REGISTRY: &[ChannelMeta] = &[ fields: &[ ChannelField { key: "client_id", label: "Client ID", field_type: FieldType::Text, env_var: None, required: true, placeholder: "abc123def", advanced: false }, ChannelField { key: "client_secret_env", label: "Client Secret", field_type: FieldType::Secret, env_var: Some("REDDIT_CLIENT_SECRET"), required: true, placeholder: "abc123...", advanced: false }, - ChannelField { key: "username", label: "Bot Username", field_type: FieldType::Text, env_var: None, required: true, placeholder: "openfang_bot", advanced: false }, + ChannelField { key: "username", label: "Bot Username", field_type: FieldType::Text, env_var: None, required: true, placeholder: "omtae_bot", advanced: false }, ChannelField { key: "password_env", label: "Bot Password", field_type: FieldType::Secret, env_var: Some("REDDIT_PASSWORD"), required: true, placeholder: "password", advanced: false }, - ChannelField { key: "subreddits", label: "Subreddits", field_type: FieldType::List, env_var: None, required: false, placeholder: "openfang, rust", advanced: true }, + ChannelField { key: "subreddits", label: "Subreddits", field_type: FieldType::List, env_var: None, required: false, placeholder: "omtae, rust", advanced: true }, ChannelField { key: "default_agent", label: "Default Agent", field_type: FieldType::Text, env_var: None, required: false, placeholder: "assistant", advanced: true }, ], setup_steps: &["Create a Reddit app at reddit.com/prefs/apps (script type)", "Copy Client ID and Secret", "Enter bot credentials below"], @@ -2130,14 +2213,14 @@ const CHANNEL_REGISTRY: &[ChannelMeta] = &[ setup_type: "form", fields: &[ ChannelField { key: "server", label: "Server", field_type: FieldType::Text, env_var: None, required: true, placeholder: "irc.libera.chat", advanced: false }, - ChannelField { key: "nick", label: "Nickname", field_type: FieldType::Text, env_var: None, required: true, placeholder: "openfang", advanced: false }, - ChannelField { key: "channels", label: "Channels", field_type: FieldType::List, env_var: None, required: false, placeholder: "#openfang, #general", advanced: false }, + ChannelField { key: "nick", label: "Nickname", field_type: FieldType::Text, env_var: None, required: true, placeholder: "omtae", advanced: false }, + ChannelField { key: "channels", label: "Channels", field_type: FieldType::List, env_var: None, required: false, placeholder: "#omtae, #general", advanced: false }, ChannelField { key: "port", label: "Port", field_type: FieldType::Number, env_var: None, required: false, placeholder: "6667", advanced: true }, ChannelField { key: "use_tls", label: "Use TLS", field_type: FieldType::Text, env_var: None, required: false, placeholder: "false", advanced: true }, ChannelField { key: "default_agent", label: "Default Agent", field_type: FieldType::Text, env_var: None, required: false, placeholder: "assistant", advanced: true }, ], setup_steps: &["Choose an IRC server", "Enter server, nick, and channels below"], - config_template: "[channels.irc]\nserver = \"irc.libera.chat\"\nnick = \"openfang\"", + config_template: "[channels.irc]\nserver = \"irc.libera.chat\"\nnick = \"omtae\"", }, ChannelMeta { name: "xmpp", display_name: "XMPP/Jabber", icon: "XM", @@ -2253,12 +2336,12 @@ const CHANNEL_REGISTRY: &[ChannelMeta] = &[ setup_type: "form", fields: &[ ChannelField { key: "oauth_token_env", label: "OAuth Token", field_type: FieldType::Secret, env_var: Some("TWITCH_OAUTH_TOKEN"), required: true, placeholder: "oauth:abc123...", advanced: false }, - ChannelField { key: "nick", label: "Bot Nickname", field_type: FieldType::Text, env_var: None, required: true, placeholder: "openfang", advanced: false }, + ChannelField { key: "nick", label: "Bot Nickname", field_type: FieldType::Text, env_var: None, required: true, placeholder: "omtae", advanced: false }, ChannelField { key: "channels", label: "Channels (no #)", field_type: FieldType::List, env_var: None, required: true, placeholder: "mychannel", advanced: false }, ChannelField { key: "default_agent", label: "Default Agent", field_type: FieldType::Text, env_var: None, required: false, placeholder: "assistant", advanced: true }, ], setup_steps: &["Generate an OAuth token at twitchapps.com/tmi", "Enter token, nick, and channel below"], - config_template: "[channels.twitch]\noauth_token_env = \"TWITCH_OAUTH_TOKEN\"\nnick = \"openfang\"", + config_template: "[channels.twitch]\noauth_token_env = \"TWITCH_OAUTH_TOKEN\"\nnick = \"omtae\"", }, // ── Notifications (4) ─────────────────────────────────────────── ChannelMeta { @@ -2268,7 +2351,7 @@ const CHANNEL_REGISTRY: &[ChannelMeta] = &[ quick_setup: "Just enter a topic name", setup_type: "form", fields: &[ - ChannelField { key: "topic", label: "Topic", field_type: FieldType::Text, env_var: None, required: true, placeholder: "openfang-alerts", advanced: false }, + ChannelField { key: "topic", label: "Topic", field_type: FieldType::Text, env_var: None, required: true, placeholder: "omtae-alerts", advanced: false }, ChannelField { key: "server_url", label: "Server URL", field_type: FieldType::Text, env_var: None, required: false, placeholder: "https://ntfy.sh", advanced: true }, ChannelField { key: "token_env", label: "Auth Token", field_type: FieldType::Secret, env_var: Some("NTFY_TOKEN"), required: false, placeholder: "tk_abc123...", advanced: true }, ChannelField { key: "default_agent", label: "Default Agent", field_type: FieldType::Text, env_var: None, required: false, placeholder: "assistant", advanced: true }, @@ -2314,14 +2397,14 @@ const CHANNEL_REGISTRY: &[ChannelMeta] = &[ setup_type: "form", fields: &[ ChannelField { key: "host", label: "Host", field_type: FieldType::Text, env_var: None, required: true, placeholder: "mumble.example.com", advanced: false }, - ChannelField { key: "username", label: "Username", field_type: FieldType::Text, env_var: None, required: true, placeholder: "openfang", advanced: false }, + ChannelField { key: "username", label: "Username", field_type: FieldType::Text, env_var: None, required: true, placeholder: "omtae", advanced: false }, ChannelField { key: "password_env", label: "Server Password", field_type: FieldType::Secret, env_var: Some("MUMBLE_PASSWORD"), required: false, placeholder: "password", advanced: true }, ChannelField { key: "port", label: "Port", field_type: FieldType::Number, env_var: None, required: false, placeholder: "64738", advanced: true }, ChannelField { key: "channel", label: "Channel", field_type: FieldType::Text, env_var: None, required: false, placeholder: "Root", advanced: true }, ChannelField { key: "default_agent", label: "Default Agent", field_type: FieldType::Text, env_var: None, required: false, placeholder: "assistant", advanced: true }, ], setup_steps: &["Enter host and username below", "Optionally add a password"], - config_template: "[channels.mumble]\nhost = \"\"\nusername = \"openfang\"", + config_template: "[channels.mumble]\nhost = \"\"\nusername = \"omtae\"", }, ChannelMeta { name: "wecom", display_name: "WeCom", icon: "WC", @@ -2344,7 +2427,7 @@ const CHANNEL_REGISTRY: &[ChannelMeta] = &[ ]; /// Check if a channel is configured (has a `[channels.xxx]` section in config). -fn is_channel_configured(config: &openfang_types::config::ChannelsConfig, name: &str) -> bool { +fn is_channel_configured(config: &omtae_types::config::ChannelsConfig, name: &str) -> bool { match name { "telegram" => config.telegram.is_some(), "discord" => config.discord.is_some(), @@ -2470,7 +2553,7 @@ mod channel_meta_tests { /// Serialize a channel's config to a JSON Value for pre-populating dashboard forms. fn channel_config_values( - config: &openfang_types::config::ChannelsConfig, + config: &omtae_types::config::ChannelsConfig, name: &str, ) -> Option { match name { @@ -2729,7 +2812,7 @@ pub async fn configure_channel( } }; - let home = openfang_kernel::config::openfang_home(); + let home = omtae_kernel::config::omtae_home(); let secrets_path = home.join("secrets.env"); let config_path = home.join("config.toml"); let mut config_fields: HashMap = HashMap::new(); @@ -2827,7 +2910,7 @@ pub async fn remove_channel( } }; - let home = openfang_kernel::config::openfang_home(); + let home = omtae_kernel::config::omtae_home(); let secrets_path = home.join("secrets.env"); let config_path = home.join("config.toml"); @@ -2963,7 +3046,7 @@ pub async fn test_channel( /// Send a real test message to a specific channel/chat on the given platform. async fn send_channel_test_message(channel_name: &str, target_id: &str) -> Result<(), String> { let client = reqwest::Client::new(); - let test_msg = "OpenFang test message — your channel is connected!"; + let test_msg = "OMTAE test message — your channel is connected!"; match channel_name { "discord" => { @@ -3247,7 +3330,7 @@ async fn gateway_http_get(url_with_path: &str) -> Result impl IntoResponse { - let agents_dir = openfang_kernel::config::openfang_home().join("agents"); + let agents_dir = omtae_kernel::config::omtae_home().join("agents"); let mut templates = Vec::new(); if let Ok(entries) = std::fs::read_dir(&agents_dir) { @@ -3309,7 +3392,7 @@ fn get_template_category(name: &str) -> &str { /// GET /api/templates/:name — Get template details. pub async fn get_template(Path(name): Path) -> impl IntoResponse { - let agents_dir = openfang_kernel::config::openfang_home().join("agents"); + let agents_dir = omtae_kernel::config::omtae_home().join("agents"); let manifest_path = agents_dir.join(&name).join("agent.toml"); if !manifest_path.exists() { @@ -3372,7 +3455,7 @@ pub async fn get_agent_kv( State(state): State>, Path(_id): Path, ) -> impl IntoResponse { - let agent_id = openfang_kernel::kernel::shared_memory_agent_id(); + let agent_id = omtae_kernel::kernel::shared_memory_agent_id(); match state.kernel.memory.list_kv(agent_id) { Ok(pairs) => { @@ -3397,7 +3480,7 @@ pub async fn get_agent_kv_key( State(state): State>, Path((_id, key)): Path<(String, String)>, ) -> impl IntoResponse { - let agent_id = openfang_kernel::kernel::shared_memory_agent_id(); + let agent_id = omtae_kernel::kernel::shared_memory_agent_id(); match state.kernel.memory.structured_get(agent_id, &key) { Ok(Some(val)) => ( @@ -3424,7 +3507,7 @@ pub async fn set_agent_kv_key( Path((_id, key)): Path<(String, String)>, Json(body): Json, ) -> impl IntoResponse { - let agent_id = openfang_kernel::kernel::shared_memory_agent_id(); + let agent_id = omtae_kernel::kernel::shared_memory_agent_id(); let value = body.get("value").cloned().unwrap_or(body); @@ -3448,7 +3531,7 @@ pub async fn delete_agent_kv_key( State(state): State>, Path((_id, key)): Path<(String, String)>, ) -> impl IntoResponse { - let agent_id = openfang_kernel::kernel::shared_memory_agent_id(); + let agent_id = omtae_kernel::kernel::shared_memory_agent_id(); match state.kernel.memory.structured_delete(agent_id, &key) { Ok(()) => ( @@ -3468,6 +3551,24 @@ pub async fn delete_agent_kv_key( /// GET /api/health — Minimal liveness probe (public, no auth required). /// Returns only status and version to prevent information leakage. /// Use GET /api/health/detail for full diagnostics (requires auth). +/// GET /api/system/gpu — NVIDIA GPU telemetry (nvidia-smi). +pub async fn system_gpu() -> impl IntoResponse { + let stats = crate::gpu::collect_gpu_stats().await; + Json(stats) +} + +/// GET /api/system/drift — Report runtime/config drift (no auto-fix). +pub async fn system_drift(State(state): State>) -> impl IntoResponse { + let report = state.kernel.run_drift_check(false); + Json(report) +} + +/// POST /api/system/drift — Run drift scan and apply safe auto-remediation. +pub async fn system_drift_remediate(State(state): State>) -> impl IntoResponse { + let report = state.kernel.run_drift_check(true); + Json(report) +} + pub async fn health(State(state): State>) -> impl IntoResponse { // Run the database check on a blocking thread so we never hold the // std::sync::Mutex on a tokio worker thread. This prevents @@ -3475,7 +3576,7 @@ pub async fn health(State(state): State>) -> impl IntoResponse { // is holding the database lock for session saves. let memory = state.kernel.memory.clone(); let db_ok = tokio::task::spawn_blocking(move || { - let shared_id = openfang_types::agent::AgentId(uuid::Uuid::from_bytes([ + let shared_id = omtae_types::agent::AgentId(uuid::Uuid::from_bytes([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, ])); memory.structured_get(shared_id, "__health_check__").is_ok() @@ -3497,7 +3598,7 @@ pub async fn health_detail(State(state): State>) -> impl IntoRespo let memory = state.kernel.memory.clone(); let db_ok = tokio::task::spawn_blocking(move || { - let shared_id = openfang_types::agent::AgentId(uuid::Uuid::from_bytes([ + let shared_id = omtae_types::agent::AgentId(uuid::Uuid::from_bytes([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, ])); memory.structured_get(shared_id, "__health_check__").is_ok() @@ -3526,50 +3627,50 @@ pub async fn health_detail(State(state): State>) -> impl IntoRespo /// GET /api/metrics — Prometheus text-format metrics. /// -/// Returns counters and gauges for monitoring OpenFang in production: -/// - `openfang_agents_active` — number of active agents -/// - `openfang_uptime_seconds` — seconds since daemon started -/// - `openfang_tokens_total` — total tokens consumed (per agent) -/// - `openfang_tool_calls_total` — total tool calls (per agent) -/// - `openfang_panics_total` — supervisor panic count -/// - `openfang_restarts_total` — supervisor restart count +/// Returns counters and gauges for monitoring OMTAE in production: +/// - `omtae_agents_active` — number of active agents +/// - `omtae_uptime_seconds` — seconds since daemon started +/// - `omtae_tokens_total` — total tokens consumed (per agent) +/// - `omtae_tool_calls_total` — total tool calls (per agent) +/// - `omtae_panics_total` — supervisor panic count +/// - `omtae_restarts_total` — supervisor restart count pub async fn prometheus_metrics(State(state): State>) -> impl IntoResponse { let mut out = String::with_capacity(2048); // Uptime let uptime = state.started_at.elapsed().as_secs(); - out.push_str("# HELP openfang_uptime_seconds Time since daemon started.\n"); - out.push_str("# TYPE openfang_uptime_seconds gauge\n"); - out.push_str(&format!("openfang_uptime_seconds {uptime}\n\n")); + out.push_str("# HELP omtae_uptime_seconds Time since daemon started.\n"); + out.push_str("# TYPE omtae_uptime_seconds gauge\n"); + out.push_str(&format!("omtae_uptime_seconds {uptime}\n\n")); // Active agents let agents = state.kernel.registry.list(); let active = agents .iter() - .filter(|a| matches!(a.state, openfang_types::agent::AgentState::Running)) + .filter(|a| matches!(a.state, omtae_types::agent::AgentState::Running)) .count(); - out.push_str("# HELP openfang_agents_active Number of active agents.\n"); - out.push_str("# TYPE openfang_agents_active gauge\n"); - out.push_str(&format!("openfang_agents_active {active}\n")); - out.push_str("# HELP openfang_agents_total Total number of registered agents.\n"); - out.push_str("# TYPE openfang_agents_total gauge\n"); - out.push_str(&format!("openfang_agents_total {}\n\n", agents.len())); + out.push_str("# HELP omtae_agents_active Number of active agents.\n"); + out.push_str("# TYPE omtae_agents_active gauge\n"); + out.push_str(&format!("omtae_agents_active {active}\n")); + out.push_str("# HELP omtae_agents_total Total number of registered agents.\n"); + out.push_str("# TYPE omtae_agents_total gauge\n"); + out.push_str(&format!("omtae_agents_total {}\n\n", agents.len())); // Per-agent token and tool usage - out.push_str("# HELP openfang_tokens_total Total tokens consumed (rolling hourly window).\n"); - out.push_str("# TYPE openfang_tokens_total gauge\n"); - out.push_str("# HELP openfang_tool_calls_total Total tool calls (rolling hourly window).\n"); - out.push_str("# TYPE openfang_tool_calls_total gauge\n"); + out.push_str("# HELP omtae_tokens_total Total tokens consumed (rolling hourly window).\n"); + out.push_str("# TYPE omtae_tokens_total gauge\n"); + out.push_str("# HELP omtae_tool_calls_total Total tool calls (rolling hourly window).\n"); + out.push_str("# TYPE omtae_tool_calls_total gauge\n"); for agent in &agents { let name = &agent.name; let provider = &agent.manifest.model.provider; let model = &agent.manifest.model.model; if let Some((tokens, tools)) = state.kernel.scheduler.get_usage(agent.id) { out.push_str(&format!( - "openfang_tokens_total{{agent=\"{name}\",provider=\"{provider}\",model=\"{model}\"}} {tokens}\n" + "omtae_tokens_total{{agent=\"{name}\",provider=\"{provider}\",model=\"{model}\"}} {tokens}\n" )); out.push_str(&format!( - "openfang_tool_calls_total{{agent=\"{name}\"}} {tools}\n" + "omtae_tool_calls_total{{agent=\"{name}\"}} {tools}\n" )); } } @@ -3577,21 +3678,21 @@ pub async fn prometheus_metrics(State(state): State>) -> impl Into // Supervisor health let health = state.kernel.supervisor.health(); - out.push_str("# HELP openfang_panics_total Total supervisor panics since start.\n"); - out.push_str("# TYPE openfang_panics_total counter\n"); - out.push_str(&format!("openfang_panics_total {}\n", health.panic_count)); - out.push_str("# HELP openfang_restarts_total Total supervisor restarts since start.\n"); - out.push_str("# TYPE openfang_restarts_total counter\n"); + out.push_str("# HELP omtae_panics_total Total supervisor panics since start.\n"); + out.push_str("# TYPE omtae_panics_total counter\n"); + out.push_str(&format!("omtae_panics_total {}\n", health.panic_count)); + out.push_str("# HELP omtae_restarts_total Total supervisor restarts since start.\n"); + out.push_str("# TYPE omtae_restarts_total counter\n"); out.push_str(&format!( - "openfang_restarts_total {}\n\n", + "omtae_restarts_total {}\n\n", health.restart_count )); // Version info - out.push_str("# HELP openfang_info OpenFang version and build info.\n"); - out.push_str("# TYPE openfang_info gauge\n"); + out.push_str("# HELP omtae_info OMTAE version and build info.\n"); + out.push_str("# TYPE omtae_info gauge\n"); out.push_str(&format!( - "openfang_info{{version=\"{}\"}} 1\n", + "omtae_info{{version=\"{}\"}} 1\n", env!("CARGO_PKG_VERSION") )); @@ -3612,7 +3713,7 @@ pub async fn prometheus_metrics(State(state): State>) -> impl Into /// GET /api/skills — List installed skills. pub async fn list_skills(State(state): State>) -> impl IntoResponse { let skills_dir = state.kernel.config.home_dir.join("skills"); - let mut registry = openfang_skills::registry::SkillRegistry::new(skills_dir); + let mut registry = omtae_skills::registry::SkillRegistry::new(skills_dir); let _ = registry.load_all(); // Snapshot of user-provided overrides for the `config_resolved_count`. @@ -3635,16 +3736,16 @@ pub async fn list_skills(State(state): State>) -> impl IntoRespons .iter() .map(|s| { let source = match &s.manifest.source { - Some(openfang_skills::SkillSource::ClawHub { slug, version }) => { + Some(omtae_skills::SkillSource::ClawHub { slug, version }) => { serde_json::json!({"type": "clawhub", "slug": slug, "version": version}) } - Some(openfang_skills::SkillSource::OpenClaw) => { + Some(omtae_skills::SkillSource::OpenClaw) => { serde_json::json!({"type": "openclaw"}) } - Some(openfang_skills::SkillSource::Bundled) => { + Some(omtae_skills::SkillSource::Bundled) => { serde_json::json!({"type": "bundled"}) } - Some(openfang_skills::SkillSource::Native) | None => { + Some(omtae_skills::SkillSource::Native) | None => { serde_json::json!({"type": "local"}) } }; @@ -3700,10 +3801,10 @@ pub async fn install_skill( Json(req): Json, ) -> impl IntoResponse { let skills_dir = state.kernel.config.home_dir.join("skills"); - let config = openfang_skills::marketplace::MarketplaceConfig::default(); - let client = openfang_skills::marketplace::MarketplaceClient::new(config); + let config = omtae_skills::marketplace::MarketplaceConfig::default(); + let client = omtae_skills::marketplace::MarketplaceClient::new(config); - let opts = openfang_skills::installer::InstallOptions { + let opts = omtae_skills::installer::InstallOptions { require_signed: req.require_signed, allowed_signer_keys: req.allowed_signer_keys.clone(), }; @@ -3739,7 +3840,7 @@ pub async fn uninstall_skill( Json(req): Json, ) -> impl IntoResponse { let skills_dir = state.kernel.config.home_dir.join("skills"); - let mut registry = openfang_skills::registry::SkillRegistry::new(skills_dir); + let mut registry = omtae_skills::registry::SkillRegistry::new(skills_dir); let _ = registry.load_all(); match registry.remove(&req.name) { @@ -3760,7 +3861,7 @@ pub async fn uninstall_skill( /// POST /api/skills/reload — Hot-reload the skill registry from disk. /// -/// Called by the CLI after `openfang skill install` to notify the running +/// Called by the CLI after `omtae skill install` to notify the running /// daemon that new skill files were added to the skills directory (#752). pub async fn reload_skills(State(state): State>) -> impl IntoResponse { state.kernel.reload_skills(); @@ -3778,7 +3879,7 @@ pub async fn audit_append( State(state): State>, Json(req): Json, ) -> impl IntoResponse { - use openfang_runtime::audit::AuditAction; + use omtae_runtime::audit::AuditAction; // SECURITY: bound input sizes so a wrapper cannot wedge the chain with // unbounded strings. The audit table stores TEXT columns and the chain @@ -3882,8 +3983,8 @@ pub async fn marketplace_search( return Json(serde_json::json!({"results": [], "total": 0})); } - let config = openfang_skills::marketplace::MarketplaceConfig::default(); - let client = openfang_skills::marketplace::MarketplaceClient::new(config); + let config = omtae_skills::marketplace::MarketplaceConfig::default(); + let client = omtae_skills::marketplace::MarketplaceClient::new(config); match client.search(&query).await { Ok(results) => { @@ -3942,7 +4043,7 @@ pub async fn clawhub_search( } let cache_dir = state.kernel.config.home_dir.join(".cache").join("clawhub"); - let client = openfang_skills::clawhub::ClawHubClient::new(cache_dir); + let client = omtae_skills::clawhub::ClawHubClient::new(cache_dir); let skills_dir = state.kernel.config.home_dir.join("skills"); match client.search(&query, limit).await { @@ -3999,11 +4100,11 @@ pub async fn clawhub_browse( Query(params): Query>, ) -> impl IntoResponse { let sort = match params.get("sort").map(|s| s.as_str()) { - Some("downloads") => openfang_skills::clawhub::ClawHubSort::Downloads, - Some("stars") => openfang_skills::clawhub::ClawHubSort::Stars, - Some("updated") => openfang_skills::clawhub::ClawHubSort::Updated, - Some("rating") => openfang_skills::clawhub::ClawHubSort::Rating, - _ => openfang_skills::clawhub::ClawHubSort::Trending, + Some("downloads") => omtae_skills::clawhub::ClawHubSort::Downloads, + Some("stars") => omtae_skills::clawhub::ClawHubSort::Stars, + Some("updated") => omtae_skills::clawhub::ClawHubSort::Updated, + Some("rating") => omtae_skills::clawhub::ClawHubSort::Rating, + _ => omtae_skills::clawhub::ClawHubSort::Trending, }; let limit: u32 = params @@ -4022,7 +4123,7 @@ pub async fn clawhub_browse( } let cache_dir = state.kernel.config.home_dir.join(".cache").join("clawhub"); - let client = openfang_skills::clawhub::ClawHubClient::new(cache_dir); + let client = omtae_skills::clawhub::ClawHubClient::new(cache_dir); let skills_dir = state.kernel.config.home_dir.join("skills"); match client.browse(sort, limit, cursor).await { @@ -4068,7 +4169,7 @@ pub async fn clawhub_skill_detail( Path(slug): Path, ) -> impl IntoResponse { let cache_dir = state.kernel.config.home_dir.join(".cache").join("clawhub"); - let client = openfang_skills::clawhub::ClawHubClient::new(cache_dir); + let client = omtae_skills::clawhub::ClawHubClient::new(cache_dir); let skills_dir = state.kernel.config.home_dir.join("skills"); let is_installed = client.is_installed(&slug, &skills_dir); @@ -4132,7 +4233,7 @@ pub async fn clawhub_skill_code( Path(slug): Path, ) -> impl IntoResponse { let cache_dir = state.kernel.config.home_dir.join(".cache").join("clawhub"); - let client = openfang_skills::clawhub::ClawHubClient::new(cache_dir); + let client = omtae_skills::clawhub::ClawHubClient::new(cache_dir); // Try to fetch SKILL.md first, then fallback to package.json let mut code = String::new(); @@ -4176,7 +4277,7 @@ pub async fn clawhub_install( ) -> impl IntoResponse { let skills_dir = state.kernel.config.home_dir.join("skills"); let cache_dir = state.kernel.config.home_dir.join(".cache").join("clawhub"); - let client = openfang_skills::clawhub::ClawHubClient::new(cache_dir); + let client = omtae_skills::clawhub::ClawHubClient::new(cache_dir); // Check if already installed if client.is_installed(&req.slug, &skills_dir) { @@ -4226,11 +4327,11 @@ pub async fn clawhub_install( } Err(e) => { let msg = format!("{e}"); - let status = if matches!(e, openfang_skills::SkillError::SecurityBlocked(_)) { + let status = if matches!(e, omtae_skills::SkillError::SecurityBlocked(_)) { StatusCode::FORBIDDEN } else if is_clawhub_rate_limit(&e) { StatusCode::TOO_MANY_REQUESTS - } else if matches!(e, openfang_skills::SkillError::Network(_)) { + } else if matches!(e, omtae_skills::SkillError::Network(_)) { StatusCode::BAD_GATEWAY } else { StatusCode::INTERNAL_SERVER_ERROR @@ -4242,15 +4343,15 @@ pub async fn clawhub_install( } /// Check whether a SkillError represents a ClawHub rate-limit (429). -fn is_clawhub_rate_limit(err: &openfang_skills::SkillError) -> bool { - matches!(err, openfang_skills::SkillError::RateLimited(_)) +fn is_clawhub_rate_limit(err: &omtae_skills::SkillError) -> bool { + matches!(err, omtae_skills::SkillError::RateLimited(_)) } /// Convert a browse entry (nested stats/tags) to a flat JSON object for the frontend. fn clawhub_browse_entry_to_json( - entry: &openfang_skills::clawhub::ClawHubBrowseEntry, + entry: &omtae_skills::clawhub::ClawHubBrowseEntry, ) -> serde_json::Value { - let version = openfang_skills::clawhub::ClawHubClient::entry_version(entry); + let version = omtae_skills::clawhub::ClawHubClient::entry_version(entry); serde_json::json!({ "slug": entry.slug, "name": entry.display_name, @@ -4800,7 +4901,7 @@ pub async fn upsert_hand( pub async fn activate_hand( State(state): State>, Path(hand_id): Path, - body: Option>, + body: Option>, ) -> impl IntoResponse { let (config, instance_name) = match body.map(|b| b.0) { Some(r) => (r.config, r.instance_name), @@ -4821,7 +4922,7 @@ pub async fn activate_hand( if let Some(entry) = entry { if !matches!( entry.manifest.schedule, - openfang_types::agent::ScheduleMode::Reactive + omtae_types::agent::ScheduleMode::Reactive ) { state.kernel.start_background_for_agent( agent_id, @@ -5025,7 +5126,7 @@ pub async fn hand_stats( }; // Read dashboard metrics from shared structured memory (memory_store uses shared namespace) - let shared_id = openfang_kernel::kernel::shared_memory_agent_id(); + let shared_id = omtae_kernel::kernel::shared_memory_agent_id(); let mut metrics = serde_json::Map::new(); for metric in &def.dashboard.metrics { // Try shared memory first (where memory_store tool writes), fall back to agent-specific @@ -5106,7 +5207,7 @@ pub async fn hand_instance_browser( .browser_ctx .send_command( &agent_id_str, - openfang_runtime::browser::BrowserCommand::ReadPage, + omtae_runtime::browser::BrowserCommand::ReadPage, ) .await { @@ -5119,7 +5220,7 @@ pub async fn hand_instance_browser( if content.len() > 2000 { content = format!( "{}... (truncated)", - openfang_types::truncate_str(&content, 2000) + omtae_types::truncate_str(&content, 2000) ); } } @@ -5136,7 +5237,7 @@ pub async fn hand_instance_browser( .browser_ctx .send_command( &agent_id_str, - openfang_runtime::browser::BrowserCommand::Screenshot, + omtae_runtime::browser::BrowserCommand::Screenshot, ) .await { @@ -5176,20 +5277,20 @@ pub async fn list_mcp_servers(State(state): State>) -> impl IntoRe .iter() .map(|s| { let transport = match &s.transport { - openfang_types::config::McpTransportEntry::Stdio { command, args } => { + omtae_types::config::McpTransportEntry::Stdio { command, args } => { serde_json::json!({ "type": "stdio", "command": command, "args": args, }) } - openfang_types::config::McpTransportEntry::Sse { url } => { + omtae_types::config::McpTransportEntry::Sse { url } => { serde_json::json!({ "type": "sse", "url": url, }) } - openfang_types::config::McpTransportEntry::Http { url } => { + omtae_types::config::McpTransportEntry::Http { url } => { serde_json::json!({ "type": "http", "url": url, @@ -5695,7 +5796,7 @@ pub async fn agent_budget_status( }; let quota = &entry.manifest.resources; - let usage_store = openfang_memory::usage::UsageStore::new(state.kernel.memory.usage_conn()); + let usage_store = omtae_memory::usage::UsageStore::new(state.kernel.memory.usage_conn()); let hourly = usage_store.query_hourly(agent_id).unwrap_or(0.0); let daily = usage_store.query_daily(agent_id).unwrap_or(0.0); let monthly = usage_store.query_monthly(agent_id).unwrap_or(0.0); @@ -5735,7 +5836,7 @@ pub async fn agent_budget_status( /// GET /api/budget/agents — Per-agent cost ranking (top spenders). pub async fn agent_budget_ranking(State(state): State>) -> impl IntoResponse { - let usage_store = openfang_memory::usage::UsageStore::new(state.kernel.memory.usage_conn()); + let usage_store = omtae_memory::usage::UsageStore::new(state.kernel.memory.usage_conn()); let agents: Vec = state .kernel .registry @@ -5832,7 +5933,7 @@ pub async fn delete_session( Path(id): Path, ) -> impl IntoResponse { let session_id = match id.parse::() { - Ok(u) => openfang_types::agent::SessionId(u), + Ok(u) => omtae_types::agent::SessionId(u), Err(_) => { return ( StatusCode::BAD_REQUEST, @@ -5860,7 +5961,7 @@ pub async fn set_session_label( Json(req): Json, ) -> impl IntoResponse { let session_id = match id.parse::() { - Ok(u) => openfang_types::agent::SessionId(u), + Ok(u) => omtae_types::agent::SessionId(u), Err(_) => { return ( StatusCode::BAD_REQUEST, @@ -5873,7 +5974,7 @@ pub async fn set_session_label( // Validate label if present if let Some(lbl) = label { - if let Err(e) = openfang_types::agent::SessionLabel::new(lbl) { + if let Err(e) = omtae_types::agent::SessionLabel::new(lbl) { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({"error": e.to_string()})), @@ -5903,7 +6004,7 @@ pub async fn find_session_by_label( Path((agent_id_str, label)): Path<(String, String)>, ) -> impl IntoResponse { let agent_id = match agent_id_str.parse::() { - Ok(u) => openfang_types::agent::AgentId(u), + Ok(u) => omtae_types::agent::AgentId(u), Err(_) => { // Try name lookup match state.kernel.registry.find_by_name(&agent_id_str) { @@ -6205,9 +6306,9 @@ pub async fn security_status(State(state): State>) -> impl IntoRes /// GET /api/migrate/detect — Auto-detect OpenClaw installation. pub async fn migrate_detect() -> impl IntoResponse { - match openfang_migrate::openclaw::detect_openclaw_home() { + match omtae_migrate::openclaw::detect_openclaw_home() { Some(path) => { - let scan = openfang_migrate::openclaw::scan_openclaw_workspace(&path); + let scan = omtae_migrate::openclaw::scan_openclaw_workspace(&path); ( StatusCode::OK, Json(serde_json::json!({ @@ -6237,16 +6338,16 @@ pub async fn migrate_scan(Json(req): Json) -> impl IntoRespo Json(serde_json::json!({"error": "Directory not found"})), ); } - let scan = openfang_migrate::openclaw::scan_openclaw_workspace(&path); + let scan = omtae_migrate::openclaw::scan_openclaw_workspace(&path); (StatusCode::OK, Json(serde_json::json!(scan))) } /// POST /api/migrate — Run migration from another agent framework. pub async fn run_migrate(Json(req): Json) -> impl IntoResponse { let source = match req.source.as_str() { - "openclaw" => openfang_migrate::MigrateSource::OpenClaw, - "langchain" => openfang_migrate::MigrateSource::LangChain, - "autogpt" => openfang_migrate::MigrateSource::AutoGpt, + "openclaw" => omtae_migrate::MigrateSource::OpenClaw, + "langchain" => omtae_migrate::MigrateSource::LangChain, + "autogpt" => omtae_migrate::MigrateSource::AutoGpt, other => { return ( StatusCode::BAD_REQUEST, @@ -6257,14 +6358,14 @@ pub async fn run_migrate(Json(req): Json) -> impl IntoResponse { } }; - let options = openfang_migrate::MigrateOptions { + let options = omtae_migrate::MigrateOptions { source, source_dir: std::path::PathBuf::from(&req.source_dir), target_dir: std::path::PathBuf::from(&req.target_dir), dry_run: req.dry_run, }; - match openfang_migrate::run_migration(&options) { + match omtae_migrate::run_migration(&options) { Ok(report) => { let imported: Vec = report .imported @@ -6313,6 +6414,200 @@ pub async fn run_migrate(Json(req): Json) -> impl IntoResponse { // ── Model Catalog Endpoints ───────────────────────────────────────── +/// GET /api/models/profiles — vLLM swap profiles from `~/.omtae/models.toml`. +pub async fn list_model_profiles(State(state): State>) -> impl IntoResponse { + let home = &state.kernel.config.home_dir; + let profiles = match crate::model_profiles::load_profiles(home) { + Ok(p) => p, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"error": e})), + ); + } + }; + let active = crate::model_profiles::read_active_profile_id(home) + .or_else(|| { + profiles + .iter() + .find(|p| p.omtae_model_id == state.kernel.config.default_model.model) + .map(|p| p.id.clone()) + }); + let list: Vec = profiles + .iter() + .map(|p| { + serde_json::json!({ + "id": p.id, + "display_name": p.display_name, + "vllm_path": p.vllm_path, + "tensor_parallel": p.tensor_parallel, + "max_model_len": p.max_model_len, + "gpu_mem_util": p.gpu_mem_util, + "vllm_served_name": p.vllm_served_name, + "omtae_model_id": p.omtae_model_id, + "start_script": p.start_script, + "quantization": p.quantization, + "on_disk": crate::model_profiles::profile_available_on_disk(p), + }) + }) + .collect(); + ( + StatusCode::OK, + Json(serde_json::json!({ + "profiles": list, + "active_profile": active, + "models_toml": crate::model_profiles::models_toml_path(home).display().to_string(), + })), + ) +} + +/// GET /api/models/active — Active vLLM profile + OMTAE default model. +pub async fn get_active_model_profile(State(state): State>) -> impl IntoResponse { + let home = &state.kernel.config.home_dir; + let profiles = crate::model_profiles::load_profiles(home).unwrap_or_default(); + let active_id = crate::model_profiles::read_active_profile_id(home); + let active_profile = active_id + .as_ref() + .and_then(|id| crate::model_profiles::find_profile(&profiles, id)); + let dm = effective_default_model(&state.kernel); + ( + StatusCode::OK, + Json(serde_json::json!({ + "active_profile": active_id, + "active_profile_display": active_profile.map(|p| &p.display_name), + "omtae_model_id": dm.model, + "default_provider": dm.provider, + "vllm_restart_command": "omtae-model use ", + "cli_status_command": "omtae-model status", + })), + ) +} + +/// PUT /api/models/active — Set OMTAE default model from a profile (vLLM restart via CLI). +pub async fn set_active_model_profile( + State(state): State>, + Json(body): Json, +) -> impl IntoResponse { + let profile_id = match body.get("profile_id").and_then(|v| v.as_str()) { + Some(id) => id.to_string(), + None => { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({"status": "error", "error": "missing profile_id"})), + ); + } + }; + let restart_vllm = body + .get("restart_vllm") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let home = &state.kernel.config.home_dir; + let profiles = match crate::model_profiles::load_profiles(home) { + Ok(p) => p, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"status": "error", "error": e})), + ); + } + }; + let Some(profile) = crate::model_profiles::find_profile(&profiles, &profile_id) else { + return ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "status": "error", + "error": format!("unknown profile: {profile_id}"), + "hint": "omtae-model list" + })), + ); + }; + if !crate::model_profiles::profile_available_on_disk(profile) { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "status": "error", + "error": format!("weights not found at {}", profile.vllm_path), + "profile_id": profile_id, + })), + ); + } + + if let Err(e) = crate::model_profiles::write_active_profile_id(home, &profile_id) { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"status": "error", "error": e})), + ); + } + if let Err(e) = crate::model_profiles::persist_default_model_id(home, &profile.omtae_model_id) { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"status": "error", "error": e})), + ); + } + + let reload_status = match state.kernel.reload_config() { + Ok(plan) if plan.restart_required => "applied_partial", + Ok(_) => "applied", + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"status": "error", "error": format!("reload failed: {e}")})), + ); + } + }; + + state.kernel.audit_log.record( + "system", + omtae_runtime::audit::AuditAction::ConfigChange, + format!("active model profile: {profile_id} -> {}", profile.omtae_model_id), + "completed", + ); + + let mut vllm_note = format!( + "OMTAE default set to {}. Restart vLLM: omtae-model use {}", + profile.omtae_model_id, profile_id + ); + let mut vllm_restarted = false; + + if restart_vllm { + let allowed = std::env::var("OMTAE_ALLOW_VLLM_RESTART") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false); + if allowed { + let script = profile.start_script.clone(); + match tokio::process::Command::new("/usr/bin/bash") + .arg(&script) + .spawn() + { + Ok(_) => { + vllm_restarted = true; + vllm_note = format!("vLLM start script launched: {script}"); + } + Err(e) => { + vllm_note = format!("vLLM restart failed to spawn: {e}. Run: omtae-model use {profile_id}"); + } + } + } else { + vllm_note = format!( + "Set OMTAE_ALLOW_VLLM_RESTART=1 on daemon to restart from API, or run: omtae-model use {profile_id}" + ); + } + } + + ( + StatusCode::OK, + Json(serde_json::json!({ + "status": reload_status, + "profile_id": profile_id, + "omtae_model_id": profile.omtae_model_id, + "vllm_served_name": profile.vllm_served_name, + "vllm_restarted": vllm_restarted, + "message": vllm_note, + })), + ) +} + /// GET /api/models — List all models in the catalog. /// /// Query parameters: @@ -6352,7 +6647,7 @@ pub async fn list_models( if available_only { let provider = catalog.get_provider(&m.provider); if let Some(p) = provider { - if p.auth_status == openfang_types::model_catalog::AuthStatus::Missing { + if p.auth_status == omtae_types::model_catalog::AuthStatus::Missing { return false; } } @@ -6363,8 +6658,8 @@ pub async fn list_models( // Custom models from unknown providers are assumed available let available = catalog .get_provider(&m.provider) - .map(|p| p.auth_status != openfang_types::model_catalog::AuthStatus::Missing) - .unwrap_or(m.tier == openfang_types::model_catalog::ModelTier::Custom); + .map(|p| p.auth_status != omtae_types::model_catalog::AuthStatus::Missing) + .unwrap_or(m.tier == omtae_types::model_catalog::ModelTier::Custom); serde_json::json!({ "id": m.id, "display_name": m.display_name, @@ -6437,8 +6732,8 @@ pub async fn get_model( Some(m) => { let available = catalog .get_provider(&m.provider) - .map(|p| p.auth_status != openfang_types::model_catalog::AuthStatus::Missing) - .unwrap_or(m.tier == openfang_types::model_catalog::ModelTier::Custom); + .map(|p| p.auth_status != omtae_types::model_catalog::AuthStatus::Missing) + .unwrap_or(m.tier == omtae_types::model_catalog::ModelTier::Custom); ( StatusCode::OK, Json(serde_json::json!({ @@ -6474,7 +6769,7 @@ pub async fn get_model( /// endpoint responds instantly on repeated dashboard loads even when local /// providers are unreachable (fixes #474). pub async fn list_providers(State(state): State>) -> impl IntoResponse { - let provider_list: Vec = { + let provider_list: Vec = { let catalog = state .kernel .model_catalog @@ -6496,13 +6791,18 @@ pub async fn list_providers(State(state): State>) -> impl IntoResp let probe_futures: Vec<_> = local_providers .iter() .map(|(_, id, url)| { - openfang_runtime::provider_health::probe_provider_cached(id, url, cache) + omtae_runtime::provider_health::probe_provider_cached( + id, + url, + None, + cache, + ) }) .collect(); let probe_results = futures::future::join_all(probe_futures).await; // Index probe results by provider list position for O(1) lookup - let mut probe_map: HashMap = + let mut probe_map: HashMap = HashMap::with_capacity(local_providers.len()); for ((idx, _, _), result) in local_providers.iter().zip(probe_results) { probe_map.insert(*idx, result); @@ -6556,7 +6856,7 @@ pub async fn list_providers(State(state): State>) -> impl IntoResp /// POST /api/models/custom — Add a custom model to the catalog. /// -/// Persists to `~/.openfang/custom_models.json` and makes the model immediately +/// Persists to `~/.omtae/custom_models.json` and makes the model immediately /// available for agent assignment. pub async fn add_custom_model( State(state): State>, @@ -6594,11 +6894,11 @@ pub async fn add_custom_model( .unwrap_or(&id) .to_string(); - let entry = openfang_types::model_catalog::ModelCatalogEntry { + let entry = omtae_types::model_catalog::ModelCatalogEntry { id: id.clone(), display_name: display, provider: provider.clone(), - tier: openfang_types::model_catalog::ModelTier::Custom, + tier: omtae_types::model_catalog::ModelTier::Custom, context_window, max_output_tokens: max_output, input_cost_per_m: body @@ -6692,15 +6992,15 @@ pub async fn a2a_agent_card(State(state): State>) -> impl IntoResp let base_url = format!("http://{}", state.kernel.config.api_listen); if let Some(first) = agents.first() { - let card = openfang_runtime::a2a::build_agent_card(&first.manifest, &base_url); + let card = omtae_runtime::a2a::build_agent_card(&first.manifest, &base_url); ( StatusCode::OK, Json(serde_json::to_value(&card).unwrap_or_default()), ) } else { let card = serde_json::json!({ - "name": "openfang", - "description": "OpenFang Agent OS — no agents spawned yet", + "name": "omtae", + "description": "OMTAE Agent OS — no agents spawned yet", "url": format!("{base_url}/a2a"), "version": "0.1.0", "capabilities": { "streaming": true }, @@ -6720,7 +7020,7 @@ pub async fn a2a_list_agents(State(state): State>) -> impl IntoRes let cards: Vec = agents .iter() .map(|entry| { - let card = openfang_runtime::a2a::build_agent_card(&entry.manifest, &base_url); + let card = omtae_runtime::a2a::build_agent_card(&entry.manifest, &base_url); serde_json::to_value(&card).unwrap_or_default() }) .collect(); @@ -6768,13 +7068,13 @@ pub async fn a2a_send_task( let session_id = request["params"]["sessionId"].as_str().map(String::from); // Create the task in the store as Working - let task = openfang_runtime::a2a::A2aTask { + let task = omtae_runtime::a2a::A2aTask { id: task_id.clone(), session_id: session_id.clone(), - status: openfang_runtime::a2a::A2aTaskStatus::Working.into(), - messages: vec![openfang_runtime::a2a::A2aMessage { + status: omtae_runtime::a2a::A2aTaskStatus::Working.into(), + messages: vec![omtae_runtime::a2a::A2aMessage { role: "user".to_string(), - parts: vec![openfang_runtime::a2a::A2aPart::Text { + parts: vec![omtae_runtime::a2a::A2aPart::Text { text: message_text.clone(), }], }], @@ -6785,9 +7085,9 @@ pub async fn a2a_send_task( // Send message to agent match state.kernel.send_message(agent.id, &message_text).await { Ok(result) => { - let response_msg = openfang_runtime::a2a::A2aMessage { + let response_msg = omtae_runtime::a2a::A2aMessage { role: "agent".to_string(), - parts: vec![openfang_runtime::a2a::A2aPart::Text { + parts: vec![omtae_runtime::a2a::A2aPart::Text { text: result.response, }], }; @@ -6807,9 +7107,9 @@ pub async fn a2a_send_task( } } Err(e) => { - let error_msg = openfang_runtime::a2a::A2aMessage { + let error_msg = omtae_runtime::a2a::A2aMessage { role: "agent".to_string(), - parts: vec![openfang_runtime::a2a::A2aPart::Text { + parts: vec![omtae_runtime::a2a::A2aPart::Text { text: format!("Error: {e}"), }], }; @@ -6908,7 +7208,7 @@ pub async fn a2a_discover_external( } }; - let client = openfang_runtime::a2a::A2aClient::new(); + let client = omtae_runtime::a2a::A2aClient::new(); match client.discover(&url).await { Ok(card) => { let card_json = serde_json::to_value(&card).unwrap_or_default(); @@ -6966,7 +7266,7 @@ pub async fn a2a_send_external( }; let session_id = body["session_id"].as_str(); - let client = openfang_runtime::a2a::A2aClient::new(); + let client = omtae_runtime::a2a::A2aClient::new(); match client.send_task(&url, &message, session_id).await { Ok(task) => ( StatusCode::OK, @@ -6995,7 +7295,7 @@ pub async fn a2a_external_task_status( } }; - let client = openfang_runtime::a2a::A2aClient::new(); + let client = omtae_runtime::a2a::A2aClient::new(); match client.get_task(&url, &task_id).await { Ok(task) => ( StatusCode::OK, @@ -7027,7 +7327,7 @@ pub async fn mcp_http( .read() .unwrap_or_else(|e| e.into_inner()); for skill_tool in registry.all_tool_definitions() { - tools.push(openfang_types::tool::ToolDefinition { + tools.push(omtae_types::tool::ToolDefinition { name: skill_tool.name.clone(), description: skill_tool.description.clone(), input_schema: skill_tool.input_schema.clone(), @@ -7065,9 +7365,9 @@ pub async fn mcp_http( .snapshot(); // Execute the tool via the kernel's tool runner - let kernel_handle: Arc = - state.kernel.clone() as Arc; - let result = openfang_runtime::tool_runner::execute_tool( + let kernel_handle: Arc = + state.kernel.clone() as Arc; + let result = omtae_runtime::tool_runner::execute_tool( "mcp-http", tool_name, &arguments, @@ -7107,7 +7407,7 @@ pub async fn mcp_http( } // For non-tools/call methods (initialize, tools/list, etc.), delegate to the handler - let response = openfang_runtime::mcp_server::handle_mcp_request(&request, &tools).await; + let response = omtae_runtime::mcp_server::handle_mcp_request(&request, &tools).await; Json(response) } @@ -7179,7 +7479,7 @@ pub async fn switch_agent_session( } }; let session_id = match session_id_str.parse::() { - Ok(uuid) => openfang_types::agent::SessionId(uuid), + Ok(uuid) => omtae_types::agent::SessionId(uuid), Err(_) => { return ( StatusCode::BAD_REQUEST, @@ -7333,11 +7633,19 @@ pub async fn stop_agent( match state.kernel.stop_agent_run(agent_id) { Ok(true) => ( StatusCode::OK, - Json(serde_json::json!({"status": "ok", "message": "Run cancelled"})), + Json(serde_json::json!({ + "status": "ok", + "message": "Run cancelled", + "background_paused": state.kernel.background.is_paused(agent_id), + })), ), Ok(false) => ( StatusCode::OK, - Json(serde_json::json!({"status": "ok", "message": "No active run"})), + Json(serde_json::json!({ + "status": "ok", + "message": "No active run", + "background_paused": state.kernel.background.is_paused(agent_id), + })), ), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, @@ -7596,7 +7904,7 @@ pub async fn get_agent_mcp_servers( if let Ok(mcp_tools) = state.kernel.mcp_tools.lock() { let mut seen = std::collections::HashSet::new(); for tool in mcp_tools.iter() { - if let Some(server) = openfang_runtime::mcp::extract_mcp_server(&tool.name) { + if let Some(server) = omtae_runtime::mcp::extract_mcp_server(&tool.name) { if seen.insert(server.to_string()) { available.push(server.to_string()); } @@ -7660,7 +7968,7 @@ pub async fn set_agent_mcp_servers( /// POST /api/providers/{name}/key — Save an API key for a provider. /// -/// SECURITY: Writes to `~/.openfang/secrets.env`, sets env var in process, +/// SECURITY: Writes to `~/.omtae/secrets.env`, sets env var in process, /// and refreshes auth detection. Key is zeroized after use. pub async fn set_provider_key( State(state): State>, @@ -7774,7 +8082,7 @@ pub async fn set_provider_key( // Hot-update the in-memory default model override so resolve_driver() // immediately creates drivers for the new provider — no restart needed. { - let new_dm = openfang_types::config::DefaultModelConfig { + let new_dm = omtae_types::config::DefaultModelConfig { provider: name.clone(), model: model_id, api_key_env: env_var.clone(), @@ -7816,7 +8124,7 @@ pub async fn set_provider_key( let base = guard .clone() .unwrap_or_else(|| state.kernel.config.default_model.clone()); - *guard = Some(openfang_types::config::DefaultModelConfig { + *guard = Some(omtae_types::config::DefaultModelConfig { api_key_env: env_var.clone(), ..base }); @@ -7907,10 +8215,8 @@ pub async fn test_provider( .unwrap_or_else(|e| e.into_inner()); match catalog.get_provider(&name) { Some(p) => { - // Find a default model for this provider to use in the test request - let model_id = catalog - .default_model_for_provider(&name) - .unwrap_or_default(); + let model_id = + model_for_provider_test(&state.kernel, &catalog, &name); ( p.api_key_env.clone(), p.base_url.clone(), @@ -7938,7 +8244,7 @@ pub async fn test_provider( // Attempt a lightweight connectivity test let start = std::time::Instant::now(); - let driver_config = openfang_runtime::llm_driver::DriverConfig { + let driver_config = omtae_runtime::llm_driver::DriverConfig { provider: name.clone(), api_key, base_url: if base_url.is_empty() { @@ -7950,12 +8256,12 @@ pub async fn test_provider( subprocess_timeout_secs: None, }; - match openfang_runtime::drivers::create_driver(&driver_config) { + match omtae_runtime::drivers::create_driver(&driver_config) { Ok(driver) => { // Send a minimal completion request to test connectivity - let test_req = openfang_runtime::llm_driver::CompletionRequest { + let test_req = omtae_runtime::llm_driver::CompletionRequest { model: default_model.clone(), - messages: vec![openfang_types::message::Message::user("Hi")], + messages: vec![omtae_types::message::Message::user("Hi")], tools: vec![], max_tokens: 1, temperature: 0.0, @@ -8040,7 +8346,20 @@ pub async fn set_provider_url( } // Probe reachability at the new URL - let probe = openfang_runtime::provider_health::probe_provider(&name, &base_url).await; + let api_key = { + let catalog = state + .kernel + .model_catalog + .read() + .unwrap_or_else(|e| e.into_inner()); + provider_api_key_from_env(&catalog, &name) + }; + let probe = omtae_runtime::provider_health::probe_provider( + &name, + &base_url, + api_key.as_deref(), + ) + .await; // Merge discovered models into catalog if !probe.discovered_models.is_empty() { @@ -8146,7 +8465,7 @@ pub async fn create_skill( ); } - // Write skill.toml to ~/.openfang/skills/{name}/ + // Write skill.toml to ~/.omtae/skills/{name}/ let skill_dir = state.kernel.config.home_dir.join("skills").join(&name); if skill_dir.exists() { return ( @@ -8215,7 +8534,7 @@ pub async fn create_skill( fn load_skill_declared_config( skills_dir: &std::path::Path, skill_name: &str, -) -> Option> { +) -> Option> { // 1. User-installed skill: skills_dir//skill.toml, falling back to // SKILL.md frontmatter if no TOML has been generated yet. let skill_dir = skills_dir.join(skill_name); @@ -8223,7 +8542,7 @@ fn load_skill_declared_config( let toml_path = skill_dir.join("skill.toml"); if toml_path.exists() { if let Ok(text) = std::fs::read_to_string(&toml_path) { - if let Ok(manifest) = toml::from_str::(&text) { + if let Ok(manifest) = toml::from_str::(&text) { if manifest.skill.name == skill_name { return Some(manifest.config); } @@ -8234,7 +8553,7 @@ fn load_skill_declared_config( if skillmd_path.exists() { if let Ok(text) = std::fs::read_to_string(&skillmd_path) { if let Ok(converted) = - openfang_skills::openclaw_compat::convert_skillmd_str(skill_name, &text) + omtae_skills::openclaw_compat::convert_skillmd_str(skill_name, &text) { return Some(converted.config_vars); } @@ -8243,10 +8562,10 @@ fn load_skill_declared_config( } // 2. Bundled skill: look up by name in the compile-time catalog. - for (bundled_name, content) in openfang_skills::bundled::bundled_skills() { + for (bundled_name, content) in omtae_skills::bundled::bundled_skills() { if bundled_name == skill_name { if let Ok(converted) = - openfang_skills::openclaw_compat::convert_skillmd_str(bundled_name, content) + omtae_skills::openclaw_compat::convert_skillmd_str(bundled_name, content) { return Some(converted.config_vars); } @@ -8260,12 +8579,12 @@ fn load_skill_declared_config( /// /// Returns `None` if the skill is not installed. Resolved values are /// redacted when the variable name looks secret (see -/// [`openfang_skills::config_injection::is_secret_name`]). +/// [`omtae_skills::config_injection::is_secret_name`]). fn build_skill_config_snapshot( state: &Arc, skill_name: &str, ) -> Option { - use openfang_skills::config_injection::is_secret_name; + use omtae_skills::config_injection::is_secret_name; let skills_dir = state.kernel.config.home_dir.join("skills"); let declared = load_skill_declared_config(&skills_dir, skill_name)?; @@ -8870,8 +9189,8 @@ pub async fn list_integrations(State(state): State>) -> impl IntoR let status = match &info.installed { Some(inst) if !inst.enabled => "disabled", Some(_) => match h.as_ref().map(|h| &h.status) { - Some(openfang_extensions::IntegrationStatus::Ready) => "ready", - Some(openfang_extensions::IntegrationStatus::Error(_)) => "error", + Some(omtae_extensions::IntegrationStatus::Ready) => "ready", + Some(omtae_extensions::IntegrationStatus::Error(_)) => "error", _ => "installed", }, None => continue, // Only show installed @@ -8966,7 +9285,7 @@ pub async fn add_integration( format!("Unknown integration: '{}'", id), )) } else { - let entry = openfang_extensions::InstalledIntegration { + let entry = omtae_extensions::InstalledIntegration { id: id.clone(), installed_at: chrono::Utc::now(), enabled: true, @@ -9125,7 +9444,7 @@ pub async fn reload_integrations(State(state): State>) -> impl Int // --------------------------------------------------------------------------- // // Historical note: an earlier implementation of `/api/schedules*` wrote to a -// shared-memory key (`__openfang_schedules`) that no executor ever read — so +// shared-memory key (`__omtae_schedules`) that no executor ever read — so // scheduled jobs registered via this API never actually fired (#1069). These // routes now delegate to the kernel's real cron scheduler, which already // backs `/api/cron/jobs*`. The request/response shape has been preserved as @@ -9134,10 +9453,10 @@ pub async fn reload_integrations(State(state): State>) -> impl Int /// Convert an internal `CronJob` into the legacy `/api/schedules` response /// shape so existing dashboard code keeps working. fn cron_job_to_schedule_view( - kernel: &OpenFangKernel, - job: &openfang_types::scheduler::CronJob, + kernel: &OMTAEKernel, + job: &omtae_types::scheduler::CronJob, ) -> serde_json::Value { - use openfang_types::scheduler::{CronAction, CronSchedule}; + use omtae_types::scheduler::{CronAction, CronSchedule}; let cron = match &job.schedule { CronSchedule::Cron { expr, .. } => expr.clone(), @@ -9274,7 +9593,7 @@ pub async fn create_schedule( if let Some(arr) = delivery_targets_raw.as_array() { for (idx, t) in arr.iter().enumerate() { if let Err(e) = - serde_json::from_value::(t.clone()) + serde_json::from_value::(t.clone()) { return ( StatusCode::BAD_REQUEST, @@ -9314,14 +9633,14 @@ pub async fn create_schedule( .unwrap_or_default(); if !enabled { if let Ok(uuid) = uuid::Uuid::parse_str(&job_id) { - let cj_id = openfang_types::scheduler::CronJobId(uuid); + let cj_id = omtae_types::scheduler::CronJobId(uuid); let _ = state.kernel.cron_scheduler.set_enabled(cj_id, false); let _ = state.kernel.cron_scheduler.persist(); } } // Build response in the legacy shape. let body = if let Ok(uuid) = uuid::Uuid::parse_str(&job_id) { - let cj_id = openfang_types::scheduler::CronJobId(uuid); + let cj_id = omtae_types::scheduler::CronJobId(uuid); match state.kernel.cron_scheduler.get_job(cj_id) { Some(job) => cron_job_to_schedule_view(&state.kernel, &job), None => serde_json::json!({ @@ -9371,7 +9690,7 @@ pub async fn update_schedule( ); } }; - let cj_id = openfang_types::scheduler::CronJobId(uuid); + let cj_id = omtae_types::scheduler::CronJobId(uuid); if state.kernel.cron_scheduler.get_job(cj_id).is_none() { return ( @@ -9402,10 +9721,10 @@ pub async fn update_schedule( ); } let arr = raw_targets.as_array().unwrap(); - let mut parsed: Vec = + let mut parsed: Vec = Vec::with_capacity(arr.len()); for (idx, t) in arr.iter().enumerate() { - match serde_json::from_value::(t.clone()) + match serde_json::from_value::(t.clone()) { Ok(dt) => parsed.push(dt), Err(e) => { @@ -9464,7 +9783,7 @@ pub async fn delete_schedule( ); } }; - let cj_id = openfang_types::scheduler::CronJobId(uuid); + let cj_id = omtae_types::scheduler::CronJobId(uuid); match state.kernel.cron_scheduler.remove_job(cj_id) { Ok(_) => { let _ = state.kernel.cron_scheduler.persist(); @@ -9494,16 +9813,16 @@ pub async fn run_schedule( ); } }; - let cj_id = openfang_types::scheduler::CronJobId(uuid); + let cj_id = omtae_types::scheduler::CronJobId(uuid); let job = match state.kernel.cron_scheduler.try_claim_for_run(cj_id) { Ok(j) => j, - Err(openfang_kernel::cron::ClaimError::NotFound) => { + Err(omtae_kernel::cron::ClaimError::NotFound) => { return ( StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "Schedule not found"})), ); } - Err(openfang_kernel::cron::ClaimError::Disabled) => { + Err(omtae_kernel::cron::ClaimError::Disabled) => { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({"error": "Schedule is disabled"})), @@ -9553,7 +9872,7 @@ pub async fn schedule_delivery_log( ); } }; - let cj_id = openfang_types::scheduler::CronJobId(uuid); + let cj_id = omtae_types::scheduler::CronJobId(uuid); let job = match state.kernel.cron_scheduler.get_job(cj_id) { Some(j) => j, None => { @@ -9705,7 +10024,7 @@ pub struct PatchAgentConfigRequest { pub provider: Option, pub api_key_env: Option, pub base_url: Option, - pub fallback_models: Option>, + pub fallback_models: Option>, } /// PATCH /api/agents/{id}/config — Hot-update agent name, description, system prompt, and identity. @@ -10645,7 +10964,7 @@ pub async fn upload_file( // Generate file ID and save let file_id = uuid::Uuid::new_v4().to_string(); - let upload_dir = std::env::temp_dir().join("openfang_uploads"); + let upload_dir = std::env::temp_dir().join("omtae_uploads"); if let Err(e) = std::fs::create_dir_all(&upload_dir) { tracing::warn!("Failed to create upload dir: {e}"); return ( @@ -10674,10 +10993,10 @@ pub async fn upload_file( // Auto-transcribe audio uploads using the media engine let transcription = if content_type.starts_with("audio/") { - let attachment = openfang_types::media::MediaAttachment { - media_type: openfang_types::media::MediaType::Audio, + let attachment = omtae_types::media::MediaAttachment { + media_type: omtae_types::media::MediaType::Audio, mime_type: content_type.clone(), - source: openfang_types::media::MediaSource::FilePath { + source: omtae_types::media::MediaSource::FilePath { path: file_path.to_string_lossy().to_string(), }, size_bytes: size as u64, @@ -10727,7 +11046,7 @@ pub async fn serve_upload(Path(file_id): Path) -> impl IntoResponse { ); } - let file_path = std::env::temp_dir().join("openfang_uploads").join(&file_id); + let file_path = std::env::temp_dir().join("omtae_uploads").join(&file_id); // Look up metadata from registry; fall back to disk probe for generated images // (image_generate saves files without registering in UPLOAD_REGISTRY). @@ -10813,9 +11132,9 @@ pub async fn list_approvals(State(state): State>) -> impl IntoResp let request = record.request; let agent_name = agent_name_for(&request.agent_id); let status = match record.decision { - openfang_types::approval::ApprovalDecision::Approved => "approved", - openfang_types::approval::ApprovalDecision::Denied => "rejected", - openfang_types::approval::ApprovalDecision::TimedOut => "expired", + omtae_types::approval::ApprovalDecision::Approved => "approved", + omtae_types::approval::ApprovalDecision::Denied => "rejected", + omtae_types::approval::ApprovalDecision::TimedOut => "expired", }; serde_json::json!({ "id": request.id, @@ -10867,7 +11186,7 @@ pub async fn create_approval( State(state): State>, Json(req): Json, ) -> impl IntoResponse { - use openfang_types::approval::{ApprovalRequest, RiskLevel}; + use omtae_types::approval::{ApprovalRequest, RiskLevel}; let policy = state.kernel.approval_manager.policy(); let id = uuid::Uuid::new_v4(); @@ -10919,7 +11238,7 @@ pub async fn approve_request( match state.kernel.approval_manager.resolve( uuid, - openfang_types::approval::ApprovalDecision::Approved, + omtae_types::approval::ApprovalDecision::Approved, Some("api".to_string()), ) { Ok(resp) => ( @@ -10949,7 +11268,7 @@ pub async fn reject_request( match state.kernel.approval_manager.resolve( uuid, - openfang_types::approval::ApprovalDecision::Denied, + omtae_types::approval::ApprovalDecision::Denied, Some("api".to_string()), ) { Ok(resp) => ( @@ -10975,7 +11294,7 @@ pub async fn config_reload(State(state): State>) -> impl IntoRespo // SECURITY: Record config reload in audit trail state.kernel.audit_log.record( "system", - openfang_runtime::audit::AuditAction::ConfigChange, + omtae_runtime::audit::AuditAction::ConfigChange, "config reload requested via API", "pending", ); @@ -11230,7 +11549,7 @@ pub async fn config_set( state.kernel.audit_log.record( "system", - openfang_runtime::audit::AuditAction::ConfigChange, + omtae_runtime::audit::AuditAction::ConfigChange, format!("config set: {path}"), "completed", ); @@ -11365,7 +11684,7 @@ pub async fn delete_cron_job( ) -> impl IntoResponse { match uuid::Uuid::parse_str(&id) { Ok(uuid) => { - let job_id = openfang_types::scheduler::CronJobId(uuid); + let job_id = omtae_types::scheduler::CronJobId(uuid); match state.kernel.cron_scheduler.remove_job(job_id) { Ok(_) => { let _ = state.kernel.cron_scheduler.persist(); @@ -11396,7 +11715,7 @@ pub async fn toggle_cron_job( let enabled = body["enabled"].as_bool().unwrap_or(true); match uuid::Uuid::parse_str(&id) { Ok(uuid) => { - let job_id = openfang_types::scheduler::CronJobId(uuid); + let job_id = omtae_types::scheduler::CronJobId(uuid); match state.kernel.cron_scheduler.set_enabled(job_id, enabled) { Ok(()) => { let _ = state.kernel.cron_scheduler.persist(); @@ -11425,7 +11744,7 @@ pub async fn cron_job_status( ) -> impl IntoResponse { match uuid::Uuid::parse_str(&id) { Ok(uuid) => { - let job_id = openfang_types::scheduler::CronJobId(uuid); + let job_id = omtae_types::scheduler::CronJobId(uuid); match state.kernel.cron_scheduler.get_meta(job_id) { Some(meta) => ( StatusCode::OK, @@ -11466,18 +11785,18 @@ pub async fn run_cron_job( ); } }; - let job_id = openfang_types::scheduler::CronJobId(uuid); + let job_id = omtae_types::scheduler::CronJobId(uuid); // Atomically check existence + enabled + reserve next_run in one lock hold. let job = match state.kernel.cron_scheduler.try_claim_for_run(job_id) { Ok(j) => j, - Err(openfang_kernel::cron::ClaimError::NotFound) => { + Err(omtae_kernel::cron::ClaimError::NotFound) => { return ( StatusCode::NOT_FOUND, Json(serde_json::json!({"status": "error", "error": "Job not found"})), ); } - Err(openfang_kernel::cron::ClaimError::Disabled) => { + Err(omtae_kernel::cron::ClaimError::Disabled) => { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({"status": "error", "error": "Job is disabled"})), @@ -11515,7 +11834,7 @@ pub async fn run_cron_job( pub async fn webhook_wake( State(state): State>, headers: axum::http::HeaderMap, - Json(body): Json, + Json(body): Json, ) -> impl IntoResponse { // Check if webhook triggers are enabled let wh_config = match &state.kernel.config.webhook_triggers { @@ -11574,7 +11893,7 @@ pub async fn webhook_wake( pub async fn webhook_agent( State(state): State>, headers: axum::http::HeaderMap, - Json(body): Json, + Json(body): Json, ) -> impl IntoResponse { // Check if webhook triggers are enabled let wh_config = match &state.kernel.config.webhook_triggers { @@ -11671,7 +11990,7 @@ pub async fn list_bindings(State(state): State>) -> impl IntoRespo /// POST /api/bindings — Add a new agent binding. pub async fn add_binding( State(state): State>, - Json(binding): Json, + Json(binding): Json, ) -> impl IntoResponse { // Validate agent exists let agents = state.kernel.registry.list(); @@ -11718,7 +12037,7 @@ pub async fn pairing_request(State(state): State>) -> impl IntoRes } match state.kernel.pairing.create_pairing_request() { Ok(req) => { - let qr_uri = format!("openfang://pair?token={}", req.token); + let qr_uri = format!("omtae://pair?token={}", req.token); Json(serde_json::json!({ "token": req.token, "qr_uri": qr_uri, @@ -11759,7 +12078,7 @@ pub async fn pairing_complete( .get("push_token") .and_then(|v| v.as_str()) .map(String::from); - let device_info = openfang_kernel::pairing::PairedDevice { + let device_info = omtae_kernel::pairing::PairedDevice { device_id: uuid::Uuid::new_v4().to_string(), display_name: display_name.to_string(), platform: platform.to_string(), @@ -11843,7 +12162,7 @@ pub async fn pairing_notify( let title = body .get("title") .and_then(|v| v.as_str()) - .unwrap_or("OpenFang"); + .unwrap_or("OMTAE"); let message = body.get("message").and_then(|v| v.as_str()).unwrap_or(""); if message.is_empty() { return ( @@ -11882,7 +12201,7 @@ pub async fn pairing_notify( /// /// Unknown surface values return 400. pub async fn list_commands(Query(params): Query) -> impl IntoResponse { - use openfang_types::commands::{self, CommandCategory, Surfaces}; + use omtae_types::commands::{self, CommandCategory, Surfaces}; let surface_raw = params.surface.as_deref().unwrap_or("web"); let surface = match surface_raw.to_ascii_lowercase().as_str() { @@ -11979,7 +12298,7 @@ pub async fn copilot_oauth_start() -> impl IntoResponse { // Clean up expired flows first COPILOT_FLOWS.retain(|_, state| state.expires_at > Instant::now()); - match openfang_runtime::copilot_oauth::start_device_flow().await { + match omtae_runtime::copilot_oauth::start_device_flow().await { Ok(resp) => { let poll_id = uuid::Uuid::new_v4().to_string(); @@ -12041,12 +12360,12 @@ pub async fn copilot_oauth_poll( let device_code = flow.device_code.clone(); drop(flow); - match openfang_runtime::copilot_oauth::poll_device_flow(&device_code).await { - openfang_runtime::copilot_oauth::DeviceFlowStatus::Pending => ( + match omtae_runtime::copilot_oauth::poll_device_flow(&device_code).await { + omtae_runtime::copilot_oauth::DeviceFlowStatus::Pending => ( StatusCode::OK, Json(serde_json::json!({"status": "pending"})), ), - openfang_runtime::copilot_oauth::DeviceFlowStatus::Complete { access_token } => { + omtae_runtime::copilot_oauth::DeviceFlowStatus::Complete { access_token } => { // Store in vault (best-effort) state.kernel.store_credential("GITHUB_TOKEN", &access_token); @@ -12080,7 +12399,7 @@ pub async fn copilot_oauth_poll( Json(serde_json::json!({"status": "complete"})), ) } - openfang_runtime::copilot_oauth::DeviceFlowStatus::SlowDown { new_interval } => { + omtae_runtime::copilot_oauth::DeviceFlowStatus::SlowDown { new_interval } => { // Update interval if let Some(mut f) = COPILOT_FLOWS.get_mut(&poll_id) { f.interval = new_interval; @@ -12090,21 +12409,21 @@ pub async fn copilot_oauth_poll( Json(serde_json::json!({"status": "pending", "interval": new_interval})), ) } - openfang_runtime::copilot_oauth::DeviceFlowStatus::Expired => { + omtae_runtime::copilot_oauth::DeviceFlowStatus::Expired => { COPILOT_FLOWS.remove(&poll_id); ( StatusCode::OK, Json(serde_json::json!({"status": "expired"})), ) } - openfang_runtime::copilot_oauth::DeviceFlowStatus::AccessDenied => { + omtae_runtime::copilot_oauth::DeviceFlowStatus::AccessDenied => { COPILOT_FLOWS.remove(&poll_id); ( StatusCode::OK, Json(serde_json::json!({"status": "denied"})), ) } - openfang_runtime::copilot_oauth::DeviceFlowStatus::Error(e) => ( + omtae_runtime::copilot_oauth::DeviceFlowStatus::Error(e) => ( StatusCode::OK, Json(serde_json::json!({"status": "error", "error": e})), ), @@ -12117,7 +12436,7 @@ pub async fn copilot_oauth_poll( /// GET /api/comms/topology — Build agent topology graph from registry. pub async fn comms_topology(State(state): State>) -> impl IntoResponse { - use openfang_types::comms::{EdgeKind, TopoEdge, TopoNode, Topology}; + use omtae_types::comms::{EdgeKind, TopoEdge, TopoNode, Topology}; let agents = state.kernel.registry.list(); @@ -12148,8 +12467,8 @@ pub async fn comms_topology(State(state): State>) -> impl IntoResp let events = state.kernel.event_bus.history(500).await; let mut peer_pairs = std::collections::HashSet::new(); for event in &events { - if let openfang_types::event::EventPayload::Message(_) = &event.payload { - if let openfang_types::event::EventTarget::Agent(target_id) = &event.target { + if let omtae_types::event::EventPayload::Message(_) = &event.payload { + if let omtae_types::event::EventTarget::Agent(target_id) = &event.target { let from = event.source.to_string(); let to = target_id.to_string(); // Deduplicate: only one edge per pair, skip self-loops @@ -12176,11 +12495,11 @@ pub async fn comms_topology(State(state): State>) -> impl IntoResp /// Filter a kernel event into a CommsEvent, if it represents inter-agent communication. fn filter_to_comms_event( - event: &openfang_types::event::Event, - agents: &[openfang_types::agent::AgentEntry], -) -> Option { - use openfang_types::comms::{CommsEvent, CommsEventKind}; - use openfang_types::event::{EventPayload, EventTarget, LifecycleEvent}; + event: &omtae_types::event::Event, + agents: &[omtae_types::agent::AgentEntry], +) -> Option { + use omtae_types::comms::{CommsEvent, CommsEventKind}; + use omtae_types::event::{EventPayload, EventTarget, LifecycleEvent}; let resolve_name = |id: &str| -> String { agents @@ -12204,7 +12523,7 @@ fn filter_to_comms_event( source_name: resolve_name(&event.source.to_string()), target_id: target_id.clone(), target_name: resolve_name(&target_id), - detail: openfang_types::truncate_str(&msg.content, 200).to_string(), + detail: omtae_types::truncate_str(&msg.content, 200).to_string(), }) } EventPayload::Lifecycle(lifecycle) => match lifecycle { @@ -12236,10 +12555,10 @@ fn filter_to_comms_event( /// Convert an audit entry into a CommsEvent if it represents inter-agent activity. fn audit_to_comms_event( - entry: &openfang_runtime::audit::AuditEntry, - agents: &[openfang_types::agent::AgentEntry], -) -> Option { - use openfang_types::comms::{CommsEvent, CommsEventKind}; + entry: &omtae_runtime::audit::AuditEntry, + agents: &[omtae_types::agent::AgentEntry], +) -> Option { + use omtae_types::comms::{CommsEvent, CommsEventKind}; let resolve_name = |id: &str| -> String { agents @@ -12250,7 +12569,7 @@ fn audit_to_comms_event( if id.is_empty() || id == "system" { "system".to_string() } else { - openfang_types::truncate_str(id, 12).to_string() + omtae_types::truncate_str(id, 12).to_string() } }) }; @@ -12276,17 +12595,17 @@ fn audit_to_comms_event( "{} in / {} out — {}", in_tok, out_tok, - openfang_types::truncate_str(&entry.outcome, 80) + omtae_types::truncate_str(&entry.outcome, 80) ) } } else if entry.outcome != "ok" { format!( "{} — {}", - openfang_types::truncate_str(&entry.detail, 80), - openfang_types::truncate_str(&entry.outcome, 80) + omtae_types::truncate_str(&entry.detail, 80), + omtae_types::truncate_str(&entry.outcome, 80) ) } else { - openfang_types::truncate_str(&entry.detail, 200).to_string() + omtae_types::truncate_str(&entry.detail, 200).to_string() }; (CommsEventKind::AgentMessage, detail, "user") } @@ -12294,7 +12613,7 @@ fn audit_to_comms_event( CommsEventKind::AgentSpawned, format!( "Agent spawned: {}", - openfang_types::truncate_str(&entry.detail, 100) + omtae_types::truncate_str(&entry.detail, 100) ), "", ), @@ -12302,7 +12621,7 @@ fn audit_to_comms_event( CommsEventKind::AgentTerminated, format!( "Agent killed: {}", - openfang_types::truncate_str(&entry.detail, 100) + omtae_types::truncate_str(&entry.detail, 100) ), "", ), @@ -12347,7 +12666,7 @@ pub async fn comms_events( // Primary source: event bus (has full source/target context) let bus_events = state.kernel.event_bus.history(500).await; - let mut comms_events: Vec = bus_events + let mut comms_events: Vec = bus_events .iter() .filter_map(|e| filter_to_comms_event(e, &agents)) .collect(); @@ -12425,10 +12744,10 @@ pub async fn comms_events_stream(State(state): State>) -> axum::re /// POST /api/comms/send — Send a message from one agent to another. pub async fn comms_send( State(state): State>, - Json(req): Json, + Json(req): Json, ) -> impl IntoResponse { // Validate from agent exists - let from_id: openfang_types::agent::AgentId = match req.from_agent_id.parse() { + let from_id: omtae_types::agent::AgentId = match req.from_agent_id.parse() { Ok(id) => id, Err(_) => { return ( @@ -12445,7 +12764,7 @@ pub async fn comms_send( } // Validate to agent exists - let to_id: openfang_types::agent::AgentId = match req.to_agent_id.parse() { + let to_id: omtae_types::agent::AgentId = match req.to_agent_id.parse() { Ok(id) => id, Err(_) => { return ( @@ -12489,7 +12808,7 @@ pub async fn comms_send( /// POST /api/comms/task — Post a task to the agent task queue. pub async fn comms_task( State(state): State>, - Json(req): Json, + Json(req): Json, ) -> impl IntoResponse { if req.title.is_empty() { return ( @@ -12528,12 +12847,68 @@ pub async fn comms_task( /// POST /api/auth/login — Authenticate with username/password, returns session token. pub async fn auth_login( State(state): State>, + headers: axum::http::HeaderMap, Json(req): Json, ) -> axum::response::Response { use axum::body::Body; use axum::response::Response; - let auth_cfg = &state.kernel.config.auth; + let cfg = &state.kernel.config; + + // PIN login (`[dashboard]` in config.toml) + if cfg.dashboard.pin_auth_active() { + let pin = req.get("pin").and_then(|v| v.as_str()).unwrap_or(""); + if !cfg.verify_dashboard_pin(pin) { + state.kernel.audit_log.record( + "system", + omtae_runtime::audit::AuditAction::AuthAttempt, + "dashboard PIN login failed", + "invalid PIN".to_string(), + ); + return Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header("content-type", "application/json") + .body(Body::from( + serde_json::json!({"error": "Invalid PIN"}).to_string(), + )) + .unwrap(); + } + + let secret = omtae_types::config::KernelConfig::pin_session_secret(&cfg.dashboard.pin); + let ttl_hours = if cfg.auth.session_ttl_hours > 0 { + cfg.auth.session_ttl_hours + } else { + 168 + }; + let token = crate::session_auth::create_session_token("dashboard", &secret, ttl_hours); + let ttl_secs = ttl_hours * 3600; + let secure = crate::session_auth::cookie_should_be_secure(&headers); + let cookie = crate::session_auth::format_session_cookie(&token, ttl_secs, secure); + + state.kernel.audit_log.record( + "system", + omtae_runtime::audit::AuditAction::AuthAttempt, + "dashboard PIN login success", + String::new(), + ); + + return Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/json") + .header("set-cookie", &cookie) + .body(Body::from( + serde_json::json!({ + "status": "ok", + "token": token, + "username": "dashboard", + "mode": "pin", + }) + .to_string(), + )) + .unwrap(); + } + + let auth_cfg = &cfg.auth; if !auth_cfg.enabled { return Response::builder() .status(StatusCode::NOT_FOUND) @@ -12563,7 +12938,7 @@ pub async fn auth_login( // Audit log the failed attempt state.kernel.audit_log.record( "system", - openfang_runtime::audit::AuditAction::AuthAttempt, + omtae_runtime::audit::AuditAction::AuthAttempt, "dashboard login failed", format!("username: {username}"), ); @@ -12576,23 +12951,17 @@ pub async fn auth_login( .unwrap(); } - // Derive the session secret the same way as server.rs - let api_key = state.kernel.config.api_key.trim().to_string(); - let secret = if !api_key.is_empty() { - api_key - } else { - auth_cfg.password_hash.clone() - }; + let secret = state.kernel.config.dashboard_session_secret(); let token = crate::session_auth::create_session_token(username, &secret, auth_cfg.session_ttl_hours); let ttl_secs = auth_cfg.session_ttl_hours * 3600; - let cookie = - format!("openfang_session={token}; Path=/; HttpOnly; SameSite=Strict; Max-Age={ttl_secs}"); + let secure = crate::session_auth::cookie_should_be_secure(&headers); + let cookie = crate::session_auth::format_session_cookie(&token, ttl_secs, secure); state.kernel.audit_log.record( "system", - openfang_runtime::audit::AuditAction::AuthAttempt, + omtae_runtime::audit::AuditAction::AuthAttempt, "dashboard login success", format!("username: {username}"), ); @@ -12613,13 +12982,17 @@ pub async fn auth_login( } /// POST /api/auth/logout — Clear the session cookie. -pub async fn auth_logout() -> impl IntoResponse { - let cookie = "openfang_session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0"; - ( - StatusCode::OK, - [("content-type", "application/json"), ("set-cookie", cookie)], - serde_json::json!({"status": "ok"}).to_string(), - ) +pub async fn auth_logout(headers: axum::http::HeaderMap) -> impl IntoResponse { + let secure = crate::session_auth::cookie_should_be_secure(&headers); + let cookie = crate::session_auth::format_session_cookie_clear(secure); + axum::http::Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/json") + .header("set-cookie", &cookie) + .body(axum::body::Body::from( + serde_json::json!({"status": "ok"}).to_string(), + )) + .unwrap() } /// GET /api/auth/check — Check current authentication state. @@ -12627,36 +13000,54 @@ pub async fn auth_check( State(state): State>, request: axum::http::Request, ) -> impl IntoResponse { - let auth_cfg = &state.kernel.config.auth; - if !auth_cfg.enabled { + let cfg = &state.kernel.config; + if !cfg.dashboard_auth_enabled() { return Json(serde_json::json!({ "authenticated": true, "mode": "none", })); } - // Derive the session secret the same way as server.rs - let api_key = state.kernel.config.api_key.trim().to_string(); - let secret = if !api_key.is_empty() { - api_key + let secret = cfg.dashboard_session_secret(); + let mode = if cfg.dashboard.pin_auth_active() { + "pin" } else { - auth_cfg.password_hash.clone() + "session" }; - // Check session cookie + // Check session cookie, PIN header, or Bearer session token (mobile fallbacks). let session_user = crate::session_auth::extract_session_cookie(request.headers()) - .and_then(|token| crate::session_auth::verify_session_token(&token, &secret)); + .and_then(|token| crate::session_auth::verify_session_token(&token, &secret)) + .or_else(|| { + request + .headers() + .get("authorization") + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.strip_prefix("Bearer ")) + .and_then(|token| crate::session_auth::verify_session_token(token, &secret)) + }); - if let Some(username) = session_user { + let pin_user = if session_user.is_none() && cfg.dashboard.pin_auth_active() { + request + .headers() + .get("x-omtae-pin") + .and_then(|v| v.to_str().ok()) + .filter(|pin| cfg.verify_dashboard_pin(pin)) + .map(|_| "dashboard".to_string()) + } else { + None + }; + + if let Some(username) = session_user.or(pin_user) { Json(serde_json::json!({ "authenticated": true, - "mode": "session", + "mode": mode, "username": username, })) } else { Json(serde_json::json!({ "authenticated": false, - "mode": "session", + "mode": mode, })) } } @@ -12695,14 +13086,14 @@ mod channel_config_tests { #[test] fn test_is_channel_configured_wecom_none() { - let config = openfang_types::config::ChannelsConfig::default(); + let config = omtae_types::config::ChannelsConfig::default(); assert!(!is_channel_configured(&config, "wecom")); } #[test] fn test_is_channel_configured_wecom_some() { - let mut config = openfang_types::config::ChannelsConfig::default(); - config.wecom = Some(openfang_types::config::WeComConfig { + let mut config = omtae_types::config::ChannelsConfig::default(); + config.wecom = Some(omtae_types::config::WeComConfig { corp_id: "test_corp".to_string(), agent_id: "test_agent".to_string(), secret_env: "WECOM_SECRET".to_string(), @@ -12710,7 +13101,7 @@ mod channel_config_tests { token: Some("token".to_string()), encoding_aes_key: Some("aes_key".to_string()), default_agent: Some("assistant".to_string()), - overrides: openfang_types::config::ChannelOverrides::default(), + overrides: omtae_types::config::ChannelOverrides::default(), }); assert!(is_channel_configured(&config, "wecom")); } diff --git a/crates/openfang-api/src/server.rs b/crates/openfang-api/src/server.rs index a1a2bc9c06..ecbcea50dc 100644 --- a/crates/openfang-api/src/server.rs +++ b/crates/openfang-api/src/server.rs @@ -1,4 +1,4 @@ -//! OpenFang daemon server — boots the kernel and serves the HTTP API. +//! OMTAE daemon server — boots the kernel and serves the HTTP API. use crate::channel_bridge; use crate::middleware; @@ -7,7 +7,7 @@ use crate::routes::{self, AppState}; use crate::webchat; use crate::ws; use axum::Router; -use openfang_kernel::OpenFangKernel; +use omtae_kernel::OMTAEKernel; use std::net::SocketAddr; use std::path::Path; use std::sync::Arc; @@ -17,7 +17,7 @@ use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; use tracing::info; -/// Daemon info written to `~/.openfang/daemon.json` so the CLI can find us. +/// Daemon info written to `~/.omtae/daemon.json` so the CLI can find us. #[derive(serde::Serialize, serde::Deserialize)] pub struct DaemonInfo { pub pid: u32, @@ -29,13 +29,13 @@ pub struct DaemonInfo { /// Build the full API router with all routes, middleware, and state. /// -/// This is extracted from `run_daemon()` so that embedders (e.g. openfang-desktop) +/// This is extracted from `run_daemon()` so that embedders (e.g. omtae-desktop) /// can create the router without starting the full daemon lifecycle. /// /// Returns `(router, shared_state)`. The caller can use `state.bridge_manager` /// to shut down the bridge on exit. pub async fn build_router( - kernel: Arc, + kernel: Arc, listen_addr: SocketAddr, ) -> (Router<()>, Arc) { // Start channel bridges (Telegram, etc.) @@ -50,7 +50,7 @@ pub async fn build_router( channels_config: tokio::sync::RwLock::new(channels_config), shutdown_notify: Arc::new(tokio::sync::Notify::new()), clawhub_cache: dashmap::DashMap::new(), - provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), + provider_probe_cache: omtae_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(kernel.config.budget.clone())), }); @@ -113,21 +113,36 @@ pub async fn build_router( if state.kernel.config.auth.enabled && !ph.is_empty() && !ph.starts_with("$argon2") { tracing::warn!( "Dashboard auth password_hash is not in Argon2id format. \ - Login will fail. Regenerate with: openfang auth hash-password" + Login will fail. Regenerate with: omtae auth hash-password" ); } // Trim whitespace so `api_key = ""` or `api_key = " "` both disable auth. - let api_key = state.kernel.config.api_key.trim().to_string(); + let pin_active = state.kernel.config.dashboard.pin_auth_active(); + let api_key = state.kernel.config.effective_api_key_for_auth(); + let auth_enabled = state.kernel.config.dashboard_auth_enabled(); + let session_secret = state.kernel.config.dashboard_session_secret(); let allow_no_auth = std::env::var("OPENFANG_ALLOW_NO_AUTH") .map(|v| matches!(v.trim(), "1" | "true" | "TRUE" | "yes" | "on")) .unwrap_or(false); + if pin_active { + tracing::info!( + "Dashboard PIN auth enabled — Bearer api_key disabled for HTTP/WS. \ + Personal tunnel use only; see OMTAE-TUNNEL.md." + ); + if state.kernel.config.dashboard.pin.trim() == "123456" { + tracing::warn!( + "Dashboard PIN is still the default (123456). Change [dashboard].pin in config.toml." + ); + } + } + // Fail-closed warning: if no api_key and no dashboard auth, and the // server is bound to a non-loopback address without an explicit opt-in, // shout about it. The middleware will reject non-loopback traffic. let bind_is_loopback = listen_addr.ip().is_loopback(); - if api_key.is_empty() && !state.kernel.config.auth.enabled && !bind_is_loopback { + if api_key.is_empty() && !auth_enabled && !bind_is_loopback { if allow_no_auth { tracing::warn!( "OPENFANG_ALLOW_NO_AUTH=1 is set. Running WITHOUT authentication on {}. \ @@ -147,11 +162,11 @@ pub async fn build_router( let auth_state = crate::middleware::AuthState { api_key: api_key.clone(), - auth_enabled: state.kernel.config.auth.enabled, - session_secret: if !api_key.is_empty() { - api_key.clone() - } else if state.kernel.config.auth.enabled { - state.kernel.config.auth.password_hash.clone() + raw_api_key: state.kernel.config.api_key.trim().to_string(), + auth_enabled, + session_secret: session_secret.clone(), + dashboard_pin: if pin_active { + state.kernel.config.dashboard.pin.trim().to_string() } else { String::new() }, @@ -165,6 +180,8 @@ pub async fn build_router( .route("/favicon.ico", axum::routing::get(webchat::favicon_ico)) .route("/manifest.json", axum::routing::get(webchat::manifest_json)) .route("/sw.js", axum::routing::get(webchat::sw_js)) + .route("/i18n/en.json", axum::routing::get(webchat::i18n_en_json)) + .route("/i18n/ru.json", axum::routing::get(webchat::i18n_ru_json)) .route( "/api/metrics", axum::routing::get(routes::prometheus_metrics), @@ -175,6 +192,23 @@ pub async fn build_router( axum::routing::get(routes::health_detail), ) .route("/api/status", axum::routing::get(routes::status)) + .route("/api/system/gpu", axum::routing::get(routes::system_gpu)) + .route( + "/api/brain/status", + axum::routing::get(crate::brain::brain_status), + ) + .route( + "/api/brain/list", + axum::routing::get(crate::brain::brain_list), + ) + .route( + "/api/brain/file", + axum::routing::get(crate::brain::brain_file).post(crate::brain::brain_write), + ) + .route( + "/api/system/drift", + axum::routing::get(routes::system_drift).post(routes::system_drift_remediate), + ) .route("/api/version", axum::routing::get(routes::version)) .route( "/api/agents", @@ -604,6 +638,14 @@ pub async fn build_router( "/api/models/custom/{*id}", axum::routing::delete(routes::remove_custom_model), ) + .route( + "/api/models/profiles", + axum::routing::get(routes::list_model_profiles), + ) + .route( + "/api/models/active", + axum::routing::get(routes::get_active_model_profile).put(routes::set_active_model_profile), + ) .route("/api/models/{*id}", axum::routing::get(routes::get_model)) .route("/api/providers", axum::routing::get(routes::list_providers)) // Copilot OAuth (must be before parametric {name} routes) @@ -797,11 +839,11 @@ pub async fn build_router( (app, state) } -/// Start the OpenFang daemon: boot kernel + HTTP API server. +/// Start the OMTAE daemon: boot kernel + HTTP API server. /// /// This function blocks until Ctrl+C or a shutdown request. pub async fn run_daemon( - kernel: OpenFangKernel, + kernel: OMTAEKernel, listen_addr: &str, daemon_info_path: Option<&Path>, ) -> Result<(), Box> { @@ -810,6 +852,7 @@ pub async fn run_daemon( let kernel = Arc::new(kernel); kernel.set_self_handle(); kernel.start_background_agents(); + omtae_kernel::drift_guard::spawn_drift_watchdog(kernel.clone()); // Config file hot-reload watcher (polls every 30 seconds) { @@ -879,7 +922,7 @@ pub async fn run_daemon( } } - info!("OpenFang API server listening on http://{addr}"); + info!("OMTAE API server listening on http://{addr}"); info!("WebChat UI available at http://{addr}/",); info!("WebSocket endpoint: ws://{addr}/api/agents/{{id}}/ws",); @@ -923,7 +966,7 @@ pub async fn run_daemon( // Shutdown kernel kernel.shutdown(); - info!("OpenFang daemon stopped"); + info!("OMTAE daemon stopped"); Ok(()) } @@ -1018,7 +1061,7 @@ fn is_process_alive(pid: u32) -> bool { } } -/// Check if an OpenFang daemon is actually responding at the given address. +/// Check if an OMTAE daemon is actually responding at the given address. /// This avoids false positives where a different process reused the same PID /// after a system reboot. fn is_daemon_responding(addr: &str) -> bool { diff --git a/crates/openfang-api/src/session_auth.rs b/crates/openfang-api/src/session_auth.rs index 9de34b8069..3a0726cf79 100644 --- a/crates/openfang-api/src/session_auth.rs +++ b/crates/openfang-api/src/session_auth.rs @@ -17,7 +17,71 @@ pub fn create_session_token(username: &str, secret: &str, ttl_hours: u64) -> Str base64::engine::general_purpose::STANDARD.encode(format!("{payload}:{signature}")) } -/// Extract the `openfang_session` cookie value from a `Cookie` header string. +/// True when the request arrived over HTTPS (direct TLS or proxy headers). +pub fn request_is_secure(headers: &axum::http::HeaderMap) -> bool { + if headers + .get("x-forwarded-proto") + .and_then(|v| v.to_str().ok()) + .is_some_and(|p| p.eq_ignore_ascii_case("https")) + { + return true; + } + if headers + .get("x-forwarded-ssl") + .and_then(|v| v.to_str().ok()) + .is_some_and(|v| v.eq_ignore_ascii_case("on")) + { + return true; + } + if headers + .get("cf-visitor") + .and_then(|v| v.to_str().ok()) + .is_some_and(|v| v.contains("\"scheme\":\"https\"") || v.contains("scheme=https")) + { + return true; + } + headers + .get(axum::http::header::FORWARDED) + .and_then(|v| v.to_str().ok()) + .is_some_and(|f| f.to_ascii_lowercase().contains("proto=https")) +} + +/// Whether the `Set-Cookie` `Secure` flag should be set for this request. +/// +/// Uses HTTPS detection plus non-localhost `Host` so Cloudflare tunnel clients +/// (`desk.omtaeservices.biz`) always get `Secure; SameSite=Lax` cookies. +pub fn cookie_should_be_secure(headers: &axum::http::HeaderMap) -> bool { + if request_is_secure(headers) { + return true; + } + let host = headers + .get("host") + .and_then(|v| v.to_str().ok()) + .map(|h| h.split(':').next().unwrap_or(h).to_ascii_lowercase()); + match host.as_deref() { + Some("localhost") | Some("127.0.0.1") | Some("[::1]") => false, + Some(_) => true, + None => false, + } +} + +/// Build the `Set-Cookie` value for a dashboard session. +/// +/// Uses `SameSite=Lax` (tunnel-friendly) and `Secure` on HTTPS so mobile +/// browsers accept the cookie behind Cloudflare. +pub fn format_session_cookie(token: &str, ttl_secs: u64, secure: bool) -> String { + let secure_flag = if secure { "; Secure" } else { "" }; + format!( + "omtae_session={token}; Path=/; HttpOnly; SameSite=Lax{secure_flag}; Max-Age={ttl_secs}" + ) +} + +/// Clear the session cookie (logout). +pub fn format_session_cookie_clear(secure: bool) -> String { + format_session_cookie("", 0, secure) +} + +/// Extract the `omtae_session` cookie value from a `Cookie` header string. /// /// Returns `None` if the header is absent or the cookie is not present. /// Used by both the HTTP auth middleware and the WebSocket upgrade handler so @@ -30,7 +94,7 @@ pub fn extract_session_cookie(headers: &axum::http::HeaderMap) -> Option .and_then(|cookies| { cookies.split(';').find_map(|c| { c.trim() - .strip_prefix("openfang_session=") + .strip_prefix("omtae_session=") .map(|v| v.to_string()) }) }) @@ -86,6 +150,17 @@ pub fn hash_password(password: &str) -> String { .to_string() } +/// Constant-time compare for `[dashboard].pin`. +pub fn verify_dashboard_pin(provided: &str, stored: &str) -> bool { + use subtle::ConstantTimeEq; + let a = provided.trim().as_bytes(); + let b = stored.trim().as_bytes(); + if a.is_empty() || b.is_empty() || a.len() != b.len() { + return false; + } + a.ct_eq(b).into() +} + /// Verify a password against a stored Argon2id hash (PHC string format). pub fn verify_password(password: &str, stored_hash: &str) -> bool { use argon2::{password_hash::PasswordHash, Argon2, PasswordVerifier}; @@ -166,7 +241,7 @@ mod tests { let mut h = axum::http::HeaderMap::new(); h.insert( "cookie", - "foo=bar; openfang_session=abc.def.ghi; baz=qux" + "foo=bar; omtae_session=abc.def.ghi; baz=qux" .parse() .unwrap(), ); @@ -186,10 +261,46 @@ mod tests { assert_eq!(extract_session_cookie(&h), None); } + #[test] + fn test_verify_dashboard_pin() { + assert!(verify_dashboard_pin("1234", "1234")); + assert!(!verify_dashboard_pin("1235", "1234")); + assert!(!verify_dashboard_pin("1234", "")); + } + #[test] fn test_extract_session_cookie_only_value() { let mut h = axum::http::HeaderMap::new(); - h.insert("cookie", "openfang_session=lonely".parse().unwrap()); + h.insert("cookie", "omtae_session=lonely".parse().unwrap()); assert_eq!(extract_session_cookie(&h).as_deref(), Some("lonely")); } + + #[test] + fn test_format_session_cookie_secure() { + let c = format_session_cookie("tok", 3600, true); + assert!(c.contains("Secure")); + assert!(c.contains("SameSite=Lax")); + assert!(c.contains("omtae_session=tok")); + } + + #[test] + fn test_request_is_secure_forwarded_proto() { + let mut h = axum::http::HeaderMap::new(); + h.insert("x-forwarded-proto", "https".parse().unwrap()); + assert!(request_is_secure(&h)); + } + + #[test] + fn test_cookie_should_be_secure_public_host() { + let mut h = axum::http::HeaderMap::new(); + h.insert("host", "desk.omtaeservices.biz".parse().unwrap()); + assert!(cookie_should_be_secure(&h)); + } + + #[test] + fn test_cookie_should_be_secure_localhost() { + let mut h = axum::http::HeaderMap::new(); + h.insert("host", "127.0.0.1:4200".parse().unwrap()); + assert!(!cookie_should_be_secure(&h)); + } } diff --git a/crates/openfang-api/src/types.rs b/crates/openfang-api/src/types.rs index e4b9e61a77..57e7838676 100644 --- a/crates/openfang-api/src/types.rs +++ b/crates/openfang-api/src/types.rs @@ -1,4 +1,4 @@ -//! Request/response types for the OpenFang API. +//! Request/response types for the OMTAE API. use serde::{Deserialize, Serialize}; @@ -8,7 +8,7 @@ pub struct SpawnRequest { /// Agent manifest as TOML string (optional if `template` is provided). #[serde(default)] pub manifest_toml: String, - /// Template name from `~/.openfang/agents/{template}/agent.toml`. + /// Template name from `~/.omtae/agents/{template}/agent.toml`. /// When provided and `manifest_toml` is empty, the template is loaded automatically. #[serde(default)] pub template: Option, @@ -91,7 +91,7 @@ pub struct AgentUpdateRequest { /// Request to change an agent's operational mode. #[derive(Debug, Deserialize)] pub struct SetModeRequest { - pub mode: openfang_types::agent::AgentMode, + pub mode: omtae_types::agent::AgentMode, } /// Request to run a migration. diff --git a/crates/openfang-api/src/webchat.rs b/crates/openfang-api/src/webchat.rs index 14f44c62e1..b163cce92d 100644 --- a/crates/openfang-api/src/webchat.rs +++ b/crates/openfang-api/src/webchat.rs @@ -22,7 +22,7 @@ const NONCE_PLACEHOLDER: &str = "__NONCE__"; /// Not used for the dashboard page (nonce prevents caching) but retained /// for potential future use by static asset handlers. #[allow(dead_code)] -const ETAG: &str = concat!("\"openfang-", env!("CARGO_PKG_VERSION"), "\""); +const ETAG: &str = concat!("\"omtae-", env!("CARGO_PKG_VERSION"), "\""); /// Embedded logo PNG for single-binary deployment. const LOGO_PNG: &[u8] = include_bytes!("../static/logo.png"); @@ -30,7 +30,7 @@ const LOGO_PNG: &[u8] = include_bytes!("../static/logo.png"); /// Embedded favicon ICO for browser tabs. const FAVICON_ICO: &[u8] = include_bytes!("../static/favicon.ico"); -/// GET /logo.png — Serve the OpenFang logo. +/// GET /logo.png — Serve the OMTAE logo. pub async fn logo_png() -> impl IntoResponse { ( [ @@ -41,7 +41,7 @@ pub async fn logo_png() -> impl IntoResponse { ) } -/// GET /favicon.ico — Serve the OpenFang favicon. +/// GET /favicon.ico — Serve the OMTAE favicon. pub async fn favicon_ico() -> impl IntoResponse { ( [ @@ -58,6 +58,32 @@ const MANIFEST_JSON: &str = include_str!("../static/manifest.json"); /// Embedded service worker for PWA support. const SW_JS: &str = include_str!("../static/sw.js"); +/// Embedded i18n translation bundles (served at `/i18n/{lang}.json`). +const I18N_EN_JSON: &str = include_str!("../static/i18n/en.json"); +const I18N_RU_JSON: &str = include_str!("../static/i18n/ru.json"); + +/// GET /i18n/en.json — English UI strings for the dashboard. +pub async fn i18n_en_json() -> impl IntoResponse { + ( + [ + (header::CONTENT_TYPE, "application/json; charset=utf-8"), + (header::CACHE_CONTROL, "public, max-age=3600"), + ], + I18N_EN_JSON, + ) +} + +/// GET /i18n/ru.json — Russian UI strings for the dashboard. +pub async fn i18n_ru_json() -> impl IntoResponse { + ( + [ + (header::CONTENT_TYPE, "application/json; charset=utf-8"), + (header::CACHE_CONTROL, "public, max-age=3600"), + ], + I18N_RU_JSON, + ) +} + /// GET /manifest.json — Serve the PWA web app manifest. pub async fn manifest_json() -> impl IntoResponse { ( @@ -80,7 +106,7 @@ pub async fn sw_js() -> impl IntoResponse { ) } -/// GET / — Serve the OpenFang Dashboard single-page application. +/// GET / — Serve the OMTAE Dashboard single-page application. /// /// Generates a unique CSP nonce on every request and injects it into both /// the `\n", + // i18n (must load before app code that calls window.i18n / window.t) + "\n", // App code " diff --git a/crates/openfang-api/static/index_head.html b/crates/openfang-api/static/index_head.html index 7bea957865..b1230d149d 100644 --- a/crates/openfang-api/static/index_head.html +++ b/crates/openfang-api/static/index_head.html @@ -1,13 +1,22 @@ - + -OpenFang Dashboard +OMTAE Dashboard - + + + diff --git a/crates/openfang-api/static/js/api.js b/crates/openfang-api/static/js/api.js index dccb499f9a..6cf9327c4d 100644 --- a/crates/openfang-api/static/js/api.js +++ b/crates/openfang-api/static/js/api.js @@ -1,8 +1,8 @@ -// OpenFang API Client — Fetch wrapper, WebSocket manager, auth injection, toast notifications +// OMTAE API Client — Fetch wrapper, WebSocket manager, auth injection, toast notifications 'use strict'; // ── Toast Notification System ── -var OpenFangToast = (function() { +var OMTAEToast = (function() { var _container = null; var _toastId = 0; @@ -117,22 +117,30 @@ var OpenFangToast = (function() { // ── Friendly Error Messages ── function friendlyError(status, serverMsg) { - if (status === 0 || !status) return 'Cannot reach daemon — is openfang running?'; + if (status === 0 || !status) return 'Cannot reach daemon — is omtae running?'; if (status === 401) return 'Not authorized — check your API key'; if (status === 403) return 'Permission denied'; if (status === 404) return serverMsg || 'Resource not found'; if (status === 429) return 'Rate limited — slow down and try again'; if (status === 413) return 'Request too large'; - if (status === 500) return 'Server error — check daemon logs'; - if (status === 502 || status === 503) return 'Daemon unavailable — is it running?'; + if (status === 500) return serverMsg || 'Server error — check daemon logs'; + if (status === 400) return serverMsg || 'Bad request'; + if (status === 502) return serverMsg || 'Upstream LLM error — check provider key and logs'; + if (status === 503) return 'Daemon unavailable — is it running?'; return serverMsg || 'Unexpected error (' + status + ')'; } // ── API Client ── -var OpenFangAPI = (function() { +var OMTAEAPI = (function() { var BASE = window.location.origin; var WS_BASE = BASE.replace(/^http/, 'ws'); var _authToken = ''; + var _sessionToken = (function() { + try { return sessionStorage.getItem('omtae-session-token') || ''; } catch(e) { return ''; } + })(); + var _pinHeader = (function() { + try { return sessionStorage.getItem('omtae-pin') || ''; } catch(e) { return ''; } + })(); // Connection state tracking var _connectionState = 'connected'; @@ -141,9 +149,30 @@ var OpenFangAPI = (function() { function setAuthToken(token) { _authToken = token; } + function setSessionToken(token) { + _sessionToken = token || ''; + try { + if (_sessionToken) sessionStorage.setItem('omtae-session-token', _sessionToken); + else sessionStorage.removeItem('omtae-session-token'); + } catch(e) { /* ignore */ } + } + + function setPinHeader(pin) { + _pinHeader = pin || ''; + try { + if (_pinHeader) sessionStorage.setItem('omtae-pin', _pinHeader); + else sessionStorage.removeItem('omtae-pin'); + } catch(e) { /* ignore */ } + } + function headers() { var h = { 'Content-Type': 'application/json' }; - if (_authToken) h['Authorization'] = 'Bearer ' + _authToken; + if (_authToken) { + h['Authorization'] = 'Bearer ' + _authToken; + } else if (_sessionToken) { + h['Authorization'] = 'Bearer ' + _sessionToken; + } + if (_pinHeader) h['X-OMTAE-Pin'] = _pinHeader; return h; } @@ -156,18 +185,18 @@ var OpenFangAPI = (function() { function onConnectionChange(fn) { _connectionListeners.push(fn); } function request(method, path, body) { - var opts = { method: method, headers: headers() }; + var opts = { method: method, headers: headers(), credentials: 'include' }; if (body !== undefined) opts.body = JSON.stringify(body); return fetch(BASE + path, opts).then(function(r) { if (_connectionState !== 'connected') setConnectionState('connected'); if (!r.ok) { // On 401, auto-show auth prompt so the user can re-enter their key - if (r.status === 401 && typeof Alpine !== 'undefined') { + if (r.status === 401 && path !== '/api/auth/login' && typeof Alpine !== 'undefined') { try { var store = Alpine.store('app'); if (store && !store.showAuthPrompt) { _authToken = ''; - localStorage.removeItem('openfang-api-key'); + localStorage.removeItem('omtae-api-key'); store.showAuthPrompt = true; } } catch(e2) { /* ignore Alpine errors */ } @@ -191,7 +220,7 @@ var OpenFangAPI = (function() { }).catch(function(e) { if (e.name === 'TypeError' && e.message.includes('Failed to fetch')) { setConnectionState('disconnected'); - throw new Error('Cannot connect to daemon — is openfang running?'); + throw new Error('Cannot connect to daemon — is omtae running?'); } throw e; }); @@ -223,7 +252,13 @@ var OpenFangAPI = (function() { function _doConnect(agentId) { try { var url = WS_BASE + '/api/agents/' + agentId + '/ws'; - if (_authToken) url += '?token=' + encodeURIComponent(_authToken); + if (_authToken) { + url += '?token=' + encodeURIComponent(_authToken); + } else if (_sessionToken) { + url += '?session=' + encodeURIComponent(_sessionToken); + } else if (_pinHeader) { + url += '?pin=' + encodeURIComponent(_pinHeader); + } var socket = new WebSocket(url); _ws = socket; @@ -234,7 +269,7 @@ var OpenFangAPI = (function() { _reconnectAttempts = 0; setConnectionState('connected'); if (_reconnectAttempt > 0) { - OpenFangToast.success('Reconnected'); + OMTAEToast.success('Reconnected'); _reconnectAttempt = 0; } if (_wsCallbacks.onOpen) _wsCallbacks.onOpen(); @@ -261,7 +296,7 @@ var OpenFangAPI = (function() { _reconnectAttempt = _reconnectAttempts; setConnectionState('reconnecting'); if (_reconnectAttempts === 1) { - OpenFangToast.warn('Connection lost, reconnecting...'); + OMTAEToast.warn('Connection lost, reconnecting...'); } var delay = Math.min(1000 * Math.pow(2, _reconnectAttempts - 1), 10000); _reconnectTimer = setTimeout(function() { _doConnect(_wsAgentId); }, delay); @@ -269,7 +304,7 @@ var OpenFangAPI = (function() { } if (_wsAgentId && _reconnectAttempts >= MAX_RECONNECT) { setConnectionState('disconnected'); - OpenFangToast.error('Connection lost — switched to HTTP mode', 0); + OMTAEToast.error('Connection lost — switched to HTTP mode', 0); } if (_wsCallbacks.onClose) _wsCallbacks.onClose(); }; @@ -310,12 +345,15 @@ var OpenFangAPI = (function() { function upload(agentId, file) { var hdrs = {}; if (_authToken) hdrs['Authorization'] = 'Bearer ' + _authToken; + else if (_sessionToken) hdrs['Authorization'] = 'Bearer ' + _sessionToken; + if (_pinHeader) hdrs['X-OMTAE-Pin'] = _pinHeader; var form = new FormData(); form.append('file', file); form.append('filename', file.name); return fetch(BASE + '/api/agents/' + agentId + '/upload', { method: 'POST', headers: hdrs, + credentials: 'include', body: form }).then(function(r) { if (!r.ok) throw new Error('Upload failed'); @@ -325,6 +363,8 @@ var OpenFangAPI = (function() { return { setAuthToken: setAuthToken, + setSessionToken: setSessionToken, + setPinHeader: setPinHeader, getToken: getToken, get: get, post: post, diff --git a/crates/openfang-api/static/js/app.js b/crates/openfang-api/static/js/app.js index 8d2659ece5..e9a3ab1e0f 100644 --- a/crates/openfang-api/static/js/app.js +++ b/crates/openfang-api/static/js/app.js @@ -1,4 +1,4 @@ -// OpenFang App — Alpine.js init, hash router, global store +// OMTAE App — Alpine.js init, hash router, global store 'use strict'; // Marked.js configuration @@ -122,8 +122,8 @@ function toolIcon(toolName) { // Alpine.js global store document.addEventListener('alpine:init', function() { // Restore saved API key on load - var savedKey = localStorage.getItem('openfang-api-key'); - if (savedKey) OpenFangAPI.setAuthToken(savedKey); + var savedKey = localStorage.getItem('omtae-api-key'); + if (savedKey) OMTAEAPI.setAuthToken(savedKey); Alpine.store('app', { agents: [], @@ -137,20 +137,28 @@ document.addEventListener('alpine:init', function() { pendingApprovalCount: 0, lastPendingApprovalSignature: '', pendingAgent: null, - focusMode: localStorage.getItem('openfang-focus') === 'true', + focusMode: localStorage.getItem('omtae-focus') === 'true', showOnboarding: false, showAuthPrompt: false, authMode: 'apikey', sessionUser: null, + gpuStats: { + available: false, + gpu0: { temp: 0, usage: 0, memory: 0, memoryTotal: 24, processes: [] }, + gpu1: { temp: 0, usage: 0, memory: 0, memoryTotal: 24, processes: [] }, + usedVram: 0, + totalVram: 48, + model: 'vLLM' + }, toggleFocusMode() { this.focusMode = !this.focusMode; - localStorage.setItem('openfang-focus', this.focusMode); + localStorage.setItem('omtae-focus', this.focusMode); }, async refreshAgents() { try { - var agents = await OpenFangAPI.get('/api/agents'); + var agents = await OMTAEAPI.get('/api/agents'); this.agents = Array.isArray(agents) ? agents : []; this.agentCount = this.agents.length; } catch(e) { /* silent */ } @@ -158,15 +166,15 @@ document.addEventListener('alpine:init', function() { async refreshApprovals() { try { - var data = await OpenFangAPI.get('/api/approvals'); + var data = await OMTAEAPI.get('/api/approvals'); var approvals = Array.isArray(data) ? data : (data.approvals || []); var pending = approvals.filter(function(a) { return a.status === 'pending'; }); var signature = pending .map(function(a) { return a.id; }) .sort() .join(','); - if (pending.length > 0 && signature !== this.lastPendingApprovalSignature && typeof OpenFangToast !== 'undefined') { - OpenFangToast.warn('An agent is waiting for approval. Open Approvals to review.'); + if (pending.length > 0 && signature !== this.lastPendingApprovalSignature && typeof OMTAEToast !== 'undefined') { + OMTAEToast.warn('An agent is waiting for approval. Open Approvals to review.'); } this.pendingApprovalCount = pending.length; this.lastPendingApprovalSignature = signature; @@ -175,7 +183,7 @@ document.addEventListener('alpine:init', function() { async checkStatus() { try { - var s = await OpenFangAPI.get('/api/status'); + var s = await OMTAEAPI.get('/api/status'); this.connected = true; this.booting = false; this.lastError = ''; @@ -184,14 +192,23 @@ document.addEventListener('alpine:init', function() { } catch(e) { this.connected = false; this.lastError = e.message || 'Unknown error'; - console.warn('[OpenFang] Status check failed:', e.message); + console.warn('[OMTAE] Status check failed:', e.message); } }, + async refreshGpu() { + try { + var g = await OMTAEAPI.get('/api/system/gpu'); + if (g && typeof g === 'object') { + this.gpuStats = g; + } + } catch(e) { /* silent — no nvidia-smi on this host */ } + }, + async checkOnboarding() { - if (localStorage.getItem('openfang-onboarded')) return; + if (localStorage.getItem('omtae-onboarded')) return; try { - var config = await OpenFangAPI.get('/api/config'); + var config = await OMTAEAPI.get('/api/config'); var apiKey = config && config.api_key; var noKey = !apiKey || apiKey === 'not set' || apiKey === ''; if (noKey && this.agentCount === 0) { @@ -205,25 +222,25 @@ document.addEventListener('alpine:init', function() { dismissOnboarding() { this.showOnboarding = false; - localStorage.setItem('openfang-onboarded', 'true'); + localStorage.setItem('omtae-onboarded', 'true'); }, async checkAuth() { try { // First check if session-based auth is configured - var authInfo = await OpenFangAPI.get('/api/auth/check'); + var authInfo = await OMTAEAPI.get('/api/auth/check'); if (authInfo.mode === 'none') { // No session auth — fall back to API key detection this.authMode = 'apikey'; this.sessionUser = null; - } else if (authInfo.mode === 'session') { - this.authMode = 'session'; + } else if (authInfo.mode === 'pin' || authInfo.mode === 'session') { + this.authMode = authInfo.mode; if (authInfo.authenticated) { this.sessionUser = authInfo.username; this.showAuthPrompt = false; return; } - // Session auth enabled but not authenticated — show login prompt + // Dashboard auth enabled but not authenticated — show login prompt this.showAuthPrompt = true; return; } @@ -231,14 +248,14 @@ document.addEventListener('alpine:init', function() { // API key mode detection try { - await OpenFangAPI.get('/api/tools'); + await OMTAEAPI.get('/api/tools'); this.showAuthPrompt = false; } catch(e) { if (e.message && (e.message.indexOf('Not authorized') >= 0 || e.message.indexOf('401') >= 0 || e.message.indexOf('Missing Authorization') >= 0 || e.message.indexOf('Unauthorized') >= 0)) { - var saved = localStorage.getItem('openfang-api-key'); + var saved = localStorage.getItem('omtae-api-key'); if (saved) { - OpenFangAPI.setAuthToken(''); - localStorage.removeItem('openfang-api-key'); + OMTAEAPI.setAuthToken(''); + localStorage.removeItem('omtae-api-key'); } this.showAuthPrompt = true; } @@ -247,38 +264,60 @@ document.addEventListener('alpine:init', function() { submitApiKey(key) { if (!key || !key.trim()) return; - OpenFangAPI.setAuthToken(key.trim()); - localStorage.setItem('openfang-api-key', key.trim()); + OMTAEAPI.setAuthToken(key.trim()); + localStorage.setItem('omtae-api-key', key.trim()); this.showAuthPrompt = false; this.refreshAgents(); }, + async sessionPinLogin(pin) { + var trimmed = (pin || '').trim(); + if (!trimmed) return; + try { + var result = await OMTAEAPI.post('/api/auth/login', { pin: trimmed }); + if (result.status === 'ok') { + this.sessionUser = result.username || 'dashboard'; + OMTAEAPI.setPinHeader(trimmed); + if (result.token) OMTAEAPI.setSessionToken(result.token); + this.showAuthPrompt = false; + this.refreshAgents(); + } else { + OMTAEToast.error(result.error || 'Invalid PIN'); + } + } catch(e) { + OMTAEToast.error(e.message || 'Invalid PIN'); + } + }, + async sessionLogin(username, password) { try { - var result = await OpenFangAPI.post('/api/auth/login', { username: username, password: password }); + var result = await OMTAEAPI.post('/api/auth/login', { username: username, password: password }); if (result.status === 'ok') { this.sessionUser = result.username; + if (result.token) OMTAEAPI.setSessionToken(result.token); this.showAuthPrompt = false; this.refreshAgents(); } else { - OpenFangToast.error(result.error || 'Login failed'); + OMTAEToast.error(result.error || 'Login failed'); } } catch(e) { - OpenFangToast.error(e.message || 'Login failed'); + OMTAEToast.error(e.message || 'Login failed'); } }, async sessionLogout() { try { - await OpenFangAPI.post('/api/auth/logout'); + await OMTAEAPI.post('/api/auth/logout'); } catch(e) { /* ignore */ } + OMTAEAPI.setPinHeader(''); + OMTAEAPI.setSessionToken(''); this.sessionUser = null; this.showAuthPrompt = true; }, clearApiKey() { - OpenFangAPI.setAuthToken(''); - localStorage.removeItem('openfang-api-key'); + OMTAEAPI.setAuthToken(''); + localStorage.removeItem('omtae-api-key'); } }); }); @@ -287,13 +326,9 @@ document.addEventListener('alpine:init', function() { function app() { return { page: 'agents', - themeMode: localStorage.getItem('openfang-theme-mode') || 'system', - theme: (() => { - var mode = localStorage.getItem('openfang-theme-mode') || 'system'; - if (mode === 'system') return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; - return mode; - })(), - sidebarCollapsed: localStorage.getItem('openfang-sidebar') === 'collapsed', + themeMode: 'dark', + theme: 'dark', + sidebarCollapsed: localStorage.getItem('omtae-sidebar') === 'collapsed', mobileMenuOpen: false, connected: false, wsConnected: false, @@ -304,15 +339,10 @@ function app() { init() { var self = this; + localStorage.setItem('omtae-theme-mode', 'dark'); + this.applyTheme('dark'); - // Listen for OS theme changes (only matters when mode is 'system') - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) { - if (self.themeMode === 'system') { - self.theme = e.matches ? 'dark' : 'light'; - } - }); - - // Hash routing + // Connection state listener var validPages = ['overview','agents','sessions','approvals','comms','workflows','scheduler','channels','skills','hands','analytics','logs','runtime','settings','wizard']; var pageRedirects = { 'chat': 'agents', @@ -358,12 +388,12 @@ function app() { } // Escape — close mobile menu if (e.key === 'Escape') { - self.mobileMenuOpen = false; + self.closeMobileMenu(); } }); // Connection state listener - OpenFangAPI.onConnectionChange(function(state) { + OMTAEAPI.onConnectionChange(function(state) { Alpine.store('app').connectionState = state; }); @@ -376,43 +406,68 @@ function app() { self.pollStatus(); Alpine.store('app').refreshApprovals(); }, 5000); + + // Close mobile drawer when viewport crosses desktop breakpoint + var mobileMq = window.matchMedia('(min-width: 769px) and (hover: hover), (min-width: 1025px)'); + function onViewportChange() { + if (mobileMq.matches) self.closeMobileMenu(); + } + if (mobileMq.addEventListener) { + mobileMq.addEventListener('change', onViewportChange); + } else if (mobileMq.addListener) { + mobileMq.addListener(onViewportChange); + } + window.addEventListener('orientationchange', function() { + setTimeout(onViewportChange, 100); + }); }, navigate(p) { this.page = p; window.location.hash = p; + this.closeMobileMenu(); + }, + + toggleMobileMenu() { + this.mobileMenuOpen = !this.mobileMenuOpen; + document.body.classList.toggle('mobile-nav-open', this.mobileMenuOpen); + }, + + closeMobileMenu() { + if (!this.mobileMenuOpen) return; this.mobileMenuOpen = false; + document.body.classList.remove('mobile-nav-open'); + }, + + applyTheme(theme) { + document.documentElement.setAttribute('data-theme', 'dark'); }, setTheme(mode) { - this.themeMode = mode; - localStorage.setItem('openfang-theme-mode', mode); - if (mode === 'system') { - this.theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; - } else { - this.theme = mode; - } + this.themeMode = 'dark'; + this.theme = 'dark'; + localStorage.setItem('omtae-theme-mode', 'dark'); + this.applyTheme('dark'); }, toggleTheme() { - var modes = ['light', 'system', 'dark']; - var next = modes[(modes.indexOf(this.themeMode) + 1) % modes.length]; - this.setTheme(next); + this.setTheme('dark'); }, toggleSidebar() { this.sidebarCollapsed = !this.sidebarCollapsed; - localStorage.setItem('openfang-sidebar', this.sidebarCollapsed ? 'collapsed' : 'expanded'); + localStorage.setItem('omtae-sidebar', this.sidebarCollapsed ? 'collapsed' : 'expanded'); }, async pollStatus() { var store = Alpine.store('app'); await store.checkStatus(); + await store.refreshGpu(); await store.refreshAgents(); this.connected = store.connected; this.version = store.version; this.agentCount = store.agentCount; - this.wsConnected = OpenFangAPI.isWsConnected(); + this.wsConnected = OMTAEAPI.isWsConnected(); } }; } diff --git a/crates/openfang-api/static/js/katex.js b/crates/openfang-api/static/js/katex.js index 8a704ade84..d0e1a3779d 100644 --- a/crates/openfang-api/static/js/katex.js +++ b/crates/openfang-api/static/js/katex.js @@ -32,7 +32,7 @@ function ensureKatexLoaded() { if (katexLoadPromise) return katexLoadPromise; katexLoadPromise = new Promise(function (resolve) { - var cssId = 'openfang-katex-css'; + var cssId = 'omtae-katex-css'; if (!document.getElementById(cssId)) { var link = document.createElement('link'); link.id = cssId; diff --git a/crates/openfang-api/static/js/pages/agents.js b/crates/openfang-api/static/js/pages/agents.js index b6702855f1..c30a8b92ee 100644 --- a/crates/openfang-api/static/js/pages/agents.js +++ b/crates/openfang-api/static/js/pages/agents.js @@ -1,4 +1,4 @@ -// OpenFang Agents Page — Multi-step spawn wizard, detail view with tabs, file editor, personality presets +// OMTAE Agents Page — Multi-step spawn wizard, detail view with tabs, file editor, personality presets 'use strict'; /** Escape a string for use inside TOML triple-quoted strings ("""\n...\n"""). @@ -43,7 +43,7 @@ function agentsPage() { spawnProviders: [], // populated from /api/providers on wizard open spawnProvidersLoading: false, spawnStep: 1, - spawnIdentity: { emoji: '', color: '#FF5C00', archetype: '' }, + spawnIdentity: { emoji: '', color: '#00F0FF', archetype: '' }, selectedPreset: '', soulContent: '', emojiOptions: [ @@ -142,7 +142,7 @@ function agentsPage() { async loadSpawnProfiles() { if (this.spawnProfilesLoaded) return; try { - var data = await OpenFangAPI.get('/api/profiles'); + var data = await OMTAEAPI.get('/api/profiles'); this.spawnProfiles = data.profiles || []; this.spawnProfilesLoaded = true; } catch(e) { this.spawnProfiles = []; } @@ -223,6 +223,9 @@ function agentsPage() { } this.loading = false; + // Restore preferred chat agent (researcher/analyst/orchestrator/coder) + this._restorePreferredChat(); + // If a pending agent was set (e.g. from wizard or redirect), open chat inline var store = Alpine.store('app'); if (store.pendingAgent) { @@ -252,8 +255,8 @@ function agentsPage() { this.tplLoadError = ''; try { var results = await Promise.all([ - OpenFangAPI.get('/api/templates'), - OpenFangAPI.get('/api/providers').catch(function() { return { providers: [] }; }) + OMTAEAPI.get('/api/templates'), + OMTAEAPI.get('/api/providers').catch(function() { return { providers: [] }; }) ]); // Combine static and dynamic templates this.builtinTemplates = [ @@ -328,13 +331,50 @@ function agentsPage() { }, chatWithAgent(agent) { + if (typeof ofIsPreferredAgent === 'function' && !ofIsPreferredAgent(agent)) { + var preferred = typeof ofPickPreferredAgent === 'function' + ? ofPickPreferredAgent(this.agents || []) + : null; + if (preferred) agent = preferred; + } Alpine.store('app').pendingAgent = agent; this.activeChatAgent = agent; }, + _restorePreferredChat: function() { + var store = Alpine.store('app'); + if (store && store.pendingAgent) return; + var agents = this.agents || []; + if (!agents.length) return; + + var storedId = null; + try { storedId = localStorage.getItem('of-active-agent'); } catch(e) { /* ignore */ } + + var match = null; + if (storedId) { + for (var i = 0; i < agents.length; i++) { + if (agents[i] && agents[i].id === storedId) { match = agents[i]; break; } + } + } + + if (match && typeof ofIsPreferredAgent === 'function' && !ofIsPreferredAgent(match)) { + try { localStorage.removeItem('of-active-agent'); } catch(e) { /* ignore */ } + match = null; + } + + if (!match && typeof ofPickPreferredAgent === 'function') { + match = ofPickPreferredAgent(agents); + } + + if (match) { + store.pendingAgent = match; + this.activeChatAgent = match; + } + }, + closeChat() { this.activeChatAgent = null; - OpenFangAPI.wsDisconnect(); + OMTAEAPI.wsDisconnect(); }, buildConfigForm(agent) { @@ -343,7 +383,7 @@ function agentsPage() { name: (agent && agent.name) || '', system_prompt: (agent && agent.system_prompt) || '', emoji: identity.emoji || '', - color: identity.color || '#FF5C00', + color: identity.color || '#00F0FF', archetype: identity.archetype || '', vibe: identity.vibe || '' }; @@ -360,7 +400,7 @@ function agentsPage() { // fields such as system_prompt and identity metadata are hydrated. var detail = agent; try { - var full = await OpenFangAPI.get('/api/agents/' + agent.id); + var full = await OMTAEAPI.get('/api/agents/' + agent.id); detail = Object.assign({}, agent, full, { identity: Object.assign({}, (agent && agent.identity) || {}, (full && full.identity) || {}) }); @@ -373,36 +413,36 @@ function agentsPage() { killAgent(agent) { var self = this; - OpenFangToast.confirm('Stop Agent', 'Stop agent "' + agent.name + '"? The agent will be shut down.', async function() { + OMTAEToast.confirm('Stop Agent', 'Stop agent "' + agent.name + '"? The agent will be shut down.', async function() { try { - await OpenFangAPI.del('/api/agents/' + agent.id); - OpenFangToast.success('Agent "' + agent.name + '" stopped'); + await OMTAEAPI.del('/api/agents/' + agent.id); + OMTAEToast.success('Agent "' + agent.name + '" stopped'); self.showDetailModal = false; await Alpine.store('app').refreshAgents(); } catch(e) { - OpenFangToast.error('Failed to stop agent: ' + e.message); + OMTAEToast.error('Failed to stop agent: ' + e.message); } }); }, - // Issue #1163: uninstall an agent (kill + remove ~/.openfang/agents//). + // Issue #1163: uninstall an agent (kill + remove ~/.omtae/agents//). uninstallAgent(agent) { var self = this; - OpenFangToast.confirm( + OMTAEToast.confirm( 'Uninstall Agent', 'Uninstall agent "' + agent.name + '"? This stops the agent AND deletes its files from your workspace. This cannot be undone.', async function() { try { - var res = await OpenFangAPI.del('/api/agents/' + agent.id + '/uninstall'); + var res = await OMTAEAPI.del('/api/agents/' + agent.id + '/uninstall'); var msg = 'Agent "' + agent.name + '" uninstalled'; if (res && res.dir_removed === false) { msg += ' (no on-disk files found)'; } - OpenFangToast.success(msg); + OMTAEToast.success(msg); self.showDetailModal = false; await Alpine.store('app').refreshAgents(); } catch(e) { - OpenFangToast.error('Failed to uninstall agent: ' + e.message); + OMTAEToast.error('Failed to uninstall agent: ' + e.message); } } ); @@ -411,18 +451,18 @@ function agentsPage() { killAllAgents() { var list = this.filteredAgents; if (!list.length) return; - OpenFangToast.confirm('Stop All Agents', 'Stop ' + list.length + ' agent(s)? All agents will be shut down.', async function() { + OMTAEToast.confirm('Stop All Agents', 'Stop ' + list.length + ' agent(s)? All agents will be shut down.', async function() { var errors = []; for (var i = 0; i < list.length; i++) { try { - await OpenFangAPI.del('/api/agents/' + list[i].id); + await OMTAEAPI.del('/api/agents/' + list[i].id); } catch(e) { errors.push(list[i].name + ': ' + e.message); } } await Alpine.store('app').refreshAgents(); if (errors.length) { - OpenFangToast.error('Some agents failed to stop: ' + errors.join(', ')); + OMTAEToast.error('Some agents failed to stop: ' + errors.join(', ')); } else { - OpenFangToast.success(list.length + ' agent(s) stopped'); + OMTAEToast.success(list.length + ' agent(s) stopped'); } }); }, @@ -432,7 +472,7 @@ function agentsPage() { this.showSpawnModal = true; this.spawnStep = 1; this.spawnMode = 'wizard'; - this.spawnIdentity = { emoji: '', color: '#FF5C00', archetype: '' }; + this.spawnIdentity = { emoji: '', color: '#00F0FF', archetype: '' }; this.selectedPreset = ''; this.soulContent = ''; this.spawnForm.name = ''; @@ -444,8 +484,8 @@ function agentsPage() { this.spawnProvidersLoading = true; try { var results = await Promise.all([ - OpenFangAPI.get('/api/status').catch(function() { return {}; }), - OpenFangAPI.get('/api/providers').catch(function() { return { providers: [] }; }) + OMTAEAPI.get('/api/status').catch(function() { return {}; }), + OMTAEAPI.get('/api/providers').catch(function() { return { providers: [] }; }) ]); var status = results[0]; var provData = results[1]; @@ -460,7 +500,7 @@ function agentsPage() { nextStep() { if (this.spawnStep === 1 && !this.spawnForm.name.trim()) { - OpenFangToast.warn('Please enter an agent name'); + OMTAEToast.warn('Please enter an agent name'); return; } if (this.spawnStep < 5) this.spawnStep++; @@ -502,12 +542,12 @@ function agentsPage() { async setMode(agent, mode) { try { - await OpenFangAPI.put('/api/agents/' + agent.id + '/mode', { mode: mode }); + await OMTAEAPI.put('/api/agents/' + agent.id + '/mode', { mode: mode }); agent.mode = mode; - OpenFangToast.success('Mode set to ' + mode); + OMTAEToast.success('Mode set to ' + mode); await Alpine.store('app').refreshAgents(); } catch(e) { - OpenFangToast.error('Failed to set mode: ' + e.message); + OMTAEToast.error('Failed to set mode: ' + e.message); } }, @@ -516,12 +556,12 @@ function agentsPage() { var toml = this.spawnMode === 'wizard' ? this.generateToml() : this.spawnToml; if (!toml.trim()) { this.spawning = false; - OpenFangToast.warn('Manifest is empty \u2014 enter agent config first'); + OMTAEToast.warn('Manifest is empty \u2014 enter agent config first'); return; } try { - var res = await OpenFangAPI.post('/api/agents', { manifest_toml: toml }); + var res = await OMTAEAPI.post('/api/agents', { manifest_toml: toml }); if (res.agent_id) { // Post-spawn: update identity + write SOUL.md if personality preset selected var patchBody = {}; @@ -531,24 +571,24 @@ function agentsPage() { if (this.selectedPreset) patchBody.vibe = this.selectedPreset; if (Object.keys(patchBody).length) { - OpenFangAPI.patch('/api/agents/' + res.agent_id + '/config', patchBody).catch(function(e) { console.warn('Post-spawn config patch failed:', e.message); }); + OMTAEAPI.patch('/api/agents/' + res.agent_id + '/config', patchBody).catch(function(e) { console.warn('Post-spawn config patch failed:', e.message); }); } if (this.soulContent.trim()) { - OpenFangAPI.put('/api/agents/' + res.agent_id + '/files/SOUL.md', { content: '# Soul\n' + this.soulContent }).catch(function(e) { console.warn('SOUL.md write failed:', e.message); }); + OMTAEAPI.put('/api/agents/' + res.agent_id + '/files/SOUL.md', { content: '# Soul\n' + this.soulContent }).catch(function(e) { console.warn('SOUL.md write failed:', e.message); }); } this.showSpawnModal = false; this.spawnForm.name = ''; this.spawnToml = ''; this.spawnStep = 1; - OpenFangToast.success('Agent "' + (res.name || 'new') + '" spawned'); + OMTAEToast.success('Agent "' + (res.name || 'new') + '" spawned'); await Alpine.store('app').refreshAgents(); this.chatWithAgent({ id: res.agent_id, name: res.name, model_provider: '?', model_name: '?' }); } else { - OpenFangToast.error('Spawn failed: ' + (res.error || 'Unknown error')); + OMTAEToast.error('Spawn failed: ' + (res.error || 'Unknown error')); } } catch(e) { - OpenFangToast.error('Failed to spawn agent: ' + e.message); + OMTAEToast.error('Failed to spawn agent: ' + e.message); } this.spawning = false; }, @@ -558,11 +598,11 @@ function agentsPage() { if (!this.detailAgent) return; this.filesLoading = true; try { - var data = await OpenFangAPI.get('/api/agents/' + this.detailAgent.id + '/files'); + var data = await OMTAEAPI.get('/api/agents/' + this.detailAgent.id + '/files'); this.agentFiles = data.files || []; } catch(e) { this.agentFiles = []; - OpenFangToast.error('Failed to load files: ' + e.message); + OMTAEToast.error('Failed to load files: ' + e.message); } this.filesLoading = false; }, @@ -575,11 +615,11 @@ function agentsPage() { return; } try { - var data = await OpenFangAPI.get('/api/agents/' + this.detailAgent.id + '/files/' + encodeURIComponent(file.name)); + var data = await OMTAEAPI.get('/api/agents/' + this.detailAgent.id + '/files/' + encodeURIComponent(file.name)); this.editingFile = file.name; this.fileContent = data.content || ''; } catch(e) { - OpenFangToast.error('Failed to read file: ' + e.message); + OMTAEToast.error('Failed to read file: ' + e.message); } }, @@ -587,11 +627,11 @@ function agentsPage() { if (!this.editingFile || !this.detailAgent) return; this.fileSaving = true; try { - await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/files/' + encodeURIComponent(this.editingFile), { content: this.fileContent }); - OpenFangToast.success(this.editingFile + ' saved'); + await OMTAEAPI.put('/api/agents/' + this.detailAgent.id + '/files/' + encodeURIComponent(this.editingFile), { content: this.fileContent }); + OMTAEToast.success(this.editingFile + ' saved'); await this.loadAgentFiles(); } catch(e) { - OpenFangToast.error('Failed to save file: ' + e.message); + OMTAEToast.error('Failed to save file: ' + e.message); } this.fileSaving = false; }, @@ -606,11 +646,11 @@ function agentsPage() { if (!this.detailAgent) return; this.configSaving = true; try { - await OpenFangAPI.patch('/api/agents/' + this.detailAgent.id + '/config', this.configForm); - OpenFangToast.success('Config updated'); + await OMTAEAPI.patch('/api/agents/' + this.detailAgent.id + '/config', this.configForm); + OMTAEToast.success('Config updated'); await Alpine.store('app').refreshAgents(); } catch(e) { - OpenFangToast.error('Failed to save config: ' + e.message); + OMTAEToast.error('Failed to save config: ' + e.message); } this.configSaving = false; }, @@ -619,14 +659,14 @@ function agentsPage() { async cloneAgent(agent) { var newName = (agent.name || 'agent') + '-copy'; try { - var res = await OpenFangAPI.post('/api/agents/' + agent.id + '/clone', { new_name: newName }); + var res = await OMTAEAPI.post('/api/agents/' + agent.id + '/clone', { new_name: newName }); if (res.agent_id) { - OpenFangToast.success('Cloned as "' + res.name + '"'); + OMTAEToast.success('Cloned as "' + res.name + '"'); await Alpine.store('app').refreshAgents(); this.showDetailModal = false; } } catch(e) { - OpenFangToast.error('Clone failed: ' + e.message); + OMTAEToast.error('Clone failed: ' + e.message); } }, @@ -636,31 +676,31 @@ function agentsPage() { var manifestToml = template.manifest_toml; if (!manifestToml) { // If template doesn't have manifest_toml, fetch it from the API - var data = await OpenFangAPI.get('/api/templates/' + encodeURIComponent(template.name)); + var data = await OMTAEAPI.get('/api/templates/' + encodeURIComponent(template.name)); manifestToml = data.manifest_toml; } if (manifestToml) { - var res = await OpenFangAPI.post('/api/agents', { manifest_toml: manifestToml }); + var res = await OMTAEAPI.post('/api/agents', { manifest_toml: manifestToml }); if (res.agent_id) { - OpenFangToast.success('Agent "' + (res.name || template.name) + '" spawned from template'); + OMTAEToast.success('Agent "' + (res.name || template.name) + '" spawned from template'); await Alpine.store('app').refreshAgents(); this.chatWithAgent({ id: res.agent_id, name: res.name || template.name, model_provider: '?', model_name: '?' }); } } } catch(e) { - OpenFangToast.error('Failed to spawn from template: ' + e.message); + OMTAEToast.error('Failed to spawn from template: ' + e.message); } }, // ── Clear agent history ── async clearHistory(agent) { var self = this; - OpenFangToast.confirm('Clear History', 'Clear all conversation history for "' + agent.name + '"? This cannot be undone.', async function() { + OMTAEToast.confirm('Clear History', 'Clear all conversation history for "' + agent.name + '"? This cannot be undone.', async function() { try { - await OpenFangAPI.del('/api/agents/' + agent.id + '/history'); - OpenFangToast.success('History cleared for "' + agent.name + '"'); + await OMTAEAPI.del('/api/agents/' + agent.id + '/history'); + OMTAEToast.success('History cleared for "' + agent.name + '"'); } catch(e) { - OpenFangToast.error('Failed to clear history: ' + e.message); + OMTAEToast.error('Failed to clear history: ' + e.message); } }); }, @@ -670,9 +710,9 @@ function agentsPage() { if (!this.detailAgent || !this.newModelValue.trim()) return; this.modelSaving = true; try { - var resp = await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/model', { model: this.newModelValue.trim() }); + var resp = await OMTAEAPI.put('/api/agents/' + this.detailAgent.id + '/model', { model: this.newModelValue.trim() }); var providerInfo = (resp && resp.provider) ? ' (provider: ' + resp.provider + ')' : ''; - OpenFangToast.success('Model changed' + providerInfo + ' (memory reset)'); + OMTAEToast.success('Model changed' + providerInfo + ' (memory reset)'); this.editingModel = false; await Alpine.store('app').refreshAgents(); // Refresh detailAgent @@ -681,7 +721,7 @@ function agentsPage() { if (agents[i].id === this.detailAgent.id) { this.detailAgent = agents[i]; break; } } } catch(e) { - OpenFangToast.error('Failed to change model: ' + e.message); + OMTAEToast.error('Failed to change model: ' + e.message); } this.modelSaving = false; }, @@ -692,8 +732,8 @@ function agentsPage() { this.modelSaving = true; try { var combined = this.newProviderValue.trim() + '/' + this.detailAgent.model_name; - var resp = await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/model', { model: combined }); - OpenFangToast.success('Provider changed to ' + (resp && resp.provider ? resp.provider : this.newProviderValue.trim())); + var resp = await OMTAEAPI.put('/api/agents/' + this.detailAgent.id + '/model', { model: combined }); + OMTAEToast.success('Provider changed to ' + (resp && resp.provider ? resp.provider : this.newProviderValue.trim())); this.editingProvider = false; await Alpine.store('app').refreshAgents(); var agents = Alpine.store('app').agents; @@ -701,7 +741,7 @@ function agentsPage() { if (agents[i].id === this.detailAgent.id) { this.detailAgent = agents[i]; break; } } } catch(e) { - OpenFangToast.error('Failed to change provider: ' + e.message); + OMTAEToast.error('Failed to change provider: ' + e.message); } this.modelSaving = false; }, @@ -715,12 +755,12 @@ function agentsPage() { if (!this.detailAgent._fallbacks) this.detailAgent._fallbacks = []; this.detailAgent._fallbacks.push({ provider: provider, model: model }); try { - await OpenFangAPI.patch('/api/agents/' + this.detailAgent.id + '/config', { + await OMTAEAPI.patch('/api/agents/' + this.detailAgent.id + '/config', { fallback_models: this.detailAgent._fallbacks }); - OpenFangToast.success('Fallback added: ' + provider + '/' + model); + OMTAEToast.success('Fallback added: ' + provider + '/' + model); } catch(e) { - OpenFangToast.error('Failed to save fallbacks: ' + e.message); + OMTAEToast.error('Failed to save fallbacks: ' + e.message); this.detailAgent._fallbacks.pop(); } this.editingFallback = false; @@ -731,12 +771,12 @@ function agentsPage() { if (!this.detailAgent || !this.detailAgent._fallbacks) return; var removed = this.detailAgent._fallbacks.splice(idx, 1); try { - await OpenFangAPI.patch('/api/agents/' + this.detailAgent.id + '/config', { + await OMTAEAPI.patch('/api/agents/' + this.detailAgent.id + '/config', { fallback_models: this.detailAgent._fallbacks }); - OpenFangToast.success('Fallback removed'); + OMTAEToast.success('Fallback removed'); } catch(e) { - OpenFangToast.error('Failed to save fallbacks: ' + e.message); + OMTAEToast.error('Failed to save fallbacks: ' + e.message); this.detailAgent._fallbacks.splice(idx, 0, removed[0]); } }, @@ -746,7 +786,7 @@ function agentsPage() { if (!this.detailAgent) return; this.toolFiltersLoading = true; try { - this.toolFilters = await OpenFangAPI.get('/api/agents/' + this.detailAgent.id + '/tools'); + this.toolFilters = await OMTAEAPI.get('/api/agents/' + this.detailAgent.id + '/tools'); } catch(e) { this.toolFilters = { tool_allowlist: [], tool_blocklist: [] }; } @@ -784,9 +824,9 @@ function agentsPage() { async saveToolFilters() { if (!this.detailAgent) return; try { - await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/tools', this.toolFilters); + await OMTAEAPI.put('/api/agents/' + this.detailAgent.id + '/tools', this.toolFilters); } catch(e) { - OpenFangToast.error('Failed to update tool filters: ' + e.message); + OMTAEToast.error('Failed to update tool filters: ' + e.message); } }, @@ -799,14 +839,14 @@ function agentsPage() { toml += 'system_prompt = """\n' + tomlMultilineEscape(t.system_prompt) + '\n"""\n'; try { - var res = await OpenFangAPI.post('/api/agents', { manifest_toml: toml }); + var res = await OMTAEAPI.post('/api/agents', { manifest_toml: toml }); if (res.agent_id) { - OpenFangToast.success('Agent "' + t.name + '" spawned'); + OMTAEToast.success('Agent "' + t.name + '" spawned'); await Alpine.store('app').refreshAgents(); this.chatWithAgent({ id: res.agent_id, name: t.name, model_provider: t.provider, model_name: t.model }); } } catch(e) { - OpenFangToast.error('Failed to spawn agent: ' + e.message); + OMTAEToast.error('Failed to spawn agent: ' + e.message); } } }; diff --git a/crates/openfang-api/static/js/pages/approvals.js b/crates/openfang-api/static/js/pages/approvals.js index 52426a5b85..b3f9e6b427 100644 --- a/crates/openfang-api/static/js/pages/approvals.js +++ b/crates/openfang-api/static/js/pages/approvals.js @@ -1,4 +1,4 @@ -// OpenFang Approvals Page — Execution approval queue for sensitive agent actions +// OMTAE Approvals Page — Execution approval queue for sensitive agent actions 'use strict'; function approvalsPage() { @@ -38,7 +38,7 @@ function approvalsPage() { this.loading = true; this.loadError = ''; try { - var data = await OpenFangAPI.get('/api/approvals'); + var data = await OMTAEAPI.get('/api/approvals'); this.approvals = data.approvals || []; } catch(e) { this.loadError = e.message || 'Could not load approvals.'; @@ -48,23 +48,23 @@ function approvalsPage() { async approve(id) { try { - await OpenFangAPI.post('/api/approvals/' + id + '/approve', {}); - OpenFangToast.success('Approved'); + await OMTAEAPI.post('/api/approvals/' + id + '/approve', {}); + OMTAEToast.success('Approved'); await this.loadData(); } catch(e) { - OpenFangToast.error(e.message); + OMTAEToast.error(e.message); } }, async reject(id) { var self = this; - OpenFangToast.confirm('Reject Action', 'Are you sure you want to reject this action?', async function() { + OMTAEToast.confirm('Reject Action', 'Are you sure you want to reject this action?', async function() { try { - await OpenFangAPI.post('/api/approvals/' + id + '/reject', {}); - OpenFangToast.success('Rejected'); + await OMTAEAPI.post('/api/approvals/' + id + '/reject', {}); + OMTAEToast.success('Rejected'); await self.loadData(); } catch(e) { - OpenFangToast.error(e.message); + OMTAEToast.error(e.message); } }); }, diff --git a/crates/openfang-api/static/js/pages/brain.js b/crates/openfang-api/static/js/pages/brain.js new file mode 100644 index 0000000000..593af5582b --- /dev/null +++ b/crates/openfang-api/static/js/pages/brain.js @@ -0,0 +1,106 @@ +// OMTAE Brain Page — Obsidian vault browser (leads, skiptrace reports) +'use strict'; + +function brainPage() { + return { + status: {}, + currentPath: '', + entries: [], + recentLeads: [], + selectedFile: null, + fileContent: '', + fileMeta: {}, + loading: true, + fileLoading: false, + loadError: '', + pathStack: [], + + async loadBrain() { + this.loading = true; + this.loadError = ''; + try { + var status = await OMTAEAPI.get('/api/brain/status'); + this.status = status || {}; + await this.loadDirectory(''); + } catch (e) { + this.loadError = e.message || 'Could not load brain vault.'; + } + this.loading = false; + }, + + async loadDirectory(path) { + var q = path ? ('?path=' + encodeURIComponent(path)) : ''; + var data = await OMTAEAPI.get('/api/brain/list' + q); + this.currentPath = data.path || path || ''; + this.entries = data.entries || []; + this.recentLeads = data.recent_leads || []; + if (data.vault_path) { + this.status.vault_path = data.vault_path; + this.status.vault_name = data.vault_name; + } + this.pathStack = this.currentPath ? this.currentPath.split('/') : []; + }, + + async openEntry(entry) { + if (!entry) return; + if (entry.kind === 'dir') { + this.selectedFile = null; + this.fileContent = ''; + await this.loadDirectory(entry.path); + return; + } + await this.openFile(entry.path); + }, + + async openFile(path) { + this.fileLoading = true; + this.selectedFile = path; + try { + var data = await OMTAEAPI.get('/api/brain/file?path=' + encodeURIComponent(path)); + this.fileContent = data.content || ''; + this.fileMeta = data; + } catch (e) { + this.fileContent = ''; + if (typeof OMTAEToast !== 'undefined') OMTAEToast.error(e.message || 'Could not open file'); + } + this.fileLoading = false; + }, + + async goUp() { + if (!this.currentPath) return; + var parts = this.currentPath.split('/'); + parts.pop(); + await this.loadDirectory(parts.join('/')); + }, + + async goRoot() { + this.selectedFile = null; + this.fileContent = ''; + await this.loadDirectory(''); + }, + + async goToCrumb(index) { + var parts = this.pathStack.slice(0, index + 1); + await this.loadDirectory(parts.join('/')); + }, + + obsidianLink(path) { + var vault = (this.status && this.status.vault_name) || 'omtae-brain'; + var rel = path || this.selectedFile || ''; + return 'obsidian://open?vault=' + encodeURIComponent(vault) + '&file=' + encodeURIComponent(rel); + }, + + renderPreview(text) { + return typeof renderMarkdown === 'function' ? renderMarkdown(text) : escapeHtml(text); + }, + + formatModified(entry) { + if (!entry || !entry.modified) return ''; + try { + return new Date(entry.modified).toLocaleString(); + } catch (e) { + return entry.modified; + } + } + }; +} diff --git a/crates/openfang-api/static/js/pages/channels.js b/crates/openfang-api/static/js/pages/channels.js index 84568e7e54..59d9061243 100644 --- a/crates/openfang-api/static/js/pages/channels.js +++ b/crates/openfang-api/static/js/pages/channels.js @@ -1,4 +1,4 @@ -// OpenFang Channels Page — OpenClaw-style setup UX with QR code support +// OMTAE Channels Page — OpenClaw-style setup UX with QR code support 'use strict'; function channelsPage() { @@ -89,7 +89,7 @@ function channelsPage() { this.loading = true; this.loadError = ''; try { - var data = await OpenFangAPI.get('/api/channels'); + var data = await OMTAEAPI.get('/api/channels'); this.allChannels = (data.channels || []).map(function(ch) { ch.connected = ch.configured && ch.has_token; return ch; @@ -111,7 +111,7 @@ function channelsPage() { async refreshStatus() { try { - var data = await OpenFangAPI.get('/api/channels'); + var data = await OMTAEAPI.get('/api/channels'); var byName = {}; (data.channels || []).forEach(function(ch) { byName[ch.name] = ch; }); this.allChannels.forEach(function(c) { @@ -178,7 +178,7 @@ function channelsPage() { this.qr.connected = false; this.qr.expired = false; try { - var result = await OpenFangAPI.post('/api/channels/whatsapp/qr/start', {}); + var result = await OMTAEAPI.post('/api/channels/whatsapp/qr/start', {}); this.qr.available = result.available || false; this.qr.dataUrl = result.qr_data_url || ''; this.qr.sessionId = result.session_id || ''; @@ -189,7 +189,7 @@ function channelsPage() { this.pollQR(); } if (this.qr.connected) { - OpenFangToast.success('WhatsApp connected!'); + OMTAEToast.success('WhatsApp connected!'); await this.refreshStatus(); } } catch(e) { @@ -203,13 +203,13 @@ function channelsPage() { if (this.qrPollTimer) clearInterval(this.qrPollTimer); this.qrPollTimer = setInterval(async function() { try { - var result = await OpenFangAPI.get('/api/channels/whatsapp/qr/status?session_id=' + encodeURIComponent(self.qr.sessionId)); + var result = await OMTAEAPI.get('/api/channels/whatsapp/qr/status?session_id=' + encodeURIComponent(self.qr.sessionId)); if (result.connected) { clearInterval(self.qrPollTimer); self.qrPollTimer = null; self.qr.connected = true; self.qr.message = result.message || 'Connected!'; - OpenFangToast.success('WhatsApp linked successfully!'); + OMTAEToast.success('WhatsApp linked successfully!'); await self.refreshStatus(); } else if (result.expired) { clearInterval(self.qrPollTimer); @@ -230,26 +230,26 @@ function channelsPage() { var name = this.setupModal.name; this.configuring = true; try { - await OpenFangAPI.post('/api/channels/' + name + '/configure', { + await OMTAEAPI.post('/api/channels/' + name + '/configure', { fields: this.formValues }); this.setupStep = 2; // Auto-test after save try { - var testResult = await OpenFangAPI.post('/api/channels/' + name + '/test', {}); + var testResult = await OMTAEAPI.post('/api/channels/' + name + '/test', {}); if (testResult.status === 'ok') { this.testPassed = true; this.setupStep = 3; - OpenFangToast.success(this.setupModal.display_name + ' activated!'); + OMTAEToast.success(this.setupModal.display_name + ' activated!'); } else { - OpenFangToast.success(this.setupModal.display_name + ' saved. ' + (testResult.message || '')); + OMTAEToast.success(this.setupModal.display_name + ' saved. ' + (testResult.message || '')); } } catch(te) { - OpenFangToast.success(this.setupModal.display_name + ' saved. Test to verify connection.'); + OMTAEToast.success(this.setupModal.display_name + ' saved. Test to verify connection.'); } await this.refreshStatus(); } catch(e) { - OpenFangToast.error('Failed: ' + (e.message || 'Unknown error')); + OMTAEToast.error('Failed: ' + (e.message || 'Unknown error')); } this.configuring = false; }, @@ -259,14 +259,14 @@ function channelsPage() { var name = this.setupModal.name; var displayName = this.setupModal.display_name; var self = this; - OpenFangToast.confirm('Remove Channel', 'Remove ' + displayName + ' configuration? This will deactivate the channel.', async function() { + OMTAEToast.confirm('Remove Channel', 'Remove ' + displayName + ' configuration? This will deactivate the channel.', async function() { try { - await OpenFangAPI.delete('/api/channels/' + name + '/configure'); - OpenFangToast.success(displayName + ' removed and deactivated.'); + await OMTAEAPI.delete('/api/channels/' + name + '/configure'); + OMTAEToast.success(displayName + ' removed and deactivated.'); await self.refreshStatus(); self.setupModal = null; } catch(e) { - OpenFangToast.error('Failed: ' + (e.message || 'Unknown error')); + OMTAEToast.error('Failed: ' + (e.message || 'Unknown error')); } }); }, @@ -276,16 +276,16 @@ function channelsPage() { var name = this.setupModal.name; this.testing[name] = true; try { - var result = await OpenFangAPI.post('/api/channels/' + name + '/test', {}); + var result = await OMTAEAPI.post('/api/channels/' + name + '/test', {}); if (result.status === 'ok') { this.testPassed = true; this.setupStep = 3; - OpenFangToast.success(result.message); + OMTAEToast.success(result.message); } else { - OpenFangToast.error(result.message); + OMTAEToast.error(result.message); } } catch(e) { - OpenFangToast.error('Test failed: ' + (e.message || 'Unknown error')); + OMTAEToast.error('Test failed: ' + (e.message || 'Unknown error')); } this.testing[name] = false; }, @@ -295,9 +295,9 @@ function channelsPage() { if (!tpl) return; try { await navigator.clipboard.writeText(tpl); - OpenFangToast.success('Copied to clipboard'); + OMTAEToast.success('Copied to clipboard'); } catch(e) { - OpenFangToast.error('Copy failed'); + OMTAEToast.error('Copy failed'); } }, diff --git a/crates/openfang-api/static/js/pages/chat.js b/crates/openfang-api/static/js/pages/chat.js index bee1be4ea7..f047ce43ab 100644 --- a/crates/openfang-api/static/js/pages/chat.js +++ b/crates/openfang-api/static/js/pages/chat.js @@ -1,6 +1,22 @@ -// OpenFang Chat Page — Agent chat with markdown + streaming +// OMTAE Chat Page — Agent chat with markdown + streaming 'use strict'; +var OF_PREFERRED_AGENTS = ['researcher', 'analyst', 'orchestrator', 'coder']; + +function ofPickPreferredAgent(agents) { + if (!agents || !agents.length) return null; + for (var i = 0; i < OF_PREFERRED_AGENTS.length; i++) { + for (var j = 0; j < agents.length; j++) { + if (agents[j] && agents[j].name === OF_PREFERRED_AGENTS[i]) return agents[j]; + } + } + return agents[0]; +} + +function ofIsPreferredAgent(agent) { + return agent && OF_PREFERRED_AGENTS.indexOf(agent.name) >= 0; +} + function chatPage() { var msgId = 0; return { @@ -8,6 +24,7 @@ function chatPage() { messages: [], inputText: '', sending: false, + agentInferencing: false, // true when background tick or WS typing without user send messageQueue: [], // Queue for messages sent while streaming thinkingMode: 'off', // 'off' | 'on' | 'stream' _wsAgent: null, @@ -42,6 +59,7 @@ function chatPage() { modelSwitching: false, _modelCache: null, _modelCacheTime: 0, + _defaultModelName: 'Qwen2.5-Coder-32B-Instruct-AWQ', slashCommands: [], // Loaded dynamically with i18n in init() _slashCommandsLoaded: false, tokenCount: 0, @@ -91,10 +109,19 @@ function chatPage() { }, get modelDisplayName() { - if (!this.currentAgent) return ''; - var name = this.currentAgent.model_name || ''; + var name = ''; + if (this.currentAgent) { + name = this.currentAgent.model_name || ''; + var prov = (this.currentAgent.model_provider || '').toLowerCase(); + if (prov && prov !== 'vllm' && prov !== 'default') { + name = this._defaultModelName || name; + } + } else { + name = this._defaultModelName || ''; + } + if (!name) return this._defaultModelName || ''; var short = name.replace(/-\d{8}$/, ''); - return short.length > 24 ? short.substring(0, 22) + '\u2026' : short; + return short.length > 28 ? short.substring(0, 26) + '\u2026' : short; }, get switcherProviders() { @@ -143,6 +170,13 @@ function chatPage() { // Fetch dynamic commands from server this.fetchCommands(); + // Load default model from daemon config (vLLM on Jay's desk) + OMTAEAPI.get('/api/config').then(function(cfg) { + if (cfg && cfg.default_model && cfg.default_model.model) { + self._defaultModelName = cfg.default_model.model; + } + }).catch(function() { /* silent */ }); + // Observe DOM for new messages and render LaTeX this._latexObserver = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { @@ -217,6 +251,22 @@ function chatPage() { if (!self.currentAgent && agents && agents.length) { self._restoreActiveAgent(); } + // Sync inferencing / schedule flags for the active agent chip + if (self.currentAgent && agents && agents.length) { + for (var i = 0; i < agents.length; i++) { + if (agents[i].id === self.currentAgent.id) { + self.currentAgent.is_inferencing = agents[i].is_inferencing; + self.currentAgent.schedule = agents[i].schedule; + self.currentAgent.background_paused = agents[i].background_paused; + if (!agents[i].is_inferencing && !self.sending) { + self.agentInferencing = false; + } else if (agents[i].is_inferencing) { + self.agentInferencing = true; + } + break; + } + } + } }); // Watch for slash commands + model autocomplete @@ -226,7 +276,7 @@ function chatPage() { self.showSlashMenu = false; self.modelPickerFilter = modelMatch[1].toLowerCase(); if (!self.modelPickerList.length) { - OpenFangAPI.get('/api/models').then(function(data) { + OMTAEAPI.get('/api/models').then(function(data) { self.modelPickerList = (data.models || []).filter(function(m) { return m.available; }); self.showModelPicker = true; self.modelPickerIdx = 0; @@ -275,7 +325,7 @@ function chatPage() { }); return; } - OpenFangAPI.get('/api/models').then(function(data) { + OMTAEAPI.get('/api/models').then(function(data) { var models = (data.models || []).filter(function(m) { return m.available; }); self._modelCache = models; self._modelCacheTime = Date.now(); @@ -289,7 +339,7 @@ function chatPage() { if (el) el.focus(); }); }).catch(function(e) { - OpenFangToast.error('Failed to load models: ' + e.message); + OMTAEToast.error('Failed to load models: ' + e.message); }); }, @@ -299,15 +349,15 @@ function chatPage() { var self = this; this.modelSwitching = true; var t = typeof window.t === 'function' ? window.t : function(s) { return s; }; - OpenFangAPI.put('/api/agents/' + this.currentAgent.id + '/model', { model: model.id }).then(function(resp) { + OMTAEAPI.put('/api/agents/' + this.currentAgent.id + '/model', { model: model.id }).then(function(resp) { // Use server-resolved model/provider to stay in sync (fixes #387/#466) self.currentAgent.model_name = (resp && resp.model) || model.id; self.currentAgent.model_provider = (resp && resp.provider) || model.provider; - OpenFangToast.success(t('chat.model_switched') + ' ' + (model.display_name || model.id)); + OMTAEToast.success(t('chat.model_switched') + ' ' + (model.display_name || model.id)); self.showModelSwitcher = false; self.modelSwitching = false; }).catch(function(e) { - OpenFangToast.error(t('chat.model_switch_failed') + ': ' + e.message); + OMTAEToast.error(t('chat.model_switch_failed') + ': ' + e.message); self.modelSwitching = false; }); }, @@ -343,7 +393,7 @@ function chatPage() { // the help panel and autocomplete stay in sync with the backend registry. fetchCommands: function() { var self = this; - OpenFangAPI.get('/api/commands?surface=web').then(function(data) { + OMTAEAPI.get('/api/commands?surface=web').then(function(data) { var cmds = (data && data.commands) || []; if (!cmds.length) return; self.slashCommands = cmds.map(function(c) { @@ -418,15 +468,28 @@ function chatPage() { return lines.join('\n'); }, + showStopButton: function() { + return this.sending || this.agentInferencing || + (this.currentAgent && this.currentAgent.is_inferencing); + }, + + scheduleBadge: function() { + if (!this.currentAgent) return ''; + if (this.currentAgent.background_paused) return 'paused'; + return this.currentAgent.schedule || 'reactive'; + }, + // Clear any stuck typing indicator after 120s _resetTypingTimeout: function() { var self = this; if (self._typingTimeout) clearTimeout(self._typingTimeout); self._typingTimeout = setTimeout(function() { - // Auto-clear stuck typing indicators + // Auto-clear stuck typing / GENERATING when server state is stale self.messages = self.messages.filter(function(m) { return !m.thinking; }); self.sending = false; - }, 120000); + self.agentInferencing = false; + if (self.currentAgent) self.currentAgent.is_inferencing = false; + }, 180000); }, _clearTypingTimeout: function() { @@ -451,28 +514,34 @@ function chatPage() { break; case '/new': if (self.currentAgent) { - OpenFangAPI.post('/api/agents/' + self.currentAgent.id + '/session/reset', {}).then(function() { + OMTAEAPI.post('/api/agents/' + self.currentAgent.id + '/session/reset', {}).then(function() { self.messages = []; - OpenFangToast.success('Session reset'); - }).catch(function(e) { OpenFangToast.error('Reset failed: ' + e.message); }); + OMTAEToast.success('Session reset'); + }).catch(function(e) { OMTAEToast.error('Reset failed: ' + e.message); }); } break; case '/compact': if (self.currentAgent) { self.messages.push({ id: ++msgId, role: 'system', text: 'Compacting session...', meta: '', tools: [] }); - OpenFangAPI.post('/api/agents/' + self.currentAgent.id + '/session/compact', {}).then(function(res) { + OMTAEAPI.post('/api/agents/' + self.currentAgent.id + '/session/compact', {}).then(function(res) { self.messages.push({ id: ++msgId, role: 'system', text: res.message || 'Compaction complete', meta: '', tools: [] }); self.scrollToBottom(); - }).catch(function(e) { OpenFangToast.error('Compaction failed: ' + e.message); }); + }).catch(function(e) { OMTAEToast.error('Compaction failed: ' + e.message); }); } break; case '/stop': if (self.currentAgent) { - OpenFangAPI.post('/api/agents/' + self.currentAgent.id + '/stop', {}).then(function(res) { - self.messages.push({ id: ++msgId, role: 'system', text: res.message || 'Run cancelled', meta: '', tools: [] }); + OMTAEAPI.post('/api/agents/' + self.currentAgent.id + '/stop', {}).then(function(res) { + var msg = res.message || 'Run cancelled'; + if (res.background_paused) { + msg += ' (continuous/periodic background ticks paused — restart daemon to resume)'; + } + self.messages.push({ id: ++msgId, role: 'system', text: msg, meta: '', tools: [] }); self.sending = false; + self.agentInferencing = false; + if (self.currentAgent) self.currentAgent.background_paused = !!res.background_paused; self.scrollToBottom(); - }).catch(function(e) { OpenFangToast.error('Stop failed: ' + e.message); }); + }).catch(function(e) { OMTAEToast.error('Stop failed: ' + e.message); }); } break; case '/usage': @@ -504,31 +573,31 @@ function chatPage() { break; case '/context': // Send via WS command - if (self.currentAgent && OpenFangAPI.isWsConnected()) { - OpenFangAPI.wsSend({ type: 'command', command: 'context', args: '' }); + if (self.currentAgent && OMTAEAPI.isWsConnected()) { + OMTAEAPI.wsSend({ type: 'command', command: 'context', args: '' }); } else { - self.messages.push({ id: ++msgId, role: 'system', text: 'Not connected (' + (OpenFangAPI.getConnectionState ? OpenFangAPI.getConnectionState() : 'unknown') + '). Pick an agent or check that your session is still valid.', meta: '', tools: [] }); + self.messages.push({ id: ++msgId, role: 'system', text: 'Not connected (' + (OMTAEAPI.getConnectionState ? OMTAEAPI.getConnectionState() : 'unknown') + '). Pick an agent or check that your session is still valid.', meta: '', tools: [] }); self.scrollToBottom(); } break; case '/verbose': - if (self.currentAgent && OpenFangAPI.isWsConnected()) { - OpenFangAPI.wsSend({ type: 'command', command: 'verbose', args: cmdArgs }); + if (self.currentAgent && OMTAEAPI.isWsConnected()) { + OMTAEAPI.wsSend({ type: 'command', command: 'verbose', args: cmdArgs }); } else { - self.messages.push({ id: ++msgId, role: 'system', text: 'Not connected (' + (OpenFangAPI.getConnectionState ? OpenFangAPI.getConnectionState() : 'unknown') + '). Pick an agent or check that your session is still valid.', meta: '', tools: [] }); + self.messages.push({ id: ++msgId, role: 'system', text: 'Not connected (' + (OMTAEAPI.getConnectionState ? OMTAEAPI.getConnectionState() : 'unknown') + '). Pick an agent or check that your session is still valid.', meta: '', tools: [] }); self.scrollToBottom(); } break; case '/queue': - if (self.currentAgent && OpenFangAPI.isWsConnected()) { - OpenFangAPI.wsSend({ type: 'command', command: 'queue', args: '' }); + if (self.currentAgent && OMTAEAPI.isWsConnected()) { + OMTAEAPI.wsSend({ type: 'command', command: 'queue', args: '' }); } else { - self.messages.push({ id: ++msgId, role: 'system', text: 'Not connected (' + (OpenFangAPI.getConnectionState ? OpenFangAPI.getConnectionState() : 'unknown') + ').', meta: '', tools: [] }); + self.messages.push({ id: ++msgId, role: 'system', text: 'Not connected (' + (OMTAEAPI.getConnectionState ? OMTAEAPI.getConnectionState() : 'unknown') + ').', meta: '', tools: [] }); self.scrollToBottom(); } break; case '/status': - OpenFangAPI.get('/api/status').then(function(s) { + OMTAEAPI.get('/api/status').then(function(s) { self.messages.push({ id: ++msgId, role: 'system', text: '**System Status**\n- Agents: ' + (s.agent_count || 0) + '\n- Uptime: ' + (s.uptime_seconds || 0) + 's\n- Version: ' + (s.version || '?'), meta: '', tools: [] }); self.scrollToBottom(); }).catch(function() {}); @@ -536,7 +605,7 @@ function chatPage() { case '/model': if (self.currentAgent) { if (cmdArgs) { - OpenFangAPI.put('/api/agents/' + self.currentAgent.id + '/model', { model: cmdArgs }).then(function(resp) { + OMTAEAPI.put('/api/agents/' + self.currentAgent.id + '/model', { model: cmdArgs }).then(function(resp) { // Use server-resolved model/provider (fixes #387/#466) var resolvedModel = (resp && resp.model) || cmdArgs; var resolvedProvider = (resp && resp.provider) || ''; @@ -544,7 +613,7 @@ function chatPage() { if (resolvedProvider) { self.currentAgent.model_provider = resolvedProvider; } self.messages.push({ id: ++msgId, role: 'system', text: 'Model switched to: `' + resolvedModel + '`' + (resolvedProvider ? ' (provider: `' + resolvedProvider + '`)' : ''), meta: '', tools: [] }); self.scrollToBottom(); - }).catch(function(e) { OpenFangToast.error('Model switch failed: ' + e.message); }); + }).catch(function(e) { OMTAEToast.error('Model switch failed: ' + e.message); }); } else { self.messages.push({ id: ++msgId, role: 'system', text: '**Current Model**\n- Provider: `' + (self.currentAgent.model_provider || '?') + '`\n- Model: `' + (self.currentAgent.model_name || '?') + '`', meta: '', tools: [] }); self.scrollToBottom(); @@ -558,7 +627,7 @@ function chatPage() { self.messages = []; break; case '/exit': - OpenFangAPI.wsDisconnect(); + OMTAEAPI.wsDisconnect(); self._wsAgent = null; self.currentAgent = null; self.messages = []; @@ -566,7 +635,7 @@ function chatPage() { window.dispatchEvent(new Event('close-chat')); break; case '/budget': - OpenFangAPI.get('/api/budget').then(function(b) { + OMTAEAPI.get('/api/budget').then(function(b) { var fmt = function(v) { return v > 0 ? '$' + v.toFixed(2) : 'unlimited'; }; self.messages.push({ id: ++msgId, role: 'system', text: '**Budget Status**\n' + '- Hourly: $' + (b.hourly_spend||0).toFixed(4) + ' / ' + fmt(b.hourly_limit) + '\n' + @@ -576,7 +645,7 @@ function chatPage() { }).catch(function() {}); break; case '/peers': - OpenFangAPI.get('/api/network/status').then(function(ns) { + OMTAEAPI.get('/api/network/status').then(function(ns) { self.messages.push({ id: ++msgId, role: 'system', text: '**OFP Network**\n' + '- Status: ' + (ns.enabled ? 'Enabled' : 'Disabled') + '\n' + '- Connected peers: ' + (ns.connected_peers||0) + ' / ' + (ns.total_peers||0), meta: '', tools: [] }); @@ -584,7 +653,7 @@ function chatPage() { }).catch(function() {}); break; case '/a2a': - OpenFangAPI.get('/api/a2a/agents').then(function(res) { + OMTAEAPI.get('/api/a2a/agents').then(function(res) { var agents = res.agents || []; if (!agents.length) { self.messages.push({ id: ++msgId, role: 'system', text: 'No external A2A agents discovered.', meta: '', tools: [] }); @@ -602,14 +671,28 @@ function chatPage() { // refresh, so the WebSocket re-attaches to the same session and any // in-flight tool output streams back into the chat (#1179). _restoreActiveAgent: function() { + var agents = (Alpine.store('app') && Alpine.store('app').agents) || []; + if (!agents.length) return; + var storedId = null; try { storedId = localStorage.getItem('of-active-agent'); } catch(e) { /* ignore */ } - if (!storedId) return; - var agents = (Alpine.store('app') && Alpine.store('app').agents) || []; + var match = null; - for (var i = 0; i < agents.length; i++) { - if (agents[i] && agents[i].id === storedId) { match = agents[i]; break; } + if (storedId) { + for (var i = 0; i < agents.length; i++) { + if (agents[i] && agents[i].id === storedId) { match = agents[i]; break; } + } + } + + if (match && !ofIsPreferredAgent(match)) { + try { localStorage.removeItem('of-active-agent'); } catch(e) { /* ignore */ } + match = null; + } + + if (!match) { + match = ofPickPreferredAgent(agents); } + if (match) { this.selectAgent(match); } @@ -643,7 +726,7 @@ function chatPage() { async loadSession(agentId) { var self = this; try { - var data = await OpenFangAPI.get('/api/agents/' + agentId + '/session'); + var data = await OMTAEAPI.get('/api/agents/' + agentId + '/session'); if (data.messages && data.messages.length) { // Defense-in-depth (#935): never render system-role messages in the // conversation history view, even if the backend somehow returns @@ -682,7 +765,7 @@ function chatPage() { // Multi-session: load session list for current agent async loadSessions(agentId) { try { - var data = await OpenFangAPI.get('/api/agents/' + agentId + '/sessions'); + var data = await OMTAEAPI.get('/api/agents/' + agentId + '/sessions'); this.sessions = data.sessions || []; } catch(e) { this.sessions = []; } }, @@ -694,16 +777,16 @@ function chatPage() { var label = prompt(t('chat.session_name_prompt')); if (label === null) return; // cancelled try { - await OpenFangAPI.post('/api/agents/' + this.currentAgent.id + '/sessions', { + await OMTAEAPI.post('/api/agents/' + this.currentAgent.id + '/sessions', { label: label.trim() || undefined }); await this.loadSessions(this.currentAgent.id); await this.loadSession(this.currentAgent.id); this.messages = []; this.scrollToBottom(); - if (typeof OpenFangToast !== 'undefined') OpenFangToast.success(t('chat.session_created')); + if (typeof OMTAEToast !== 'undefined') OMTAEToast.success(t('chat.session_created')); } catch(e) { - if (typeof OpenFangToast !== 'undefined') OpenFangToast.error(t('chat.session_create_failed')); + if (typeof OMTAEToast !== 'undefined') OMTAEToast.error(t('chat.session_create_failed')); } }, @@ -711,7 +794,7 @@ function chatPage() { async switchSession(sessionId) { if (!this.currentAgent) return; try { - await OpenFangAPI.post('/api/agents/' + this.currentAgent.id + '/sessions/' + sessionId + '/switch', {}); + await OMTAEAPI.post('/api/agents/' + this.currentAgent.id + '/sessions/' + sessionId + '/switch', {}); this.messages = []; await this.loadSession(this.currentAgent.id); await this.loadSessions(this.currentAgent.id); @@ -719,7 +802,7 @@ function chatPage() { this._wsAgent = null; this.connectWs(this.currentAgent.id); } catch(e) { - if (typeof OpenFangToast !== 'undefined') OpenFangToast.error('Failed to switch session'); + if (typeof OMTAEToast !== 'undefined') OMTAEToast.error('Failed to switch session'); } }, @@ -728,7 +811,7 @@ function chatPage() { this._wsAgent = agentId; var self = this; - OpenFangAPI.wsConnect(agentId, { + OMTAEAPI.wsConnect(agentId, { onOpen: function() { Alpine.store('app').wsConnected = true; }, @@ -777,6 +860,7 @@ function chatPage() { // New typing lifecycle case 'typing': if (data.state === 'start') { + if (!this.sending) this.agentInferencing = true; if (!this.messages.length || !this.messages[this.messages.length - 1].thinking) { this.messages.push({ id: ++msgId, role: 'agent', text: 'Processing...', meta: '', thinking: true, streaming: true, tools: [] }); this.scrollToBottom(); @@ -791,6 +875,7 @@ function chatPage() { } this._resetTypingTimeout(); } else if (data.state === 'stop') { + this.agentInferencing = false; this._clearTypingTimeout(); } break; @@ -910,6 +995,9 @@ function chatPage() { lastMsg3.tools[ri].running = false; lastMsg3.tools[ri].result = data.result || ''; lastMsg3.tools[ri].is_error = !!data.is_error; + if (data.tool === 'agent_send') { + lastMsg3.tools[ri].expanded = true; + } // Extract image URLs from image_generate or browser_screenshot results if ((data.tool === 'image_generate' || data.tool === 'browser_screenshot') && !data.is_error) { try { @@ -975,6 +1063,7 @@ function chatPage() { } this.messages.push({ id: ++msgId, role: 'agent', text: finalText, meta: meta, tools: streamedTools, ts: Date.now() }); this.sending = false; + this.agentInferencing = false; this.tokenCount = 0; this.scrollToBottom(); var self3 = this; @@ -989,6 +1078,7 @@ function chatPage() { this._clearTypingTimeout(); this.messages = this.messages.filter(function(m) { return !m.thinking && !m.streaming; }); this.sending = false; + this.agentInferencing = false; this.tokenCount = 0; // No message bubble added — the agent was silent var selfSilent = this; @@ -1000,6 +1090,7 @@ function chatPage() { this.messages = this.messages.filter(function(m) { return !m.thinking && !m.streaming; }); this.messages.push({ id: ++msgId, role: 'system', text: 'Error: ' + data.content, meta: '', tools: [], ts: Date.now() }); this.sending = false; + this.agentInferencing = false; this.tokenCount = 0; this.scrollToBottom(); var self2 = this; @@ -1097,11 +1188,11 @@ function chatPage() { var att = this.attachments[i]; att.uploading = true; try { - var uploadRes = await OpenFangAPI.upload(this.currentAgent.id, att.file); + var uploadRes = await OMTAEAPI.upload(this.currentAgent.id, att.file); fileRefs.push('[File: ' + att.file.name + ']'); uploadedFiles.push({ file_id: uploadRes.file_id, filename: uploadRes.filename, content_type: uploadRes.content_type }); } catch(e) { - OpenFangToast.error('Failed to upload ' + att.file.name); + OMTAEToast.error('Failed to upload ' + att.file.name); fileRefs.push('[File: ' + att.file.name + ' (upload failed)]'); } att.uploading = false; @@ -1142,7 +1233,7 @@ function chatPage() { // Try WebSocket first var wsPayload = { type: 'message', content: finalText }; if (uploadedFiles && uploadedFiles.length) wsPayload.attachments = uploadedFiles; - if (OpenFangAPI.wsSend(wsPayload)) { + if (OMTAEAPI.wsSend(wsPayload)) { this.messages.push({ id: ++msgId, role: 'agent', text: '', meta: '', thinking: true, streaming: true, tools: [], ts: Date.now() }); this.scrollToBottom(); return; @@ -1150,8 +1241,8 @@ function chatPage() { // HTTP fallback var t = typeof window.t === 'function' ? window.t : function(s) { return s; }; - if (!OpenFangAPI.isWsConnected()) { - OpenFangToast.info(t('chat.using_http_mode')); + if (!OMTAEAPI.isWsConnected()) { + OMTAEToast.info(t('chat.using_http_mode')); } this.messages.push({ id: ++msgId, role: 'agent', text: '', meta: '', thinking: true, tools: [], ts: Date.now() }); this.scrollToBottom(); @@ -1159,7 +1250,7 @@ function chatPage() { try { var httpBody = { message: finalText }; if (uploadedFiles && uploadedFiles.length) httpBody.attachments = uploadedFiles; - var res = await OpenFangAPI.post('/api/agents/' + this.currentAgent.id + '/message', httpBody); + var res = await OMTAEAPI.post('/api/agents/' + this.currentAgent.id + '/message', httpBody); this.messages = this.messages.filter(function(m) { return !m.thinking; }); var httpMeta = (res.input_tokens || 0) + ' in / ' + (res.output_tokens || 0) + ' out'; if (res.cost_usd != null) httpMeta += ' | $' + res.cost_usd.toFixed(4); @@ -1183,12 +1274,18 @@ function chatPage() { stopAgent: function() { if (!this.currentAgent) return; var self = this; - OpenFangAPI.post('/api/agents/' + this.currentAgent.id + '/stop', {}).then(function(res) { - self.messages.push({ id: ++msgId, role: 'system', text: res.message || 'Run cancelled', meta: '', tools: [], ts: Date.now() }); + OMTAEAPI.post('/api/agents/' + this.currentAgent.id + '/stop', {}).then(function(res) { + var msg = res.message || 'Run cancelled'; + if (res.background_paused) { + msg += ' (background schedule paused)'; + } + self.messages.push({ id: ++msgId, role: 'system', text: msg, meta: '', tools: [], ts: Date.now() }); self.sending = false; + self.agentInferencing = false; + if (self.currentAgent) self.currentAgent.background_paused = !!res.background_paused; self.scrollToBottom(); self.$nextTick(function() { self._processQueue(); }); - }).catch(function(e) { OpenFangToast.error('Stop failed: ' + e.message); }); + }).catch(function(e) { OMTAEToast.error('Stop failed: ' + e.message); }); }, killAgent() { @@ -1196,36 +1293,36 @@ function chatPage() { var self = this; var t = typeof window.t === 'function' ? window.t : function(s) { return s; }; var name = this.currentAgent.name; - OpenFangToast.confirm(t('chat.stop_agent_title'), t('chat.stop_agent_confirm') + ' "' + name + '"?', async function() { + OMTAEToast.confirm(t('chat.stop_agent_title'), t('chat.stop_agent_confirm') + ' "' + name + '"?', async function() { try { - await OpenFangAPI.del('/api/agents/' + self.currentAgent.id); - OpenFangAPI.wsDisconnect(); + await OMTAEAPI.del('/api/agents/' + self.currentAgent.id); + OMTAEAPI.wsDisconnect(); self._wsAgent = null; self.currentAgent = null; self.messages = []; try { localStorage.removeItem('of-active-agent'); } catch(e) { /* ignore */ } - OpenFangToast.success(t('chat.agent_stopped') + ' "' + name + '"'); + OMTAEToast.success(t('chat.agent_stopped') + ' "' + name + '"'); Alpine.store('app').refreshAgents(); } catch(e) { - OpenFangToast.error(t('chat.stop_agent_failed') + ': ' + e.message); + OMTAEToast.error(t('chat.stop_agent_failed') + ': ' + e.message); } }); }, - // Permanently uninstall the agent: kill + remove ~/.openfang/agents// + // Permanently uninstall the agent: kill + remove ~/.omtae/agents// // Issue #1163. uninstallAgent: function() { if (!this.currentAgent) return; var self = this; var name = this.currentAgent.name; var agentId = this.currentAgent.id; - OpenFangToast.confirm( + OMTAEToast.confirm( 'Uninstall Agent', 'Uninstall agent "' + name + '"? This stops the agent AND deletes its files from your workspace. This cannot be undone.', async function() { try { - var res = await OpenFangAPI.del('/api/agents/' + agentId + '/uninstall'); - OpenFangAPI.wsDisconnect(); + var res = await OMTAEAPI.del('/api/agents/' + agentId + '/uninstall'); + OMTAEAPI.wsDisconnect(); self._wsAgent = null; self.currentAgent = null; self.messages = []; @@ -1234,10 +1331,10 @@ function chatPage() { if (res && res.dir_removed === false) { msg += ' (no on-disk files found)'; } - OpenFangToast.success(msg); + OMTAEToast.success(msg); Alpine.store('app').refreshAgents(); } catch(e) { - OpenFangToast.error('Failed to uninstall agent: ' + e.message); + OMTAEToast.error('Failed to uninstall agent: ' + e.message); } } ); @@ -1263,7 +1360,7 @@ function chatPage() { for (var i = 0; i < files.length; i++) { var file = files[i]; if (file.size > 10 * 1024 * 1024) { - OpenFangToast.warn('File "' + file.name + '" exceeds 10MB limit'); + OMTAEToast.warn('File "' + file.name + '" exceeds 10MB limit'); continue; } var typeOk = allowed.indexOf(file.type) !== -1; @@ -1272,7 +1369,7 @@ function chatPage() { typeOk = allowedExts.indexOf(ext) !== -1 || file.type.startsWith('image/'); } if (!typeOk) { - OpenFangToast.warn('File type not supported: ' + file.name); + OMTAEToast.warn('File type not supported: ' + file.name); continue; } var preview = null; @@ -1351,7 +1448,7 @@ function chatPage() { this.recordingTime = 0; this._recordingTimer = setInterval(function() { self.recordingTime++; }, 1000); } catch(e) { - if (typeof OpenFangToast !== 'undefined') OpenFangToast.error('Microphone access denied'); + if (typeof OMTAEToast !== 'undefined') OMTAEToast.error('Microphone access denied'); } }, @@ -1378,7 +1475,7 @@ function chatPage() { // Upload audio file var ext = blob.type.includes('webm') ? 'webm' : blob.type.includes('ogg') ? 'ogg' : 'mp3'; var file = new File([blob], 'voice_' + Date.now() + '.' + ext, { type: blob.type }); - var upload = await OpenFangAPI.upload(this.currentAgent.id, file); + var upload = await OMTAEAPI.upload(this.currentAgent.id, file); // Remove the "Transcribing..." message this.messages = this.messages.filter(function(m) { return !m.thinking || m.role !== 'system'; }); @@ -1390,7 +1487,7 @@ function chatPage() { this._sendPayload(text, [upload], []); } catch(e) { this.messages = this.messages.filter(function(m) { return !m.thinking || m.role !== 'system'; }); - if (typeof OpenFangToast !== 'undefined') OpenFangToast.error('Failed to upload audio: ' + (e.message || 'unknown error')); + if (typeof OMTAEToast !== 'undefined') OMTAEToast.error('Failed to upload audio: ' + (e.message || 'unknown error')); } }, diff --git a/crates/openfang-api/static/js/pages/comms.js b/crates/openfang-api/static/js/pages/comms.js index 3e3ba92460..61ce69f477 100644 --- a/crates/openfang-api/static/js/pages/comms.js +++ b/crates/openfang-api/static/js/pages/comms.js @@ -1,4 +1,4 @@ -// OpenFang Comms Page — Agent topology & inter-agent communication feed +// OMTAE Comms Page — Agent topology & inter-agent communication feed 'use strict'; function commsPage() { @@ -24,8 +24,8 @@ function commsPage() { this.loadError = ''; try { var results = await Promise.all([ - OpenFangAPI.get('/api/comms/topology'), - OpenFangAPI.get('/api/comms/events?limit=200') + OMTAEAPI.get('/api/comms/topology'), + OMTAEAPI.get('/api/comms/events?limit=200') ]); this.topology = results[0] || { nodes: [], edges: [] }; this.events = results[1] || []; @@ -40,7 +40,7 @@ function commsPage() { if (this.sseSource) this.sseSource.close(); var self = this; var url = '/api/comms/events/stream'; - if (OpenFangAPI.apiKey) url += '?token=' + encodeURIComponent(OpenFangAPI.apiKey); + if (OMTAEAPI.apiKey) url += '?token=' + encodeURIComponent(OMTAEAPI.apiKey); this.sseSource = new EventSource(url); this.sseSource.onmessage = function(ev) { if (ev.data === 'ping') return; @@ -65,7 +65,7 @@ function commsPage() { async refreshTopology() { try { - this.topology = await OpenFangAPI.get('/api/comms/topology'); + this.topology = await OMTAEAPI.get('/api/comms/topology'); } catch(e) { /* silent */ } }, @@ -163,15 +163,15 @@ function commsPage() { if (!this.sendFrom || !this.sendTo || !this.sendMsg.trim()) return; this.sendLoading = true; try { - await OpenFangAPI.post('/api/comms/send', { + await OMTAEAPI.post('/api/comms/send', { from_agent_id: this.sendFrom, to_agent_id: this.sendTo, message: this.sendMsg }); - OpenFangToast.success('Message sent'); + OMTAEToast.success('Message sent'); this.showSendModal = false; } catch(e) { - OpenFangToast.error(e.message || 'Send failed'); + OMTAEToast.error(e.message || 'Send failed'); } this.sendLoading = false; }, @@ -189,11 +189,11 @@ function commsPage() { try { var body = { title: this.taskTitle, description: this.taskDesc }; if (this.taskAssign) body.assigned_to = this.taskAssign; - await OpenFangAPI.post('/api/comms/task', body); - OpenFangToast.success('Task posted'); + await OMTAEAPI.post('/api/comms/task', body); + OMTAEToast.success('Task posted'); this.showTaskModal = false; } catch(e) { - OpenFangToast.error(e.message || 'Task failed'); + OMTAEToast.error(e.message || 'Task failed'); } this.taskLoading = false; } diff --git a/crates/openfang-api/static/js/pages/hands.js b/crates/openfang-api/static/js/pages/hands.js index c52b3361eb..4d1fe3d716 100644 --- a/crates/openfang-api/static/js/pages/hands.js +++ b/crates/openfang-api/static/js/pages/hands.js @@ -1,4 +1,4 @@ -// OpenFang Hands Page — curated autonomous capability packages +// OMTAE Hands Page — curated autonomous capability packages 'use strict'; function handsPage() { @@ -42,7 +42,7 @@ function handsPage() { this.loading = true; this.loadError = ''; try { - var data = await OpenFangAPI.get('/api/hands'); + var data = await OMTAEAPI.get('/api/hands'); this.hands = data.hands || []; } catch(e) { this.hands = []; @@ -54,7 +54,7 @@ function handsPage() { async loadActive() { this.activeLoading = true; try { - var data = await OpenFangAPI.get('/api/hands/active'); + var data = await OMTAEAPI.get('/api/hands/active'); this.instances = (data.instances || []).map(function(i) { i._stats = null; return i; @@ -74,7 +74,7 @@ function handsPage() { async showDetail(handId) { try { - var data = await OpenFangAPI.get('/api/hands/' + handId); + var data = await OMTAEAPI.get('/api/hands/' + handId); this.detailHand = data; } catch(e) { for (var i = 0; i < this.hands.length; i++) { @@ -96,7 +96,7 @@ function handsPage() { this.setupLoading = true; this.setupWizard = null; try { - var data = await OpenFangAPI.get('/api/hands/' + handId); + var data = await OMTAEAPI.get('/api/hands/' + handId); // Pre-populate settings defaults this.settingsValues = {}; if (data.settings && data.settings.length > 0) { @@ -167,7 +167,7 @@ function handsPage() { }; try { - var data = await OpenFangAPI.post('/api/hands/' + handId + '/install-deps', {}); + var data = await OMTAEAPI.post('/api/hands/' + handId + '/install-deps', {}); var results = data.results || []; this.installProgress.results = results; this.installProgress.current = results.length; @@ -229,7 +229,7 @@ function handsPage() { if (!this.setupWizard) return; this.setupChecking = true; try { - var data = await OpenFangAPI.post('/api/hands/' + this.setupWizard.id + '/check-deps', {}); + var data = await OMTAEAPI.post('/api/hands/' + this.setupWizard.id + '/check-deps', {}); if (data.requirements && this.setupWizard.requirements) { for (var i = 0; i < this.setupWizard.requirements.length; i++) { var existing = this.setupWizard.requirements[i]; @@ -415,7 +415,7 @@ function handsPage() { if (name) { payload.instance_name = name; } - var data = await OpenFangAPI.post('/api/hands/' + handId + '/activate', payload); + var data = await OMTAEAPI.post('/api/hands/' + handId + '/activate', payload); var label = data.instance_name || data.agent_name || data.instance_id; this.showToast('Hand "' + handId + '" activated as ' + label); this.closeSetupWizard(); @@ -448,7 +448,7 @@ function handsPage() { async pauseHand(inst) { try { - await OpenFangAPI.post('/api/hands/instances/' + inst.instance_id + '/pause', {}); + await OMTAEAPI.post('/api/hands/instances/' + inst.instance_id + '/pause', {}); inst.status = 'Paused'; } catch(e) { this.showToast('Pause failed: ' + (e.message || 'unknown error')); @@ -457,7 +457,7 @@ function handsPage() { async resumeHand(inst) { try { - await OpenFangAPI.post('/api/hands/instances/' + inst.instance_id + '/resume', {}); + await OMTAEAPI.post('/api/hands/instances/' + inst.instance_id + '/resume', {}); inst.status = 'Active'; } catch(e) { this.showToast('Resume failed: ' + (e.message || 'unknown error')); @@ -467,20 +467,20 @@ function handsPage() { async deactivate(inst) { var self = this; var handName = inst.agent_name || inst.hand_id; - OpenFangToast.confirm('Deactivate Hand', 'Deactivate hand "' + handName + '"? This will kill its agent.', async function() { + OMTAEToast.confirm('Deactivate Hand', 'Deactivate hand "' + handName + '"? This will kill its agent.', async function() { try { - await OpenFangAPI.delete('/api/hands/instances/' + inst.instance_id); + await OMTAEAPI.delete('/api/hands/instances/' + inst.instance_id); self.instances = self.instances.filter(function(i) { return i.instance_id !== inst.instance_id; }); - OpenFangToast.success('Hand deactivated.'); + OMTAEToast.success('Hand deactivated.'); } catch(e) { - OpenFangToast.error('Deactivation failed: ' + (e.message || 'unknown error')); + OMTAEToast.error('Deactivation failed: ' + (e.message || 'unknown error')); } }); }, async loadStats(inst) { try { - var data = await OpenFangAPI.get('/api/hands/instances/' + inst.instance_id + '/stats'); + var data = await OMTAEAPI.get('/api/hands/instances/' + inst.instance_id + '/stats'); inst._stats = data.metrics || {}; } catch(e) { inst._stats = { 'Error': { value: e.message || 'Could not load stats', format: 'text' } }; @@ -541,7 +541,7 @@ function handsPage() { if (!this.browserViewer) return; var id = this.browserViewer.instance_id; try { - var data = await OpenFangAPI.get('/api/hands/instances/' + id + '/browser'); + var data = await OMTAEAPI.get('/api/hands/instances/' + id + '/browser'); if (data.active) { this.browserViewer.url = data.url || ''; this.browserViewer.title = data.title || ''; @@ -635,7 +635,7 @@ function handsPage() { // Fetch basic stats from the hand stats endpoint try { - var stats = await OpenFangAPI.get('/api/hands/instances/' + inst.instance_id + '/stats'); + var stats = await OMTAEAPI.get('/api/hands/instances/' + inst.instance_id + '/stats'); var m = stats.metrics || {}; if (m['Portfolio Value']) data.portfolio_value = this._metricVal(m['Portfolio Value']); if (m['Total P&L']) data.total_pnl = this._metricVal(m['Total P&L']); @@ -665,7 +665,7 @@ function handsPage() { for (var i = 0; i < kvKeys.length; i++) { try { - var resp = await OpenFangAPI.get('/api/memory/agents/' + agentId + '/kv/' + kvKeys[i]); + var resp = await OMTAEAPI.get('/api/memory/agents/' + agentId + '/kv/' + kvKeys[i]); if (resp && resp.value !== null && resp.value !== undefined) { var val = resp.value; this._applyKvToData(data, kvKeys[i], val); @@ -749,7 +749,7 @@ function handsPage() { (!document.documentElement.getAttribute('data-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches); var gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)'; var textColor = isDark ? '#8A8380' : '#6B6560'; - var accentColor = '#FF5C00'; + var accentColor = '#00F0FF'; var successColor = isDark ? '#4ADE80' : '#22C55E'; var errorColor = '#EF4444'; @@ -766,8 +766,8 @@ function handsPage() { // Determine gradient var eqCtx = eqCanvas.getContext('2d'); var gradient = eqCtx.createLinearGradient(0, 0, 0, eqCanvas.parentElement.clientHeight || 180); - gradient.addColorStop(0, isDark ? 'rgba(255, 92, 0, 0.25)' : 'rgba(255, 92, 0, 0.15)'); - gradient.addColorStop(1, 'rgba(255, 92, 0, 0)'); + gradient.addColorStop(0, isDark ? 'rgba(0, 240, 255, 0.25)' : 'rgba(0, 240, 255, 0.15)'); + gradient.addColorStop(1, 'rgba(0, 240, 255, 0)'); this._chartEquity = new Chart(eqCtx, { type: 'line', @@ -910,7 +910,7 @@ function handsPage() { datasets: [{ data: radarValues, borderColor: accentColor, - backgroundColor: isDark ? 'rgba(255, 92, 0, 0.2)' : 'rgba(255, 92, 0, 0.12)', + backgroundColor: isDark ? 'rgba(0, 240, 255, 0.2)' : 'rgba(0, 240, 255, 0.12)', borderWidth: 2, pointBackgroundColor: accentColor, pointRadius: 4, diff --git a/crates/openfang-api/static/js/pages/logs.js b/crates/openfang-api/static/js/pages/logs.js index 5ad41ddbcc..28f05e4209 100644 --- a/crates/openfang-api/static/js/pages/logs.js +++ b/crates/openfang-api/static/js/pages/logs.js @@ -1,4 +1,4 @@ -// OpenFang Logs Page — Real-time log viewer (SSE streaming + polling fallback) + Audit Trail tab +// OMTAE Logs Page — Real-time log viewer (SSE streaming + polling fallback) + Audit Trail tab 'use strict'; function logsPage() { @@ -33,7 +33,7 @@ function logsPage() { var url = '/api/logs/stream'; var sep = '?'; - var token = OpenFangAPI.getToken(); + var token = OMTAEAPI.getToken(); if (token) { url += sep + 'token=' + encodeURIComponent(token); sep = '&'; } try { @@ -105,7 +105,7 @@ function logsPage() { async fetchLogs() { if (this.loading) this.loadError = ''; try { - var data = await OpenFangAPI.get('/api/audit/recent?n=200'); + var data = await OMTAEAPI.get('/api/audit/recent?n=200'); this.entries = data.entries || []; if (this.autoRefresh && !this.hovering) { this.$nextTick(function() { @@ -187,7 +187,7 @@ function logsPage() { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; - a.download = 'openfang-logs-' + new Date().toISOString().slice(0, 10) + '.txt'; + a.download = 'omtae-logs-' + new Date().toISOString().slice(0, 10) + '.txt'; a.click(); URL.revokeObjectURL(url); }, @@ -203,7 +203,7 @@ function logsPage() { this.auditLoading = true; this.auditLoadError = ''; try { - var data = await OpenFangAPI.get('/api/audit/recent?n=200'); + var data = await OMTAEAPI.get('/api/audit/recent?n=200'); this.auditEntries = data.entries || []; this.tipHash = data.tip_hash || ''; } catch(e) { @@ -234,16 +234,16 @@ function logsPage() { async verifyChain() { try { - var data = await OpenFangAPI.get('/api/audit/verify'); + var data = await OMTAEAPI.get('/api/audit/verify'); this.chainValid = data.valid === true; if (this.chainValid) { - OpenFangToast.success('Audit chain verified — ' + (data.entries || 0) + ' entries valid'); + OMTAEToast.success('Audit chain verified — ' + (data.entries || 0) + ' entries valid'); } else { - OpenFangToast.error('Audit chain broken!'); + OMTAEToast.error('Audit chain broken!'); } } catch(e) { this.chainValid = false; - OpenFangToast.error('Chain verification failed: ' + e.message); + OMTAEToast.error('Chain verification failed: ' + e.message); } }, diff --git a/crates/openfang-api/static/js/pages/overview.js b/crates/openfang-api/static/js/pages/overview.js index 3e890e2499..b773903503 100644 --- a/crates/openfang-api/static/js/pages/overview.js +++ b/crates/openfang-api/static/js/pages/overview.js @@ -1,4 +1,4 @@ -// OpenFang Overview Dashboard — Landing page with system stats + provider status +// OMTAE Overview Dashboard — Landing page with system stats + provider status 'use strict'; function overviewPage() { @@ -11,6 +11,35 @@ function overviewPage() { providers: [], mcpServers: [], skillCount: 0, + drift: { ok: true, findings: [], fixes_applied: [] }, + driftRemediating: false, + driftDismissedAt: (function() { + try { + var v = sessionStorage.getItem('omtae-drift-dismissed'); + return v ? Number(v) : null; + } catch(e) { return null; } + })(), + + get driftAlerts() { + var findings = (this.drift && this.drift.findings) || []; + var manual = (this.drift && this.drift.manual_allowlist) || []; + var manualSet = {}; + manual.forEach(function(n) { + if (n) manualSet[String(n).trim().toLowerCase()] = true; + }); + return findings.filter(function(f) { + if (f.remediated || f.severity === 'info' || f.severity === 'debug') return false; + if (f.check === 'allowlist' && manualSet) { + var m = (f.message || '').match(/Agent '([^']+)'/); + if (m && manualSet[m[1].trim().toLowerCase()]) return false; + } + return true; + }); + }, + + get showDriftCard() { + return this.driftAlerts.length > 0 && !this.driftDismissedAt; + }, loading: true, loadError: '', refreshTimer: null, @@ -28,7 +57,8 @@ function overviewPage() { this.loadChannels(), this.loadProviders(), this.loadMcpServers(), - this.loadSkills() + this.loadSkills(), + this.loadDrift() ]); this.lastRefresh = Date.now(); } catch(e) { @@ -50,7 +80,8 @@ function overviewPage() { this.loadChannels(), this.loadProviders(), this.loadMcpServers(), - this.loadSkills() + this.loadSkills(), + this.loadDrift() ]); this.lastRefresh = Date.now(); } catch(e) { /* silent */ } @@ -70,19 +101,53 @@ function overviewPage() { async loadHealth() { try { - this.health = await OpenFangAPI.get('/api/health'); + this.health = await OMTAEAPI.get('/api/health'); } catch(e) { this.health = { status: 'unreachable' }; } }, + async loadDrift() { + try { + var report = await OMTAEAPI.get('/api/system/drift'); + this.drift = report || { ok: true, findings: [], fixes_applied: [] }; + if (this.driftAlerts.length === 0) { + this.driftDismissedAt = null; + try { sessionStorage.removeItem('omtae-drift-dismissed'); } catch(e) { /* ignore */ } + } + } catch(e) { + this.drift = { ok: true, findings: [], fixes_applied: [] }; + } + }, + + dismissDrift() { + this.driftDismissedAt = Date.now(); + try { sessionStorage.setItem('omtae-drift-dismissed', String(this.driftDismissedAt)); } catch(e) { /* ignore */ } + }, + + async remediateDrift() { + this.driftRemediating = true; + try { + var report = await OMTAEAPI.post('/api/system/drift', {}); + this.drift = report || { ok: true, findings: [], fixes_applied: [] }; + if (this.drift.fixes_applied && this.drift.fixes_applied.length) { + OMTAEToast.success('Applied ' + this.drift.fixes_applied.length + ' fix(es)'); + Alpine.store('app').refreshAgents(); + } + await Promise.all([this.loadDrift(), this.loadStatus()]); + } catch(e) { + OMTAEToast.error(e.message || 'Drift remediation failed'); + } + this.driftRemediating = false; + }, + async loadStatus() { try { - this.status = await OpenFangAPI.get('/api/status'); + this.status = await OMTAEAPI.get('/api/status'); } catch(e) { this.status = {}; throw e; } }, async loadUsage() { try { - var data = await OpenFangAPI.get('/api/usage'); + var data = await OMTAEAPI.get('/api/usage'); var agents = data.agents || []; var totalTokens = 0; var totalTools = 0; @@ -105,35 +170,35 @@ function overviewPage() { async loadAudit() { try { - var data = await OpenFangAPI.get('/api/audit/recent?n=8'); + var data = await OMTAEAPI.get('/api/audit/recent?n=8'); this.recentAudit = data.entries || []; } catch(e) { this.recentAudit = []; } }, async loadChannels() { try { - var data = await OpenFangAPI.get('/api/channels'); + var data = await OMTAEAPI.get('/api/channels'); this.channels = (data.channels || []).filter(function(ch) { return ch.has_token; }); } catch(e) { this.channels = []; } }, async loadProviders() { try { - var data = await OpenFangAPI.get('/api/providers'); + var data = await OMTAEAPI.get('/api/providers'); this.providers = data.providers || []; } catch(e) { this.providers = []; } }, async loadMcpServers() { try { - var data = await OpenFangAPI.get('/api/mcp/servers'); + var data = await OMTAEAPI.get('/api/mcp/servers'); this.mcpServers = data.servers || []; } catch(e) { this.mcpServers = []; } }, async loadSkills() { try { - var data = await OpenFangAPI.get('/api/skills'); + var data = await OMTAEAPI.get('/api/skills'); this.skillCount = (data.skills || []).length; } catch(e) { this.skillCount = 0; } }, diff --git a/crates/openfang-api/static/js/pages/runtime.js b/crates/openfang-api/static/js/pages/runtime.js index a381fe9f6e..e62832b800 100644 --- a/crates/openfang-api/static/js/pages/runtime.js +++ b/crates/openfang-api/static/js/pages/runtime.js @@ -14,20 +14,30 @@ document.addEventListener('alpine:init', function() { logLevel: '-', networkEnabled: false, providers: [], + modelProfiles: [], + activeProfileId: '', + selectedProfileId: '', + restartVllmOnApply: false, + modelSwitchStatus: '', + modelSwitchBusy: false, async loadData() { this.loading = true; try { var results = await Promise.all([ - OpenFangAPI.get('/api/status'), - OpenFangAPI.get('/api/version'), - OpenFangAPI.get('/api/providers'), - OpenFangAPI.get('/api/agents') + OMTAEAPI.get('/api/status'), + OMTAEAPI.get('/api/version'), + OMTAEAPI.get('/api/providers'), + OMTAEAPI.get('/api/agents'), + OMTAEAPI.get('/api/models/profiles').catch(function() { return { profiles: [], active_profile: '' }; }), + OMTAEAPI.get('/api/models/active').catch(function() { return {}; }) ]); var status = results[0]; var ver = results[1]; var prov = results[2]; var agents = results[3]; + var prof = results[4] || {}; + var active = results[5] || {}; this.version = ver.version || '-'; this.platform = ver.platform || '-'; @@ -49,10 +59,39 @@ document.addEventListener('alpine:init', function() { this.providers = (prov.providers || []).filter(function(p) { return p.auth_status === 'Configured' || p.reachable || p.is_local; }); + + this.modelProfiles = prof.profiles || []; + this.activeProfileId = prof.active_profile || active.active_profile || ''; + if (!this.selectedProfileId && this.activeProfileId) { + this.selectedProfileId = this.activeProfileId; + } else if (!this.selectedProfileId && this.modelProfiles.length) { + this.selectedProfileId = this.modelProfiles[0].id; + } } catch(e) { console.error('Runtime load error:', e); } this.loading = false; + }, + + async applyModelProfile() { + if (!this.selectedProfileId) return; + this.modelSwitchBusy = true; + this.modelSwitchStatus = ''; + try { + var res = await OMTAEAPI.put('/api/models/active', { + profile_id: this.selectedProfileId, + restart_vllm: !!this.restartVllmOnApply + }); + this.activeProfileId = this.selectedProfileId; + this.defaultModel = res.omtae_model_id || this.defaultModel; + this.modelSwitchStatus = res.message || 'Applied.'; + if (res.message && res.message.indexOf('omtae-model use') >= 0) { + this.modelSwitchStatus += ' Full GPU swap: omtae-model use ' + this.selectedProfileId; + } + } catch (e) { + this.modelSwitchStatus = (e && e.message) ? e.message : 'Switch failed'; + } + this.modelSwitchBusy = false; } }; }); diff --git a/crates/openfang-api/static/js/pages/scheduler.js b/crates/openfang-api/static/js/pages/scheduler.js index 82822bc440..edce60a562 100644 --- a/crates/openfang-api/static/js/pages/scheduler.js +++ b/crates/openfang-api/static/js/pages/scheduler.js @@ -1,4 +1,4 @@ -// OpenFang Scheduler Page — Cron job management + event triggers unified view +// OMTAE Scheduler Page — Cron job management + event triggers unified view 'use strict'; function schedulerPage() { @@ -85,7 +85,7 @@ function schedulerPage() { async loadChannelTypes() { try { - var data = await OpenFangAPI.get('/api/channels'); + var data = await OMTAEAPI.get('/api/channels'); // /api/channels returns an array of channel descriptors; pull names. var list = Array.isArray(data) ? data : (data && data.channels) || []; var names = []; @@ -102,7 +102,7 @@ function schedulerPage() { }, async loadJobs() { - var data = await OpenFangAPI.get('/api/cron/jobs'); + var data = await OMTAEAPI.get('/api/cron/jobs'); var raw = data.jobs || []; // Normalize cron API response to flat fields the UI expects this.jobs = raw.map(function(j) { @@ -132,7 +132,7 @@ function schedulerPage() { this.trigLoading = true; this.trigLoadError = ''; try { - var data = await OpenFangAPI.get('/api/triggers'); + var data = await OMTAEAPI.get('/api/triggers'); this.triggers = Array.isArray(data) ? data : []; } catch(e) { this.triggers = []; @@ -185,11 +185,11 @@ function schedulerPage() { async createJob() { if (!this.newJob.name.trim()) { - OpenFangToast.warn('Please enter a job name'); + OMTAEToast.warn('Please enter a job name'); return; } if (!this.newJob.cron.trim()) { - OpenFangToast.warn('Please enter a cron expression'); + OMTAEToast.warn('Please enter a cron expression'); return; } this.creating = true; @@ -206,13 +206,13 @@ function schedulerPage() { if (this.newJob.delivery_targets && this.newJob.delivery_targets.length) { body.delivery_targets = this.newJob.delivery_targets.map(this.sanitizeTarget); } - await OpenFangAPI.post('/api/cron/jobs', body); + await OMTAEAPI.post('/api/cron/jobs', body); this.showCreateForm = false; this.newJob = { name: '', cron: '', agent_id: '', message: '', enabled: true, delivery_targets: [] }; - OpenFangToast.success('Schedule "' + jobName + '" created'); + OMTAEToast.success('Schedule "' + jobName + '" created'); await this.loadJobs(); } catch(e) { - OpenFangToast.error('Failed to create schedule: ' + (e.message || e)); + OMTAEToast.error('Failed to create schedule: ' + (e.message || e)); } this.creating = false; }, @@ -220,24 +220,24 @@ function schedulerPage() { async toggleJob(job) { try { var newState = !job.enabled; - await OpenFangAPI.put('/api/cron/jobs/' + job.id + '/enable', { enabled: newState }); + await OMTAEAPI.put('/api/cron/jobs/' + job.id + '/enable', { enabled: newState }); job.enabled = newState; - OpenFangToast.success('Schedule ' + (newState ? 'enabled' : 'paused')); + OMTAEToast.success('Schedule ' + (newState ? 'enabled' : 'paused')); } catch(e) { - OpenFangToast.error('Failed to toggle schedule: ' + (e.message || e)); + OMTAEToast.error('Failed to toggle schedule: ' + (e.message || e)); } }, deleteJob(job) { var self = this; var jobName = job.name || job.id; - OpenFangToast.confirm('Delete Schedule', 'Delete "' + jobName + '"? This cannot be undone.', async function() { + OMTAEToast.confirm('Delete Schedule', 'Delete "' + jobName + '"? This cannot be undone.', async function() { try { - await OpenFangAPI.del('/api/cron/jobs/' + job.id); + await OMTAEAPI.del('/api/cron/jobs/' + job.id); self.jobs = self.jobs.filter(function(j) { return j.id !== job.id; }); - OpenFangToast.success('Schedule "' + jobName + '" deleted'); + OMTAEToast.success('Schedule "' + jobName + '" deleted'); } catch(e) { - OpenFangToast.error('Failed to delete schedule: ' + (e.message || e)); + OMTAEToast.error('Failed to delete schedule: ' + (e.message || e)); } }); }, @@ -245,17 +245,17 @@ function schedulerPage() { async runNow(job) { this.runningJobId = job.id; try { - var result = await OpenFangAPI.post('/api/cron/jobs/' + job.id + '/run', {}); + var result = await OMTAEAPI.post('/api/cron/jobs/' + job.id + '/run', {}); if (result.status === 'triggered' || result.status === 'completed') { - OpenFangToast.success('Job "' + (job.name || 'job') + '" triggered'); + OMTAEToast.success('Job "' + (job.name || 'job') + '" triggered'); // Don't update job.last_run here — the job runs asynchronously in the // background. The real last_run is set by the server on completion and // will appear on the next data refresh. } else { - OpenFangToast.error('Run failed: ' + (result.error || 'Unknown error')); + OMTAEToast.error('Run failed: ' + (result.error || 'Unknown error')); } } catch(e) { - OpenFangToast.error('Run failed: ' + (e.message || e)); + OMTAEToast.error('Run failed: ' + (e.message || e)); } this.runningJobId = ''; }, @@ -296,7 +296,7 @@ function schedulerPage() { addDraftTarget() { var err = this.validateTarget(this.draftTarget); if (err) { - OpenFangToast.warn(err); + OMTAEToast.warn(err); return; } if (!Array.isArray(this.newJob.delivery_targets)) this.newJob.delivery_targets = []; @@ -395,7 +395,7 @@ function schedulerPage() { this.deliveryLogError = ''; this.deliveryLogLoading = true; try { - var data = await OpenFangAPI.get('/api/schedules/' + job.id + '/delivery-log'); + var data = await OMTAEAPI.get('/api/schedules/' + job.id + '/delivery-log'); this.deliveryLog = { targets: Array.isArray(data.targets) ? data.targets : [], entries: Array.isArray(data.entries) ? data.entries : [] @@ -435,7 +435,7 @@ function schedulerPage() { addDraftTargetToEdit() { var err = this.validateTarget(this.draftTarget); if (err) { - OpenFangToast.warn(err); + OMTAEToast.warn(err); return; } this.editingTargets.push(this.sanitizeTarget(this.draftTarget)); @@ -452,14 +452,14 @@ function schedulerPage() { this.savingTargets = true; try { var clean = this.editingTargets.map(this.sanitizeTarget); - await OpenFangAPI.put('/api/schedules/' + this.editingTargetsJobId, { + await OMTAEAPI.put('/api/schedules/' + this.editingTargetsJobId, { delivery_targets: clean }); - OpenFangToast.success('Delivery targets updated'); + OMTAEToast.success('Delivery targets updated'); this.cancelEditTargets(); await this.loadJobs(); } catch(e) { - OpenFangToast.error('Failed to update targets: ' + (e.message || e)); + OMTAEToast.error('Failed to update targets: ' + (e.message || e)); } this.savingTargets = false; }, @@ -489,23 +489,23 @@ function schedulerPage() { async toggleTrigger(trigger) { try { var newState = !trigger.enabled; - await OpenFangAPI.put('/api/triggers/' + trigger.id, { enabled: newState }); + await OMTAEAPI.put('/api/triggers/' + trigger.id, { enabled: newState }); trigger.enabled = newState; - OpenFangToast.success('Trigger ' + (newState ? 'enabled' : 'disabled')); + OMTAEToast.success('Trigger ' + (newState ? 'enabled' : 'disabled')); } catch(e) { - OpenFangToast.error('Failed to toggle trigger: ' + (e.message || e)); + OMTAEToast.error('Failed to toggle trigger: ' + (e.message || e)); } }, deleteTrigger(trigger) { var self = this; - OpenFangToast.confirm('Delete Trigger', 'Delete this trigger? This cannot be undone.', async function() { + OMTAEToast.confirm('Delete Trigger', 'Delete this trigger? This cannot be undone.', async function() { try { - await OpenFangAPI.del('/api/triggers/' + trigger.id); + await OMTAEAPI.del('/api/triggers/' + trigger.id); self.triggers = self.triggers.filter(function(t) { return t.id !== trigger.id; }); - OpenFangToast.success('Trigger deleted'); + OMTAEToast.success('Trigger deleted'); } catch(e) { - OpenFangToast.error('Failed to delete trigger: ' + (e.message || e)); + OMTAEToast.error('Failed to delete trigger: ' + (e.message || e)); } }); }, diff --git a/crates/openfang-api/static/js/pages/sessions.js b/crates/openfang-api/static/js/pages/sessions.js index a2c716f9f6..95abcd5e69 100644 --- a/crates/openfang-api/static/js/pages/sessions.js +++ b/crates/openfang-api/static/js/pages/sessions.js @@ -1,4 +1,4 @@ -// OpenFang Sessions Page — Session listing + Memory tab +// OMTAE Sessions Page — Session listing + Memory tab 'use strict'; function sessionsPage() { @@ -26,7 +26,7 @@ function sessionsPage() { this.loading = true; this.loadError = ''; try { - var data = await OpenFangAPI.get('/api/sessions'); + var data = await OMTAEAPI.get('/api/sessions'); var sessions = data.sessions || []; var agents = Alpine.store('app').agents; var agentMap = {}; @@ -65,16 +65,16 @@ function sessionsPage() { deleteSession(sessionId) { var self = this; var t = window.i18n ? window.i18n.t.bind(window.i18n) : function(k) { return k; }; - OpenFangToast.confirm( + OMTAEToast.confirm( t('sessions.delete_session') || 'Delete Session', t('sessions.delete_confirm') || 'This will permanently remove the session and its messages.', async function() { try { - await OpenFangAPI.del('/api/sessions/' + sessionId); + await OMTAEAPI.del('/api/sessions/' + sessionId); self.sessions = self.sessions.filter(function(s) { return s.session_id !== sessionId; }); - OpenFangToast.success('Session deleted'); + OMTAEToast.success('Session deleted'); } catch(e) { - OpenFangToast.error('Failed to delete session: ' + e.message); + OMTAEToast.error('Failed to delete session: ' + e.message); } } ); @@ -86,7 +86,7 @@ function sessionsPage() { this.memLoading = true; this.memLoadError = ''; try { - var data = await OpenFangAPI.get('/api/memory/agents/' + this.memAgentId + '/kv'); + var data = await OMTAEAPI.get('/api/memory/agents/' + this.memAgentId + '/kv'); this.kvPairs = data.kv_pairs || []; } catch(e) { this.kvPairs = []; @@ -100,30 +100,30 @@ function sessionsPage() { var value; try { value = JSON.parse(this.newValue); } catch(e) { value = this.newValue; } try { - await OpenFangAPI.put('/api/memory/agents/' + this.memAgentId + '/kv/' + encodeURIComponent(this.newKey), { value: value }); + await OMTAEAPI.put('/api/memory/agents/' + this.memAgentId + '/kv/' + encodeURIComponent(this.newKey), { value: value }); this.showAdd = false; - OpenFangToast.success('Key "' + this.newKey + '" saved'); + OMTAEToast.success('Key "' + this.newKey + '" saved'); this.newKey = ''; this.newValue = '""'; await this.loadKv(); } catch(e) { - OpenFangToast.error('Failed to save key: ' + e.message); + OMTAEToast.error('Failed to save key: ' + e.message); } }, deleteKey(key) { var self = this; var t = window.i18n ? window.i18n.t.bind(window.i18n) : function(k) { return k; }; - OpenFangToast.confirm( + OMTAEToast.confirm( t('sessions.delete_key') || 'Delete Key', (t('sessions.delete_key_confirm') || 'Delete key') + ' "' + key + '"? This cannot be undone.', async function() { try { - await OpenFangAPI.del('/api/memory/agents/' + self.memAgentId + '/kv/' + encodeURIComponent(key)); - OpenFangToast.success('Key "' + key + '" deleted'); + await OMTAEAPI.del('/api/memory/agents/' + self.memAgentId + '/kv/' + encodeURIComponent(key)); + OMTAEToast.success('Key "' + key + '" deleted'); await self.loadKv(); } catch(e) { - OpenFangToast.error('Failed to delete key: ' + e.message); + OMTAEToast.error('Failed to delete key: ' + e.message); } } ); @@ -144,13 +144,13 @@ function sessionsPage() { var value; try { value = JSON.parse(this.editingValue); } catch(e) { value = this.editingValue; } try { - await OpenFangAPI.put('/api/memory/agents/' + this.memAgentId + '/kv/' + encodeURIComponent(this.editingKey), { value: value }); - OpenFangToast.success('Key "' + this.editingKey + '" updated'); + await OMTAEAPI.put('/api/memory/agents/' + this.memAgentId + '/kv/' + encodeURIComponent(this.editingKey), { value: value }); + OMTAEToast.success('Key "' + this.editingKey + '" updated'); this.editingKey = null; this.editingValue = ''; await this.loadKv(); } catch(e) { - OpenFangToast.error('Failed to save: ' + e.message); + OMTAEToast.error('Failed to save: ' + e.message); } } }; diff --git a/crates/openfang-api/static/js/pages/settings.js b/crates/openfang-api/static/js/pages/settings.js index 7b46c2d001..fd259e79a9 100644 --- a/crates/openfang-api/static/js/pages/settings.js +++ b/crates/openfang-api/static/js/pages/settings.js @@ -1,4 +1,4 @@ -// OpenFang Settings Page — Provider Hub, Model Catalog, Config, Tools + Security, Network, Migration tabs +// OMTAE Settings Page — Provider Hub, Model Catalog, Config, Tools + Security, Network, Migration tabs 'use strict'; function settingsPage() { @@ -122,7 +122,7 @@ function settingsPage() { { name: 'Bearer Token Authentication', key: 'auth', description: 'All non-health endpoints require Authorization: Bearer header. When no API key is configured, all requests are restricted to localhost only.', - configHint: 'Set api_key in ~/.openfang/config.toml for remote access. Empty = localhost only.', + configHint: 'Set api_key in ~/.omtae/config.toml for remote access. Empty = localhost only.', valueKey: 'auth' } ], @@ -187,8 +187,8 @@ function settingsPage() { async loadSysInfo() { try { - var ver = await OpenFangAPI.get('/api/version'); - var status = await OpenFangAPI.get('/api/status'); + var ver = await OMTAEAPI.get('/api/version'); + var status = await OMTAEAPI.get('/api/status'); this.sysInfo = { version: ver.version || '-', platform: ver.platform || '-', @@ -203,27 +203,27 @@ function settingsPage() { async loadUsage() { try { - var data = await OpenFangAPI.get('/api/usage'); + var data = await OMTAEAPI.get('/api/usage'); this.usageData = data.agents || []; } catch(e) { this.usageData = []; } }, async loadTools() { try { - var data = await OpenFangAPI.get('/api/tools'); + var data = await OMTAEAPI.get('/api/tools'); this.tools = data.tools || []; } catch(e) { this.tools = []; } }, async loadConfig() { try { - this.config = await OpenFangAPI.get('/api/config'); + this.config = await OMTAEAPI.get('/api/config'); } catch(e) { this.config = {}; } }, async loadProviders() { try { - var data = await OpenFangAPI.get('/api/providers'); + var data = await OMTAEAPI.get('/api/providers'); this.providers = data.providers || []; for (var i = 0; i < this.providers.length; i++) { var p = this.providers[i]; @@ -241,7 +241,7 @@ function settingsPage() { async loadModels() { try { - var data = await OpenFangAPI.get('/api/models'); + var data = await OMTAEAPI.get('/api/models'); this.models = data.models || []; } catch(e) { this.models = []; } }, @@ -251,7 +251,7 @@ function settingsPage() { if (!id) return; this.customModelStatus = 'Adding...'; try { - await OpenFangAPI.post('/api/models/custom', { + await OMTAEAPI.post('/api/models/custom', { id: id, provider: this.customModelProvider || 'openrouter', context_window: this.customModelContext || 128000, @@ -269,19 +269,19 @@ function settingsPage() { async deleteCustomModel(modelId) { if (!confirm('Delete custom model "' + modelId + '"?')) return; try { - await OpenFangAPI.del('/api/models/custom/' + encodeURIComponent(modelId)); - OpenFangToast.success('Model deleted'); + await OMTAEAPI.del('/api/models/custom/' + encodeURIComponent(modelId)); + OMTAEToast.success('Model deleted'); await this.loadModels(); } catch(e) { - OpenFangToast.error('Failed to delete: ' + (e.message || 'Unknown error')); + OMTAEToast.error('Failed to delete: ' + (e.message || 'Unknown error')); } }, async loadConfigSchema() { try { var results = await Promise.all([ - OpenFangAPI.get('/api/config/schema').catch(function() { return {}; }), - OpenFangAPI.get('/api/config') + OMTAEAPI.get('/api/config/schema').catch(function() { return {}; }), + OMTAEAPI.get('/api/config') ]); this.configSchema = results[0].sections || null; this.configValues = results[1] || {}; @@ -303,11 +303,11 @@ function settingsPage() { var path = (sectionMeta && sectionMeta.root_level) ? field : key; this.configSaving[key] = true; try { - await OpenFangAPI.post('/api/config/set', { path: path, value: value }); + await OMTAEAPI.post('/api/config/set', { path: path, value: value }); this.configDirty[key] = false; - OpenFangToast.success('Saved ' + field); + OMTAEToast.success('Saved ' + field); } catch(e) { - OpenFangToast.error('Failed to save: ' + e.message); + OMTAEToast.error('Failed to save: ' + e.message); } this.configSaving[key] = false; }, @@ -490,30 +490,30 @@ function settingsPage() { async saveProviderKey(provider) { var key = this.providerKeyInputs[provider.id]; - if (!key || !key.trim()) { OpenFangToast.error('Please enter an API key'); return; } + if (!key || !key.trim()) { OMTAEToast.error('Please enter an API key'); return; } try { - var resp = await OpenFangAPI.post('/api/providers/' + encodeURIComponent(provider.id) + '/key', { key: key.trim() }); + var resp = await OMTAEAPI.post('/api/providers/' + encodeURIComponent(provider.id) + '/key', { key: key.trim() }); if (resp && resp.switched_default) { - OpenFangToast.warning(resp.message || 'Default provider was switched to ' + provider.display_name); + OMTAEToast.warning(resp.message || 'Default provider was switched to ' + provider.display_name); } else { - OpenFangToast.success('API key saved for ' + provider.display_name); + OMTAEToast.success('API key saved for ' + provider.display_name); } this.providerKeyInputs[provider.id] = ''; await this.loadProviders(); await this.loadModels(); } catch(e) { - OpenFangToast.error('Failed to save key: ' + e.message); + OMTAEToast.error('Failed to save key: ' + e.message); } }, async removeProviderKey(provider) { try { - await OpenFangAPI.del('/api/providers/' + encodeURIComponent(provider.id) + '/key'); - OpenFangToast.success('API key removed for ' + provider.display_name); + await OMTAEAPI.del('/api/providers/' + encodeURIComponent(provider.id) + '/key'); + OMTAEToast.success('API key removed for ' + provider.display_name); await this.loadProviders(); await this.loadModels(); } catch(e) { - OpenFangToast.error('Failed to remove key: ' + e.message); + OMTAEToast.error('Failed to remove key: ' + e.message); } }, @@ -521,7 +521,7 @@ function settingsPage() { this.copilotOAuth.polling = true; this.copilotOAuth.userCode = ''; try { - var resp = await OpenFangAPI.post('/api/providers/github-copilot/oauth/start', {}); + var resp = await OMTAEAPI.post('/api/providers/github-copilot/oauth/start', {}); this.copilotOAuth.userCode = resp.user_code; this.copilotOAuth.verificationUri = resp.verification_uri; this.copilotOAuth.pollId = resp.poll_id; @@ -529,7 +529,7 @@ function settingsPage() { window.open(resp.verification_uri, '_blank'); this.pollCopilotOAuth(); } catch(e) { - OpenFangToast.error('Failed to start Copilot login: ' + e.message); + OMTAEToast.error('Failed to start Copilot login: ' + e.message); this.copilotOAuth.polling = false; } }, @@ -539,9 +539,9 @@ function settingsPage() { setTimeout(async function() { if (!self.copilotOAuth.pollId) return; try { - var resp = await OpenFangAPI.get('/api/providers/github-copilot/oauth/poll/' + self.copilotOAuth.pollId); + var resp = await OMTAEAPI.get('/api/providers/github-copilot/oauth/poll/' + self.copilotOAuth.pollId); if (resp.status === 'complete') { - OpenFangToast.success('GitHub Copilot authenticated successfully!'); + OMTAEToast.success('GitHub Copilot authenticated successfully!'); self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 }; await self.loadProviders(); await self.loadModels(); @@ -549,17 +549,17 @@ function settingsPage() { if (resp.interval) self.copilotOAuth.interval = resp.interval; self.pollCopilotOAuth(); } else if (resp.status === 'expired') { - OpenFangToast.error('Device code expired. Please try again.'); + OMTAEToast.error('Device code expired. Please try again.'); self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 }; } else if (resp.status === 'denied') { - OpenFangToast.error('Access denied by user.'); + OMTAEToast.error('Access denied by user.'); self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 }; } else { - OpenFangToast.error('OAuth error: ' + (resp.error || resp.status)); + OMTAEToast.error('OAuth error: ' + (resp.error || resp.status)); self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 }; } } catch(e) { - OpenFangToast.error('Poll error: ' + e.message); + OMTAEToast.error('Poll error: ' + e.message); self.copilotOAuth = { polling: false, userCode: '', verificationUri: '', pollId: '', interval: 5 }; } }, self.copilotOAuth.interval * 1000); @@ -569,66 +569,66 @@ function settingsPage() { this.providerTesting[provider.id] = true; this.providerTestResults[provider.id] = null; try { - var result = await OpenFangAPI.post('/api/providers/' + encodeURIComponent(provider.id) + '/test', {}); + var result = await OMTAEAPI.post('/api/providers/' + encodeURIComponent(provider.id) + '/test', {}); this.providerTestResults[provider.id] = result; if (result.status === 'ok') { - OpenFangToast.success(provider.display_name + ' connected (' + (result.latency_ms || '?') + 'ms)'); + OMTAEToast.success(provider.display_name + ' connected (' + (result.latency_ms || '?') + 'ms)'); } else { - OpenFangToast.error(provider.display_name + ': ' + (result.error || 'Connection failed')); + OMTAEToast.error(provider.display_name + ': ' + (result.error || 'Connection failed')); } } catch(e) { this.providerTestResults[provider.id] = { status: 'error', error: e.message }; - OpenFangToast.error('Test failed: ' + e.message); + OMTAEToast.error('Test failed: ' + e.message); } this.providerTesting[provider.id] = false; }, async saveProviderUrl(provider) { var url = this.providerUrlInputs[provider.id]; - if (!url || !url.trim()) { OpenFangToast.error('Please enter a base URL'); return; } + if (!url || !url.trim()) { OMTAEToast.error('Please enter a base URL'); return; } url = url.trim(); if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) { - OpenFangToast.error('URL must start with http:// or https://'); return; + OMTAEToast.error('URL must start with http:// or https://'); return; } this.providerUrlSaving[provider.id] = true; try { - var result = await OpenFangAPI.put('/api/providers/' + encodeURIComponent(provider.id) + '/url', { base_url: url }); + var result = await OMTAEAPI.put('/api/providers/' + encodeURIComponent(provider.id) + '/url', { base_url: url }); if (result.reachable) { - OpenFangToast.success(provider.display_name + ' URL saved — reachable (' + (result.latency_ms || '?') + 'ms)'); + OMTAEToast.success(provider.display_name + ' URL saved — reachable (' + (result.latency_ms || '?') + 'ms)'); } else { - OpenFangToast.warning(provider.display_name + ' URL saved but not reachable'); + OMTAEToast.warning(provider.display_name + ' URL saved but not reachable'); } await this.loadProviders(); } catch(e) { - OpenFangToast.error('Failed to save URL: ' + e.message); + OMTAEToast.error('Failed to save URL: ' + e.message); } this.providerUrlSaving[provider.id] = false; }, async addCustomProvider() { var name = this.customProviderName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-'); - if (!name) { OpenFangToast.error('Please enter a provider name'); return; } + if (!name) { OMTAEToast.error('Please enter a provider name'); return; } var url = this.customProviderUrl.trim(); - if (!url) { OpenFangToast.error('Please enter a base URL'); return; } + if (!url) { OMTAEToast.error('Please enter a base URL'); return; } if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) { - OpenFangToast.error('URL must start with http:// or https://'); return; + OMTAEToast.error('URL must start with http:// or https://'); return; } this.addingCustomProvider = true; this.customProviderStatus = ''; try { - var result = await OpenFangAPI.put('/api/providers/' + encodeURIComponent(name) + '/url', { base_url: url }); + var result = await OMTAEAPI.put('/api/providers/' + encodeURIComponent(name) + '/url', { base_url: url }); if (this.customProviderKey.trim()) { - await OpenFangAPI.post('/api/providers/' + encodeURIComponent(name) + '/key', { key: this.customProviderKey.trim() }); + await OMTAEAPI.post('/api/providers/' + encodeURIComponent(name) + '/key', { key: this.customProviderKey.trim() }); } this.customProviderName = ''; this.customProviderUrl = ''; this.customProviderKey = ''; this.customProviderStatus = ''; - OpenFangToast.success('Provider "' + name + '" added' + (result.reachable ? ' (reachable)' : ' (not reachable yet)')); + OMTAEToast.success('Provider "' + name + '" added' + (result.reachable ? ' (reachable)' : ' (not reachable yet)')); await this.loadProviders(); } catch(e) { this.customProviderStatus = 'Error: ' + (e.message || 'Failed'); - OpenFangToast.error('Failed to add provider: ' + e.message); + OMTAEToast.error('Failed to add provider: ' + e.message); } this.addingCustomProvider = false; }, @@ -637,7 +637,7 @@ function settingsPage() { async loadSecurity() { this.secLoading = true; try { - this.securityData = await OpenFangAPI.get('/api/security'); + this.securityData = await OMTAEAPI.get('/api/security'); } catch(e) { this.securityData = null; } @@ -700,7 +700,7 @@ function settingsPage() { this.verifyingChain = true; this.chainResult = null; try { - var res = await OpenFangAPI.get('/api/audit/verify'); + var res = await OMTAEAPI.get('/api/audit/verify'); this.chainResult = res; } catch(e) { this.chainResult = { valid: false, error: e.message }; @@ -713,7 +713,7 @@ function settingsPage() { this.peersLoading = true; this.peersLoadError = ''; try { - var data = await OpenFangAPI.get('/api/peers'); + var data = await OMTAEAPI.get('/api/peers'); this.peers = (data.peers || []).map(function(p) { return { node_id: p.node_id, @@ -737,7 +737,7 @@ function settingsPage() { this._peerPollTimer = setInterval(async function() { if (self.tab !== 'network') { self.stopPeerPolling(); return; } try { - var data = await OpenFangAPI.get('/api/peers'); + var data = await OMTAEAPI.get('/api/peers'); self.peers = (data.peers || []).map(function(p) { return { node_id: p.node_id, @@ -760,7 +760,7 @@ function settingsPage() { async autoDetect() { this.detecting = true; try { - var data = await OpenFangAPI.get('/api/migrate/detect'); + var data = await OMTAEAPI.get('/api/migrate/detect'); if (data.detected && data.scan) { this.sourcePath = data.path; this.scanResult = data.scan; @@ -778,16 +778,16 @@ function settingsPage() { if (!this.sourcePath) return; this.scanning = true; try { - var data = await OpenFangAPI.post('/api/migrate/scan', { path: this.sourcePath }); + var data = await OMTAEAPI.post('/api/migrate/scan', { path: this.sourcePath }); if (data.error) { - OpenFangToast.error('Scan error: ' + data.error); + OMTAEToast.error('Scan error: ' + data.error); this.scanning = false; return; } this.scanResult = data; this.migStep = 'preview'; } catch(e) { - OpenFangToast.error('Scan failed: ' + e.message); + OMTAEToast.error('Scan failed: ' + e.message); } this.scanning = false; }, @@ -797,7 +797,7 @@ function settingsPage() { try { var target = this.targetPath; if (!target) target = ''; - var data = await OpenFangAPI.post('/api/migrate', { + var data = await OMTAEAPI.post('/api/migrate', { source: 'openclaw', source_dir: this.sourcePath || (this.scanResult ? this.scanResult.path : ''), target_dir: target, diff --git a/crates/openfang-api/static/js/pages/skills.js b/crates/openfang-api/static/js/pages/skills.js index 8a4ddf7929..b59c235b71 100644 --- a/crates/openfang-api/static/js/pages/skills.js +++ b/crates/openfang-api/static/js/pages/skills.js @@ -1,4 +1,4 @@ -// OpenFang Skills Page — OpenClaw/ClawHub ecosystem + local skills + MCP servers +// OMTAE Skills Page — OpenClaw/ClawHub ecosystem + local skills + MCP servers 'use strict'; function skillsPage() { @@ -99,7 +99,7 @@ function skillsPage() { this.loading = true; this.loadError = ''; try { - var data = await OpenFangAPI.get('/api/skills'); + var data = await OMTAEAPI.get('/api/skills'); this.skills = (data.skills || []).map(function(s) { return { name: s.name, @@ -132,7 +132,7 @@ function skillsPage() { this.configError = ''; this.configLoading = true; try { - var data = await OpenFangAPI.get('/api/skills/' + encodeURIComponent(skill.name) + '/config'); + var data = await OMTAEAPI.get('/api/skills/' + encodeURIComponent(skill.name) + '/config'); this.configDeclared = data.declared || {}; this.configResolved = data.resolved || {}; // Pre-populate draft values only for vars the user has already @@ -214,7 +214,7 @@ function skillsPage() { async saveSkillConfig() { if (!this.configSkill) return; if (this.hasInvalidConfig()) { - OpenFangToast.error('Fill in all required variables before saving.'); + OMTAEToast.error('Fill in all required variables before saving.'); return; } this.configSaving = true; @@ -229,8 +229,8 @@ function skillsPage() { if (v.length > 0) payload[n] = v; } try { - await OpenFangAPI.put('/api/skills/' + encodeURIComponent(this.configSkill.name) + '/config', { values: payload }); - OpenFangToast.success('Saved, reloading agents\u2026'); + await OMTAEAPI.put('/api/skills/' + encodeURIComponent(this.configSkill.name) + '/config', { values: payload }); + OMTAEToast.success('Saved, reloading agents\u2026'); // Refresh the modal contents so the new source/value shows up. var refreshed = this.configSkill; await this.loadSkills(); @@ -241,7 +241,7 @@ function skillsPage() { if (updated) await self.openSkillConfig(updated); } catch(e) { this.configError = e.message || 'Save failed.'; - OpenFangToast.error('Save failed: ' + (e.message || 'unknown error')); + OMTAEToast.error('Save failed: ' + (e.message || 'unknown error')); } this.configSaving = false; }, @@ -257,18 +257,18 @@ function skillsPage() { return; } try { - await OpenFangAPI.del('/api/skills/' + encodeURIComponent(this.configSkill.name) + '/config/' + encodeURIComponent(name)); - OpenFangToast.success('Reset ' + name); + await OMTAEAPI.del('/api/skills/' + encodeURIComponent(this.configSkill.name) + '/config/' + encodeURIComponent(name)); + OMTAEToast.success('Reset ' + name); // Refresh modal state from server. - var data = await OpenFangAPI.get('/api/skills/' + encodeURIComponent(this.configSkill.name) + '/config'); + var data = await OMTAEAPI.get('/api/skills/' + encodeURIComponent(this.configSkill.name) + '/config'); this.configResolved = data.resolved || {}; this.configDraft[name] = ''; } catch(e) { var msg = e.message || 'Reset failed'; if (msg.indexOf('required') !== -1 || msg.indexOf('409') !== -1) { - OpenFangToast.error('Cannot reset: ' + decl.description + ' is required with no fallback.'); + OMTAEToast.error('Cannot reset: ' + decl.description + ' is required with no fallback.'); } else { - OpenFangToast.error('Reset failed: ' + msg); + OMTAEToast.error('Reset failed: ' + msg); } } }, @@ -303,7 +303,7 @@ function skillsPage() { this.clawhubLoading = true; this.clawhubError = ''; try { - var data = await OpenFangAPI.get('/api/clawhub/search?q=' + encodeURIComponent(this.clawhubSearch.trim()) + '&limit=20'); + var data = await OMTAEAPI.get('/api/clawhub/search?q=' + encodeURIComponent(this.clawhubSearch.trim()) + '&limit=20'); this.clawhubResults = data.items || []; if (data.error) this.clawhubError = data.error; } catch(e) { @@ -335,7 +335,7 @@ function skillsPage() { this.clawhubError = ''; this.clawhubNextCursor = null; try { - var data = await OpenFangAPI.get('/api/clawhub/browse?sort=' + this.clawhubSort + '&limit=20'); + var data = await OMTAEAPI.get('/api/clawhub/browse?sort=' + this.clawhubSort + '&limit=20'); this.clawhubBrowseResults = data.items || []; this.clawhubNextCursor = data.next_cursor || null; if (data.error) this.clawhubError = data.error; @@ -352,7 +352,7 @@ function skillsPage() { if (!this.clawhubNextCursor || this.clawhubLoading) return; this.clawhubLoading = true; try { - var data = await OpenFangAPI.get('/api/clawhub/browse?sort=' + this.clawhubSort + '&limit=20&cursor=' + encodeURIComponent(this.clawhubNextCursor)); + var data = await OMTAEAPI.get('/api/clawhub/browse?sort=' + this.clawhubSort + '&limit=20&cursor=' + encodeURIComponent(this.clawhubNextCursor)); this.clawhubBrowseResults = this.clawhubBrowseResults.concat(data.items || []); this.clawhubNextCursor = data.next_cursor || null; } catch(e) { @@ -367,10 +367,10 @@ function skillsPage() { this.skillDetail = null; this.installResult = null; try { - var data = await OpenFangAPI.get('/api/clawhub/skill/' + encodeURIComponent(slug)); + var data = await OMTAEAPI.get('/api/clawhub/skill/' + encodeURIComponent(slug)); this.skillDetail = data; } catch(e) { - OpenFangToast.error('Failed to load skill details'); + OMTAEToast.error('Failed to load skill details'); } this.detailLoading = false; }, @@ -390,12 +390,12 @@ function skillsPage() { } this.skillCodeLoading = true; try { - var data = await OpenFangAPI.get('/api/clawhub/skill/' + encodeURIComponent(slug) + '/code'); + var data = await OMTAEAPI.get('/api/clawhub/skill/' + encodeURIComponent(slug) + '/code'); this.skillCode = data.code || ''; this.skillCodeFilename = data.filename || 'source'; this.showSkillCode = true; } catch(e) { - OpenFangToast.error('Could not load skill source code'); + OMTAEToast.error('Could not load skill source code'); } this.skillCodeLoading = false; }, @@ -405,12 +405,12 @@ function skillsPage() { this.installingSlug = slug; this.installResult = null; try { - var data = await OpenFangAPI.post('/api/clawhub/install', { slug: slug }); + var data = await OMTAEAPI.post('/api/clawhub/install', { slug: slug }); this.installResult = data; if (data.warnings && data.warnings.length > 0) { - OpenFangToast.success('Skill "' + data.name + '" installed with ' + data.warnings.length + ' warning(s)'); + OMTAEToast.success('Skill "' + data.name + '" installed with ' + data.warnings.length + ' warning(s)'); } else { - OpenFangToast.success('Skill "' + data.name + '" installed successfully'); + OMTAEToast.success('Skill "' + data.name + '" installed successfully'); } // Update installed state in detail modal if open if (this.skillDetail && this.skillDetail.slug === slug) { @@ -420,11 +420,11 @@ function skillsPage() { } catch(e) { var msg = e.message || 'Install failed'; if (msg.includes('already_installed')) { - OpenFangToast.error('Skill is already installed'); + OMTAEToast.error('Skill is already installed'); } else if (msg.includes('SecurityBlocked')) { - OpenFangToast.error('Skill blocked by security scan'); + OMTAEToast.error('Skill blocked by security scan'); } else { - OpenFangToast.error('Install failed: ' + msg); + OMTAEToast.error('Install failed: ' + msg); } } this.installingSlug = null; @@ -434,16 +434,16 @@ function skillsPage() { uninstallSkill: function(name) { var self = this; var t = window.i18n ? window.i18n.t.bind(window.i18n) : function(k) { return k; }; - OpenFangToast.confirm( + OMTAEToast.confirm( t('skills.uninstall_skill') || 'Uninstall Skill', t('skills.uninstall_confirm') + ' "' + name + '"? This cannot be undone.', async function() { try { - await OpenFangAPI.post('/api/skills/uninstall', { name: name }); - OpenFangToast.success('Skill "' + name + '" uninstalled'); + await OMTAEAPI.post('/api/skills/uninstall', { name: name }); + OMTAEToast.success('Skill "' + name + '" uninstalled'); await self.loadSkills(); } catch(e) { - OpenFangToast.error('Failed to uninstall skill: ' + e.message); + OMTAEToast.error('Failed to uninstall skill: ' + e.message); } } ); @@ -452,17 +452,17 @@ function skillsPage() { // Create prompt-only skill async createDemoSkill(skill) { try { - await OpenFangAPI.post('/api/skills/create', { + await OMTAEAPI.post('/api/skills/create', { name: skill.name, description: skill.description, runtime: 'prompt_only', prompt_context: skill.prompt_context || skill.description }); - OpenFangToast.success('Skill "' + skill.name + '" created'); + OMTAEToast.success('Skill "' + skill.name + '" created'); this.tab = 'installed'; await this.loadSkills(); } catch(e) { - OpenFangToast.error('Failed to create skill: ' + e.message); + OMTAEToast.error('Failed to create skill: ' + e.message); } }, @@ -470,7 +470,7 @@ function skillsPage() { async loadMcpServers() { this.mcpLoading = true; try { - var data = await OpenFangAPI.get('/api/mcp/servers'); + var data = await OMTAEAPI.get('/api/mcp/servers'); this.mcpServers = data; } catch(e) { this.mcpServers = { configured: [], connected: [], total_configured: 0, total_connected: 0 }; diff --git a/crates/openfang-api/static/js/pages/usage.js b/crates/openfang-api/static/js/pages/usage.js index c2ea5b6ca5..98d849e121 100644 --- a/crates/openfang-api/static/js/pages/usage.js +++ b/crates/openfang-api/static/js/pages/usage.js @@ -1,4 +1,4 @@ -// OpenFang Analytics Page — Full usage analytics with per-model and per-agent breakdowns +// OMTAE Analytics Page — Full usage analytics with per-model and per-agent breakdowns // Includes Cost Dashboard with donut chart, bar chart, projections, and provider breakdown. 'use strict'; @@ -18,7 +18,7 @@ function analyticsPage() { // Chart colors for providers (stable palette) _chartColors: [ - '#FF5C00', '#3B82F6', '#10B981', '#F59E0B', '#8B5CF6', + '#00F0FF', '#3B82F6', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899', '#06B6D4', '#EF4444', '#84CC16', '#F97316', '#6366F1', '#14B8A6', '#E11D48', '#A855F7', '#22D3EE' ], @@ -43,7 +43,7 @@ function analyticsPage() { async loadSummary() { try { - this.summary = await OpenFangAPI.get('/api/usage/summary'); + this.summary = await OMTAEAPI.get('/api/usage/summary'); } catch(e) { this.summary = { total_input_tokens: 0, total_output_tokens: 0, total_cost_usd: 0, call_count: 0, total_tool_calls: 0 }; throw e; @@ -52,21 +52,21 @@ function analyticsPage() { async loadByModel() { try { - var data = await OpenFangAPI.get('/api/usage/by-model'); + var data = await OMTAEAPI.get('/api/usage/by-model'); this.byModel = data.models || []; } catch(e) { this.byModel = []; } }, async loadByAgent() { try { - var data = await OpenFangAPI.get('/api/usage'); + var data = await OMTAEAPI.get('/api/usage'); this.byAgent = data.agents || []; } catch(e) { this.byAgent = []; } }, async loadDailyCosts() { try { - var data = await OpenFangAPI.get('/api/usage/daily'); + var data = await OMTAEAPI.get('/api/usage/daily'); this.dailyCosts = data.days || []; this.todayCost = data.today_cost_usd || 0; this.firstEventDate = data.first_event_date || null; diff --git a/crates/openfang-api/static/js/pages/wizard.js b/crates/openfang-api/static/js/pages/wizard.js index a0a77c90fa..dd7ad1f0ab 100644 --- a/crates/openfang-api/static/js/pages/wizard.js +++ b/crates/openfang-api/static/js/pages/wizard.js @@ -1,4 +1,4 @@ -// OpenFang Setup Wizard — First-run guided setup (Provider + Agent + Channel) +// OMTAE Setup Wizard — First-run guided setup (Provider + Agent + Channel) 'use strict'; /** Escape a string for use inside TOML triple-quoted strings ("""\n...\n"""). */ @@ -11,6 +11,18 @@ function wizardTomlBasicEscape(s) { return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t'); } +/** i18n lookup with English fallback (works before /i18n/*.json loads). */ +function wizardTr(key, fallback) { + if (window.i18n && typeof window.i18n.tr === 'function') { + return window.i18n.tr(key, fallback); + } + if (window.i18n && typeof window.i18n.t === 'function') { + var v = window.i18n.t(key); + return v !== key ? v : fallback; + } + return fallback; +} + function wizardPage() { return { step: 1, @@ -177,32 +189,31 @@ function wizardPage() { tryItInput: '', tryItSending: false, get suggestedMessages() { - var t = window.i18n ? window.i18n.t.bind(window.i18n) : function(k) { return k; }; return { 'General': [ - t('wizard.suggestions.general.1') || 'What can you help me with?', - t('wizard.suggestions.general.2') || 'Tell me a fun fact', - t('wizard.suggestions.general.3') || 'Summarize the latest AI news' + wizardTr('wizard.suggestions.general.1', 'What can you help me with?'), + wizardTr('wizard.suggestions.general.2', 'Tell me a fun fact'), + wizardTr('wizard.suggestions.general.3', 'Summarize the latest AI news') ], 'Development': [ - t('wizard.suggestions.development.1') || 'Write a Python hello world', - t('wizard.suggestions.development.2') || 'Explain async/await', - t('wizard.suggestions.development.3') || 'Review this code snippet' + wizardTr('wizard.suggestions.development.1', 'Write a Python hello world'), + wizardTr('wizard.suggestions.development.2', 'Explain async/await'), + wizardTr('wizard.suggestions.development.3', 'Review this code snippet') ], 'Research': [ - t('wizard.suggestions.research.1') || 'Explain quantum computing simply', - t('wizard.suggestions.research.2') || 'Compare React vs Vue', - t('wizard.suggestions.research.3') || 'What are the latest trends in AI?' + wizardTr('wizard.suggestions.research.1', 'Explain quantum computing simply'), + wizardTr('wizard.suggestions.research.2', 'Compare React vs Vue'), + wizardTr('wizard.suggestions.research.3', 'What are the latest trends in AI?') ], 'Writing': [ - t('wizard.suggestions.writing.1') || 'Help me write a professional email', - t('wizard.suggestions.writing.2') || 'Improve this paragraph', - t('wizard.suggestions.writing.3') || 'Write a blog intro about AI' + wizardTr('wizard.suggestions.writing.1', 'Help me write a professional email'), + wizardTr('wizard.suggestions.writing.2', 'Improve this paragraph'), + wizardTr('wizard.suggestions.writing.3', 'Write a blog intro about AI') ], 'Business': [ - t('wizard.suggestions.business.1') || 'Draft a meeting agenda', - t('wizard.suggestions.business.2') || 'How do I handle a complaint?', - t('wizard.suggestions.business.3') || 'Create a project status update' + wizardTr('wizard.suggestions.business.1', 'Draft a meeting agenda'), + wizardTr('wizard.suggestions.business.2', 'How do I handle a complaint?'), + wizardTr('wizard.suggestions.business.3', 'Create a project status update') ] }; }, @@ -218,7 +229,7 @@ function wizardPage() { this.tryItMessages.push({ role: 'user', text: text }); this.tryItSending = true; try { - var res = await OpenFangAPI.post('/api/agents/' + this.createdAgent.id + '/message', { message: text }); + var res = await OMTAEAPI.post('/api/agents/' + this.createdAgent.id + '/message', { message: text }); this.tryItMessages.push({ role: 'agent', text: res.response || '(no response)' }); localStorage.setItem('of-first-msg', 'true'); } catch(e) { @@ -284,9 +295,14 @@ function wizardPage() { await this.loadProviders(); // Pre-select first unconfigured provider, or first one var unconfigured = this.providers.filter(function(p) { - return p.auth_status !== 'configured' && p.api_key_env; + return p.auth_status !== 'configured' && p.api_key_env && p.key_required !== false; }); - if (unconfigured.length > 0) { + var vllm = this.providers.find(function(p) { return p.id === 'vllm'; }); + if (vllm && (vllm.auth_status === 'configured' || vllm.key_required === false)) { + this.selectedProvider = 'vllm'; + this.keySaved = true; + this.setupSummary.provider = vllm.display_name; + } else if (unconfigured.length > 0) { this.selectedProvider = unconfigured[0].id; } else if (this.providers.length > 0) { this.selectedProvider = this.providers[0].id; @@ -338,7 +354,11 @@ function wizardPage() { }, get canGoNext() { - if (this.step === 2) return this.keySaved || this.hasConfiguredProvider || this.claudeCodeDetected; + if (this.step === 2) { + var p = this.selectedProviderObj; + if (p && (p.key_required === false || p.auth_status === 'configured')) return true; + return this.keySaved || this.hasConfiguredProvider || this.claudeCodeDetected; + } if (this.step === 3) return this.agentName.trim().length > 0; return true; }, @@ -356,7 +376,7 @@ function wizardPage() { async loadProviders() { try { - var data = await OpenFangAPI.get('/api/providers'); + var data = await OMTAEAPI.get('/api/providers'); this.providers = data.providers || []; } catch(e) { this.providers = []; } }, @@ -416,23 +436,29 @@ function wizardPage() { async saveKey() { var provider = this.selectedProviderObj; if (!provider) return; + if (provider.key_required === false) { + this.keySaved = true; + this.setupSummary.provider = provider.display_name; + await this.testKey(); + return; + } var key = this.apiKeyInput.trim(); if (!key) { - OpenFangToast.error(window.i18n ? window.i18n.t('wizard.enter_api_key') : 'Please enter an API key'); + OMTAEToast.error(window.i18n ? window.i18n.t('wizard.enter_api_key') : 'Please enter an API key'); return; } this.savingKey = true; try { - await OpenFangAPI.post('/api/providers/' + encodeURIComponent(provider.id) + '/key', { key: key }); + await OMTAEAPI.post('/api/providers/' + encodeURIComponent(provider.id) + '/key', { key: key }); this.apiKeyInput = ''; this.keySaved = true; this.setupSummary.provider = provider.display_name; - OpenFangToast.success((window.i18n ? window.i18n.t('wizard.api_key_saved') : 'API key saved for') + ' ' + provider.display_name); + OMTAEToast.success((window.i18n ? window.i18n.t('wizard.api_key_saved') : 'API key saved for') + ' ' + provider.display_name); await this.loadProviders(); // Auto-test after saving await this.testKey(); } catch(e) { - OpenFangToast.error((window.i18n ? window.i18n.t('wizard.failed_save_key') : 'Failed to save key:') + ' ' + e.message); + OMTAEToast.error((window.i18n ? window.i18n.t('wizard.failed_save_key') : 'Failed to save key:') + ' ' + e.message); } this.savingKey = false; }, @@ -443,16 +469,16 @@ function wizardPage() { this.testingProvider = true; this.testResult = null; try { - var result = await OpenFangAPI.post('/api/providers/' + encodeURIComponent(provider.id) + '/test', {}); + var result = await OMTAEAPI.post('/api/providers/' + encodeURIComponent(provider.id) + '/test', {}); this.testResult = result; if (result.status === 'ok') { - OpenFangToast.success(provider.display_name + ' ' + (window.i18n ? window.i18n.t('wizard.connected') : 'connected') + ' (' + (result.latency_ms || '?') + 'ms)'); + OMTAEToast.success(provider.display_name + ' ' + (window.i18n ? window.i18n.t('wizard.connected') : 'connected') + ' (' + (result.latency_ms || '?') + 'ms)'); } else { - OpenFangToast.error(provider.display_name + ': ' + (result.error || (window.i18n ? window.i18n.t('wizard.connection_failed') : 'Connection failed'))); + OMTAEToast.error(provider.display_name + ': ' + (result.error || (window.i18n ? window.i18n.t('wizard.connection_failed') : 'Connection failed'))); } } catch(e) { this.testResult = { status: 'error', error: e.message }; - OpenFangToast.error((window.i18n ? window.i18n.t('wizard.test_failed') : 'Test failed:') + ' ' + e.message); + OMTAEToast.error((window.i18n ? window.i18n.t('wizard.test_failed') : 'Test failed:') + ' ' + e.message); } this.testingProvider = false; }, @@ -461,20 +487,20 @@ function wizardPage() { this.testingProvider = true; this.testResult = null; try { - var result = await OpenFangAPI.post('/api/providers/claude-code/test', {}); + var result = await OMTAEAPI.post('/api/providers/claude-code/test', {}); this.testResult = result; if (result.status === 'ok') { this.claudeCodeDetected = true; this.keySaved = true; this.setupSummary.provider = 'Claude Code'; - OpenFangToast.success('Claude Code detected (' + (result.latency_ms || '?') + 'ms)'); + OMTAEToast.success('Claude Code detected (' + (result.latency_ms || '?') + 'ms)'); } else { this.testResult = { status: 'error', error: 'Claude Code CLI not detected' }; - OpenFangToast.error('Claude Code CLI not detected. Make sure you\'ve run: npm install -g @anthropic-ai/claude-code && claude auth'); + OMTAEToast.error('Claude Code CLI not detected. Make sure you\'ve run: npm install -g @anthropic-ai/claude-code && claude auth'); } } catch(e) { this.testResult = { status: 'error', error: e.message }; - OpenFangToast.error('Claude Code CLI not detected. Make sure you\'ve run: npm install -g @anthropic-ai/claude-code && claude auth'); + OMTAEToast.error('Claude Code CLI not detected. Make sure you\'ve run: npm install -g @anthropic-ai/claude-code && claude auth'); } this.testingProvider = false; }, @@ -494,7 +520,7 @@ function wizardPage() { if (!tpl) return; var name = this.agentName.trim(); if (!name) { - OpenFangToast.error(window.i18n ? window.i18n.t('wizard.enter_agent_name') : 'Please enter a name for your agent'); + OMTAEToast.error(window.i18n ? window.i18n.t('wizard.enter_agent_name') : 'Please enter a name for your agent'); return; } @@ -507,27 +533,29 @@ function wizardPage() { model = this.defaultModelForProvider(provider) || tpl.model; } - var toml = '[agent]\n'; - toml += 'name = "' + wizardTomlBasicEscape(name) + '"\n'; + var toml = 'name = "' + wizardTomlBasicEscape(name) + '"\n'; toml += 'description = "' + wizardTomlBasicEscape(tpl.description) + '"\n'; + toml += 'module = "builtin:chat"\n'; toml += 'profile = "' + tpl.profile + '"\n\n'; toml += '[model]\nprovider = "' + provider + '"\n'; toml += 'model = "' + model + '"\n'; + // Keep headroom on 8k-context local models (default 4096 max_tokens overflows). + toml += 'max_tokens = 2048\n'; toml += 'system_prompt = """\n' + wizardTomlMultilineEscape(tpl.system_prompt) + '\n"""\n'; this.creatingAgent = true; try { - var res = await OpenFangAPI.post('/api/agents', { manifest_toml: toml }); + var res = await OMTAEAPI.post('/api/agents', { manifest_toml: toml }); if (res.agent_id) { this.createdAgent = { id: res.agent_id, name: res.name || name }; this.setupSummary.agent = res.name || name; - OpenFangToast.success((window.i18n ? window.i18n.t('wizard.agent_created') : 'Agent') + ' "' + (res.name || name) + '" ' + (window.i18n ? window.i18n.t('wizard.agent_created_suffix') || 'created' : 'created')); + OMTAEToast.success((window.i18n ? window.i18n.t('wizard.agent_created') : 'Agent') + ' "' + (res.name || name) + '" ' + (window.i18n ? window.i18n.t('wizard.agent_created_suffix') || 'created' : 'created')); await Alpine.store('app').refreshAgents(); } else { - OpenFangToast.error((window.i18n ? window.i18n.t('wizard.failed_create_agent') : 'Failed:') + ' ' + (res.error || 'Unknown error')); + OMTAEToast.error((window.i18n ? window.i18n.t('wizard.failed_create_agent') : 'Failed:') + ' ' + (res.error || 'Unknown error')); } } catch(e) { - OpenFangToast.error((window.i18n ? window.i18n.t('wizard.failed_create_agent') : 'Failed to create agent:') + ' ' + e.message); + OMTAEToast.error((window.i18n ? window.i18n.t('wizard.failed_create_agent') : 'Failed to create agent:') + ' ' + e.message); } this.creatingAgent = false; }, @@ -574,7 +602,7 @@ function wizardPage() { if (!ch) return; var token = this.channelToken.trim(); if (!token) { - OpenFangToast.error((window.i18n ? window.i18n.t('wizard.enter_token') : 'Please enter the') + ' ' + ch.token_label); + OMTAEToast.error((window.i18n ? window.i18n.t('wizard.enter_token') : 'Please enter the') + ' ' + ch.token_label); return; } this.configuringChannel = true; @@ -582,12 +610,12 @@ function wizardPage() { var fields = {}; fields[ch.token_env.toLowerCase()] = token; fields.token = token; - await OpenFangAPI.post('/api/channels/' + ch.name + '/configure', { fields: fields }); + await OMTAEAPI.post('/api/channels/' + ch.name + '/configure', { fields: fields }); this.channelConfigured = true; this.setupSummary.channel = ch.display_name; - OpenFangToast.success(ch.display_name + ' ' + (window.i18n ? window.i18n.t('wizard.channel_configured') : 'configured and activated.')); + OMTAEToast.success(ch.display_name + ' ' + (window.i18n ? window.i18n.t('wizard.channel_configured') : 'configured and activated.')); } catch(e) { - OpenFangToast.error((window.i18n ? window.i18n.t('wizard.failed_configure') : 'Failed:') + ' ' + (e.message || 'Unknown error')); + OMTAEToast.error((window.i18n ? window.i18n.t('wizard.failed_configure') : 'Failed:') + ' ' + (e.message || 'Unknown error')); } this.configuringChannel = false; }, @@ -595,7 +623,7 @@ function wizardPage() { // ── Step 6: Finish ── finish() { - localStorage.setItem('openfang-onboarded', 'true'); + localStorage.setItem('omtae-onboarded', 'true'); Alpine.store('app').showOnboarding = false; // Navigate to agents with chat if an agent was created, otherwise overview if (this.createdAgent) { @@ -608,7 +636,7 @@ function wizardPage() { }, finishAndDismiss() { - localStorage.setItem('openfang-onboarded', 'true'); + localStorage.setItem('omtae-onboarded', 'true'); Alpine.store('app').showOnboarding = false; window.location.hash = 'overview'; } diff --git a/crates/openfang-api/static/js/pages/workflow-builder.js b/crates/openfang-api/static/js/pages/workflow-builder.js index 45f89cd35c..797e478236 100644 --- a/crates/openfang-api/static/js/pages/workflow-builder.js +++ b/crates/openfang-api/static/js/pages/workflow-builder.js @@ -1,4 +1,4 @@ -// OpenFang Visual Workflow Builder — Drag-and-drop workflow designer +// OMTAE Visual Workflow Builder — Drag-and-drop workflow designer 'use strict'; function workflowBuilder() { @@ -48,7 +48,7 @@ function workflowBuilder() { var self = this; // Load agents for the agent step dropdown try { - var list = await OpenFangAPI.get('/api/agents'); + var list = await OMTAEAPI.get('/api/agents'); self.agents = Array.isArray(list) ? list : []; } catch(_) { self.agents = []; @@ -558,15 +558,15 @@ function workflowBuilder() { steps.push(step); } try { - await OpenFangAPI.post('/api/workflows', { + await OMTAEAPI.post('/api/workflows', { name: this.workflowName || 'untitled', description: this.workflowDescription || '', steps: steps }); - OpenFangToast.success('Workflow saved!'); + OMTAEToast.success('Workflow saved!'); this.showSaveModal = false; } catch(e) { - OpenFangToast.error('Failed to save: ' + e.message); + OMTAEToast.error('Failed to save: ' + e.message); } }, diff --git a/crates/openfang-api/static/js/pages/workflows.js b/crates/openfang-api/static/js/pages/workflows.js index 79610ba3c9..1acd74caf3 100644 --- a/crates/openfang-api/static/js/pages/workflows.js +++ b/crates/openfang-api/static/js/pages/workflows.js @@ -1,4 +1,4 @@ -// OpenFang Workflows Page — Workflow builder + run history +// OMTAE Workflows Page — Workflow builder + run history 'use strict'; function workflowsPage() { @@ -21,7 +21,7 @@ function workflowsPage() { this.loading = true; this.loadError = ''; try { - this.workflows = await OpenFangAPI.get('/api/workflows'); + this.workflows = await OMTAEAPI.get('/api/workflows'); } catch(e) { this.workflows = []; this.loadError = e.message || 'Could not load workflows.'; @@ -37,13 +37,13 @@ function workflowsPage() { }); try { var wfName = this.newWf.name; - await OpenFangAPI.post('/api/workflows', { name: wfName, description: this.newWf.description, steps: steps }); + await OMTAEAPI.post('/api/workflows', { name: wfName, description: this.newWf.description, steps: steps }); this.showCreateModal = false; this.newWf = { name: '', description: '', steps: [{ name: '', agent_name: '', mode: 'sequential', prompt: '{{input}}' }] }; - OpenFangToast.success('Workflow "' + wfName + '" created'); + OMTAEToast.success('Workflow "' + wfName + '" created'); await this.loadWorkflows(); } catch(e) { - OpenFangToast.error('Failed to create workflow: ' + e.message); + OMTAEToast.error('Failed to create workflow: ' + e.message); } }, @@ -58,40 +58,40 @@ function workflowsPage() { this.running = true; this.runResult = ''; try { - var res = await OpenFangAPI.post('/api/workflows/' + this.runModal.id + '/run', { input: this.runInput }); + var res = await OMTAEAPI.post('/api/workflows/' + this.runModal.id + '/run', { input: this.runInput }); this.runResult = res.output || JSON.stringify(res, null, 2); - OpenFangToast.success('Workflow completed'); + OMTAEToast.success('Workflow completed'); } catch(e) { this.runResult = 'Error: ' + e.message; - OpenFangToast.error('Workflow failed: ' + e.message); + OMTAEToast.error('Workflow failed: ' + e.message); } this.running = false; }, async viewRuns(wf) { try { - var runs = await OpenFangAPI.get('/api/workflows/' + wf.id + '/runs'); + var runs = await OMTAEAPI.get('/api/workflows/' + wf.id + '/runs'); this.runResult = JSON.stringify(runs, null, 2); this.runModal = wf; } catch(e) { - OpenFangToast.error('Failed to load run history: ' + e.message); + OMTAEToast.error('Failed to load run history: ' + e.message); } }, async deleteWorkflow(wf) { if (!confirm('Delete workflow "' + wf.name + '"? This cannot be undone.')) return; try { - await OpenFangAPI.delete('/api/workflows/' + wf.id); - OpenFangToast.success('Workflow "' + wf.name + '" deleted'); + await OMTAEAPI.delete('/api/workflows/' + wf.id); + OMTAEToast.success('Workflow "' + wf.name + '" deleted'); await this.loadWorkflows(); } catch(e) { - OpenFangToast.error('Failed to delete workflow: ' + e.message); + OMTAEToast.error('Failed to delete workflow: ' + e.message); } }, async showEditModal(wf) { try { - var full = await OpenFangAPI.get('/api/workflows/' + wf.id); + var full = await OMTAEAPI.get('/api/workflows/' + wf.id); this.editWf = { name: full.name || '', description: full.description || '', @@ -109,7 +109,7 @@ function workflowsPage() { } this.editModal = wf; } catch(e) { - OpenFangToast.error('Failed to load workflow: ' + e.message); + OMTAEToast.error('Failed to load workflow: ' + e.message); } }, @@ -120,12 +120,12 @@ function workflowsPage() { }); try { var wfName = this.editWf.name; - await OpenFangAPI.put('/api/workflows/' + this.editModal.id, { name: wfName, description: this.editWf.description, steps: steps }); + await OMTAEAPI.put('/api/workflows/' + this.editModal.id, { name: wfName, description: this.editWf.description, steps: steps }); this.editModal = null; - OpenFangToast.success('Workflow "' + wfName + '" updated'); + OMTAEToast.success('Workflow "' + wfName + '" updated'); await this.loadWorkflows(); } catch(e) { - OpenFangToast.error('Failed to update workflow: ' + e.message); + OMTAEToast.error('Failed to update workflow: ' + e.message); } } }; diff --git a/crates/openfang-api/static/logo.png b/crates/openfang-api/static/logo.png index cfe72c0b5c..281c32b625 100644 Binary files a/crates/openfang-api/static/logo.png and b/crates/openfang-api/static/logo.png differ diff --git a/crates/openfang-api/static/manifest.json b/crates/openfang-api/static/manifest.json index fa230c071a..d94be03082 100644 --- a/crates/openfang-api/static/manifest.json +++ b/crates/openfang-api/static/manifest.json @@ -1,12 +1,12 @@ { - "name": "OpenFang Agent OS", - "short_name": "OpenFang", + "name": "OMTAE Agent OS", + "short_name": "OMTAE", "description": "Open-source Agent Operating System", "start_url": "/", "display": "standalone", - "background_color": "#0a0a0f", - "theme_color": "#6366f1", + "background_color": "#171816", + "theme_color": "#5F6556", "icons": [ - {"src": "/logo.png", "sizes": "128x128", "type": "image/png"} + {"src": "/logo.png", "sizes": "64x64", "type": "image/png"} ] } diff --git a/crates/openfang-api/tests/api_integration_test.rs b/crates/openfang-api/tests/api_integration_test.rs index d31a2ef651..20464c6e40 100644 --- a/crates/openfang-api/tests/api_integration_test.rs +++ b/crates/openfang-api/tests/api_integration_test.rs @@ -1,18 +1,18 @@ -//! Real HTTP integration tests for the OpenFang API. +//! Real HTTP integration tests for the OMTAE API. //! //! These tests boot a real kernel, start a real axum HTTP server on a random //! port, and hit actual endpoints with reqwest. No mocking. //! //! Tests that require an LLM API call are gated behind GROQ_API_KEY. //! -//! Run: cargo test -p openfang-api --test api_integration_test -- --nocapture +//! Run: cargo test -p omtae-api --test api_integration_test -- --nocapture use axum::Router; -use openfang_api::middleware; -use openfang_api::routes::{self, AppState}; -use openfang_api::ws; -use openfang_kernel::OpenFangKernel; -use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use omtae_api::middleware; +use omtae_api::routes::{self, AppState}; +use omtae_api::ws; +use omtae_kernel::OMTAEKernel; +use omtae_types::config::{DefaultModelConfig, KernelConfig}; use std::sync::Arc; use std::time::Instant; use tower_http::cors::CorsLayer; @@ -66,7 +66,7 @@ async fn start_test_server_with_provider( ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let kernel = Arc::new(kernel); kernel.set_self_handle(); @@ -78,7 +78,7 @@ async fn start_test_server_with_provider( channels_config: tokio::sync::RwLock::new(Default::default()), shutdown_notify: Arc::new(tokio::sync::Notify::new()), clawhub_cache: dashmap::DashMap::new(), - provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), + provider_probe_cache: omtae_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), }); @@ -324,7 +324,7 @@ async fn test_list_agents_includes_inferencing_flag() { assert_eq!(resp.status(), 201); let body: serde_json::Value = resp.json().await.unwrap(); let agent_id_str = body["agent_id"].as_str().unwrap().to_string(); - let agent_id: openfang_types::agent::AgentId = agent_id_str.parse().unwrap(); + let agent_id: omtae_types::agent::AgentId = agent_id_str.parse().unwrap(); // Baseline: idle agent must report is_inferencing = false. let resp = client @@ -429,7 +429,7 @@ async fn test_agent_session_empty() { /// mode opt-in). #[tokio::test] async fn test_agent_session_filters_system_messages() { - use openfang_types::message::{Message, Role}; + use omtae_types::message::{Message, Role}; let server = start_test_server().await; let client = reqwest::Client::new(); @@ -447,7 +447,7 @@ async fn test_agent_session_filters_system_messages() { // Look up the agent's session id and inject a forged history that // contains a system-role message (simulating what an OpenAI-compat // client could push, or what a future regression might persist). - let agent_id: openfang_types::agent::AgentId = agent_id_str.parse().unwrap(); + let agent_id: omtae_types::agent::AgentId = agent_id_str.parse().unwrap(); let entry = server.state.kernel.registry.get(agent_id).unwrap(); let session_id = entry.session_id; let mut session = server @@ -461,7 +461,7 @@ async fn test_agent_session_filters_system_messages() { session.messages = vec![ Message { role: Role::System, - content: openfang_types::message::MessageContent::Text( + content: omtae_types::message::MessageContent::Text( "INTERNAL SYSTEM PROMPT — must not leak to UI".to_string(), ), ..Default::default() @@ -911,7 +911,7 @@ async fn start_test_server_with_auth(api_key: &str) -> TestServer { ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let kernel = Arc::new(kernel); kernel.set_self_handle(); @@ -923,18 +923,18 @@ async fn start_test_server_with_auth(api_key: &str) -> TestServer { channels_config: tokio::sync::RwLock::new(Default::default()), shutdown_notify: Arc::new(tokio::sync::Notify::new()), clawhub_cache: dashmap::DashMap::new(), - provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), + provider_probe_cache: omtae_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), }); - let api_key = state.kernel.config.api_key.trim().to_string(); + let api_key = state.kernel.config.effective_api_key_for_auth(); let auth_state = middleware::AuthState { api_key: api_key.clone(), - auth_enabled: state.kernel.config.auth.enabled, - session_secret: if !api_key.is_empty() { - api_key.clone() - } else if state.kernel.config.auth.enabled { - state.kernel.config.auth.password_hash.clone() + raw_api_key: state.kernel.config.api_key.trim().to_string(), + auth_enabled: state.kernel.config.dashboard_auth_enabled(), + session_secret: state.kernel.config.dashboard_session_secret(), + dashboard_pin: if state.kernel.config.dashboard.pin_auth_active() { + state.kernel.config.dashboard.pin.trim().to_string() } else { String::new() }, @@ -1286,7 +1286,7 @@ async fn test_schedules_delivery_targets_roundtrip() { let delivery_targets = serde_json::json!([ { "type": "channel", "channel_type": "telegram", "recipient": "chat_12345" }, { "type": "webhook", "url": "https://example.com/hook", "auth_header": "Bearer abc" }, - { "type": "local_file", "path": "/tmp/openfang-test.log", "append": true }, + { "type": "local_file", "path": "/tmp/omtae-test.log", "append": true }, { "type": "email", "to": "alice@example.com", "subject_template": "Cron: {job}" }, ]); diff --git a/crates/openfang-api/tests/daemon_lifecycle_test.rs b/crates/openfang-api/tests/daemon_lifecycle_test.rs index c62cba9b7a..f8d0eb36f8 100644 --- a/crates/openfang-api/tests/daemon_lifecycle_test.rs +++ b/crates/openfang-api/tests/daemon_lifecycle_test.rs @@ -4,11 +4,11 @@ //! and graceful shutdown sequence. use axum::Router; -use openfang_api::middleware; -use openfang_api::routes::{self, AppState}; -use openfang_api::server::{read_daemon_info, DaemonInfo}; -use openfang_kernel::OpenFangKernel; -use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use omtae_api::middleware; +use omtae_api::routes::{self, AppState}; +use omtae_api::server::{read_daemon_info, DaemonInfo}; +use omtae_kernel::OMTAEKernel; +use omtae_types::config::{DefaultModelConfig, KernelConfig}; use std::sync::Arc; use std::time::Instant; use tower_http::cors::CorsLayer; @@ -103,7 +103,7 @@ async fn test_full_daemon_lifecycle() { ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let kernel = Arc::new(kernel); kernel.set_self_handle(); @@ -115,7 +115,7 @@ async fn test_full_daemon_lifecycle() { channels_config: tokio::sync::RwLock::new(Default::default()), shutdown_notify: Arc::new(tokio::sync::Notify::new()), clawhub_cache: dashmap::DashMap::new(), - provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), + provider_probe_cache: omtae_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), }); @@ -231,7 +231,7 @@ async fn test_server_immediate_responsiveness() { ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).unwrap(); + let kernel = OMTAEKernel::boot_with_config(config).unwrap(); let kernel = Arc::new(kernel); let state = Arc::new(AppState { @@ -242,7 +242,7 @@ async fn test_server_immediate_responsiveness() { channels_config: tokio::sync::RwLock::new(Default::default()), shutdown_notify: Arc::new(tokio::sync::Notify::new()), clawhub_cache: dashmap::DashMap::new(), - provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), + provider_probe_cache: omtae_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), }); diff --git a/crates/openfang-api/tests/load_test.rs b/crates/openfang-api/tests/load_test.rs index c0bfc9ae59..8d9fd6fd79 100644 --- a/crates/openfang-api/tests/load_test.rs +++ b/crates/openfang-api/tests/load_test.rs @@ -1,15 +1,15 @@ -//! Load & performance tests for the OpenFang API. +//! Load & performance tests for the OMTAE API. //! //! Measures throughput under concurrent access: agent spawning, API endpoint //! latency, session management, and memory usage. //! -//! Run: cargo test -p openfang-api --test load_test -- --nocapture +//! Run: cargo test -p omtae-api --test load_test -- --nocapture use axum::Router; -use openfang_api::middleware; -use openfang_api::routes::{self, AppState}; -use openfang_kernel::OpenFangKernel; -use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use omtae_api::middleware; +use omtae_api::routes::{self, AppState}; +use omtae_kernel::OMTAEKernel; +use omtae_types::config::{DefaultModelConfig, KernelConfig}; use std::sync::Arc; use std::time::{Duration, Instant}; use tower_http::cors::CorsLayer; @@ -47,7 +47,7 @@ async fn start_test_server() -> TestServer { ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let kernel = Arc::new(kernel); kernel.set_self_handle(); @@ -59,7 +59,7 @@ async fn start_test_server() -> TestServer { channels_config: tokio::sync::RwLock::new(Default::default()), shutdown_notify: Arc::new(tokio::sync::Notify::new()), clawhub_cache: dashmap::DashMap::new(), - provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), + provider_probe_cache: omtae_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), }); @@ -575,7 +575,7 @@ async fn load_metrics_sustained() { .unwrap(); assert_eq!(res.status().as_u16(), 200); let body = res.text().await.unwrap(); - assert!(body.contains("openfang_agents_active")); + assert!(body.contains("omtae_agents_active")); } let elapsed = start.elapsed(); diff --git a/crates/openfang-api/tests/skill_config_api_test.rs b/crates/openfang-api/tests/skill_config_api_test.rs index e8a1e60a72..06c5a48077 100644 --- a/crates/openfang-api/tests/skill_config_api_test.rs +++ b/crates/openfang-api/tests/skill_config_api_test.rs @@ -6,13 +6,13 @@ //! no runtime config, so the synthetic skill fixture is what lets us prove //! the wire contract. //! -//! Run: cargo test -p openfang-api --test skill_config_api_test -- --nocapture +//! Run: cargo test -p omtae-api --test skill_config_api_test -- --nocapture use axum::Router; -use openfang_api::middleware; -use openfang_api::routes::{self, AppState}; -use openfang_kernel::OpenFangKernel; -use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use omtae_api::middleware; +use omtae_api::routes::{self, AppState}; +use omtae_kernel::OMTAEKernel; +use omtae_types::config::{DefaultModelConfig, KernelConfig}; use std::sync::Arc; use std::time::Instant; use tower_http::cors::CorsLayer; @@ -84,7 +84,7 @@ async fn start_test_server() -> TestServer { // Plant synthetic skill BEFORE booting so the initial skill load picks it up. plant_skill_with_config(&home, "test-config-skill"); - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boot"); let kernel = Arc::new(kernel); kernel.set_self_handle(); @@ -96,7 +96,7 @@ async fn start_test_server() -> TestServer { channels_config: tokio::sync::RwLock::new(Default::default()), shutdown_notify: Arc::new(tokio::sync::Notify::new()), clawhub_cache: dashmap::DashMap::new(), - provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), + provider_probe_cache: omtae_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), }); diff --git a/crates/openfang-channels/Cargo.toml b/crates/openfang-channels/Cargo.toml index e0ef71d551..69488bf271 100644 --- a/crates/openfang-channels/Cargo.toml +++ b/crates/openfang-channels/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "openfang-channels" +name = "omtae-channels" version.workspace = true edition.workspace = true license.workspace = true -description = "Channel Bridge Layer — pluggable messaging integrations for OpenFang" +description = "Channel Bridge Layer — pluggable messaging integrations for OMTAE" [dependencies] -openfang-types = { path = "../openfang-types" } +omtae-types = { path = "../omtae-types" } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/openfang-channels/src/bluesky.rs b/crates/openfang-channels/src/bluesky.rs index 4943c6adb6..78c4938635 100644 --- a/crates/openfang-channels/src/bluesky.rs +++ b/crates/openfang-channels/src/bluesky.rs @@ -324,7 +324,7 @@ fn parse_bluesky_notification( sender: ChannelUser { platform_id: author_did.to_string(), display_name, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/bridge.rs b/crates/openfang-channels/src/bridge.rs index 2043aeaa76..da81566b72 100644 --- a/crates/openfang-channels/src/bridge.rs +++ b/crates/openfang-channels/src/bridge.rs @@ -1,6 +1,6 @@ -//! Channel bridge — connects channel adapters to the OpenFang kernel. +//! Channel bridge — connects channel adapters to the OMTAE kernel. //! -//! Defines `ChannelBridgeHandle` (implemented by openfang-api on the kernel) and +//! Defines `ChannelBridgeHandle` (implemented by omtae-api on the kernel) and //! `BridgeManager` which owns running adapters and dispatches messages. use crate::formatter; @@ -12,11 +12,11 @@ use crate::types::{ use async_trait::async_trait; use dashmap::DashMap; use futures::StreamExt; -use openfang_types::agent::AgentId; -use openfang_types::approval::ApprovalRequest; -use openfang_types::commands::{self as slash_commands, Surfaces}; -use openfang_types::config::{ChannelOverrides, DmPolicy, GroupPolicy, OutputFormat, PrefixStyle}; -use openfang_types::message::ContentBlock; +use omtae_types::agent::AgentId; +use omtae_types::approval::ApprovalRequest; +use omtae_types::commands::{self as slash_commands, Surfaces}; +use omtae_types::config::{ChannelOverrides, DmPolicy, GroupPolicy, OutputFormat, PrefixStyle}; +use omtae_types::message::ContentBlock; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::watch; @@ -70,7 +70,7 @@ fn is_channel_command(name: &str) -> bool { fn format_channel_help() -> String { let sections = ["General", "Session", "Info", "Automation", "Monitoring"]; - let mut msg = String::from("OpenFang Bot Commands:"); + let mut msg = String::from("OMTAE Bot Commands:"); for section in sections { let commands: Vec<&ChatCommandSpec> = channel_command_specs() @@ -95,8 +95,8 @@ fn format_channel_help() -> String { /// Kernel operations needed by channel adapters. /// -/// Defined here to avoid circular deps (openfang-channels can't depend on openfang-kernel). -/// Implemented in openfang-api on the actual kernel. +/// Defined here to avoid circular deps (omtae-channels can't depend on omtae-kernel). +/// Implemented in omtae-api on the actual kernel. #[async_trait] pub trait ChannelBridgeHandle: Send + Sync { /// Send a message to an agent and get the text response. @@ -685,14 +685,14 @@ fn sender_user_id(message: &ChannelMessage) -> &str { /// can route to dedicated agents. The channel ID source is delegated to /// [`ChannelMessage::channel_id`] — the single source of truth shared with /// config validation (see `CHANNELS_WITH_PLATFORM_ID_AS_CHANNEL` in -/// `openfang-types::config`). `peer_id` uses the resolved user ID, not +/// `omtae-types::config`). `peer_id` uses the resolved user ID, not /// `sender.platform_id`, so user-scoped bindings still match correctly on /// Discord/Slack/etc. where `platform_id` holds the channel. /// /// This replaces the earlier heuristic `sender_channel_id()` (which inferred /// "platform_id is the channel" from "metadata has `sender_user_id`"). The /// allowlist is explicit, the metadata-fallback path is documented, and -/// adapters can be added or removed in one place (`openfang-types::config`) +/// adapters can be added or removed in one place (`omtae-types::config`) /// without touching this file. fn binding_context_for(message: &ChannelMessage) -> BindingContext { BindingContext { @@ -1142,7 +1142,7 @@ async fn dispatch_message( let mut responses = Vec::new(); match strategy { - openfang_types::config::BroadcastStrategy::Parallel => { + omtae_types::config::BroadcastStrategy::Parallel => { let mut handles_vec = Vec::new(); for (name, maybe_id) in &targets { if let Some(aid) = maybe_id { @@ -1165,7 +1165,7 @@ async fn dispatch_message( } } } - openfang_types::config::BroadcastStrategy::Sequential => { + omtae_types::config::BroadcastStrategy::Sequential => { for (name, maybe_id) in &targets { if let Some(aid) = maybe_id { match handle.send_message(*aid, &text).await { @@ -1214,7 +1214,7 @@ async fn dispatch_message( router.resolve_with_context( &message.channel, sender_user_id(message), - message.sender.openfang_user.as_deref(), + message.sender.omtae_user.as_deref(), &binding_ctx, ) }); @@ -1610,7 +1610,7 @@ async fn download_image_to_blocks(url: &str, caption: Option<&str>) -> Vec { let agents = handle.list_agents().await.unwrap_or_default(); - let mut msg = "Welcome to OpenFang! I connect you to AI agents.\n\nAvailable agents:\n" + let mut msg = "Welcome to OMTAE! I connect you to AI agents.\n\nAvailable agents:\n" .to_string(); if agents.is_empty() { msg.push_str(" (none running)\n"); @@ -2017,7 +2017,7 @@ async fn handle_command( let agent_id = router.resolve( &crate::types::ChannelType::CLI, user_id, - sender.openfang_user.as_deref(), + sender.omtae_user.as_deref(), ); match agent_id { Some(aid) => handle @@ -2031,7 +2031,7 @@ async fn handle_command( let agent_id = router.resolve( &crate::types::ChannelType::CLI, user_id, - sender.openfang_user.as_deref(), + sender.omtae_user.as_deref(), ); match agent_id { Some(aid) => handle @@ -2045,7 +2045,7 @@ async fn handle_command( let agent_id = router.resolve( &crate::types::ChannelType::CLI, user_id, - sender.openfang_user.as_deref(), + sender.omtae_user.as_deref(), ); match agent_id { Some(aid) => { @@ -2069,7 +2069,7 @@ async fn handle_command( let agent_id = router.resolve( &crate::types::ChannelType::CLI, user_id, - sender.openfang_user.as_deref(), + sender.omtae_user.as_deref(), ); match agent_id { Some(aid) => handle @@ -2083,7 +2083,7 @@ async fn handle_command( let agent_id = router.resolve( &crate::types::ChannelType::CLI, user_id, - sender.openfang_user.as_deref(), + sender.omtae_user.as_deref(), ); match agent_id { Some(aid) => handle @@ -2097,7 +2097,7 @@ async fn handle_command( let agent_id = router.resolve( &crate::types::ChannelType::CLI, user_id, - sender.openfang_user.as_deref(), + sender.omtae_user.as_deref(), ); match agent_id { Some(aid) => { @@ -2262,7 +2262,7 @@ mod tests { let sender = ChannelUser { platform_id: "user1".to_string(), display_name: "Test".to_string(), - openfang_user: None, + omtae_user: None, }; let result = handle_command("agents", &[], &handle, &router, &sender, "user1").await; @@ -2282,7 +2282,7 @@ mod tests { let sender = ChannelUser { platform_id: "user1".to_string(), display_name: "Test".to_string(), - openfang_user: None, + omtae_user: None, }; // Select existing agent @@ -2317,7 +2317,7 @@ mod tests { let sender = ChannelUser { platform_id: "channel-123".to_string(), display_name: "Test".to_string(), - openfang_user: None, + omtae_user: None, }; let user_id = "user-789"; @@ -2354,7 +2354,7 @@ mod tests { let sender = ChannelUser { platform_id: "user1".to_string(), display_name: "Test".to_string(), - openfang_user: None, + omtae_user: None, }; let result = handle_command("agent", &[], &handle, &router, &sender, "user1").await; @@ -2437,7 +2437,7 @@ mod tests { sender: crate::types::ChannelUser { platform_id: platform_id.to_string(), display_name: "Tester".to_string(), - openfang_user: None, + omtae_user: None, }, content: ChannelContent::Text("hi".to_string()), target_agent: None, diff --git a/crates/openfang-channels/src/dingtalk.rs b/crates/openfang-channels/src/dingtalk.rs index 1875927a6b..3e814ffc93 100644 --- a/crates/openfang-channels/src/dingtalk.rs +++ b/crates/openfang-channels/src/dingtalk.rs @@ -211,7 +211,7 @@ impl ChannelAdapter for DingTalkAdapter { sender: ChannelUser { platform_id: sender_id, display_name: sender_nick, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/dingtalk_stream.rs b/crates/openfang-channels/src/dingtalk_stream.rs index dd854b4314..bbc09d3b73 100644 --- a/crates/openfang-channels/src/dingtalk_stream.rs +++ b/crates/openfang-channels/src/dingtalk_stream.rs @@ -368,7 +368,7 @@ async fn get_ws_endpoint( sub_type: "CALLBACK".to_string(), topic: "/v1.0/im/bot/messages/get".to_string(), }], - ua: "openfang/0.3", + ua: "omtae/0.3", local_ip: "", }; let resp: OpenConnectionResponse = http @@ -530,7 +530,7 @@ where sender: ChannelUser { platform_id: uid, display_name: cb.sender_nick, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/discord.rs b/crates/openfang-channels/src/discord.rs index 8b5ff0773c..8bc9b92338 100644 --- a/crates/openfang-channels/src/discord.rs +++ b/crates/openfang-channels/src/discord.rs @@ -1,4 +1,4 @@ -//! Discord Gateway adapter for the OpenFang channel bridge. +//! Discord Gateway adapter for the OMTAE channel bridge. //! //! Uses Discord Gateway WebSocket (v10) for receiving messages and the REST API //! for sending responses. No external Discord crate — just `tokio-tungstenite` + `reqwest`. @@ -483,8 +483,8 @@ impl ChannelAdapter for DiscordAdapter { "intents": intents, "properties": { "os": "linux", - "browser": "openfang", - "device": "openfang" + "browser": "omtae", + "device": "omtae" } } }) @@ -826,8 +826,8 @@ async fn parse_discord_message( ) -> Option { // Diagnostic: dump the raw Discord payload so we can ground attachment // parsing in real JSON. Gated by RUST_LOG; silent at default `info` level. - // Enable with: RUST_LOG=openfang_channels::discord=debug - debug!(target: "openfang_channels::discord", payload = %d, "discord raw message payload"); + // Enable with: RUST_LOG=omtae_channels::discord=debug + debug!(target: "omtae_channels::discord", payload = %d, "discord raw message payload"); let author = d.get("author")?; let author_id = author["id"].as_str()?; @@ -983,7 +983,7 @@ async fn parse_discord_message( sender: ChannelUser { platform_id: effective_channel_id, display_name, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, @@ -1067,7 +1067,7 @@ mod tests { "content": "My own message", "author": { "id": "bot123", - "username": "openfang", + "username": "omtae", "discriminator": "0" }, "timestamp": "2024-01-01T00:00:00+00:00" @@ -1130,7 +1130,7 @@ mod tests { "content": "My own message", "author": { "id": "bot123", - "username": "openfang", + "username": "omtae", "discriminator": "0", "bot": true }, @@ -1324,7 +1324,7 @@ mod tests { "channel_id": "ch1", "guild_id": "guild1", "content": "Hey <@bot123> help me", - "mentions": [{"id": "bot123", "username": "openfang"}], + "mentions": [{"id": "bot123", "username": "omtae"}], "author": { "id": "user1", "username": "alice", diff --git a/crates/openfang-channels/src/discourse.rs b/crates/openfang-channels/src/discourse.rs index acb27f4277..ab77b2c49a 100644 --- a/crates/openfang-channels/src/discourse.rs +++ b/crates/openfang-channels/src/discourse.rs @@ -300,7 +300,7 @@ impl ChannelAdapter for DiscourseAdapter { sender: ChannelUser { platform_id: username.to_string(), display_name, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/email.rs b/crates/openfang-channels/src/email.rs index d76b414a10..f57bd8bea5 100644 --- a/crates/openfang-channels/src/email.rs +++ b/crates/openfang-channels/src/email.rs @@ -405,7 +405,7 @@ impl ChannelAdapter for EmailAdapter { sender: ChannelUser { platform_id: from_addr.clone(), display_name: from_addr.clone(), - openfang_user: None, + omtae_user: None, }, content: ChannelContent::Text(text), target_agent: None, // Routing handled by bridge AgentRouter @@ -451,7 +451,7 @@ impl ChannelAdapter for EmailAdapter { let body = text[pos + 2..].to_string(); (subj, body) } else { - ("OpenFang Reply".to_string(), text) + ("OMTAE Reply".to_string(), text) } } else { // Check reply context for subject continuity @@ -459,7 +459,7 @@ impl ChannelAdapter for EmailAdapter { .reply_ctx .get(&to_addr) .map(|ctx| format!("Re: {}", ctx.subject)) - .unwrap_or_else(|| "OpenFang Reply".to_string()); + .unwrap_or_else(|| "OMTAE Reply".to_string()); (subj, text) }; diff --git a/crates/openfang-channels/src/feishu.rs b/crates/openfang-channels/src/feishu.rs index 7a19a035c9..aec6aea09c 100644 --- a/crates/openfang-channels/src/feishu.rs +++ b/crates/openfang-channels/src/feishu.rs @@ -643,7 +643,7 @@ impl FeishuAdapter { sender: ChannelUser { platform_id: chat_id, display_name: open_id, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, @@ -1339,7 +1339,7 @@ fn parse_event( sender: ChannelUser { platform_id: chat_id, display_name: sender_id, - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, diff --git a/crates/openfang-channels/src/flock.rs b/crates/openfang-channels/src/flock.rs index d481575e2e..c1ffe2bb2c 100644 --- a/crates/openfang-channels/src/flock.rs +++ b/crates/openfang-channels/src/flock.rs @@ -219,7 +219,7 @@ fn parse_flock_event(event: &serde_json::Value, own_user_id: &str) -> Option` //! - Plain text: strips all formatting -use openfang_types::config::OutputFormat; +use omtae_types::config::OutputFormat; /// Format a message for a specific channel output format. pub fn format_for_channel(text: &str, format: OutputFormat) -> String { diff --git a/crates/openfang-channels/src/gitter.rs b/crates/openfang-channels/src/gitter.rs index 4d3a5a4ed6..53dafdcf3b 100644 --- a/crates/openfang-channels/src/gitter.rs +++ b/crates/openfang-channels/src/gitter.rs @@ -276,7 +276,7 @@ impl ChannelAdapter for GitterAdapter { sender: ChannelUser { platform_id: username.clone(), display_name, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/google_chat.rs b/crates/openfang-channels/src/google_chat.rs index b199645cc7..0ff11f3824 100644 --- a/crates/openfang-channels/src/google_chat.rs +++ b/crates/openfang-channels/src/google_chat.rs @@ -306,7 +306,7 @@ impl ChannelAdapter for GoogleChatAdapter { sender: ChannelUser { platform_id: space_name.to_string(), display_name: sender_name.to_string(), - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, diff --git a/crates/openfang-channels/src/gotify.rs b/crates/openfang-channels/src/gotify.rs index c0d93b333f..d3b4701041 100644 --- a/crates/openfang-channels/src/gotify.rs +++ b/crates/openfang-channels/src/gotify.rs @@ -248,7 +248,7 @@ impl ChannelAdapter for GotifyAdapter { } else { title.clone() }, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, @@ -314,7 +314,7 @@ impl ChannelAdapter for GotifyAdapter { ChannelContent::Text(t) => t, _ => "(Unsupported content type)".to_string(), }; - self.api_send_message("OpenFang", &text, 5).await + self.api_send_message("OMTAE", &text, 5).await } async fn send_typing(&self, _user: &ChannelUser) -> Result<(), Box> { diff --git a/crates/openfang-channels/src/guilded.rs b/crates/openfang-channels/src/guilded.rs index f18aacf106..dba81bfb73 100644 --- a/crates/openfang-channels/src/guilded.rs +++ b/crates/openfang-channels/src/guilded.rs @@ -278,7 +278,7 @@ impl ChannelAdapter for GuildedAdapter { sender: ChannelUser { platform_id: channel_id, display_name: created_by.to_string(), - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, diff --git a/crates/openfang-channels/src/irc.rs b/crates/openfang-channels/src/irc.rs index a9b87e46b3..b610d86711 100644 --- a/crates/openfang-channels/src/irc.rs +++ b/crates/openfang-channels/src/irc.rs @@ -1,4 +1,4 @@ -//! IRC channel adapter for the OpenFang channel bridge. +//! IRC channel adapter for the OMTAE channel bridge. //! //! Uses raw TCP via `tokio::net::TcpStream` with `tokio::io` buffered I/O for //! plaintext IRC connections. Implements the core IRC protocol: NICK, USER, JOIN, @@ -45,7 +45,7 @@ pub struct IrcAdapter { nick: String, /// SECURITY: Optional server password, zeroized on drop. password: Option>, - /// IRC channels to join (e.g., ["#openfang", "#bots"]). + /// IRC channels to join (e.g., ["#omtae", "#bots"]). channels: Vec, /// Reserved for future TLS support. Currently only plaintext is implemented. #[allow(dead_code)] @@ -206,7 +206,7 @@ fn parse_privmsg(line: &IrcLine, bot_nick: &str) -> Option { sender: ChannelUser { platform_id, display_name: sender_nick.to_string(), - openfang_user: None, + omtae_user: None, }, content, target_agent: None, @@ -275,7 +275,7 @@ impl ChannelAdapter for IrcAdapter { registration.push_str(&format!("PASS {}\r\n", pass.as_str())); } registration.push_str(&format!("NICK {nick}\r\n")); - registration.push_str(&format!("USER {nick} 0 * :OpenFang Bot\r\n")); + registration.push_str(&format!("USER {nick} 0 * :OMTAE Bot\r\n")); if let Err(e) = writer.write_all(registration.as_bytes()).await { warn!("IRC registration send failed: {e}"); @@ -391,7 +391,7 @@ impl ChannelAdapter for IrcAdapter { _ = shutdown_rx.changed() => { if *shutdown_rx.borrow() { info!("IRC adapter shutting down"); - let _ = writer.write_all(b"QUIT :OpenFang shutting down\r\n").await; + let _ = writer.write_all(b"QUIT :OMTAE shutting down\r\n").await; return; } } @@ -459,9 +459,9 @@ mod tests { let adapter = IrcAdapter::new( "irc.libera.chat".to_string(), 6667, - "openfang".to_string(), + "omtae".to_string(), None, - vec!["#openfang".to_string()], + vec!["#omtae".to_string()], false, ); assert_eq!(adapter.name(), "irc"); @@ -507,10 +507,10 @@ mod tests { #[test] fn test_parse_irc_line_privmsg() { - let line = parse_irc_line(":alice!alice@host PRIVMSG #openfang :Hello everyone!").unwrap(); + let line = parse_irc_line(":alice!alice@host PRIVMSG #omtae :Hello everyone!").unwrap(); assert_eq!(line.prefix.as_deref(), Some("alice!alice@host")); assert_eq!(line.command, "PRIVMSG"); - assert_eq!(line.params, vec!["#openfang"]); + assert_eq!(line.params, vec!["#omtae"]); assert_eq!(line.trailing.as_deref(), Some("Hello everyone!")); } @@ -525,9 +525,9 @@ mod tests { #[test] fn test_parse_irc_line_no_trailing() { - let line = parse_irc_line(":alice!alice@host JOIN #openfang").unwrap(); + let line = parse_irc_line(":alice!alice@host JOIN #omtae").unwrap(); assert_eq!(line.command, "JOIN"); - assert_eq!(line.params, vec!["#openfang"]); + assert_eq!(line.params, vec!["#omtae"]); assert!(line.trailing.is_none()); } @@ -552,14 +552,14 @@ mod tests { let line = IrcLine { prefix: Some("alice!alice@host".to_string()), command: "PRIVMSG".to_string(), - params: vec!["#openfang".to_string()], + params: vec!["#omtae".to_string()], trailing: Some("Hello from IRC!".to_string()), }; - let msg = parse_privmsg(&line, "openfang-bot").unwrap(); + let msg = parse_privmsg(&line, "omtae-bot").unwrap(); assert_eq!(msg.channel, ChannelType::Custom("irc".to_string())); assert_eq!(msg.sender.display_name, "alice"); - assert_eq!(msg.sender.platform_id, "#openfang"); + assert_eq!(msg.sender.platform_id, "#omtae"); assert!(msg.is_group); assert!(matches!(msg.content, ChannelContent::Text(ref t) if t == "Hello from IRC!")); } @@ -569,11 +569,11 @@ mod tests { let line = IrcLine { prefix: Some("bob!bob@host".to_string()), command: "PRIVMSG".to_string(), - params: vec!["openfang-bot".to_string()], + params: vec!["omtae-bot".to_string()], trailing: Some("Private message".to_string()), }; - let msg = parse_privmsg(&line, "openfang-bot").unwrap(); + let msg = parse_privmsg(&line, "omtae-bot").unwrap(); assert!(!msg.is_group); assert_eq!(msg.sender.platform_id, "bob"); // DM replies go to sender } @@ -581,13 +581,13 @@ mod tests { #[test] fn test_parse_privmsg_skips_self() { let line = IrcLine { - prefix: Some("openfang-bot!bot@host".to_string()), + prefix: Some("omtae-bot!bot@host".to_string()), command: "PRIVMSG".to_string(), - params: vec!["#openfang".to_string()], + params: vec!["#omtae".to_string()], trailing: Some("My own message".to_string()), }; - let msg = parse_privmsg(&line, "openfang-bot"); + let msg = parse_privmsg(&line, "omtae-bot"); assert!(msg.is_none()); } @@ -596,11 +596,11 @@ mod tests { let line = IrcLine { prefix: Some("alice!alice@host".to_string()), command: "PRIVMSG".to_string(), - params: vec!["#openfang".to_string()], + params: vec!["#omtae".to_string()], trailing: Some("/agent hello-world".to_string()), }; - let msg = parse_privmsg(&line, "openfang-bot").unwrap(); + let msg = parse_privmsg(&line, "omtae-bot").unwrap(); match &msg.content { ChannelContent::Command { name, args } => { assert_eq!(name, "agent"); @@ -615,11 +615,11 @@ mod tests { let line = IrcLine { prefix: Some("alice!alice@host".to_string()), command: "PRIVMSG".to_string(), - params: vec!["#openfang".to_string()], + params: vec!["#omtae".to_string()], trailing: Some("".to_string()), }; - let msg = parse_privmsg(&line, "openfang-bot"); + let msg = parse_privmsg(&line, "omtae-bot"); assert!(msg.is_none()); } @@ -628,11 +628,11 @@ mod tests { let line = IrcLine { prefix: Some("alice!alice@host".to_string()), command: "PRIVMSG".to_string(), - params: vec!["#openfang".to_string()], + params: vec!["#omtae".to_string()], trailing: None, }; - let msg = parse_privmsg(&line, "openfang-bot"); + let msg = parse_privmsg(&line, "omtae-bot"); assert!(msg.is_none()); } @@ -641,11 +641,11 @@ mod tests { let line = IrcLine { prefix: Some("alice!alice@host".to_string()), command: "NOTICE".to_string(), - params: vec!["#openfang".to_string()], + params: vec!["#omtae".to_string()], trailing: Some("Notice text".to_string()), }; - let msg = parse_privmsg(&line, "openfang-bot"); + let msg = parse_privmsg(&line, "omtae-bot"); assert!(msg.is_none()); } } diff --git a/crates/openfang-channels/src/keybase.rs b/crates/openfang-channels/src/keybase.rs index f619368719..520693fbdb 100644 --- a/crates/openfang-channels/src/keybase.rs +++ b/crates/openfang-channels/src/keybase.rs @@ -370,7 +370,7 @@ impl ChannelAdapter for KeybaseAdapter { sender: ChannelUser { platform_id: conv_key.clone(), display_name: sender_username.to_string(), - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, diff --git a/crates/openfang-channels/src/lib.rs b/crates/openfang-channels/src/lib.rs index 7b122d2a24..c59f741146 100644 --- a/crates/openfang-channels/src/lib.rs +++ b/crates/openfang-channels/src/lib.rs @@ -1,4 +1,4 @@ -//! Channel Bridge Layer for the OpenFang Agent OS. +//! Channel Bridge Layer for the OMTAE Agent OS. //! //! Provides 40 pluggable messaging integrations that convert platform messages //! into unified `ChannelMessage` events for the kernel. diff --git a/crates/openfang-channels/src/line.rs b/crates/openfang-channels/src/line.rs index fa3ecd80ce..5938952be3 100644 --- a/crates/openfang-channels/src/line.rs +++ b/crates/openfang-channels/src/line.rs @@ -338,7 +338,7 @@ fn parse_line_event(event: &serde_json::Value) -> Option { sender: ChannelUser { platform_id: reply_to, display_name: user_id, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/linkedin.rs b/crates/openfang-channels/src/linkedin.rs index 8435b5b0d8..30ae9077e7 100644 --- a/crates/openfang-channels/src/linkedin.rs +++ b/crates/openfang-channels/src/linkedin.rs @@ -325,7 +325,7 @@ impl ChannelAdapter for LinkedInAdapter { sender: ChannelUser { platform_id: sender_urn.clone(), display_name: sender_name, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/mastodon.rs b/crates/openfang-channels/src/mastodon.rs index 9a704bfe63..f5d8f78e70 100644 --- a/crates/openfang-channels/src/mastodon.rs +++ b/crates/openfang-channels/src/mastodon.rs @@ -250,7 +250,7 @@ fn parse_mastodon_notification( sender: ChannelUser { platform_id: account_id.to_string(), display_name, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/matrix.rs b/crates/openfang-channels/src/matrix.rs index 316b76fd16..a9e19f40d1 100644 --- a/crates/openfang-channels/src/matrix.rs +++ b/crates/openfang-channels/src/matrix.rs @@ -26,7 +26,7 @@ type TokenPair = Arc, Option>)>>; pub struct MatrixAdapter { /// Matrix homeserver URL (e.g., `"https://matrix.org"`). homeserver_url: String, - /// Bot's user ID (e.g., "@openfang:matrix.org"). + /// Bot's user ID (e.g., "@omtae:matrix.org"). user_id: String, /// SECURITY: Access + refresh tokens are zeroized on drop. Stored behind /// an RwLock so the sync loop and send paths see rotated tokens after a @@ -628,7 +628,7 @@ impl ChannelAdapter for MatrixAdapter { sender: ChannelUser { platform_id: room_id.clone(), display_name: sender.to_string(), - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, diff --git a/crates/openfang-channels/src/mattermost.rs b/crates/openfang-channels/src/mattermost.rs index 02bd5ddae6..2dbf111324 100644 --- a/crates/openfang-channels/src/mattermost.rs +++ b/crates/openfang-channels/src/mattermost.rs @@ -1,4 +1,4 @@ -//! Mattermost channel adapter for the OpenFang channel bridge. +//! Mattermost channel adapter for the OMTAE channel bridge. //! //! Uses the Mattermost WebSocket API v4 for real-time message reception and the //! REST API v4 for sending messages. No external Mattermost crate — just @@ -215,7 +215,7 @@ fn parse_mattermost_event( sender: ChannelUser { platform_id: channel_id.to_string(), display_name: sender_name.to_string(), - openfang_user: None, + omtae_user: None, }, content, target_agent: None, @@ -626,7 +626,7 @@ mod tests { "data": { "post": serde_json::to_string(&post).unwrap(), "channel_type": "O", - "sender_name": "openfang-bot" + "sender_name": "omtae-bot" } }); diff --git a/crates/openfang-channels/src/messenger.rs b/crates/openfang-channels/src/messenger.rs index 9c04a171d2..b8ec5aa801 100644 --- a/crates/openfang-channels/src/messenger.rs +++ b/crates/openfang-channels/src/messenger.rs @@ -240,7 +240,7 @@ fn parse_messenger_entry(entry: &serde_json::Value) -> Vec { sender: ChannelUser { platform_id: sender_id, display_name: String::new(), // Messenger doesn't include name in webhook - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/mqtt.rs b/crates/openfang-channels/src/mqtt.rs index a3e5b1549a..c154472709 100644 --- a/crates/openfang-channels/src/mqtt.rs +++ b/crates/openfang-channels/src/mqtt.rs @@ -8,8 +8,8 @@ //! ```toml //! [channels.mqtt] //! broker_url = "tcp://broker.hivemq.com:1883" -//! subscribe_topic = "openfang/inbox" -//! publish_topic = "openfang/outbox" +//! subscribe_topic = "omtae/inbox" +//! publish_topic = "omtae/outbox" //! username_env = "MQTT_USERNAME" //! password_env = "MQTT_PASSWORD" //! use_tls = false @@ -170,7 +170,7 @@ impl MqttAdapter { fn build_mqtt_options(&self) -> Result> { let (host, port) = self.parse_broker_url()?; let client_id = if self.client_id.is_empty() { - format!("openfang-{}", uuid::Uuid::new_v4()) + format!("omtae-{}", uuid::Uuid::new_v4()) } else { self.client_id.clone() }; @@ -336,7 +336,7 @@ impl ChannelAdapter for MqttAdapter { sender: ChannelUser { platform_id: "mqtt-user".to_string(), display_name: "MQTT User".to_string(), - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/mumble.rs b/crates/openfang-channels/src/mumble.rs index e8418db12d..43de219f93 100644 --- a/crates/openfang-channels/src/mumble.rs +++ b/crates/openfang-channels/src/mumble.rs @@ -95,14 +95,14 @@ impl MumbleAdapter { /// /// Simplified encoding: version fields as varint-like protobuf. /// Field 1 (version): 0x00010500 (1.5.0) - /// Field 2 (release): "OpenFang" + /// Field 2 (release): "OMTAE" fn build_version_packet() -> Vec { let mut payload = Vec::new(); // Field 1: fixed32 version = 0x00010500 (tag = 0x0D for wire type 5) payload.push(0x0D); payload.extend_from_slice(&0x0001_0500u32.to_le_bytes()); // Field 2: string release (tag = 0x12) - let release = b"OpenFang"; + let release = b"OMTAE"; payload.push(0x12); payload.push(release.len() as u8); payload.extend_from_slice(release); @@ -421,7 +421,7 @@ impl ChannelAdapter for MumbleAdapter { sender: ChannelUser { platform_id: format!("session-{actor}"), display_name: format!("user-{actor}"), - openfang_user: None, + omtae_user: None, }, content, target_agent: None, @@ -522,7 +522,7 @@ mod tests { "mumble.example.com".to_string(), 0, "secret".to_string(), - "OpenFangBot".to_string(), + "OMTAEBot".to_string(), "General".to_string(), ); assert_eq!(adapter.name(), "mumble"); diff --git a/crates/openfang-channels/src/nextcloud.rs b/crates/openfang-channels/src/nextcloud.rs index b4856abc22..375ed633f9 100644 --- a/crates/openfang-channels/src/nextcloud.rs +++ b/crates/openfang-channels/src/nextcloud.rs @@ -363,7 +363,7 @@ impl ChannelAdapter for NextcloudAdapter { sender: ChannelUser { platform_id: room_token.clone(), display_name: actor_display.to_string(), - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, diff --git a/crates/openfang-channels/src/nostr.rs b/crates/openfang-channels/src/nostr.rs index ec54ade461..963dfa90a8 100644 --- a/crates/openfang-channels/src/nostr.rs +++ b/crates/openfang-channels/src/nostr.rs @@ -73,7 +73,7 @@ impl NostrAdapter { fn build_subscription(&self, pubkey: &str) -> String { let filter = serde_json::json!([ "REQ", - "openfang-sub", + "omtae-sub", { "kinds": [4], "#p": [pubkey], @@ -167,7 +167,7 @@ impl ChannelAdapter for NostrAdapter { let pubkey = self.derive_pubkey(); info!( "Nostr adapter starting (pubkey: {}...)", - openfang_types::truncate_str(&pubkey, 16) + omtae_types::truncate_str(&pubkey, 16) ); if self.relays.is_empty() { @@ -219,7 +219,7 @@ impl ChannelAdapter for NostrAdapter { let sub_msg = { let filter = serde_json::json!([ "REQ", - "openfang-sub", + "omtae-sub", { "kinds": [4], "#p": [&own_pubkey], @@ -246,7 +246,7 @@ impl ChannelAdapter for NostrAdapter { _ = relay_shutdown_rx.changed() => { info!("Nostr: relay {relay_url} shutting down"); // Send CLOSE - let close_msg = serde_json::json!(["CLOSE", "openfang-sub"]); + let close_msg = serde_json::json!(["CLOSE", "omtae-sub"]); let _ = write.send( tokio_tungstenite::tungstenite::Message::Text( serde_json::to_string(&close_msg).unwrap_or_default() @@ -342,9 +342,9 @@ impl ChannelAdapter for NostrAdapter { platform_id: sender_pubkey.clone(), display_name: format!( "{}...", - openfang_types::truncate_str(&sender_pubkey, 8) + omtae_types::truncate_str(&sender_pubkey, 8) ), - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, @@ -460,7 +460,7 @@ mod tests { let pubkey = adapter.derive_pubkey(); let sub = adapter.build_subscription(&pubkey); assert!(sub.contains("REQ")); - assert!(sub.contains("openfang-sub")); + assert!(sub.contains("omtae-sub")); assert!(sub.contains(&pubkey)); } diff --git a/crates/openfang-channels/src/ntfy.rs b/crates/openfang-channels/src/ntfy.rs index 508d2aad31..ad2d0a0c7f 100644 --- a/crates/openfang-channels/src/ntfy.rs +++ b/crates/openfang-channels/src/ntfy.rs @@ -267,7 +267,7 @@ impl ChannelAdapter for NtfyAdapter { sender: ChannelUser { platform_id: sender_name.to_string(), display_name: sender_name.to_string(), - openfang_user: None, + omtae_user: None, }, content, target_agent: None, @@ -329,7 +329,7 @@ impl ChannelAdapter for NtfyAdapter { ChannelContent::Text(t) => t, _ => "(Unsupported content type)".to_string(), }; - self.publish(&text, Some("OpenFang")).await + self.publish(&text, Some("OMTAE")).await } async fn send_typing(&self, _user: &ChannelUser) -> Result<(), Box> { diff --git a/crates/openfang-channels/src/pumble.rs b/crates/openfang-channels/src/pumble.rs index 0aa97e8518..8ed4518a55 100644 --- a/crates/openfang-channels/src/pumble.rs +++ b/crates/openfang-channels/src/pumble.rs @@ -195,7 +195,7 @@ fn parse_pumble_event(event: &serde_json::Value, own_bot_id: &str) -> Option Opti sender: ChannelUser { platform_id: author.to_string(), display_name: author.to_string(), - openfang_user: None, + omtae_user: None, }, content, target_agent: None, @@ -554,13 +554,13 @@ mod tests { vec![ "rust".to_string(), "programming".to_string(), - "r/openfang".to_string(), + "r/omtae".to_string(), ], ); assert_eq!(adapter.subreddits.len(), 3); assert!(adapter.is_monitored_subreddit("rust")); assert!(adapter.is_monitored_subreddit("programming")); - assert!(adapter.is_monitored_subreddit("openfang")); + assert!(adapter.is_monitored_subreddit("omtae")); assert!(!adapter.is_monitored_subreddit("news")); } diff --git a/crates/openfang-channels/src/revolt.rs b/crates/openfang-channels/src/revolt.rs index d2b7202f4f..66419809f1 100644 --- a/crates/openfang-channels/src/revolt.rs +++ b/crates/openfang-channels/src/revolt.rs @@ -285,7 +285,7 @@ fn parse_revolt_message( sender: ChannelUser { platform_id: channel_id, display_name: author.to_string(), - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, diff --git a/crates/openfang-channels/src/rocketchat.rs b/crates/openfang-channels/src/rocketchat.rs index 1102450275..71a021bb7c 100644 --- a/crates/openfang-channels/src/rocketchat.rs +++ b/crates/openfang-channels/src/rocketchat.rs @@ -311,7 +311,7 @@ impl ChannelAdapter for RocketChatAdapter { sender: ChannelUser { platform_id: channel_id.clone(), display_name: sender_username.to_string(), - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, diff --git a/crates/openfang-channels/src/router.rs b/crates/openfang-channels/src/router.rs index 8bc141dcf9..c5fb21462c 100644 --- a/crates/openfang-channels/src/router.rs +++ b/crates/openfang-channels/src/router.rs @@ -2,8 +2,8 @@ use crate::types::ChannelType; use dashmap::DashMap; -use openfang_types::agent::AgentId; -use openfang_types::config::{AgentBinding, BroadcastConfig, BroadcastStrategy}; +use omtae_types::agent::AgentId; +use omtae_types::config::{AgentBinding, BroadcastConfig, BroadcastStrategy}; use std::sync::Mutex; use tracing::warn; @@ -30,7 +30,7 @@ pub struct BindingContext { /// /// Routing priority: bindings (most specific first) > direct routes > user defaults > system default. pub struct AgentRouter { - /// Default agent per user (keyed by openfang_user or platform_id). + /// Default agent per user (keyed by omtae_user or platform_id). user_defaults: DashMap, /// Direct routes: (channel_type_key, platform_user_id) -> AgentId. direct_routes: DashMap<(String, String), AgentId>, @@ -430,7 +430,7 @@ mod tests { router.register_agent("coder".to_string(), agent_id); router.load_bindings(&[AgentBinding { agent: "coder".to_string(), - match_rule: openfang_types::config::BindingMatchRule { + match_rule: omtae_types::config::BindingMatchRule { channel: Some("telegram".to_string()), ..Default::default() }, @@ -452,7 +452,7 @@ mod tests { router.register_agent("support".to_string(), agent_id); router.load_bindings(&[AgentBinding { agent: "support".to_string(), - match_rule: openfang_types::config::BindingMatchRule { + match_rule: omtae_types::config::BindingMatchRule { peer_id: Some("vip_user".to_string()), ..Default::default() }, @@ -472,7 +472,7 @@ mod tests { router.register_agent("admin-bot".to_string(), agent_id); router.load_bindings(&[AgentBinding { agent: "admin-bot".to_string(), - match_rule: openfang_types::config::BindingMatchRule { + match_rule: omtae_types::config::BindingMatchRule { guild_id: Some("guild_123".to_string()), roles: vec!["admin".to_string()], ..Default::default() @@ -513,14 +513,14 @@ mod tests { router.load_bindings(&[ AgentBinding { agent: "general".to_string(), - match_rule: openfang_types::config::BindingMatchRule { + match_rule: omtae_types::config::BindingMatchRule { channel: Some("discord".to_string()), ..Default::default() }, }, AgentBinding { agent: "specific".to_string(), - match_rule: openfang_types::config::BindingMatchRule { + match_rule: omtae_types::config::BindingMatchRule { channel: Some("discord".to_string()), peer_id: Some("user1".to_string()), guild_id: Some("guild_1".to_string()), @@ -611,7 +611,7 @@ mod tests { // Don't register the agent — binding should match but resolve_binding returns None router.load_bindings(&[AgentBinding { agent: "ghost-agent".to_string(), - match_rule: openfang_types::config::BindingMatchRule { + match_rule: omtae_types::config::BindingMatchRule { channel: Some("telegram".to_string()), ..Default::default() }, @@ -631,7 +631,7 @@ mod tests { router.add_binding(AgentBinding { agent: "test".to_string(), - match_rule: openfang_types::config::BindingMatchRule { + match_rule: omtae_types::config::BindingMatchRule { channel: Some("slack".to_string()), ..Default::default() }, @@ -645,7 +645,7 @@ mod tests { #[test] fn test_binding_specificity_scores() { - use openfang_types::config::BindingMatchRule; + use omtae_types::config::BindingMatchRule; let empty = BindingMatchRule::default(); assert_eq!(empty.specificity(), 0); @@ -696,7 +696,7 @@ mod tests { router.register_agent("ops-bot".to_string(), agent_id); router.load_bindings(&[AgentBinding { agent: "ops-bot".to_string(), - match_rule: openfang_types::config::BindingMatchRule { + match_rule: omtae_types::config::BindingMatchRule { channel: Some("discord".to_string()), channel_id: Some("1477803840265781391".to_string()), ..Default::default() @@ -738,14 +738,14 @@ mod tests { router.load_bindings(&[ AgentBinding { agent: "general".to_string(), - match_rule: openfang_types::config::BindingMatchRule { + match_rule: omtae_types::config::BindingMatchRule { channel_id: Some("ch-medical".to_string()), ..Default::default() }, }, AgentBinding { agent: "researcher".to_string(), - match_rule: openfang_types::config::BindingMatchRule { + match_rule: omtae_types::config::BindingMatchRule { channel_id: Some("ch-medical".to_string()), peer_id: Some("user-a".to_string()), ..Default::default() @@ -778,12 +778,12 @@ mod tests { // than silently producing a wide-open binding. This is the highest- // leverage line in the patch from issue #1127. let bad = r#"{ "channnel_id": "ch-1" }"#; - let r: Result = serde_json::from_str(bad); + let r: Result = serde_json::from_str(bad); assert!(r.is_err(), "unknown field must be rejected by serde"); // Sanity: known fields still parse. let good = r#"{ "channel_id": "ch-1", "channel": "discord" }"#; - let r: openfang_types::config::BindingMatchRule = serde_json::from_str(good).unwrap(); + let r: omtae_types::config::BindingMatchRule = serde_json::from_str(good).unwrap(); assert_eq!(r.channel_id.as_deref(), Some("ch-1")); assert_eq!(r.channel.as_deref(), Some("discord")); } diff --git a/crates/openfang-channels/src/signal.rs b/crates/openfang-channels/src/signal.rs index 8f6ce3fc51..50dbbf4a98 100644 --- a/crates/openfang-channels/src/signal.rs +++ b/crates/openfang-channels/src/signal.rs @@ -195,7 +195,7 @@ impl ChannelAdapter for SignalAdapter { sender: ChannelUser { platform_id: source.clone(), display_name: source_name, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/slack.rs b/crates/openfang-channels/src/slack.rs index 3229d9df0e..407bf294b2 100644 --- a/crates/openfang-channels/src/slack.rs +++ b/crates/openfang-channels/src/slack.rs @@ -1,4 +1,4 @@ -//! Slack Socket Mode adapter for the OpenFang channel bridge. +//! Slack Socket Mode adapter for the OMTAE channel bridge. //! //! Uses Slack Socket Mode WebSocket (app token) for receiving events and the //! Web API (bot token) for sending responses. No external Slack crate. @@ -589,7 +589,7 @@ async fn parse_slack_event( sender: ChannelUser { platform_id: channel.to_string(), display_name: user_id.to_string(), // Slack user IDs as display name - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/teams.rs b/crates/openfang-channels/src/teams.rs index e6a9e93b11..f4a293bf2d 100644 --- a/crates/openfang-channels/src/teams.rs +++ b/crates/openfang-channels/src/teams.rs @@ -1,4 +1,4 @@ -//! Microsoft Teams channel adapter for the OpenFang channel bridge. +//! Microsoft Teams channel adapter for the OMTAE channel bridge. //! //! Uses Bot Framework v3 REST API for sending messages and a lightweight axum //! HTTP webhook server for receiving inbound activities. OAuth2 client credentials @@ -258,7 +258,7 @@ fn parse_teams_activity( sender: ChannelUser { platform_id: conversation_id, display_name: from_name.to_string(), - openfang_user: None, + omtae_user: None, }, content, target_agent: None, @@ -473,7 +473,7 @@ mod tests { "text": "Bot reply", "from": { "id": "app-id-123", - "name": "OpenFang Bot" + "name": "OMTAE Bot" }, "conversation": { "id": "conv-789" diff --git a/crates/openfang-channels/src/telegram.rs b/crates/openfang-channels/src/telegram.rs index 5341ed5a2e..3aa0e041a4 100644 --- a/crates/openfang-channels/src/telegram.rs +++ b/crates/openfang-channels/src/telegram.rs @@ -1,4 +1,4 @@ -//! Telegram Bot API adapter for the OpenFang channel bridge. +//! Telegram Bot API adapter for the OMTAE channel bridge. //! //! Uses long-polling via `getUpdates` with exponential backoff on failures. //! No external Telegram crate — just `reqwest` for full control over error handling. @@ -1087,7 +1087,7 @@ async fn parse_telegram_update( sender: ChannelUser { platform_id: chat_id.to_string(), display_name, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, @@ -1514,7 +1514,7 @@ mod tests { "from": { "id": 123, "first_name": "X" }, "chat": { "id": 123, "type": "private" }, "date": 1700000000, - "text": "/agents@myopenfangbot", + "text": "/agents@myomtaebot", "entities": [{ "type": "bot_command", "offset": 0, "length": 17 }] } }); diff --git a/crates/openfang-channels/src/threema.rs b/crates/openfang-channels/src/threema.rs index 74244c7dfd..5dcb81cffe 100644 --- a/crates/openfang-channels/src/threema.rs +++ b/crates/openfang-channels/src/threema.rs @@ -163,7 +163,7 @@ fn parse_threema_webhook( sender: ChannelUser { platform_id: from.clone(), display_name: from.clone(), - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/twist.rs b/crates/openfang-channels/src/twist.rs index d935475ecc..295432c5e8 100644 --- a/crates/openfang-channels/src/twist.rs +++ b/crates/openfang-channels/src/twist.rs @@ -451,7 +451,7 @@ impl ChannelAdapter for TwistAdapter { sender: ChannelUser { platform_id: thread_id.clone(), display_name: creator_name.to_string(), - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, diff --git a/crates/openfang-channels/src/twitch.rs b/crates/openfang-channels/src/twitch.rs index 6279cff62c..391f233431 100644 --- a/crates/openfang-channels/src/twitch.rs +++ b/crates/openfang-channels/src/twitch.rs @@ -27,7 +27,7 @@ const MAX_MESSAGE_LEN: usize = 500; /// Twitch IRC channel adapter. /// /// Connects to Twitch chat via the IRC protocol and bridges messages to the -/// OpenFang channel system. Supports multiple channels simultaneously. +/// OMTAE channel system. Supports multiple channels simultaneously. pub struct TwitchAdapter { /// SECURITY: OAuth token is zeroized on drop. oauth_token: Zeroizing, @@ -245,7 +245,7 @@ impl ChannelAdapter for TwitchAdapter { sender: ChannelUser { platform_id: channel.clone(), display_name: sender_nick, - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, @@ -325,7 +325,7 @@ mod tests { let adapter = TwitchAdapter::new( "test-oauth-token".to_string(), vec!["testchannel".to_string()], - "openfang_bot".to_string(), + "omtae_bot".to_string(), ); assert_eq!(adapter.name(), "twitch"); assert_eq!( diff --git a/crates/openfang-channels/src/types.rs b/crates/openfang-channels/src/types.rs index 035735134b..7a9f93bd96 100644 --- a/crates/openfang-channels/src/types.rs +++ b/crates/openfang-channels/src/types.rs @@ -1,7 +1,7 @@ //! Core channel bridge types. use chrono::{DateTime, Utc}; -use openfang_types::agent::AgentId; +use omtae_types::agent::AgentId; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::pin::Pin; @@ -35,8 +35,8 @@ pub struct ChannelUser { pub platform_id: String, /// Human-readable display name. pub display_name: String, - /// Optional mapping to an OpenFang user identity. - pub openfang_user: Option, + /// Optional mapping to an OMTAE user identity. + pub omtae_user: Option, } /// Content types that can be received from a channel. @@ -113,9 +113,9 @@ pub struct ChannelMessage { pub metadata: HashMap, } -// Re-export the adapter allowlist from openfang-types so config validation +// Re-export the adapter allowlist from omtae-types so config validation // and routing share a single source of truth (no drift between the two). -pub use openfang_types::config::CHANNELS_WITH_PLATFORM_ID_AS_CHANNEL; +pub use omtae_types::config::CHANNELS_WITH_PLATFORM_ID_AS_CHANNEL; impl ChannelMessage { /// Return the platform-native channel/conversation ID for this message, @@ -281,12 +281,12 @@ pub struct ChannelStatus { pub last_error: Option, } -// Re-export policy/format types from openfang-types for convenience. -pub use openfang_types::config::{DmPolicy, GroupPolicy, OutputFormat}; +// Re-export policy/format types from omtae-types for convenience. +pub use omtae_types::config::{DmPolicy, GroupPolicy, OutputFormat}; /// Trait that every channel adapter must implement. /// -/// A channel adapter bridges a messaging platform to the OpenFang kernel by converting +/// A channel adapter bridges a messaging platform to the OMTAE kernel by converting /// platform-specific messages into `ChannelMessage` events and sending responses back. #[async_trait] pub trait ChannelAdapter: Send + Sync { @@ -385,7 +385,7 @@ pub fn split_message(text: &str, max_len: usize) -> Vec<&str> { break; } // Try to split at a newline near the boundary (UTF-8 safe) - let safe_end = openfang_types::truncate_str(remaining, max_len).len(); + let safe_end = omtae_types::truncate_str(remaining, max_len).len(); let split_at = remaining[..safe_end].rfind('\n').unwrap_or(safe_end); let (chunk, rest) = remaining.split_at(split_at); chunks.push(chunk); @@ -410,7 +410,7 @@ mod tests { sender: ChannelUser { platform_id: "user1".to_string(), display_name: "Alice".to_string(), - openfang_user: None, + omtae_user: None, }, content: ChannelContent::Text("Hello!".to_string()), target_agent: None, @@ -464,7 +464,7 @@ mod tests { sender: ChannelUser { platform_id: "C123".to_string(), display_name: "x".to_string(), - openfang_user: None, + omtae_user: None, }, content: ChannelContent::Text("hi".to_string()), target_agent: None, diff --git a/crates/openfang-channels/src/viber.rs b/crates/openfang-channels/src/viber.rs index b303b8be3c..c75657a377 100644 --- a/crates/openfang-channels/src/viber.rs +++ b/crates/openfang-channels/src/viber.rs @@ -31,7 +31,7 @@ const VIBER_ACCOUNT_INFO_URL: &str = "https://chatapi.viber.com/pa/get_account_i const MAX_MESSAGE_LEN: usize = 7000; /// Sender name shown in Viber messages from the bot. -const DEFAULT_SENDER_NAME: &str = "OpenFang"; +const DEFAULT_SENDER_NAME: &str = "OMTAE"; /// Viber Bot API adapter. /// @@ -183,7 +183,7 @@ impl ViberAdapter { "receiver": receiver, "min_api_version": 1, "sender": sender, - "tracking_data": "openfang", + "tracking_data": "omtae", "type": "text", "text": chunk, }); @@ -286,7 +286,7 @@ fn parse_viber_event(event: &serde_json::Value) -> Option { sender: ChannelUser { platform_id: sender_id, display_name: sender_name, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, diff --git a/crates/openfang-channels/src/webex.rs b/crates/openfang-channels/src/webex.rs index 36e260d9ac..6e493a8202 100644 --- a/crates/openfang-channels/src/webex.rs +++ b/crates/openfang-channels/src/webex.rs @@ -83,7 +83,7 @@ impl WebexAdapter { let bot_id = body["id"].as_str().unwrap_or("unknown").to_string(); let display_name = body["displayName"] .as_str() - .unwrap_or("OpenFang Bot") + .unwrap_or("OMTAE Bot") .to_string(); *self.bot_info.write().await = Some((bot_id.clone(), display_name.clone())); @@ -122,7 +122,7 @@ impl WebexAdapter { ) -> Result> { let url = format!("{}/webhooks", WEBEX_API_BASE); let body = serde_json::json!({ - "name": "OpenFang Bot Webhook", + "name": "OMTAE Bot Webhook", "targetUrl": target_url, "resource": "messages", "event": "created", @@ -406,7 +406,7 @@ impl ChannelAdapter for WebexAdapter { sender: ChannelUser { platform_id: full_room_id, display_name: sender_email.to_string(), - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, diff --git a/crates/openfang-channels/src/webhook.rs b/crates/openfang-channels/src/webhook.rs index 9dc5e13a80..c9e142754c 100644 --- a/crates/openfang-channels/src/webhook.rs +++ b/crates/openfang-channels/src/webhook.rs @@ -23,7 +23,7 @@ const MAX_MESSAGE_LEN: usize = 65535; /// Generic HTTP webhook channel adapter. /// -/// The most flexible adapter in the OpenFang channel suite. Any system that +/// The most flexible adapter in the OMTAE channel suite. Any system that /// can send/receive HTTP requests with HMAC-SHA256 signatures can integrate /// through this adapter. /// @@ -252,7 +252,7 @@ impl ChannelAdapter for WebhookAdapter { sender: ChannelUser { platform_id: sender_id, display_name: sender_name, - openfang_user: None, + omtae_user: None, }, content, target_agent: None, @@ -321,8 +321,8 @@ impl ChannelAdapter for WebhookAdapter { for chunk in chunks { let body = serde_json::json!({ - "sender_id": "openfang", - "sender_name": "OpenFang", + "sender_id": "omtae", + "sender_name": "OMTAE", "recipient_id": user.platform_id, "recipient_name": user.display_name, "message": chunk, diff --git a/crates/openfang-channels/src/wecom.rs b/crates/openfang-channels/src/wecom.rs index 12afadb41e..26b095b960 100644 --- a/crates/openfang-channels/src/wecom.rs +++ b/crates/openfang-channels/src/wecom.rs @@ -485,7 +485,7 @@ impl ChannelAdapter for WeComAdapter { sender: ChannelUser { platform_id: user_id.clone(), display_name: user_id.clone(), - openfang_user: None, + omtae_user: None, }, content: ChannelContent::Text(String::new()), target_agent: None, @@ -511,7 +511,7 @@ impl ChannelAdapter for WeComAdapter { sender: ChannelUser { platform_id: user_id.clone(), display_name: user_id.clone(), - openfang_user: None, + omtae_user: None, }, content: ChannelContent::Text(content), target_agent: None, @@ -664,7 +664,7 @@ mod tests { ) .expect("echostr should decrypt"); - assert_eq!(plain, "openfang-wecom-check"); + assert_eq!(plain, "omtae-wecom-check"); } #[test] diff --git a/crates/openfang-channels/src/xmpp.rs b/crates/openfang-channels/src/xmpp.rs index a8d00f2904..35991ff831 100644 --- a/crates/openfang-channels/src/xmpp.rs +++ b/crates/openfang-channels/src/xmpp.rs @@ -230,7 +230,7 @@ mod tests { let user = ChannelUser { platform_id: "user@example.com".to_string(), display_name: "Test User".to_string(), - openfang_user: None, + omtae_user: None, }; let result = adapter .send(&user, ChannelContent::Text("hello".to_string())) diff --git a/crates/openfang-channels/src/zulip.rs b/crates/openfang-channels/src/zulip.rs index fbdcbd5f4d..c23cfd1f37 100644 --- a/crates/openfang-channels/src/zulip.rs +++ b/crates/openfang-channels/src/zulip.rs @@ -391,7 +391,7 @@ impl ChannelAdapter for ZulipAdapter { sender: ChannelUser { platform_id, display_name: sender_name.to_string(), - openfang_user: None, + omtae_user: None, }, content: msg_content, target_agent: None, @@ -441,7 +441,7 @@ impl ChannelAdapter for ZulipAdapter { .await?; } else { // Use the thread_id (topic) if available, otherwise default topic - let topic = "OpenFang"; + let topic = "OMTAE"; self.api_send_message("stream", &user.platform_id, topic, &text) .await?; } diff --git a/crates/openfang-channels/tests/bridge_integration_test.rs b/crates/openfang-channels/tests/bridge_integration_test.rs index 85ea20729a..32320ea817 100644 --- a/crates/openfang-channels/tests/bridge_integration_test.rs +++ b/crates/openfang-channels/tests/bridge_integration_test.rs @@ -9,12 +9,12 @@ use async_trait::async_trait; use futures::Stream; -use openfang_channels::bridge::{BridgeManager, ChannelBridgeHandle}; -use openfang_channels::router::AgentRouter; -use openfang_channels::types::{ +use omtae_channels::bridge::{BridgeManager, ChannelBridgeHandle}; +use omtae_channels::router::AgentRouter; +use omtae_channels::types::{ ChannelAdapter, ChannelContent, ChannelMessage, ChannelType, ChannelUser, }; -use openfang_types::agent::AgentId; +use omtae_types::agent::AgentId; use std::collections::HashMap; use std::pin::Pin; use std::sync::{Arc, Mutex}; @@ -155,7 +155,7 @@ fn make_text_msg(channel: ChannelType, user_id: &str, text: &str) -> ChannelMess sender: ChannelUser { platform_id: user_id.to_string(), display_name: "TestUser".to_string(), - openfang_user: None, + omtae_user: None, }, content: ChannelContent::Text(text.to_string()), target_agent: None, @@ -178,7 +178,7 @@ fn make_command_msg( sender: ChannelUser { platform_id: user_id.to_string(), display_name: "TestUser".to_string(), - openfang_user: None, + omtae_user: None, }, content: ChannelContent::Command { name: cmd.to_string(), diff --git a/crates/openfang-cli/Cargo.toml b/crates/openfang-cli/Cargo.toml index 073fea5bde..8303c476e0 100644 --- a/crates/openfang-cli/Cargo.toml +++ b/crates/openfang-cli/Cargo.toml @@ -1,21 +1,21 @@ [package] -name = "openfang-cli" +name = "omtae-cli" version.workspace = true edition.workspace = true license.workspace = true -description = "CLI tool for the OpenFang Agent OS" +description = "CLI tool for the OMTAE Agent OS" [[bin]] -name = "openfang" +name = "omtae" path = "src/main.rs" [dependencies] -openfang-types = { path = "../openfang-types" } -openfang-kernel = { path = "../openfang-kernel" } -openfang-api = { path = "../openfang-api" } -openfang-migrate = { path = "../openfang-migrate" } -openfang-skills = { path = "../openfang-skills" } -openfang-extensions = { path = "../openfang-extensions" } +omtae-types = { path = "../omtae-types" } +omtae-kernel = { path = "../omtae-kernel" } +omtae-api = { path = "../omtae-api" } +omtae-migrate = { path = "../omtae-migrate" } +omtae-skills = { path = "../omtae-skills" } +omtae-extensions = { path = "../omtae-extensions" } zeroize = { workspace = true } tokio = { workspace = true } clap = { workspace = true } @@ -27,7 +27,7 @@ serde_json = { workspace = true } toml = { workspace = true } dirs = { workspace = true } reqwest = { workspace = true, features = ["blocking"] } -openfang-runtime = { path = "../openfang-runtime" } +omtae-runtime = { path = "../omtae-runtime" } uuid = { workspace = true } ratatui = { workspace = true } colored = { workspace = true } diff --git a/crates/openfang-cli/src/bundled_agents.rs b/crates/openfang-cli/src/bundled_agents.rs index 0194859b16..0f6678f465 100644 --- a/crates/openfang-cli/src/bundled_agents.rs +++ b/crates/openfang-cli/src/bundled_agents.rs @@ -1,7 +1,7 @@ //! Compile-time embedded agent templates. //! //! All 30 bundled agent templates are embedded into the binary via `include_str!`. -//! This ensures `openfang agent new` works immediately after install — no filesystem +//! This ensures `omtae agent new` works immediately after install — no filesystem //! discovery needed. /// Returns all bundled agent templates as `(name, toml_content)` pairs. @@ -118,7 +118,7 @@ pub fn bundled_agents() -> Vec<(&'static str, &'static str)> { ] } -/// Install bundled agent templates to `~/.openfang/agents/`. +/// Install bundled agent templates to `~/.omtae/agents/`. /// Skips any template that already exists on disk (user customization preserved). pub fn install_bundled_agents(agents_dir: &std::path::Path) { for (name, content) in bundled_agents() { diff --git a/crates/openfang-cli/src/dotenv.rs b/crates/openfang-cli/src/dotenv.rs index 5bdfa149f0..31476f3fba 100644 --- a/crates/openfang-cli/src/dotenv.rs +++ b/crates/openfang-cli/src/dotenv.rs @@ -1,4 +1,4 @@ -//! Minimal `.env` file loader/saver for `~/.openfang/.env`. +//! Minimal `.env` file loader/saver for `~/.omtae/.env`. //! //! No external crate needed — hand-rolled for simplicity. //! Format: `KEY=VALUE` lines, `#` comments, optional quotes. @@ -6,20 +6,20 @@ use std::collections::BTreeMap; use std::path::PathBuf; -/// Get the OpenFang home directory, respecting OPENFANG_HOME env var. -fn dotenv_openfang_home() -> Option { +/// Get the OMTAE home directory, respecting OPENFANG_HOME env var. +fn dotenv_omtae_home() -> Option { if let Ok(home) = std::env::var("OPENFANG_HOME") { return Some(PathBuf::from(home)); } - dirs::home_dir().map(|h| h.join(".openfang")) + dirs::home_dir().map(|h| h.join(".omtae")) } -/// Return the path to `~/.openfang/.env`. +/// Return the path to `~/.omtae/.env`. pub fn env_file_path() -> Option { - dotenv_openfang_home().map(|h| h.join(".env")) + dotenv_omtae_home().map(|h| h.join(".env")) } -/// Load `~/.openfang/.env` and `~/.openfang/secrets.env` into `std::env`. +/// Load `~/.omtae/.env` and `~/.omtae/secrets.env` into `std::env`. /// /// System env vars take priority — existing vars are NOT overridden. /// `secrets.env` is loaded second so `.env` values take priority over secrets @@ -31,9 +31,9 @@ pub fn load_dotenv() { load_env_file(secrets_env_path()); } -/// Return the path to `~/.openfang/secrets.env`. +/// Return the path to `~/.omtae/secrets.env`. pub fn secrets_env_path() -> Option { - dotenv_openfang_home().map(|h| h.join("secrets.env")) + dotenv_omtae_home().map(|h| h.join("secrets.env")) } fn load_env_file(path: Option) { @@ -61,7 +61,7 @@ fn load_env_file(path: Option) { } } -/// Upsert a key in `~/.openfang/.env`. +/// Upsert a key in `~/.omtae/.env`. /// /// Creates the file if missing. Sets 0600 permissions on Unix. /// Also sets the key in the current process environment. @@ -83,7 +83,7 @@ pub fn save_env_key(key: &str, value: &str) -> Result<(), String> { Ok(()) } -/// Remove a key from `~/.openfang/.env`. +/// Remove a key from `~/.omtae/.env`. /// /// Also removes it from the current process environment. pub fn remove_env_key(key: &str) -> Result<(), String> { @@ -98,7 +98,7 @@ pub fn remove_env_key(key: &str) -> Result<(), String> { Ok(()) } -/// List key names (without values) from `~/.openfang/.env`. +/// List key names (without values) from `~/.omtae/.env`. #[allow(dead_code)] pub fn list_env_keys() -> Vec { let path = match env_file_path() { @@ -165,7 +165,7 @@ fn read_env_file(path: &PathBuf) -> BTreeMap { /// Write key-value pairs back to the .env file with a header comment. fn write_env_file(path: &PathBuf, entries: &BTreeMap) -> Result<(), String> { let mut content = - String::from("# OpenFang environment — managed by `openfang config set-key`\n"); + String::from("# OMTAE environment — managed by `omtae config set-key`\n"); content.push_str("# Do not edit while the daemon is running.\n\n"); for (key, value) in entries { diff --git a/crates/openfang-cli/src/launcher.rs b/crates/openfang-cli/src/launcher.rs index 18a8f12363..77e34466eb 100644 --- a/crates/openfang-cli/src/launcher.rs +++ b/crates/openfang-cli/src/launcher.rs @@ -1,6 +1,6 @@ //! Interactive launcher — lightweight Ratatui one-shot menu. //! -//! Shown when `openfang` is run with no subcommand in a TTY. +//! Shown when `omtae` is run with no subcommand in a TTY. //! Full-width left-aligned layout, adapts for first-time vs returning users. use ratatui::crossterm::event::{self, Event as CtEvent, KeyCode, KeyEventKind}; @@ -43,7 +43,7 @@ fn is_first_run() -> bool { std::path::PathBuf::from(h) } else { match dirs::home_dir() { - Some(h) => h.join(".openfang"), + Some(h) => h.join(".omtae"), None => return true, } }; @@ -358,7 +358,7 @@ fn draw(frame: &mut ratatui::Frame, state: &mut LauncherState) { let header_lines = vec![ Line::from(vec![ Span::styled( - "OpenFang", + "OMTAE", Style::default() .fg(theme::ACCENT) .add_modifier(Modifier::BOLD), @@ -378,7 +378,7 @@ fn draw(frame: &mut ratatui::Frame, state: &mut LauncherState) { } else { let header = Line::from(vec![ Span::styled( - "OpenFang", + "OMTAE", Style::default() .fg(theme::ACCENT) .add_modifier(Modifier::BOLD), @@ -549,9 +549,9 @@ pub fn launch_desktop_app() { let dir = exe.as_ref().and_then(|e| e.parent()); #[cfg(windows)] - let name = "openfang-desktop.exe"; + let name = "omtae-desktop.exe"; #[cfg(not(windows))] - let name = "openfang-desktop"; + let name = "omtae-desktop"; // Check sibling of current exe first let sibling = dir.map(|d| d.join(name)); @@ -576,7 +576,7 @@ pub fn launch_desktop_app() { Err(e) => { ui::error_with_fix( &format!("Failed to launch desktop app: {e}"), - "Build it: cargo build -p openfang-desktop", + "Build it: cargo build -p omtae-desktop", ); } } @@ -584,7 +584,7 @@ pub fn launch_desktop_app() { _ => { ui::error_with_fix( "Desktop app not found", - "Build it: cargo build -p openfang-desktop", + "Build it: cargo build -p omtae-desktop", ); } } diff --git a/crates/openfang-cli/src/main.rs b/crates/openfang-cli/src/main.rs index 62e728ba97..2174d1dd92 100644 --- a/crates/openfang-cli/src/main.rs +++ b/crates/openfang-cli/src/main.rs @@ -1,6 +1,6 @@ -//! OpenFang CLI — command-line interface for the OpenFang Agent OS. +//! OMTAE CLI — command-line interface for the OMTAE Agent OS. //! -//! When a daemon is running (`openfang start`), the CLI talks to it over HTTP. +//! When a daemon is running (`omtae start`), the CLI talks to it over HTTP. //! Otherwise, commands boot an in-process kernel (single-shot mode). mod bundled_agents; @@ -15,9 +15,9 @@ mod ui; use clap::{Parser, Subcommand}; use colored::Colorize; -use openfang_api::server::read_daemon_info; -use openfang_kernel::OpenFangKernel; -use openfang_types::agent::{AgentId, AgentManifest}; +use omtae_api::server::read_daemon_info; +use omtae_kernel::OMTAEKernel; +use omtae_types::agent::{AgentId, AgentManifest}; use std::io::{self, BufRead, Write}; use std::path::PathBuf; use std::sync::atomic::AtomicBool; @@ -63,34 +63,34 @@ const AFTER_HELP: &str = "\ \x1b[1mHint:\x1b[0m Commands suffixed with [*] have subcommands. Run ` --help` for details. \x1b[1;36mExamples:\x1b[0m - openfang init Initialize config and data directories - openfang start Start the kernel daemon - openfang tui Launch the interactive terminal dashboard - openfang chat Quick chat with the default agent - openfang agent new coder Spawn a new agent from a template - openfang models list Browse available LLM models - openfang add github Install the GitHub integration - openfang doctor Run diagnostic health checks - openfang channel setup Interactive channel setup wizard - openfang cron list List scheduled jobs - openfang uninstall Completely remove OpenFang from your system + omtae init Initialize config and data directories + omtae start Start the kernel daemon + omtae tui Launch the interactive terminal dashboard + omtae chat Quick chat with the default agent + omtae agent new coder Spawn a new agent from a template + omtae models list Browse available LLM models + omtae add github Install the GitHub integration + omtae doctor Run diagnostic health checks + omtae channel setup Interactive channel setup wizard + omtae cron list List scheduled jobs + omtae uninstall Completely remove OMTAE from your system \x1b[1;36mQuick Start:\x1b[0m - 1. openfang init Set up config + API key - 2. openfang start Launch the daemon - 3. openfang chat Start chatting! + 1. omtae init Set up config + API key + 2. omtae start Launch the daemon + 3. omtae chat Start chatting! \x1b[1;36mMore:\x1b[0m - Docs: https://github.com/RightNow-AI/openfang + Docs: https://github.com/RightNow-AI/omtae Dashboard: http://127.0.0.1:4200/ (when daemon is running)"; -/// OpenFang — the open-source Agent Operating System. +/// OMTAE — the open-source Agent Operating System. #[derive(Parser)] #[command( - name = "openfang", + name = "omtae", version, - about = "\u{1F40D} OpenFang \u{2014} Open-source Agent Operating System", - long_about = "\u{1F40D} OpenFang \u{2014} Open-source Agent Operating System\n\n\ + about = "\u{1F40D} OMTAE \u{2014} Open-source Agent Operating System", + long_about = "\u{1F40D} OMTAE \u{2014} Open-source Agent Operating System\n\n\ Deploy, manage, and orchestrate AI agents from your terminal.\n\ 40 channels \u{00b7} 60 skills \u{00b7} 50+ models \u{00b7} infinite possibilities.", after_help = AFTER_HELP, @@ -106,13 +106,13 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Initialize OpenFang (create ~/.openfang/ and default config). + /// Initialize OMTAE (create ~/.omtae/ and default config). Init { /// Quick mode: no prompts, just write config + .env (for CI/scripts). #[arg(long)] quick: bool, }, - /// Start the OpenFang kernel daemon (API server + kernel). + /// Start the OMTAE kernel daemon (API server + kernel). Start { /// Auto-approve all tool calls (no confirmation prompts). #[arg(long)] @@ -129,7 +129,7 @@ enum Commands { /// Manage event triggers (list, create, delete) [*]. #[command(subcommand)] Trigger(TriggerCommands), - /// Migrate from another agent framework to OpenFang. + /// Migrate from another agent framework to OMTAE. Migrate(MigrateArgs), /// Manage skills (install, list, search, create, remove) [*]. #[command(subcommand)] @@ -222,7 +222,7 @@ enum Commands { #[arg(long)] json: bool, }, - /// Tail the OpenFang log file. + /// Tail the OMTAE log file. Logs { /// Number of lines to show. #[arg(long, default_value = "50")] @@ -287,7 +287,7 @@ enum Commands { #[arg(long)] confirm: bool, }, - /// Completely uninstall OpenFang from your system. + /// Completely uninstall OMTAE from your system. Uninstall { /// Skip confirmation prompt (also --yes). #[arg(long, alias = "yes")] @@ -486,12 +486,12 @@ enum ConfigCommands { /// Dotted key path to remove (e.g. "api.cors_origin"). key: String, }, - /// Save an API key to ~/.openfang/.env (prompts interactively). + /// Save an API key to ~/.omtae/.env (prompts interactively). SetKey { /// Provider name (groq, anthropic, openai, gemini, deepseek, etc.). provider: String, }, - /// Remove an API key from ~/.openfang/.env. + /// Remove an API key from ~/.omtae/.env. DeleteKey { /// Provider name. provider: String, @@ -837,7 +837,7 @@ fn config_log_level() -> String { } else { dirs::home_dir() .unwrap_or_else(std::env::temp_dir) - .join(".openfang") + .join(".omtae") .join("config.toml") }; if let Ok(content) = std::fs::read_to_string(config_path) { @@ -866,19 +866,19 @@ fn init_tracing_stderr() { .init(); } -/// Get the OpenFang home directory, respecting OPENFANG_HOME env var. -fn cli_openfang_home() -> std::path::PathBuf { +/// Get the OMTAE home directory, respecting OPENFANG_HOME env var. +fn cli_omtae_home() -> std::path::PathBuf { if let Ok(home) = std::env::var("OPENFANG_HOME") { return std::path::PathBuf::from(home); } dirs::home_dir() .unwrap_or_else(std::env::temp_dir) - .join(".openfang") + .join(".omtae") } /// Redirect tracing to a log file so it doesn't corrupt the ratatui TUI. fn init_tracing_file() { - let log_dir = cli_openfang_home(); + let log_dir = cli_omtae_home(); let _ = std::fs::create_dir_all(&log_dir); let log_path = log_dir.join("tui.log"); @@ -919,7 +919,7 @@ fn write_stdout_safe(msg: &str) { } fn main() { - // Load ~/.openfang/.env into process environment (system env takes priority). + // Load ~/.omtae/.env into process environment (system env takes priority). dotenv::load_dotenv(); let cli = Cli::parse(); @@ -1164,7 +1164,7 @@ pub(crate) fn restrict_dir_permissions(path: &std::path::Path) { pub(crate) fn restrict_dir_permissions(_path: &std::path::Path) {} pub(crate) fn find_daemon() -> Option { - let home_dir = cli_openfang_home(); + let home_dir = cli_omtae_home(); let info = read_daemon_info(&home_dir)?; // Normalize listen address: replace 0.0.0.0 with 127.0.0.1 to avoid @@ -1217,7 +1217,7 @@ pub(crate) fn daemon_json( if status.is_server_error() { ui::error_with_fix( &format!("Daemon returned error ({})", status), - "Check daemon logs: ~/.openfang/tui.log", + "Check daemon logs: ~/.omtae/tui.log", ); } body @@ -1227,17 +1227,17 @@ pub(crate) fn daemon_json( if msg.contains("timed out") || msg.contains("Timeout") { ui::error_with_fix( "Request timed out", - "The agent may be processing a complex request. Try again, or check `openfang status`", + "The agent may be processing a complex request. Try again, or check `omtae status`", ); } else if msg.contains("Connection refused") || msg.contains("connect") { ui::error_with_fix( "Cannot connect to daemon", - "Is the daemon running? Start it with: openfang start", + "Is the daemon running? Start it with: omtae start", ); } else { ui::error_with_fix( &format!("Daemon communication error: {msg}"), - "Check `openfang status` or restart: openfang start", + "Check `omtae status` or restart: omtae start", ); } std::process::exit(1); @@ -1258,23 +1258,23 @@ fn cmd_init(quick: bool) { } }; - let openfang_dir = cli_openfang_home(); + let omtae_dir = cli_omtae_home(); // --- Ensure directories exist --- - if !openfang_dir.exists() { - std::fs::create_dir_all(&openfang_dir).unwrap_or_else(|e| { + if !omtae_dir.exists() { + std::fs::create_dir_all(&omtae_dir).unwrap_or_else(|e| { ui::error_with_fix( - &format!("Failed to create {}", openfang_dir.display()), + &format!("Failed to create {}", omtae_dir.display()), &format!("Check permissions on {}", home.display()), ); eprintln!(" {e}"); std::process::exit(1); }); - restrict_dir_permissions(&openfang_dir); + restrict_dir_permissions(&omtae_dir); } for sub in ["data", "agents"] { - let dir = openfang_dir.join(sub); + let dir = omtae_dir.join(sub); if !dir.exists() { std::fs::create_dir_all(&dir).unwrap_or_else(|e| { eprintln!("Error creating {sub} dir: {e}"); @@ -1284,43 +1284,43 @@ fn cmd_init(quick: bool) { } // Install bundled agent templates (skips existing ones to preserve user edits) - bundled_agents::install_bundled_agents(&openfang_dir.join("agents")); + bundled_agents::install_bundled_agents(&omtae_dir.join("agents")); if quick { - cmd_init_quick(&openfang_dir); + cmd_init_quick(&omtae_dir); } else if !std::io::IsTerminal::is_terminal(&std::io::stdin()) || !std::io::IsTerminal::is_terminal(&std::io::stdout()) { ui::hint("Non-interactive terminal detected — running in quick mode"); - ui::hint("For the interactive wizard, run: openfang init (in a terminal)"); - cmd_init_quick(&openfang_dir); + ui::hint("For the interactive wizard, run: omtae init (in a terminal)"); + cmd_init_quick(&omtae_dir); } else { - cmd_init_interactive(&openfang_dir); + cmd_init_interactive(&omtae_dir); } } /// Quick init: no prompts, auto-detect, write config + .env, print next steps. -fn cmd_init_quick(openfang_dir: &std::path::Path) { +fn cmd_init_quick(omtae_dir: &std::path::Path) { ui::banner(); ui::blank(); let (provider, api_key_env, model) = detect_best_provider(); - write_config_if_missing(openfang_dir, provider, model, api_key_env); + write_config_if_missing(omtae_dir, provider, model, api_key_env); ui::blank(); - ui::success("OpenFang initialized (quick mode)"); + ui::success("OMTAE initialized (quick mode)"); ui::kv("Provider", provider); ui::kv("Model", model); ui::blank(); ui::next_steps(&[ - "Start the daemon: openfang start", - "Chat: openfang chat", + "Start the daemon: omtae start", + "Chat: omtae chat", ]); } /// Interactive 5-step onboarding wizard (ratatui TUI). -fn cmd_init_interactive(openfang_dir: &std::path::Path) { +fn cmd_init_interactive(omtae_dir: &std::path::Path) { use tui::screens::init_wizard::{self, InitResult, LaunchChoice}; match init_wizard::run() { @@ -1332,7 +1332,7 @@ fn cmd_init_interactive(openfang_dir: &std::path::Path) { } => { // Print summary after TUI restores terminal ui::blank(); - ui::success("OpenFang initialized!"); + ui::success("OMTAE initialized!"); ui::kv("Provider", &provider); ui::kv("Model", &model); @@ -1344,7 +1344,7 @@ fn cmd_init_interactive(openfang_dir: &std::path::Path) { // Execute the user's chosen launch action. match launch { LaunchChoice::Desktop => { - launch_desktop_app(openfang_dir); + launch_desktop_app(omtae_dir); } LaunchChoice::Dashboard => { if let Some(base) = find_daemon() { @@ -1354,7 +1354,7 @@ fn cmd_init_interactive(openfang_dir: &std::path::Path) { ui::hint(&format!("Could not open browser. Visit: {url}")); } } else { - ui::error("Daemon is not running. Start it with: openfang start"); + ui::error("Daemon is not running. Start it with: omtae start"); } } LaunchChoice::Chat => { @@ -1374,24 +1374,24 @@ fn cmd_init_interactive(openfang_dir: &std::path::Path) { } } -/// Launch the openfang-desktop Tauri app, connecting to the running daemon. -fn launch_desktop_app(_openfang_dir: &std::path::Path) { +/// Launch the omtae-desktop Tauri app, connecting to the running daemon. +fn launch_desktop_app(_omtae_dir: &std::path::Path) { // Look for the desktop binary next to our own executable. let desktop_bin = { let exe = std::env::current_exe().ok(); let dir = exe.as_ref().and_then(|e| e.parent()); #[cfg(windows)] - let name = "openfang-desktop.exe"; + let name = "omtae-desktop.exe"; #[cfg(not(windows))] - let name = "openfang-desktop"; + let name = "omtae-desktop"; dir.map(|d| d.join(name)) }; match desktop_bin { Some(ref path) if path.exists() => { - ui::success("Launching OpenFang Desktop..."); + ui::success("Launching OMTAE Desktop..."); match std::process::Command::new(path) .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) @@ -1403,13 +1403,13 @@ fn launch_desktop_app(_openfang_dir: &std::path::Path) { } Err(e) => { ui::error(&format!("Failed to launch desktop app: {e}")); - ui::hint("Try: openfang dashboard"); + ui::hint("Try: omtae dashboard"); } } } _ => { ui::error("Desktop app not found."); - ui::hint("Install it with: cargo install openfang-desktop"); + ui::hint("Install it with: cargo install omtae-desktop"); ui::hint("Falling back to web dashboard..."); ui::blank(); if let Some(base) = find_daemon() { @@ -1424,7 +1424,7 @@ fn launch_desktop_app(_openfang_dir: &std::path::Path) { // opener may still fail asynchronously. ui::hint(&format!("Dashboard: {url}")); } else { - ui::hint("Daemon is not running. Start it with: openfang start"); + ui::hint("Daemon is not running. Start it with: omtae start"); ui::hint("Then open: http://127.0.0.1:4200"); } } @@ -1511,18 +1511,18 @@ fn check_ollama_available() -> bool { /// Write config.toml if it doesn't already exist. fn write_config_if_missing( - openfang_dir: &std::path::Path, + omtae_dir: &std::path::Path, provider: &str, model: &str, api_key_env: &str, ) { - let config_path = openfang_dir.join("config.toml"); + let config_path = omtae_dir.join("config.toml"); if config_path.exists() { ui::check_ok(&format!("Config already exists: {}", config_path.display())); } else { let default_config = format!( - r#"# OpenFang Agent OS configuration -# See https://github.com/RightNow-AI/openfang for documentation + r#"# OMTAE Agent OS configuration +# See https://github.com/RightNow-AI/omtae for documentation # For Docker, change to "0.0.0.0:4200" or set OPENFANG_LISTEN env var. api_listen = "127.0.0.1:4200" @@ -1549,7 +1549,7 @@ fn cmd_start(config: Option, yolo: bool) { if let Some(base) = find_daemon() { ui::error_with_fix( &format!("Daemon already running at {base}"), - "Use `openfang status` to check it, or stop it first", + "Use `omtae status` to check it, or stop it first", ); std::process::exit(1); } @@ -1561,12 +1561,12 @@ fn cmd_start(config: Option, yolo: bool) { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { - let mut kernel_config = openfang_kernel::config::load_config(config.as_deref()); + let mut kernel_config = omtae_kernel::config::load_config(config.as_deref()); if yolo { kernel_config.approval.auto_approve = true; kernel_config.approval.apply_shorthands(); } - let kernel = match OpenFangKernel::boot_with_config(kernel_config) { + let kernel = match OMTAEKernel::boot_with_config(kernel_config) { Ok(k) => k, Err(e) => { boot_kernel_error(&e); @@ -1598,29 +1598,29 @@ fn cmd_start(config: Option, yolo: bool) { ui::kv("Provider", &provider); ui::kv("Model", &model); ui::blank(); - ui::hint("Open the dashboard in your browser, or run `openfang chat`"); + ui::hint("Open the dashboard in your browser, or run `omtae chat`"); ui::hint("Press Ctrl+C to stop the daemon"); ui::blank(); if let Err(e) = - openfang_api::server::run_daemon(kernel, &listen_addr, Some(&daemon_info_path)).await + omtae_api::server::run_daemon(kernel, &listen_addr, Some(&daemon_info_path)).await { ui::error(&format!("Daemon error: {e}")); std::process::exit(1); } ui::blank(); - println!(" OpenFang daemon stopped."); + println!(" OMTAE daemon stopped."); }); } -/// Read the api_key from ~/.openfang/config.toml (if any). +/// Read the api_key from ~/.omtae/config.toml (if any). /// /// Returns `None` when the key is missing, empty, or whitespace-only — /// meaning the daemon is running in public (unauthenticated) mode. fn read_api_key() -> Option { // 1. Config file takes precedence - let config_path = cli_openfang_home().join("config.toml"); + let config_path = cli_omtae_home().join("config.toml"); if let Ok(text) = std::fs::read_to_string(config_path) { if let Ok(table) = text.parse::() { if let Some(key) = table.get("api_key").and_then(|v| v.as_str()) { @@ -1657,7 +1657,7 @@ fn cmd_stop() { } // Still alive — force kill via PID { - let of_dir = cli_openfang_home(); + let of_dir = cli_omtae_home(); if let Some(info) = read_daemon_info(&of_dir) { force_kill_pid(info.pid); let _ = std::fs::remove_file(of_dir.join("daemon.json")); @@ -1676,7 +1676,7 @@ fn cmd_stop() { None => { ui::warn_with_fix( "No running daemon found", - "Is it running? Check with: openfang status", + "Is it running? Check with: omtae status", ); } } @@ -1698,27 +1698,27 @@ fn force_kill_pid(pid: u32) { } /// Show context-aware error for kernel boot failures. -fn boot_kernel_error(e: &openfang_kernel::error::KernelError) { +fn boot_kernel_error(e: &omtae_kernel::error::KernelError) { let msg = e.to_string(); if msg.contains("parse") || msg.contains("toml") || msg.contains("config") { ui::error_with_fix( "Failed to parse configuration", - "Check your config.toml syntax: openfang config show", + "Check your config.toml syntax: omtae config show", ); } else if msg.contains("database") || msg.contains("locked") || msg.contains("sqlite") { ui::error_with_fix( "Database error (file may be locked)", - "Check if another OpenFang process is running: openfang status", + "Check if another OMTAE process is running: omtae status", ); } else if msg.contains("key") || msg.contains("API") || msg.contains("auth") { ui::error_with_fix( "LLM provider authentication failed", - "Run `openfang doctor` to check your API key configuration", + "Run `omtae doctor` to check your API key configuration", ); } else { ui::error_with_fix( &format!("Failed to boot kernel: {msg}"), - "Run `openfang doctor` to diagnose the issue", + "Run `omtae doctor` to diagnose the issue", ); } } @@ -1727,7 +1727,7 @@ fn cmd_agent_spawn(config: Option, manifest_path: PathBuf) { if !manifest_path.exists() { ui::error_with_fix( &format!("Manifest file not found: {}", manifest_path.display()), - "Use `openfang agent new` to spawn from a template instead", + "Use `omtae agent new` to spawn from a template instead", ); std::process::exit(1); } @@ -1767,7 +1767,7 @@ fn cmd_agent_spawn(config: Option, manifest_path: PathBuf) { println!("Agent spawned (in-process mode)."); println!(" ID: {id}"); println!("\n Note: Agent will be lost when this process exits."); - println!(" For persistent agents, use `openfang start` first."); + println!(" For persistent agents, use `omtae start` first."); } Err(e) => { eprintln!("Failed to spawn agent: {e}"); @@ -1913,7 +1913,7 @@ fn cmd_agent_set(agent_id_str: &str, field: &str, value: &str) { std::process::exit(1); } } else { - eprintln!("No running daemon found. Start one with: openfang start"); + eprintln!("No running daemon found. Start one with: omtae start"); std::process::exit(1); } } @@ -1929,7 +1929,7 @@ fn cmd_agent_new(config: Option, template_name: Option) { if all_templates.is_empty() { ui::error_with_fix( "No agent templates found", - "Run `openfang init` to set up the agents directory", + "Run `omtae init` to set up the agents directory", ); std::process::exit(1); } @@ -1941,7 +1941,7 @@ fn cmd_agent_new(config: Option, template_name: Option) { None => { ui::error_with_fix( &format!("Template '{name}' not found"), - "Run `openfang agent new` to see available templates", + "Run `omtae agent new` to see available templates", ); std::process::exit(1); } @@ -2000,7 +2000,7 @@ fn spawn_template_agent(config: Option, template: &templates::AgentTemp ui::kv("Model", &format!("{provider}/{model}")); } ui::blank(); - ui::hint(&format!("Chat: openfang chat {}", template.name)); + ui::hint(&format!("Chat: omtae chat {}", template.name)); } else { ui::error(&format!( "Failed to spawn: {}", @@ -2023,9 +2023,9 @@ fn spawn_template_agent(config: Option, template: &templates::AgentTemp ui::success(&format!("Agent '{}' spawned (in-process)", template.name)); ui::kv("ID", &id.to_string()); ui::blank(); - ui::hint(&format!("Chat: openfang chat {}", template.name)); + ui::hint(&format!("Chat: omtae chat {}", template.name)); ui::hint("Note: Agent will be lost when this process exits"); - ui::hint("For persistent agents, use `openfang start` first"); + ui::hint("For persistent agents, use `omtae start` first"); } Err(e) => { ui::error(&format!("Failed to spawn agent: {e}")); @@ -2048,7 +2048,7 @@ fn cmd_status(config: Option, json: bool) { return; } - ui::section("OpenFang Daemon Status"); + ui::section("OMTAE Daemon Status"); ui::blank(); ui::kv_ok("Status", body["status"].as_str().unwrap_or("?")); ui::kv( @@ -2101,7 +2101,7 @@ fn cmd_status(config: Option, json: bool) { return; } - ui::section("OpenFang Status (in-process)"); + ui::section("OMTAE Status (in-process)"); ui::blank(); ui::kv("Agents", &agent_count.to_string()); ui::kv("Provider", &kernel.config.default_model.provider); @@ -2109,7 +2109,7 @@ fn cmd_status(config: Option, json: bool) { ui::kv("Data dir", &kernel.config.data_dir.display().to_string()); ui::kv_warn("Daemon", "NOT RUNNING"); ui::blank(); - ui::hint("Run `openfang start` to launch the daemon"); + ui::hint("Run `omtae start` to launch the daemon"); if agent_count > 0 { ui::blank(); @@ -2127,33 +2127,33 @@ fn cmd_doctor(json: bool, repair: bool) { let mut repaired = false; if !json { - ui::step("OpenFang Doctor"); + ui::step("OMTAE Doctor"); println!(); } let home = dirs::home_dir(); if let Some(_h) = &home { - let openfang_dir = cli_openfang_home(); + let omtae_dir = cli_omtae_home(); - // --- Check 1: OpenFang directory --- - if openfang_dir.exists() { + // --- Check 1: OMTAE directory --- + if omtae_dir.exists() { if !json { - ui::check_ok(&format!("OpenFang directory: {}", openfang_dir.display())); + ui::check_ok(&format!("OMTAE directory: {}", omtae_dir.display())); } - checks.push(serde_json::json!({"check": "openfang_dir", "status": "ok", "path": openfang_dir.display().to_string()})); + checks.push(serde_json::json!({"check": "omtae_dir", "status": "ok", "path": omtae_dir.display().to_string()})); } else if repair { if !json { - ui::check_fail("OpenFang directory not found."); + ui::check_fail("OMTAE directory not found."); } let answer = prompt_input(" Create it now? [Y/n] "); if answer.is_empty() || answer.starts_with('y') || answer.starts_with('Y') { - if std::fs::create_dir_all(&openfang_dir).is_ok() { - restrict_dir_permissions(&openfang_dir); + if std::fs::create_dir_all(&omtae_dir).is_ok() { + restrict_dir_permissions(&omtae_dir); for sub in ["data", "agents"] { - let _ = std::fs::create_dir_all(openfang_dir.join(sub)); + let _ = std::fs::create_dir_all(omtae_dir.join(sub)); } if !json { - ui::check_ok("Created OpenFang directory"); + ui::check_ok("Created OMTAE directory"); } repaired = true; } else { @@ -2165,17 +2165,17 @@ fn cmd_doctor(json: bool, repair: bool) { } else { all_ok = false; } - checks.push(serde_json::json!({"check": "openfang_dir", "status": if repaired { "repaired" } else { "fail" }})); + checks.push(serde_json::json!({"check": "omtae_dir", "status": if repaired { "repaired" } else { "fail" }})); } else { if !json { - ui::check_fail("OpenFang directory not found. Run `openfang init` first."); + ui::check_fail("OMTAE directory not found. Run `omtae init` first."); } - checks.push(serde_json::json!({"check": "openfang_dir", "status": "fail"})); + checks.push(serde_json::json!({"check": "omtae_dir", "status": "fail"})); all_ok = false; } // --- Check 2: .env file exists + permissions --- - let env_path = openfang_dir.join(".env"); + let env_path = omtae_dir.join(".env"); if env_path.exists() { #[cfg(unix)] { @@ -2215,14 +2215,14 @@ fn cmd_doctor(json: bool, repair: bool) { } else { if !json { ui::check_warn( - ".env file not found (create with: openfang config set-key )", + ".env file not found (create with: omtae config set-key )", ); } checks.push(serde_json::json!({"check": "env_file", "status": "warn"})); } // --- Check 3: Config TOML syntax validation --- - let config_path = openfang_dir.join("config.toml"); + let config_path = omtae_dir.join("config.toml"); if config_path.exists() { let config_content = std::fs::read_to_string(&config_path).unwrap_or_default(); match toml::from_str::(&config_content) { @@ -2235,7 +2235,7 @@ fn cmd_doctor(json: bool, repair: bool) { Err(e) => { if !json { ui::check_fail(&format!("Config file has syntax errors: {e}")); - ui::hint("Fix with: openfang config edit"); + ui::hint("Fix with: omtae config edit"); } checks.push(serde_json::json!({"check": "config_syntax", "status": "fail", "error": e.to_string()})); all_ok = false; @@ -2249,8 +2249,8 @@ fn cmd_doctor(json: bool, repair: bool) { if answer.is_empty() || answer.starts_with('y') || answer.starts_with('Y') { let (provider, api_key_env, model) = detect_best_provider(); let default_config = format!( - r#"# OpenFang Agent OS configuration -# See https://github.com/RightNow-AI/openfang for documentation + r#"# OMTAE Agent OS configuration +# See https://github.com/RightNow-AI/omtae for documentation # For Docker, change to "0.0.0.0:4200" or set OPENFANG_LISTEN env var. api_listen = "127.0.0.1:4200" @@ -2264,7 +2264,7 @@ api_key_env = "{api_key_env}" decay_rate = 0.05 "# ); - let _ = std::fs::create_dir_all(&openfang_dir); + let _ = std::fs::create_dir_all(&omtae_dir); if std::fs::write(&config_path, default_config).is_ok() { restrict_file_permissions(&config_path); if !json { @@ -2292,11 +2292,11 @@ decay_rate = 0.05 // --- Check 4: Port availability --- // Read api_listen from config (default: 127.0.0.1:4200) let api_listen = { - let cfg_path = openfang_dir.join("config.toml"); + let cfg_path = omtae_dir.join("config.toml"); if cfg_path.exists() { std::fs::read_to_string(&cfg_path) .ok() - .and_then(|s| toml::from_str::(&s).ok()) + .and_then(|s| toml::from_str::(&s).ok()) .map(|c| c.api_listen) .unwrap_or_else(|| "127.0.0.1:4200".to_string()) } else { @@ -2314,7 +2314,7 @@ decay_rate = 0.05 checks.push(serde_json::json!({"check": "daemon", "status": "ok", "url": base})); } else { if !json { - ui::check_warn("Daemon not running (start with `openfang start`)"); + ui::check_warn("Daemon not running (start with `omtae start`)"); } checks.push(serde_json::json!({"check": "daemon", "status": "warn"})); @@ -2343,7 +2343,7 @@ decay_rate = 0.05 } // --- Check 5: Stale daemon.json --- - let daemon_json_path = openfang_dir.join("daemon.json"); + let daemon_json_path = omtae_dir.join("daemon.json"); if daemon_json_path.exists() && daemon_running.is_none() { if repair { let _ = std::fs::remove_file(&daemon_json_path); @@ -2360,7 +2360,7 @@ decay_rate = 0.05 } // --- Check 6: Database file --- - let db_path = openfang_dir.join("data").join("openfang.db"); + let db_path = omtae_dir.join("data").join("omtae.db"); if db_path.exists() { // Quick SQLite magic bytes check if let Ok(bytes) = std::fs::read(&db_path) { @@ -2388,7 +2388,7 @@ decay_rate = 0.05 #[cfg(unix)] { if let Ok(output) = std::process::Command::new("df") - .args(["-m", &openfang_dir.display().to_string()]) + .args(["-m", &omtae_dir.display().to_string()]) .output() { let stdout = String::from_utf8_lossy(&output.stdout); @@ -2419,7 +2419,7 @@ decay_rate = 0.05 } // --- Check 8: Agent manifests parse correctly --- - let agents_dir = openfang_dir.join("agents"); + let agents_dir = omtae_dir.join("agents"); if agents_dir.exists() { let mut agent_errors = Vec::new(); if let Ok(entries) = std::fs::read_dir(&agents_dir) { @@ -2509,8 +2509,8 @@ decay_rate = 0.05 // Check GitHub Copilot auth (separate from env var checks) { - let openfang_dir = cli_openfang_home(); - if openfang_runtime::drivers::copilot::copilot_auth_available(&openfang_dir) { + let omtae_dir = cli_omtae_home(); + if omtae_runtime::drivers::copilot::copilot_auth_available(&omtae_dir) { any_key_set = true; if !json { ui::check_ok("GitHub Copilot (authenticated via device flow)"); @@ -2531,7 +2531,7 @@ decay_rate = 0.05 ui::suggest_cmd("Gemini:", "https://aistudio.google.com (free tier)"); ui::suggest_cmd("DeepSeek:", "https://platform.deepseek.com (low cost)"); ui::blank(); - ui::hint("Or run: openfang config set-key groq"); + ui::hint("Or run: omtae config set-key groq"); } all_ok = false; } @@ -2576,8 +2576,8 @@ decay_rate = 0.05 // --- Check 11: .env keys vs config api_key_env consistency --- { - let openfang_dir = cli_openfang_home(); - let config_path = openfang_dir.join("config.toml"); + let omtae_dir = cli_omtae_home(); + let config_path = omtae_dir.join("config.toml"); if config_path.exists() { let config_str = std::fs::read_to_string(&config_path).unwrap_or_default(); // Look for api_key_env references in config @@ -2602,14 +2602,14 @@ decay_rate = 0.05 // --- Check 12: Config deserialization into KernelConfig --- { - let openfang_dir = cli_openfang_home(); - let config_path = openfang_dir.join("config.toml"); + let omtae_dir = cli_omtae_home(); + let config_path = omtae_dir.join("config.toml"); if config_path.exists() { if !json { println!("\n Config Validation:"); } let config_content = std::fs::read_to_string(&config_path).unwrap_or_default(); - match toml::from_str::(&config_content) { + match toml::from_str::(&config_content) { Ok(cfg) => { if !json { ui::check_ok("Config deserializes into KernelConfig"); @@ -2630,7 +2630,7 @@ decay_rate = 0.05 if !cfg.include.is_empty() { let mut include_ok = true; for inc in &cfg.include { - let inc_path = openfang_dir.join(inc); + let inc_path = omtae_dir.join(inc); if inc_path.exists() { if !json { ui::check_ok(&format!("Include file: {inc}")); @@ -2660,7 +2660,7 @@ decay_rate = 0.05 for server in &cfg.mcp_servers { // Validate transport config match &server.transport { - openfang_types::config::McpTransportEntry::Stdio { + omtae_types::config::McpTransportEntry::Stdio { command, .. } => { @@ -2674,8 +2674,8 @@ decay_rate = 0.05 checks.push(serde_json::json!({"check": "mcp_server_config", "status": "warn", "name": server.name})); } } - openfang_types::config::McpTransportEntry::Sse { url } - | openfang_types::config::McpTransportEntry::Http { url } => { + omtae_types::config::McpTransportEntry::Sse { url } + | omtae_types::config::McpTransportEntry::Http { url } => { if url.is_empty() { if !json { ui::check_warn(&format!( @@ -2707,8 +2707,8 @@ decay_rate = 0.05 if !json { println!("\n Skills:"); } - let skills_dir = cli_openfang_home().join("skills"); - let mut skill_reg = openfang_skills::registry::SkillRegistry::new(skills_dir.clone()); + let skills_dir = cli_omtae_home().join("skills"); + let mut skill_reg = omtae_skills::registry::SkillRegistry::new(skills_dir.clone()); skill_reg.load_bundled(); let bundled_count = skill_reg.count(); if !json { @@ -2745,11 +2745,11 @@ decay_rate = 0.05 let mut injection_warnings = 0; for skill in &skills { if let Some(ref prompt) = skill.manifest.prompt_context { - let warnings = openfang_skills::verify::SkillVerifier::scan_prompt_content(prompt); + let warnings = omtae_skills::verify::SkillVerifier::scan_prompt_content(prompt); let has_critical = warnings.iter().any(|w| { matches!( w.severity, - openfang_skills::verify::WarningSeverity::Critical + omtae_skills::verify::WarningSeverity::Critical ) }); if has_critical { @@ -2785,9 +2785,9 @@ decay_rate = 0.05 if !json { println!("\n Extensions:"); } - let openfang_dir = cli_openfang_home(); + let omtae_dir = cli_omtae_home(); let mut ext_registry = - openfang_extensions::registry::IntegrationRegistry::new(&openfang_dir); + omtae_extensions::registry::IntegrationRegistry::new(&omtae_dir); ext_registry.load_bundled(); let _ = ext_registry.load_installed(); let template_count = ext_registry.template_count(); @@ -3019,16 +3019,16 @@ decay_rate = 0.05 } else { println!(); if all_ok { - ui::success("All checks passed! OpenFang is ready."); + ui::success("All checks passed! OMTAE is ready."); if find_daemon().is_none() { - ui::hint("Start the daemon: openfang start"); + ui::hint("Start the daemon: omtae start"); } } else if repaired { - ui::success("Repairs applied. Re-run `openfang doctor` to verify."); + ui::success("Repairs applied. Re-run `omtae doctor` to verify."); } else { ui::error("Some checks failed."); if !repair { - ui::hint("Run `openfang doctor --repair` to attempt auto-fix"); + ui::hint("Run `omtae doctor --repair` to attempt auto-fix"); } } } @@ -3052,7 +3052,7 @@ fn cmd_dashboard() { Err(e) => { ui::error_with_fix( &format!("Could not start daemon: {e}"), - "Start it manually: openfang start", + "Start it manually: omtae start", ); std::process::exit(1); } @@ -3195,7 +3195,7 @@ pub(crate) fn open_in_browser(url: &str) -> bool { fn cmd_completion(shell: clap_complete::Shell) { use clap::CommandFactory; let mut cmd = Cli::command(); - clap_complete::generate(shell, &mut cmd, "openfang", &mut std::io::stdout()); + clap_complete::generate(shell, &mut cmd, "omtae", &mut std::io::stdout()); } // --------------------------------------------------------------------------- @@ -3481,16 +3481,16 @@ fn cmd_trigger_delete(trigger_id: &str) { fn require_daemon(command: &str) -> String { find_daemon().unwrap_or_else(|| { ui::error_with_fix( - &format!("`openfang {command}` requires a running daemon"), - "Start the daemon: openfang start", + &format!("`omtae {command}` requires a running daemon"), + "Start the daemon: omtae start", ); - ui::hint("Or try `openfang chat` which works without a daemon"); + ui::hint("Or try `omtae chat` which works without a daemon"); std::process::exit(1); }) } -fn boot_kernel(config: Option) -> OpenFangKernel { - match OpenFangKernel::boot(config.as_deref()) { +fn boot_kernel(config: Option) -> OMTAEKernel { + match OMTAEKernel::boot(config.as_deref()) { Ok(k) => k, Err(e) => { boot_kernel_error(&e); @@ -3505,9 +3505,9 @@ fn boot_kernel(config: Option) -> OpenFangKernel { fn cmd_migrate(args: MigrateArgs) { let source = match args.from { - MigrateSourceArg::Openclaw => openfang_migrate::MigrateSource::OpenClaw, - MigrateSourceArg::Langchain => openfang_migrate::MigrateSource::LangChain, - MigrateSourceArg::Autogpt => openfang_migrate::MigrateSource::AutoGpt, + MigrateSourceArg::Openclaw => omtae_migrate::MigrateSource::OpenClaw, + MigrateSourceArg::Langchain => omtae_migrate::MigrateSource::LangChain, + MigrateSourceArg::Autogpt => omtae_migrate::MigrateSource::AutoGpt, }; let source_dir = args.source_dir.unwrap_or_else(|| { @@ -3516,27 +3516,27 @@ fn cmd_migrate(args: MigrateArgs) { std::process::exit(1); }); match source { - openfang_migrate::MigrateSource::OpenClaw => home.join(".openclaw"), - openfang_migrate::MigrateSource::LangChain => home.join(".langchain"), - openfang_migrate::MigrateSource::AutoGpt => home.join("Auto-GPT"), + omtae_migrate::MigrateSource::OpenClaw => home.join(".openclaw"), + omtae_migrate::MigrateSource::LangChain => home.join(".langchain"), + omtae_migrate::MigrateSource::AutoGpt => home.join("Auto-GPT"), } }); - let target_dir = cli_openfang_home(); + let target_dir = cli_omtae_home(); println!("Migrating from {} ({})...", source, source_dir.display()); if args.dry_run { println!(" (dry run — no changes will be made)\n"); } - let options = openfang_migrate::MigrateOptions { + let options = omtae_migrate::MigrateOptions { source, source_dir, target_dir, dry_run: args.dry_run, }; - match openfang_migrate::run_migration(&options) { + match omtae_migrate::run_migration(&options) { Ok(report) => { report.print_summary(); @@ -3562,7 +3562,7 @@ fn cmd_migrate(args: MigrateArgs) { // --------------------------------------------------------------------------- fn cmd_skill_install(source: &str) { - let home = openfang_home(); + let home = omtae_home(); let skills_dir = home.join("skills"); std::fs::create_dir_all(&skills_dir).unwrap_or_else(|e| { eprintln!("Error creating skills directory: {e}"); @@ -3575,14 +3575,14 @@ fn cmd_skill_install(source: &str) { let manifest_path = source_path.join("skill.toml"); if !manifest_path.exists() { // Check if it's an OpenClaw skill - if openfang_skills::openclaw_compat::detect_openclaw_skill(&source_path) { + if omtae_skills::openclaw_compat::detect_openclaw_skill(&source_path) { println!("Detected OpenClaw skill format. Converting..."); - match openfang_skills::openclaw_compat::convert_openclaw_skill(&source_path) { + match omtae_skills::openclaw_compat::convert_openclaw_skill(&source_path) { Ok(manifest) => { let dest = skills_dir.join(&manifest.skill.name); // Copy skill directory copy_dir_recursive(&source_path, &dest); - if let Err(e) = openfang_skills::openclaw_compat::write_openfang_manifest( + if let Err(e) = omtae_skills::openclaw_compat::write_omtae_manifest( &dest, &manifest, ) { eprintln!("Failed to write manifest: {e}"); @@ -3607,7 +3607,7 @@ fn cmd_skill_install(source: &str) { eprintln!("Error reading skill.toml: {e}"); std::process::exit(1); }); - let manifest: openfang_skills::SkillManifest = + let manifest: omtae_skills::SkillManifest = toml::from_str(&toml_str).unwrap_or_else(|e| { eprintln!("Error parsing skill.toml: {e}"); std::process::exit(1); @@ -3656,13 +3656,13 @@ fn cmd_skill_install(source: &str) { // Reuse the local directory install logic on the cloned repo let manifest_path = clone_path.join("skill.toml"); if !manifest_path.exists() { - if openfang_skills::openclaw_compat::detect_openclaw_skill(&clone_path) { + if omtae_skills::openclaw_compat::detect_openclaw_skill(&clone_path) { println!("Detected OpenClaw skill format. Converting..."); - match openfang_skills::openclaw_compat::convert_openclaw_skill(&clone_path) { + match omtae_skills::openclaw_compat::convert_openclaw_skill(&clone_path) { Ok(manifest) => { let dest = skills_dir.join(&manifest.skill.name); copy_dir_recursive(&clone_path, &dest); - if let Err(e) = openfang_skills::openclaw_compat::write_openfang_manifest( + if let Err(e) = omtae_skills::openclaw_compat::write_omtae_manifest( &dest, &manifest, ) { eprintln!("Failed to write manifest: {e}"); @@ -3686,7 +3686,7 @@ fn cmd_skill_install(source: &str) { eprintln!("Error reading skill.toml: {e}"); std::process::exit(1); }); - let manifest: openfang_skills::SkillManifest = + let manifest: omtae_skills::SkillManifest = toml::from_str(&toml_str).unwrap_or_else(|e| { eprintln!("Error parsing skill.toml: {e}"); std::process::exit(1); @@ -3703,8 +3703,8 @@ fn cmd_skill_install(source: &str) { // Remote install from FangHub println!("Installing {source} from FangHub..."); let rt = tokio::runtime::Runtime::new().unwrap(); - let client = openfang_skills::marketplace::MarketplaceClient::new( - openfang_skills::marketplace::MarketplaceConfig::default(), + let client = omtae_skills::marketplace::MarketplaceClient::new( + omtae_skills::marketplace::MarketplaceConfig::default(), ); match rt.block_on(client.install(source, &skills_dir)) { Ok(version) => { @@ -3730,19 +3730,19 @@ fn notify_daemon_skill_reload() { ui::step("Daemon notified — skill registry reloaded."); } _ => { - ui::check_warn("Could not notify daemon. Restart with: openfang restart"); + ui::check_warn("Could not notify daemon. Restart with: omtae restart"); } } } else { - ui::hint("Start the daemon to make this skill available to agents: openfang start"); + ui::hint("Start the daemon to make this skill available to agents: omtae start"); } } fn cmd_skill_list() { - let home = openfang_home(); + let home = omtae_home(); let skills_dir = home.join("skills"); - let mut registry = openfang_skills::registry::SkillRegistry::new(skills_dir); + let mut registry = omtae_skills::registry::SkillRegistry::new(skills_dir); match registry.load_all() { Ok(0) => println!("No skills installed."), Ok(count) => { @@ -3770,10 +3770,10 @@ fn cmd_skill_list() { } fn cmd_skill_remove(name: &str) { - let home = openfang_home(); + let home = omtae_home(); let skills_dir = home.join("skills"); - let mut registry = openfang_skills::registry::SkillRegistry::new(skills_dir); + let mut registry = omtae_skills::registry::SkillRegistry::new(skills_dir); let _ = registry.load_all(); match registry.remove(name) { Ok(()) => println!("Removed skill: {name}"), @@ -3786,8 +3786,8 @@ fn cmd_skill_remove(name: &str) { fn cmd_skill_search(query: &str) { let rt = tokio::runtime::Runtime::new().unwrap(); - let client = openfang_skills::marketplace::MarketplaceClient::new( - openfang_skills::marketplace::MarketplaceConfig::default(), + let client = omtae_skills::marketplace::MarketplaceClient::new( + omtae_skills::marketplace::MarketplaceConfig::default(), ); match rt.block_on(client.search(query)) { Ok(results) if results.is_empty() => println!("No skills found for \"{query}\"."), @@ -3819,7 +3819,7 @@ fn cmd_skill_create() { runtime }; - let home = openfang_home(); + let home = omtae_home(); let skill_dir = home.join("skills").join(&name); std::fs::create_dir_all(skill_dir.join("src")).unwrap_or_else(|e| { eprintln!("Error creating skill directory: {e}"); @@ -3857,7 +3857,7 @@ capabilities = [] let entry_content = match runtime.as_str() { "python" => format!( r#"#!/usr/bin/env python3 -"""OpenFang skill: {name}""" +"""OMTAE skill: {name}""" import json import sys @@ -3891,9 +3891,9 @@ if __name__ == "__main__": println!(" {entry_path}"); println!("\nNext steps:"); println!(" 1. Edit the entry point to implement your skill logic"); - println!(" 2. Test locally: openfang skill test"); + println!(" 2. Test locally: omtae skill test"); println!( - " 3. Install: openfang skill install {}", + " 3. Install: omtae skill install {}", skill_dir.display() ); } @@ -3903,11 +3903,11 @@ if __name__ == "__main__": // --------------------------------------------------------------------------- fn cmd_channel_list() { - let home = openfang_home(); + let home = omtae_home(); let config_path = home.join("config.toml"); if !config_path.exists() { - println!("No configuration found. Run `openfang init` first."); + println!("No configuration found. Run `omtae init` first."); return; } @@ -3950,7 +3950,7 @@ fn cmd_channel_list() { ); } - println!("\nUse `openfang channel setup ` to configure a channel."); + println!("\nUse `omtae channel setup ` to configure a channel."); } fn cmd_channel_setup(channel: Option<&str>) { @@ -4009,7 +4009,7 @@ fn cmd_channel_setup(channel: Option<&str>) { // Save token to .env match dotenv::save_env_key("TELEGRAM_BOT_TOKEN", &token) { - Ok(()) => ui::success("Token saved to ~/.openfang/.env"), + Ok(()) => ui::success("Token saved to ~/.omtae/.env"), Err(_) => println!(" export TELEGRAM_BOT_TOKEN={token}"), } @@ -4039,7 +4039,7 @@ fn cmd_channel_setup(channel: Option<&str>) { maybe_write_channel_config("discord", config_block); match dotenv::save_env_key("DISCORD_BOT_TOKEN", &token) { - Ok(()) => ui::success("Token saved to ~/.openfang/.env"), + Ok(()) => ui::success("Token saved to ~/.omtae/.env"), Err(_) => println!(" export DISCORD_BOT_TOKEN={token}"), } @@ -4067,13 +4067,13 @@ fn cmd_channel_setup(channel: Option<&str>) { if !app_token.is_empty() { match dotenv::save_env_key("SLACK_APP_TOKEN", &app_token) { - Ok(()) => ui::success("App token saved to ~/.openfang/.env"), + Ok(()) => ui::success("App token saved to ~/.omtae/.env"), Err(_) => println!(" export SLACK_APP_TOKEN={app_token}"), } } if !bot_token.is_empty() { match dotenv::save_env_key("SLACK_BOT_TOKEN", &bot_token) { - Ok(()) => ui::success("Bot token saved to ~/.openfang/.env"), + Ok(()) => ui::success("Bot token saved to ~/.omtae/.env"), Err(_) => println!(" export SLACK_BOT_TOKEN={bot_token}"), } } @@ -4107,7 +4107,7 @@ fn cmd_channel_setup(channel: Option<&str>) { ] { if !val.is_empty() { match dotenv::save_env_key(key, val) { - Ok(()) => ui::success(&format!("{key} saved to ~/.openfang/.env")), + Ok(()) => ui::success(&format!("{key} saved to ~/.omtae/.env")), Err(_) => println!(" export {key}={val}"), } } @@ -4139,11 +4139,11 @@ fn cmd_channel_setup(channel: Option<&str>) { if !password.is_empty() { match dotenv::save_env_key("EMAIL_PASSWORD", &password) { - Ok(()) => ui::success("Password saved to ~/.openfang/.env"), + Ok(()) => ui::success("Password saved to ~/.omtae/.env"), Err(_) => println!(" export EMAIL_PASSWORD=your_app_password"), } } else { - ui::hint("Set later: openfang config set-key email (or export EMAIL_PASSWORD=...)"); + ui::hint("Set later: omtae config set-key email (or export EMAIL_PASSWORD=...)"); } ui::blank(); @@ -4173,7 +4173,7 @@ fn cmd_channel_setup(channel: Option<&str>) { if !phone.is_empty() { match dotenv::save_env_key("SIGNAL_PHONE", &phone) { - Ok(()) => ui::success("Phone saved to ~/.openfang/.env"), + Ok(()) => ui::success("Phone saved to ~/.omtae/.env"), Err(_) => println!(" export SIGNAL_PHONE={phone}"), } } @@ -4186,10 +4186,10 @@ fn cmd_channel_setup(channel: Option<&str>) { ui::section("Setting up Matrix"); ui::blank(); println!(" 1. Create a bot account on your Matrix homeserver"); - println!(" (e.g., register @openfang-bot:matrix.org)"); + println!(" (e.g., register @omtae-bot:matrix.org)"); println!(" 2. Obtain an access token:"); println!(" curl -X POST https://matrix.org/_matrix/client/r0/login \\"); - println!(" -d '{{\"type\":\"m.login.password\",\"user\":\"openfang-bot\",\"password\":\"...\"}}'"); + println!(" -d '{{\"type\":\"m.login.password\",\"user\":\"omtae-bot\",\"password\":\"...\"}}'"); println!(" Copy the access_token from the response."); println!(" 3. Invite the bot to rooms you want it to monitor."); ui::blank(); @@ -4208,7 +4208,7 @@ fn cmd_channel_setup(channel: Option<&str>) { let _ = dotenv::save_env_key("MATRIX_HOMESERVER", &homeserver); if !token.is_empty() { match dotenv::save_env_key("MATRIX_ACCESS_TOKEN", &token) { - Ok(()) => ui::success("Token saved to ~/.openfang/.env"), + Ok(()) => ui::success("Token saved to ~/.omtae/.env"), Err(_) => println!(" export MATRIX_ACCESS_TOKEN={token}"), } } @@ -4229,11 +4229,11 @@ fn cmd_channel_setup(channel: Option<&str>) { /// Offer to append a channel config block to config.toml if it doesn't already exist. fn maybe_write_channel_config(channel: &str, config_block: &str) { - let home = openfang_home(); + let home = omtae_home(); let config_path = home.join("config.toml"); if !config_path.exists() { - ui::hint("No config.toml found. Run `openfang init` first."); + ui::hint("No config.toml found. Run `omtae init` first."); return; } @@ -4262,7 +4262,7 @@ fn notify_daemon_restart() { if find_daemon().is_some() { ui::check_warn("Restart the daemon to activate this channel"); } else { - ui::hint("Start the daemon: openfang start"); + ui::hint("Start the daemon: omtae start"); } } @@ -4283,7 +4283,7 @@ fn cmd_channel_test(channel: &str) { ); } } else { - eprintln!("Channel test requires a running daemon. Start with: openfang start"); + eprintln!("Channel test requires a running daemon. Start with: omtae start"); std::process::exit(1); } } @@ -4308,7 +4308,7 @@ fn cmd_channel_toggle(channel: &str, enable: bool) { } } else { println!("Note: Channel {channel} will be {action} when the daemon starts."); - println!("Edit ~/.openfang/config.toml to persist this change."); + println!("Edit ~/.omtae/config.toml to persist this change."); } } @@ -4360,7 +4360,7 @@ fn cmd_hand_install(path: &str) { body["id"].as_str().unwrap_or("?"), ); println!( - "Use `openfang hand activate {}` to start it.", + "Use `omtae hand activate {}` to start it.", body["id"].as_str().unwrap_or("?") ); } @@ -4403,7 +4403,7 @@ fn cmd_hand_list() { .collect::(), ); } - println!("\nUse `openfang hand activate ` to activate a hand."); + println!("\nUse `omtae hand activate ` to activate a hand."); } } @@ -4727,7 +4727,7 @@ fn cmd_hand_config( ui::error(&format!("Failed to update hand '{id}' settings: {err}")); if err.contains("No active instance") { ui::hint(&format!( - "Activate the hand first: openfang hand activate {id}" + "Activate the hand first: omtae hand activate {id}" )); } std::process::exit(1); @@ -4865,7 +4865,7 @@ pub(crate) fn test_api_key(provider: &str, env_var: &str) -> bool { // Background daemon start // --------------------------------------------------------------------------- -/// Spawn `openfang start` as a detached background process. +/// Spawn `omtae start` as a detached background process. /// /// Polls for daemon health for up to 10 seconds. Returns the daemon URL on success. pub(crate) fn start_daemon_background() -> Result { @@ -4913,12 +4913,12 @@ pub(crate) fn start_daemon_background() -> Result { // --------------------------------------------------------------------------- fn cmd_config_show() { - let home = openfang_home(); + let home = omtae_home(); let config_path = home.join("config.toml"); if !config_path.exists() { println!("No configuration found at: {}", config_path.display()); - println!("Run `openfang init` to create one."); + println!("Run `omtae init` to create one."); return; } @@ -4932,7 +4932,7 @@ fn cmd_config_show() { } fn cmd_config_edit() { - let home = openfang_home(); + let home = omtae_home(); let config_path = home.join("config.toml"); let editor = std::env::var("EDITOR") @@ -4994,11 +4994,11 @@ fn lookup_config_value(table: &toml::Value, key: &str) -> ConfigGetOutcome { } fn cmd_config_get(key: &str) { - let home = openfang_home(); + let home = omtae_home(); let config_path = home.join("config.toml"); if !config_path.exists() { - ui::error_with_fix("No config file found", "Run `openfang init` first"); + ui::error_with_fix("No config file found", "Run `omtae init` first"); std::process::exit(1); } @@ -5010,7 +5010,7 @@ fn cmd_config_get(key: &str) { let table: toml::Value = toml::from_str(&content).unwrap_or_else(|e| { ui::error_with_fix( &format!("Config parse error: {e}"), - "Fix your config.toml syntax, or run `openfang config edit`", + "Fix your config.toml syntax, or run `omtae config edit`", ); std::process::exit(1); }); @@ -5032,11 +5032,11 @@ fn cmd_config_get(key: &str) { } fn cmd_config_set(key: &str, value: &str) { - let home = openfang_home(); + let home = omtae_home(); let config_path = home.join("config.toml"); if !config_path.exists() { - ui::error_with_fix("No config file found", "Run `openfang init` first"); + ui::error_with_fix("No config file found", "Run `omtae init` first"); std::process::exit(1); } @@ -5155,11 +5155,11 @@ fn cmd_config_set(key: &str, value: &str) { } fn cmd_config_unset(key: &str) { - let home = openfang_home(); + let home = omtae_home(); let config_path = home.join("config.toml"); if !config_path.exists() { - ui::error_with_fix("No config file found", "Run `openfang init` first"); + ui::error_with_fix("No config file found", "Run `omtae init` first"); std::process::exit(1); } @@ -5225,17 +5225,17 @@ fn cmd_config_unset(key: &str) { fn cmd_config_set_key(provider: &str) { // GitHub Copilot uses OAuth device flow, not a simple API key paste. if provider == "github-copilot" || provider == "copilot" { - let openfang_dir = cli_openfang_home(); + let omtae_dir = cli_omtae_home(); let rt = tokio::runtime::Runtime::new().unwrap_or_else(|e| { ui::error(&format!("Failed to create async runtime: {e}")); std::process::exit(1); }); - match rt.block_on(openfang_runtime::drivers::copilot::run_interactive_setup( - &openfang_dir, + match rt.block_on(omtae_runtime::drivers::copilot::run_interactive_setup( + &omtae_dir, )) { Ok(_) => { ui::success("GitHub Copilot configured successfully"); - ui::hint("Restart the daemon: openfang stop && openfang start"); + ui::hint("Restart the daemon: omtae stop && omtae start"); } Err(e) => { ui::error(&format!("Copilot setup failed: {e}")); @@ -5260,7 +5260,7 @@ fn cmd_config_set_key(provider: &str) { // Always save to dotenv as fallback match dotenv::save_env_key(&env_var, &key) { Ok(()) => { - ui::success(&format!("Saved {env_var} to ~/.openfang/.env")); + ui::success(&format!("Saved {env_var} to ~/.omtae/.env")); // Test the key print!(" Testing key... "); io::stdout().flush().unwrap(); @@ -5282,10 +5282,10 @@ fn cmd_config_delete_key(provider: &str) { // Remove from vault (best-effort) { - let home = openfang_home(); + let home = omtae_home(); let vault_path = home.join("vault.enc"); if vault_path.exists() { - let mut vault = openfang_extensions::vault::CredentialVault::new(vault_path); + let mut vault = omtae_extensions::vault::CredentialVault::new(vault_path); if vault.unlock().is_ok() { let _ = vault.remove(&env_var); } @@ -5293,7 +5293,7 @@ fn cmd_config_delete_key(provider: &str) { } match dotenv::remove_env_key(&env_var) { - Ok(()) => ui::success(&format!("Removed {env_var} from ~/.openfang/.env")), + Ok(()) => ui::success(&format!("Removed {env_var} from ~/.omtae/.env")), Err(e) => { ui::error(&format!("Failed to remove key: {e}")); std::process::exit(1); @@ -5306,7 +5306,7 @@ fn cmd_config_test_key(provider: &str) { if std::env::var(&env_var).is_err() { ui::error(&format!("{env_var} not set")); - ui::hint(&format!("Set it: openfang config set-key {provider}")); + ui::hint(&format!("Set it: omtae config set-key {provider}")); std::process::exit(1); } @@ -5316,7 +5316,7 @@ fn cmd_config_test_key(provider: &str) { println!("{}", "OK".bright_green()); } else { println!("{}", "FAILED (401/403)".bright_red()); - ui::hint(&format!("Update key: openfang config set-key {provider}")); + ui::hint(&format!("Update key: omtae config set-key {provider}")); std::process::exit(1); } } @@ -5327,12 +5327,12 @@ fn cmd_config_test_key(provider: &str) { fn save_credential_prefer_vault(env_var: &str, value: &str) { use zeroize::Zeroizing; - let home = openfang_home(); + let home = omtae_home(); let vault_path = home.join("vault.enc"); if !vault_path.exists() { return; } - let mut vault = openfang_extensions::vault::CredentialVault::new(vault_path); + let mut vault = omtae_extensions::vault::CredentialVault::new(vault_path); if vault.unlock().is_err() { return; } @@ -5353,7 +5353,7 @@ fn cmd_quick_chat(config: Option, agent: Option) { // Helpers // --------------------------------------------------------------------------- -pub(crate) fn openfang_home() -> PathBuf { +pub(crate) fn omtae_home() -> PathBuf { if let Ok(home) = std::env::var("OPENFANG_HOME") { return PathBuf::from(home); } @@ -5362,7 +5362,7 @@ pub(crate) fn openfang_home() -> PathBuf { eprintln!("Error: Could not determine home directory"); std::process::exit(1); }) - .join(".openfang") + .join(".omtae") } fn prompt_input(prompt: &str) -> String { @@ -5389,12 +5389,12 @@ pub(crate) fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) { } // --------------------------------------------------------------------------- -// Integration commands (openfang add/remove/integrations) +// Integration commands (omtae add/remove/integrations) // --------------------------------------------------------------------------- fn cmd_integration_add(name: &str, key: Option<&str>) { - let home = openfang_home(); - let mut registry = openfang_extensions::registry::IntegrationRegistry::new(&home); + let home = omtae_home(); + let mut registry = omtae_extensions::registry::IntegrationRegistry::new(&home); registry.load_bundled(); let _ = registry.load_installed(); @@ -5415,7 +5415,7 @@ fn cmd_integration_add(name: &str, key: Option<&str>) { let dotenv_path = home.join(".env"); let vault_path = home.join("vault.enc"); let vault = if vault_path.exists() { - let mut v = openfang_extensions::vault::CredentialVault::new(vault_path); + let mut v = omtae_extensions::vault::CredentialVault::new(vault_path); if v.unlock().is_ok() { Some(v) } else { @@ -5425,7 +5425,7 @@ fn cmd_integration_add(name: &str, key: Option<&str>) { None }; let mut resolver = - openfang_extensions::credentials::CredentialResolver::new(vault, Some(&dotenv_path)) + omtae_extensions::credentials::CredentialResolver::new(vault, Some(&dotenv_path)) .with_interactive(true); // Build provided keys map @@ -5437,7 +5437,7 @@ fn cmd_integration_add(name: &str, key: Option<&str>) { } } - match openfang_extensions::installer::install_integration( + match omtae_extensions::installer::install_integration( &mut registry, &mut resolver, name, @@ -5445,15 +5445,15 @@ fn cmd_integration_add(name: &str, key: Option<&str>) { ) { Ok(result) => { match &result.status { - openfang_extensions::IntegrationStatus::Ready => { + omtae_extensions::IntegrationStatus::Ready => { ui::success(&result.message); } - openfang_extensions::IntegrationStatus::Setup => { + omtae_extensions::IntegrationStatus::Setup => { println!("{}", result.message.yellow()); println!("\nTo add credentials:"); for env in &template.required_env { if env.is_secret { - println!(" openfang vault set {} # {}", env.name, env.help); + println!(" omtae vault set {} # {}", env.name, env.help); if let Some(ref url) = env.get_url { println!(" Get it here: {url}"); } @@ -5479,12 +5479,12 @@ fn cmd_integration_add(name: &str, key: Option<&str>) { } fn cmd_integration_remove(name: &str) { - let home = openfang_home(); - let mut registry = openfang_extensions::registry::IntegrationRegistry::new(&home); + let home = omtae_home(); + let mut registry = omtae_extensions::registry::IntegrationRegistry::new(&home); registry.load_bundled(); let _ = registry.load_installed(); - match openfang_extensions::installer::remove_integration(&mut registry, name) { + match omtae_extensions::installer::remove_integration(&mut registry, name) { Ok(msg) => { ui::success(&msg); // Hot-reload daemon @@ -5503,19 +5503,19 @@ fn cmd_integration_remove(name: &str) { } fn cmd_integrations_list(query: Option<&str>) { - let home = openfang_home(); - let mut registry = openfang_extensions::registry::IntegrationRegistry::new(&home); + let home = omtae_home(); + let mut registry = omtae_extensions::registry::IntegrationRegistry::new(&home); registry.load_bundled(); let _ = registry.load_installed(); let dotenv_path = home.join(".env"); let resolver = - openfang_extensions::credentials::CredentialResolver::new(None, Some(&dotenv_path)); + omtae_extensions::credentials::CredentialResolver::new(None, Some(&dotenv_path)); let entries = if let Some(q) = query { - openfang_extensions::installer::search_integrations(®istry, q) + omtae_extensions::installer::search_integrations(®istry, q) } else { - openfang_extensions::installer::list_integrations(®istry, &resolver) + omtae_extensions::installer::list_integrations(®istry, &resolver) }; if entries.is_empty() { @@ -5530,7 +5530,7 @@ fn cmd_integrations_list(query: Option<&str>) { // Group by category let mut by_category: std::collections::BTreeMap< String, - Vec<&openfang_extensions::installer::IntegrationListEntry>, + Vec<&omtae_extensions::installer::IntegrationListEntry>, > = std::collections::BTreeMap::new(); for entry in &entries { by_category @@ -5543,15 +5543,15 @@ fn cmd_integrations_list(query: Option<&str>) { println!("\n{}", format!(" {category}").bold()); for item in items { let status_badge = match &item.status { - openfang_extensions::IntegrationStatus::Ready => "[Ready]".green().to_string(), - openfang_extensions::IntegrationStatus::Setup => "[Setup]".yellow().to_string(), - openfang_extensions::IntegrationStatus::Available => { + omtae_extensions::IntegrationStatus::Ready => "[Ready]".green().to_string(), + omtae_extensions::IntegrationStatus::Setup => "[Setup]".yellow().to_string(), + omtae_extensions::IntegrationStatus::Available => { "[Available]".dimmed().to_string() } - openfang_extensions::IntegrationStatus::Error(msg) => { + omtae_extensions::IntegrationStatus::Error(msg) => { format!("[Error: {msg}]").red().to_string() } - openfang_extensions::IntegrationStatus::Disabled => { + omtae_extensions::IntegrationStatus::Disabled => { "[Disabled]".dimmed().to_string() } }; @@ -5569,22 +5569,22 @@ fn cmd_integrations_list(query: Option<&str>) { .iter() .filter(|e| matches!( e.status, - openfang_extensions::IntegrationStatus::Ready - | openfang_extensions::IntegrationStatus::Setup + omtae_extensions::IntegrationStatus::Ready + | omtae_extensions::IntegrationStatus::Setup )) .count() ); - println!(" Use `openfang add ` to install an integration."); + println!(" Use `omtae add ` to install an integration."); } // --------------------------------------------------------------------------- -// Vault commands (openfang vault init/set/list/remove) +// Vault commands (omtae vault init/set/list/remove) // --------------------------------------------------------------------------- fn cmd_vault_init() { - let home = openfang_home(); + let home = omtae_home(); let vault_path = home.join("vault.enc"); - let mut vault = openfang_extensions::vault::CredentialVault::new(vault_path); + let mut vault = omtae_extensions::vault::CredentialVault::new(vault_path); match vault.init() { Ok(()) => ui::success("Credential vault initialized."), @@ -5598,12 +5598,12 @@ fn cmd_vault_init() { fn cmd_vault_set(key: &str) { use zeroize::Zeroizing; - let home = openfang_home(); + let home = omtae_home(); let vault_path = home.join("vault.enc"); - let mut vault = openfang_extensions::vault::CredentialVault::new(vault_path); + let mut vault = omtae_extensions::vault::CredentialVault::new(vault_path); if !vault.exists() { - ui::error("Vault not initialized. Run: openfang vault init"); + ui::error("Vault not initialized. Run: omtae vault init"); std::process::exit(1); } @@ -5628,12 +5628,12 @@ fn cmd_vault_set(key: &str) { } fn cmd_vault_list() { - let home = openfang_home(); + let home = omtae_home(); let vault_path = home.join("vault.enc"); - let mut vault = openfang_extensions::vault::CredentialVault::new(vault_path); + let mut vault = omtae_extensions::vault::CredentialVault::new(vault_path); if !vault.exists() { - println!("Vault not initialized. Run: openfang vault init"); + println!("Vault not initialized. Run: omtae vault init"); return; } @@ -5654,9 +5654,9 @@ fn cmd_vault_list() { } fn cmd_vault_remove(key: &str) { - let home = openfang_home(); + let home = omtae_home(); let vault_path = home.join("vault.enc"); - let mut vault = openfang_extensions::vault::CredentialVault::new(vault_path); + let mut vault = omtae_extensions::vault::CredentialVault::new(vault_path); if !vault.exists() { ui::error("Vault not initialized."); @@ -5678,17 +5678,17 @@ fn cmd_vault_remove(key: &str) { } // --------------------------------------------------------------------------- -// Scaffold commands (openfang new skill/integration) +// Scaffold commands (omtae new skill/integration) // --------------------------------------------------------------------------- fn cmd_scaffold(kind: ScaffoldKind) { let cwd = std::env::current_dir().unwrap_or_default(); let result = match kind { ScaffoldKind::Skill => { - openfang_extensions::installer::scaffold_skill(&cwd.join("my-skill")) + omtae_extensions::installer::scaffold_skill(&cwd.join("my-skill")) } ScaffoldKind::Integration => { - openfang_extensions::installer::scaffold_integration(&cwd.join("my-integration")) + omtae_extensions::installer::scaffold_integration(&cwd.join("my-integration")) } }; match result { @@ -5743,7 +5743,7 @@ fn cmd_models_list(provider_filter: Option<&str>, json: bool) { } } else { // Standalone: use ModelCatalog directly - let catalog = openfang_runtime::model_catalog::ModelCatalog::new(); + let catalog = omtae_runtime::model_catalog::ModelCatalog::new(); let models = catalog.list_models(); if json { let arr: Vec = models @@ -5808,7 +5808,7 @@ fn cmd_models_aliases(json: bool) { ); } } else { - let catalog = openfang_runtime::model_catalog::ModelCatalog::new(); + let catalog = omtae_runtime::model_catalog::ModelCatalog::new(); let aliases = catalog.list_aliases(); if json { let obj: serde_json::Map = aliases @@ -5859,7 +5859,7 @@ fn cmd_models_providers(json: bool) { ); } } else { - let catalog = openfang_runtime::model_catalog::ModelCatalog::new(); + let catalog = omtae_runtime::model_catalog::ModelCatalog::new(); let providers = catalog.list_providers(); if json { let arr: Vec = providers @@ -5919,7 +5919,7 @@ fn cmd_models_set(model: Option) { /// Interactive model picker — shows numbered list, accepts number or model ID. fn pick_model() -> String { - let catalog = openfang_runtime::model_catalog::ModelCatalog::new(); + let catalog = omtae_runtime::model_catalog::ModelCatalog::new(); let models = catalog.list_models(); if models.is_empty() { @@ -5930,7 +5930,7 @@ fn pick_model() -> String { // Group by provider for display let mut by_provider: std::collections::BTreeMap< String, - Vec<&openfang_types::model_catalog::ModelCatalogEntry>, + Vec<&omtae_types::model_catalog::ModelCatalogEntry>, > = std::collections::BTreeMap::new(); for m in models { by_provider.entry(m.provider.clone()).or_default().push(m); @@ -6207,7 +6207,7 @@ fn cmd_sessions(agent: Option<&str>, json: bool) { } fn cmd_logs(lines: usize, follow: bool) { - let log_path = cli_openfang_home().join("tui.log"); + let log_path = cli_omtae_home().join("tui.log"); if !log_path.exists() { ui::error_with_fix( @@ -6285,7 +6285,7 @@ fn cmd_health(json: bool) { std::process::exit(1); } ui::error("Daemon is not running."); - ui::hint("Start it with: openfang start"); + ui::hint("Start it with: omtae start"); std::process::exit(1); } } @@ -6302,7 +6302,7 @@ fn cmd_auth_hash_password() { ui::error("Passwords do not match."); std::process::exit(1); } - let hash = openfang_api::session_auth::hash_password(&password); + let hash = omtae_api::session_auth::hash_password(&password); println!(); ui::success("Argon2id hash generated. Add this to your config.toml:"); println!(); @@ -6546,7 +6546,7 @@ fn cmd_devices_pair() { ui::section("Device Pairing"); ui::blank(); // Render a simple text-based QR representation - println!(" Scan this QR code with the OpenFang mobile app:"); + println!(" Scan this QR code with the OMTAE mobile app:"); ui::blank(); println!(" {qr}"); ui::blank(); @@ -6712,7 +6712,7 @@ fn cmd_system_info(json: bool) { ); return; } - ui::section("OpenFang System Info"); + ui::section("OMTAE System Info"); ui::blank(); ui::kv("Version", env!("CARGO_PKG_VERSION")); ui::kv("Status", body["status"].as_str().unwrap_or("?")); @@ -6739,11 +6739,11 @@ fn cmd_system_info(json: bool) { ); return; } - ui::section("OpenFang System Info"); + ui::section("OMTAE System Info"); ui::blank(); ui::kv("Version", env!("CARGO_PKG_VERSION")); ui::kv_warn("Daemon", "NOT RUNNING"); - ui::hint("Start with: openfang start"); + ui::hint("Start with: omtae start"); } } @@ -6755,22 +6755,22 @@ fn cmd_system_version(json: bool) { ); return; } - println!("openfang {}", env!("CARGO_PKG_VERSION")); + println!("omtae {}", env!("CARGO_PKG_VERSION")); } fn cmd_reset(confirm: bool) { - let openfang_dir = cli_openfang_home(); + let omtae_dir = cli_omtae_home(); - if !openfang_dir.exists() { + if !omtae_dir.exists() { println!( "Nothing to reset — {} does not exist.", - openfang_dir.display() + omtae_dir.display() ); return; } if !confirm { - println!(" This will delete all data in {}", openfang_dir.display()); + println!(" This will delete all data in {}", omtae_dir.display()); println!(" Including: config, database, agent manifests, credentials."); println!(); let answer = prompt_input(" Are you sure? Type 'yes' to confirm: "); @@ -6780,10 +6780,10 @@ fn cmd_reset(confirm: bool) { } } - match std::fs::remove_dir_all(&openfang_dir) { - Ok(()) => ui::success(&format!("Removed {}", openfang_dir.display())), + match std::fs::remove_dir_all(&omtae_dir) { + Ok(()) => ui::success(&format!("Removed {}", omtae_dir.display())), Err(e) => { - ui::error(&format!("Failed to remove {}: {e}", openfang_dir.display())); + ui::error(&format!("Failed to remove {}: {e}", omtae_dir.display())); std::process::exit(1); } } @@ -6794,26 +6794,26 @@ fn cmd_reset(confirm: bool) { // --------------------------------------------------------------------------- fn cmd_uninstall(confirm: bool, keep_config: bool) { - let openfang_dir = cli_openfang_home(); + let omtae_dir = cli_omtae_home(); let exe_path = std::env::current_exe().ok(); // Step 1: Show what will be removed println!(); println!( " {}", - "This will completely uninstall OpenFang from your system." + "This will completely uninstall OMTAE from your system." .bold() .red() ); println!(); - if openfang_dir.exists() { + if omtae_dir.exists() { if keep_config { println!( " • Remove data in {} (keeping config files)", - openfang_dir.display() + omtae_dir.display() ); } else { - println!(" • Remove {}", openfang_dir.display()); + println!(" • Remove {}", omtae_dir.display()); } } if let Some(ref exe) = exe_path { @@ -6825,9 +6825,9 @@ fn cmd_uninstall(confirm: bool, keep_config: bool) { .join(".cargo") .join("bin") .join(if cfg!(windows) { - "openfang.exe" + "omtae.exe" } else { - "openfang" + "omtae" }); if cargo_bin.exists() && exe_path.as_ref().is_none_or(|e| *e != cargo_bin) { println!(" • Remove cargo binary: {}", cargo_bin.display()); @@ -6854,9 +6854,9 @@ fn cmd_uninstall(confirm: bool, keep_config: bool) { std::thread::sleep(std::time::Duration::from_secs(1)); // Force kill if still alive if find_daemon().is_some() { - if let Some(info) = read_daemon_info(&openfang_dir) { + if let Some(info) = read_daemon_info(&omtae_dir) { force_kill_pid(info.pid); - let _ = std::fs::remove_file(openfang_dir.join("daemon.json")); + let _ = std::fs::remove_file(omtae_dir.join("daemon.json")); } } } @@ -6872,15 +6872,15 @@ fn cmd_uninstall(confirm: bool, keep_config: bool) { } } - // Step 6: Remove ~/.openfang/ data - if openfang_dir.exists() { + // Step 6: Remove ~/.omtae/ data + if omtae_dir.exists() { if keep_config { - remove_dir_except_config(&openfang_dir); + remove_dir_except_config(&omtae_dir); ui::success("Removed data (kept config files)"); } else { - match std::fs::remove_dir_all(&openfang_dir) { - Ok(()) => ui::success(&format!("Removed {}", openfang_dir.display())), - Err(e) => ui::error(&format!("Failed to remove {}: {e}", openfang_dir.display())), + match std::fs::remove_dir_all(&omtae_dir) { + Ok(()) => ui::success(&format!("Removed {}", omtae_dir.display())), + Err(e) => ui::error(&format!("Failed to remove {}: {e}", omtae_dir.display())), } } } @@ -6899,7 +6899,7 @@ fn cmd_uninstall(confirm: bool, keep_config: bool) { } println!(); - ui::success("OpenFang has been uninstalled. Goodbye!"); + ui::success("OMTAE has been uninstalled. Goodbye!"); } /// Remove auto-start / launch-agent / systemd entries. @@ -6913,7 +6913,7 @@ fn remove_autostart_entries(home: &std::path::Path) { "delete", r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run", "/v", - "OpenFang", + "OMTAE", "/f", ]) .output(); @@ -6927,7 +6927,7 @@ fn remove_autostart_entries(home: &std::path::Path) { #[cfg(target_os = "macos")] { - let plist = home.join("Library/LaunchAgents/ai.openfang.desktop.plist"); + let plist = home.join("Library/LaunchAgents/ai.omtae.desktop.plist"); if plist.exists() { // Unload first let _ = std::process::Command::new("launchctl") @@ -6942,7 +6942,7 @@ fn remove_autostart_entries(home: &std::path::Path) { #[cfg(target_os = "linux")] { - let desktop_file = home.join(".config/autostart/OpenFang.desktop"); + let desktop_file = home.join(".config/autostart/OMTAE.desktop"); if desktop_file.exists() { match std::fs::remove_file(&desktop_file) { Ok(()) => ui::success("Removed Linux autostart entry"), @@ -6951,10 +6951,10 @@ fn remove_autostart_entries(home: &std::path::Path) { } // Also check for systemd user service - let service_file = home.join(".config/systemd/user/openfang.service"); + let service_file = home.join(".config/systemd/user/omtae.service"); if service_file.exists() { let _ = std::process::Command::new("systemctl") - .args(["--user", "disable", "--now", "openfang.service"]) + .args(["--user", "disable", "--now", "omtae.service"]) .output(); match std::fs::remove_file(&service_file) { Ok(()) => { @@ -6969,9 +6969,9 @@ fn remove_autostart_entries(home: &std::path::Path) { } } -/// Remove lines from shell config files that add openfang to PATH. +/// Remove lines from shell config files that add omtae to PATH. #[allow(unused_variables)] -fn clean_path_entries(home: &std::path::Path, openfang_dir: &str) { +fn clean_path_entries(home: &std::path::Path, omtae_dir: &str) { #[cfg(not(windows))] { let shell_files = [ @@ -6991,7 +6991,7 @@ fn clean_path_entries(home: &std::path::Path, openfang_dir: &str) { }; let filtered: Vec<&str> = content .lines() - .filter(|line| !is_openfang_path_line(line, openfang_dir)) + .filter(|line| !is_omtae_path_line(line, omtae_dir)) .collect(); if filtered.len() < content.lines().count() { let new_content = filtered.join("\n"); @@ -7010,7 +7010,7 @@ fn clean_path_entries(home: &std::path::Path, openfang_dir: &str) { #[cfg(windows)] { - // Read User PATH via PowerShell, filter out openfang entries, write back + // Read User PATH via PowerShell, filter out omtae entries, write back let output = std::process::Command::new("powershell") .args([ "-NoProfile", @@ -7023,12 +7023,12 @@ fn clean_path_entries(home: &std::path::Path, openfang_dir: &str) { let current = String::from_utf8_lossy(&out.stdout); let current = current.trim(); if !current.is_empty() { - let dir_lower = openfang_dir.to_lowercase(); + let dir_lower = omtae_dir.to_lowercase(); let filtered: Vec<&str> = current .split(';') .filter(|entry| { let e = entry.trim().to_lowercase(); - !e.is_empty() && !e.contains("openfang") && !e.contains(&dir_lower) + !e.is_empty() && !e.contains("omtae") && !e.contains(&dir_lower) }) .collect(); if filtered.len() < current.split(';').count() { @@ -7050,13 +7050,13 @@ fn clean_path_entries(home: &std::path::Path, openfang_dir: &str) { } } -/// Returns true if a shell config line is an openfang PATH export. -/// Must match BOTH an openfang reference AND a PATH-setting pattern. +/// Returns true if a shell config line is an omtae PATH export. +/// Must match BOTH an omtae reference AND a PATH-setting pattern. #[cfg(any(not(windows), test))] -fn is_openfang_path_line(line: &str, openfang_dir: &str) -> bool { +fn is_omtae_path_line(line: &str, omtae_dir: &str) -> bool { let lower = line.to_lowercase(); - let has_openfang = lower.contains("openfang") || lower.contains(&openfang_dir.to_lowercase()); - if !has_openfang { + let has_omtae = lower.contains("omtae") || lower.contains(&omtae_dir.to_lowercase()); + if !has_omtae { return false; } // Match common PATH-setting patterns @@ -7067,10 +7067,10 @@ fn is_openfang_path_line(line: &str, openfang_dir: &str) -> bool { || lower.contains("fish_add_path") } -/// Remove everything in ~/.openfang/ except config files. -fn remove_dir_except_config(openfang_dir: &std::path::Path) { +/// Remove everything in ~/.omtae/ except config files. +fn remove_dir_except_config(omtae_dir: &std::path::Path) { let keep = ["config.toml", ".env", "secrets.env"]; - let Ok(entries) = std::fs::read_dir(openfang_dir) else { + let Ok(entries) = std::fs::read_dir(omtae_dir) else { return; }; for entry in entries.flatten() { @@ -7143,8 +7143,8 @@ mod tests { #[test] fn test_doctor_skill_registry_loads_bundled() { - let skills_dir = std::env::temp_dir().join("openfang-doctor-test-skills"); - let mut skill_reg = openfang_skills::registry::SkillRegistry::new(skills_dir); + let skills_dir = std::env::temp_dir().join("omtae-doctor-test-skills"); + let mut skill_reg = omtae_skills::registry::SkillRegistry::new(skills_dir); let count = skill_reg.load_bundled(); assert!(count > 0, "Should load bundled skills"); assert_eq!(skill_reg.count(), count); @@ -7152,9 +7152,9 @@ mod tests { #[test] fn test_doctor_extension_registry_loads_bundled() { - let tmp = std::env::temp_dir().join("openfang-doctor-test-ext"); + let tmp = std::env::temp_dir().join("omtae-doctor-test-ext"); let _ = std::fs::create_dir_all(&tmp); - let mut ext_reg = openfang_extensions::registry::IntegrationRegistry::new(&tmp); + let mut ext_reg = omtae_extensions::registry::IntegrationRegistry::new(&tmp); let count = ext_reg.load_bundled(); assert!(count > 0, "Should load bundled integration templates"); assert_eq!(ext_reg.template_count(), count); @@ -7163,9 +7163,9 @@ mod tests { #[test] fn test_doctor_config_deser_default() { // Default KernelConfig should serialize/deserialize round-trip - let config = openfang_types::config::KernelConfig::default(); + let config = omtae_types::config::KernelConfig::default(); let toml_str = toml::to_string_pretty(&config).unwrap(); - let parsed: openfang_types::config::KernelConfig = toml::from_str(&toml_str).unwrap(); + let parsed: omtae_types::config::KernelConfig = toml::from_str(&toml_str).unwrap(); assert_eq!(parsed.api_listen, config.api_listen); } @@ -7180,7 +7180,7 @@ provider = "groq" model = "llama-3.3-70b-versatile" api_key_env = "GROQ_API_KEY" "#; - let config: openfang_types::config::KernelConfig = toml::from_str(config_toml).unwrap(); + let config: omtae_types::config::KernelConfig = toml::from_str(config_toml).unwrap(); assert_eq!(config.include.len(), 2); assert_eq!(config.include[0], "providers.toml"); assert_eq!(config.include[1], "agents.toml"); @@ -7201,10 +7201,10 @@ provider = "groq" model = "llama-3.3-70b-versatile" api_key_env = "GROQ_API_KEY" "#; - let config: openfang_types::config::KernelConfig = toml::from_str(config_toml).unwrap(); + let config: omtae_types::config::KernelConfig = toml::from_str(config_toml).unwrap(); assert_eq!( config.exec_policy.mode, - openfang_types::config::ExecSecurityMode::Allowlist + omtae_types::config::ExecSecurityMode::Allowlist ); assert_eq!(config.exec_policy.safe_bins.len(), 3); assert_eq!(config.exec_policy.timeout_secs, 30); @@ -7229,11 +7229,11 @@ type = "stdio" command = "npx" args = ["-y", "@modelcontextprotocol/server-github"] "#; - let config: openfang_types::config::KernelConfig = toml::from_str(config_toml).unwrap(); + let config: omtae_types::config::KernelConfig = toml::from_str(config_toml).unwrap(); assert_eq!(config.mcp_servers.len(), 1); assert_eq!(config.mcp_servers[0].name, "github"); match &config.mcp_servers[0].transport { - openfang_types::config::McpTransportEntry::Stdio { command, args } => { + omtae_types::config::McpTransportEntry::Stdio { command, args } => { assert_eq!(command, "npx"); assert_eq!(args.len(), 2); } @@ -7244,14 +7244,14 @@ args = ["-y", "@modelcontextprotocol/server-github"] #[test] fn test_doctor_skill_injection_scan_clean() { let clean_content = "This is a normal skill prompt with helpful instructions."; - let warnings = openfang_skills::verify::SkillVerifier::scan_prompt_content(clean_content); + let warnings = omtae_skills::verify::SkillVerifier::scan_prompt_content(clean_content); assert!(warnings.is_empty(), "Clean content should have no warnings"); } #[test] fn test_doctor_hook_event_variants() { // Verify all 4 hook event types are constructable - use openfang_types::agent::HookEvent; + use omtae_types::agent::HookEvent; let events = [ HookEvent::BeforeToolCall, HookEvent::AfterToolCall, @@ -7440,39 +7440,39 @@ enabled = true #[test] fn test_uninstall_path_line_filter() { - use super::is_openfang_path_line; - let dir = "/home/user/.openfang/bin"; + use super::is_omtae_path_line; + let dir = "/home/user/.omtae/bin"; - // Should match: openfang PATH exports - assert!(is_openfang_path_line( - r#"export PATH="$HOME/.openfang/bin:$PATH""#, + // Should match: omtae PATH exports + assert!(is_omtae_path_line( + r#"export PATH="$HOME/.omtae/bin:$PATH""#, dir )); - assert!(is_openfang_path_line( - r#"export PATH="/home/user/.openfang/bin:$PATH""#, + assert!(is_omtae_path_line( + r#"export PATH="/home/user/.omtae/bin:$PATH""#, dir )); - assert!(is_openfang_path_line( - "set -gx PATH $HOME/.openfang/bin $PATH", + assert!(is_omtae_path_line( + "set -gx PATH $HOME/.omtae/bin $PATH", dir )); - assert!(is_openfang_path_line( - "fish_add_path $HOME/.openfang/bin", + assert!(is_omtae_path_line( + "fish_add_path $HOME/.omtae/bin", dir )); // Should NOT match: unrelated PATH exports - assert!(!is_openfang_path_line( + assert!(!is_omtae_path_line( r#"export PATH="$HOME/.cargo/bin:$PATH""#, dir )); - assert!(!is_openfang_path_line( + assert!(!is_omtae_path_line( r#"export PATH="/usr/local/bin:$PATH""#, dir )); - // Should NOT match: openfang lines that aren't PATH-related - assert!(!is_openfang_path_line("# openfang config", dir)); - assert!(!is_openfang_path_line("alias of=openfang", dir)); + // Should NOT match: omtae lines that aren't PATH-related + assert!(!is_omtae_path_line("# omtae config", dir)); + assert!(!is_omtae_path_line("alias of=omtae", dir)); } } diff --git a/crates/openfang-cli/src/mcp.rs b/crates/openfang-cli/src/mcp.rs index f0e226c257..4b5fe12788 100644 --- a/crates/openfang-cli/src/mcp.rs +++ b/crates/openfang-cli/src/mcp.rs @@ -1,12 +1,12 @@ -//! MCP (Model Context Protocol) server for OpenFang. +//! MCP (Model Context Protocol) server for OMTAE. //! //! Exposes running agents as MCP tools over JSON-RPC 2.0 stdio. -//! Each agent becomes a callable tool named `openfang_agent_{name}`. +//! Each agent becomes a callable tool named `omtae_agent_{name}`. //! //! Protocol: Content-Length framing over stdin/stdout. //! Connects to running daemon via HTTP, falls back to in-process kernel. -use openfang_kernel::OpenFangKernel; +use omtae_kernel::OMTAEKernel; use serde_json::{json, Value}; use std::io::{self, BufRead, Write}; @@ -17,7 +17,7 @@ enum McpBackend { client: reqwest::blocking::Client, }, InProcess { - kernel: Box, + kernel: Box, rt: tokio::runtime::Runtime, }, } @@ -80,7 +80,7 @@ impl McpBackend { } } McpBackend::InProcess { kernel, rt } => { - let aid: openfang_types::agent::AgentId = + let aid: omtae_types::agent::AgentId = agent_id.parse().map_err(|_| "Invalid agent ID")?; let result = rt .block_on(kernel.send_message(aid, message)) @@ -90,9 +90,9 @@ impl McpBackend { } } - /// Find agent ID by tool name (strip `openfang_agent_` prefix, match by name). + /// Find agent ID by tool name (strip `omtae_agent_` prefix, match by name). fn resolve_tool_agent(&self, tool_name: &str) -> Option { - let agent_name = tool_name.strip_prefix("openfang_agent_")?.replace('_', "-"); + let agent_name = tool_name.strip_prefix("omtae_agent_")?.replace('_', "-"); let agents = self.list_agents(); // Try exact match first (with underscores replaced by hyphens) for (id, name, _) in &agents { @@ -101,7 +101,7 @@ impl McpBackend { } } // Try with underscores - let agent_name_underscore = tool_name.strip_prefix("openfang_agent_")?; + let agent_name_underscore = tool_name.strip_prefix("omtae_agent_")?; for (id, name, _) in &agents { if name.replace('-', "_").to_lowercase() == agent_name_underscore.to_lowercase() { return Some(id.clone()); @@ -145,7 +145,7 @@ fn create_backend(config: Option) -> McpBackend { } // Fall back to in-process kernel - let kernel = match OpenFangKernel::boot(config.as_deref()) { + let kernel = match OMTAEKernel::boot(config.as_deref()) { Ok(k) => k, Err(e) => { eprintln!("Failed to boot kernel for MCP: {e}"); @@ -242,7 +242,7 @@ fn handle_message(backend: &McpBackend, msg: &Value) -> Option { "tools": {} }, "serverInfo": { - "name": "openfang", + "name": "omtae", "version": env!("CARGO_PKG_VERSION") } }); @@ -256,9 +256,9 @@ fn handle_message(backend: &McpBackend, msg: &Value) -> Option { let tools: Vec = agents .iter() .map(|(_, name, description)| { - let tool_name = format!("openfang_agent_{}", name.replace('-', "_")); + let tool_name = format!("omtae_agent_{}", name.replace('-', "_")); let desc = if description.is_empty() { - format!("Send a message to OpenFang agent '{name}'") + format!("Send a message to OMTAE agent '{name}'") } else { description.clone() }; @@ -378,7 +378,7 @@ mod tests { let resp = handle_message(&backend, &msg).unwrap(); assert_eq!(resp["id"], 1); assert_eq!(resp["result"]["protocolVersion"], "2024-11-05"); - assert_eq!(resp["result"]["serverInfo"]["name"], "openfang"); + assert_eq!(resp["result"]["serverInfo"]["name"], "omtae"); } #[test] diff --git a/crates/openfang-cli/src/templates.rs b/crates/openfang-cli/src/templates.rs index df4daf4fa3..96797d1dd6 100644 --- a/crates/openfang-cli/src/templates.rs +++ b/crates/openfang-cli/src/templates.rs @@ -14,7 +14,7 @@ pub struct AgentTemplate { /// Discover template directories. Checks: /// 1. The repo `agents/` dir (for dev builds) -/// 2. `~/.openfang/agents/` (installed templates) +/// 2. `~/.omtae/agents/` (installed templates) /// 3. `OPENFANG_AGENTS_DIR` env var pub fn discover_template_dirs() -> Vec { let mut dirs = Vec::new(); @@ -39,9 +39,9 @@ pub fn discover_template_dirs() -> Vec { let of_home = if let Ok(h) = std::env::var("OPENFANG_HOME") { PathBuf::from(h) } else if let Some(home) = dirs::home_dir() { - home.join(".openfang") + home.join(".omtae") } else { - std::env::temp_dir().join(".openfang") + std::env::temp_dir().join(".omtae") }; { let agents = of_home.join("agents"); diff --git a/crates/openfang-cli/src/tui/chat_runner.rs b/crates/openfang-cli/src/tui/chat_runner.rs index f10a5942e3..83afa2ba83 100644 --- a/crates/openfang-cli/src/tui/chat_runner.rs +++ b/crates/openfang-cli/src/tui/chat_runner.rs @@ -1,4 +1,4 @@ -//! Standalone chat TUI for `openfang chat`. +//! Standalone chat TUI for `omtae chat`. //! //! Launches a focused ratatui chat screen — same beautiful rendering as the //! full TUI's Chat tab, but without the 17-tab chrome. Reuses 100% of @@ -7,9 +7,9 @@ use super::event::{self, AppEvent}; use super::screens::chat::{self, ChatAction, ChatState, Role}; use super::theme; -use openfang_kernel::OpenFangKernel; -use openfang_runtime::llm_driver::StreamEvent; -use openfang_types::agent::AgentId; +use omtae_kernel::OMTAEKernel; +use omtae_runtime::llm_driver::StreamEvent; +use omtae_types::agent::AgentId; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::Style; use ratatui::text::{Line, Span}; @@ -22,7 +22,7 @@ use std::time::Duration; enum Backend { Daemon { base_url: String }, - InProcess { kernel: Arc }, + InProcess { kernel: Arc }, None, } @@ -162,7 +162,7 @@ impl StandaloneChat { fn handle_stream_done( &mut self, - result: Result, + result: Result, ) { self.chat.finalize_stream(); match result { @@ -190,7 +190,7 @@ impl StandaloneChat { // ── Kernel lifecycle ───────────────────────────────────────────────────── - fn handle_kernel_ready(&mut self, kernel: Arc) { + fn handle_kernel_ready(&mut self, kernel: Arc) { self.booting = false; self.boot_error = None; self.backend = Backend::InProcess { kernel }; @@ -613,7 +613,7 @@ impl StandaloneChat { } None => { self.boot_error = - Some("No agent templates found. Run `openfang init`.".to_string()); + Some("No agent templates found. Run `omtae init`.".to_string()); } } } @@ -650,7 +650,7 @@ impl StandaloneChat { match template { Some(t) => { - let manifest: openfang_types::agent::AgentManifest = + let manifest: omtae_types::agent::AgentManifest = match toml::from_str(&t.content) { Ok(m) => m, Err(e) => { @@ -671,7 +671,7 @@ impl StandaloneChat { } None => { self.chat.status_msg = - Some("No agent templates found. Run `openfang init`.".to_string()); + Some("No agent templates found. Run `omtae init`.".to_string()); } } } diff --git a/crates/openfang-cli/src/tui/event.rs b/crates/openfang-cli/src/tui/event.rs index c0befe0667..61602712ed 100644 --- a/crates/openfang-cli/src/tui/event.rs +++ b/crates/openfang-cli/src/tui/event.rs @@ -1,9 +1,9 @@ //! Event system: crossterm polling, tick timer, streaming bridges. -use openfang_kernel::OpenFangKernel; -use openfang_runtime::agent_loop::AgentLoopResult; -use openfang_runtime::llm_driver::StreamEvent; -use openfang_types::agent::AgentId; +use omtae_kernel::OMTAEKernel; +use omtae_runtime::agent_loop::AgentLoopResult; +use omtae_runtime::llm_driver::StreamEvent; +use omtae_types::agent::AgentId; use ratatui::crossterm::event::{self, Event as CtEvent, KeyEvent, KeyEventKind}; use std::sync::{mpsc, Arc}; use std::time::Duration; @@ -33,7 +33,7 @@ use super::screens::{ #[derive(Clone)] pub enum BackendRef { Daemon(String), - InProcess(Arc), + InProcess(Arc), } // ── AppEvent ──────────────────────────────────────────────────────────────── @@ -49,7 +49,7 @@ pub enum AppEvent { /// The streaming agent loop finished. StreamDone(Result), /// The kernel finished booting in the background. - KernelReady(Arc), + KernelReady(Arc), /// The kernel failed to boot. KernelError(String), /// An agent was successfully spawned (daemon mode). @@ -275,7 +275,7 @@ pub fn spawn_kernel_boot(config: Option, tx: mpsc::Sender { let k = Arc::new(k); k.set_self_handle(); @@ -290,7 +290,7 @@ pub fn spawn_kernel_boot(config: Option, tx: mpsc::Sender, + kernel: Arc, agent_id: AgentId, message: String, tx: mpsc::Sender, @@ -422,8 +422,8 @@ pub fn spawn_daemon_stream( // token display, but do NOT terminate — the agent // loop may continue with tool results. let _ = tx.send(AppEvent::Stream(StreamEvent::ContentComplete { - stop_reason: openfang_types::message::StopReason::EndTurn, - usage: openfang_types::message::TokenUsage { + stop_reason: omtae_types::message::StopReason::EndTurn, + usage: omtae_types::message::TokenUsage { input_tokens: total_input_tokens, output_tokens: total_output_tokens, }, @@ -436,7 +436,7 @@ pub fn spawn_daemon_stream( // Connection closed — agent loop is truly done. let _ = tx.send(AppEvent::StreamDone(Ok(AgentLoopResult { response: String::new(), - total_usage: openfang_types::message::TokenUsage { + total_usage: omtae_types::message::TokenUsage { input_tokens: total_input_tokens, output_tokens: total_output_tokens, }, @@ -472,7 +472,7 @@ fn daemon_fallback( let output_tokens = body["output_tokens"].as_u64().unwrap_or(0); Ok(AgentLoopResult { response: response.to_string(), - total_usage: openfang_types::message::TokenUsage { + total_usage: omtae_types::message::TokenUsage { input_tokens, output_tokens, }, @@ -1040,7 +1040,7 @@ pub fn spawn_fetch_agent_skills(backend: BackendRef, agent_id: String, tx: mpsc: } BackendRef::InProcess(kernel) => { if let Ok(uuid) = uuid::Uuid::parse_str(&agent_id) { - let aid = openfang_types::agent::AgentId(uuid); + let aid = omtae_types::agent::AgentId(uuid); let assigned = kernel .registry .get(aid) @@ -1106,7 +1106,7 @@ pub fn spawn_fetch_agent_mcp_servers( } BackendRef::InProcess(kernel) => { if let Ok(uuid) = uuid::Uuid::parse_str(&agent_id) { - let aid = openfang_types::agent::AgentId(uuid); + let aid = omtae_types::agent::AgentId(uuid); let assigned = kernel .registry .get(aid) @@ -1116,7 +1116,7 @@ pub fn spawn_fetch_agent_mcp_servers( if let Ok(mcp_tools) = kernel.mcp_tools.lock() { let mut seen = std::collections::HashSet::new(); for tool in mcp_tools.iter() { - if let Some(server) = openfang_runtime::mcp::extract_mcp_server(&tool.name) + if let Some(server) = omtae_runtime::mcp::extract_mcp_server(&tool.name) { if seen.insert(server.to_string()) { available.push(server.to_string()); @@ -1161,7 +1161,7 @@ pub fn spawn_update_agent_skills( } BackendRef::InProcess(kernel) => { if let Ok(uuid) = uuid::Uuid::parse_str(&agent_id) { - let aid = openfang_types::agent::AgentId(uuid); + let aid = omtae_types::agent::AgentId(uuid); match kernel.set_agent_skills(aid, skills) { Ok(()) => { let _ = tx.send(AppEvent::AgentSkillsUpdated(agent_id)); @@ -1205,7 +1205,7 @@ pub fn spawn_update_agent_mcp_servers( } BackendRef::InProcess(kernel) => { if let Ok(uuid) = uuid::Uuid::parse_str(&agent_id) { - let aid = openfang_types::agent::AgentId(uuid); + let aid = omtae_types::agent::AgentId(uuid); match kernel.set_agent_mcp_servers(aid, servers) { Ok(()) => { let _ = tx.send(AppEvent::AgentMcpServersUpdated(agent_id)); diff --git a/crates/openfang-cli/src/tui/mod.rs b/crates/openfang-cli/src/tui/mod.rs index eeef99c440..54677711cb 100644 --- a/crates/openfang-cli/src/tui/mod.rs +++ b/crates/openfang-cli/src/tui/mod.rs @@ -1,4 +1,4 @@ -//! Ratatui TUI for OpenFang interactive mode. +//! Ratatui TUI for OMTAE interactive mode. //! //! Two-level navigation: Phase::Boot (Welcome/Wizard) → Phase::Main with 16 tabs. @@ -8,10 +8,10 @@ pub mod screens; pub mod theme; use event::{AppEvent, BackendRef}; -use openfang_kernel::OpenFangKernel; -use openfang_runtime::llm_driver::StreamEvent; -use openfang_types::agent::AgentId; -use openfang_types::commands::{self, Surfaces}; +use omtae_kernel::OMTAEKernel; +use omtae_runtime::llm_driver::StreamEvent; +use omtae_types::agent::AgentId; +use omtae_types::commands::{self, Surfaces}; use screens::{ agents, audit, channels, chat, comms, dashboard, extensions, hands, logs, memory, peers, security, sessions, settings, skills, templates, triggers, usage, welcome, wizard, workflows, @@ -116,7 +116,7 @@ impl Tab { enum Backend { Daemon { base_url: String }, - InProcess { kernel: Arc }, + InProcess { kernel: Arc }, None, } @@ -1197,7 +1197,7 @@ impl App { fn handle_stream_done( &mut self, - result: Result, + result: Result, ) { self.chat.finalize_stream(); match result { @@ -1225,7 +1225,7 @@ impl App { // ─── Kernel lifecycle ──────────────────────────────────────────────────── - fn handle_kernel_ready(&mut self, kernel: Arc) { + fn handle_kernel_ready(&mut self, kernel: Arc) { self.kernel_booting = false; self.backend = Backend::InProcess { kernel }; self.agents.reset(); @@ -1840,7 +1840,7 @@ impl App { event::spawn_daemon_agent(base_url.clone(), toml_content, self.event_tx.clone()); } Backend::InProcess { kernel } => { - let manifest: openfang_types::agent::AgentManifest = + let manifest: omtae_types::agent::AgentManifest = match toml::from_str(&toml_content) { Ok(m) => m, Err(e) => { diff --git a/crates/openfang-cli/src/tui/screens/agents.rs b/crates/openfang-cli/src/tui/screens/agents.rs index fe1a92bdc0..98d6360d25 100644 --- a/crates/openfang-cli/src/tui/screens/agents.rs +++ b/crates/openfang-cli/src/tui/screens/agents.rs @@ -109,7 +109,7 @@ pub struct DaemonAgent { #[derive(Clone)] pub struct InProcessAgent { - pub id: openfang_types::agent::AgentId, + pub id: omtae_types::agent::AgentId, pub name: String, pub state: String, pub provider: String, @@ -231,7 +231,7 @@ impl AgentSelectState { } /// Load in-process agents from the kernel. - pub fn load_inprocess_agents(&mut self, kernel: &openfang_kernel::OpenFangKernel) { + pub fn load_inprocess_agents(&mut self, kernel: &omtae_kernel::OMTAEKernel) { self.inprocess_agents.clear(); for entry in kernel.registry.list() { self.inprocess_agents.push(InProcessAgent { @@ -1496,7 +1496,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/audit.rs b/crates/openfang-cli/src/tui/screens/audit.rs index 2a0430fa74..98746cae2e 100644 --- a/crates/openfang-cli/src/tui/screens/audit.rs +++ b/crates/openfang-cli/src/tui/screens/audit.rs @@ -339,7 +339,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/chat.rs b/crates/openfang-cli/src/tui/screens/chat.rs index 681d8b385e..e81883a7f3 100644 --- a/crates/openfang-cli/src/tui/screens/chat.rs +++ b/crates/openfang-cli/src/tui/screens/chat.rs @@ -562,7 +562,7 @@ fn draw_model_picker(f: &mut Frame, area: Rect, state: &ChatState) { &entry.display_name }; let name_display = if name.len() > max_name && max_name > 1 { - let truncated = openfang_types::truncate_str(name, max_name.saturating_sub(1)); + let truncated = omtae_types::truncate_str(name, max_name.saturating_sub(1)); format!("{truncated}\u{2026}") } else { name.to_string() @@ -888,7 +888,7 @@ fn truncate_line(s: &str, max_len: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max_len.saturating_sub(1)) + omtae_types::truncate_str(s, max_len.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/comms.rs b/crates/openfang-cli/src/tui/screens/comms.rs index 4ea61d0b57..6f6f14895f 100644 --- a/crates/openfang-cli/src/tui/screens/comms.rs +++ b/crates/openfang-cli/src/tui/screens/comms.rs @@ -746,7 +746,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/dashboard.rs b/crates/openfang-cli/src/tui/screens/dashboard.rs index a6b8b25e84..9c1cf986d5 100644 --- a/crates/openfang-cli/src/tui/screens/dashboard.rs +++ b/crates/openfang-cli/src/tui/screens/dashboard.rs @@ -275,7 +275,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/extensions.rs b/crates/openfang-cli/src/tui/screens/extensions.rs index 7ba69a6fba..397a4316e1 100644 --- a/crates/openfang-cli/src/tui/screens/extensions.rs +++ b/crates/openfang-cli/src/tui/screens/extensions.rs @@ -519,7 +519,7 @@ fn draw_health(f: &mut Frame, area: Rect, state: &mut ExtensionsState) { let error_display = if h.last_error.is_empty() { "\u{2014}".to_string() } else if h.last_error.len() > 30 { - format!("{}...", openfang_types::truncate_str(&h.last_error, 27)) + format!("{}...", omtae_types::truncate_str(&h.last_error, 27)) } else { h.last_error.clone() }; diff --git a/crates/openfang-cli/src/tui/screens/hands.rs b/crates/openfang-cli/src/tui/screens/hands.rs index 52c0aef2c9..90833f6173 100644 --- a/crates/openfang-cli/src/tui/screens/hands.rs +++ b/crates/openfang-cli/src/tui/screens/hands.rs @@ -429,5 +429,5 @@ fn draw_active(f: &mut Frame, area: Rect, state: &mut HandsState) { } fn truncate(s: &str, max: usize) -> &str { - openfang_types::truncate_str(s, max) + omtae_types::truncate_str(s, max) } diff --git a/crates/openfang-cli/src/tui/screens/init_wizard.rs b/crates/openfang-cli/src/tui/screens/init_wizard.rs index 53d9c557d6..54ada3f568 100644 --- a/crates/openfang-cli/src/tui/screens/init_wizard.rs +++ b/crates/openfang-cli/src/tui/screens/init_wizard.rs @@ -1,6 +1,6 @@ //! Standalone ratatui init wizard: 6-step onboarding flow. //! -//! Launched by `openfang init` (without `--quick`). Takes over the terminal, +//! Launched by `omtae init` (without `--quick`). Takes over the terminal, //! runs its own event loop, and returns an `InitResult`. use ratatui::crossterm::event::{self, Event as CtEvent, KeyCode, KeyEventKind}; @@ -13,8 +13,8 @@ use std::path::PathBuf; use std::time::{Duration, Instant}; use crate::tui::theme; -use openfang_runtime::model_catalog::ModelCatalog; -use openfang_types::model_catalog::ModelTier; +use omtae_runtime::model_catalog::ModelCatalog; +use omtae_types::model_catalog::ModelTier; // ── Provider metadata ────────────────────────────────────────────────────── @@ -240,7 +240,7 @@ const PROVIDERS: &[ProviderInfo] = &[ name: "vllm", display: "vLLM", env_var: "VLLM_API_KEY", - default_model: "local-model", + default_model: "Qwen2.5-Coder-32B-Instruct-AWQ", needs_key: false, hint: "local", }, @@ -275,7 +275,7 @@ mod tests { let minimax = PROVIDERS.iter().find(|provider| provider.name == "minimax"); assert!( minimax.is_some(), - "MiniMax should be selectable in openfang init" + "MiniMax should be selectable in omtae init" ); let minimax = minimax.unwrap(); assert_eq!(minimax.env_var, "MINIMAX_API_KEY"); @@ -368,8 +368,8 @@ struct State { migration_phase: MigrationPhase, migration_choice_list: ListState, openclaw_path: Option, - openclaw_scan: Option, - migration_report: Option, + openclaw_scan: Option, + migration_report: Option, migration_error: Option, migration_done_at: Option, migrated_provider: Option, @@ -465,7 +465,7 @@ impl State { let gemini_via_google = std::env::var("GOOGLE_API_KEY").is_ok(); for (i, p) in PROVIDERS.iter().enumerate() { let detected = if p.name == "claude-code" { - openfang_runtime::drivers::claude_code::claude_code_available() + omtae_runtime::drivers::claude_code::claude_code_available() } else { (!p.env_var.is_empty() && std::env::var(p.env_var).is_ok()) || (p.name == "gemini" && gemini_via_google) @@ -476,7 +476,7 @@ impl State { } for (i, p) in PROVIDERS.iter().enumerate() { let detected = if p.name == "claude-code" { - openfang_runtime::drivers::claude_code::claude_code_available() + omtae_runtime::drivers::claude_code::claude_code_available() } else { (!p.env_var.is_empty() && std::env::var(p.env_var).is_ok()) || (p.name == "gemini" && gemini_via_google) @@ -521,7 +521,7 @@ impl State { fn is_provider_detected(&self, prov_idx: usize) -> bool { let p = &PROVIDERS[prov_idx]; if p.name == "claude-code" { - return openfang_runtime::drivers::claude_code::claude_code_available(); + return omtae_runtime::drivers::claude_code::claude_code_available(); } (!p.env_var.is_empty() && std::env::var(p.env_var).is_ok()) || (p.name == "gemini" && std::env::var("GOOGLE_API_KEY").is_ok()) @@ -675,7 +675,7 @@ pub fn run() -> InitResult { let (test_tx, test_rx) = std::sync::mpsc::channel::(); let (migrate_tx, migrate_rx) = - std::sync::mpsc::channel::>(); + std::sync::mpsc::channel::>(); let (copilot_tx, copilot_rx) = std::sync::mpsc::channel::>(); let result = loop { @@ -748,13 +748,13 @@ pub fn run() -> InitResult { // ── Migration detection (resolves in 1 frame) ── if state.step == Step::Migration && state.migration_phase == MigrationPhase::Detecting { - match openfang_migrate::openclaw::detect_openclaw_home() { + match omtae_migrate::openclaw::detect_openclaw_home() { None => { // No OpenClaw found — skip migration entirely state.advance_to_provider(); } Some(path) => { - let scan = openfang_migrate::openclaw::scan_openclaw_workspace(&path); + let scan = omtae_migrate::openclaw::scan_openclaw_workspace(&path); let has_content = scan.has_config || !scan.agents.is_empty() || !scan.channels.is_empty() @@ -865,7 +865,7 @@ pub fn run() -> InitResult { // Kick off background auth let copilot_tx = copilot_tx.clone(); std::thread::spawn(move || { - let openfang_dir = crate::cli_openfang_home(); + let omtae_dir = crate::cli_omtae_home(); let rt = match tokio::runtime::Runtime::new() { Ok(rt) => rt, Err(e) => { @@ -889,7 +889,7 @@ pub fn run() -> InitResult { }; // Step 1: request device code - use openfang_runtime::drivers::copilot; + use omtae_runtime::drivers::copilot; let device = match copilot::request_device_code(&http).await { Ok(d) => d, @@ -926,7 +926,7 @@ pub fn run() -> InitResult { }; // Save tokens - if let Err(e) = tokens.save(&openfang_dir) { + if let Err(e) = tokens.save(&omtae_dir) { let _ = copilot_tx.send(Err(e)); return; } @@ -997,7 +997,7 @@ pub fn run() -> InitResult { CopilotAuthStatus::WaitingForUser ) && !state.copilot_verification_uri.is_empty() => { - let _ = openfang_runtime::drivers::copilot::open_verification_url( + let _ = omtae_runtime::drivers::copilot::open_verification_url( &state.copilot_verification_uri, ); } @@ -1160,7 +1160,7 @@ pub fn run() -> InitResult { fn handle_migration_key( state: &mut State, code: KeyCode, - migrate_tx: &std::sync::mpsc::Sender>, + migrate_tx: &std::sync::mpsc::Sender>, ) { match state.migration_phase { MigrationPhase::Detecting => {} // auto-resolves, no keys @@ -1190,18 +1190,18 @@ fn handle_migration_key( } else { dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) - .join(".openfang") + .join(".omtae") }; let tx = migrate_tx.clone(); std::thread::spawn(move || { - let options = openfang_migrate::MigrateOptions { - source: openfang_migrate::MigrateSource::OpenClaw, + let options = omtae_migrate::MigrateOptions { + source: omtae_migrate::MigrateSource::OpenClaw, source_dir, target_dir, dry_run: false, }; let result = - openfang_migrate::run_migration(&options).map_err(|e| format!("{e}")); + omtae_migrate::run_migration(&options).map_err(|e| format!("{e}")); let _ = tx.send(result); }); } else { @@ -1310,20 +1310,20 @@ fn save_config(state: &mut State) { } }; - let openfang_dir = if let Ok(h) = std::env::var("OPENFANG_HOME") { + let omtae_dir = if let Ok(h) = std::env::var("OPENFANG_HOME") { PathBuf::from(h) } else { match dirs::home_dir() { - Some(h) => h.join(".openfang"), + Some(h) => h.join(".omtae"), None => { state.save_error = "Could not determine home directory".to_string(); return; } } }; - let _ = std::fs::create_dir_all(openfang_dir.join("agents")); - let _ = std::fs::create_dir_all(openfang_dir.join("data")); - crate::restrict_dir_permissions(&openfang_dir); + let _ = std::fs::create_dir_all(omtae_dir.join("agents")); + let _ = std::fs::create_dir_all(omtae_dir.join("data")); + crate::restrict_dir_permissions(&omtae_dir); let model = if state.model_input.is_empty() { p.default_model @@ -1349,7 +1349,7 @@ complex_threshold = 500 String::new() }; - let config_path = openfang_dir.join("config.toml"); + let config_path = omtae_dir.join("config.toml"); let api_key_line = if p.env_var.is_empty() { String::new() } else { @@ -1357,8 +1357,8 @@ complex_threshold = 500 }; let config = format!( - r#"# OpenFang Agent OS configuration -# See https://github.com/RightNow-AI/openfang for documentation + r#"# OMTAE Agent OS configuration +# See https://github.com/RightNow-AI/omtae for documentation api_listen = "127.0.0.1:4200" @@ -1397,15 +1397,15 @@ decay_rate = 0.05 } } -/// Check if the `openfang-desktop` binary exists next to the current exe. +/// Check if the `omtae-desktop` binary exists next to the current exe. fn find_desktop_binary() -> Option { let exe = std::env::current_exe().ok()?; let dir = exe.parent()?; #[cfg(windows)] - let name = "openfang-desktop.exe"; + let name = "omtae-desktop.exe"; #[cfg(not(windows))] - let name = "openfang-desktop"; + let name = "omtae-desktop"; let path = dir.join(name); if path.exists() { @@ -1446,10 +1446,10 @@ fn draw(f: &mut Frame, area: Rect, state: &mut State) { ]) .split(content); - // Header: "OpenFang Init Step X of 7" + // Header: "OMTAE Init Step X of 7" let header = Line::from(vec![ Span::styled( - "OpenFang", + "OMTAE", Style::default() .fg(theme::ACCENT) .add_modifier(Modifier::BOLD), @@ -1805,7 +1805,7 @@ fn draw_migration_done(f: &mut Frame, area: Rect, state: &State) { ])); } else if let Some(ref report) = state.migration_report { // Group imported items by kind - use openfang_migrate::report::ItemKind; + use omtae_migrate::report::ItemKind; let config_count = report .imported .iter() @@ -2174,7 +2174,7 @@ fn draw_api_key(f: &mut Frame, area: Rect, state: &mut State) { ); f.render_widget( Paragraph::new(Line::from(vec![Span::styled( - " Saved to ~/.openfang/.env", + " Saved to ~/.omtae/.env", theme::dim_style(), )])), chunks[3], @@ -2190,7 +2190,7 @@ fn draw_api_key(f: &mut Frame, area: Rect, state: &mut State) { ); f.render_widget( Paragraph::new(Line::from(vec![Span::styled( - " Saved to ~/.openfang/.env", + " Saved to ~/.omtae/.env", theme::dim_style(), )])), chunks[3], @@ -2402,7 +2402,7 @@ fn draw_routing_pick(f: &mut Frame, area: Rect, state: &mut State, tier: usize) .split('/') .next_back() .unwrap_or(&state.routing_models[t]); - let display = openfang_types::truncate_str(short, 14); + let display = omtae_types::truncate_str(short, 14); summary_spans.push(Span::styled( format!("{name}:{display}"), Style::default().fg(*c), @@ -2609,7 +2609,7 @@ fn draw_complete(f: &mut Frame, area: Rect, state: &mut State) { // ── Question ── f.render_widget( Paragraph::new(Line::from(vec![Span::styled( - " How do you want to use OpenFang?", + " How do you want to use OMTAE?", Style::default() .fg(theme::ACCENT) .add_modifier(Modifier::BOLD), diff --git a/crates/openfang-cli/src/tui/screens/logs.rs b/crates/openfang-cli/src/tui/screens/logs.rs index 64494c7a02..8971971f1f 100644 --- a/crates/openfang-cli/src/tui/screens/logs.rs +++ b/crates/openfang-cli/src/tui/screens/logs.rs @@ -399,7 +399,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/memory.rs b/crates/openfang-cli/src/tui/screens/memory.rs index dc9a2d06cf..bbb8336cb3 100644 --- a/crates/openfang-cli/src/tui/screens/memory.rs +++ b/crates/openfang-cli/src/tui/screens/memory.rs @@ -307,7 +307,7 @@ fn draw_agent_select(f: &mut Frame, area: Rect, state: &mut MemoryState) { .iter() .map(|a| { let id_short = if a.id.len() > 12 { - format!("{}\u{2026}", openfang_types::truncate_str(&a.id, 12)) + format!("{}\u{2026}", omtae_types::truncate_str(&a.id, 12)) } else { a.id.clone() }; @@ -395,7 +395,7 @@ fn draw_kv_browser(f: &mut Frame, area: Rect, state: &mut MemoryState) { .iter() .map(|kv| { let val_display = if kv.value.len() > 40 { - format!("{}\u{2026}", openfang_types::truncate_str(&kv.value, 39)) + format!("{}\u{2026}", omtae_types::truncate_str(&kv.value, 39)) } else { kv.value.clone() }; @@ -541,7 +541,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/peers.rs b/crates/openfang-cli/src/tui/screens/peers.rs index 09354415c7..9a0d4f8308 100644 --- a/crates/openfang-cli/src/tui/screens/peers.rs +++ b/crates/openfang-cli/src/tui/screens/peers.rs @@ -145,7 +145,7 @@ pub fn draw(f: &mut Frame, area: Rect, state: &mut PeersState) { .iter() .map(|p| { let id_short = if p.node_id.len() > 12 { - format!("{}\u{2026}", openfang_types::truncate_str(&p.node_id, 12)) + format!("{}\u{2026}", omtae_types::truncate_str(&p.node_id, 12)) } else { p.node_id.clone() }; @@ -206,7 +206,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/sessions.rs b/crates/openfang-cli/src/tui/screens/sessions.rs index 091a3f85da..a9fe0cd950 100644 --- a/crates/openfang-cli/src/tui/screens/sessions.rs +++ b/crates/openfang-cli/src/tui/screens/sessions.rs @@ -245,7 +245,7 @@ pub fn draw(f: &mut Frame, area: Rect, state: &mut SessionsState) { .map(|&idx| { let s = &state.sessions[idx]; let id_short = if s.id.len() > 12 { - format!("{}\u{2026}", openfang_types::truncate_str(&s.id, 12)) + format!("{}\u{2026}", omtae_types::truncate_str(&s.id, 12)) } else { s.id.clone() }; @@ -304,7 +304,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/settings.rs b/crates/openfang-cli/src/tui/screens/settings.rs index 167a204c3a..a3250a863f 100644 --- a/crates/openfang-cli/src/tui/screens/settings.rs +++ b/crates/openfang-cli/src/tui/screens/settings.rs @@ -594,7 +594,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/skills.rs b/crates/openfang-cli/src/tui/screens/skills.rs index c7fec0e1a5..210b287c40 100644 --- a/crates/openfang-cli/src/tui/screens/skills.rs +++ b/crates/openfang-cli/src/tui/screens/skills.rs @@ -740,7 +740,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/templates.rs b/crates/openfang-cli/src/tui/screens/templates.rs index 193d3e464d..a2992f259f 100644 --- a/crates/openfang-cli/src/tui/screens/templates.rs +++ b/crates/openfang-cli/src/tui/screens/templates.rs @@ -397,7 +397,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/triggers.rs b/crates/openfang-cli/src/tui/screens/triggers.rs index 8aeb093bd6..a28d315eee 100644 --- a/crates/openfang-cli/src/tui/screens/triggers.rs +++ b/crates/openfang-cli/src/tui/screens/triggers.rs @@ -547,7 +547,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/usage.rs b/crates/openfang-cli/src/tui/screens/usage.rs index 525e315c2f..87538f99b3 100644 --- a/crates/openfang-cli/src/tui/screens/usage.rs +++ b/crates/openfang-cli/src/tui/screens/usage.rs @@ -433,7 +433,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/screens/welcome.rs b/crates/openfang-cli/src/tui/screens/welcome.rs index 768a51ca42..755037921d 100644 --- a/crates/openfang-cli/src/tui/screens/welcome.rs +++ b/crates/openfang-cli/src/tui/screens/welcome.rs @@ -135,7 +135,7 @@ impl WelcomeState { }); self.menu_items.push(MenuItem { label: "Exit", - hint: "quit OpenFang", + hint: "quit OMTAE", action: WelcomeAction::Exit, }); self.menu.select(Some(0)); @@ -373,7 +373,7 @@ pub fn draw(f: &mut Frame, area: Rect, state: &mut WelcomeState) { Span::styled("No API keys detected", Style::default().fg(theme::YELLOW)), ])); status_lines.push(Line::from(vec![Span::styled( - " Run 'openfang init' to get started", + " Run 'omtae init' to get started", theme::hint_style(), )])); } diff --git a/crates/openfang-cli/src/tui/screens/wizard.rs b/crates/openfang-cli/src/tui/screens/wizard.rs index 7b30bad8ca..850c7690e9 100644 --- a/crates/openfang-cli/src/tui/screens/wizard.rs +++ b/crates/openfang-cli/src/tui/screens/wizard.rs @@ -154,7 +154,7 @@ const PROVIDERS: &[ProviderInfo] = &[ ProviderInfo { name: "vllm", env_var: "VLLM_API_KEY", - default_model: "local-model", + default_model: "Qwen2.5-Coder-32B-Instruct-AWQ", needs_key: false, }, ProviderInfo { @@ -171,7 +171,7 @@ pub fn needs_setup() -> bool { std::path::PathBuf::from(h) } else { match dirs::home_dir() { - Some(h) => h.join(".openfang"), + Some(h) => h.join(".omtae"), None => return true, } }; @@ -234,7 +234,7 @@ impl WizardState { // Detected providers first for (i, p) in PROVIDERS.iter().enumerate() { let detected = if p.name == "claude-code" { - openfang_runtime::drivers::claude_code::claude_code_available() + omtae_runtime::drivers::claude_code::claude_code_available() } else { !p.env_var.is_empty() && std::env::var(p.env_var).is_ok() }; @@ -245,7 +245,7 @@ impl WizardState { // Then the rest for (i, p) in PROVIDERS.iter().enumerate() { let detected = if p.name == "claude-code" { - openfang_runtime::drivers::claude_code::claude_code_available() + omtae_runtime::drivers::claude_code::claude_code_available() } else { !p.env_var.is_empty() && std::env::var(p.env_var).is_ok() }; @@ -384,11 +384,11 @@ impl WizardState { } }; - let openfang_dir = if let Ok(h) = std::env::var("OPENFANG_HOME") { + let omtae_dir = if let Ok(h) = std::env::var("OPENFANG_HOME") { std::path::PathBuf::from(h) } else { match dirs::home_dir() { - Some(h) => h.join(".openfang"), + Some(h) => h.join(".omtae"), None => { self.status_msg = "Could not determine home directory".to_string(); self.step = WizardStep::Done; @@ -396,9 +396,9 @@ impl WizardState { } } }; - let _ = std::fs::create_dir_all(openfang_dir.join("agents")); - let _ = std::fs::create_dir_all(openfang_dir.join("data")); - crate::restrict_dir_permissions(&openfang_dir); + let _ = std::fs::create_dir_all(omtae_dir.join("agents")); + let _ = std::fs::create_dir_all(omtae_dir.join("data")); + crate::restrict_dir_permissions(&omtae_dir); let api_key_line = if !self.api_key_input.is_empty() { format!("api_key = \"{}\"", self.api_key_input) @@ -415,7 +415,7 @@ impl WizardState { }; let config = format!( - r#"# OpenFang Agent OS configuration + r#"# OMTAE Agent OS configuration # Generated by setup wizard [default_model] @@ -432,7 +432,7 @@ listen_addr = "127.0.0.1:4200" provider = p.name, ); - let config_path = openfang_dir.join("config.toml"); + let config_path = omtae_dir.join("config.toml"); match std::fs::write(&config_path, &config) { Ok(()) => { crate::restrict_file_permissions(&config_path); @@ -535,7 +535,7 @@ fn draw_provider(f: &mut Frame, area: Rect, state: &mut WizardState) { .map(|&idx| { let p = &PROVIDERS[idx]; let hint = if p.name == "claude-code" { - if openfang_runtime::drivers::claude_code::claude_code_available() { + if omtae_runtime::drivers::claude_code::claude_code_available() { "CLI detected".to_string() } else { "no API key needed".to_string() diff --git a/crates/openfang-cli/src/tui/screens/workflows.rs b/crates/openfang-cli/src/tui/screens/workflows.rs index 21514923a6..ef5477261c 100644 --- a/crates/openfang-cli/src/tui/screens/workflows.rs +++ b/crates/openfang-cli/src/tui/screens/workflows.rs @@ -699,7 +699,7 @@ fn truncate(s: &str, max: usize) -> String { } else { format!( "{}\u{2026}", - openfang_types::truncate_str(s, max.saturating_sub(1)) + omtae_types::truncate_str(s, max.saturating_sub(1)) ) } } diff --git a/crates/openfang-cli/src/tui/theme.rs b/crates/openfang-cli/src/tui/theme.rs index 59409c89e6..09c911a8d7 100644 --- a/crates/openfang-cli/src/tui/theme.rs +++ b/crates/openfang-cli/src/tui/theme.rs @@ -1,6 +1,4 @@ -//! Color palette matching the OpenFang landing page design system. -//! -//! Core palette from globals.css + code syntax from constants.ts. +//! Color palette matching the OMTAE tactical matte design system. #![allow(dead_code)] // Full palette — some colors reserved for future screens. @@ -8,8 +6,8 @@ use ratatui::style::{Color, Modifier, Style}; // ── Core Palette (dark mode for terminal) ─────────────────────────────────── -pub const ACCENT: Color = Color::Rgb(255, 92, 0); // #FF5C00 — OpenFang orange -pub const ACCENT_DIM: Color = Color::Rgb(224, 82, 0); // #E05200 +pub const ACCENT: Color = Color::Rgb(240, 136, 62); // #F0883E — Tactical Amber +pub const ACCENT_DIM: Color = Color::Rgb(196, 110, 40); // #C46E28 — Dark Amber pub const BG_PRIMARY: Color = Color::Rgb(15, 14, 14); // #0F0E0E — dark background pub const BG_CARD: Color = Color::Rgb(31, 29, 28); // #1F1D1C — dark surface diff --git a/crates/openfang-cli/src/ui.rs b/crates/openfang-cli/src/ui.rs index 2442a772fe..b9d168389b 100644 --- a/crates/openfang-cli/src/ui.rs +++ b/crates/openfang-cli/src/ui.rs @@ -42,12 +42,12 @@ pub fn error(msg: &str) { // New themed output helpers // --------------------------------------------------------------------------- -/// Brand banner: ">> OpenFang Agent OS" +/// Brand banner: ">> OMTAE Agent OS" pub fn banner() { println!( " {} {}", ">>".bright_cyan().bold(), - "OpenFang Agent OS".bold() + "OMTAE Agent OS".bold() ); println!(" {}", "The open-source agent operating system".dimmed()); } diff --git a/crates/openfang-desktop/Cargo.toml b/crates/openfang-desktop/Cargo.toml index 2678754af6..f5c5b67bd2 100644 --- a/crates/openfang-desktop/Cargo.toml +++ b/crates/openfang-desktop/Cargo.toml @@ -1,17 +1,17 @@ [package] -name = "openfang-desktop" +name = "omtae-desktop" version.workspace = true edition.workspace = true license.workspace = true -description = "Native desktop application for the OpenFang Agent OS (Tauri 2.0)" +description = "Native desktop application for the OMTAE Agent OS (Tauri 2.0)" [build-dependencies] tauri-build = { version = "2", features = [] } [dependencies] -openfang-kernel = { path = "../openfang-kernel" } -openfang-api = { path = "../openfang-api" } -openfang-types = { path = "../openfang-types" } +omtae-kernel = { path = "../omtae-kernel" } +omtae-api = { path = "../omtae-api" } +omtae-types = { path = "../omtae-types" } tokio = { workspace = true } axum = { workspace = true } serde_json = { workspace = true } @@ -34,5 +34,5 @@ open = "5" custom-protocol = ["tauri/custom-protocol"] [[bin]] -name = "openfang-desktop" +name = "omtae-desktop" path = "src/main.rs" diff --git a/crates/openfang-desktop/capabilities/default.json b/crates/openfang-desktop/capabilities/default.json index d727894220..5ffbb72572 100644 --- a/crates/openfang-desktop/capabilities/default.json +++ b/crates/openfang-desktop/capabilities/default.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/tauri/refs/heads/dev/crates/tauri-utils/schema.json", "identifier": "default", - "description": "Default permissions for the OpenFang desktop app", + "description": "Default permissions for the OMTAE desktop app", "windows": ["main"], "permissions": [ "core:default", diff --git a/crates/openfang-desktop/gen/schemas/capabilities.json b/crates/openfang-desktop/gen/schemas/capabilities.json index c12e68d6f6..392e4dd0f7 100644 --- a/crates/openfang-desktop/gen/schemas/capabilities.json +++ b/crates/openfang-desktop/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"Default permissions for the OpenFang desktop app","local":true,"windows":["main"],"permissions":["core:default","notification:default","shell:default","dialog:default","global-shortcut:allow-register","global-shortcut:allow-unregister","global-shortcut:allow-is-registered","autostart:default","updater:default"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"Default permissions for the OMTAE desktop app","local":true,"windows":["main"],"permissions":["core:default","notification:default","shell:default","dialog:default","global-shortcut:allow-register","global-shortcut:allow-unregister","global-shortcut:allow-is-registered","autostart:default","updater:default"]}} \ No newline at end of file diff --git a/crates/openfang-desktop/src/commands.rs b/crates/openfang-desktop/src/commands.rs index 96d3fd04a8..09325acf82 100644 --- a/crates/openfang-desktop/src/commands.rs +++ b/crates/openfang-desktop/src/commands.rs @@ -1,7 +1,7 @@ //! Tauri IPC command handlers. use crate::{KernelState, PortState}; -use openfang_kernel::config::openfang_home; +use omtae_kernel::config::omtae_home; use tauri_plugin_autostart::ManagerExt; use tauri_plugin_dialog::DialogExt; use tracing::info; @@ -38,7 +38,7 @@ pub fn get_agent_count(kernel_state: tauri::State<'_, KernelState>) -> usize { /// Open a native file picker to import an agent TOML manifest. /// /// Validates the TOML as a valid `AgentManifest`, copies it to -/// `~/.openfang/agents/{name}/agent.toml`, then spawns the agent. +/// `~/.omtae/agents/{name}/agent.toml`, then spawns the agent. #[tauri::command] pub fn import_agent_toml( app: tauri::AppHandle, @@ -59,11 +59,11 @@ pub fn import_agent_toml( let content = std::fs::read_to_string(file_path.as_path().ok_or("Invalid file path")?) .map_err(|e| format!("Failed to read file: {e}"))?; - let manifest: openfang_types::agent::AgentManifest = + let manifest: omtae_types::agent::AgentManifest = toml::from_str(&content).map_err(|e| format!("Invalid agent manifest: {e}"))?; let agent_name = manifest.name.clone(); - let agent_dir = openfang_home().join("agents").join(&agent_name); + let agent_dir = omtae_home().join("agents").join(&agent_name); std::fs::create_dir_all(&agent_dir) .map_err(|e| format!("Failed to create agent directory: {e}"))?; @@ -81,7 +81,7 @@ pub fn import_agent_toml( /// Open a native file picker to import a skill file. /// -/// Copies the selected file to `~/.openfang/skills/` and triggers a +/// Copies the selected file to `~/.omtae/skills/` and triggers a /// hot-reload of the skill registry. #[tauri::command] pub fn import_skill_file( @@ -107,7 +107,7 @@ pub fn import_skill_file( .to_string_lossy() .to_string(); - let skills_dir = openfang_home().join("skills"); + let skills_dir = omtae_home().join("skills"); std::fs::create_dir_all(&skills_dir) .map_err(|e| format!("Failed to create skills directory: {e}"))?; @@ -154,18 +154,18 @@ pub async fn install_update(app: tauri::AppHandle) -> Result<(), String> { crate::updater::download_and_install_update(&app).await } -/// Open the OpenFang config directory (`~/.openfang/`) in the OS file manager. +/// Open the OMTAE config directory (`~/.omtae/`) in the OS file manager. #[tauri::command] pub fn open_config_dir() -> Result<(), String> { - let dir = openfang_home(); + let dir = omtae_home(); std::fs::create_dir_all(&dir).map_err(|e| format!("Failed to create config dir: {e}"))?; open::that(&dir).map_err(|e| format!("Failed to open directory: {e}")) } -/// Open the OpenFang logs directory (`~/.openfang/logs/`) in the OS file manager. +/// Open the OMTAE logs directory (`~/.omtae/logs/`) in the OS file manager. #[tauri::command] pub fn open_logs_dir() -> Result<(), String> { - let dir = openfang_home().join("logs"); + let dir = omtae_home().join("logs"); std::fs::create_dir_all(&dir).map_err(|e| format!("Failed to create logs dir: {e}"))?; open::that(&dir).map_err(|e| format!("Failed to open directory: {e}")) } diff --git a/crates/openfang-desktop/src/lib.rs b/crates/openfang-desktop/src/lib.rs index 2700478486..f0843f0127 100644 --- a/crates/openfang-desktop/src/lib.rs +++ b/crates/openfang-desktop/src/lib.rs @@ -1,4 +1,4 @@ -//! OpenFang Desktop — Native Tauri 2.0 wrapper for the OpenFang Agent OS. +//! OMTAE Desktop — Native Tauri 2.0 wrapper for the OMTAE Agent OS. //! //! Boots the kernel + embedded API server, then opens a native window pointing //! at the WebUI. Includes system tray, single-instance enforcement, native OS @@ -10,8 +10,8 @@ mod shortcuts; mod tray; mod updater; -use openfang_kernel::OpenFangKernel; -use openfang_types::event::{EventPayload, LifecycleEvent, SystemEvent}; +use omtae_kernel::OMTAEKernel; +use omtae_types::event::{EventPayload, LifecycleEvent, SystemEvent}; use std::sync::Arc; use std::time::Instant; use tauri::{Manager, WebviewUrl, WebviewWindowBuilder}; @@ -23,7 +23,7 @@ pub struct PortState(pub u16); /// Managed state: the kernel instance and startup time. pub struct KernelState { - pub kernel: Arc, + pub kernel: Arc, pub started_at: Instant, } @@ -34,18 +34,18 @@ pub fn run() { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "openfang=info,tauri=info".into()), + .unwrap_or_else(|_| "omtae=info,tauri=info".into()), ) .init(); - info!("Starting OpenFang Desktop..."); + info!("Starting OMTAE Desktop..."); // Boot kernel + embedded server (blocks until port is known) - let server_handle = server::start_server().expect("Failed to start OpenFang server"); + let server_handle = server::start_server().expect("Failed to start OMTAE server"); let port = server_handle.port; let kernel_for_notifications = server_handle.kernel.clone(); - info!("OpenFang server running on port {port}"); + info!("OMTAE server running on port {port}"); let url = format!("http://127.0.0.1:{port}"); @@ -114,7 +114,7 @@ pub fn run() { "main", WebviewUrl::External(url.parse().expect("Invalid server URL")), ) - .title("OpenFang") + .title("OMTAE") .inner_size(1280.0, 800.0) .min_inner_size(800.0, 600.0) .center() @@ -145,7 +145,7 @@ pub fn run() { ), EventPayload::System(SystemEvent::KernelStopping) => ( "Kernel Stopping".to_string(), - "OpenFang kernel is shutting down".to_string(), + "OMTAE kernel is shutting down".to_string(), ), EventPayload::System(SystemEvent::QuotaEnforced { agent_id, @@ -186,7 +186,7 @@ pub fn run() { #[cfg(desktop)] updater::spawn_startup_check(app.handle().clone()); - info!("OpenFang Desktop window created"); + info!("OMTAE Desktop window created"); Ok(()) }) .on_window_event(|window, event| { diff --git a/crates/openfang-desktop/src/main.rs b/crates/openfang-desktop/src/main.rs index 172728e7ce..5a8430df51 100644 --- a/crates/openfang-desktop/src/main.rs +++ b/crates/openfang-desktop/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - openfang_desktop::run(); + omtae_desktop::run(); } diff --git a/crates/openfang-desktop/src/server.rs b/crates/openfang-desktop/src/server.rs index 053c646dbb..3815b9dbbb 100644 --- a/crates/openfang-desktop/src/server.rs +++ b/crates/openfang-desktop/src/server.rs @@ -1,10 +1,10 @@ //! Kernel lifecycle management for the desktop app. //! -//! Boots the OpenFang kernel, binds to a random localhost port, and runs the +//! Boots the OMTAE kernel, binds to a random localhost port, and runs the //! API server on a background thread with its own tokio runtime. -use openfang_api::server::build_router; -use openfang_kernel::OpenFangKernel; +use omtae_api::server::build_router; +use omtae_kernel::OMTAEKernel; use std::net::{SocketAddr, TcpListener}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -16,7 +16,7 @@ pub struct ServerHandle { /// The port the server is listening on. pub port: u16, /// The kernel instance (shared with the server). - pub kernel: Arc, + pub kernel: Arc, /// Send `true` to trigger graceful shutdown. shutdown_tx: watch::Sender, /// Join handle for the background server thread. @@ -39,7 +39,7 @@ impl ServerHandle { let _ = handle.join(); } self.kernel.shutdown(); - info!("OpenFang embedded server stopped"); + info!("OMTAE embedded server stopped"); } } } @@ -65,12 +65,12 @@ impl Drop for ServerHandle { /// thread with its own tokio runtime. pub fn start_server() -> Result> { // Load .env and secrets.env into process environment (same as CLI). - // Without this, API keys stored in ~/.openfang/.env are invisible to + // Without this, API keys stored in ~/.omtae/.env are invisible to // the kernel's provider detection and credential resolver. load_dotenv_files(); // Boot kernel (sync — no tokio needed) - let kernel = OpenFangKernel::boot(None)?; + let kernel = OMTAEKernel::boot(None)?; let kernel = Arc::new(kernel); kernel.set_self_handle(); @@ -79,14 +79,14 @@ pub fn start_server() -> Result> { let port = std_listener.local_addr()?.port(); let listen_addr: SocketAddr = std_listener.local_addr()?; - info!("OpenFang embedded server bound to http://127.0.0.1:{port}"); + info!("OMTAE embedded server bound to http://127.0.0.1:{port}"); let (shutdown_tx, shutdown_rx) = watch::channel(false); let kernel_clone = kernel.clone(); let shutdown_initiated = Arc::new(AtomicBool::new(false)); let server_thread = std::thread::Builder::new() - .name("openfang-server".into()) + .name("omtae-server".into()) .spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -113,7 +113,7 @@ pub fn start_server() -> Result> { /// Run the axum server inside a tokio runtime, shut down when the watch /// channel fires. async fn run_embedded_server( - kernel: Arc, + kernel: Arc, std_listener: TcpListener, listen_addr: SocketAddr, mut shutdown_rx: watch::Receiver, @@ -127,7 +127,7 @@ async fn run_embedded_server( let listener = tokio::net::TcpListener::from_std(std_listener) .expect("Failed to convert std TcpListener to tokio"); - info!("OpenFang embedded server listening on http://{listen_addr}"); + info!("OMTAE embedded server listening on http://{listen_addr}"); let server = axum::serve( listener, @@ -151,7 +151,7 @@ async fn run_embedded_server( } } -/// Load ~/.openfang/.env and ~/.openfang/secrets.env into the process environment. +/// Load ~/.omtae/.env and ~/.omtae/secrets.env into the process environment. /// System env vars take priority — existing vars are NOT overridden. fn load_dotenv_files() { let home = if let Ok(h) = std::env::var("OPENFANG_HOME") { @@ -163,7 +163,7 @@ fn load_dotenv_files() { if user_home.is_empty() { return; } - std::path::PathBuf::from(user_home).join(".openfang") + std::path::PathBuf::from(user_home).join(".omtae") }; for filename in &[".env", "secrets.env"] { diff --git a/crates/openfang-desktop/src/shortcuts.rs b/crates/openfang-desktop/src/shortcuts.rs index c91731bdb5..1b46d5f484 100644 --- a/crates/openfang-desktop/src/shortcuts.rs +++ b/crates/openfang-desktop/src/shortcuts.rs @@ -1,4 +1,4 @@ -//! System-wide keyboard shortcuts for the OpenFang desktop app. +//! System-wide keyboard shortcuts for the OMTAE desktop app. use tauri::{Emitter, Manager}; use tauri_plugin_global_shortcut::{Code, Modifiers, ShortcutState}; @@ -6,7 +6,7 @@ use tracing::warn; /// Build the global shortcut plugin with 3 system-wide shortcuts: /// -/// - `Ctrl+Shift+O` — Show/focus the OpenFang window +/// - `Ctrl+Shift+O` — Show/focus the OMTAE window /// - `Ctrl+Shift+N` — Show window + navigate to agents page /// - `Ctrl+Shift+C` — Show window + navigate to chat page /// diff --git a/crates/openfang-desktop/src/tray.rs b/crates/openfang-desktop/src/tray.rs index f915fd8ac8..32f3b426ec 100644 --- a/crates/openfang-desktop/src/tray.rs +++ b/crates/openfang-desktop/src/tray.rs @@ -1,6 +1,6 @@ -//! System tray setup for the OpenFang desktop app. +//! System tray setup for the OMTAE desktop app. -use openfang_kernel::config::openfang_home; +use omtae_kernel::config::omtae_home; use tauri::{ menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, @@ -85,7 +85,7 @@ pub fn setup_tray(app: &tauri::App) -> Result<(), Box> { )?; let sep3 = PredefinedMenuItem::separator(app)?; - let quit = MenuItem::with_id(app, "quit", "Quit OpenFang", true, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Quit OMTAE", true, None::<&str>)?; let menu = Menu::with_items( app, @@ -111,7 +111,7 @@ pub fn setup_tray(app: &tauri::App) -> Result<(), Box> { let _tray = TrayIconBuilder::new() .icon(tray_icon) .menu(&menu) - .tooltip("OpenFang Agent OS") + .tooltip("OMTAE Agent OS") .on_menu_event(move |app, event| match event.id().as_ref() { "show" => { if let Some(w) = app.get_webview_window("main") { @@ -154,7 +154,7 @@ pub fn setup_tray(app: &tauri::App) -> Result<(), Box> { .builder() .title("Installing Update...") .body(format!( - "Downloading OpenFang v{version}. App will restart shortly." + "Downloading OMTAE v{version}. App will restart shortly." )) .show(); // Perform install @@ -176,7 +176,7 @@ pub fn setup_tray(app: &tauri::App) -> Result<(), Box> { .notification() .builder() .title("Up to Date") - .body("You're running the latest version of OpenFang.") + .body("You're running the latest version of OMTAE.") .show(); } Err(e) => { @@ -192,7 +192,7 @@ pub fn setup_tray(app: &tauri::App) -> Result<(), Box> { }); } "open_config" => { - let dir = openfang_home(); + let dir = omtae_home(); let _ = std::fs::create_dir_all(&dir); if let Err(e) = open::that(&dir) { warn!("Failed to open config dir: {e}"); diff --git a/crates/openfang-desktop/src/updater.rs b/crates/openfang-desktop/src/updater.rs index 2bf34aa029..4a1e789587 100644 --- a/crates/openfang-desktop/src/updater.rs +++ b/crates/openfang-desktop/src/updater.rs @@ -1,4 +1,4 @@ -//! Update checker for the OpenFang desktop app. +//! Update checker for the OMTAE desktop app. use serde::Serialize; use tauri_plugin_notification::NotificationExt; @@ -32,7 +32,7 @@ pub fn spawn_startup_check(app_handle: tauri::AppHandle) { let _ = app_handle .notification() .builder() - .title("OpenFang Updating...") + .title("OMTAE Updating...") .body(format!("Installing v{version}. App will restart shortly.")) .show(); // Small delay so notification is visible diff --git a/crates/openfang-desktop/tauri.conf.json b/crates/openfang-desktop/tauri.conf.json index b2adb27d4e..188fdaefe6 100644 --- a/crates/openfang-desktop/tauri.conf.json +++ b/crates/openfang-desktop/tauri.conf.json @@ -1,8 +1,8 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "OpenFang", + "productName": "OMTAE", "version": "0.6.9", - "identifier": "ai.openfang.desktop", + "identifier": "ai.omtae.desktop", "build": {}, "app": { "windows": [], @@ -14,7 +14,7 @@ "updater": { "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEJDOTE5MDhCRDNGMTUyMEQKUldRTlV2SFRpNUNSdkZRZDcvektwZkN6bUsyM0NJZC9OeXhHRU5id1FnZWllQmNlZGRoSGRQOHkK", "endpoints": [ - "https://github.com/RightNow-AI/openfang/releases/latest/download/latest.json" + "https://github.com/RightNow-AI/omtae/releases/latest/download/latest.json" ], "windows": { "installMode": "passive" @@ -33,7 +33,7 @@ ], "category": "Productivity", "shortDescription": "Open-source Agent Operating System", - "longDescription": "OpenFang is an open-source Agent Operating System — run, orchestrate, and extend AI agents across every channel from your desktop.", + "longDescription": "OMTAE is an open-source Agent Operating System — run, orchestrate, and extend AI agents across every channel from your desktop.", "linux": { "deb": { "depends": [] diff --git a/crates/openfang-extensions/Cargo.toml b/crates/openfang-extensions/Cargo.toml index a7ae4ab1ca..b0ef61bb6d 100644 --- a/crates/openfang-extensions/Cargo.toml +++ b/crates/openfang-extensions/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "openfang-extensions" +name = "omtae-extensions" version.workspace = true edition.workspace = true license.workspace = true -description = "Extension & integration system for OpenFang — one-click MCP server setup, credential vault, OAuth2 PKCE" +description = "Extension & integration system for OMTAE — one-click MCP server setup, credential vault, OAuth2 PKCE" [dependencies] -openfang-types = { path = "../openfang-types" } +omtae-types = { path = "../omtae-types" } serde = { workspace = true } serde_json = { workspace = true } toml = { workspace = true } diff --git a/crates/openfang-extensions/integrations/github.toml b/crates/openfang-extensions/integrations/github.toml index 17cdde2a21..0f77d7c9e4 100644 --- a/crates/openfang-extensions/integrations/github.toml +++ b/crates/openfang-extensions/integrations/github.toml @@ -30,5 +30,5 @@ unhealthy_threshold = 3 setup_instructions = """ 1. Go to https://github.com/settings/tokens and create a Personal Access Token (classic or fine-grained) with 'repo' and 'read:org' scopes. 2. Paste the token into the GITHUB_PERSONAL_ACCESS_TOKEN field above. -3. Alternatively, use the OAuth flow to authorize OpenFang directly with your GitHub account. +3. Alternatively, use the OAuth flow to authorize OMTAE directly with your GitHub account. """ diff --git a/crates/openfang-extensions/integrations/gmail.toml b/crates/openfang-extensions/integrations/gmail.toml index c847b3f64a..defad9058d 100644 --- a/crates/openfang-extensions/integrations/gmail.toml +++ b/crates/openfang-extensions/integrations/gmail.toml @@ -22,6 +22,6 @@ unhealthy_threshold = 3 setup_instructions = """ 1. Click 'Connect' to initiate the OAuth flow with your Google account. -2. Grant OpenFang permission to read and modify your Gmail messages when prompted. +2. Grant OMTAE permission to read and modify your Gmail messages when prompted. 3. The connection will be established automatically after authorization. """ diff --git a/crates/openfang-extensions/integrations/google-calendar.toml b/crates/openfang-extensions/integrations/google-calendar.toml index db84ada8d5..d9173a4330 100644 --- a/crates/openfang-extensions/integrations/google-calendar.toml +++ b/crates/openfang-extensions/integrations/google-calendar.toml @@ -22,6 +22,6 @@ unhealthy_threshold = 3 setup_instructions = """ 1. Click 'Connect' to initiate the OAuth flow with your Google account. -2. Grant OpenFang access to your Google Calendar when prompted. +2. Grant OMTAE access to your Google Calendar when prompted. 3. The connection will be established automatically after authorization. """ diff --git a/crates/openfang-extensions/integrations/google-drive.toml b/crates/openfang-extensions/integrations/google-drive.toml index f3198866ea..fc03ae4d67 100644 --- a/crates/openfang-extensions/integrations/google-drive.toml +++ b/crates/openfang-extensions/integrations/google-drive.toml @@ -22,6 +22,6 @@ unhealthy_threshold = 3 setup_instructions = """ 1. Click 'Connect' to initiate the OAuth flow with your Google account. -2. Grant OpenFang read-only access to your Google Drive files when prompted. +2. Grant OMTAE read-only access to your Google Drive files when prompted. 3. The connection will be established automatically after authorization. """ diff --git a/crates/openfang-extensions/integrations/sqlite-mcp.toml b/crates/openfang-extensions/integrations/sqlite-mcp.toml index ca4f1870ea..ff4408a868 100644 --- a/crates/openfang-extensions/integrations/sqlite-mcp.toml +++ b/crates/openfang-extensions/integrations/sqlite-mcp.toml @@ -24,5 +24,5 @@ unhealthy_threshold = 3 setup_instructions = """ 1. Locate the SQLite database file you want to connect to on your local filesystem. 2. Enter the absolute path to the database file in the SQLITE_DB_PATH field above. -3. Ensure the file has appropriate read/write permissions for the OpenFang process. +3. Ensure the file has appropriate read/write permissions for the OMTAE process. """ diff --git a/crates/openfang-extensions/integrations/teams-mcp.toml b/crates/openfang-extensions/integrations/teams-mcp.toml index b47d70bf49..24675506f0 100644 --- a/crates/openfang-extensions/integrations/teams-mcp.toml +++ b/crates/openfang-extensions/integrations/teams-mcp.toml @@ -22,6 +22,6 @@ unhealthy_threshold = 3 setup_instructions = """ 1. Click 'Connect' to initiate the OAuth flow with your Microsoft account. -2. Sign in with your Microsoft 365 account and grant OpenFang permission to read teams and read/write chats. +2. Sign in with your Microsoft 365 account and grant OMTAE permission to read teams and read/write chats. 3. The connection will be established automatically after authorization. """ diff --git a/crates/openfang-extensions/src/bundled.rs b/crates/openfang-extensions/src/bundled.rs index 75b955cf72..098d17339a 100644 --- a/crates/openfang-extensions/src/bundled.rs +++ b/crates/openfang-extensions/src/bundled.rs @@ -1,7 +1,7 @@ //! Compile-time embedded integration templates. //! //! All 25 integration TOML files are baked into the binary via `include_str!()`, -//! ensuring they ship with every OpenFang build with zero filesystem dependencies. +//! ensuring they ship with every OMTAE build with zero filesystem dependencies. /// Returns all bundled integration templates as `(id, TOML content)` pairs. pub fn bundled_integrations() -> Vec<(&'static str, &'static str)> { diff --git a/crates/openfang-extensions/src/credentials.rs b/crates/openfang-extensions/src/credentials.rs index c75b42a883..15bf7db950 100644 --- a/crates/openfang-extensions/src/credentials.rs +++ b/crates/openfang-extensions/src/credentials.rs @@ -1,8 +1,8 @@ //! Credential resolution chain — resolves secrets from multiple sources. //! //! Resolution order: -//! 1. Encrypted vault (`~/.openfang/vault.enc`) -//! 2. Dotenv file (`~/.openfang/.env`) +//! 1. Encrypted vault (`~/.omtae/vault.enc`) +//! 2. Dotenv file (`~/.omtae/.env`) //! 3. Process environment variable //! 4. Interactive prompt (CLI only, when `interactive` is true) @@ -17,7 +17,7 @@ use zeroize::Zeroizing; pub struct CredentialResolver { /// Reference to the credential vault. vault: Option, - /// Dotenv entries (loaded from `~/.openfang/.env`). + /// Dotenv entries (loaded from `~/.omtae/.env`). dotenv: HashMap, /// Whether to prompt interactively as a last resort. interactive: bool, diff --git a/crates/openfang-extensions/src/installer.rs b/crates/openfang-extensions/src/installer.rs index bf088f3d54..61ee59e87a 100644 --- a/crates/openfang-extensions/src/installer.rs +++ b/crates/openfang-extensions/src/installer.rs @@ -221,7 +221,7 @@ pub fn search_integrations( /// Generate scaffold files for a new custom integration. pub fn scaffold_integration(dir: &std::path::Path) -> ExtensionResult { let template = r#"# Custom Integration Template -# Place this in ~/.openfang/integrations/ or use `openfang add --custom ` +# Place this in ~/.omtae/integrations/ or use `omtae add --custom ` id = "my-integration" name = "My Integration" @@ -248,7 +248,7 @@ unhealthy_threshold = 3 setup_instructions = """ 1. Install the MCP server: npm install -g my-mcp-server 2. Get your API key from https://example.com/api-keys -3. Run: openfang add my-integration --key= +3. Run: omtae add my-integration --key= """ "#; let path = dir.join("integration.toml"); diff --git a/crates/openfang-extensions/src/lib.rs b/crates/openfang-extensions/src/lib.rs index 929dc6ff3c..ff50e9e695 100644 --- a/crates/openfang-extensions/src/lib.rs +++ b/crates/openfang-extensions/src/lib.rs @@ -1,11 +1,11 @@ -//! OpenFang Extensions — one-click integration system. +//! OMTAE Extensions — one-click integration system. //! //! This crate provides: //! - **Integration Registry**: 25 bundled MCP server templates (GitHub, Slack, etc.) //! - **Credential Vault**: AES-256-GCM encrypted storage with OS keyring support //! - **OAuth2 PKCE**: Localhost callback flows for Google/GitHub/Microsoft/Slack //! - **Health Monitor**: Auto-reconnect with exponential backoff -//! - **Installer**: One-click `openfang add ` flow +//! - **Installer**: One-click `omtae add ` flow pub mod bundled; pub mod credentials; @@ -225,7 +225,7 @@ pub struct InstalledIntegration { pub config: HashMap, } -/// Top-level structure for `~/.openfang/integrations.toml`. +/// Top-level structure for `~/.omtae/integrations.toml`. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct IntegrationsFile { #[serde(default)] diff --git a/crates/openfang-extensions/src/oauth.rs b/crates/openfang-extensions/src/oauth.rs index beabe01b9e..ab53dc606f 100644 --- a/crates/openfang-extensions/src/oauth.rs +++ b/crates/openfang-extensions/src/oauth.rs @@ -19,15 +19,15 @@ use zeroize::Zeroizing; pub fn default_client_ids() -> HashMap<&'static str, &'static str> { let mut m = HashMap::new(); // Placeholder IDs — users should configure their own via config - m.insert("google", "openfang-google-client-id"); - m.insert("github", "openfang-github-client-id"); - m.insert("microsoft", "openfang-microsoft-client-id"); - m.insert("slack", "openfang-slack-client-id"); + m.insert("google", "omtae-google-client-id"); + m.insert("github", "omtae-github-client-id"); + m.insert("microsoft", "omtae-microsoft-client-id"); + m.insert("slack", "omtae-slack-client-id"); m } /// Resolve OAuth client IDs with config overrides applied on top of defaults. -pub fn resolve_client_ids(config: &openfang_types::config::OAuthConfig) -> HashMap { +pub fn resolve_client_ids(config: &omtae_types::config::OAuthConfig) -> HashMap { let defaults = default_client_ids(); let mut resolved: HashMap = defaults .into_iter() @@ -360,15 +360,15 @@ mod tests { #[test] fn resolve_client_ids_uses_defaults() { - let config = openfang_types::config::OAuthConfig::default(); + let config = omtae_types::config::OAuthConfig::default(); let ids = resolve_client_ids(&config); - assert_eq!(ids["google"], "openfang-google-client-id"); - assert_eq!(ids["github"], "openfang-github-client-id"); + assert_eq!(ids["google"], "omtae-google-client-id"); + assert_eq!(ids["github"], "omtae-github-client-id"); } #[test] fn resolve_client_ids_applies_overrides() { - let config = openfang_types::config::OAuthConfig { + let config = omtae_types::config::OAuthConfig { google_client_id: Some("my-real-google-id".into()), github_client_id: None, microsoft_client_id: Some("my-msft-id".into()), @@ -376,8 +376,8 @@ mod tests { }; let ids = resolve_client_ids(&config); assert_eq!(ids["google"], "my-real-google-id"); - assert_eq!(ids["github"], "openfang-github-client-id"); // default + assert_eq!(ids["github"], "omtae-github-client-id"); // default assert_eq!(ids["microsoft"], "my-msft-id"); - assert_eq!(ids["slack"], "openfang-slack-client-id"); // default + assert_eq!(ids["slack"], "omtae-slack-client-id"); // default } } diff --git a/crates/openfang-extensions/src/registry.rs b/crates/openfang-extensions/src/registry.rs index 282ad3ddab..66ab777f25 100644 --- a/crates/openfang-extensions/src/registry.rs +++ b/crates/openfang-extensions/src/registry.rs @@ -1,14 +1,14 @@ //! Integration Registry — manages bundled + installed integration templates. //! //! Loads 25 bundled MCP server templates at compile time, merges with user's -//! installed state from `~/.openfang/integrations.toml`, and converts installed +//! installed state from `~/.omtae/integrations.toml`, and converts installed //! integrations to `McpServerConfigEntry` for kernel consumption. use crate::{ ExtensionError, ExtensionResult, InstalledIntegration, IntegrationCategory, IntegrationInfo, IntegrationStatus, IntegrationTemplate, IntegrationsFile, }; -use openfang_types::config::{McpServerConfigEntry, McpTransportEntry}; +use omtae_types::config::{McpServerConfigEntry, McpTransportEntry}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use tracing::{debug, info, warn}; diff --git a/crates/openfang-extensions/src/vault.rs b/crates/openfang-extensions/src/vault.rs index 5b919e08f5..d72b73bd66 100644 --- a/crates/openfang-extensions/src/vault.rs +++ b/crates/openfang-extensions/src/vault.rs @@ -1,6 +1,6 @@ //! Credential Vault — AES-256-GCM encrypted secret storage. //! -//! Stores secrets in `~/.openfang/vault.enc`, with the master key sourced from +//! Stores secrets in `~/.omtae/vault.enc`, with the master key sourced from //! the OS keyring (Windows Credential Manager / macOS Keychain / Linux Secret Service) //! or the `OPENFANG_VAULT_KEY` env var for headless/CI environments. @@ -20,7 +20,7 @@ use zeroize::Zeroizing; /// Service name for OS keyring storage. #[cfg(not(test))] -const KEYRING_SERVICE: &str = "openfang-vault"; +const KEYRING_SERVICE: &str = "omtae-vault"; /// Username for OS keyring (used by platform keyring backends). #[allow(dead_code)] const KEYRING_USER: &str = "master-key"; @@ -136,7 +136,7 @@ impl CredentialVault { } if !self.path.exists() { return Err(ExtensionError::Vault( - "Vault not initialized. Run `openfang vault init`.".to_string(), + "Vault not initialized. Run `omtae vault init`.".to_string(), )); } @@ -216,7 +216,7 @@ impl CredentialVault { } if !self.path.exists() { return Err(ExtensionError::Vault( - "Vault not initialized. Run `openfang vault init`.".to_string(), + "Vault not initialized. Run `omtae vault init`.".to_string(), )); } self.load(&master_key)?; @@ -432,7 +432,7 @@ fn store_keyring_key(key_b64: &str) -> Result<(), String> { // than plaintext env vars. let keyring_path = dirs::data_local_dir() .unwrap_or_else(std::env::temp_dir) - .join("openfang") + .join("omtae") .join(".keyring"); std::fs::create_dir_all(keyring_path.parent().unwrap()) .map_err(|e| format!("mkdir: {e}"))?; @@ -468,7 +468,7 @@ fn load_keyring_key() -> Result, String> { { let keyring_path = dirs::data_local_dir() .unwrap_or_else(std::env::temp_dir) - .join("openfang") + .join("omtae") .join(".keyring"); if !keyring_path.exists() { return Err("Keyring file not found".to_string()); @@ -510,7 +510,7 @@ fn machine_fingerprint() -> Vec { if let Ok(host) = std::env::var("COMPUTERNAME").or_else(|_| std::env::var("HOSTNAME")) { hasher.update(host.as_bytes()); } - hasher.update(b"openfang-vault-v1"); + hasher.update(b"omtae-vault-v1"); hasher.finalize().to_vec() } diff --git a/crates/openfang-hands/Cargo.toml b/crates/openfang-hands/Cargo.toml index a64b4ecc7c..87d077b910 100644 --- a/crates/openfang-hands/Cargo.toml +++ b/crates/openfang-hands/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "openfang-hands" +name = "omtae-hands" version.workspace = true edition.workspace = true license.workspace = true -description = "Hands system for OpenFang — curated autonomous capability packages" +description = "Hands system for OMTAE — curated autonomous capability packages" [dependencies] -openfang-types = { path = "../openfang-types" } +omtae-types = { path = "../omtae-types" } serde = { workspace = true } serde_json = { workspace = true } toml = { workspace = true } diff --git a/crates/openfang-hands/bundled/browser/HAND.toml b/crates/openfang-hands/bundled/browser/HAND.toml index eb2ee452fe..e53483f15f 100644 --- a/crates/openfang-hands/bundled/browser/HAND.toml +++ b/crates/openfang-hands/bundled/browser/HAND.toml @@ -115,7 +115,7 @@ description = "AI web browser — navigates websites, fills forms, searches prod module = "builtin:chat" provider = "default" model = "default" -max_tokens = 16384 +max_tokens = 4096 temperature = 0.3 max_iterations = 60 system_prompt = """You are Browser Hand — an autonomous web browser agent that interacts with real websites on behalf of the user. diff --git a/crates/openfang-hands/bundled/browser/SKILL.md b/crates/openfang-hands/bundled/browser/SKILL.md index ab53912bdc..ad0e394497 100644 --- a/crates/openfang-hands/bundled/browser/SKILL.md +++ b/crates/openfang-hands/bundled/browser/SKILL.md @@ -2,7 +2,7 @@ name: browser-automation version: "1.0.0" description: Playwright-based browser automation patterns for autonomous web interaction -author: OpenFang +author: OMTAE tags: [browser, automation, playwright, web, scraping] tools: [browser_navigate, browser_click, browser_type, browser_screenshot, browser_read_page, browser_close] runtime: prompt_only diff --git a/crates/openfang-hands/bundled/collector/HAND.toml b/crates/openfang-hands/bundled/collector/HAND.toml index 552a45c30d..1ce6fd683b 100644 --- a/crates/openfang-hands/bundled/collector/HAND.toml +++ b/crates/openfang-hands/bundled/collector/HAND.toml @@ -151,7 +151,7 @@ description = "AI intelligence collector — monitors any target continuously wi module = "builtin:chat" provider = "default" model = "default" -max_tokens = 16384 +max_tokens = 4096 temperature = 0.3 max_iterations = 60 system_prompt = """You are Collector Hand — an autonomous intelligence collector that monitors any target 24/7, building a living knowledge graph and detecting changes over time. diff --git a/crates/openfang-hands/bundled/infisical-sync/SKILL.md b/crates/openfang-hands/bundled/infisical-sync/SKILL.md index 2b4a347c6b..5308adc669 100644 --- a/crates/openfang-hands/bundled/infisical-sync/SKILL.md +++ b/crates/openfang-hands/bundled/infisical-sync/SKILL.md @@ -2,7 +2,7 @@ name: infisical-sync-skill version: "1.0.0" description: "Expert knowledge for the Infisical Sync Hand — Infisical API reference, vault operations, error patterns, security guidance" -author: OpenFang +author: OMTAE tags: [secrets, infisical, vault, security, sync] tools: [shell_exec, vault_set, vault_get, vault_list, vault_delete, memory_store, memory_recall] runtime: prompt_only diff --git a/crates/openfang-hands/bundled/lead/HAND.toml b/crates/openfang-hands/bundled/lead/HAND.toml index fde6433218..ad0d313e60 100644 --- a/crates/openfang-hands/bundled/lead/HAND.toml +++ b/crates/openfang-hands/bundled/lead/HAND.toml @@ -166,7 +166,7 @@ description = "AI lead generation engine — discovers, enriches, deduplicates, module = "builtin:chat" provider = "default" model = "default" -max_tokens = 16384 +max_tokens = 4096 temperature = 0.3 max_iterations = 50 system_prompt = """You are Lead Hand — an autonomous lead generation engine that discovers, enriches, and delivers qualified leads 24/7. diff --git a/crates/openfang-hands/bundled/predictor/HAND.toml b/crates/openfang-hands/bundled/predictor/HAND.toml index ae1d41422d..1dbb01dbdc 100644 --- a/crates/openfang-hands/bundled/predictor/HAND.toml +++ b/crates/openfang-hands/bundled/predictor/HAND.toml @@ -171,7 +171,7 @@ description = "AI forecasting engine — collects signals, builds reasoning chai module = "builtin:chat" provider = "default" model = "default" -max_tokens = 16384 +max_tokens = 4096 temperature = 0.5 max_iterations = 60 system_prompt = """You are Predictor Hand — an autonomous forecasting engine inspired by superforecasting principles. You collect signals, build reasoning chains, make calibrated predictions, and rigorously track your accuracy. diff --git a/crates/openfang-hands/bundled/researcher/HAND.toml b/crates/openfang-hands/bundled/researcher/HAND.toml index b2afbfca8c..032260af1a 100644 --- a/crates/openfang-hands/bundled/researcher/HAND.toml +++ b/crates/openfang-hands/bundled/researcher/HAND.toml @@ -159,7 +159,7 @@ description = "AI deep researcher — conducts exhaustive investigations with cr module = "builtin:chat" provider = "default" model = "default" -max_tokens = 16384 +max_tokens = 4096 temperature = 0.3 max_iterations = 25 # Researcher makes long LLM calls; 30s (kernel default) causes false-positive diff --git a/crates/openfang-hands/bundled/trader/HAND.toml b/crates/openfang-hands/bundled/trader/HAND.toml index 9351acd4ef..c8ea606fd1 100644 --- a/crates/openfang-hands/bundled/trader/HAND.toml +++ b/crates/openfang-hands/bundled/trader/HAND.toml @@ -197,7 +197,7 @@ description = "AI market intelligence and trading engine — multi-signal analys module = "builtin:chat" provider = "default" model = "default" -max_tokens = 16384 +max_tokens = 4096 temperature = 0.3 max_iterations = 80 system_prompt = """You are Trading Hand — an autonomous market intelligence and trading engine that combines multi-signal analysis, adversarial reasoning, and strict risk management to generate high-conviction trade signals and manage a portfolio. diff --git a/crates/openfang-hands/bundled/trader/SKILL.md b/crates/openfang-hands/bundled/trader/SKILL.md index 3890b16e64..0436d4214e 100644 --- a/crates/openfang-hands/bundled/trader/SKILL.md +++ b/crates/openfang-hands/bundled/trader/SKILL.md @@ -2,7 +2,7 @@ name: trader-hand-skill version: "1.0.0" description: "Expert knowledge for autonomous market intelligence and trading — technical analysis, risk management, Alpaca API, financial data sources" -author: OpenFang +author: OMTAE tags: [trading, finance, stocks, crypto, technical-analysis, risk-management] tools: [shell_exec, file_read, file_write, web_fetch, web_search, memory_store] runtime: prompt_only diff --git a/crates/openfang-hands/bundled/twitter/HAND.toml b/crates/openfang-hands/bundled/twitter/HAND.toml index c1091cd75a..8589539522 100644 --- a/crates/openfang-hands/bundled/twitter/HAND.toml +++ b/crates/openfang-hands/bundled/twitter/HAND.toml @@ -23,7 +23,7 @@ steps = [ "Navigate to your App's 'Keys and tokens' page", "Generate a Bearer Token under 'Authentication Tokens'", "Copy the token and set it as an environment variable", - "Restart OpenFang or reload config for the change to take effect", + "Restart OMTAE or reload config for the change to take effect", ] # ─── Configurable settings ─────────────────────────────────────────────────── @@ -181,7 +181,7 @@ description = "AI Twitter/X manager — creates content, manages posting schedul module = "builtin:chat" provider = "default" model = "default" -max_tokens = 16384 +max_tokens = 4096 temperature = 0.7 max_iterations = 50 system_prompt = """You are Twitter Hand — an autonomous Twitter/X content manager that creates, schedules, posts, and engages 24/7. diff --git a/crates/openfang-hands/src/lib.rs b/crates/openfang-hands/src/lib.rs index 1bdb6783c3..ccb3d65621 100644 --- a/crates/openfang-hands/src/lib.rs +++ b/crates/openfang-hands/src/lib.rs @@ -1,4 +1,4 @@ -//! OpenFang Hands — curated autonomous capability packages. +//! OMTAE Hands — curated autonomous capability packages. //! //! A Hand is a pre-built, domain-complete agent configuration that users activate //! from a marketplace. Unlike regular agents (you chat with them), Hands work for @@ -8,7 +8,7 @@ pub mod bundled; pub mod registry; use chrono::{DateTime, Utc}; -use openfang_types::agent::AgentId; +use omtae_types::agent::AgentId; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use uuid::Uuid; @@ -333,7 +333,7 @@ pub fn parse_hand_toml(content: &str) -> Result /// Recursively copy a directory and all its contents. /// /// Used by `HandRegistry::install_from_path` to persist a custom hand's -/// source directory into `~/.openfang/hands//` so installed hands +/// source directory into `~/.omtae/hands//` so installed hands /// survive daemon restarts (issue #984). pub(crate) fn copy_dir_all( src: impl AsRef, diff --git a/crates/openfang-hands/src/registry.rs b/crates/openfang-hands/src/registry.rs index f2e73e66dd..f7449f0359 100644 --- a/crates/openfang-hands/src/registry.rs +++ b/crates/openfang-hands/src/registry.rs @@ -6,7 +6,7 @@ use crate::{ HandStatus, RequirementType, }; use dashmap::DashMap; -use openfang_types::agent::AgentId; +use omtae_types::agent::AgentId; use serde::Serialize; use sha2::{Digest, Sha256}; use std::collections::HashMap; @@ -184,9 +184,9 @@ impl HandRegistry { /// /// Returns the number of hands successfully loaded. A non-existent /// `hands_dir` returns `Ok(0)` — this is the normal case on a fresh - /// install where the user has not run `openfang hand install` yet. + /// install where the user has not run `omtae hand install` yet. /// - /// Added for issue #984 — custom hands installed via `openfang hand + /// Added for issue #984 — custom hands installed via `omtae hand /// install ` were only held in memory and lost on daemon restart. pub fn load_workspace_hands(&self, hands_dir: &std::path::Path) -> HandResult { if !hands_dir.exists() { @@ -266,10 +266,10 @@ impl HandRegistry { // session. On next restart, `load_workspace_hands` will pick it up // from disk. if let Some(home) = dirs::home_dir() { - let dest_dir = home.join(".openfang").join("hands").join(&def.id); + let dest_dir = home.join(".omtae").join("hands").join(&def.id); // Canonicalize both paths before comparing so we don't re-copy a // hand that is already being installed from its persistent - // location (e.g. `openfang hand install ~/.openfang/hands/foo`). + // location (e.g. `omtae hand install ~/.omtae/hands/foo`). let same_path = match (path.canonicalize(), dest_dir.canonicalize()) { (Ok(a), Ok(b)) => a == b, _ => path == dest_dir, @@ -1309,7 +1309,7 @@ system_prompt = "v2 — schedule changed." /// Integration test for issue #809: `hand config` round-trip. /// - /// Simulates what `openfang hand config --set KEY=VAL` does against + /// Simulates what `omtae hand config --set KEY=VAL` does against /// the registry: read current config, merge updates, write back, read /// again. Persists to a tempdir so the restart path also sees the change. #[test] @@ -1325,7 +1325,7 @@ system_prompt = "v2 — schedule changed." ); cfg.insert( "user_agent".to_string(), - serde_json::Value::String("openfang/1".into()), + serde_json::Value::String("omtae/1".into()), ); reg.update_config(inst.instance_id, cfg.clone()).unwrap(); @@ -1336,7 +1336,7 @@ system_prompt = "v2 — schedule changed." ); assert_eq!( after.config.get("user_agent"), - Some(&serde_json::Value::String("openfang/1".into())) + Some(&serde_json::Value::String("omtae/1".into())) ); // --unset path: drop a key and confirm it's gone. diff --git a/crates/openfang-kernel/Cargo.toml b/crates/openfang-kernel/Cargo.toml index 851cbfa4d4..ca2f25e5b8 100644 --- a/crates/openfang-kernel/Cargo.toml +++ b/crates/openfang-kernel/Cargo.toml @@ -1,19 +1,19 @@ [package] -name = "openfang-kernel" +name = "omtae-kernel" version.workspace = true edition.workspace = true license.workspace = true -description = "Core kernel for the OpenFang Agent OS" +description = "Core kernel for the OMTAE Agent OS" [dependencies] -openfang-types = { path = "../openfang-types" } -openfang-memory = { path = "../openfang-memory" } -openfang-runtime = { path = "../openfang-runtime" } -openfang-skills = { path = "../openfang-skills" } -openfang-hands = { path = "../openfang-hands" } -openfang-extensions = { path = "../openfang-extensions" } -openfang-wire = { path = "../openfang-wire" } -openfang-channels = { path = "../openfang-channels" } +omtae-types = { path = "../omtae-types" } +omtae-memory = { path = "../omtae-memory" } +omtae-runtime = { path = "../omtae-runtime" } +omtae-skills = { path = "../omtae-skills" } +omtae-hands = { path = "../omtae-hands" } +omtae-extensions = { path = "../omtae-extensions" } +omtae-wire = { path = "../omtae-wire" } +omtae-channels = { path = "../omtae-channels" } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/openfang-kernel/src/approval.rs b/crates/openfang-kernel/src/approval.rs index 09be0360ce..4825918072 100644 --- a/crates/openfang-kernel/src/approval.rs +++ b/crates/openfang-kernel/src/approval.rs @@ -2,7 +2,7 @@ use chrono::Utc; use dashmap::DashMap; -use openfang_types::approval::{ +use omtae_types::approval::{ ApprovalDecision, ApprovalPolicy, ApprovalRequest, ApprovalResponse, RiskLevel, }; use std::collections::VecDeque; @@ -194,7 +194,7 @@ impl ApprovalManager { #[cfg(test)] mod tests { use super::*; - use openfang_types::approval::ApprovalPolicy; + use omtae_types::approval::ApprovalPolicy; use std::sync::Arc; fn default_manager() -> ApprovalManager { diff --git a/crates/openfang-kernel/src/auth.rs b/crates/openfang-kernel/src/auth.rs index 6bc4a538c2..5a58ded3fe 100644 --- a/crates/openfang-kernel/src/auth.rs +++ b/crates/openfang-kernel/src/auth.rs @@ -1,12 +1,12 @@ //! RBAC authentication and authorization for multi-user access control. //! //! The AuthManager maps platform user identities (Telegram ID, Discord ID, etc.) -//! to OpenFang users with roles, then enforces permission checks on actions. +//! to OMTAE users with roles, then enforces permission checks on actions. use dashmap::DashMap; -use openfang_types::agent::UserId; -use openfang_types::config::UserConfig; -use openfang_types::error::{OpenFangError, OpenFangResult}; +use omtae_types::agent::UserId; +use omtae_types::config::UserConfig; +use omtae_types::error::{OMTAEError, OMTAEResult}; use std::fmt; use tracing::info; @@ -86,7 +86,7 @@ impl Action { /// A resolved user identity. #[derive(Debug, Clone)] pub struct UserIdentity { - /// OpenFang user ID. + /// OMTAE user ID. pub id: UserId, /// Display name. pub name: String, @@ -96,7 +96,7 @@ pub struct UserIdentity { /// RBAC authentication and authorization manager. pub struct AuthManager { - /// Known users by their OpenFang user ID. + /// Known users by their OMTAE user ID. users: DashMap, /// Channel binding index: "channel_type:platform_id" → UserId. channel_index: DashMap, @@ -140,7 +140,7 @@ impl AuthManager { /// Identify a user from a channel identity. /// - /// Returns the OpenFang UserId if a matching channel binding exists, + /// Returns the OMTAE UserId if a matching channel binding exists, /// or None for unrecognized users. pub fn identify(&self, channel_type: &str, platform_id: &str) -> Option { let key = format!("{channel_type}:{platform_id}"); @@ -155,17 +155,17 @@ impl AuthManager { /// Authorize a user for an action. /// /// Returns Ok(()) if the user has sufficient permissions, or AuthDenied error. - pub fn authorize(&self, user_id: UserId, action: &Action) -> OpenFangResult<()> { + pub fn authorize(&self, user_id: UserId, action: &Action) -> OMTAEResult<()> { let identity = self .users .get(&user_id) - .ok_or_else(|| OpenFangError::AuthDenied("Unknown user".to_string()))?; + .ok_or_else(|| OMTAEError::AuthDenied("Unknown user".to_string()))?; let required = action.required_role(); if identity.role >= required { Ok(()) } else { - Err(OpenFangError::AuthDenied(format!( + Err(OMTAEError::AuthDenied(format!( "User '{}' (role: {}) lacks permission for {:?} (requires: {})", identity.name, identity.role, action, required ))) diff --git a/crates/openfang-kernel/src/auto_reply.rs b/crates/openfang-kernel/src/auto_reply.rs index e80d09f7fa..91a3aaf978 100644 --- a/crates/openfang-kernel/src/auto_reply.rs +++ b/crates/openfang-kernel/src/auto_reply.rs @@ -1,7 +1,7 @@ //! Auto-reply background engine — trigger-driven background replies with concurrency control. -use openfang_types::agent::AgentId; -use openfang_types::config::AutoReplyConfig; +use omtae_types::agent::AgentId; +use omtae_types::config::AutoReplyConfig; use std::sync::Arc; use tokio::sync::Semaphore; use tracing::{debug, info, warn}; @@ -63,7 +63,7 @@ impl AutoReplyEngine { /// The `send_fn` is called with the agent response to deliver it back to the channel. pub async fn execute_reply( &self, - kernel_handle: Arc, + kernel_handle: Arc, agent_id: AgentId, message: String, reply_channel: AutoReplyChannel, @@ -100,7 +100,7 @@ impl AutoReplyEngine { let result = tokio::time::timeout( std::time::Duration::from_secs(timeout_secs), - kernel_handle.send_to_agent(&agent_id.to_string(), &message), + kernel_handle.send_to_agent(&agent_id.to_string(), &message, None, None), ) .await; diff --git a/crates/openfang-kernel/src/background.rs b/crates/openfang-kernel/src/background.rs index f2ec8b3baf..448327ef5d 100644 --- a/crates/openfang-kernel/src/background.rs +++ b/crates/openfang-kernel/src/background.rs @@ -7,7 +7,7 @@ use crate::triggers::TriggerPattern; use dashmap::DashMap; -use openfang_types::agent::{AgentId, ScheduleMode}; +use omtae_types::agent::{AgentId, ScheduleMode}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tokio::sync::watch; @@ -21,6 +21,8 @@ const MAX_CONCURRENT_BG_LLM: usize = 5; pub struct BackgroundExecutor { /// Running background task handles, keyed by agent ID. tasks: DashMap>, + /// Agents whose background loop is paused (user clicked Stop or `/stop`). + paused: Arc>, /// Shutdown signal receiver (from Supervisor). shutdown_rx: watch::Receiver, /// SECURITY: Global semaphore to limit concurrent background LLM calls. @@ -32,11 +34,30 @@ impl BackgroundExecutor { pub fn new(shutdown_rx: watch::Receiver) -> Self { Self { tasks: DashMap::new(), + paused: Arc::new(DashMap::new()), shutdown_rx, llm_semaphore: Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_BG_LLM)), } } + /// Returns true when continuous/periodic ticks are paused for this agent. + pub fn is_paused(&self, agent_id: AgentId) -> bool { + self.paused.contains_key(&agent_id) + } + + /// Pause background ticks and abort any running loop task. + pub fn pause_agent(&self, agent_id: AgentId) { + self.paused.insert(agent_id, ()); + self.stop_agent(agent_id); + info!(id = %agent_id, "Background loop paused"); + } + + /// Resume background ticks (clears pause flag; caller must restart the loop). + pub fn resume_agent(&self, agent_id: AgentId) { + self.paused.remove(&agent_id); + info!(id = %agent_id, "Background loop pause cleared"); + } + /// Start a background loop for an agent based on its schedule mode. /// /// For `Continuous` and `Periodic` modes, spawns a tokio task that @@ -44,7 +65,7 @@ impl BackgroundExecutor { /// For `Proactive` mode, registers triggers — no dedicated task needed. /// /// `send_message` is a closure that sends a message to the given agent - /// and returns a result. It captures an `Arc` from the caller. + /// and returns a result. It captures an `Arc` from the caller. pub fn start_agent( &self, agent_id: AgentId, @@ -64,6 +85,7 @@ impl BackgroundExecutor { let mut shutdown = self.shutdown_rx.clone(); let busy = Arc::new(AtomicBool::new(false)); let semaphore = self.llm_semaphore.clone(); + let pause_map = Arc::clone(&self.paused); info!( agent = %name, id = %agent_id, @@ -81,6 +103,11 @@ impl BackgroundExecutor { } } + if pause_map.contains_key(&agent_id) { + debug!(agent = %name, "Continuous loop: paused — skipping tick"); + continue; + } + // Skip if previous tick is still running if busy .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) @@ -101,8 +128,10 @@ impl BackgroundExecutor { let prompt = format!( "[AUTONOMOUS TICK] You are running in continuous mode. \ - Check your goals, review shared memory for pending tasks, \ - and take any necessary actions. Agent: {name}" + Check your goals and shared memory for pending tasks. \ + If nothing needs action, respond with exactly NO_REPLY. \ + Do not spawn agents or delegate unless a task is explicitly pending. \ + Agent: {name}" ); debug!(agent = %name, "Continuous loop: sending self-prompt"); let busy_clone = busy.clone(); @@ -126,6 +155,7 @@ impl BackgroundExecutor { let mut shutdown = self.shutdown_rx.clone(); let busy = Arc::new(AtomicBool::new(false)); let semaphore = self.llm_semaphore.clone(); + let pause_map = Arc::clone(&self.paused); info!( agent = %name, id = %agent_id, @@ -143,6 +173,11 @@ impl BackgroundExecutor { } } + if pause_map.contains_key(&agent_id) { + debug!(agent = %name, "Periodic loop: paused — skipping tick"); + continue; + } + if busy .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) .is_err() @@ -429,6 +464,18 @@ mod tests { executor.stop_agent(agent_id); } + #[test] + fn test_pause_and_resume() { + let (_tx, rx) = watch::channel(false); + let executor = BackgroundExecutor::new(rx); + let id = AgentId::new(); + assert!(!executor.is_paused(id)); + executor.pause_agent(id); + assert!(executor.is_paused(id)); + executor.resume_agent(id); + assert!(!executor.is_paused(id)); + } + #[test] fn test_executor_active_count() { let (_tx, rx) = watch::channel(false); diff --git a/crates/openfang-kernel/src/capabilities.rs b/crates/openfang-kernel/src/capabilities.rs index f2cd76d9bf..411c6f0376 100644 --- a/crates/openfang-kernel/src/capabilities.rs +++ b/crates/openfang-kernel/src/capabilities.rs @@ -1,8 +1,8 @@ //! Capability manager — enforces capability-based security. use dashmap::DashMap; -use openfang_types::agent::AgentId; -use openfang_types::capability::{capability_matches, Capability, CapabilityCheck}; +use omtae_types::agent::AgentId; +use omtae_types::capability::{capability_matches, Capability, CapabilityCheck}; use tracing::debug; /// Manages capability grants for all agents. diff --git a/crates/openfang-kernel/src/config.rs b/crates/openfang-kernel/src/config.rs index 8e1b66b036..633fb9a2a0 100644 --- a/crates/openfang-kernel/src/config.rs +++ b/crates/openfang-kernel/src/config.rs @@ -1,9 +1,9 @@ -//! Configuration loading from `~/.openfang/config.toml` with defaults. +//! Configuration loading from `~/.omtae/config.toml` with defaults. //! //! Supports config includes: the `include` field specifies additional TOML files //! to load and deep-merge before the root config (root overrides includes). -use openfang_types::config::KernelConfig; +use omtae_types::config::KernelConfig; use std::collections::HashSet; use std::path::{Path, PathBuf}; use tracing::info; @@ -278,7 +278,7 @@ pub fn deep_merge_toml(base: &mut toml::Value, overlay: &toml::Value) { /// /// No-op if `root_value` is not a table or has no `bindings` array. fn lenient_extract_bindings(root_value: &mut toml::Value) { - use openfang_types::config::AgentBinding; + use omtae_types::config::AgentBinding; let tbl = match root_value { toml::Value::Table(t) => t, @@ -341,21 +341,21 @@ fn lenient_extract_bindings(root_value: &mut toml::Value) { /// Get the default config file path. /// -/// Respects `OPENFANG_HOME` env var (e.g. `OPENFANG_HOME=/opt/openfang`). +/// Respects `OPENFANG_HOME` env var (e.g. `OPENFANG_HOME=/opt/omtae`). pub fn default_config_path() -> PathBuf { - openfang_home().join("config.toml") + omtae_home().join("config.toml") } -/// Get the OpenFang home directory. +/// Get the OMTAE home directory. /// -/// Priority: `OPENFANG_HOME` env var > `~/.openfang`. -pub fn openfang_home() -> PathBuf { +/// Priority: `OPENFANG_HOME` env var > `~/.omtae`. +pub fn omtae_home() -> PathBuf { if let Ok(home) = std::env::var("OPENFANG_HOME") { return PathBuf::from(home); } dirs::home_dir() .unwrap_or_else(std::env::temp_dir) - .join(".openfang") + .join(".omtae") } #[cfg(test)] @@ -757,6 +757,35 @@ match_rule = {{ channel = "discord", channel_id = "2" }} assert_eq!(config.bindings[0].agent, "good"); } + #[test] + fn test_watchdog_manual_allowlist_deserializes() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("config.toml"); + let mut f = std::fs::File::create(&path).unwrap(); + writeln!( + f, + r#" +log_level = "info" + +[agents] +autospawn = ["coder"] + +[watchdog] +kill_disallowed = false +manual_allowlist = ["orchestrator", "browser-hand"] +"# + ) + .unwrap(); + drop(f); + + let config = load_config(Some(&path)); + assert_eq!( + config.watchdog.manual_allowlist, + vec!["orchestrator".to_string(), "browser-hand".to_string()] + ); + assert!(!config.watchdog.kill_disallowed); + } + #[test] fn test_no_includes_works() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/openfang-kernel/src/config_reload.rs b/crates/openfang-kernel/src/config_reload.rs index 833876bd03..7575731a69 100644 --- a/crates/openfang-kernel/src/config_reload.rs +++ b/crates/openfang-kernel/src/config_reload.rs @@ -7,7 +7,7 @@ //! //! **Restart required**: api_listen, api_key, network, memory. -use openfang_types::config::{KernelConfig, ReloadMode}; +use omtae_types::config::{KernelConfig, ReloadMode}; use tracing::{info, warn}; // --------------------------------------------------------------------------- @@ -326,7 +326,7 @@ pub fn should_apply_hot(mode: ReloadMode, plan: &ReloadPlan) -> bool { #[cfg(test)] mod tests { use super::*; - use openfang_types::config::KernelConfig; + use omtae_types::config::KernelConfig; /// Helper: create a default config for diffing. fn default_cfg() -> KernelConfig { @@ -432,7 +432,7 @@ mod tests { let a = default_cfg(); let mut b = default_cfg(); // Change the channels config by adding a Telegram config - b.channels.telegram = Some(openfang_types::config::TelegramConfig { + b.channels.telegram = Some(omtae_types::config::TelegramConfig { bot_token_env: "TG_TOKEN".to_string(), ..Default::default() }); @@ -443,7 +443,7 @@ mod tests { #[test] fn test_usage_footer_hot_reload() { - use openfang_types::config::UsageFooterMode; + use omtae_types::config::UsageFooterMode; let a = default_cfg(); let mut b = default_cfg(); b.usage_footer = UsageFooterMode::Off; @@ -507,7 +507,7 @@ mod tests { /// operators have no live tuning knob for their non-default driver. #[test] fn test_fallback_providers_subprocess_timeout_hot_reload() { - use openfang_types::config::FallbackProviderConfig; + use omtae_types::config::FallbackProviderConfig; let mut a = default_cfg(); let mut b = default_cfg(); a.fallback_providers.push(FallbackProviderConfig { @@ -539,7 +539,7 @@ mod tests { /// emits the hot-action so the new provider is picked up without bounce. #[test] fn test_fallback_providers_add_entry_hot_reload() { - use openfang_types::config::FallbackProviderConfig; + use omtae_types::config::FallbackProviderConfig; let a = default_cfg(); let mut b = default_cfg(); b.fallback_providers.push(FallbackProviderConfig { @@ -562,7 +562,7 @@ mod tests { #[test] fn test_mixed_changes() { - use openfang_types::config::UsageFooterMode; + use omtae_types::config::UsageFooterMode; let a = default_cfg(); let mut b = default_cfg(); // Restart-required @@ -589,7 +589,7 @@ mod tests { #[test] fn test_noop_changes() { - use openfang_types::config::KernelMode; + use omtae_types::config::KernelMode; let a = default_cfg(); let mut b = default_cfg(); b.log_level = "debug".to_string(); diff --git a/crates/openfang-kernel/src/cron.rs b/crates/openfang-kernel/src/cron.rs index 495277356f..e1fb258479 100644 --- a/crates/openfang-kernel/src/cron.rs +++ b/crates/openfang-kernel/src/cron.rs @@ -1,4 +1,4 @@ -//! Cron job scheduler engine for the OpenFang kernel. +//! Cron job scheduler engine for the OMTAE kernel. //! //! Manages scheduled jobs (recurring and one-shot) across all agents. //! This is separate from `scheduler.rs` which handles agent resource tracking. @@ -9,9 +9,9 @@ use chrono::{Duration, Utc}; use dashmap::DashMap; -use openfang_types::agent::AgentId; -use openfang_types::error::{OpenFangError, OpenFangResult}; -use openfang_types::scheduler::{CronJob, CronJobId, CronSchedule}; +use omtae_types::agent::AgentId; +use omtae_types::error::{OMTAEError, OMTAEResult}; +use omtae_types::scheduler::{CronJob, CronJobId, CronSchedule}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -35,7 +35,7 @@ pub enum ClaimError { /// Runtime metadata for a cron job that extends the base `CronJob` type. /// -/// The `CronJob` struct in `openfang-types` is intentionally lean (no +/// The `CronJob` struct in `omtae-types` is intentionally lean (no /// `one_shot`, `last_status`, or error tracking). The scheduler tracks /// these operational details separately. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -84,7 +84,7 @@ pub struct CronScheduler { impl CronScheduler { /// Create a new scheduler. /// - /// `home_dir` is the OpenFang data directory; jobs are persisted to + /// `home_dir` is the OMTAE data directory; jobs are persisted to /// `/cron_jobs.json`. `max_total_jobs` caps the total number /// of jobs across all agents. pub fn new(home_dir: &Path, max_total_jobs: usize) -> Self { @@ -106,14 +106,14 @@ impl CronScheduler { /// /// Returns the number of jobs loaded. If the persistence file does not /// exist, returns `Ok(0)` without error. - pub fn load(&self) -> OpenFangResult { + pub fn load(&self) -> OMTAEResult { if !self.persist_path.exists() { return Ok(0); } let data = std::fs::read_to_string(&self.persist_path) - .map_err(|e| OpenFangError::Internal(format!("Failed to read cron jobs: {e}")))?; + .map_err(|e| OMTAEError::Internal(format!("Failed to read cron jobs: {e}")))?; let metas: Vec = serde_json::from_str(&data) - .map_err(|e| OpenFangError::Internal(format!("Failed to parse cron jobs: {e}")))?; + .map_err(|e| OMTAEError::Internal(format!("Failed to parse cron jobs: {e}")))?; let count = metas.len(); for meta in metas { self.jobs.insert(meta.job.id, meta); @@ -123,16 +123,16 @@ impl CronScheduler { } /// Persist all jobs to disk via atomic write (write to `.tmp`, then rename). - pub fn persist(&self) -> OpenFangResult<()> { + pub fn persist(&self) -> OMTAEResult<()> { let metas: Vec = self.jobs.iter().map(|r| r.value().clone()).collect(); let data = serde_json::to_string_pretty(&metas) - .map_err(|e| OpenFangError::Internal(format!("Failed to serialize cron jobs: {e}")))?; + .map_err(|e| OMTAEError::Internal(format!("Failed to serialize cron jobs: {e}")))?; let tmp_path = self.persist_path.with_extension("json.tmp"); std::fs::write(&tmp_path, data.as_bytes()).map_err(|e| { - OpenFangError::Internal(format!("Failed to write cron jobs temp file: {e}")) + OMTAEError::Internal(format!("Failed to write cron jobs temp file: {e}")) })?; std::fs::rename(&tmp_path, &self.persist_path).map_err(|e| { - OpenFangError::Internal(format!("Failed to rename cron jobs file: {e}")) + OMTAEError::Internal(format!("Failed to rename cron jobs file: {e}")) })?; debug!(count = metas.len(), "Persisted cron jobs"); Ok(()) @@ -145,11 +145,11 @@ impl CronScheduler { /// /// `one_shot` controls whether the job is removed after a single /// successful execution. - pub fn add_job(&self, mut job: CronJob, one_shot: bool) -> OpenFangResult { + pub fn add_job(&self, mut job: CronJob, one_shot: bool) -> OMTAEResult { // Global limit let max_jobs = self.max_total_jobs.load(Ordering::Relaxed); if self.jobs.len() >= max_jobs { - return Err(OpenFangError::Internal(format!( + return Err(OMTAEError::Internal(format!( "Global cron job limit reached ({})", max_jobs ))); @@ -164,7 +164,7 @@ impl CronScheduler { // CronJob.validate returns Result<(), String> job.validate(agent_count) - .map_err(OpenFangError::InvalidInput)?; + .map_err(OMTAEError::InvalidInput)?; // Compute initial next_run job.next_run = Some(compute_next_run(&job.schedule)); @@ -175,16 +175,16 @@ impl CronScheduler { } /// Remove a job by ID. Returns the removed `CronJob`. - pub fn remove_job(&self, id: CronJobId) -> OpenFangResult { + pub fn remove_job(&self, id: CronJobId) -> OMTAEResult { self.jobs .remove(&id) .map(|(_, meta)| meta.job) - .ok_or_else(|| OpenFangError::Internal(format!("Cron job {id} not found"))) + .ok_or_else(|| OMTAEError::Internal(format!("Cron job {id} not found"))) } /// Enable or disable a job. Re-enabling resets errors and recomputes /// `next_run`. - pub fn set_enabled(&self, id: CronJobId, enabled: bool) -> OpenFangResult<()> { + pub fn set_enabled(&self, id: CronJobId, enabled: bool) -> OMTAEResult<()> { match self.jobs.get_mut(&id) { Some(mut meta) => { meta.job.enabled = enabled; @@ -194,7 +194,7 @@ impl CronScheduler { } Ok(()) } - None => Err(OpenFangError::Internal(format!("Cron job {id} not found"))), + None => Err(OMTAEError::Internal(format!("Cron job {id} not found"))), } } @@ -206,14 +206,14 @@ impl CronScheduler { pub fn set_delivery_targets( &self, id: CronJobId, - targets: Vec, - ) -> OpenFangResult<()> { + targets: Vec, + ) -> OMTAEResult<()> { match self.jobs.get_mut(&id) { Some(mut meta) => { meta.job.delivery_targets = targets; Ok(()) } - None => Err(OpenFangError::Internal(format!("Cron job {id} not found"))), + None => Err(OMTAEError::Internal(format!("Cron job {id} not found"))), } } @@ -395,7 +395,7 @@ impl CronScheduler { meta.job.last_run = Some(Utc::now()); meta.last_status = Some(format!( "error: {}", - openfang_types::truncate_str(error_msg, 256) + omtae_types::truncate_str(error_msg, 256) )); meta.consecutive_errors += 1; if meta.consecutive_errors >= MAX_CONSECUTIVE_ERRORS { @@ -509,7 +509,7 @@ pub fn compute_next_run_after( mod tests { use super::*; use chrono::{Duration, Timelike}; - use openfang_types::scheduler::{CronAction, CronDelivery}; + use omtae_types::scheduler::{CronAction, CronDelivery}; /// Build a minimal valid `CronJob` with an `Every` schedule. fn make_job(agent_id: AgentId) -> CronJob { @@ -610,7 +610,7 @@ mod tests { #[test] fn test_add_job_per_agent_limit() { - // MAX_JOBS_PER_AGENT = 50 in openfang-types + // MAX_JOBS_PER_AGENT = 50 in omtae-types let (sched, _tmp) = make_scheduler(1000); let agent = AgentId::new(); diff --git a/crates/openfang-kernel/src/cron_delivery.rs b/crates/openfang-kernel/src/cron_delivery.rs index 5d079a1088..b3b4b6eace 100644 --- a/crates/openfang-kernel/src/cron_delivery.rs +++ b/crates/openfang-kernel/src/cron_delivery.rs @@ -6,12 +6,12 @@ //! concurrently. Failures in one target do not abort delivery to the //! others — every target's outcome is returned in a [`DeliveryResult`]. //! -//! This is the OpenFang port of the Hermes Agent multi-destination cron +//! This is the OMTAE port of the Hermes Agent multi-destination cron //! pattern: one job → N destinations (channels / webhooks / files / email). use futures::future::join_all; -use openfang_channels::bridge::ChannelBridgeHandle; -use openfang_types::scheduler::CronDeliveryTarget; +use omtae_channels::bridge::ChannelBridgeHandle; +use omtae_types::scheduler::CronDeliveryTarget; use serde::{Deserialize, Serialize}; use std::path::Path; use std::sync::Arc; @@ -276,8 +276,8 @@ async fn deliver_local_file(path: &Path, append: bool, output: &str) -> Result<( mod tests { use super::*; use async_trait::async_trait; - use openfang_channels::bridge::ChannelBridgeHandle; - use openfang_types::agent::AgentId; + use omtae_channels::bridge::ChannelBridgeHandle; + use omtae_types::agent::AgentId; use std::sync::Mutex; /// Mock bridge that records every channel send. Optionally fails for diff --git a/crates/openfang-kernel/src/drift_guard.rs b/crates/openfang-kernel/src/drift_guard.rs new file mode 100644 index 0000000000..1978b2d2ef --- /dev/null +++ b/crates/openfang-kernel/src/drift_guard.rs @@ -0,0 +1,507 @@ +//! Runtime drift detection and safe auto-remediation. +//! +//! Runs on daemon boot and on a periodic interval (see `[watchdog]` in config.toml). +//! Only applies fixes that are safe without user confirmation: autospawn sync, +//! optionally stopping non-allowlisted agents (only when `[watchdog] kill_disallowed = true`), +//! clearing stuck LLM runs. +//! +//! Allowlist and model checks are report-only by default — they never kill agents or +//! block the dashboard. + +use crate::kernel::OMTAEKernel; +use chrono::Utc; +use omtae_types::agent::{AgentEntry, AgentManifest, AgentState, ScheduleMode}; +use omtae_types::config::{DefaultModelConfig, WatchdogConfig}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use tracing::{info, warn}; + +/// One drift finding from a check. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DriftFinding { + pub check: String, + pub severity: String, + pub message: String, + #[serde(default)] + pub remediated: bool, +} + +/// Result of a drift scan (GET `/api/system/drift` or periodic task). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DriftReport { + pub ok: bool, + pub checked_at: String, + pub findings: Vec, + pub fixes_applied: Vec, + /// Echo of `[watchdog] manual_allowlist` so the dashboard can hide expected on-demand agents. + #[serde(default)] + pub manual_allowlist: Vec, +} + +fn now_epoch_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +/// Normalize agent names for allowlist comparisons (trim + lowercase). +fn normalize_agent_name(name: &str) -> String { + name.trim().to_lowercase() +} + +/// Build a set of `[watchdog] manual_allowlist` entries (on-demand agents, never drift). +fn manual_allowlist_set(names: &[String]) -> HashSet { + names.iter().map(|s| normalize_agent_name(s)).collect() +} + +fn is_manual_allowlisted(agent_name: &str, manual: &HashSet) -> bool { + manual.contains(&normalize_agent_name(agent_name)) +} + +/// Resolve manifest "default" placeholders the same way the agents API does. +fn resolve_agent_model<'a>(entry: &'a AgentEntry, dm: &'a DefaultModelConfig) -> (&'a str, &'a str) { + let provider = + if entry.manifest.model.provider.is_empty() || entry.manifest.model.provider == "default" { + dm.provider.as_str() + } else { + entry.manifest.model.provider.as_str() + }; + let model = if entry.manifest.model.model.is_empty() || entry.manifest.model.model == "default" { + dm.model.as_str() + } else { + entry.manifest.model.model.as_str() + }; + (provider, model) +} + +/// Drop remediated rows; `ok` is true when no warn/error findings remain. +fn finalize_drift_report( + findings: Vec, + fixes: Vec, + manual_allowlist: Vec, +) -> DriftReport { + let findings: Vec<_> = findings + .into_iter() + .filter(|f| !f.remediated) + .collect(); + let ok = findings + .iter() + .all(|f| f.severity == "info" || f.severity == "debug"); + DriftReport { + ok, + checked_at: Utc::now().to_rfc3339(), + findings, + fixes_applied: fixes, + manual_allowlist, + } +} + +impl OMTAEKernel { + /// Scan runtime state for drift from config; optionally apply safe fixes. + pub fn run_drift_check(&self, remediate: bool) -> DriftReport { + let cfg = &self.config.watchdog; + let manual_allowlist = cfg.manual_allowlist.clone(); + let mut findings = Vec::new(); + let mut fixes = Vec::new(); + + self.clean_stale_running_task_timestamps(); + + if cfg.checks.iter().any(|c| c == "autospawn" || c == "allowlist") { + let kill_disallowed = remediate && cfg.kill_disallowed; + self.check_autospawn(remediate, kill_disallowed, &mut findings, &mut fixes); + } + + if cfg.checks.iter().any(|c| c == "stuck_run") { + self.check_stuck_runs(cfg.stuck_run_secs, remediate, &mut findings, &mut fixes); + } + + if cfg.checks.iter().any(|c| c == "continuous") { + self.check_continuous_stuck(cfg.stuck_run_secs, remediate, &mut findings, &mut fixes); + } + + if cfg.checks.iter().any(|c| c == "model") { + let patterns = self.expected_model_patterns(cfg); + if !patterns.is_empty() { + self.check_model_drift(&patterns, &mut findings); + } + } + + finalize_drift_report(findings, fixes, manual_allowlist) + } + + /// Remove timestamps left behind when a run task finished without clearing the map. + fn clean_stale_running_task_timestamps(&self) { + let stale: Vec<_> = self + .running_task_started + .iter() + .filter_map(|e| { + let id = *e.key(); + if self.running_tasks.contains_key(&id) { + None + } else { + Some(id) + } + }) + .collect(); + for id in stale { + self.running_task_started.remove(&id); + } + } + + fn expected_model_patterns(&self, cfg: &WatchdogConfig) -> Vec { + if !cfg.expected_model_substrings.is_empty() { + return cfg + .expected_model_substrings + .iter() + .map(|s| s.to_lowercase()) + .collect(); + } + let dm = &self.config.default_model; + let mut patterns = Vec::new(); + if !dm.provider.is_empty() && dm.provider != "default" { + patterns.push(dm.provider.to_lowercase()); + } + if !dm.model.is_empty() && dm.model != "default" { + patterns.push(dm.model.to_lowercase()); + for token in dm.model.to_lowercase().split(['-', '_', '/', '.']) { + if token.len() >= 3 { + patterns.push(token.to_string()); + } + } + } + patterns.sort(); + patterns.dedup(); + patterns + } +} + +impl OMTAEKernel { + fn check_autospawn( + &self, + remediate: bool, + kill_disallowed: bool, + findings: &mut Vec, + fixes: &mut Vec, + ) { + let allowlist: Vec = self.config.agents.autospawn.clone(); + if allowlist.is_empty() { + return; + } + + let allow: HashSet = allowlist.iter().map(|s| normalize_agent_name(s)).collect(); + let manual = manual_allowlist_set(&self.config.watchdog.manual_allowlist); + let running: Vec<_> = self.registry.list(); + + for entry in &running { + if is_manual_allowlisted(&entry.name, &manual) { + continue; + } + if !allow.contains(&normalize_agent_name(&entry.name)) { + findings.push(DriftFinding { + check: "allowlist".into(), + severity: "warn".into(), + message: format!( + "Agent '{}' is running but not in [agents] autospawn (report-only; add to autospawn or manual_allowlist)", + entry.name + ), + remediated: false, + }); + if kill_disallowed && !is_manual_allowlisted(&entry.name, &manual) { + if self.kill_agent(entry.id).is_ok() { + fixes.push(format!("Stopped non-allowlisted agent '{}'", entry.name)); + if let Some(last) = findings.last_mut() { + last.remediated = true; + } + } + } + } + } + + let agents_dir = self.config.home_dir.join("agents"); + if !agents_dir.is_dir() { + return; + } + + for name in &allowlist { + if self.registry.find_by_name(name).is_some() { + continue; + } + let toml_path = agents_dir.join(name).join("agent.toml"); + if !toml_path.exists() { + findings.push(DriftFinding { + check: "autospawn".into(), + severity: "warn".into(), + message: format!( + "Autospawn agent '{}' is allowlisted but has no ~/.omtae/agents/{}/agent.toml", + name, name + ), + remediated: false, + }); + continue; + } + + findings.push(DriftFinding { + check: "autospawn".into(), + severity: "warn".into(), + message: format!("Autospawn agent '{}' is allowlisted but not running", name), + remediated: false, + }); + + if !remediate { + continue; + } + + let toml_str = match std::fs::read_to_string(&toml_path) { + Ok(s) => s, + Err(e) => { + warn!(agent = %name, "Drift autospawn: read failed: {e}"); + continue; + } + }; + let mut manifest: AgentManifest = match toml::from_str(&toml_str) { + Ok(m) => m, + Err(e) => { + warn!(agent = %name, "Drift autospawn: invalid manifest: {e}"); + continue; + } + }; + if manifest.name.is_empty() { + manifest.name = name.clone(); + } + match self.spawn_agent(manifest) { + Ok(id) => { + fixes.push(format!("Spawned missing autospawn agent '{}' ({})", name, id)); + if let Some(last) = findings.last_mut() { + last.remediated = true; + } + info!(agent = %name, "Drift guard spawned autospawn agent"); + } + Err(e) => { + warn!(agent = %name, "Drift autospawn spawn failed: {e}"); + } + } + } + } + + fn check_stuck_runs( + &self, + stuck_secs: u64, + remediate: bool, + findings: &mut Vec, + fixes: &mut Vec, + ) { + let now = now_epoch_secs(); + let stuck_ids: Vec<_> = self + .running_task_started + .iter() + .filter_map(|e| { + let agent_id = *e.key(); + if !self.running_tasks.contains_key(&agent_id) { + return None; + } + let started = *e.value(); + if now.saturating_sub(started) >= stuck_secs { + Some(agent_id) + } else { + None + } + }) + .collect(); + + for agent_id in stuck_ids { + let name = self + .registry + .get(agent_id) + .map(|e| e.name.clone()) + .unwrap_or_else(|| agent_id.to_string()); + findings.push(DriftFinding { + check: "stuck_run".into(), + severity: "error".into(), + message: format!( + "Agent '{}' LLM run exceeded {}s while inferencing", + name, stuck_secs + ), + remediated: false, + }); + if remediate { + if self.stop_agent_run(agent_id).unwrap_or(false) { + fixes.push(format!("Cancelled stuck run for '{}'", name)); + if let Some(last) = findings.last_mut() { + last.remediated = true; + } + } + } + } + } + + fn check_continuous_stuck( + &self, + stuck_secs: u64, + remediate: bool, + findings: &mut Vec, + fixes: &mut Vec, + ) { + let now = now_epoch_secs(); + for entry in self.registry.list() { + let is_continuous = matches!( + entry.manifest.schedule, + ScheduleMode::Continuous { .. } | ScheduleMode::Periodic { .. } + ); + if !is_continuous { + continue; + } + if !self.running_tasks.contains_key(&entry.id) { + continue; + } + let started = self + .running_task_started + .get(&entry.id) + .map(|e| *e) + .unwrap_or(0); + if started == 0 || now.saturating_sub(started) < stuck_secs { + continue; + } + findings.push(DriftFinding { + check: "continuous".into(), + severity: "warn".into(), + message: format!( + "Agent '{}' on {:?} schedule has been inferencing >{}s", + entry.name, entry.manifest.schedule, stuck_secs + ), + remediated: false, + }); + if remediate { + let _ = self.stop_agent_run(entry.id); + self.background.pause_agent(entry.id); + fixes.push(format!( + "Stopped continuous/periodic runaway for '{}'", + entry.name + )); + if let Some(last) = findings.last_mut() { + last.remediated = true; + } + } + } + } + + fn check_model_drift(&self, expected_substrings: &[String], findings: &mut Vec) { + let dm = &self.config.default_model; + for entry in self.registry.list() { + if entry.state != AgentState::Running { + continue; + } + let (provider, model) = resolve_agent_model(&entry, dm); + let haystack = format!("{}/{}", provider.to_lowercase(), model.to_lowercase()); + let ok = expected_substrings + .iter() + .any(|s| haystack.contains(s.as_str())); + if !ok { + findings.push(DriftFinding { + check: "model".into(), + severity: "info".into(), + message: format!( + "Agent '{}' uses {}:{} (informational; does not match expected patterns {:?})", + entry.name, provider, model, expected_substrings + ), + remediated: false, + }); + } + } + } +} + +/// Start periodic drift checks (call once from daemon boot). +pub fn spawn_drift_watchdog(kernel: Arc) { + let cfg = kernel.config.watchdog.clone(); + if !cfg.enabled { + return; + } + + let interval = std::time::Duration::from_secs(cfg.interval_secs.max(60)); + + { + let k = kernel.clone(); + tokio::spawn(async move { + let report = k.run_drift_check(true); + if !report.ok { + info!( + findings = report.findings.len(), + fixes = report.fixes_applied.len(), + "Boot drift check completed" + ); + } + }); + } + + tokio::spawn(async move { + loop { + tokio::time::sleep(interval).await; + let report = kernel.run_drift_check(true); + if !report.ok { + warn!( + findings = ?report.findings.iter().map(|f| &f.message).collect::>(), + fixes = ?report.fixes_applied, + "Drift watchdog applied remediation" + ); + } + } + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn manual_allowlist_normalizes_names() { + let manual = manual_allowlist_set(&[" browser-hand ".into(), "Orchestrator".into()]); + assert!(is_manual_allowlisted("browser-hand", &manual)); + assert!(is_manual_allowlisted("Orchestrator", &manual)); + assert!(!is_manual_allowlisted("coder", &manual)); + } + + #[test] + fn finalize_drops_remediated_and_marks_ok() { + let report = finalize_drift_report( + vec![ + DriftFinding { + check: "autospawn".into(), + severity: "warn".into(), + message: "fixed".into(), + remediated: true, + }, + DriftFinding { + check: "model".into(), + severity: "info".into(), + message: "informational".into(), + remediated: false, + }, + ], + vec!["spawned".into()], + vec!["browser-hand".into()], + ); + assert!(report.ok); + assert_eq!(report.findings.len(), 1); + assert_eq!(report.findings[0].severity, "info"); + assert_eq!(report.manual_allowlist, vec!["browser-hand"]); + } + + #[test] + fn finalize_not_ok_when_warn_remains() { + let report = finalize_drift_report( + vec![DriftFinding { + check: "allowlist".into(), + severity: "warn".into(), + message: "still bad".into(), + remediated: false, + }], + vec![], + vec![], + ); + assert!(!report.ok); + assert_eq!(report.findings.len(), 1); + } +} diff --git a/crates/openfang-kernel/src/error.rs b/crates/openfang-kernel/src/error.rs index 1496c38f58..ed256df49c 100644 --- a/crates/openfang-kernel/src/error.rs +++ b/crates/openfang-kernel/src/error.rs @@ -1,14 +1,14 @@ //! Kernel-specific error types. -use openfang_types::error::OpenFangError; +use omtae_types::error::OMTAEError; use thiserror::Error; -/// Kernel error type wrapping OpenFangError with kernel-specific context. +/// Kernel error type wrapping OMTAEError with kernel-specific context. #[derive(Error, Debug)] pub enum KernelError { - /// A wrapped OpenFangError. + /// A wrapped OMTAEError. #[error(transparent)] - OpenFang(#[from] OpenFangError), + OMTAE(#[from] OMTAEError), /// The kernel failed to boot. #[error("Boot failed: {0}")] diff --git a/crates/openfang-kernel/src/event_bus.rs b/crates/openfang-kernel/src/event_bus.rs index 4324c2f0cf..02a844e2a4 100644 --- a/crates/openfang-kernel/src/event_bus.rs +++ b/crates/openfang-kernel/src/event_bus.rs @@ -1,8 +1,8 @@ //! Event bus — pub/sub with pattern matching and history ring buffer. use dashmap::DashMap; -use openfang_types::agent::AgentId; -use openfang_types::event::{Event, EventTarget}; +use omtae_types::agent::AgentId; +use omtae_types::event::{Event, EventTarget}; use std::collections::VecDeque; use std::sync::Arc; use tokio::sync::{broadcast, RwLock}; @@ -107,7 +107,7 @@ impl Default for EventBus { #[cfg(test)] mod tests { use super::*; - use openfang_types::event::{EventPayload, SystemEvent}; + use omtae_types::event::{EventPayload, SystemEvent}; #[tokio::test] async fn test_publish_and_history() { diff --git a/crates/openfang-kernel/src/heartbeat.rs b/crates/openfang-kernel/src/heartbeat.rs index 75554b1da6..e3ece729d4 100644 --- a/crates/openfang-kernel/src/heartbeat.rs +++ b/crates/openfang-kernel/src/heartbeat.rs @@ -12,7 +12,7 @@ use crate::registry::AgentRegistry; use chrono::Utc; use dashmap::DashMap; -use openfang_types::agent::{AgentEntry, AgentId, AgentState, ScheduleMode}; +use omtae_types::agent::{AgentEntry, AgentId, AgentState, ScheduleMode}; use tracing::{debug, warn}; /// Default heartbeat check interval (seconds). @@ -304,7 +304,7 @@ pub fn summarize(statuses: &[HeartbeatStatus]) -> HeartbeatSummary { mod tests { use super::*; use chrono::Duration; - use openfang_types::agent::*; + use omtae_types::agent::*; use std::collections::HashMap; /// Helper: build a minimal AgentEntry for heartbeat tests. diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 8f59414c97..bea7f2ba41 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -1,4 +1,4 @@ -//! OpenFangKernel — assembles all subsystems and provides the main API. +//! OMTAEKernel — assembles all subsystems and provides the main API. use crate::auth::AuthManager; use crate::background::{self, BackgroundExecutor}; @@ -13,34 +13,34 @@ use crate::supervisor::Supervisor; use crate::triggers::{TriggerEngine, TriggerId, TriggerPattern}; use crate::workflow::{StepAgent, Workflow, WorkflowEngine, WorkflowId, WorkflowRunId}; -use openfang_memory::MemorySubstrate; -use openfang_runtime::agent_loop::{ +use omtae_memory::MemorySubstrate; +use omtae_runtime::agent_loop::{ run_agent_loop, run_agent_loop_streaming, strip_provider_prefix, AgentLoopResult, }; -use openfang_runtime::audit::AuditLog; -use openfang_runtime::drivers; -use openfang_runtime::kernel_handle::{self, KernelHandle}; -use openfang_runtime::llm_driver::{ +use omtae_runtime::audit::AuditLog; +use omtae_runtime::drivers; +use omtae_runtime::kernel_handle::{self, KernelHandle}; +use omtae_runtime::llm_driver::{ CompletionRequest, CompletionResponse, DriverConfig, LlmDriver, LlmError, StreamEvent, }; -use openfang_runtime::python_runtime::{self, PythonConfig}; -use openfang_runtime::routing::ModelRouter; -use openfang_runtime::sandbox::{SandboxConfig, WasmSandbox}; -use openfang_runtime::tool_runner::builtin_tool_definitions; -use openfang_types::agent::*; -use openfang_types::capability::Capability; -use openfang_types::config::{KernelConfig, OutputFormat}; -use openfang_types::error::OpenFangError; -use openfang_types::event::*; -use openfang_types::memory::Memory; -use openfang_types::tool::ToolDefinition; +use omtae_runtime::python_runtime::{self, PythonConfig}; +use omtae_runtime::routing::ModelRouter; +use omtae_runtime::sandbox::{SandboxConfig, WasmSandbox}; +use omtae_runtime::tool_runner::builtin_tool_definitions; +use omtae_types::agent::*; +use omtae_types::capability::Capability; +use omtae_types::config::{KernelConfig, OutputFormat}; +use omtae_types::error::OMTAEError; +use omtae_types::event::*; +use omtae_types::memory::Memory; +use omtae_types::tool::ToolDefinition; use async_trait::async_trait; use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock, Weak}; use tracing::{debug, info, warn}; -/// The main OpenFang kernel — coordinates all subsystems. +/// The main OMTAE kernel — coordinates all subsystems. /// Stub LLM driver used when no providers are configured. /// Returns a helpful error so the dashboard still boots and users can configure providers. struct StubDriver; @@ -57,7 +57,7 @@ impl LlmDriver for StubDriver { } } -pub struct OpenFangKernel { +pub struct OMTAEKernel { /// Kernel configuration. pub config: KernelConfig, /// Agent registry. @@ -89,9 +89,9 @@ pub struct OpenFangKernel { /// RBAC authentication manager. pub auth: AuthManager, /// Model catalog registry (RwLock for auth status refresh from API). - pub model_catalog: std::sync::RwLock, + pub model_catalog: std::sync::RwLock, /// Skill registry for plugin skills (RwLock for hot-reload on install/uninstall). - pub skill_registry: std::sync::RwLock, + pub skill_registry: std::sync::RwLock, /// Per-skill config overrides applied on top of `self.config.skills`. /// /// Written by the API (`PUT /api/skills/{id}/config`) so the user's edits @@ -103,37 +103,39 @@ pub struct OpenFangKernel { >, /// Tracks running agent tasks for cancellation support. pub running_tasks: dashmap::DashMap, + /// Unix epoch seconds when each agent's current LLM task started. + pub running_task_started: dashmap::DashMap, /// MCP server connections (lazily initialized at start_background_agents). - pub mcp_connections: tokio::sync::Mutex>, + pub mcp_connections: tokio::sync::Mutex>, /// MCP tool definitions cache (populated after connections are established). pub mcp_tools: std::sync::Mutex>, /// A2A task store for tracking task lifecycle. - pub a2a_task_store: openfang_runtime::a2a::A2aTaskStore, + pub a2a_task_store: omtae_runtime::a2a::A2aTaskStore, /// Discovered external A2A agent cards. - pub a2a_external_agents: std::sync::Mutex>, + pub a2a_external_agents: std::sync::Mutex>, /// Web tools context (multi-provider search + SSRF-protected fetch + caching). - pub web_ctx: openfang_runtime::web_search::WebToolsContext, + pub web_ctx: omtae_runtime::web_search::WebToolsContext, /// Browser automation manager (Playwright bridge sessions). - pub browser_ctx: openfang_runtime::browser::BrowserManager, + pub browser_ctx: omtae_runtime::browser::BrowserManager, /// Media understanding engine (image description, audio transcription). - pub media_engine: openfang_runtime::media_understanding::MediaEngine, + pub media_engine: omtae_runtime::media_understanding::MediaEngine, /// Text-to-speech engine. - pub tts_engine: openfang_runtime::tts::TtsEngine, + pub tts_engine: omtae_runtime::tts::TtsEngine, /// Device pairing manager. pub pairing: crate::pairing::PairingManager, /// Embedding driver for vector similarity search (None = text fallback). pub embedding_driver: - Option>, + Option>, /// Hand registry — curated autonomous capability packages. - pub hand_registry: openfang_hands::registry::HandRegistry, + pub hand_registry: omtae_hands::registry::HandRegistry, /// Credential resolver — vault → dotenv → env var priority chain. - pub credential_resolver: std::sync::Mutex, + pub credential_resolver: std::sync::Mutex, /// Extension/integration registry (bundled MCP templates + install state). - pub extension_registry: std::sync::RwLock, + pub extension_registry: std::sync::RwLock, /// Integration health monitor. - pub extension_health: openfang_extensions::health::HealthMonitor, + pub extension_health: omtae_extensions::health::HealthMonitor, /// Effective MCP server list (manual config + extension-installed, merged at boot). - pub effective_mcp_servers: std::sync::RwLock>, + pub effective_mcp_servers: std::sync::RwLock>, /// Delivery receipt tracker (bounded LRU, max 10K entries). pub delivery_tracker: DeliveryTracker, /// Cron job scheduler. @@ -141,29 +143,29 @@ pub struct OpenFangKernel { /// Execution approval manager. pub approval_manager: crate::approval::ApprovalManager, /// Agent bindings for multi-account routing (Mutex for runtime add/remove). - pub bindings: std::sync::Mutex>, + pub bindings: std::sync::Mutex>, /// Broadcast configuration. - pub broadcast: openfang_types::config::BroadcastConfig, + pub broadcast: omtae_types::config::BroadcastConfig, /// Auto-reply engine. pub auto_reply_engine: crate::auto_reply::AutoReplyEngine, /// Plugin lifecycle hook registry. - pub hooks: openfang_runtime::hooks::HookRegistry, + pub hooks: omtae_runtime::hooks::HookRegistry, /// Persistent process manager for interactive sessions (REPLs, servers). - pub process_manager: Arc, + pub process_manager: Arc, /// OFP peer registry — tracks connected peers (OnceLock for safe init after Arc creation). - pub peer_registry: OnceLock, + pub peer_registry: OnceLock, /// OFP peer node — the local networking node (OnceLock for safe init after Arc creation). - pub peer_node: OnceLock>, + pub peer_node: OnceLock>, /// Boot timestamp for uptime calculation. pub booted_at: std::time::Instant, /// WhatsApp Web gateway child process PID (for shutdown cleanup). pub whatsapp_gateway_pid: Arc>>, /// Channel adapters registered at bridge startup (for proactive `channel_send` tool). pub channel_adapters: - dashmap::DashMap>, + dashmap::DashMap>, /// Hot-reloadable default model override (set via config hot-reload, read at agent spawn). pub default_model_override: - std::sync::RwLock>, + std::sync::RwLock>, /// Hot-reloadable fallback provider chain override. /// /// Set by `apply_hot_actions(ReloadFallbackProviders)` when @@ -173,19 +175,19 @@ pub struct OpenFangKernel { /// driver build without a daemon bounce. `None` means "fall back to the /// boot-time `self.config.fallback_providers`". (#1129) pub fallback_providers_override: - std::sync::RwLock>>, + std::sync::RwLock>>, /// Per-agent message locks — serializes LLM calls for the same agent to prevent /// session corruption when multiple messages arrive concurrently (e.g. rapid voice /// messages via Telegram). Different agents can still run in parallel. agent_msg_locks: dashmap::DashMap>>, /// Weak self-reference for trigger dispatch (set after Arc wrapping). - self_handle: OnceLock>, + self_handle: OnceLock>, } /// Bounded in-memory delivery receipt tracker. /// Stores up to `MAX_RECEIPTS` most recent delivery receipts per agent. pub struct DeliveryTracker { - receipts: dashmap::DashMap>, + receipts: dashmap::DashMap>, } impl Default for DeliveryTracker { @@ -206,7 +208,7 @@ impl DeliveryTracker { } /// Record a delivery receipt for an agent. - pub fn record(&self, agent_id: AgentId, receipt: openfang_channels::types::DeliveryReceipt) { + pub fn record(&self, agent_id: AgentId, receipt: omtae_channels::types::DeliveryReceipt) { let mut entry = self.receipts.entry(agent_id).or_default(); entry.push(receipt); // Per-agent cap @@ -232,7 +234,7 @@ impl DeliveryTracker { &self, agent_id: AgentId, limit: usize, - ) -> Vec { + ) -> Vec { self.receipts .get(&agent_id) .map(|entries| entries.iter().rev().take(limit).cloned().collect()) @@ -243,12 +245,12 @@ impl DeliveryTracker { pub fn sent_receipt( channel: &str, recipient: &str, - ) -> openfang_channels::types::DeliveryReceipt { - openfang_channels::types::DeliveryReceipt { + ) -> omtae_channels::types::DeliveryReceipt { + omtae_channels::types::DeliveryReceipt { message_id: uuid::Uuid::new_v4().to_string(), channel: channel.to_string(), recipient: Self::sanitize_recipient(recipient), - status: openfang_channels::types::DeliveryStatus::Sent, + status: omtae_channels::types::DeliveryStatus::Sent, timestamp: chrono::Utc::now(), error: None, } @@ -259,12 +261,12 @@ impl DeliveryTracker { channel: &str, recipient: &str, error: &str, - ) -> openfang_channels::types::DeliveryReceipt { - openfang_channels::types::DeliveryReceipt { + ) -> omtae_channels::types::DeliveryReceipt { + omtae_channels::types::DeliveryReceipt { message_id: uuid::Uuid::new_v4().to_string(), channel: channel.to_string(), recipient: Self::sanitize_recipient(recipient), - status: openfang_channels::types::DeliveryStatus::Failed, + status: omtae_channels::types::DeliveryStatus::Failed, timestamp: chrono::Utc::now(), // Sanitize error: no credentials, max 256 chars error: Some( @@ -290,12 +292,12 @@ impl DeliveryTracker { /// Create the agent's private state directory layout. Holds identity files, /// AGENT.json, sessions/, memory/, and logs/. Lives under -/// `~/.openfang/workspaces/{name}/` regardless of where the user pointed the +/// `~/.omtae/workspaces/{name}/` regardless of where the user pointed the /// user-facing workspace. See issue #1097. fn ensure_state_dir(state_dir: &Path, workspace: &Path) -> KernelResult<()> { for subdir in &["sessions", "logs", "memory"] { std::fs::create_dir_all(state_dir.join(subdir)).map_err(|e| { - KernelError::OpenFang(OpenFangError::Internal(format!( + KernelError::OMTAE(OMTAEError::Internal(format!( "Failed to create state dir {}/{subdir}: {e}", state_dir.display() ))) @@ -321,7 +323,7 @@ fn ensure_state_dir(state_dir: &Path, workspace: &Path) -> KernelResult<()> { fn ensure_workspace(workspace: &Path) -> KernelResult<()> { for subdir in &["data", "output", "skills"] { std::fs::create_dir_all(workspace.join(subdir)).map_err(|e| { - KernelError::OpenFang(OpenFangError::Internal(format!( + KernelError::OMTAE(OMTAEError::Internal(format!( "Failed to create workspace dir {}/{subdir}: {e}", workspace.display() ))) @@ -486,7 +488,7 @@ fn append_daily_memory_log(state_dir: &Path, response: &str) { } } // Truncate long responses for the log (UTF-8 safe) - let summary = openfang_types::truncate_str(trimmed, 500); + let summary = omtae_types::truncate_str(trimmed, 500); let timestamp = chrono::Utc::now().format("%H:%M:%S").to_string(); if let Ok(mut f) = std::fs::OpenOptions::new() .create(true) @@ -520,7 +522,7 @@ fn read_identity_file(state_dir: &Path, filename: &str) -> Option { return None; } if content.len() > MAX_IDENTITY_FILE_BYTES { - Some(openfang_types::truncate_str(&content, MAX_IDENTITY_FILE_BYTES).to_string()) + Some(omtae_types::truncate_str(&content, MAX_IDENTITY_FILE_BYTES).to_string()) } else { Some(content) } @@ -546,7 +548,7 @@ fn gethostname() -> Option { } } -impl OpenFangKernel { +impl OMTAEKernel { /// Boot the kernel with configuration from the given path. pub fn boot(config_path: Option<&Path>) -> KernelResult { let config = load_config(config_path); @@ -555,10 +557,10 @@ impl OpenFangKernel { /// Fetch live Copilot models by exchanging the persisted token and querying the API. /// Works both inside and outside a tokio runtime. - fn fetch_copilot_models(openfang_dir: &Path) -> Result, String> { - use openfang_runtime::drivers::copilot; + fn fetch_copilot_models(omtae_dir: &Path) -> Result, String> { + use omtae_runtime::drivers::copilot; - let tokens = copilot::PersistedTokens::load(openfang_dir) + let tokens = copilot::PersistedTokens::load(omtae_dir) .ok_or("No persisted Copilot tokens found")?; let fetch = async { @@ -595,7 +597,7 @@ impl OpenFangKernel { debug!("rustls crypto provider already installed, skipping"); } - use openfang_types::config::KernelMode; + use omtae_types::config::KernelMode; // Env var overrides — useful for Docker where config.toml is baked in. if let Ok(listen) = std::env::var("OPENFANG_LISTEN") { @@ -619,13 +621,13 @@ impl OpenFangKernel { match config.mode { KernelMode::Stable => { - info!("Booting OpenFang kernel in STABLE mode — conservative defaults enforced"); + info!("Booting OMTAE kernel in STABLE mode — conservative defaults enforced"); } KernelMode::Dev => { - warn!("Booting OpenFang kernel in DEV mode — experimental features enabled"); + warn!("Booting OMTAE kernel in DEV mode — experimental features enabled"); } KernelMode::Default => { - info!("Booting OpenFang kernel..."); + info!("Booting OMTAE kernel..."); } } @@ -644,7 +646,7 @@ impl OpenFangKernel { .memory .sqlite_path .clone() - .unwrap_or_else(|| config.data_dir.join("openfang.db")); + .unwrap_or_else(|| config.data_dir.join("omtae.db")); let memory = Arc::new( MemorySubstrate::open(&db_path, config.memory.decay_rate, &config.memory) .map_err(|e| KernelError::BootFailed(format!("Memory init failed: {e}")))?, @@ -654,7 +656,7 @@ impl OpenFangKernel { let credential_resolver = { let vault_path = config.home_dir.join("vault.enc"); let vault = if vault_path.exists() { - let mut v = openfang_extensions::vault::CredentialVault::new(vault_path); + let mut v = omtae_extensions::vault::CredentialVault::new(vault_path); match v.unlock() { Ok(()) => { info!("Credential vault unlocked ({} entries)", v.len()); @@ -669,7 +671,7 @@ impl OpenFangKernel { None }; let dotenv_path = config.home_dir.join(".env"); - openfang_extensions::credentials::CredentialResolver::new(vault, Some(&dotenv_path)) + omtae_extensions::credentials::CredentialResolver::new(vault, Some(&dotenv_path)) }; // Create LLM driver. @@ -794,7 +796,7 @@ impl OpenFangKernel { // Use the chain, or create a stub driver if everything failed let driver: Arc = if driver_chain.len() > 1 { - Arc::new(openfang_runtime::drivers::fallback::FallbackDriver::with_models(model_chain)) + Arc::new(omtae_runtime::drivers::fallback::FallbackDriver::with_models(model_chain)) } else if let Some(single) = driver_chain.into_iter().next() { single } else { @@ -806,7 +808,7 @@ impl OpenFangKernel { // Initialize metering engine (shares the same SQLite connection as the memory substrate) let metering = Arc::new(MeteringEngine::new(Arc::new( - openfang_memory::usage::UsageStore::new(memory.usage_conn()), + omtae_memory::usage::UsageStore::new(memory.usage_conn()), ))); let supervisor = Supervisor::new(); @@ -823,7 +825,7 @@ impl OpenFangKernel { } // Initialize model catalog, detect provider auth, and apply URL overrides - let mut model_catalog = openfang_runtime::model_catalog::ModelCatalog::new(); + let mut model_catalog = omtae_runtime::model_catalog::ModelCatalog::new(); model_catalog.detect_auth(); // Env-var overrides for local providers (OLLAMA_HOST, LMSTUDIO_BASE_URL, etc.). // Applied before `provider_urls` so explicit config.toml entries win. See #1154. @@ -835,12 +837,12 @@ impl OpenFangKernel { config.provider_urls.len() ); } - // Load user's custom models from ~/.openfang/custom_models.json + // Load user's custom models from ~/.omtae/custom_models.json let custom_models_path = config.home_dir.join("custom_models.json"); model_catalog.load_custom_models(&custom_models_path); // Fetch live Copilot models if authenticated - if openfang_runtime::drivers::copilot::copilot_auth_available(&config.home_dir) { + if omtae_runtime::drivers::copilot::copilot_auth_available(&config.home_dir) { let copilot_dir = config.home_dir.clone(); match Self::fetch_copilot_models(&copilot_dir) { Ok(models) => { @@ -866,7 +868,7 @@ impl OpenFangKernel { // Initialize skill registry let skills_dir = config.home_dir.join("skills"); - let mut skill_registry = openfang_skills::registry::SkillRegistry::new(skills_dir); + let mut skill_registry = omtae_skills::registry::SkillRegistry::new(skills_dir); // Install user-supplied per-skill config from `[skills.]` sections // before loading so the loader can resolve declared config frontmatter. skill_registry.set_skill_configs(config.skills.clone()); @@ -893,15 +895,21 @@ impl OpenFangKernel { skill_registry.freeze(); } + if config.ecc.enabled { + info!( + "ECC integration enabled — runtime TOOL_REQUIRED gates active; install skills via scripts/install-ecc-skills.sh" + ); + } + // Initialize hand registry (curated autonomous packages) - let hand_registry = openfang_hands::registry::HandRegistry::new(); + let hand_registry = omtae_hands::registry::HandRegistry::new(); let hand_count = hand_registry.load_bundled(); if hand_count > 0 { info!("Loaded {hand_count} bundled hand(s)"); } // Load custom hands from the user's workspace (issue #984). - // Hands installed via `openfang hand install ` are persisted to + // Hands installed via `omtae hand install ` are persisted to // `/hands//` so they survive daemon restarts. let workspace_hands_dir = config.home_dir.join("hands"); match hand_registry.load_workspace_hands(&workspace_hands_dir) { @@ -919,7 +927,7 @@ impl OpenFangKernel { // Initialize extension/integration registry let mut extension_registry = - openfang_extensions::registry::IntegrationRegistry::new(&config.home_dir); + omtae_extensions::registry::IntegrationRegistry::new(&config.home_dir); let ext_bundled = extension_registry.load_bundled(); match extension_registry.load_installed() { Ok(count) => { @@ -947,13 +955,13 @@ impl OpenFangKernel { } // Initialize integration health monitor - let health_config = openfang_extensions::health::HealthMonitorConfig { + let health_config = omtae_extensions::health::HealthMonitorConfig { auto_reconnect: config.extensions.auto_reconnect, max_reconnect_attempts: config.extensions.reconnect_max_attempts, max_backoff_secs: config.extensions.reconnect_max_backoff_secs, check_interval_secs: config.extensions.health_check_interval_secs, }; - let extension_health = openfang_extensions::health::HealthMonitor::new(health_config); + let extension_health = omtae_extensions::health::HealthMonitor::new(health_config); // Register all installed integrations for health monitoring for inst in extension_registry.to_mcp_configs() { extension_health.register(&inst.name); @@ -961,13 +969,13 @@ impl OpenFangKernel { // Initialize web tools (multi-provider search + SSRF-protected fetch + caching) let cache_ttl = std::time::Duration::from_secs(config.web.cache_ttl_minutes * 60); - let web_cache = Arc::new(openfang_runtime::web_cache::WebCache::new(cache_ttl)); - let web_ctx = openfang_runtime::web_search::WebToolsContext { - search: openfang_runtime::web_search::WebSearchEngine::new( + let web_cache = Arc::new(omtae_runtime::web_cache::WebCache::new(cache_ttl)); + let web_ctx = omtae_runtime::web_search::WebToolsContext { + search: omtae_runtime::web_search::WebSearchEngine::new( config.web.clone(), web_cache.clone(), ), - fetch: openfang_runtime::web_fetch::WebFetchEngine::new( + fetch: omtae_runtime::web_fetch::WebFetchEngine::new( config.web.fetch.clone(), web_cache, ), @@ -975,9 +983,9 @@ impl OpenFangKernel { // Auto-detect embedding driver for vector similarity search let embedding_driver: Option< - Arc, + Arc, > = { - use openfang_runtime::embedding::create_embedding_driver; + use omtae_runtime::embedding::create_embedding_driver; let configured_model = &config.memory.embedding_model; if let Some(ref provider) = config.memory.embedding_provider { // Explicit config takes priority — use the configured embedding model. @@ -1079,14 +1087,14 @@ impl OpenFangKernel { } }; - let browser_ctx = openfang_runtime::browser::BrowserManager::new(config.browser.clone()); + let browser_ctx = omtae_runtime::browser::BrowserManager::new(config.browser.clone()); // Initialize media understanding engine let media_engine = - openfang_runtime::media_understanding::MediaEngine::new(config.media.clone()); + omtae_runtime::media_understanding::MediaEngine::new(config.media.clone()); // Closes #1051: thread MediaConfig URL overrides into the TTS engine // so local OpenAI/ElevenLabs-compatible services can be targeted. - let tts_engine = openfang_runtime::tts::TtsEngine::new(config.tts.clone()).with_base_urls( + let tts_engine = omtae_runtime::tts::TtsEngine::new(config.tts.clone()).with_base_urls( config.media.tts_openai_base_url.clone(), config.media.tts_elevenlabs_base_url.clone(), ); @@ -1188,9 +1196,10 @@ impl OpenFangKernel { skill_registry: std::sync::RwLock::new(skill_registry), skill_config_overrides: std::sync::RwLock::new(None), running_tasks: dashmap::DashMap::new(), + running_task_started: dashmap::DashMap::new(), mcp_connections: tokio::sync::Mutex::new(Vec::new()), mcp_tools: std::sync::Mutex::new(Vec::new()), - a2a_task_store: openfang_runtime::a2a::A2aTaskStore::default(), + a2a_task_store: omtae_runtime::a2a::A2aTaskStore::default(), a2a_external_agents: std::sync::Mutex::new(Vec::new()), web_ctx, browser_ctx, @@ -1209,8 +1218,8 @@ impl OpenFangKernel { bindings: std::sync::Mutex::new(initial_bindings), broadcast: initial_broadcast, auto_reply_engine, - hooks: openfang_runtime::hooks::HookRegistry::new(), - process_manager: Arc::new(openfang_runtime::process_manager::ProcessManager::new(5)), + hooks: omtae_runtime::hooks::HookRegistry::new(), + process_manager: Arc::new(omtae_runtime::process_manager::ProcessManager::new(5)), peer_registry: OnceLock::new(), peer_node: OnceLock::new(), booted_at: std::time::Instant::now(), @@ -1231,14 +1240,14 @@ impl OpenFangKernel { // for every subsequent install/upsert/reload. { let audit_log_initial = Arc::clone(&kernel.audit_log); - for (hand_id, toml_content, _skill) in openfang_hands::bundled::bundled_hands() { + for (hand_id, toml_content, _skill) in omtae_hands::bundled::bundled_hands() { use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); hasher.update(toml_content.as_bytes()); let hash = hex::encode(hasher.finalize()); audit_log_initial.record( "kernel", - openfang_runtime::audit::AuditAction::ConfigChange, + omtae_runtime::audit::AuditAction::ConfigChange, format!("HAND.toml load hand={hand_id} sha256={hash}"), "ok", ); @@ -1250,7 +1259,7 @@ impl OpenFangKernel { .set_audit_callback(Arc::new(move |hand_id: &str, hash: &str| { audit_log_for_cb.record( "kernel", - openfang_runtime::audit::AuditAction::ConfigChange, + omtae_runtime::audit::AuditAction::ConfigChange, format!("HAND.toml reload hand={hand_id} sha256={hash}"), "ok", ); @@ -1285,7 +1294,7 @@ impl OpenFangKernel { if toml_path.exists() { match std::fs::read_to_string(&toml_path) { Ok(toml_str) => { - match toml::from_str::( + match toml::from_str::( &toml_str, ) { Ok(disk_manifest) => { @@ -1340,6 +1349,9 @@ impl OpenFangKernel { disk_manifest, &entry.manifest, ); + normalize_request_driven_agent_schedule( + &mut entry.manifest, + ); // Persist the update back to DB if let Err(e) = kernel.memory.save_agent(&entry) { warn!( @@ -1462,7 +1474,7 @@ impl OpenFangKernel { } } - // Issue #1140: auto-spawn agents from `~/.openfang/agents//agent.toml` + // Issue #1140: auto-spawn agents from `~/.omtae/agents//agent.toml` // that are present on disk but not yet in the registry. Without this, // user-placed agent dirs never appear in `GET /api/agents` (and thus // the chat tab's dropdown) until they are explicitly spawned via API @@ -1486,6 +1498,11 @@ impl OpenFangKernel { Some(n) => n.to_string_lossy().to_string(), None => continue, }; + if !kernel.config.agents.autospawn.is_empty() + && !kernel.config.agents.autospawn.iter().any(|a| a == &dir_name) + { + continue; + } // Skip if an agent with this name already exists in the // registry (was restored from DB or already spawned). if kernel.registry.find_by_name(&dir_name).is_some() { @@ -1502,7 +1519,7 @@ impl OpenFangKernel { continue; } }; - let mut manifest: openfang_types::agent::AgentManifest = + let mut manifest: omtae_types::agent::AgentManifest = match toml::from_str(&toml_str) { Ok(m) => m, Err(e) => { @@ -1527,7 +1544,7 @@ impl OpenFangKernel { info!( agent = %dir_name, id = %id, - "Auto-spawned agent from ~/.openfang/agents" + "Auto-spawned agent from ~/.omtae/agents" ); } Err(e) => { @@ -1540,19 +1557,48 @@ impl OpenFangKernel { } } if auto_spawned > 0 { - info!("Auto-spawned {auto_spawned} agent(s) from ~/.openfang/agents"); + info!("Auto-spawned {auto_spawned} agent(s) from ~/.omtae/agents"); + } + } + } + + // Enforce `[agents] autospawn` allowlist — stop agents restored from DB that are not listed. + if kernel.config.watchdog.kill_disallowed && !kernel.config.agents.autospawn.is_empty() { + let allow: std::collections::HashSet<&str> = kernel + .config + .agents + .autospawn + .iter() + .map(|s| s.as_str()) + .collect(); + let manual: std::collections::HashSet = kernel + .config + .watchdog + .manual_allowlist + .iter() + .map(|s| s.trim().to_lowercase()) + .collect(); + for entry in kernel.registry.list() { + if manual.contains(&entry.name.trim().to_lowercase()) { + continue; + } + if !allow.contains(entry.name.as_str()) { + info!(agent = %entry.name, "Stopping agent not in autospawn allowlist"); + let _ = kernel.kill_agent(entry.id); } } } // If no agents exist (fresh install), spawn a default assistant - if kernel.registry.list().is_empty() { + let spawn_default_assistant = kernel.config.agents.autospawn.is_empty() + || kernel.config.agents.autospawn.iter().any(|a| a == "assistant"); + if kernel.registry.list().is_empty() && spawn_default_assistant { info!("No agents found — spawning default assistant"); let dm = &kernel.config.default_model; let manifest = AgentManifest { name: "assistant".to_string(), description: "General-purpose assistant".to_string(), - model: openfang_types::agent::ModelConfig { + model: omtae_types::agent::ModelConfig { provider: dm.provider.clone(), model: dm.model.clone(), system_prompt: "You are a helpful AI assistant.".to_string(), @@ -1587,7 +1633,7 @@ impl OpenFangKernel { } } - info!("OpenFang kernel booted successfully"); + info!("OMTAE kernel booted successfully"); Ok(kernel) } @@ -1614,7 +1660,7 @@ impl OpenFangKernel { let session = self .memory .create_session(agent_id) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; let session_id = session.id; // Inherit kernel exec_policy as fallback if agent manifest doesn't have one @@ -1682,6 +1728,8 @@ impl OpenFangKernel { Some(self.config.resolve_api_key_env(&manifest.model.provider)); } + normalize_request_driven_agent_schedule(&mut manifest); + // Normalize: strip provider prefix from model name if present let normalized = strip_provider_prefix(&manifest.model.model, &manifest.model.provider); if normalized != manifest.model.model { @@ -1691,7 +1739,7 @@ impl OpenFangKernel { // Apply global budget defaults to agent resource quotas apply_budget_defaults(&self.config.budget, &mut manifest.resources); - // Agent private state always lives under ~/.openfang/workspaces/{name}/. + // Agent private state always lives under ~/.omtae/workspaces/{name}/. // This is name-based so SOUL.md and per-agent memory survive recreation // and never get dumped into a user-supplied workspace path. See #1097. let state_dir = manifest @@ -1742,7 +1790,7 @@ impl OpenFangKernel { }; self.registry .register(entry.clone()) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; // Update parent's children list if let Some(parent_id) = parent { @@ -1752,14 +1800,14 @@ impl OpenFangKernel { // Persist agent to SQLite so it survives restarts self.memory .save_agent(&entry) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; info!(agent = %name, id = %agent_id, "Agent spawned"); // SECURITY: Record agent spawn in audit trail self.audit_log.record( agent_id.to_string(), - openfang_runtime::audit::AuditAction::AgentSpawn, + omtae_runtime::audit::AuditAction::AgentSpawn, format!("name={name}, parent={parent:?}"), "ok", ); @@ -1797,14 +1845,14 @@ impl OpenFangKernel { /// Call this before `spawn_agent` when a `SignedManifest` JSON is provided /// alongside the TOML. Returns the verified manifest TOML string on success. pub fn verify_signed_manifest(&self, signed_json: &str) -> KernelResult { - let signed: openfang_types::manifest_signing::SignedManifest = + let signed: omtae_types::manifest_signing::SignedManifest = serde_json::from_str(signed_json).map_err(|e| { - KernelError::OpenFang(openfang_types::error::OpenFangError::Config(format!( + KernelError::OMTAE(omtae_types::error::OMTAEError::Config(format!( "Invalid signed manifest JSON: {e}" ))) })?; signed.verify().map_err(|e| { - KernelError::OpenFang(openfang_types::error::OpenFangError::Config(format!( + KernelError::OMTAE(omtae_types::error::OMTAEError::Config(format!( "Manifest signature verification failed: {e}" ))) })?; @@ -1839,7 +1887,7 @@ impl OpenFangKernel { &self, agent_id: AgentId, message: &str, - blocks: Vec, + blocks: Vec, ) -> KernelResult { let handle: Option> = self .self_handle @@ -1891,7 +1939,7 @@ impl OpenFangKernel { agent_id: AgentId, message: &str, kernel_handle: Option>, - content_blocks: Option>, + content_blocks: Option>, sender_id: Option, sender_name: Option, ) -> KernelResult { @@ -1909,10 +1957,10 @@ impl OpenFangKernel { // Enforce quota before running the agent loop self.scheduler .check_quota(agent_id) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; let entry = self.registry.get(agent_id).ok_or_else(|| { - KernelError::OpenFang(OpenFangError::AgentNotFound(agent_id.to_string())) + KernelError::OMTAE(OMTAEError::AgentNotFound(agent_id.to_string())) })?; // Dispatch based on module type @@ -1946,7 +1994,7 @@ impl OpenFangKernel { // SECURITY: Record successful message in audit trail self.audit_log.record( agent_id.to_string(), - openfang_runtime::audit::AuditAction::AgentMessage, + omtae_runtime::audit::AuditAction::AgentMessage, format!( "tokens_in={}, tokens_out={}", result.total_usage.input_tokens, result.total_usage.output_tokens @@ -1960,7 +2008,7 @@ impl OpenFangKernel { // SECURITY: Record failed message in audit trail self.audit_log.record( agent_id.to_string(), - openfang_runtime::audit::AuditAction::AgentMessage, + omtae_runtime::audit::AuditAction::AgentMessage, "agent loop failed", format!("error: {e}"), ); @@ -1988,7 +2036,7 @@ impl OpenFangKernel { kernel_handle: Option>, sender_id: Option, sender_name: Option, - content_blocks: Option>, + content_blocks: Option>, ) -> KernelResult<( tokio::sync::mpsc::Receiver, tokio::task::JoinHandle>, @@ -1996,10 +2044,10 @@ impl OpenFangKernel { // Enforce quota before spawning the streaming task self.scheduler .check_quota(agent_id) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; let entry = self.registry.get(agent_id).ok_or_else(|| { - KernelError::OpenFang(OpenFangError::AgentNotFound(agent_id.to_string())) + KernelError::OMTAE(OMTAEError::AgentNotFound(agent_id.to_string())) })?; let is_wasm = entry.manifest.module.starts_with("wasm:"); @@ -2033,7 +2081,7 @@ impl OpenFangKernel { .await; let _ = tx .send(StreamEvent::ContentComplete { - stop_reason: openfang_types::message::StopReason::EndTurn, + stop_reason: omtae_types::message::StopReason::EndTurn, usage: result.total_usage, }) .await; @@ -2060,8 +2108,8 @@ impl OpenFangKernel { let mut session = self .memory .get_session(entry.session_id) - .map_err(KernelError::OpenFang)? - .unwrap_or_else(|| openfang_memory::session::Session { + .map_err(KernelError::OMTAE)? + .unwrap_or_else(|| omtae_memory::session::Session { id: entry.session_id, agent_id, messages: Vec::new(), @@ -2069,19 +2117,26 @@ impl OpenFangKernel { label: None, }); + let driver = self.resolve_driver(&entry.manifest)?; + + // Look up model's actual context window from the catalog + let ctx_window = self.model_catalog.read().ok().and_then(|cat| { + cat.find_model(&entry.manifest.model.model) + .map(|m| m.context_window as usize) + }); + // Check if auto-compaction is needed: message-count OR token-count OR quota-headroom trigger let needs_compact = { - use openfang_runtime::compactor::{ - estimate_token_count, needs_compaction as check_compact, - needs_compaction_by_tokens, CompactionConfig, + use omtae_runtime::compactor::{ + estimate_token_count, needs_compaction, needs_compaction_by_tokens, }; - let config = CompactionConfig::default(); - let by_messages = check_compact(&session, &config); + let config = self.agent_compaction_config(ctx_window); let estimated = estimate_token_count( &session.messages, Some(&entry.manifest.model.system_prompt), None, ); + let by_messages = needs_compaction(&session, &config); let by_tokens = needs_compaction_by_tokens(estimated, &config); if by_tokens && !by_messages { info!( @@ -2110,20 +2165,12 @@ impl OpenFangKernel { by_messages || by_tokens || by_quota }; - let driver = self.resolve_driver(&entry.manifest)?; - - // Look up model's actual context window from the catalog - let ctx_window = self.model_catalog.read().ok().and_then(|cat| { - cat.find_model(&entry.manifest.model.model) - .map(|m| m.context_window as usize) - }); - let (tx, rx) = tokio::sync::mpsc::channel::(64); let mut manifest = entry.manifest.clone(); // Lazy backfill: create state_dir and workspace for existing agents // spawned before this field existed. Private state always lives under - // ~/.openfang/workspaces/{name}/; the user-facing workspace defaults + // ~/.omtae/workspaces/{name}/; the user-facing workspace defaults // to the same path unless the manifest already pinned one. See #1097. if manifest.state_dir.is_none() { let state_dir = self.config.effective_workspaces_dir().join(&manifest.name); @@ -2149,7 +2196,7 @@ impl OpenFangKernel { } // Build workspace-aware skill snapshot BEFORE tool list and prompt building. - // Loading order: bundled → global (~/.openfang/skills) → workspace skills. + // Loading order: bundled → global (~/.omtae/skills) → workspace skills. // Each layer overrides duplicates from the previous layer. (#851, #808) let skill_snapshot = { let mut snapshot = self @@ -2197,7 +2244,7 @@ impl OpenFangKernel { }) .collect(); - let prompt_ctx = openfang_runtime::prompt_builder::PromptContext { + let prompt_ctx = omtae_runtime::prompt_builder::PromptContext { agent_name: manifest.name.clone(), agent_description: manifest.description.clone(), base_system_prompt: manifest.model.system_prompt.clone(), @@ -2249,7 +2296,7 @@ impl OpenFangKernel { .and_then(|s| read_identity_file(s, "BOOTSTRAP.md")), workspace_context: manifest.workspace.as_ref().map(|w| { let mut ws_ctx = - openfang_runtime::workspace_context::WorkspaceContext::detect(w); + omtae_runtime::workspace_context::WorkspaceContext::detect(w); ws_ctx.build_context_section() }), identity_md: manifest @@ -2276,15 +2323,15 @@ impl OpenFangKernel { // (cron jobs, integrations) reach the LLM on the next message. // Opt out via `cache_context = true` on the manifest. (#843) context_md: manifest.workspace.as_ref().and_then(|w| { - openfang_runtime::agent_context::load_context_md(w, manifest.cache_context) + omtae_runtime::agent_context::load_context_md(w, manifest.cache_context) }), }; manifest.model.system_prompt = - openfang_runtime::prompt_builder::build_system_prompt(&prompt_ctx); + omtae_runtime::prompt_builder::build_system_prompt(&prompt_ctx); // Store canonical context separately for injection as user message // (keeps system prompt stable across turns for provider prompt caching) if let Some(cc_msg) = - openfang_runtime::prompt_builder::build_canonical_context_message(&prompt_ctx) + omtae_runtime::prompt_builder::build_canonical_context_message(&prompt_ctx) { manifest.metadata.insert( "canonical_context_msg".to_string(), @@ -2296,7 +2343,7 @@ impl OpenFangKernel { let memory = Arc::clone(&self.memory); // Build link context from user message (auto-extract URLs for the agent) let message_owned = if let Some(link_ctx) = - openfang_runtime::link_understanding::build_link_context(message, &self.config.links) + omtae_runtime::link_understanding::build_link_context(message, &self.config.links) { format!("{message}{link_ctx}") } else { @@ -2328,9 +2375,9 @@ impl OpenFangKernel { // Create a phase callback that emits PhaseChange events to WS/SSE clients let phase_tx = tx.clone(); - let phase_cb: openfang_runtime::agent_loop::PhaseCallback = + let phase_cb: omtae_runtime::agent_loop::PhaseCallback = std::sync::Arc::new(move |phase| { - use openfang_runtime::agent_loop::LoopPhase; + use omtae_runtime::agent_loop::LoopPhase; let (phase_str, detail) = match &phase { LoopPhase::Thinking => ("thinking".to_string(), None), LoopPhase::ToolUse { tool_name } => { @@ -2391,7 +2438,7 @@ impl OpenFangKernel { // ping timeout before post-processing finished). drop(phase_cb); - match result { + let loop_result = match result { Ok(result) => { // Append new messages to canonical session for cross-channel memory if session.messages.len() > messages_before { @@ -2431,7 +2478,7 @@ impl OpenFangKernel { ); let _ = kernel_clone .metering - .record(&openfang_memory::usage::UsageRecord { + .record(&omtae_memory::usage::UsageRecord { agent_id, model: model.clone(), input_tokens: result.total_usage.input_tokens, @@ -2447,10 +2494,10 @@ impl OpenFangKernel { // Post-loop compaction check: if session now exceeds token threshold, // trigger compaction in background for the next call. { - use openfang_runtime::compactor::{ - estimate_token_count, needs_compaction_by_tokens, CompactionConfig, + use omtae_runtime::compactor::{ + estimate_token_count, needs_compaction_by_tokens, }; - let config = CompactionConfig::default(); + let config = kernel_clone.agent_compaction_config(ctx_window); let estimated = estimate_token_count(&session.messages, None, None); if needs_compaction_by_tokens(estimated, &config) { let kc = kernel_clone.clone(); @@ -2468,12 +2515,20 @@ impl OpenFangKernel { Err(e) => { kernel_clone.supervisor.record_panic(); warn!(agent_id = %agent_id, error = %e, "Streaming agent loop failed"); - Err(KernelError::OpenFang(e)) + Err(KernelError::OMTAE(e)) } - } + }; + kernel_clone.running_tasks.remove(&agent_id); + kernel_clone.running_task_started.remove(&agent_id); + loop_result }); - // Store abort handle for cancellation support + // Store abort handle for cancellation support (cleared when the loop task exits). + let started_at = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + self.running_task_started.insert(agent_id, started_at); self.running_tasks.insert(agent_id, handle.abort_handle()); Ok((rx, handle)) @@ -2499,7 +2554,7 @@ impl OpenFangKernel { info!(agent = %entry.name, path = %wasm_path.display(), "Executing WASM agent"); let wasm_bytes = std::fs::read(&wasm_path).map_err(|e| { - KernelError::OpenFang(OpenFangError::Internal(format!( + KernelError::OMTAE(OMTAEError::Internal(format!( "Failed to read WASM module '{}': {e}", wasm_path.display() ))) @@ -2532,7 +2587,7 @@ impl OpenFangKernel { ) .await .map_err(|e| { - KernelError::OpenFang(OpenFangError::Internal(format!( + KernelError::OMTAE(OMTAEError::Internal(format!( "WASM execution failed: {e}" ))) })?; @@ -2555,7 +2610,7 @@ impl OpenFangKernel { Ok(AgentLoopResult { response, - total_usage: openfang_types::message::TokenUsage { + total_usage: omtae_types::message::TokenUsage { input_tokens: 0, output_tokens: 0, }, @@ -2606,7 +2661,7 @@ impl OpenFangKernel { ) .await .map_err(|e| { - KernelError::OpenFang(OpenFangError::Internal(format!( + KernelError::OMTAE(OMTAEError::Internal(format!( "Python execution failed: {e}" ))) })?; @@ -2615,7 +2670,7 @@ impl OpenFangKernel { Ok(AgentLoopResult { response: result.response, - total_usage: openfang_types::message::TokenUsage { + total_usage: omtae_types::message::TokenUsage { input_tokens: 0, output_tokens: 0, }, @@ -2634,20 +2689,20 @@ impl OpenFangKernel { agent_id: AgentId, message: &str, kernel_handle: Option>, - content_blocks: Option>, + content_blocks: Option>, sender_id: Option, sender_name: Option, ) -> KernelResult { // Check metering quota before starting self.metering .check_quota(agent_id, &entry.manifest.resources) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; let mut session = self .memory .get_session(entry.session_id) - .map_err(KernelError::OpenFang)? - .unwrap_or_else(|| openfang_memory::session::Session { + .map_err(KernelError::OMTAE)? + .unwrap_or_else(|| omtae_memory::session::Session { id: entry.session_id, agent_id, messages: Vec::new(), @@ -2655,13 +2710,18 @@ impl OpenFangKernel { label: None, }); + let ctx_window = self.model_catalog.read().ok().and_then(|cat| { + cat.find_model(&entry.manifest.model.model) + .map(|m| m.context_window as usize) + }); + // Pre-emptive compaction: compact before LLM call if session is large or quota headroom is low { - use openfang_runtime::compactor::{ + use omtae_runtime::compactor::{ estimate_token_count, needs_compaction as check_compact, - needs_compaction_by_tokens, CompactionConfig, + needs_compaction_by_tokens, }; - let config = CompactionConfig::default(); + let config = self.agent_compaction_config(ctx_window); let by_messages = check_compact(&session, &config); let estimated = estimate_token_count( &session.messages, @@ -2697,7 +2757,7 @@ impl OpenFangKernel { let mut manifest = entry.manifest.clone(); // Lazy backfill: create state_dir and workspace for existing agents. - // Private state lives under ~/.openfang/workspaces/{name}/. User-facing + // Private state lives under ~/.omtae/workspaces/{name}/. User-facing // workspace stays at whatever the manifest pinned (or defaults to the // state_dir). See issue #1097. if manifest.state_dir.is_none() { @@ -2724,7 +2784,7 @@ impl OpenFangKernel { } // Build workspace-aware skill snapshot BEFORE tool list and prompt building. - // Loading order: bundled → global (~/.openfang/skills) → workspace skills. + // Loading order: bundled → global (~/.omtae/skills) → workspace skills. // Each layer overrides duplicates from the previous layer. (#851, #808) let skill_snapshot = { let mut snapshot = self @@ -2780,7 +2840,7 @@ impl OpenFangKernel { }) .collect(); - let prompt_ctx = openfang_runtime::prompt_builder::PromptContext { + let prompt_ctx = omtae_runtime::prompt_builder::PromptContext { agent_name: manifest.name.clone(), agent_description: manifest.description.clone(), base_system_prompt: manifest.model.system_prompt.clone(), @@ -2832,7 +2892,7 @@ impl OpenFangKernel { .and_then(|s| read_identity_file(s, "BOOTSTRAP.md")), workspace_context: manifest.workspace.as_ref().map(|w| { let mut ws_ctx = - openfang_runtime::workspace_context::WorkspaceContext::detect(w); + omtae_runtime::workspace_context::WorkspaceContext::detect(w); ws_ctx.build_context_section() }), identity_md: manifest @@ -2857,15 +2917,15 @@ impl OpenFangKernel { sender_name, // Re-read context.md per turn by default (#843). context_md: manifest.workspace.as_ref().and_then(|w| { - openfang_runtime::agent_context::load_context_md(w, manifest.cache_context) + omtae_runtime::agent_context::load_context_md(w, manifest.cache_context) }), }; manifest.model.system_prompt = - openfang_runtime::prompt_builder::build_system_prompt(&prompt_ctx); + omtae_runtime::prompt_builder::build_system_prompt(&prompt_ctx); // Store canonical context separately for injection as user message // (keeps system prompt stable across turns for provider prompt caching) if let Some(cc_msg) = - openfang_runtime::prompt_builder::build_canonical_context_message(&prompt_ctx) + omtae_runtime::prompt_builder::build_canonical_context_message(&prompt_ctx) { manifest.metadata.insert( "canonical_context_msg".to_string(), @@ -2874,7 +2934,7 @@ impl OpenFangKernel { } } - let is_stable = self.config.mode == openfang_types::config::KernelMode::Stable; + let is_stable = self.config.mode == omtae_types::config::KernelMode::Stable; if is_stable { // In Stable mode: use pinned_model if set, otherwise default model @@ -2893,7 +2953,7 @@ impl OpenFangKernel { // Build a probe request to score complexity let probe = CompletionRequest { model: strip_provider_prefix(&manifest.model.model, &manifest.model.provider), - messages: vec![openfang_types::message::Message::user(message)], + messages: vec![omtae_types::message::Message::user(message)], tools: tools.clone(), max_tokens: manifest.model.max_tokens, temperature: manifest.model.temperature, @@ -2921,7 +2981,7 @@ impl OpenFangKernel { let driver = self.resolve_driver(&manifest)?; - // Look up model's actual context window from the catalog + // Re-resolve context window after model routing may have changed the model id. let ctx_window = self.model_catalog.read().ok().and_then(|cat| { cat.find_model(&manifest.model.model) .map(|m| m.context_window as usize) @@ -2932,7 +2992,7 @@ impl OpenFangKernel { // Build link context from user message (auto-extract URLs for the agent) let message_with_links = if let Some(link_ctx) = - openfang_runtime::link_understanding::build_link_context(message, &self.config.links) + omtae_runtime::link_understanding::build_link_context(message, &self.config.links) { format!("{message}{link_ctx}") } else { @@ -2971,7 +3031,7 @@ impl OpenFangKernel { content_blocks, ) .await - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; // Append new messages to canonical session for cross-channel memory if session.messages.len() > messages_before { @@ -3002,7 +3062,7 @@ impl OpenFangKernel { result.total_usage.input_tokens, result.total_usage.output_tokens, ); - let _ = self.metering.record(&openfang_memory::usage::UsageRecord { + let _ = self.metering.record(&omtae_memory::usage::UsageRecord { agent_id, model: model.clone(), input_tokens: result.total_usage.input_tokens, @@ -3014,14 +3074,14 @@ impl OpenFangKernel { // Populate cost on the result based on usage_footer mode let mut result = result; match self.config.usage_footer { - openfang_types::config::UsageFooterMode::Off => { + omtae_types::config::UsageFooterMode::Off => { result.cost_usd = None; } - openfang_types::config::UsageFooterMode::Cost - | openfang_types::config::UsageFooterMode::Full => { + omtae_types::config::UsageFooterMode::Cost + | omtae_types::config::UsageFooterMode::Full => { result.cost_usd = if cost > 0.0 { Some(cost) } else { None }; } - openfang_types::config::UsageFooterMode::Tokens => { + omtae_types::config::UsageFooterMode::Tokens => { // Tokens are already in result.total_usage, omit cost result.cost_usd = None; } @@ -3047,7 +3107,7 @@ impl OpenFangKernel { /// and creates a fresh session ID. pub fn reset_session(&self, agent_id: AgentId) -> KernelResult<()> { let entry = self.registry.get(agent_id).ok_or_else(|| { - KernelError::OpenFang(OpenFangError::AgentNotFound(agent_id.to_string())) + KernelError::OMTAE(OMTAEError::AgentNotFound(agent_id.to_string())) })?; // Auto-save session context to workspace memory before clearing @@ -3064,12 +3124,12 @@ impl OpenFangKernel { let new_session = self .memory .create_session(agent_id) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; // Update registry with new session ID self.registry .update_session_id(agent_id, new_session.id) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; // Reset quota tracking so /new clears "token quota exceeded" self.scheduler.reset_usage(agent_id); @@ -3083,7 +3143,7 @@ impl OpenFangKernel { /// Creates a fresh empty session afterward so the agent is still usable. pub fn clear_agent_history(&self, agent_id: AgentId) -> KernelResult<()> { let _entry = self.registry.get(agent_id).ok_or_else(|| { - KernelError::OpenFang(OpenFangError::AgentNotFound(agent_id.to_string())) + KernelError::OMTAE(OMTAEError::AgentNotFound(agent_id.to_string())) })?; // Delete all regular sessions @@ -3096,12 +3156,12 @@ impl OpenFangKernel { let new_session = self .memory .create_session(agent_id) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; // Update registry with new session ID self.registry .update_session_id(agent_id, new_session.id) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; info!(agent_id = %agent_id, "All agent history cleared"); Ok(()) @@ -3111,13 +3171,13 @@ impl OpenFangKernel { pub fn list_agent_sessions(&self, agent_id: AgentId) -> KernelResult> { // Verify agent exists let entry = self.registry.get(agent_id).ok_or_else(|| { - KernelError::OpenFang(OpenFangError::AgentNotFound(agent_id.to_string())) + KernelError::OMTAE(OMTAEError::AgentNotFound(agent_id.to_string())) })?; let mut sessions = self .memory .list_agent_sessions(agent_id) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; // Mark the active session for s in &mut sessions { @@ -3142,18 +3202,18 @@ impl OpenFangKernel { ) -> KernelResult { // Verify agent exists let _entry = self.registry.get(agent_id).ok_or_else(|| { - KernelError::OpenFang(OpenFangError::AgentNotFound(agent_id.to_string())) + KernelError::OMTAE(OMTAEError::AgentNotFound(agent_id.to_string())) })?; let session = self .memory .create_session_with_label(agent_id, label) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; // Switch to the new session self.registry .update_session_id(agent_id, session.id) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; info!(agent_id = %agent_id, label = ?label, "Created new session"); @@ -3171,27 +3231,27 @@ impl OpenFangKernel { ) -> KernelResult<()> { // Verify agent exists let _entry = self.registry.get(agent_id).ok_or_else(|| { - KernelError::OpenFang(OpenFangError::AgentNotFound(agent_id.to_string())) + KernelError::OMTAE(OMTAEError::AgentNotFound(agent_id.to_string())) })?; // Verify session exists and belongs to this agent let session = self .memory .get_session(session_id) - .map_err(KernelError::OpenFang)? + .map_err(KernelError::OMTAE)? .ok_or_else(|| { - KernelError::OpenFang(OpenFangError::Internal("Session not found".to_string())) + KernelError::OMTAE(OMTAEError::Internal("Session not found".to_string())) })?; if session.agent_id != agent_id { - return Err(KernelError::OpenFang(OpenFangError::Internal( + return Err(KernelError::OMTAE(OMTAEError::Internal( "Session belongs to a different agent".to_string(), ))); } self.registry .update_session_id(agent_id, session_id) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; info!(agent_id = %agent_id, session_id = %session_id.0, "Switched session"); Ok(()) @@ -3202,9 +3262,9 @@ impl OpenFangKernel { &self, agent_id: AgentId, entry: &AgentEntry, - session: &openfang_memory::session::Session, + session: &omtae_memory::session::Session, ) { - use openfang_types::message::{MessageContent, Role}; + use omtae_types::message::{MessageContent, Role}; // Take last 10 messages (or all if fewer) let recent = &session.messages[session.messages.len().saturating_sub(10)..]; @@ -3243,7 +3303,7 @@ impl OpenFangKernel { .take(5) .enumerate() .map(|(i, t)| { - let truncated = openfang_types::truncate_str(t, 200); + let truncated = omtae_types::truncate_str(t, 200); format!("{}. {}", i + 1, truncated) }) .collect::>() @@ -3277,6 +3337,47 @@ impl OpenFangKernel { /// /// This is best-effort: a failure to write is logged but does not /// propagate as an error — the authoritative copy lives in SQLite. + /// Reload an agent's manifest from `~/.omtae/agents//agent.toml` into + /// the in-memory registry (capabilities, exec_policy, tools, prompt, etc.). + /// Used by hot-reload paths so disk edits take effect without a daemon restart. + pub fn reload_agent_manifest_from_disk(&self, agent_id: AgentId) -> KernelResult<()> { + let entry = self + .registry + .get(agent_id) + .ok_or_else(|| OMTAEError::AgentNotFound(agent_id.to_string()))?; + let name = entry.name.clone(); + let toml_path = self + .config + .home_dir + .join("agents") + .join(&name) + .join("agent.toml"); + let toml_str = std::fs::read_to_string(&toml_path).map_err(|e| { + OMTAEError::Config(format!( + "Failed to read {}: {e}", + toml_path.display() + )) + })?; + let disk_manifest: AgentManifest = toml::from_str(&toml_str).map_err(|e| { + OMTAEError::Config(format!("Invalid agent TOML for {name}: {e}")) + })?; + let merged = merge_disk_manifest_preserving_kernel_defaults(disk_manifest, &entry.manifest); + let caps = manifest_to_capabilities(&merged); + self.capabilities.grant(agent_id, caps); + let mut updated = entry; + updated.manifest = merged; + normalize_request_driven_agent_schedule(&mut updated.manifest); + if updated.manifest.exec_policy.is_none() { + updated.manifest.exec_policy = Some(self.config.exec_policy.clone()); + } + self.registry.replace_manifest(agent_id, updated.manifest.clone())?; + if let Some(entry) = self.registry.get(agent_id) { + let _ = self.memory.save_agent(&entry); + } + info!(agent = %name, "Reloaded agent manifest from disk"); + Ok(()) + } + pub fn persist_manifest_to_disk(&self, agent_id: AgentId) { if let Some(entry) = self.registry.get(agent_id) { let dir = self.config.home_dir.join("agents").join(&entry.name); @@ -3384,12 +3485,12 @@ impl OpenFangKernel { api_key_env, None, ) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; info!(agent_id = %agent_id, model = %normalized_model, provider = %provider, "Agent model+provider updated"); } else { self.registry .update_model(agent_id, normalized_model.clone()) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; info!(agent_id = %agent_id, model = %normalized_model, "Agent model updated (provider unchanged)"); } @@ -3419,7 +3520,7 @@ impl OpenFangKernel { let known = registry.skill_names(); for name in &skills { if !known.contains(name) { - return Err(KernelError::OpenFang(OpenFangError::Internal(format!( + return Err(KernelError::OMTAE(OMTAEError::Internal(format!( "Unknown skill: {name}" )))); } @@ -3428,7 +3529,7 @@ impl OpenFangKernel { self.registry .update_skills(agent_id, skills.clone()) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; if let Some(entry) = self.registry.get(agent_id) { let _ = self.memory.save_agent(&entry); @@ -3450,14 +3551,14 @@ impl OpenFangKernel { let mut known_servers: std::collections::HashSet = std::collections::HashSet::new(); for tool in mcp_tools.iter() { - if let Some(s) = openfang_runtime::mcp::extract_mcp_server(&tool.name) { + if let Some(s) = omtae_runtime::mcp::extract_mcp_server(&tool.name) { known_servers.insert(s.to_string()); } } for name in &servers { - let normalized = openfang_runtime::mcp::normalize_name(name); + let normalized = omtae_runtime::mcp::normalize_name(name); if !known_servers.contains(&normalized) { - return Err(KernelError::OpenFang(OpenFangError::Internal(format!( + return Err(KernelError::OMTAE(OMTAEError::Internal(format!( "Unknown MCP server: {name}" )))); } @@ -3467,7 +3568,7 @@ impl OpenFangKernel { self.registry .update_mcp_servers(agent_id, servers.clone()) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; if let Some(entry) = self.registry.get(agent_id) { let _ = self.memory.save_agent(&entry); @@ -3486,7 +3587,7 @@ impl OpenFangKernel { ) -> KernelResult<()> { self.registry .update_tool_filters(agent_id, allowlist.clone(), blocklist.clone()) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; if let Some(entry) = self.registry.get(agent_id) { let _ = self.memory.save_agent(&entry); @@ -3504,13 +3605,13 @@ impl OpenFangKernel { /// Get session token usage and estimated cost for an agent. pub fn session_usage_cost(&self, agent_id: AgentId) -> KernelResult<(u64, u64, f64)> { let entry = self.registry.get(agent_id).ok_or_else(|| { - KernelError::OpenFang(OpenFangError::AgentNotFound(agent_id.to_string())) + KernelError::OMTAE(OMTAEError::AgentNotFound(agent_id.to_string())) })?; let session = self .memory .get_session(entry.session_id) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; let (input_tokens, output_tokens) = session .map(|s| { @@ -3521,9 +3622,9 @@ impl OpenFangKernel { let len = msg.content.text_content().len() as u64; let tokens = len / 4; match msg.role { - openfang_types::message::Role::User => input += tokens, - openfang_types::message::Role::Assistant => output += tokens, - openfang_types::message::Role::System => input += tokens, + omtae_types::message::Role::User => input += tokens, + omtae_types::message::Role::Assistant => output += tokens, + omtae_types::message::Role::System => input += tokens, } } (input, output) @@ -3542,32 +3643,154 @@ impl OpenFangKernel { } /// Cancel an agent's currently running LLM task. + /// + /// Also pauses continuous/periodic background ticks so the agent does not + /// immediately restart from an autonomous schedule. pub fn stop_agent_run(&self, agent_id: AgentId) -> KernelResult { + // Pause background schedule first so a tick cannot race with abort. + if let Some(entry) = self.registry.get(agent_id) { + if !matches!(entry.manifest.schedule, ScheduleMode::Reactive) { + self.background.pause_agent(agent_id); + } + } + if let Some((_, handle)) = self.running_tasks.remove(&agent_id) { + self.running_task_started.remove(&agent_id); handle.abort(); info!(agent_id = %agent_id, "Agent run cancelled"); Ok(true) } else { + self.running_task_started.remove(&agent_id); Ok(false) } } + /// Resolved compaction settings for an agent (catalog context window + `[compaction]` overrides). + fn agent_compaction_config(&self, ctx_window: Option) -> omtae_runtime::compactor::CompactionConfig { + omtae_runtime::compactor::build_compaction_config( + ctx_window, + &omtae_runtime::compactor::CompactionUserSettings { + threshold: self.config.compaction.threshold, + keep_recent: self.config.compaction.keep_recent, + token_threshold_ratio: self.config.compaction.token_threshold_ratio, + max_summary_tokens: self.config.compaction.max_summary_tokens, + }, + ) + } + + /// Shrink a target agent's session before an inbound `agent_send` delegation. + /// + /// Specialist agents often accumulate large tool outputs (web_fetch, etc.). On 16k + /// local models that leaves no headroom for the delegated message and causes + /// `Context too long` failures that look like "agents can't hear each other". + async fn prepare_session_for_inbound(&self, agent_id: AgentId) -> KernelResult<()> { + use omtae_runtime::compactor::estimate_token_count; + use omtae_runtime::context_overflow::{recover_from_overflow, RecoveryStage}; + + let entry = self.registry.get(agent_id).ok_or_else(|| { + KernelError::OMTAE(OMTAEError::AgentNotFound(agent_id.to_string())) + })?; + + let ctx_window = self.model_catalog.read().ok().and_then(|cat| { + cat.find_model(&entry.manifest.model.model) + .map(|m| m.context_window as usize) + }); + + let session = self + .memory + .get_session(entry.session_id) + .map_err(KernelError::OMTAE)? + .unwrap_or_else(|| omtae_memory::session::Session { + id: entry.session_id, + agent_id, + messages: Vec::new(), + context_window_tokens: 0, + label: None, + }); + + let config = self.agent_compaction_config(ctx_window); + let cw = config.context_window_tokens.max(ctx_window.unwrap_or(8192)); + let estimated = estimate_token_count( + &session.messages, + Some(&entry.manifest.model.system_prompt), + None, + ); + // Delegation needs more headroom than a normal turn (nested tools + reply). + let delegation_threshold = ((cw as f64) * 0.55) as usize; + if estimated <= delegation_threshold { + return Ok(()); + } + + info!( + agent_id = %agent_id, + agent = %entry.name, + estimated_tokens = estimated, + threshold = delegation_threshold, + "Preparing target session for inbound delegation" + ); + + if let Err(e) = self.compact_agent_session(agent_id).await { + warn!(agent_id = %agent_id, error = %e, "Pre-delegation compaction failed"); + } + + let session = self + .memory + .get_session(entry.session_id) + .map_err(KernelError::OMTAE)? + .unwrap_or(session); + + let estimated = estimate_token_count( + &session.messages, + Some(&entry.manifest.model.system_prompt), + None, + ); + if estimated <= delegation_threshold { + return Ok(()); + } + + let mut msgs = session.messages.clone(); + let stage = recover_from_overflow( + &mut msgs, + &entry.manifest.model.system_prompt, + &[], + cw, + ); + if stage != RecoveryStage::None && stage != RecoveryStage::FinalError { + let mut updated = session; + updated.messages = omtae_runtime::session_repair::validate_and_repair(&msgs); + self.memory + .save_session(&updated) + .map_err(KernelError::OMTAE)?; + info!(agent_id = %agent_id, ?stage, "Applied overflow recovery before delegation"); + } + + Ok(()) + } + /// Compact an agent's session using LLM-based summarization. /// /// Replaces the existing text-truncation compaction with an intelligent /// LLM-generated summary of older messages, keeping only recent messages. pub async fn compact_agent_session(&self, agent_id: AgentId) -> KernelResult { - use openfang_runtime::compactor::{compact_session, needs_compaction, CompactionConfig}; + use omtae_runtime::compactor::{ + compact_session, estimate_token_count, needs_compaction, needs_compaction_by_tokens, + }; + use omtae_runtime::context_overflow::{recover_from_overflow, RecoveryStage}; let entry = self.registry.get(agent_id).ok_or_else(|| { - KernelError::OpenFang(OpenFangError::AgentNotFound(agent_id.to_string())) + KernelError::OMTAE(OMTAEError::AgentNotFound(agent_id.to_string())) })?; + let ctx_window = self.model_catalog.read().ok().and_then(|cat| { + cat.find_model(&entry.manifest.model.model) + .map(|m| m.context_window as usize) + }); + let session = self .memory .get_session(entry.session_id) - .map_err(KernelError::OpenFang)? - .unwrap_or_else(|| openfang_memory::session::Session { + .map_err(KernelError::OMTAE)? + .unwrap_or_else(|| omtae_memory::session::Session { id: entry.session_id, agent_id, messages: Vec::new(), @@ -3575,9 +3798,14 @@ impl OpenFangKernel { label: None, }); - let config = CompactionConfig::default(); + let config = self.agent_compaction_config(ctx_window); + let estimated = estimate_token_count( + &session.messages, + Some(&entry.manifest.model.system_prompt), + None, + ); - if !needs_compaction(&session, &config) { + if !needs_compaction(&session, &config) && !needs_compaction_by_tokens(estimated, &config) { return Ok(format!( "No compaction needed ({} messages, threshold {})", session.messages.len(), @@ -3585,28 +3813,61 @@ impl OpenFangKernel { )); } + // Few messages but high token count (large tool outputs) — trim before LLM summarization. + if session.messages.len() <= config.keep_recent + && needs_compaction_by_tokens(estimated, &config) + { + let mut msgs = session.messages.clone(); + let cw = config.context_window_tokens.max(ctx_window.unwrap_or(8192)); + let stage = recover_from_overflow( + &mut msgs, + &entry.manifest.model.system_prompt, + &[], + cw, + ); + if stage != RecoveryStage::None { + let mut updated_session = session; + updated_session.messages = + omtae_runtime::session_repair::validate_and_repair(&msgs); + self.memory + .save_session(&updated_session) + .map_err(KernelError::OMTAE)?; + return Ok(format!( + "Applied overflow recovery ({stage:?}) — trimmed session for token pressure." + )); + } + } + + if session.messages.len() <= config.keep_recent { + return Ok(format!( + "Session has {} messages (keep_recent={}); could not reduce token usage further.", + session.messages.len(), + config.keep_recent + )); + } + let driver = self.resolve_driver(&entry.manifest)?; let model = entry.manifest.model.model.clone(); let result = compact_session(driver, &model, &session, &config) .await - .map_err(|e| KernelError::OpenFang(OpenFangError::Internal(e)))?; + .map_err(|e| KernelError::OMTAE(OMTAEError::Internal(e)))?; // Store the LLM summary in the canonical session self.memory .store_llm_summary(agent_id, &result.summary, result.kept_messages.clone()) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; // Post-compaction audit: validate and repair the kept messages let (repaired_messages, repair_stats) = - openfang_runtime::session_repair::validate_and_repair_with_stats(&result.kept_messages); + omtae_runtime::session_repair::validate_and_repair_with_stats(&result.kept_messages); // Also update the regular session with the repaired messages let mut updated_session = session; updated_session.messages = repaired_messages; self.memory .save_session(&updated_session) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; // Build result message with audit summary let mut msg = format!( @@ -3638,18 +3899,18 @@ impl OpenFangKernel { pub fn context_report( &self, agent_id: AgentId, - ) -> KernelResult { - use openfang_runtime::compactor::generate_context_report; + ) -> KernelResult { + use omtae_runtime::compactor::generate_context_report; let entry = self.registry.get(agent_id).ok_or_else(|| { - KernelError::OpenFang(OpenFangError::AgentNotFound(agent_id.to_string())) + KernelError::OMTAE(OMTAEError::AgentNotFound(agent_id.to_string())) })?; let session = self .memory .get_session(entry.session_id) - .map_err(KernelError::OpenFang)? - .unwrap_or_else(|| openfang_memory::session::Session { + .map_err(KernelError::OMTAE)? + .unwrap_or_else(|| omtae_memory::session::Session { id: entry.session_id, agent_id, messages: Vec::new(), @@ -3660,11 +3921,17 @@ impl OpenFangKernel { let system_prompt = &entry.manifest.model.system_prompt; // Use the agent's actual filtered tools instead of all builtins let tools = self.available_tools(agent_id); - // Use 200K default or the model's known context window let context_window = if session.context_window_tokens > 0 { - session.context_window_tokens + session.context_window_tokens as usize } else { - 200_000 + self.model_catalog + .read() + .ok() + .and_then(|cat| { + cat.find_model(&entry.manifest.model.model) + .map(|m| m.context_window as usize) + }) + .unwrap_or(200_000) }; Ok(generate_context_report( @@ -3685,11 +3952,11 @@ impl OpenFangKernel { /// See issue #890 — allows an orchestrator agent to wake other agents. pub fn activate_agent(&self, agent_id: AgentId) -> KernelResult { let entry = self.registry.get(agent_id).ok_or_else(|| { - KernelError::OpenFang(OpenFangError::AgentNotFound(agent_id.to_string())) + KernelError::OMTAE(OMTAEError::AgentNotFound(agent_id.to_string())) })?; if entry.state == AgentState::Terminated { - return Err(KernelError::OpenFang(OpenFangError::Internal(format!( + return Err(KernelError::OMTAE(OMTAEError::Internal(format!( "Agent {} is Terminated and cannot be activated", entry.name )))); @@ -3701,7 +3968,7 @@ impl OpenFangKernel { self.registry .set_state(agent_id, AgentState::Running) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; info!( agent = %name, @@ -3718,7 +3985,7 @@ impl OpenFangKernel { let entry = self .registry .remove(agent_id) - .map_err(KernelError::OpenFang)?; + .map_err(KernelError::OMTAE)?; self.background.stop_agent(agent_id); self.scheduler.unregister(agent_id); self.capabilities.revoke_all(agent_id); @@ -3739,7 +4006,7 @@ impl OpenFangKernel { // SECURITY: Record agent kill in audit trail self.audit_log.record( agent_id.to_string(), - openfang_runtime::audit::AuditAction::AgentKill, + omtae_runtime::audit::AuditAction::AgentKill, format!("name={}", entry.name), "ok", ); @@ -3756,14 +4023,14 @@ impl OpenFangKernel { hand_id: &str, config: std::collections::HashMap, instance_name: Option, - ) -> KernelResult { - use openfang_hands::HandError; + ) -> KernelResult { + use omtae_hands::HandError; let def = self .hand_registry .get_definition(hand_id) .ok_or_else(|| { - KernelError::OpenFang(OpenFangError::AgentNotFound(format!( + KernelError::OMTAE(OMTAEError::AgentNotFound(format!( "Hand not found: {hand_id}" ))) })? @@ -3774,10 +4041,10 @@ impl OpenFangKernel { .hand_registry .activate(hand_id, config, instance_name.clone()) .map_err(|e| match e { - HandError::AlreadyActive(id) => KernelError::OpenFang(OpenFangError::Internal( + HandError::AlreadyActive(id) => KernelError::OMTAE(OMTAEError::Internal( format!("Hand already active: {id}"), )), - other => KernelError::OpenFang(OpenFangError::Internal(other.to_string())), + other => KernelError::OMTAE(OMTAEError::Internal(other.to_string())), })?; // Build an agent manifest from the hand definition. @@ -3843,8 +4110,8 @@ impl OpenFangKernel { mcp_servers: def.mcp_servers.clone(), // Hands are curated packages — if they declare shell_exec, grant full exec access exec_policy: if def.tools.iter().any(|t| t == "shell_exec") { - Some(openfang_types::config::ExecPolicy { - mode: openfang_types::config::ExecSecurityMode::Full, + Some(omtae_types::config::ExecPolicy { + mode: omtae_types::config::ExecSecurityMode::Full, timeout_secs: 300, // hands may run long commands (ffmpeg, yt-dlp) no_output_timeout_secs: 120, ..Default::default() @@ -3864,7 +4131,7 @@ impl OpenFangKernel { }; // Resolve hand settings → prompt block + env vars - let resolved = openfang_hands::resolve_settings(&def.settings, &instance.config); + let resolved = omtae_hands::resolve_settings(&def.settings, &instance.config); if !resolved.prompt_block.is_empty() { manifest.model.system_prompt = format!( "{}\n\n---\n\n{}", @@ -3875,8 +4142,8 @@ impl OpenFangKernel { let mut allowed_env = resolved.env_vars; for req in &def.requires { match req.requirement_type { - openfang_hands::RequirementType::ApiKey - | openfang_hands::RequirementType::EnvVar + omtae_hands::RequirementType::ApiKey + | omtae_hands::RequirementType::EnvVar if !req.check_value.is_empty() && !allowed_env.contains(&req.check_value) => { allowed_env.push(req.check_value.clone()); @@ -3917,7 +4184,7 @@ impl OpenFangKernel { // would always be a no-op without this snapshot — same pattern as // saved_triggers above. Fixes the silent loss of cron jobs across // every daemon restart for hand-style agents. - let saved_crons: Vec = old_agent_id + let saved_crons: Vec = old_agent_id .map(|id| self.cron_scheduler.list_jobs(id)) .unwrap_or_default(); if let Some(old) = existing { @@ -3994,7 +4261,7 @@ impl OpenFangKernel { // Link agent to instance self.hand_registry .set_agent(instance.instance_id, agent_id) - .map_err(|e| KernelError::OpenFang(OpenFangError::Internal(e.to_string())))?; + .map_err(|e| KernelError::OMTAE(OMTAEError::Internal(e.to_string())))?; info!( hand = %hand_id, @@ -4018,7 +4285,7 @@ impl OpenFangKernel { let instance = self .hand_registry .deactivate(instance_id) - .map_err(|e| KernelError::OpenFang(OpenFangError::Internal(e.to_string())))?; + .map_err(|e| KernelError::OMTAE(OMTAEError::Internal(e.to_string())))?; if let Some(agent_id) = instance.agent_id { if let Err(e) = self.kill_agent(agent_id) { @@ -4054,14 +4321,14 @@ impl OpenFangKernel { pub fn pause_hand(&self, instance_id: uuid::Uuid) -> KernelResult<()> { self.hand_registry .pause(instance_id) - .map_err(|e| KernelError::OpenFang(OpenFangError::Internal(e.to_string()))) + .map_err(|e| KernelError::OMTAE(OMTAEError::Internal(e.to_string()))) } /// Resume a paused hand. pub fn resume_hand(&self, instance_id: uuid::Uuid) -> KernelResult<()> { self.hand_registry .resume(instance_id) - .map_err(|e| KernelError::OpenFang(OpenFangError::Internal(e.to_string()))) + .map_err(|e| KernelError::OMTAE(OMTAEError::Internal(e.to_string()))) } /// Set the weak self-reference for trigger dispatch. @@ -4074,7 +4341,7 @@ impl OpenFangKernel { // ─── Agent Binding management ────────────────────────────────────── /// List all agent bindings. - pub fn list_bindings(&self) -> Vec { + pub fn list_bindings(&self) -> Vec { self.bindings .lock() .unwrap_or_else(|e| e.into_inner()) @@ -4082,7 +4349,7 @@ impl OpenFangKernel { } /// Add a binding at runtime. - pub fn add_binding(&self, binding: openfang_types::config::AgentBinding) { + pub fn add_binding(&self, binding: omtae_types::config::AgentBinding) { let mut bindings = self.bindings.lock().unwrap_or_else(|e| e.into_inner()); bindings.push(binding); // Sort by specificity descending @@ -4090,7 +4357,7 @@ impl OpenFangKernel { } /// Remove a binding by index, returns the removed binding if valid. - pub fn remove_binding(&self, index: usize) -> Option { + pub fn remove_binding(&self, index: usize) -> Option { let mut bindings = self.bindings.lock().unwrap_or_else(|e| e.into_inner()); if index < bindings.len() { Some(bindings.remove(index)) @@ -4135,7 +4402,7 @@ impl OpenFangKernel { fn apply_hot_actions( &self, plan: &crate::config_reload::ReloadPlan, - new_config: &openfang_types::config::KernelConfig, + new_config: &omtae_types::config::KernelConfig, ) { use crate::config_reload::HotAction; @@ -4244,7 +4511,7 @@ impl OpenFangKernel { ) -> KernelResult { // Verify agent exists if self.registry.get(agent_id).is_none() { - return Err(KernelError::OpenFang(OpenFangError::AgentNotFound( + return Err(KernelError::OMTAE(OMTAEError::AgentNotFound( agent_id.to_string(), ))); } @@ -4287,7 +4554,7 @@ impl OpenFangKernel { .create_run(workflow_id, input) .await .ok_or_else(|| { - KernelError::OpenFang(OpenFangError::Internal("Workflow not found".to_string())) + KernelError::OMTAE(OMTAEError::Internal("Workflow not found".to_string())) })?; // Agent resolver: looks up by name or ID in the registry @@ -4328,12 +4595,12 @@ impl OpenFangKernel { ) .await .map_err(|_| { - KernelError::OpenFang(OpenFangError::Internal(format!( + KernelError::OMTAE(OMTAEError::Internal(format!( "Workflow timed out after {MAX_WORKFLOW_SECS}s" ))) })? .map_err(|e| { - KernelError::OpenFang(OpenFangError::Internal(format!("Workflow failed: {e}"))) + KernelError::OMTAE(OMTAEError::Internal(format!("Workflow failed: {e}"))) })?; Ok((run_id, output)) @@ -4390,7 +4657,7 @@ impl OpenFangKernel { pub fn start_background_agents(self: &Arc) { // Restore previously active hands from persisted state let state_path = self.config.home_dir.join("hand_state.json"); - let saved_hands = openfang_hands::registry::HandRegistry::load_state(&state_path); + let saved_hands = omtae_hands::registry::HandRegistry::load_state(&state_path); if !saved_hands.is_empty() { info!("Restoring {} persisted hand(s)", saved_hands.len()); for (hand_id, config, old_agent_id) in saved_hands { @@ -4444,7 +4711,7 @@ impl OpenFangKernel { } let agents = self.registry.list(); - let mut bg_agents: Vec<(openfang_types::agent::AgentId, String, ScheduleMode)> = Vec::new(); + let mut bg_agents: Vec<(omtae_types::agent::AgentId, String, ScheduleMode)> = Vec::new(); for entry in &agents { if matches!(entry.manifest.schedule, ScheduleMode::Reactive) { @@ -4521,9 +4788,27 @@ impl OpenFangKernel { } for (provider_id, base_url) in &local_providers { - let result = - openfang_runtime::provider_health::probe_provider(provider_id, base_url) - .await; + let api_key = kernel + .model_catalog + .read() + .ok() + .and_then(|catalog| { + catalog.get_provider(provider_id).and_then(|p| { + if p.api_key_env.is_empty() { + None + } else { + std::env::var(&p.api_key_env) + .ok() + .filter(|k| !k.is_empty()) + } + }) + }); + let result = omtae_runtime::provider_health::probe_provider( + provider_id, + base_url, + api_key.as_deref(), + ) + .await; if result.reachable { info!( provider = %provider_id, @@ -4649,7 +4934,7 @@ impl OpenFangKernel { } } - // One-shot migration of legacy shared-memory `__openfang_schedules` + // One-shot migration of legacy shared-memory `__omtae_schedules` // entries (from the old broken `schedule_create` path) into the real // cron scheduler. Idempotent via a marker key. self.migrate_shared_memory_schedules(); @@ -4714,7 +4999,7 @@ impl OpenFangKernel { let kernel = Arc::clone(self); let agents = a2a_config.external_agents.clone(); tokio::spawn(async move { - let discovered = openfang_runtime::a2a::discover_external_agents(&agents).await; + let discovered = omtae_runtime::a2a::discover_external_agents(&agents).await; if let Ok(mut store) = kernel.a2a_external_agents.lock() { *store = discovered; } @@ -4737,7 +5022,7 @@ impl OpenFangKernel { /// Binds a TCP listener, registers with the peer registry, and connects /// to bootstrap peers from config. async fn start_ofp_node(self: &Arc) { - use openfang_wire::{PeerConfig, PeerNode, PeerRegistry}; + use omtae_wire::{PeerConfig, PeerNode, PeerRegistry}; let listen_addr_str = self .config @@ -4763,7 +5048,7 @@ impl OpenFangKernel { }; let node_id = uuid::Uuid::new_v4().to_string(); - let node_name = gethostname().unwrap_or_else(|| "openfang-node".to_string()); + let node_name = gethostname().unwrap_or_else(|| "omtae-node".to_string()); let peer_config = PeerConfig { listen_addr, @@ -4774,7 +5059,7 @@ impl OpenFangKernel { let registry = PeerRegistry::new(); - let handle: Arc = self.self_arc(); + let handle: Arc = self.self_arc(); match PeerNode::start(peer_config, registry.clone(), handle.clone()).await { Ok((node, _accept_task)) => { @@ -5005,6 +5290,33 @@ impl OpenFangKernel { name: &str, schedule: &ScheduleMode, ) { + if self.background.is_paused(agent_id) { + debug!(agent = %name, id = %agent_id, "Skipping background loop — paused"); + return; + } + + // Continuous mode is for Hands and other explicitly scheduled collectors. + // `[autonomous]` on chat agents (e.g. orchestrator) only caps tool rounds per + // user message — it must not authorize a self-prompting background loop. + if let ScheduleMode::Continuous { .. } = schedule { + if let Some(entry) = self.registry.get(agent_id) { + let is_hand = entry + .manifest + .tags + .iter() + .any(|t| t.starts_with("hand:")); + if !is_hand { + warn!( + agent = %name, + id = %agent_id, + "Refusing continuous background loop for non-hand agent. \ + Use reactive (default) or [schedule] periodic for request-driven agents." + ); + return; + } + } + } + // For proactive agents, auto-register triggers from conditions if let ScheduleMode::Proactive { conditions } = schedule { for condition in conditions { @@ -5037,7 +5349,7 @@ impl OpenFangKernel { }); } - /// Migrate legacy `__openfang_schedules` shared-memory entries into the + /// Migrate legacy `__omtae_schedules` shared-memory entries into the /// real cron scheduler. /// /// The old `schedule_create` tool and `/api/schedules` POST route wrote @@ -5050,8 +5362,8 @@ impl OpenFangKernel { /// level). Successfully migrated entries are added to the cron scheduler /// and the scheduler is persisted. pub(crate) fn migrate_shared_memory_schedules(&self) { - const LEGACY_KEY: &str = "__openfang_schedules"; - const MARKER_KEY: &str = "__openfang_schedules_migrated_v1"; + const LEGACY_KEY: &str = "__omtae_schedules"; + const MARKER_KEY: &str = "__omtae_schedules_migrated_v1"; let shared = shared_memory_agent_id(); @@ -5105,7 +5417,7 @@ impl OpenFangKernel { migrated, skipped, total = entries.len(), - "Migrated legacy __openfang_schedules entries to cron scheduler" + "Migrated legacy __omtae_schedules entries to cron scheduler" ); // Clear the legacy key (store an empty array) and mark migrated so @@ -5134,7 +5446,7 @@ impl OpenFangKernel { /// the cron scheduler. Returns `Err` with a human-readable reason when /// the entry cannot be migrated (so the caller can log and skip). fn migrate_single_schedule_entry(&self, entry: &serde_json::Value) -> Result<(), String> { - use openfang_types::scheduler::{ + use omtae_types::scheduler::{ CronAction, CronDelivery, CronJob, CronJobId, CronSchedule, }; @@ -5229,7 +5541,7 @@ impl OpenFangKernel { /// This cleanly shuts down in-memory state but preserves persistent agent /// data so agents are restored on the next boot. pub fn shutdown(&self) { - info!("Shutting down OpenFang kernel..."); + info!("Shutting down OMTAE kernel..."); // Kill WhatsApp gateway child process if running if let Ok(guard) = self.whatsapp_gateway_pid.lock() { @@ -5265,7 +5577,7 @@ impl OpenFangKernel { } info!( - "OpenFang kernel shut down ({} agents preserved)", + "OMTAE kernel shut down ({} agents preserved)", self.registry.list().len() ); } @@ -5412,7 +5724,7 @@ impl OpenFangKernel { .fallback_providers_override .read() .unwrap_or_else(|e: std::sync::PoisonError<_>| e.into_inner()); - let fb_iter: &[openfang_types::config::FallbackProviderConfig] = fb_override + let fb_iter: &[omtae_types::config::FallbackProviderConfig] = fb_override .as_deref() .unwrap_or(&self.config.fallback_providers); for fb in fb_iter { @@ -5447,7 +5759,7 @@ impl OpenFangKernel { // to a specific model, which resolves to a concrete provider through // the catalog. Skip when no override is set. let ch = &self.config.channels; - let channel_overrides: [Option<&openfang_types::config::ChannelOverrides>; 43] = [ + let channel_overrides: [Option<&omtae_types::config::ChannelOverrides>; 43] = [ ch.telegram.as_ref().map(|c| &c.overrides), ch.discord.as_ref().map(|c| &c.overrides), ch.slack.as_ref().map(|c| &c.overrides), @@ -5583,7 +5895,7 @@ impl OpenFangKernel { .fallback_providers_override .read() .unwrap_or_else(|e: std::sync::PoisonError<_>| e.into_inner()); - let effective_fallbacks: &[openfang_types::config::FallbackProviderConfig] = + let effective_fallbacks: &[omtae_types::config::FallbackProviderConfig] = fb_override_guard .as_deref() .unwrap_or(&self.config.fallback_providers); @@ -5691,7 +6003,7 @@ impl OpenFangKernel { // Primary driver uses an empty model name so the request's `model` field // (which is the agent's own model) is used as-is. let mut chain: Vec<( - std::sync::Arc, + std::sync::Arc, String, )> = vec![(primary.clone(), String::new())]; @@ -5790,7 +6102,7 @@ impl OpenFangKernel { if chain.len() > 1 { return Ok(Arc::new( - openfang_runtime::drivers::fallback::FallbackDriver::with_models(chain), + omtae_runtime::drivers::fallback::FallbackDriver::with_models(chain), )); } @@ -5799,8 +6111,8 @@ impl OpenFangKernel { /// Connect to all configured MCP servers and cache their tool definitions. async fn connect_mcp_servers(self: &Arc) { - use openfang_runtime::mcp::{McpConnection, McpServerConfig, McpTransport}; - use openfang_types::config::McpTransportEntry; + use omtae_runtime::mcp::{McpConnection, McpServerConfig, McpTransport}; + use omtae_types::config::McpTransportEntry; let servers = self .effective_mcp_servers @@ -5879,8 +6191,8 @@ impl OpenFangKernel { /// /// Called by the API reload endpoint after CLI installs/removes integrations. pub async fn reload_extension_mcps(self: &Arc) -> Result { - use openfang_runtime::mcp::{McpConnection, McpServerConfig, McpTransport}; - use openfang_types::config::McpTransportEntry; + use omtae_runtime::mcp::{McpConnection, McpServerConfig, McpTransport}; + use omtae_types::config::McpTransportEntry; // 1. Reload installed integrations from disk let installed_count = { @@ -6017,8 +6329,8 @@ impl OpenFangKernel { /// Reconnect a single extension MCP server by ID. pub async fn reconnect_extension_mcp(self: &Arc, id: &str) -> Result { - use openfang_runtime::mcp::{McpConnection, McpServerConfig, McpTransport}; - use openfang_types::config::McpTransportEntry; + use omtae_runtime::mcp::{McpConnection, McpServerConfig, McpTransport}; + use omtae_types::config::McpTransportEntry; // Find the config for this server let server_config = { @@ -6148,7 +6460,7 @@ impl OpenFangKernel { fn available_tools_with_registry( &self, agent_id: AgentId, - skill_snapshot: Option<&openfang_skills::registry::SkillRegistry>, + skill_snapshot: Option<&omtae_skills::registry::SkillRegistry>, ) -> Vec { let all_builtins = if self.config.browser.enabled { builtin_tool_definitions() @@ -6258,12 +6570,12 @@ impl OpenFangKernel { } else { let normalized: Vec = mcp_allowlist .iter() - .map(|s| openfang_runtime::mcp::normalize_name(s)) + .map(|s| omtae_runtime::mcp::normalize_name(s)) .collect(); mcp_tools .iter() .filter(|t| { - openfang_runtime::mcp::extract_mcp_server(&t.name) + omtae_runtime::mcp::extract_mcp_server(&t.name) .map(|s| normalized.iter().any(|n| n == s)) .unwrap_or(false) }) @@ -6311,7 +6623,7 @@ impl OpenFangKernel { e.manifest .exec_policy .as_ref() - .is_some_and(|p| p.mode == openfang_types::config::ExecSecurityMode::Deny) + .is_some_and(|p| p.mode == omtae_types::config::ExecSecurityMode::Deny) }); if exec_blocks_shell { all_tools.retain(|t| t.name != "shell_exec"); @@ -6338,7 +6650,7 @@ impl OpenFangKernel { return; } let skills_dir = self.config.home_dir.join("skills"); - let mut fresh = openfang_skills::registry::SkillRegistry::new(skills_dir); + let mut fresh = omtae_skills::registry::SkillRegistry::new(skills_dir); // Prefer the live override (from `PUT /api/skills/{id}/config`) so // dashboard edits survive hot-reloads without restarting the kernel. // Fall back to the boot-time config. @@ -6392,7 +6704,7 @@ impl OpenFangKernel { /// Build a compact skill summary using the provided registry (which may /// include workspace skill overrides). fn build_skill_summary_from( - registry: &openfang_skills::registry::SkillRegistry, + registry: &omtae_skills::registry::SkillRegistry, skill_allowlist: &[String], ) -> String { let skills: Vec<_> = registry @@ -6424,7 +6736,7 @@ impl OpenFangKernel { summary.push_str(&format!("- {name}: {desc} [tools: {}]\n", tools.join(", "))); } } - // Issue #1038: skill directories (e.g. ~/.openfang/skills/) live OUTSIDE + // Issue #1038: skill directories (e.g. ~/.omtae/skills/) live OUTSIDE // the workspace sandbox. Tell the agent to use the dedicated skill_* // tools instead of falling back to file_read / shell_exec to inspect them. summary.push_str( @@ -6451,7 +6763,7 @@ impl OpenFangKernel { // Normalize allowlist for matching let normalized: Vec = mcp_allowlist .iter() - .map(|s| openfang_runtime::mcp::normalize_name(s)) + .map(|s| omtae_runtime::mcp::normalize_name(s)) .collect(); // Group tools by MCP server prefix (mcp_{server}_{tool}) @@ -6523,7 +6835,7 @@ impl OpenFangKernel { /// Collect prompt context using the provided registry (which may include /// workspace skill overrides). fn collect_prompt_context_from( - registry: &openfang_skills::registry::SkillRegistry, + registry: &omtae_skills::registry::SkillRegistry, skill_allowlist: &[String], ) -> String { let mut context_parts = Vec::new(); @@ -6536,7 +6848,7 @@ impl OpenFangKernel { if !ctx.is_empty() { let is_bundled = matches!( skill.manifest.source, - Some(openfang_skills::SkillSource::Bundled) + Some(omtae_skills::SkillSource::Bundled) ); if is_bundled { // Bundled skills are trusted (shipped with binary) @@ -6572,9 +6884,9 @@ impl OpenFangKernel { /// Records success/failure on the job's metadata just like the scheduler does. pub async fn cron_run_job( self: &Arc, - job: &openfang_types::scheduler::CronJob, + job: &omtae_types::scheduler::CronJob, ) -> Result { - use openfang_types::scheduler::CronAction; + use omtae_types::scheduler::CronAction; let job_id = job.id; let agent_id = job.agent_id; @@ -6730,11 +7042,23 @@ impl OpenFangKernel { } } -/// Convert a manifest's capability declarations into Capability enums. -/// -/// If a `profile` is set and the manifest has no explicit tools, the profile's -/// implied capabilities are used as a base — preserving any non-tool overrides -/// from the manifest. +/// Request-driven meta-agents use `[autonomous]` only for per-message iteration caps. +/// They must never run a continuous self-prompt loop (causes context overflow on vLLM). +pub(crate) fn normalize_request_driven_agent_schedule(manifest: &mut AgentManifest) { + const REQUEST_DRIVEN: &[&str] = &["orchestrator"]; + if !REQUEST_DRIVEN.iter().any(|n| n == &manifest.name) { + return; + } + if !matches!(manifest.schedule, ScheduleMode::Reactive) { + warn!( + agent = %manifest.name, + schedule = ?manifest.schedule, + "Resetting schedule to reactive for request-driven meta-agent" + ); + manifest.schedule = ScheduleMode::Reactive; + } +} + /// Merge `disk` (manifest read from agent.toml) onto `entry` (manifest in DB), /// preserving kernel-assigned defaults that the user didn't write to TOML. /// @@ -6829,7 +7153,7 @@ fn manifest_to_capabilities(manifest: &AgentManifest) -> Vec { /// When the global budget config specifies limits and the agent still has /// the built-in defaults, override them so agents respect the user's config. fn apply_budget_defaults( - budget: &openfang_types::config::BudgetConfig, + budget: &omtae_types::config::BudgetConfig, resources: &mut ResourceQuota, ) { // Only override hourly if agent has unlimited (0.0) and global is set @@ -6989,12 +7313,12 @@ fn sanitize_cron_job_name(raw: &str) -> String { /// Deliver a cron job's agent response to the configured delivery target. async fn cron_deliver_response( - kernel: &OpenFangKernel, + kernel: &OMTAEKernel, agent_id: AgentId, response: &str, - delivery: &openfang_types::scheduler::CronDelivery, + delivery: &omtae_types::scheduler::CronDelivery, ) -> Result<(), String> { - use openfang_types::scheduler::CronDelivery; + use omtae_types::scheduler::CronDelivery; if response.is_empty() { return Ok(()); @@ -7080,11 +7404,11 @@ async fn cron_deliver_response( /// (they intentionally return "not implemented" / empty values since the /// fan-out engine never calls them). struct KernelCronBridge { - kernel: Arc, + kernel: Arc, } #[async_trait] -impl openfang_channels::bridge::ChannelBridgeHandle for KernelCronBridge { +impl omtae_channels::bridge::ChannelBridgeHandle for KernelCronBridge { async fn send_message(&self, _agent_id: AgentId, _message: &str) -> Result { Err("KernelCronBridge only supports send_channel_message".to_string()) } @@ -7120,15 +7444,15 @@ impl openfang_channels::bridge::ChannelBridgeHandle for KernelCronBridge { /// has already succeeded. Per-target failures are logged and counted, and /// the aggregate pass/fail counts are returned for the scheduler log. async fn cron_fan_out_targets( - kernel: &Arc, + kernel: &Arc, job_name: &str, output: &str, - targets: &[openfang_types::scheduler::CronDeliveryTarget], + targets: &[omtae_types::scheduler::CronDeliveryTarget], ) { if targets.is_empty() || output.is_empty() { return; } - let bridge: Arc = + let bridge: Arc = Arc::new(KernelCronBridge { kernel: kernel.clone(), }); @@ -7163,14 +7487,14 @@ async fn cron_fan_out_targets( } #[async_trait] -impl KernelHandle for OpenFangKernel { +impl KernelHandle for OMTAEKernel { async fn spawn_agent( &self, manifest_toml: &str, parent_id: Option<&str>, ) -> Result<(String, String), String> { // Verify manifest integrity if a signed manifest hash is present - let content_hash = openfang_types::manifest_signing::hash_manifest(manifest_toml); + let content_hash = omtae_types::manifest_signing::hash_manifest(manifest_toml); tracing::debug!(hash = %content_hash, "Manifest SHA-256 computed for integrity tracking"); let manifest: AgentManifest = @@ -7183,21 +7507,81 @@ impl KernelHandle for OpenFangKernel { Ok((id.to_string(), name)) } - async fn send_to_agent(&self, agent_id: &str, message: &str) -> Result { - // Try UUID first, then fall back to name lookup - let id: AgentId = match agent_id.parse() { - Ok(id) => id, - Err(_) => self - .registry - .find_by_name(agent_id) - .map(|e| e.id) - .ok_or_else(|| format!("Agent not found: {agent_id}"))?, - }; + async fn send_to_agent( + &self, + agent_id: &str, + message: &str, + caller_id: Option<&str>, + caller_name: Option<&str>, + ) -> Result { + let entry = self + .registry + .find_by_name_insensitive(agent_id) + .or_else(|| { + agent_id + .parse::() + .ok() + .and_then(|id| self.registry.get(id)) + }) + .ok_or_else(|| { + format!( + "Agent not found: '{agent_id}'. Call agent_list first, then pass agent_id (UUID) from that list." + ) + })?; + + let id = entry.id; + let target_name = entry.name.clone(); + + match entry.state { + AgentState::Terminated => { + return Err(format!( + "agent_send FAILED: agent '{target_name}' (id: {id}) is Terminated and cannot receive messages." + )); + } + AgentState::Running => {} + _ => { + let woken = self + .activate_agent(id) + .map_err(|e| format!("agent_send FAILED: could not activate '{target_name}': {e}"))?; + info!(agent = %woken, state = ?entry.state, "Activated specialist for agent_send"); + } + } + + if let Err(e) = self.prepare_session_for_inbound(id).await { + warn!(target = %agent_id, error = %e, "Session prep before agent_send failed"); + } + + let handle: Option> = self + .self_handle + .get() + .and_then(|w| w.upgrade()) + .map(|arc| arc as Arc); + let result = self - .send_message(id, message) + .send_message_with_handle( + id, + message, + handle, + caller_id.map(str::to_string), + caller_name.map(str::to_string), + ) .await - .map_err(|e| format!("Send failed: {e}"))?; - Ok(result.response) + .map_err(|e| format!("agent_send FAILED: Send failed: {e}"))?; + + let body = result.response.trim(); + if result.silent || body.is_empty() { + return Err(format!( + "agent_send FAILED: agent '{target_name}' (id: {id}) returned an empty response. \ + The specialist may have hit context limits, returned NO_REPLY, or failed silently. \ + Run agent_list and check state; retry after compaction or with a shorter message." + )); + } + + Ok(format!( + "agent_send OK — target: {target_name} (id: {id})\n\n\ + --- Specialist response (quote verbatim in your reply; do not invent text) ---\n\ + {body}" + )) } fn list_agents(&self) -> Vec { @@ -7227,7 +7611,7 @@ impl KernelHandle for OpenFangKernel { let id: AgentId = agent_id .parse() .map_err(|_| "Invalid agent ID".to_string())?; - OpenFangKernel::kill_agent(self, id).map_err(|e| format!("Kill failed: {e}")) + OMTAEKernel::kill_agent(self, id).map_err(|e| format!("Kill failed: {e}")) } fn activate_agent(&self, agent_id: &str) -> Result { @@ -7240,7 +7624,7 @@ impl KernelHandle for OpenFangKernel { .map(|e| e.id) .ok_or_else(|| format!("Agent not found: {agent_id}"))?, }; - OpenFangKernel::activate_agent(self, id).map_err(|e| format!("Activate failed: {e}")) + OMTAEKernel::activate_agent(self, id).map_err(|e| format!("Activate failed: {e}")) } fn memory_store(&self, key: &str, value: serde_json::Value) -> Result<(), String> { @@ -7335,13 +7719,13 @@ impl KernelHandle for OpenFangKernel { EventTarget::Broadcast, EventPayload::Custom(payload_bytes), ); - OpenFangKernel::publish_event(self, event).await; + OMTAEKernel::publish_event(self, event).await; Ok(()) } async fn knowledge_add_entity( &self, - entity: openfang_types::memory::Entity, + entity: omtae_types::memory::Entity, ) -> Result { self.memory .add_entity(entity) @@ -7351,7 +7735,7 @@ impl KernelHandle for OpenFangKernel { async fn knowledge_add_relation( &self, - relation: openfang_types::memory::Relation, + relation: omtae_types::memory::Relation, ) -> Result { self.memory .add_relation(relation) @@ -7361,8 +7745,8 @@ impl KernelHandle for OpenFangKernel { async fn knowledge_query( &self, - pattern: openfang_types::memory::GraphPattern, - ) -> Result, String> { + pattern: omtae_types::memory::GraphPattern, + ) -> Result, String> { self.memory .query_graph(pattern) .await @@ -7377,7 +7761,7 @@ impl KernelHandle for OpenFangKernel { agent_id: &str, job_json: serde_json::Value, ) -> Result { - use openfang_types::scheduler::{ + use omtae_types::scheduler::{ CronAction, CronDelivery, CronDeliveryTarget, CronJob, CronJobId, CronSchedule, }; @@ -7403,7 +7787,7 @@ impl KernelHandle for OpenFangKernel { }; let one_shot = job_json["one_shot"].as_bool().unwrap_or(false); - let aid = openfang_types::agent::AgentId( + let aid = omtae_types::agent::AgentId( uuid::Uuid::parse_str(agent_id).map_err(|e| format!("Invalid agent ID: {e}"))?, ); @@ -7439,7 +7823,7 @@ impl KernelHandle for OpenFangKernel { } async fn cron_list(&self, agent_id: &str) -> Result, String> { - let aid = openfang_types::agent::AgentId( + let aid = omtae_types::agent::AgentId( uuid::Uuid::parse_str(agent_id).map_err(|e| format!("Invalid agent ID: {e}"))?, ); let jobs = self.cron_scheduler.list_jobs(aid); @@ -7451,7 +7835,7 @@ impl KernelHandle for OpenFangKernel { } async fn cron_cancel(&self, job_id: &str) -> Result<(), String> { - let id = openfang_types::scheduler::CronJobId( + let id = omtae_types::scheduler::CronJobId( uuid::Uuid::parse_str(job_id).map_err(|e| format!("Invalid job ID: {e}"))?, ); self.cron_scheduler @@ -7579,7 +7963,7 @@ impl KernelHandle for OpenFangKernel { tool_name: &str, action_summary: &str, ) -> Result { - use openfang_types::approval::{ApprovalDecision, ApprovalRequest as TypedRequest}; + use omtae_types::approval::{ApprovalDecision, ApprovalRequest as TypedRequest}; // Hand agents are curated trusted packages — auto-approve tool execution. // Check if this agent has a "hand:" tag indicating it was spawned by activate_hand(). @@ -7674,10 +8058,10 @@ impl KernelHandle for OpenFangKernel { })? .clone(); - let user = openfang_channels::types::ChannelUser { + let user = omtae_channels::types::ChannelUser { platform_id: recipient.to_string(), display_name: recipient.to_string(), - openfang_user: None, + omtae_user: None, }; let formatted = if channel == "wecom" { @@ -7688,12 +8072,12 @@ impl KernelHandle for OpenFangKernel { .as_ref() .and_then(|c| c.overrides.output_format) .unwrap_or(OutputFormat::PlainText); - openfang_channels::formatter::format_for_wecom(message, output_format) + omtae_channels::formatter::format_for_wecom(message, output_format) } else { message.to_string() }; - let content = openfang_channels::types::ChannelContent::Text(formatted); + let content = omtae_channels::types::ChannelContent::Text(formatted); if let Some(tid) = thread_id { adapter @@ -7736,18 +8120,18 @@ impl KernelHandle for OpenFangKernel { })? .clone(); - let user = openfang_channels::types::ChannelUser { + let user = omtae_channels::types::ChannelUser { platform_id: recipient.to_string(), display_name: recipient.to_string(), - openfang_user: None, + omtae_user: None, }; let content = match media_type { - "image" => openfang_channels::types::ChannelContent::Image { + "image" => omtae_channels::types::ChannelContent::Image { url: media_url.to_string(), caption: caption.map(|s| s.to_string()), }, - "file" => openfang_channels::types::ChannelContent::File { + "file" => omtae_channels::types::ChannelContent::File { url: media_url.to_string(), filename: filename.unwrap_or("file").to_string(), mime: None, @@ -7803,13 +8187,13 @@ impl KernelHandle for OpenFangKernel { })? .clone(); - let user = openfang_channels::types::ChannelUser { + let user = omtae_channels::types::ChannelUser { platform_id: recipient.to_string(), display_name: recipient.to_string(), - openfang_user: None, + omtae_user: None, }; - let content = openfang_channels::types::ChannelContent::FileData { + let content = omtae_channels::types::ChannelContent::FileData { data, filename: filename.to_string(), mime_type: mime_type.to_string(), @@ -7837,7 +8221,7 @@ impl KernelHandle for OpenFangKernel { &self, manifest_toml: &str, parent_id: Option<&str>, - parent_caps: &[openfang_types::capability::Capability], + parent_caps: &[omtae_types::capability::Capability], ) -> Result<(String, String), String> { // Parse the child manifest to extract its capabilities let child_manifest: AgentManifest = @@ -7845,7 +8229,7 @@ impl KernelHandle for OpenFangKernel { let child_caps = manifest_to_capabilities(&child_manifest); // Enforce: child capabilities must be a subset of parent capabilities - openfang_types::capability::validate_capability_inheritance(parent_caps, &child_caps)?; + omtae_types::capability::validate_capability_inheritance(parent_caps, &child_caps)?; tracing::info!( parent = parent_id.unwrap_or("kernel"), @@ -7862,12 +8246,12 @@ impl KernelHandle for OpenFangKernel { // --- OFP Wire Protocol integration --- #[async_trait] -impl openfang_wire::peer::PeerHandle for OpenFangKernel { - fn local_agents(&self) -> Vec { +impl omtae_wire::peer::PeerHandle for OMTAEKernel { + fn local_agents(&self) -> Vec { self.registry .list() .iter() - .map(|entry| openfang_wire::message::RemoteAgentInfo { + .map(|entry| omtae_wire::message::RemoteAgentInfo { id: entry.id.0.to_string(), name: entry.name.clone(), description: entry.manifest.description.clone(), @@ -7897,13 +8281,17 @@ impl openfang_wire::peer::PeerHandle for OpenFangKernel { .ok_or_else(|| format!("Agent not found: {agent}"))? }; + if let Err(e) = self.prepare_session_for_inbound(agent_id).await { + warn!(target = %agent, error = %e, "Session prep before peer agent message failed"); + } + match self.send_message(agent_id, message).await { Ok(result) => Ok(result.response), Err(e) => Err(format!("{e}")), } } - fn discover_agents(&self, query: &str) -> Vec { + fn discover_agents(&self, query: &str) -> Vec { let q = query.to_lowercase(); self.registry .list() @@ -7917,7 +8305,7 @@ impl openfang_wire::peer::PeerHandle for OpenFangKernel { .iter() .any(|t| t.to_lowercase().contains(&q)) }) - .map(|entry| openfang_wire::message::RemoteAgentInfo { + .map(|entry| omtae_wire::message::RemoteAgentInfo { id: entry.id.0.to_string(), name: entry.name.clone(), description: entry.manifest.description.clone(), @@ -7936,7 +8324,7 @@ impl openfang_wire::peer::PeerHandle for OpenFangKernel { #[cfg(test)] mod tests { use super::*; - use openfang_types::config::ExecPolicy; + use omtae_types::config::ExecPolicy; use std::collections::HashMap; #[test] @@ -8006,7 +8394,7 @@ mod tests { routing: None, autonomous: None, pinned_model: None, - workspace: Some(std::path::PathBuf::from("/var/lib/openfang/agents/demo")), + workspace: Some(std::path::PathBuf::from("/var/lib/omtae/agents/demo")), state_dir: None, generate_identity_files: true, exec_policy: Some(ExecPolicy::default()), @@ -8033,6 +8421,19 @@ mod tests { ); } + #[test] + fn test_orchestrator_schedule_forced_reactive() { + let mut manifest = AgentManifest { + name: "orchestrator".to_string(), + schedule: ScheduleMode::Continuous { + check_interval_secs: 120, + }, + ..Default::default() + }; + normalize_request_driven_agent_schedule(&mut manifest); + assert_eq!(manifest.schedule, ScheduleMode::Reactive); + } + /// User explicitly setting workspace in TOML must take effect. #[test] fn test_merge_respects_explicit_disk_workspace() { @@ -8086,7 +8487,7 @@ mod tests { /// the on-disk `agent.toml`. #[test] fn test_exec_policy_reinherits_from_kernel_config_on_restart() { - use openfang_types::config::ExecSecurityMode; + use omtae_types::config::ExecSecurityMode; // Cached manifest from an earlier boot — still Allowlist. let cached_policy = ExecPolicy { @@ -8166,7 +8567,7 @@ mod tests { /// config.toml edits take effect. #[test] fn test_persist_strips_inherited_exec_policy() { - use openfang_types::config::ExecSecurityMode; + use omtae_types::config::ExecSecurityMode; let kernel_policy = ExecPolicy { mode: ExecSecurityMode::Full, @@ -8343,7 +8744,7 @@ mod tests { #[test] fn test_manifest_to_capabilities_with_profile() { - use openfang_types::agent::ToolProfile; + use omtae_types::agent::ToolProfile; let manifest = AgentManifest { profile: Some(ToolProfile::Coding), ..Default::default() @@ -8362,7 +8763,7 @@ mod tests { #[test] fn test_manifest_to_capabilities_profile_overridden_by_explicit_tools() { - use openfang_types::agent::ToolProfile; + use omtae_types::agent::ToolProfile; let mut manifest = AgentManifest { profile: Some(ToolProfile::Coding), ..Default::default() @@ -8382,7 +8783,7 @@ mod tests { #[test] fn test_hand_activation_does_not_seed_runtime_tool_filters() { let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-kernel-hand-test"); + let home_dir = tmp.path().join("omtae-kernel-hand-test"); std::fs::create_dir_all(&home_dir).unwrap(); let config = KernelConfig { @@ -8391,7 +8792,7 @@ mod tests { ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let instance = kernel .activate_hand("browser", HashMap::new(), None) .expect("browser hand should activate"); @@ -8421,7 +8822,7 @@ mod tests { #[test] fn test_hand_owned_agent_stop_clears_hand_for_reactivation() { let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-kernel-hand-stop-test"); + let home_dir = tmp.path().join("omtae-kernel-hand-stop-test"); std::fs::create_dir_all(&home_dir).unwrap(); let config = KernelConfig { @@ -8429,7 +8830,7 @@ mod tests { data_dir: home_dir.join("data"), ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boots"); // Activate a hand and grab its agent id (mirrors what the wizard does). let instance = kernel @@ -8481,7 +8882,7 @@ mod tests { #[test] fn test_activate_agent_wakes_suspended_and_crashed() { let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-kernel-activate-test"); + let home_dir = tmp.path().join("omtae-kernel-activate-test"); std::fs::create_dir_all(&home_dir).unwrap(); let config = KernelConfig { @@ -8489,7 +8890,7 @@ mod tests { data_dir: home_dir.join("data"), ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boots"); // Suspended agent: should flip to Running. let suspended = register_test_agent(&kernel, "sleepy"); @@ -8556,10 +8957,10 @@ mod tests { #[test] fn test_activate_agent_handle_accepts_name_and_uuid() { - use openfang_runtime::kernel_handle::KernelHandle; + use omtae_runtime::kernel_handle::KernelHandle; let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-kernel-activate-handle-test"); + let home_dir = tmp.path().join("omtae-kernel-activate-handle-test"); std::fs::create_dir_all(&home_dir).unwrap(); let config = KernelConfig { @@ -8567,7 +8968,7 @@ mod tests { data_dir: home_dir.join("data"), ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boots"); let agent = register_test_agent(&kernel, "worker"); kernel @@ -8637,7 +9038,7 @@ mod tests { /// Register a minimal test agent in a booted kernel and return its ID. /// Kept local to the tests module to avoid widening the kernel's public /// surface. - fn register_test_agent(kernel: &OpenFangKernel, name: &str) -> AgentId { + fn register_test_agent(kernel: &OMTAEKernel, name: &str) -> AgentId { let agent_id = AgentId::new(); let entry = AgentEntry { id: agent_id, @@ -8662,7 +9063,7 @@ mod tests { #[test] fn test_migrate_shared_memory_schedules_imports_legacy_entries() { let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-migrate"); + let home_dir = tmp.path().join("omtae-migrate"); std::fs::create_dir_all(&home_dir).unwrap(); let config = KernelConfig { home_dir: home_dir.clone(), @@ -8670,7 +9071,7 @@ mod tests { ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boots"); // Register a target agent the legacy entries can point at. let agent = register_test_agent(&kernel, "report-agent"); @@ -8694,7 +9095,7 @@ mod tests { ]); kernel .memory - .structured_set(shared, "__openfang_schedules", legacy_entries) + .structured_set(shared, "__omtae_schedules", legacy_entries) .unwrap(); // Sanity: before migration, the cron scheduler is empty. @@ -8718,12 +9119,12 @@ mod tests { // it again. let remaining = kernel .memory - .structured_get(shared, "__openfang_schedules") + .structured_get(shared, "__omtae_schedules") .unwrap(); assert_eq!(remaining, Some(serde_json::Value::Array(vec![]))); let marker = kernel .memory - .structured_get(shared, "__openfang_schedules_migrated_v1") + .structured_get(shared, "__omtae_schedules_migrated_v1") .unwrap(); assert_eq!(marker, Some(serde_json::Value::Bool(true))); @@ -8733,14 +9134,14 @@ mod tests { #[test] fn test_migrate_shared_memory_schedules_is_idempotent() { let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-migrate-idem"); + let home_dir = tmp.path().join("omtae-migrate-idem"); std::fs::create_dir_all(&home_dir).unwrap(); let config = KernelConfig { home_dir: home_dir.clone(), data_dir: home_dir.join("data"), ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boots"); let agent = register_test_agent(&kernel, "idem-agent"); let shared = super::shared_memory_agent_id(); @@ -8748,7 +9149,7 @@ mod tests { .memory .structured_set( shared, - "__openfang_schedules", + "__omtae_schedules", serde_json::json!([{ "description": "Ping", "cron": "*/5 * * * *", @@ -8766,7 +9167,7 @@ mod tests { .memory .structured_set( shared, - "__openfang_schedules", + "__omtae_schedules", serde_json::json!([{ "description": "Ping again", "cron": "*/5 * * * *", @@ -8787,14 +9188,14 @@ mod tests { #[test] fn test_migrate_shared_memory_schedules_skips_unknown_agent() { let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-migrate-skip"); + let home_dir = tmp.path().join("omtae-migrate-skip"); std::fs::create_dir_all(&home_dir).unwrap(); let config = KernelConfig { home_dir: home_dir.clone(), data_dir: home_dir.join("data"), ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boots"); let shared = super::shared_memory_agent_id(); // Entry references an agent that does not exist in the registry. @@ -8802,7 +9203,7 @@ mod tests { .memory .structured_set( shared, - "__openfang_schedules", + "__omtae_schedules", serde_json::json!([{ "description": "Ping", "cron": "*/5 * * * *", @@ -8817,7 +9218,7 @@ mod tests { assert_eq!(kernel.cron_scheduler.total_jobs(), 0); let marker = kernel .memory - .structured_get(shared, "__openfang_schedules_migrated_v1") + .structured_get(shared, "__omtae_schedules_migrated_v1") .unwrap(); assert_eq!(marker, Some(serde_json::Value::Bool(true))); @@ -8836,10 +9237,10 @@ mod tests { #[test] fn test_subprocess_timeout_hot_reload_fallback_providers() { use crate::config_reload::{build_reload_plan, HotAction}; - use openfang_types::config::FallbackProviderConfig; + use omtae_types::config::FallbackProviderConfig; let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-1129-fallback-timeout"); + let home_dir = tmp.path().join("omtae-1129-fallback-timeout"); std::fs::create_dir_all(&home_dir).unwrap(); // Boot with one fallback provider configured at 120s. @@ -8856,7 +9257,7 @@ mod tests { subprocess_timeout_secs: Some(120), }); - let kernel = OpenFangKernel::boot_with_config(config.clone()).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config.clone()).expect("kernel boots"); // Pre-condition: nothing has been hot-reloaded yet — override slot is empty. { @@ -8913,7 +9314,7 @@ mod tests { use crate::config_reload::{build_reload_plan, HotAction}; let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-1129-default-timeout"); + let home_dir = tmp.path().join("omtae-1129-default-timeout"); std::fs::create_dir_all(&home_dir).unwrap(); let mut config = KernelConfig { @@ -8923,7 +9324,7 @@ mod tests { }; config.default_model.subprocess_timeout_secs = Some(180); - let kernel = OpenFangKernel::boot_with_config(config.clone()).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config.clone()).expect("kernel boots"); // Operator raises the timeout to 1200s. let mut new_config = config.clone(); @@ -8959,10 +9360,10 @@ mod tests { #[test] fn test_subprocess_timeout_hot_reload_adds_new_fallback() { use crate::config_reload::{build_reload_plan, HotAction}; - use openfang_types::config::FallbackProviderConfig; + use omtae_types::config::FallbackProviderConfig; let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-1129-add-fallback"); + let home_dir = tmp.path().join("omtae-1129-add-fallback"); std::fs::create_dir_all(&home_dir).unwrap(); let config = KernelConfig { @@ -8970,7 +9371,7 @@ mod tests { data_dir: home_dir.join("data"), ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config.clone()).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config.clone()).expect("kernel boots"); // Operator adds a codex fallback with a 600s timeout. let mut new_config = config.clone(); @@ -9010,10 +9411,10 @@ mod tests { #[test] fn test_referenced_providers_only_includes_configured_ones() { - use openfang_types::config::{DefaultModelConfig, FallbackProviderConfig}; + use omtae_types::config::{DefaultModelConfig, FallbackProviderConfig}; let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-1031-referenced"); + let home_dir = tmp.path().join("omtae-1031-referenced"); std::fs::create_dir_all(&home_dir).unwrap(); // Operator uses Groq as the default and Ollama as a single fallback. @@ -9047,7 +9448,7 @@ mod tests { ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boots"); let referenced = kernel.referenced_providers(); // Configured providers ARE referenced. @@ -9082,10 +9483,10 @@ mod tests { #[test] fn test_1188_referenced_providers_resolves_alias_to_provider() { - use openfang_types::config::DefaultModelConfig; + use omtae_types::config::DefaultModelConfig; let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-1188-alias"); + let home_dir = tmp.path().join("omtae-1188-alias"); std::fs::create_dir_all(&home_dir).unwrap(); // Operator sets provider = "default" and picks the model by its @@ -9105,7 +9506,7 @@ mod tests { ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boots"); let referenced = kernel.referenced_providers(); assert!( referenced.contains("anthropic"), @@ -9116,12 +9517,12 @@ mod tests { #[test] fn test_1188_referenced_providers_walks_channel_overrides() { - use openfang_types::config::{ + use omtae_types::config::{ ChannelOverrides, ChannelsConfig, DefaultModelConfig, TelegramConfig, }; let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-1188-channel"); + let home_dir = tmp.path().join("omtae-1188-channel"); std::fs::create_dir_all(&home_dir).unwrap(); let overrides = ChannelOverrides { @@ -9149,7 +9550,7 @@ mod tests { ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boots"); let referenced = kernel.referenced_providers(); assert!( referenced.contains("anthropic"), @@ -9160,10 +9561,10 @@ mod tests { #[test] fn test_1188_referenced_providers_walks_mcp_env() { - use openfang_types::config::{DefaultModelConfig, McpServerConfigEntry, McpTransportEntry}; + use omtae_types::config::{DefaultModelConfig, McpServerConfigEntry, McpTransportEntry}; let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-1188-mcp"); + let home_dir = tmp.path().join("omtae-1188-mcp"); std::fs::create_dir_all(&home_dir).unwrap(); // MCP server passes through OPENAI_API_KEY, which is the api_key_env @@ -9191,7 +9592,7 @@ mod tests { ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boots"); let referenced = kernel.referenced_providers(); assert!( referenced.contains("openai"), @@ -9202,10 +9603,10 @@ mod tests { #[test] fn test_1188_referenced_providers_walks_skill_tags() { - use openfang_types::config::DefaultModelConfig; + use omtae_types::config::DefaultModelConfig; let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-1188-skill"); + let home_dir = tmp.path().join("omtae-1188-skill"); std::fs::create_dir_all(&home_dir).unwrap(); let config = KernelConfig { @@ -9221,7 +9622,7 @@ mod tests { ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boots"); // Drop a minimal skill manifest with a tag matching a known // provider ID, then load it through the kernel's registry. @@ -9252,20 +9653,20 @@ type = "promptonly" } // ---------------------------------------------------------------------- - // Issue #1140: agents placed at ~/.openfang/agents//agent.toml + // Issue #1140: agents placed at ~/.omtae/agents//agent.toml // must auto-spawn on boot so they appear in the chat tab. // ---------------------------------------------------------------------- #[test] fn test_1140_auto_spawn_agents_from_disk() { let tmp = tempfile::tempdir().unwrap(); - let home_dir = tmp.path().join("openfang-1140"); + let home_dir = tmp.path().join("omtae-1140"); let agents_dir = home_dir.join("agents"); std::fs::create_dir_all(agents_dir.join("my-custom-agent")).unwrap(); // Write a minimal valid agent.toml for a user-placed agent. let manifest_toml = r#" name = "my-custom-agent" -description = "A user-installed agent placed in ~/.openfang/agents" +description = "A user-installed agent placed in ~/.omtae/agents" [model] provider = "default" @@ -9294,13 +9695,13 @@ system_prompt = "You are a test agent." data_dir: home_dir.join("data"), ..KernelConfig::default() }; - let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots"); + let kernel = OMTAEKernel::boot_with_config(config).expect("kernel boots"); // The disk-placed agent must be in the registry and visible via list(). let entry = kernel .registry .find_by_name("my-custom-agent") - .expect("my-custom-agent must be auto-spawned from ~/.openfang/agents"); + .expect("my-custom-agent must be auto-spawned from ~/.omtae/agents"); assert_eq!(entry.name, "my-custom-agent"); // GET /api/agents pulls from kernel.registry.list(); confirm the agent @@ -9328,7 +9729,7 @@ system_prompt = "You are a test agent." data_dir: home_dir.join("data"), ..KernelConfig::default() }; - let kernel2 = OpenFangKernel::boot_with_config(config2).expect("kernel re-boots"); + let kernel2 = OMTAEKernel::boot_with_config(config2).expect("kernel re-boots"); let count_after = kernel2.registry.list().len(); assert_eq!( count_before, count_after, @@ -9346,7 +9747,7 @@ system_prompt = "You are a test agent." /// private state directory; only the lightweight user-facing layout /// (`data/`, `output/`, `skills/`) may appear in the workspace. #[test] - fn test_workspace_outside_openfang_stays_clean() { + fn test_workspace_outside_omtae_stays_clean() { use tempfile::TempDir; let user_workspace = TempDir::new().expect("temp user workspace"); diff --git a/crates/openfang-kernel/src/lib.rs b/crates/openfang-kernel/src/lib.rs index 289e183ad2..c1a43f5ece 100644 --- a/crates/openfang-kernel/src/lib.rs +++ b/crates/openfang-kernel/src/lib.rs @@ -1,4 +1,4 @@ -//! Core kernel for the OpenFang Agent Operating System. +//! Core kernel for the OMTAE Agent Operating System. //! //! The kernel manages agent lifecycles, memory, permissions, scheduling, //! and inter-agent communication. @@ -12,6 +12,7 @@ pub mod config; pub mod config_reload; pub mod cron; pub mod cron_delivery; +pub mod drift_guard; pub mod error; pub mod event_bus; pub mod heartbeat; @@ -27,4 +28,4 @@ pub mod wizard; pub mod workflow; pub use kernel::DeliveryTracker; -pub use kernel::OpenFangKernel; +pub use kernel::OMTAEKernel; diff --git a/crates/openfang-kernel/src/metering.rs b/crates/openfang-kernel/src/metering.rs index 1671794edb..4630872822 100644 --- a/crates/openfang-kernel/src/metering.rs +++ b/crates/openfang-kernel/src/metering.rs @@ -1,8 +1,8 @@ //! Metering engine — tracks LLM cost and enforces spending quotas. -use openfang_memory::usage::{ModelUsage, UsageRecord, UsageStore, UsageSummary}; -use openfang_types::agent::{AgentId, ResourceQuota}; -use openfang_types::error::{OpenFangError, OpenFangResult}; +use omtae_memory::usage::{ModelUsage, UsageRecord, UsageStore, UsageSummary}; +use omtae_types::agent::{AgentId, ResourceQuota}; +use omtae_types::error::{OMTAEError, OMTAEResult}; use std::sync::Arc; /// The metering engine tracks usage cost and enforces quota limits. @@ -18,18 +18,18 @@ impl MeteringEngine { } /// Record a usage event (persists to SQLite). - pub fn record(&self, record: &UsageRecord) -> OpenFangResult<()> { + pub fn record(&self, record: &UsageRecord) -> OMTAEResult<()> { self.store.record(record) } /// Check if an agent is within its spending quotas (hourly, daily, monthly). /// Returns Ok(()) if under all quotas, or QuotaExceeded error if over any. - pub fn check_quota(&self, agent_id: AgentId, quota: &ResourceQuota) -> OpenFangResult<()> { + pub fn check_quota(&self, agent_id: AgentId, quota: &ResourceQuota) -> OMTAEResult<()> { // Hourly check if quota.max_cost_per_hour_usd > 0.0 { let hourly_cost = self.store.query_hourly(agent_id)?; if hourly_cost >= quota.max_cost_per_hour_usd { - return Err(OpenFangError::QuotaExceeded(format!( + return Err(OMTAEError::QuotaExceeded(format!( "Agent {} exceeded hourly cost quota: ${:.4} / ${:.4}", agent_id, hourly_cost, quota.max_cost_per_hour_usd ))); @@ -40,7 +40,7 @@ impl MeteringEngine { if quota.max_cost_per_day_usd > 0.0 { let daily_cost = self.store.query_daily(agent_id)?; if daily_cost >= quota.max_cost_per_day_usd { - return Err(OpenFangError::QuotaExceeded(format!( + return Err(OMTAEError::QuotaExceeded(format!( "Agent {} exceeded daily cost quota: ${:.4} / ${:.4}", agent_id, daily_cost, quota.max_cost_per_day_usd ))); @@ -51,7 +51,7 @@ impl MeteringEngine { if quota.max_cost_per_month_usd > 0.0 { let monthly_cost = self.store.query_monthly(agent_id)?; if monthly_cost >= quota.max_cost_per_month_usd { - return Err(OpenFangError::QuotaExceeded(format!( + return Err(OMTAEError::QuotaExceeded(format!( "Agent {} exceeded monthly cost quota: ${:.4} / ${:.4}", agent_id, monthly_cost, quota.max_cost_per_month_usd ))); @@ -64,12 +64,12 @@ impl MeteringEngine { /// Check global budget limits (across all agents). pub fn check_global_budget( &self, - budget: &openfang_types::config::BudgetConfig, - ) -> OpenFangResult<()> { + budget: &omtae_types::config::BudgetConfig, + ) -> OMTAEResult<()> { if budget.max_hourly_usd > 0.0 { let cost = self.store.query_global_hourly()?; if cost >= budget.max_hourly_usd { - return Err(OpenFangError::QuotaExceeded(format!( + return Err(OMTAEError::QuotaExceeded(format!( "Global hourly budget exceeded: ${:.4} / ${:.4}", cost, budget.max_hourly_usd ))); @@ -79,7 +79,7 @@ impl MeteringEngine { if budget.max_daily_usd > 0.0 { let cost = self.store.query_today_cost()?; if cost >= budget.max_daily_usd { - return Err(OpenFangError::QuotaExceeded(format!( + return Err(OMTAEError::QuotaExceeded(format!( "Global daily budget exceeded: ${:.4} / ${:.4}", cost, budget.max_daily_usd ))); @@ -89,7 +89,7 @@ impl MeteringEngine { if budget.max_monthly_usd > 0.0 { let cost = self.store.query_global_monthly()?; if cost >= budget.max_monthly_usd { - return Err(OpenFangError::QuotaExceeded(format!( + return Err(OMTAEError::QuotaExceeded(format!( "Global monthly budget exceeded: ${:.4} / ${:.4}", cost, budget.max_monthly_usd ))); @@ -100,7 +100,7 @@ impl MeteringEngine { } /// Get budget status — current spend vs limits for all time windows. - pub fn budget_status(&self, budget: &openfang_types::config::BudgetConfig) -> BudgetStatus { + pub fn budget_status(&self, budget: &omtae_types::config::BudgetConfig) -> BudgetStatus { let hourly = self.store.query_global_hourly().unwrap_or(0.0); let daily = self.store.query_today_cost().unwrap_or(0.0); let monthly = self.store.query_global_monthly().unwrap_or(0.0); @@ -133,12 +133,12 @@ impl MeteringEngine { } /// Get a usage summary, optionally filtered by agent. - pub fn get_summary(&self, agent_id: Option) -> OpenFangResult { + pub fn get_summary(&self, agent_id: Option) -> OMTAEResult { self.store.query_summary(agent_id) } /// Get usage grouped by model. - pub fn get_by_model(&self) -> OpenFangResult> { + pub fn get_by_model(&self) -> OMTAEResult> { self.store.query_by_model() } @@ -195,7 +195,7 @@ impl MeteringEngine { /// Falls back to the default rate ($1/$3 per million) if the model is not /// found in the catalog. pub fn estimate_cost_with_catalog( - catalog: &openfang_runtime::model_catalog::ModelCatalog, + catalog: &omtae_runtime::model_catalog::ModelCatalog, model: &str, input_tokens: u64, output_tokens: u64, @@ -207,7 +207,7 @@ impl MeteringEngine { } /// Clean up old usage records. - pub fn cleanup(&self, days: u32) -> OpenFangResult { + pub fn cleanup(&self, days: u32) -> OMTAEResult { self.store.cleanup_old(days) } } @@ -520,7 +520,7 @@ fn estimate_cost_rates(model: &str) -> (f64, f64) { #[cfg(test)] mod tests { use super::*; - use openfang_memory::MemorySubstrate; + use omtae_memory::MemorySubstrate; fn setup() -> MeteringEngine { let substrate = MemorySubstrate::open_in_memory(0.1).unwrap(); @@ -759,7 +759,7 @@ mod tests { #[test] fn test_estimate_cost_with_catalog() { - let catalog = openfang_runtime::model_catalog::ModelCatalog::new(); + let catalog = omtae_runtime::model_catalog::ModelCatalog::new(); // Sonnet: $3/M input, $15/M output let cost = MeteringEngine::estimate_cost_with_catalog( &catalog, @@ -772,7 +772,7 @@ mod tests { #[test] fn test_estimate_cost_with_catalog_alias() { - let catalog = openfang_runtime::model_catalog::ModelCatalog::new(); + let catalog = omtae_runtime::model_catalog::ModelCatalog::new(); // "sonnet" alias should resolve to same pricing let cost = MeteringEngine::estimate_cost_with_catalog(&catalog, "sonnet", 1_000_000, 1_000_000); @@ -781,7 +781,7 @@ mod tests { #[test] fn test_estimate_cost_with_catalog_unknown_uses_default() { - let catalog = openfang_runtime::model_catalog::ModelCatalog::new(); + let catalog = omtae_runtime::model_catalog::ModelCatalog::new(); // Unknown model falls back to $1/$3 let cost = MeteringEngine::estimate_cost_with_catalog( &catalog, diff --git a/crates/openfang-kernel/src/pairing.rs b/crates/openfang-kernel/src/pairing.rs index 0569f48db3..7517fc76fd 100644 --- a/crates/openfang-kernel/src/pairing.rs +++ b/crates/openfang-kernel/src/pairing.rs @@ -4,7 +4,7 @@ //! push notifications via ntfy.sh or gotify. use dashmap::DashMap; -use openfang_types::config::PairingConfig; +use omtae_types::config::PairingConfig; use serde::{Deserialize, Serialize}; use tracing::{debug, warn}; @@ -32,7 +32,7 @@ pub struct PairingRequest { } /// Persistence callback — kernel injects this so PairingManager can save without -/// taking a direct dependency on openfang-memory. +/// taking a direct dependency on omtae-memory. pub type PersistFn = Box; /// Persistence operation kind. diff --git a/crates/openfang-kernel/src/registry.rs b/crates/openfang-kernel/src/registry.rs index 124bbfe728..87e8fda066 100644 --- a/crates/openfang-kernel/src/registry.rs +++ b/crates/openfang-kernel/src/registry.rs @@ -1,8 +1,8 @@ //! Agent registry — tracks all agents, their state, and indexes. use dashmap::DashMap; -use openfang_types::agent::{AgentEntry, AgentId, AgentMode, AgentState}; -use openfang_types::error::{OpenFangError, OpenFangResult}; +use omtae_types::agent::{AgentEntry, AgentId, AgentMode, AgentState}; +use omtae_types::error::{OMTAEError, OMTAEResult}; /// Registry of all agents in the kernel. pub struct AgentRegistry { @@ -25,9 +25,9 @@ impl AgentRegistry { } /// Register a new agent. - pub fn register(&self, entry: AgentEntry) -> OpenFangResult<()> { + pub fn register(&self, entry: AgentEntry) -> OMTAEResult<()> { if self.name_index.contains_key(&entry.name) { - return Err(OpenFangError::AgentAlreadyExists(entry.name.clone())); + return Err(OMTAEError::AgentAlreadyExists(entry.name.clone())); } let id = entry.id; self.name_index.insert(entry.name.clone(), id); @@ -50,34 +50,45 @@ impl AgentRegistry { .and_then(|id| self.agents.get(id.value()).map(|e| e.value().clone())) } + /// Find an agent by name (exact index first, then case-insensitive scan). + pub fn find_by_name_insensitive(&self, name: &str) -> Option { + if let Some(entry) = self.find_by_name(name) { + return Some(entry); + } + let lower = name.to_lowercase(); + self.list() + .into_iter() + .find(|e| e.name.to_lowercase() == lower) + } + /// Update agent state. - pub fn set_state(&self, id: AgentId, state: AgentState) -> OpenFangResult<()> { + pub fn set_state(&self, id: AgentId, state: AgentState) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.state = state; entry.last_active = chrono::Utc::now(); Ok(()) } /// Update agent operational mode. - pub fn set_mode(&self, id: AgentId, mode: AgentMode) -> OpenFangResult<()> { + pub fn set_mode(&self, id: AgentId, mode: AgentMode) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.mode = mode; entry.last_active = chrono::Utc::now(); Ok(()) } /// Remove an agent from the registry. - pub fn remove(&self, id: AgentId) -> OpenFangResult { + pub fn remove(&self, id: AgentId) -> OMTAEResult { let (_, entry) = self .agents .remove(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; self.name_index.remove(&entry.name); for tag in &entry.tags { if let Some(mut ids) = self.tag_index.get_mut(tag) { @@ -108,12 +119,12 @@ impl AgentRegistry { pub fn update_session_id( &self, id: AgentId, - new_session_id: openfang_types::agent::SessionId, - ) -> OpenFangResult<()> { + new_session_id: omtae_types::agent::SessionId, + ) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.session_id = new_session_id; entry.last_active = chrono::Utc::now(); Ok(()) @@ -124,11 +135,11 @@ impl AgentRegistry { &self, id: AgentId, workspace: Option, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.manifest.workspace = workspace; entry.last_active = chrono::Utc::now(); Ok(()) @@ -141,11 +152,11 @@ impl AgentRegistry { &self, id: AgentId, state_dir: Option, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.manifest.state_dir = state_dir; entry.last_active = chrono::Utc::now(); Ok(()) @@ -155,23 +166,23 @@ impl AgentRegistry { pub fn update_identity( &self, id: AgentId, - identity: openfang_types::agent::AgentIdentity, - ) -> OpenFangResult<()> { + identity: omtae_types::agent::AgentIdentity, + ) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.identity = identity; entry.last_active = chrono::Utc::now(); Ok(()) } /// Update an agent's model configuration. - pub fn update_model(&self, id: AgentId, new_model: String) -> OpenFangResult<()> { + pub fn update_model(&self, id: AgentId, new_model: String) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.manifest.model.model = new_model; entry.last_active = chrono::Utc::now(); Ok(()) @@ -183,11 +194,11 @@ impl AgentRegistry { id: AgentId, new_model: String, new_provider: String, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.manifest.model.model = new_model; entry.manifest.model.provider = new_provider; entry.last_active = chrono::Utc::now(); @@ -202,11 +213,11 @@ impl AgentRegistry { new_provider: String, api_key_env: Option, base_url: Option, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.manifest.model.model = new_model; entry.manifest.model.provider = new_provider; entry.manifest.model.api_key_env = api_key_env; @@ -219,34 +230,34 @@ impl AgentRegistry { pub fn update_fallback_models( &self, id: AgentId, - fallback_models: Vec, - ) -> OpenFangResult<()> { + fallback_models: Vec, + ) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.manifest.fallback_models = fallback_models; entry.last_active = chrono::Utc::now(); Ok(()) } /// Update an agent's skill allowlist. - pub fn update_skills(&self, id: AgentId, skills: Vec) -> OpenFangResult<()> { + pub fn update_skills(&self, id: AgentId, skills: Vec) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.manifest.skills = skills; entry.last_active = chrono::Utc::now(); Ok(()) } /// Update an agent's MCP server allowlist. - pub fn update_mcp_servers(&self, id: AgentId, servers: Vec) -> OpenFangResult<()> { + pub fn update_mcp_servers(&self, id: AgentId, servers: Vec) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.manifest.mcp_servers = servers; entry.last_active = chrono::Utc::now(); Ok(()) @@ -258,11 +269,11 @@ impl AgentRegistry { id: AgentId, allowlist: Option>, blocklist: Option>, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; if let Some(al) = allowlist { entry.manifest.tool_allowlist = al; } @@ -281,22 +292,33 @@ impl AgentRegistry { } } + /// Replace the full agent manifest (hot-swap — takes effect on next message). + pub fn replace_manifest(&self, id: AgentId, manifest: omtae_types::agent::AgentManifest) -> OMTAEResult<()> { + let mut entry = self + .agents + .get_mut(&id) + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; + entry.manifest = manifest; + entry.last_active = chrono::Utc::now(); + Ok(()) + } + /// Update an agent's system prompt (hot-swap, takes effect on next message). - pub fn update_system_prompt(&self, id: AgentId, new_prompt: String) -> OpenFangResult<()> { + pub fn update_system_prompt(&self, id: AgentId, new_prompt: String) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.manifest.model.system_prompt = new_prompt; entry.last_active = chrono::Utc::now(); Ok(()) } /// Update an agent's name (also updates the name index). - pub fn update_name(&self, id: AgentId, new_name: String) -> OpenFangResult<()> { + pub fn update_name(&self, id: AgentId, new_name: String) -> OMTAEResult<()> { if let Some(existing_id) = self.name_index.get(&new_name).as_deref().copied() { if existing_id != id { - return Err(OpenFangError::AgentAlreadyExists(new_name)); + return Err(OMTAEError::AgentAlreadyExists(new_name)); } // Same agent owns this name — no-op return Ok(()); @@ -304,7 +326,7 @@ impl AgentRegistry { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; let old_name = entry.name.clone(); entry.name = new_name.clone(); entry.manifest.name = new_name.clone(); @@ -317,11 +339,11 @@ impl AgentRegistry { } /// Update an agent's description. - pub fn update_description(&self, id: AgentId, new_desc: String) -> OpenFangResult<()> { + pub fn update_description(&self, id: AgentId, new_desc: String) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.manifest.description = new_desc; entry.last_active = chrono::Utc::now(); Ok(()) @@ -335,11 +357,11 @@ impl AgentRegistry { daily: Option, monthly: Option, tokens_per_hour: Option, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; if let Some(v) = hourly { entry.manifest.resources.max_cost_per_hour_usd = v; } @@ -357,11 +379,11 @@ impl AgentRegistry { } /// Mark an agent's onboarding as complete. - pub fn mark_onboarding_complete(&self, id: AgentId) -> OpenFangResult<()> { + pub fn mark_onboarding_complete(&self, id: AgentId) -> OMTAEResult<()> { let mut entry = self .agents .get_mut(&id) - .ok_or_else(|| OpenFangError::AgentNotFound(id.to_string()))?; + .ok_or_else(|| OMTAEError::AgentNotFound(id.to_string()))?; entry.onboarding_completed = true; entry.onboarding_completed_at = Some(chrono::Utc::now()); entry.last_active = chrono::Utc::now(); @@ -379,7 +401,7 @@ impl Default for AgentRegistry { mod tests { use super::*; use chrono::Utc; - use openfang_types::agent::*; + use omtae_types::agent::*; use std::collections::HashMap; fn test_entry(name: &str) -> AgentEntry { @@ -447,6 +469,15 @@ mod tests { assert!(registry.find_by_name("my-agent").is_some()); } + #[test] + fn test_find_by_name_insensitive() { + let registry = AgentRegistry::new(); + let entry = test_entry("analyst"); + registry.register(entry).unwrap(); + assert!(registry.find_by_name_insensitive("Analyst").is_some()); + assert!(registry.find_by_name_insensitive("analyst").is_some()); + } + #[test] fn test_duplicate_name() { let registry = AgentRegistry::new(); diff --git a/crates/openfang-kernel/src/scheduler.rs b/crates/openfang-kernel/src/scheduler.rs index b7ee99c2ec..679af6e995 100644 --- a/crates/openfang-kernel/src/scheduler.rs +++ b/crates/openfang-kernel/src/scheduler.rs @@ -1,9 +1,9 @@ //! Agent scheduler — manages agent execution and resource tracking. use dashmap::DashMap; -use openfang_types::agent::{AgentId, ResourceQuota}; -use openfang_types::error::{OpenFangError, OpenFangResult}; -use openfang_types::message::TokenUsage; +use omtae_types::agent::{AgentId, ResourceQuota}; +use omtae_types::error::{OMTAEError, OMTAEResult}; +use omtae_types::message::TokenUsage; use std::time::Instant; use tokio::task::JoinHandle; use tracing::debug; @@ -75,7 +75,7 @@ impl AgentScheduler { } /// Check if an agent has exceeded its quota. - pub fn check_quota(&self, agent_id: AgentId) -> OpenFangResult<()> { + pub fn check_quota(&self, agent_id: AgentId) -> OMTAEResult<()> { let quota = match self.quotas.get(&agent_id) { Some(q) => q.clone(), None => return Ok(()), // No quota = no limit @@ -90,7 +90,7 @@ impl AgentScheduler { if quota.max_llm_tokens_per_hour > 0 && tracker.total_tokens > quota.max_llm_tokens_per_hour { - return Err(OpenFangError::QuotaExceeded(format!( + return Err(OMTAEError::QuotaExceeded(format!( "Token limit exceeded: {} / {}", tracker.total_tokens, quota.max_llm_tokens_per_hour ))); diff --git a/crates/openfang-kernel/src/supervisor.rs b/crates/openfang-kernel/src/supervisor.rs index ed45cca8f7..b3daa55c00 100644 --- a/crates/openfang-kernel/src/supervisor.rs +++ b/crates/openfang-kernel/src/supervisor.rs @@ -1,7 +1,7 @@ //! Process supervision — graceful shutdown, signal handling, and health monitoring. use dashmap::DashMap; -use openfang_types::agent::AgentId; +use omtae_types::agent::AgentId; use std::sync::atomic::{AtomicU64, Ordering}; use tokio::sync::watch; use tracing::{info, warn}; diff --git a/crates/openfang-kernel/src/triggers.rs b/crates/openfang-kernel/src/triggers.rs index 05283c9a05..5951f64cd2 100644 --- a/crates/openfang-kernel/src/triggers.rs +++ b/crates/openfang-kernel/src/triggers.rs @@ -6,8 +6,8 @@ use chrono::{DateTime, Utc}; use dashmap::DashMap; -use openfang_types::agent::AgentId; -use openfang_types::event::{Event, EventPayload, LifecycleEvent, SystemEvent}; +use omtae_types::agent::AgentId; +use omtae_types::event::{Event, EventPayload, LifecycleEvent, SystemEvent}; use serde::{Deserialize, Serialize}; use tracing::{debug, info}; use uuid::Uuid; @@ -377,7 +377,7 @@ fn describe_event(event: &Event) -> String { tr.tool_id, if tr.success { "succeeded" } else { "failed" }, tr.execution_time_ms, - openfang_types::truncate_str(&tr.content, 200) + omtae_types::truncate_str(&tr.content, 200) ) } EventPayload::MemoryUpdate(delta) => { @@ -467,7 +467,7 @@ fn describe_event(event: &Event) -> String { #[cfg(test)] mod tests { use super::*; - use openfang_types::event::*; + use omtae_types::event::*; #[test] fn test_register_trigger() { diff --git a/crates/openfang-kernel/src/whatsapp_gateway.rs b/crates/openfang-kernel/src/whatsapp_gateway.rs index 0916caf59a..a5b96c7362 100644 --- a/crates/openfang-kernel/src/whatsapp_gateway.rs +++ b/crates/openfang-kernel/src/whatsapp_gateway.rs @@ -1,10 +1,10 @@ //! WhatsApp Web gateway — embedded Node.js process management. //! -//! Embeds the gateway JS at compile time, extracts it to `~/.openfang/whatsapp-gateway/`, +//! Embeds the gateway JS at compile time, extracts it to `~/.omtae/whatsapp-gateway/`, //! runs `npm install` if needed, and spawns `node index.js` as a managed child process //! that auto-restarts on crash. -use crate::config::openfang_home; +use crate::config::omtae_home; use std::path::PathBuf; use std::sync::Arc; use tracing::{info, warn}; @@ -24,7 +24,7 @@ const RESTART_DELAYS: [u64; 3] = [5, 10, 20]; /// Get the gateway installation directory. fn gateway_dir() -> PathBuf { - openfang_home().join("whatsapp-gateway") + omtae_home().join("whatsapp-gateway") } /// Compute a simple hash of content for change detection. @@ -127,7 +127,7 @@ async fn node_available() -> bool { /// 5. Monitors the process and restarts on crash (up to 3 times) /// /// The PID is stored in the kernel's `whatsapp_gateway_pid` for shutdown cleanup. -pub async fn start_whatsapp_gateway(kernel: &Arc) { +pub async fn start_whatsapp_gateway(kernel: &Arc) { // Only start if WhatsApp is configured let wa_config = match &kernel.config.channels.whatsapp { Some(cfg) => cfg.clone(), @@ -154,7 +154,7 @@ pub async fn start_whatsapp_gateway(kernel: &Arc) let port = DEFAULT_GATEWAY_PORT; let api_listen = &kernel.config.api_listen; - let openfang_url = format!("http://{api_listen}"); + let omtae_url = format!("http://{api_listen}"); let default_agent = wa_config .default_agent .as_deref() @@ -184,7 +184,7 @@ pub async fn start_whatsapp_gateway(kernel: &Arc) .arg("index.js") .current_dir(&gateway_path) .env("WHATSAPP_GATEWAY_PORT", port.to_string()) - .env("OPENFANG_URL", &openfang_url) + .env("OPENFANG_URL", &omtae_url) .env("OPENFANG_DEFAULT_AGENT", &default_agent) .stdout(std::process::Stdio::inherit()) .stderr(std::process::Stdio::inherit()) @@ -272,7 +272,7 @@ mod tests { assert_ne!(GATEWAY_INDEX_JS, ""); assert_ne!(GATEWAY_PACKAGE_JSON, ""); assert!(GATEWAY_INDEX_JS.contains("WhatsApp")); - assert!(GATEWAY_PACKAGE_JSON.contains("@openfang/whatsapp-gateway")); + assert!(GATEWAY_PACKAGE_JSON.contains("@omtae/whatsapp-gateway")); } #[test] @@ -290,19 +290,19 @@ mod tests { } #[test] - fn test_gateway_dir_under_openfang_home() { + fn test_gateway_dir_under_omtae_home() { let dir = gateway_dir(); assert!(dir.ends_with("whatsapp-gateway")); assert!(dir .parent() .unwrap() .to_string_lossy() - .contains(".openfang")); + .contains(".omtae")); } #[test] fn test_write_if_changed_creates_new_file() { - let tmp = std::env::temp_dir().join("openfang_test_gateway"); + let tmp = std::env::temp_dir().join("omtae_test_gateway"); let _ = std::fs::create_dir_all(&tmp); let path = tmp.join("test_write.js"); let hash_path = path.with_extension("hash"); diff --git a/crates/openfang-kernel/src/wizard.rs b/crates/openfang-kernel/src/wizard.rs index 4338830af2..9828a3dac1 100644 --- a/crates/openfang-kernel/src/wizard.rs +++ b/crates/openfang-kernel/src/wizard.rs @@ -4,7 +4,7 @@ //! an agent to do, extracts structured intent, and generates a complete //! agent manifest (TOML config) ready to spawn. -use openfang_types::agent::{ +use omtae_types::agent::{ AgentManifest, ManifestCapabilities, ModelConfig, Priority, ResourceQuota, ScheduleMode, }; use serde::{Deserialize, Serialize}; @@ -133,7 +133,7 @@ impl SetupWizard { // safety guidelines, etc. at execution time. let tool_hints = Self::tool_hints_for(&caps.tools); let system_prompt = format!( - "You are {name}, an AI agent running inside the OpenFang Agent OS.\n\ + "You are {name}, an AI agent running inside the OMTAE Agent OS.\n\ \n\ YOUR TASK: {task}\n\ \n\ diff --git a/crates/openfang-kernel/src/workflow.rs b/crates/openfang-kernel/src/workflow.rs index a40f544a50..0946d2ea3b 100644 --- a/crates/openfang-kernel/src/workflow.rs +++ b/crates/openfang-kernel/src/workflow.rs @@ -11,7 +11,7 @@ //! Workflows are defined as Rust structs or loaded from JSON. use chrono::{DateTime, Utc}; -use openfang_types::agent::AgentId; +use omtae_types::agent::AgentId; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; diff --git a/crates/openfang-kernel/tests/integration_test.rs b/crates/openfang-kernel/tests/integration_test.rs index b8d1f76ee0..9f8ee6973f 100644 --- a/crates/openfang-kernel/tests/integration_test.rs +++ b/crates/openfang-kernel/tests/integration_test.rs @@ -1,13 +1,13 @@ //! Integration test: boot kernel -> spawn agent -> send message via Groq API. //! -//! Run with: GROQ_API_KEY=gsk_... cargo test -p openfang-kernel --test integration_test -- --nocapture +//! Run with: GROQ_API_KEY=gsk_... cargo test -p omtae-kernel --test integration_test -- --nocapture -use openfang_kernel::OpenFangKernel; -use openfang_types::agent::AgentManifest; -use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use omtae_kernel::OMTAEKernel; +use omtae_types::agent::AgentManifest; +use omtae_types::config::{DefaultModelConfig, KernelConfig}; fn test_config() -> KernelConfig { - let tmp = std::env::temp_dir().join("openfang-integration-test"); + let tmp = std::env::temp_dir().join("omtae-integration-test"); let _ = std::fs::remove_dir_all(&tmp); std::fs::create_dir_all(&tmp).unwrap(); @@ -34,7 +34,7 @@ async fn test_full_pipeline_with_groq() { // Boot kernel let config = test_config(); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); // Spawn agent let manifest: AgentManifest = toml::from_str( @@ -92,7 +92,7 @@ async fn test_multiple_agents_different_models() { } let config = test_config(); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); // Spawn agent 1: llama 70b let manifest1: AgentManifest = toml::from_str( diff --git a/crates/openfang-kernel/tests/multi_agent_test.rs b/crates/openfang-kernel/tests/multi_agent_test.rs index 58a3647f20..a35b271555 100644 --- a/crates/openfang-kernel/tests/multi_agent_test.rs +++ b/crates/openfang-kernel/tests/multi_agent_test.rs @@ -1,13 +1,13 @@ //! Multi-agent integration test: spawn 6 agents, send messages, verify all respond. //! -//! Run with: GROQ_API_KEY=gsk_... cargo test -p openfang-kernel --test multi_agent_test -- --nocapture +//! Run with: GROQ_API_KEY=gsk_... cargo test -p omtae-kernel --test multi_agent_test -- --nocapture -use openfang_kernel::OpenFangKernel; -use openfang_types::agent::AgentManifest; -use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use omtae_kernel::OMTAEKernel; +use omtae_types::agent::AgentManifest; +use omtae_types::config::{DefaultModelConfig, KernelConfig}; fn test_config() -> KernelConfig { - let tmp = std::env::temp_dir().join("openfang-multi-agent-test"); + let tmp = std::env::temp_dir().join("omtae-multi-agent-test"); let _ = std::fs::remove_dir_all(&tmp); std::fs::create_dir_all(&tmp).unwrap(); @@ -36,7 +36,7 @@ async fn test_six_agent_fleet() { return; } - let kernel = OpenFangKernel::boot_with_config(test_config()).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(test_config()).expect("Kernel should boot"); // Define all 6 agents with different roles and models let agents = vec![ diff --git a/crates/openfang-kernel/tests/wasm_agent_integration_test.rs b/crates/openfang-kernel/tests/wasm_agent_integration_test.rs index d2f465ebf1..da12369019 100644 --- a/crates/openfang-kernel/tests/wasm_agent_integration_test.rs +++ b/crates/openfang-kernel/tests/wasm_agent_integration_test.rs @@ -5,9 +5,9 @@ //! //! These tests use real WASM execution — no mocks. -use openfang_kernel::OpenFangKernel; -use openfang_types::agent::AgentManifest; -use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use omtae_kernel::OMTAEKernel; +use omtae_types::agent::AgentManifest; +use omtae_types::config::{DefaultModelConfig, KernelConfig}; use std::sync::Arc; /// Minimal echo module: returns input JSON wrapped as `{"response": "..."}`. @@ -89,7 +89,7 @@ const INFINITE_LOOP_WAT: &str = r#" /// Host-call proxy: forwards input to host_call and returns the response. const HOST_CALL_PROXY_WAT: &str = r#" (module - (import "openfang" "host_call" (func $host_call (param i32 i32) (result i64))) + (import "omtae" "host_call" (func $host_call (param i32 i32) (result i64))) (memory (export "memory") 2) (global $bump (mut i32) (i32.const 1024)) @@ -154,7 +154,7 @@ async fn test_wasm_agent_hello_response() { std::fs::write(tmp.path().join("hello.wat"), HELLO_WAT).unwrap(); let config = test_config(&tmp); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let manifest = wasm_manifest("wasm-hello", "hello.wat"); let agent_id = kernel.spawn_agent(manifest).unwrap(); @@ -177,7 +177,7 @@ async fn test_wasm_agent_echo() { std::fs::write(tmp.path().join("echo.wat"), ECHO_WAT).unwrap(); let config = test_config(&tmp); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let manifest = wasm_manifest("wasm-echo", "echo.wat"); let agent_id = kernel.spawn_agent(manifest).unwrap(); @@ -204,7 +204,7 @@ async fn test_wasm_agent_fuel_exhaustion() { std::fs::write(tmp.path().join("loop.wat"), INFINITE_LOOP_WAT).unwrap(); let config = test_config(&tmp); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let manifest = wasm_manifest("wasm-loop", "loop.wat"); let agent_id = kernel.spawn_agent(manifest).unwrap(); @@ -230,7 +230,7 @@ async fn test_wasm_agent_missing_module() { // Don't write any .wat file let config = test_config(&tmp); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let manifest = wasm_manifest("wasm-missing", "nonexistent.wasm"); let agent_id = kernel.spawn_agent(manifest).unwrap(); @@ -253,7 +253,7 @@ async fn test_wasm_agent_host_call_time() { std::fs::write(tmp.path().join("proxy.wat"), HOST_CALL_PROXY_WAT).unwrap(); let config = test_config(&tmp); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); // Proxy module forwards input to host_call — send a time_now request let toml_str = r#" @@ -297,7 +297,7 @@ async fn test_wasm_agent_streaming_fallback() { std::fs::write(tmp.path().join("hello.wat"), HELLO_WAT).unwrap(); let config = test_config(&tmp); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let kernel = Arc::new(kernel); let manifest = wasm_manifest("wasm-stream", "hello.wat"); @@ -334,7 +334,7 @@ async fn test_multiple_wasm_agents() { std::fs::write(tmp.path().join("echo.wat"), ECHO_WAT).unwrap(); let config = test_config(&tmp); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let hello_id = kernel .spawn_agent(wasm_manifest("hello-agent", "hello.wat")) @@ -364,7 +364,7 @@ async fn test_mixed_wasm_and_llm_agents() { std::fs::write(tmp.path().join("hello.wat"), HELLO_WAT).unwrap(); let config = test_config(&tmp); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); // Spawn a WASM agent let wasm_id = kernel diff --git a/crates/openfang-kernel/tests/workflow_integration_test.rs b/crates/openfang-kernel/tests/workflow_integration_test.rs index 6c9d7b03fc..d527bd2c4e 100644 --- a/crates/openfang-kernel/tests/workflow_integration_test.rs +++ b/crates/openfang-kernel/tests/workflow_integration_test.rs @@ -6,12 +6,12 @@ //! LLM tests require GROQ_API_KEY. Non-LLM tests verify the kernel-level //! workflow wiring without making real API calls. -use openfang_kernel::workflow::{ +use omtae_kernel::workflow::{ ErrorMode, StepAgent, StepMode, Workflow, WorkflowId, WorkflowStep, }; -use openfang_kernel::OpenFangKernel; -use openfang_types::agent::AgentManifest; -use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use omtae_kernel::OMTAEKernel; +use omtae_types::agent::AgentManifest; +use omtae_types::config::{DefaultModelConfig, KernelConfig}; use std::sync::Arc; fn test_config(provider: &str, model: &str, api_key_env: &str) -> KernelConfig { @@ -31,10 +31,10 @@ fn test_config(provider: &str, model: &str, api_key_env: &str) -> KernelConfig { } fn spawn_test_agent( - kernel: &OpenFangKernel, + kernel: &OMTAEKernel, name: &str, system_prompt: &str, -) -> openfang_types::agent::AgentId { +) -> omtae_types::agent::AgentId { let manifest_str = format!( r#" name = "{name}" @@ -65,7 +65,7 @@ memory_write = ["self.*"] #[tokio::test] async fn test_workflow_register_and_resolve() { let config = test_config("ollama", "test-model", "OLLAMA_API_KEY"); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let kernel = Arc::new(kernel); // Spawn agents @@ -176,7 +176,7 @@ memory_write = ["self.*"] #[tokio::test] async fn test_workflow_agent_by_id() { let config = test_config("ollama", "test-model", "OLLAMA_API_KEY"); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let manifest: AgentManifest = toml::from_str( r#" @@ -232,10 +232,10 @@ memory_write = ["self.*"] /// Test trigger registration and listing at kernel level. #[tokio::test] async fn test_trigger_registration_with_kernel() { - use openfang_kernel::triggers::TriggerPattern; + use omtae_kernel::triggers::TriggerPattern; let config = test_config("ollama", "test-model", "OLLAMA_API_KEY"); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let manifest: AgentManifest = toml::from_str( r#" @@ -310,7 +310,7 @@ async fn test_workflow_e2e_with_groq() { } let config = test_config("groq", "llama-3.3-70b-versatile", "GROQ_API_KEY"); - let kernel = OpenFangKernel::boot_with_config(config).expect("Kernel should boot"); + let kernel = OMTAEKernel::boot_with_config(config).expect("Kernel should boot"); let kernel = Arc::new(kernel); kernel.set_self_handle(); @@ -385,7 +385,7 @@ async fn test_workflow_e2e_with_groq() { let run = kernel.workflows.get_run(run_id).await.unwrap(); assert!(matches!( run.state, - openfang_kernel::workflow::WorkflowRunState::Completed + omtae_kernel::workflow::WorkflowRunState::Completed )); assert_eq!(run.step_results.len(), 2); assert_eq!(run.step_results[0].step_name, "analyze"); diff --git a/crates/openfang-memory/Cargo.toml b/crates/openfang-memory/Cargo.toml index 2ad9ee6b69..c16dda0892 100644 --- a/crates/openfang-memory/Cargo.toml +++ b/crates/openfang-memory/Cargo.toml @@ -1,16 +1,16 @@ [package] -name = "openfang-memory" +name = "omtae-memory" version.workspace = true edition.workspace = true license.workspace = true -description = "Memory substrate for the OpenFang Agent OS" +description = "Memory substrate for the OMTAE Agent OS" [features] default = ["http-memory"] http-memory = ["reqwest"] [dependencies] -openfang-types = { path = "../openfang-types" } +omtae-types = { path = "../omtae-types" } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/openfang-memory/src/consolidation.rs b/crates/openfang-memory/src/consolidation.rs index 9d1d369fc6..c8e62f1775 100644 --- a/crates/openfang-memory/src/consolidation.rs +++ b/crates/openfang-memory/src/consolidation.rs @@ -4,8 +4,8 @@ //! duplicate/similar memories. use chrono::Utc; -use openfang_types::error::{OpenFangError, OpenFangResult}; -use openfang_types::memory::ConsolidationReport; +use omtae_types::error::{OMTAEError, OMTAEResult}; +use omtae_types::memory::ConsolidationReport; use rusqlite::Connection; use std::sync::{Arc, Mutex}; @@ -24,12 +24,12 @@ impl ConsolidationEngine { } /// Run a consolidation cycle: decay old memories. - pub fn consolidate(&self) -> OpenFangResult { + pub fn consolidate(&self) -> OMTAEResult { let start = std::time::Instant::now(); let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; // Decay confidence of memories not accessed in the last 7 days let cutoff = (Utc::now() - chrono::Duration::days(7)).to_rfc3339(); @@ -41,7 +41,7 @@ impl ConsolidationEngine { WHERE deleted = 0 AND accessed_at < ?2 AND confidence > 0.1", rusqlite::params![decay_factor, cutoff], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let duration_ms = start.elapsed().as_millis() as u64; diff --git a/crates/openfang-memory/src/http_client.rs b/crates/openfang-memory/src/http_client.rs index 1bdf13dd5a..d6f9ab60b2 100644 --- a/crates/openfang-memory/src/http_client.rs +++ b/crates/openfang-memory/src/http_client.rs @@ -102,7 +102,7 @@ impl MemoryApiClient { let client = reqwest::blocking::Client::builder() .timeout(std::time::Duration::from_secs(30)) - .user_agent("openfang-memory/0.4") + .user_agent("omtae-memory/0.4") .build() .map_err(|e| MemoryApiError::Http(e.to_string()))?; diff --git a/crates/openfang-memory/src/knowledge.rs b/crates/openfang-memory/src/knowledge.rs index e4f5c7734e..8becc2abcd 100644 --- a/crates/openfang-memory/src/knowledge.rs +++ b/crates/openfang-memory/src/knowledge.rs @@ -3,8 +3,8 @@ //! Stores entities and relations with support for graph pattern queries. use chrono::Utc; -use openfang_types::error::{OpenFangError, OpenFangResult}; -use openfang_types::memory::{ +use omtae_types::error::{OMTAEError, OMTAEResult}; +use omtae_types::memory::{ Entity, EntityType, GraphMatch, GraphPattern, Relation, RelationType, }; use rusqlite::Connection; @@ -25,20 +25,20 @@ impl KnowledgeStore { } /// Add an entity to the knowledge graph. - pub fn add_entity(&self, entity: Entity) -> OpenFangResult { + pub fn add_entity(&self, entity: Entity) -> OMTAEResult { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let id = if entity.id.is_empty() { Uuid::new_v4().to_string() } else { entity.id.clone() }; let entity_type_str = serde_json::to_string(&entity.entity_type) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; let props_str = serde_json::to_string(&entity.properties) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; let now = Utc::now().to_rfc3339(); conn.execute( "INSERT INTO entities (id, entity_type, name, properties, created_at, updated_at) @@ -46,21 +46,21 @@ impl KnowledgeStore { ON CONFLICT(id) DO UPDATE SET name = ?3, properties = ?4, updated_at = ?5", rusqlite::params![id, entity_type_str, entity.name, props_str, now], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(id) } /// Add a relation between two entities. - pub fn add_relation(&self, relation: Relation) -> OpenFangResult { + pub fn add_relation(&self, relation: Relation) -> OMTAEResult { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let id = Uuid::new_v4().to_string(); let rel_type_str = serde_json::to_string(&relation.relation) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; let props_str = serde_json::to_string(&relation.properties) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; let now = Utc::now().to_rfc3339(); conn.execute( "INSERT INTO relations (id, source_entity, relation_type, target_entity, properties, confidence, created_at) @@ -75,16 +75,16 @@ impl KnowledgeStore { now, ], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(id) } /// Query the knowledge graph with a pattern. - pub fn query_graph(&self, pattern: GraphPattern) -> OpenFangResult> { + pub fn query_graph(&self, pattern: GraphPattern) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let mut sql = String::from( "SELECT @@ -107,7 +107,7 @@ impl KnowledgeStore { } if let Some(ref relation) = pattern.relation { let rel_str = serde_json::to_string(relation) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; sql.push_str(&format!(" AND r.relation_type = ?{idx}")); params.push(Box::new(rel_str)); idx += 1; @@ -124,7 +124,7 @@ impl KnowledgeStore { let mut stmt = conn .prepare(&sql) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let param_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect(); @@ -152,11 +152,11 @@ impl KnowledgeStore { t_updated: row.get(18)?, }) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let mut matches = Vec::new(); for row_result in rows { - let r = row_result.map_err(|e| OpenFangError::Memory(e.to_string()))?; + let r = row_result.map_err(|e| OMTAEError::Memory(e.to_string()))?; matches.push(GraphMatch { source: parse_entity( &r.s_id, diff --git a/crates/openfang-memory/src/lib.rs b/crates/openfang-memory/src/lib.rs index bf16737a02..fa3a109ff1 100644 --- a/crates/openfang-memory/src/lib.rs +++ b/crates/openfang-memory/src/lib.rs @@ -1,4 +1,4 @@ -//! Memory substrate for the OpenFang Agent Operating System. +//! Memory substrate for the OMTAE Agent Operating System. //! //! Provides a unified memory API over three storage backends: //! - **Structured store** (SQLite): Key-value pairs, sessions, agent state diff --git a/crates/openfang-memory/src/semantic.rs b/crates/openfang-memory/src/semantic.rs index 79d9ae4ba0..a69e20c7f3 100644 --- a/crates/openfang-memory/src/semantic.rs +++ b/crates/openfang-memory/src/semantic.rs @@ -8,9 +8,9 @@ //! When no embeddings are available, falls back to LIKE matching. use chrono::Utc; -use openfang_types::agent::AgentId; -use openfang_types::error::{OpenFangError, OpenFangResult}; -use openfang_types::memory::{MemoryFilter, MemoryFragment, MemoryId, MemorySource}; +use omtae_types::agent::AgentId; +use omtae_types::error::{OMTAEError, OMTAEResult}; +use omtae_types::memory::{MemoryFilter, MemoryFragment, MemoryId, MemorySource}; use rusqlite::Connection; use std::collections::HashMap; use std::sync::{Arc, Mutex}; @@ -61,7 +61,7 @@ impl SemanticStore { source: MemorySource, scope: &str, metadata: HashMap, - ) -> OpenFangResult { + ) -> OMTAEResult { self.remember_with_embedding(agent_id, content, source, scope, metadata, None) } @@ -77,7 +77,7 @@ impl SemanticStore { scope: &str, metadata: HashMap, embedding: Option<&[f32]>, - ) -> OpenFangResult { + ) -> OMTAEResult { // HTTP backend: route to memory-api #[cfg(feature = "http-memory")] if let Some(ref client) = self.http_client { @@ -97,17 +97,17 @@ impl SemanticStore { scope: &str, metadata: HashMap, embedding: Option<&[f32]>, - ) -> OpenFangResult { + ) -> OMTAEResult { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let id = MemoryId::new(); let now = Utc::now().to_rfc3339(); let source_str = serde_json::to_string(&source) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; let meta_str = serde_json::to_string(&metadata) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; let embedding_bytes: Option> = embedding.map(embedding_to_bytes); conn.execute( @@ -124,7 +124,7 @@ impl SemanticStore { embedding_bytes, ], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(id) } @@ -138,7 +138,7 @@ impl SemanticStore { source: MemorySource, scope: &str, metadata: &HashMap, - ) -> OpenFangResult { + ) -> OMTAEResult { let source_str = format!("{:?}", source).to_lowercase(); let importance = metadata .get("importance") @@ -174,7 +174,7 @@ impl SemanticStore { query: &str, limit: usize, filter: Option, - ) -> OpenFangResult> { + ) -> OMTAEResult> { self.recall_with_embedding(query, limit, filter, None) } @@ -189,7 +189,7 @@ impl SemanticStore { limit: usize, filter: Option, query_embedding: Option<&[f32]>, - ) -> OpenFangResult> { + ) -> OMTAEResult> { // HTTP backend: route to memory-api #[cfg(feature = "http-memory")] if let Some(ref client) = self.http_client { @@ -204,7 +204,7 @@ impl SemanticStore { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; // Build SQL: fetch candidates (broader than limit for vector re-ranking) let fetch_limit = if query_embedding.is_some() { @@ -247,7 +247,7 @@ impl SemanticStore { } if let Some(ref source) = f.source { let source_str = serde_json::to_string(source) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; sql.push_str(&format!(" AND source = ?{param_idx}")); params.push(Box::new(source_str)); let _ = param_idx; @@ -259,7 +259,7 @@ impl SemanticStore { let mut stmt = conn .prepare(&sql) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let param_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect(); @@ -290,7 +290,7 @@ impl SemanticStore { embedding_bytes, )) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let mut fragments = Vec::new(); for row_result in rows { @@ -306,14 +306,14 @@ impl SemanticStore { accessed_str, access_count, embedding_bytes, - ) = row_result.map_err(|e| OpenFangError::Memory(e.to_string()))?; + ) = row_result.map_err(|e| OMTAEError::Memory(e.to_string()))?; let id = uuid::Uuid::parse_str(&id_str) .map(MemoryId) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let agent_id = uuid::Uuid::parse_str(&agent_str) - .map(openfang_types::agent::AgentId) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map(omtae_types::agent::AgentId) + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let source: MemorySource = serde_json::from_str(&source_str).unwrap_or(MemorySource::System); let metadata: HashMap = @@ -382,7 +382,7 @@ impl SemanticStore { /// /// In HTTP mode, logs a warning (memory-api doesn't support delete yet) /// and performs the soft-delete locally only. - pub fn forget(&self, id: MemoryId) -> OpenFangResult<()> { + pub fn forget(&self, id: MemoryId) -> OMTAEResult<()> { #[cfg(feature = "http-memory")] if self.http_client.is_some() { warn!(id = %id.0, "forget() not supported via HTTP backend, local-only soft-delete"); @@ -391,27 +391,27 @@ impl SemanticStore { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; conn.execute( "UPDATE memories SET deleted = 1 WHERE id = ?1", rusqlite::params![id.0.to_string()], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } /// Update the embedding for an existing memory. - pub fn update_embedding(&self, id: MemoryId, embedding: &[f32]) -> OpenFangResult<()> { + pub fn update_embedding(&self, id: MemoryId, embedding: &[f32]) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let bytes = embedding_to_bytes(embedding); conn.execute( "UPDATE memories SET embedding = ?1 WHERE id = ?2", rusqlite::params![bytes, id.0.to_string()], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } @@ -426,12 +426,12 @@ impl SemanticStore { query: &str, limit: usize, filter: &Option, - ) -> OpenFangResult> { + ) -> OMTAEResult> { let category = filter.as_ref().and_then(|f| f.scope.as_deref()); let results = client .search(query, limit, category) - .map_err(|e| OpenFangError::Memory(format!("HTTP search failed: {e}")))?; + .map_err(|e| OMTAEError::Memory(format!("HTTP search failed: {e}")))?; let fragments: Vec = results .into_iter() diff --git a/crates/openfang-memory/src/session.rs b/crates/openfang-memory/src/session.rs index 7a18e0f9fb..f05ce8ed04 100644 --- a/crates/openfang-memory/src/session.rs +++ b/crates/openfang-memory/src/session.rs @@ -1,9 +1,9 @@ //! Session management — load/save conversation history. use chrono::Utc; -use openfang_types::agent::{AgentId, SessionId}; -use openfang_types::error::{OpenFangError, OpenFangResult}; -use openfang_types::message::{ContentBlock, Message, MessageContent, Role}; +use omtae_types::agent::{AgentId, SessionId}; +use omtae_types::error::{OMTAEError, OMTAEResult}; +use omtae_types::message::{ContentBlock, Message, MessageContent, Role}; use rusqlite::Connection; use std::io::Write; use std::path::Path; @@ -37,14 +37,14 @@ impl SessionStore { } /// Load a session from the database. - pub fn get_session(&self, session_id: SessionId) -> OpenFangResult> { + pub fn get_session(&self, session_id: SessionId) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let mut stmt = conn .prepare("SELECT agent_id, messages, context_window_tokens, label FROM sessions WHERE id = ?1") - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let result = stmt.query_row(rusqlite::params![session_id.0.to_string()], |row| { let agent_str: String = row.get(0)?; @@ -58,9 +58,9 @@ impl SessionStore { Ok((agent_str, messages_blob, tokens, label)) => { let agent_id = uuid::Uuid::parse_str(&agent_str) .map(AgentId) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let messages: Vec = rmp_serde::from_slice(&messages_blob) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; Ok(Some(Session { id: session_id, agent_id, @@ -70,18 +70,18 @@ impl SessionStore { })) } Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(OpenFangError::Memory(e.to_string())), + Err(e) => Err(OMTAEError::Memory(e.to_string())), } } /// Save a session to the database. - pub fn save_session(&self, session: &Session) -> OpenFangResult<()> { + pub fn save_session(&self, session: &Session) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let messages_blob = rmp_serde::to_vec_named(&session.messages) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; let now = Utc::now().to_rfc3339(); conn.execute( "INSERT INTO sessions (id, agent_id, messages, context_window_tokens, label, created_at, updated_at) @@ -96,63 +96,63 @@ impl SessionStore { now, ], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } /// Delete a session from the database. - pub fn delete_session(&self, session_id: SessionId) -> OpenFangResult<()> { + pub fn delete_session(&self, session_id: SessionId) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; conn.execute( "DELETE FROM sessions WHERE id = ?1", rusqlite::params![session_id.0.to_string()], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } /// Delete all sessions belonging to an agent. - pub fn delete_agent_sessions(&self, agent_id: AgentId) -> OpenFangResult<()> { + pub fn delete_agent_sessions(&self, agent_id: AgentId) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; conn.execute( "DELETE FROM sessions WHERE agent_id = ?1", rusqlite::params![agent_id.0.to_string()], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } /// Delete the canonical (cross-channel) session for an agent. - pub fn delete_canonical_session(&self, agent_id: AgentId) -> OpenFangResult<()> { + pub fn delete_canonical_session(&self, agent_id: AgentId) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; conn.execute( "DELETE FROM canonical_sessions WHERE agent_id = ?1", rusqlite::params![agent_id.0.to_string()], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } /// List all sessions with metadata (session_id, agent_id, message_count, created_at). - pub fn list_sessions(&self) -> OpenFangResult> { + pub fn list_sessions(&self) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let mut stmt = conn .prepare( "SELECT id, agent_id, messages, created_at, label FROM sessions ORDER BY created_at DESC", ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let rows = stmt .query_map([], |row| { @@ -173,17 +173,17 @@ impl SessionStore { "label": label, })) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let mut sessions = Vec::new(); for row in rows { - sessions.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + sessions.push(row.map_err(|e| OMTAEError::Memory(e.to_string()))?); } Ok(sessions) } /// Create a new empty session for an agent. - pub fn create_session(&self, agent_id: AgentId) -> OpenFangResult { + pub fn create_session(&self, agent_id: AgentId) -> OMTAEResult { let session = Session { id: SessionId::new(), agent_id, @@ -200,16 +200,16 @@ impl SessionStore { &self, session_id: SessionId, label: Option<&str>, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; conn.execute( "UPDATE sessions SET label = ?1, updated_at = ?2 WHERE id = ?3", rusqlite::params![label, Utc::now().to_rfc3339(), session_id.0.to_string()], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } @@ -218,17 +218,17 @@ impl SessionStore { &self, agent_id: AgentId, label: &str, - ) -> OpenFangResult> { + ) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let mut stmt = conn .prepare( "SELECT id, messages, context_window_tokens, label FROM sessions \ WHERE agent_id = ?1 AND label = ?2 LIMIT 1", ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let result = stmt.query_row(rusqlite::params![agent_id.0.to_string(), label], |row| { let id_str: String = row.get(0)?; @@ -242,9 +242,9 @@ impl SessionStore { Ok((id_str, messages_blob, tokens, lbl)) => { let session_id = uuid::Uuid::parse_str(&id_str) .map(SessionId) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let messages: Vec = rmp_serde::from_slice(&messages_blob) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; Ok(Some(Session { id: session_id, agent_id, @@ -254,23 +254,23 @@ impl SessionStore { })) } Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(OpenFangError::Memory(e.to_string())), + Err(e) => Err(OMTAEError::Memory(e.to_string())), } } } impl SessionStore { /// List all sessions for a specific agent. - pub fn list_agent_sessions(&self, agent_id: AgentId) -> OpenFangResult> { + pub fn list_agent_sessions(&self, agent_id: AgentId) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let mut stmt = conn .prepare( "SELECT id, messages, created_at, label FROM sessions WHERE agent_id = ?1 ORDER BY created_at DESC", ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let rows = stmt .query_map(rusqlite::params![agent_id.0.to_string()], |row| { @@ -288,11 +288,11 @@ impl SessionStore { "label": label, })) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let mut sessions = Vec::new(); for row in rows { - sessions.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + sessions.push(row.map_err(|e| OMTAEError::Memory(e.to_string()))?); } Ok(sessions) } @@ -302,7 +302,7 @@ impl SessionStore { &self, agent_id: AgentId, label: Option<&str>, - ) -> OpenFangResult { + ) -> OMTAEResult { let session = Session { id: SessionId::new(), agent_id, @@ -324,7 +324,7 @@ impl SessionStore { agent_id: AgentId, summary: &str, kept_messages: Vec, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { let mut canonical = self.load_canonical(agent_id)?; canonical.compacted_summary = Some(summary.to_string()); canonical.messages = kept_messages; @@ -361,17 +361,17 @@ pub struct CanonicalSession { impl SessionStore { /// Load the canonical session for an agent, creating one if it doesn't exist. - pub fn load_canonical(&self, agent_id: AgentId) -> OpenFangResult { + pub fn load_canonical(&self, agent_id: AgentId) -> OMTAEResult { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let mut stmt = conn .prepare( "SELECT messages, compaction_cursor, compacted_summary, updated_at \ FROM canonical_sessions WHERE agent_id = ?1", ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let result = stmt.query_row(rusqlite::params![agent_id.0.to_string()], |row| { let messages_blob: Vec = row.get(0)?; @@ -384,7 +384,7 @@ impl SessionStore { match result { Ok((messages_blob, cursor, summary, updated_at)) => { let messages: Vec = rmp_serde::from_slice(&messages_blob) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; Ok(CanonicalSession { agent_id, messages, @@ -403,7 +403,7 @@ impl SessionStore { updated_at: now, }) } - Err(e) => Err(OpenFangError::Memory(e.to_string())), + Err(e) => Err(OMTAEError::Memory(e.to_string())), } } @@ -417,7 +417,7 @@ impl SessionStore { agent_id: AgentId, new_messages: &[Message], compaction_threshold: Option, - ) -> OpenFangResult { + ) -> OMTAEResult { let mut canonical = self.load_canonical(agent_id)?; canonical.messages.extend(new_messages.iter().cloned()); @@ -436,15 +436,15 @@ impl SessionStore { } for msg in compacting { let role = match msg.role { - openfang_types::message::Role::User => "User", - openfang_types::message::Role::Assistant => "Assistant", - openfang_types::message::Role::System => "System", + omtae_types::message::Role::User => "User", + omtae_types::message::Role::Assistant => "Assistant", + omtae_types::message::Role::System => "System", }; let text = msg.content.text_content(); if !text.is_empty() { // Truncate individual messages in summary to keep it compact (UTF-8 safe) let truncated = if text.len() > 200 { - format!("{}...", openfang_types::truncate_str(&text, 200)) + format!("{}...", omtae_types::truncate_str(&text, 200)) } else { text }; @@ -482,7 +482,7 @@ impl SessionStore { &self, agent_id: AgentId, window_size: Option, - ) -> OpenFangResult<(Option, Vec)> { + ) -> OMTAEResult<(Option, Vec)> { let canonical = self.load_canonical(agent_id)?; let window = window_size.unwrap_or(DEFAULT_CANONICAL_WINDOW); let start = canonical.messages.len().saturating_sub(window); @@ -491,13 +491,13 @@ impl SessionStore { } /// Persist a canonical session to SQLite. - fn save_canonical(&self, canonical: &CanonicalSession) -> OpenFangResult<()> { + fn save_canonical(&self, canonical: &CanonicalSession) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let messages_blob = rmp_serde::to_vec_named(&canonical.messages) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; conn.execute( "INSERT INTO canonical_sessions (agent_id, messages, compaction_cursor, compacted_summary, updated_at) VALUES (?1, ?2, ?3, ?4, ?5) @@ -510,7 +510,7 @@ impl SessionStore { canonical.updated_at, ], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } } @@ -589,7 +589,7 @@ impl SessionStore { ContentBlock::Thinking { thinking, .. } => { text_parts.push(format!( "[thinking: {}]", - openfang_types::truncate_str(thinking, 200) + omtae_types::truncate_str(thinking, 200) )); } ContentBlock::RedactedThinking { .. } => { @@ -785,10 +785,10 @@ mod tests { let mut session = store.create_session(agent_id).unwrap(); session .messages - .push(openfang_types::message::Message::user("Hello")); + .push(omtae_types::message::Message::user("Hello")); session .messages - .push(openfang_types::message::Message::assistant("Hi there!")); + .push(omtae_types::message::Message::assistant("Hi there!")); store.save_session(&session).unwrap(); let dir = tempfile::TempDir::new().unwrap(); diff --git a/crates/openfang-memory/src/structured.rs b/crates/openfang-memory/src/structured.rs index fb7b45f5e3..501f1bc9c7 100644 --- a/crates/openfang-memory/src/structured.rs +++ b/crates/openfang-memory/src/structured.rs @@ -1,8 +1,8 @@ //! SQLite structured store for key-value pairs and agent persistence. use chrono::Utc; -use openfang_types::agent::{AgentEntry, AgentId}; -use openfang_types::error::{OpenFangError, OpenFangResult}; +use omtae_types::agent::{AgentEntry, AgentId}; +use omtae_types::error::{OMTAEError, OMTAEResult}; use rusqlite::Connection; use std::sync::{Arc, Mutex}; @@ -19,14 +19,14 @@ impl StructuredStore { } /// Get a value from the key-value store. - pub fn get(&self, agent_id: AgentId, key: &str) -> OpenFangResult> { + pub fn get(&self, agent_id: AgentId, key: &str) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let mut stmt = conn .prepare("SELECT value FROM kv_store WHERE agent_id = ?1 AND key = ?2") - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let result = stmt.query_row(rusqlite::params![agent_id.0.to_string(), key], |row| { let blob: Vec = row.get(0)?; Ok(blob) @@ -34,11 +34,11 @@ impl StructuredStore { match result { Ok(blob) => { let value: serde_json::Value = serde_json::from_slice(&blob) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; Ok(Some(value)) } Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(OpenFangError::Memory(e.to_string())), + Err(e) => Err(OMTAEError::Memory(e.to_string())), } } @@ -48,57 +48,57 @@ impl StructuredStore { agent_id: AgentId, key: &str, value: serde_json::Value, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let blob = - serde_json::to_vec(&value).map_err(|e| OpenFangError::Serialization(e.to_string()))?; + serde_json::to_vec(&value).map_err(|e| OMTAEError::Serialization(e.to_string()))?; let now = Utc::now().to_rfc3339(); conn.execute( "INSERT INTO kv_store (agent_id, key, value, version, updated_at) VALUES (?1, ?2, ?3, 1, ?4) ON CONFLICT(agent_id, key) DO UPDATE SET value = ?3, version = version + 1, updated_at = ?4", rusqlite::params![agent_id.0.to_string(), key, blob, now], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } /// Delete a value from the key-value store. - pub fn delete(&self, agent_id: AgentId, key: &str) -> OpenFangResult<()> { + pub fn delete(&self, agent_id: AgentId, key: &str) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; conn.execute( "DELETE FROM kv_store WHERE agent_id = ?1 AND key = ?2", rusqlite::params![agent_id.0.to_string(), key], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } /// List all key-value pairs for an agent. - pub fn list_kv(&self, agent_id: AgentId) -> OpenFangResult> { + pub fn list_kv(&self, agent_id: AgentId) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let mut stmt = conn .prepare("SELECT key, value FROM kv_store WHERE agent_id = ?1 ORDER BY key") - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let rows = stmt .query_map(rusqlite::params![agent_id.0.to_string()], |row| { let key: String = row.get(0)?; let blob: Vec = row.get(1)?; Ok((key, blob)) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let mut pairs = Vec::new(); for row in rows { - let (key, blob) = row.map_err(|e| OpenFangError::Memory(e.to_string()))?; + let (key, blob) = row.map_err(|e| OMTAEError::Memory(e.to_string()))?; let value: serde_json::Value = serde_json::from_slice(&blob).unwrap_or_else(|_| { // Fallback: try as UTF-8 string String::from_utf8(blob) @@ -111,17 +111,17 @@ impl StructuredStore { } /// Save an agent entry to the database. - pub fn save_agent(&self, entry: &AgentEntry) -> OpenFangResult<()> { + pub fn save_agent(&self, entry: &AgentEntry) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; // Use named-field encoding so new fields with #[serde(default)] are // handled gracefully when the struct evolves between versions. let manifest_blob = rmp_serde::to_vec_named(&entry.manifest) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; let state_str = serde_json::to_string(&entry.state) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; let now = Utc::now().to_rfc3339(); // Add session_id column if it doesn't exist yet (migration compat) @@ -136,7 +136,7 @@ impl StructuredStore { ); let identity_json = serde_json::to_string(&entry.identity) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; conn.execute( "INSERT INTO agents (id, name, manifest, state, created_at, updated_at, session_id, identity) @@ -153,16 +153,16 @@ impl StructuredStore { identity_json, ], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } /// Load an agent entry from the database. - pub fn load_agent(&self, agent_id: AgentId) -> OpenFangResult> { + pub fn load_agent(&self, agent_id: AgentId) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let mut stmt = conn .prepare("SELECT id, name, manifest, state, created_at, updated_at, session_id, identity FROM agents WHERE id = ?1") @@ -173,7 +173,7 @@ impl StructuredStore { conn.prepare("SELECT id, name, manifest, state, created_at, updated_at FROM agents WHERE id = ?1") }) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let col_count = stmt.column_count(); let result = stmt.query_row(rusqlite::params![agent_id.0.to_string()], |row| { @@ -204,16 +204,16 @@ impl StructuredStore { match result { Ok((name, manifest_blob, state_str, created_str, session_id_str, identity_str)) => { let manifest = rmp_serde::from_slice(&manifest_blob) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; let state = serde_json::from_str(&state_str) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; let created_at = chrono::DateTime::parse_from_rfc3339(&created_str) .map(|dt| dt.with_timezone(&Utc)) .unwrap_or_else(|_| Utc::now()); let session_id = session_id_str .and_then(|s| uuid::Uuid::parse_str(&s).ok()) - .map(openfang_types::agent::SessionId) - .unwrap_or_else(openfang_types::agent::SessionId::new); + .map(omtae_types::agent::SessionId) + .unwrap_or_else(omtae_types::agent::SessionId::new); let identity = identity_str .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_default(); @@ -235,21 +235,21 @@ impl StructuredStore { })) } Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(OpenFangError::Memory(e.to_string())), + Err(e) => Err(OMTAEError::Memory(e.to_string())), } } /// Remove an agent from the database. - pub fn remove_agent(&self, agent_id: AgentId) -> OpenFangResult<()> { + pub fn remove_agent(&self, agent_id: AgentId) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; conn.execute( "DELETE FROM agents WHERE id = ?1", rusqlite::params![agent_id.0.to_string()], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } @@ -259,11 +259,11 @@ impl StructuredStore { /// fields gracefully. When an agent is loaded with lenient defaults, it is /// automatically re-saved to upgrade the stored blob. Duplicate agent names /// are deduplicated (first occurrence wins). - pub fn load_all_agents(&self) -> OpenFangResult> { + pub fn load_all_agents(&self) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; // Try with identity+session_id columns first, fall back gracefully let mut stmt = conn @@ -276,7 +276,7 @@ impl StructuredStore { .or_else(|_| { conn.prepare("SELECT id, name, manifest, state, created_at, updated_at FROM agents") }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let col_count = stmt.column_count(); let rows = stmt @@ -306,7 +306,7 @@ impl StructuredStore { identity_str, )) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let mut agents = Vec::new(); let mut seen_names = std::collections::HashSet::new(); @@ -329,7 +329,7 @@ impl StructuredStore { continue; } - let agent_id = match uuid::Uuid::parse_str(&id_str).map(openfang_types::agent::AgentId) + let agent_id = match uuid::Uuid::parse_str(&id_str).map(omtae_types::agent::AgentId) { Ok(id) => id, Err(e) => { @@ -338,7 +338,7 @@ impl StructuredStore { } }; - let manifest: openfang_types::agent::AgentManifest = match rmp_serde::from_slice( + let manifest: omtae_types::agent::AgentManifest = match rmp_serde::from_slice( &manifest_blob, ) { Ok(m) => m, @@ -354,7 +354,7 @@ impl StructuredStore { // Auto-repair: re-serialize with current schema and queue for update. // This upgrades the stored blob so future boots don't hit lenient paths. let new_blob = rmp_serde::to_vec_named(&manifest) - .map_err(|e| OpenFangError::Serialization(e.to_string()))?; + .map_err(|e| OMTAEError::Serialization(e.to_string()))?; if new_blob != manifest_blob { tracing::info!( agent = %name, id = %id_str, @@ -375,8 +375,8 @@ impl StructuredStore { .unwrap_or_else(|_| Utc::now()); let session_id = session_id_str .and_then(|s| uuid::Uuid::parse_str(&s).ok()) - .map(openfang_types::agent::SessionId) - .unwrap_or_else(openfang_types::agent::SessionId::new); + .map(omtae_types::agent::SessionId) + .unwrap_or_else(omtae_types::agent::SessionId::new); let identity = identity_str .and_then(|s| serde_json::from_str(&s).ok()) @@ -414,14 +414,14 @@ impl StructuredStore { } /// List all agents in the database. - pub fn list_agents(&self) -> OpenFangResult> { + pub fn list_agents(&self) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let mut stmt = conn .prepare("SELECT id, name, state FROM agents") - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let rows = stmt .query_map([], |row| { Ok(( @@ -430,10 +430,10 @@ impl StructuredStore { row.get::<_, String>(2)?, )) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let mut agents = Vec::new(); for row in rows { - agents.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + agents.push(row.map_err(|e| OMTAEError::Memory(e.to_string()))?); } Ok(agents) } diff --git a/crates/openfang-memory/src/substrate.rs b/crates/openfang-memory/src/substrate.rs index 03ecd78cfc..7ce9f6f611 100644 --- a/crates/openfang-memory/src/substrate.rs +++ b/crates/openfang-memory/src/substrate.rs @@ -12,10 +12,10 @@ use crate::structured::StructuredStore; use crate::usage::UsageStore; use async_trait::async_trait; -use openfang_types::agent::{AgentEntry, AgentId, SessionId}; -use openfang_types::config::MemoryConfig; -use openfang_types::error::{OpenFangError, OpenFangResult}; -use openfang_types::memory::{ +use omtae_types::agent::{AgentEntry, AgentId, SessionId}; +use omtae_types::config::MemoryConfig; +use omtae_types::error::{OMTAEError, OMTAEResult}; +use omtae_types::memory::{ ConsolidationReport, Entity, ExportFormat, GraphMatch, GraphPattern, ImportReport, Memory, MemoryFilter, MemoryFragment, MemoryId, MemorySource, Relation, }; @@ -47,11 +47,11 @@ impl MemorySubstrate { db_path: &Path, decay_rate: f32, memory_config: &MemoryConfig, - ) -> OpenFangResult { - let conn = Connection::open(db_path).map_err(|e| OpenFangError::Memory(e.to_string()))?; + ) -> OMTAEResult { + let conn = Connection::open(db_path).map_err(|e| OMTAEError::Memory(e.to_string()))?; conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;") - .map_err(|e| OpenFangError::Memory(e.to_string()))?; - run_migrations(&conn).map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; + run_migrations(&conn).map_err(|e| OMTAEError::Memory(e.to_string()))?; let shared = Arc::new(Mutex::new(conn)); let semantic = Self::create_semantic_store(Arc::clone(&shared), memory_config); @@ -104,10 +104,10 @@ impl MemorySubstrate { } /// Create an in-memory substrate (for testing). Always uses SQLite backend. - pub fn open_in_memory(decay_rate: f32) -> OpenFangResult { + pub fn open_in_memory(decay_rate: f32) -> OMTAEResult { let conn = - Connection::open_in_memory().map_err(|e| OpenFangError::Memory(e.to_string()))?; - run_migrations(&conn).map_err(|e| OpenFangError::Memory(e.to_string()))?; + Connection::open_in_memory().map_err(|e| OMTAEError::Memory(e.to_string()))?; + run_migrations(&conn).map_err(|e| OMTAEError::Memory(e.to_string()))?; let shared = Arc::new(Mutex::new(conn)); Ok(Self { @@ -132,29 +132,29 @@ impl MemorySubstrate { } /// Save an agent entry to persistent storage. - pub fn save_agent(&self, entry: &AgentEntry) -> OpenFangResult<()> { + pub fn save_agent(&self, entry: &AgentEntry) -> OMTAEResult<()> { self.structured.save_agent(entry) } /// Load an agent entry from persistent storage. - pub fn load_agent(&self, agent_id: AgentId) -> OpenFangResult> { + pub fn load_agent(&self, agent_id: AgentId) -> OMTAEResult> { self.structured.load_agent(agent_id) } /// Remove an agent from persistent storage and cascade-delete sessions. - pub fn remove_agent(&self, agent_id: AgentId) -> OpenFangResult<()> { + pub fn remove_agent(&self, agent_id: AgentId) -> OMTAEResult<()> { // Delete associated sessions first let _ = self.sessions.delete_agent_sessions(agent_id); self.structured.remove_agent(agent_id) } /// Load all agent entries from persistent storage. - pub fn load_all_agents(&self) -> OpenFangResult> { + pub fn load_all_agents(&self) -> OMTAEResult> { self.structured.load_all_agents() } /// List all saved agents. - pub fn list_agents(&self) -> OpenFangResult> { + pub fn list_agents(&self) -> OMTAEResult> { self.structured.list_agents() } @@ -163,17 +163,17 @@ impl MemorySubstrate { &self, agent_id: AgentId, key: &str, - ) -> OpenFangResult> { + ) -> OMTAEResult> { self.structured.get(agent_id, key) } /// List all KV pairs for an agent. - pub fn list_kv(&self, agent_id: AgentId) -> OpenFangResult> { + pub fn list_kv(&self, agent_id: AgentId) -> OMTAEResult> { self.structured.list_kv(agent_id) } /// Delete a KV entry for an agent. - pub fn structured_delete(&self, agent_id: AgentId, key: &str) -> OpenFangResult<()> { + pub fn structured_delete(&self, agent_id: AgentId, key: &str) -> OMTAEResult<()> { self.structured.delete(agent_id, key) } @@ -183,52 +183,52 @@ impl MemorySubstrate { agent_id: AgentId, key: &str, value: serde_json::Value, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { self.structured.set(agent_id, key, value) } /// Get a session by ID. - pub fn get_session(&self, session_id: SessionId) -> OpenFangResult> { + pub fn get_session(&self, session_id: SessionId) -> OMTAEResult> { self.sessions.get_session(session_id) } /// Save a session. - pub fn save_session(&self, session: &Session) -> OpenFangResult<()> { + pub fn save_session(&self, session: &Session) -> OMTAEResult<()> { self.sessions.save_session(session) } /// Save a session asynchronously — runs the SQLite write in a blocking /// thread so the tokio runtime stays responsive. - pub async fn save_session_async(&self, session: &Session) -> OpenFangResult<()> { + pub async fn save_session_async(&self, session: &Session) -> OMTAEResult<()> { let sessions = self.sessions.clone(); let session = session.clone(); tokio::task::spawn_blocking(move || sessions.save_session(&session)) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } /// Create a new empty session for an agent. - pub fn create_session(&self, agent_id: AgentId) -> OpenFangResult { + pub fn create_session(&self, agent_id: AgentId) -> OMTAEResult { self.sessions.create_session(agent_id) } /// List all sessions with metadata. - pub fn list_sessions(&self) -> OpenFangResult> { + pub fn list_sessions(&self) -> OMTAEResult> { self.sessions.list_sessions() } /// Delete a session by ID. - pub fn delete_session(&self, session_id: SessionId) -> OpenFangResult<()> { + pub fn delete_session(&self, session_id: SessionId) -> OMTAEResult<()> { self.sessions.delete_session(session_id) } /// Delete all sessions belonging to an agent. - pub fn delete_agent_sessions(&self, agent_id: AgentId) -> OpenFangResult<()> { + pub fn delete_agent_sessions(&self, agent_id: AgentId) -> OMTAEResult<()> { self.sessions.delete_agent_sessions(agent_id) } /// Delete the canonical (cross-channel) session for an agent. - pub fn delete_canonical_session(&self, agent_id: AgentId) -> OpenFangResult<()> { + pub fn delete_canonical_session(&self, agent_id: AgentId) -> OMTAEResult<()> { self.sessions.delete_canonical_session(agent_id) } @@ -237,7 +237,7 @@ impl MemorySubstrate { &self, session_id: SessionId, label: Option<&str>, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { self.sessions.set_session_label(session_id, label) } @@ -246,12 +246,12 @@ impl MemorySubstrate { &self, agent_id: AgentId, label: &str, - ) -> OpenFangResult> { + ) -> OMTAEResult> { self.sessions.find_session_by_label(agent_id, label) } /// List all sessions for a specific agent. - pub fn list_agent_sessions(&self, agent_id: AgentId) -> OpenFangResult> { + pub fn list_agent_sessions(&self, agent_id: AgentId) -> OMTAEResult> { self.sessions.list_agent_sessions(agent_id) } @@ -260,7 +260,7 @@ impl MemorySubstrate { &self, agent_id: AgentId, label: Option<&str>, - ) -> OpenFangResult { + ) -> OMTAEResult { self.sessions.create_session_with_label(agent_id, label) } @@ -272,7 +272,7 @@ impl MemorySubstrate { &self, agent_id: AgentId, window_size: Option, - ) -> OpenFangResult<(Option, Vec)> { + ) -> OMTAEResult<(Option, Vec)> { self.sessions.canonical_context(agent_id, window_size) } @@ -284,8 +284,8 @@ impl MemorySubstrate { &self, agent_id: AgentId, summary: &str, - kept_messages: Vec, - ) -> OpenFangResult<()> { + kept_messages: Vec, + ) -> OMTAEResult<()> { self.sessions .store_llm_summary(agent_id, summary, kept_messages) } @@ -306,9 +306,9 @@ impl MemorySubstrate { pub fn append_canonical( &self, agent_id: AgentId, - messages: &[openfang_types::message::Message], + messages: &[omtae_types::message::Message], compaction_threshold: Option, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { self.sessions .append_canonical(agent_id, messages, compaction_threshold)?; Ok(()) @@ -319,14 +319,14 @@ impl MemorySubstrate { // ----------------------------------------------------------------- /// Load all paired devices from the database. - pub fn load_paired_devices(&self) -> OpenFangResult> { + pub fn load_paired_devices(&self) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let mut stmt = conn.prepare( "SELECT device_id, display_name, platform, paired_at, last_seen, push_token FROM paired_devices" - ).map_err(|e| OpenFangError::Memory(e.to_string()))?; + ).map_err(|e| OMTAEError::Memory(e.to_string()))?; let rows = stmt .query_map([], |row| { Ok(serde_json::json!({ @@ -338,10 +338,10 @@ impl MemorySubstrate { "push_token": row.get::<_, Option>(5)?, })) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let mut devices = Vec::new(); for row in rows { - devices.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + devices.push(row.map_err(|e| OMTAEError::Memory(e.to_string()))?); } Ok(devices) } @@ -355,29 +355,29 @@ impl MemorySubstrate { paired_at: &str, last_seen: &str, push_token: Option<&str>, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; conn.execute( "INSERT OR REPLACE INTO paired_devices (device_id, display_name, platform, paired_at, last_seen, push_token) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", rusqlite::params![device_id, display_name, platform, paired_at, last_seen, push_token], - ).map_err(|e| OpenFangError::Memory(e.to_string()))?; + ).map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } /// Remove a paired device from the database. - pub fn remove_paired_device(&self, device_id: &str) -> OpenFangResult<()> { + pub fn remove_paired_device(&self, device_id: &str) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; conn.execute( "DELETE FROM paired_devices WHERE device_id = ?1", rusqlite::params![device_id], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } @@ -394,7 +394,7 @@ impl MemorySubstrate { scope: &str, metadata: HashMap, embedding: Option<&[f32]>, - ) -> OpenFangResult { + ) -> OMTAEResult { self.semantic .remember_with_embedding(agent_id, content, source, scope, metadata, embedding) } @@ -406,13 +406,13 @@ impl MemorySubstrate { limit: usize, filter: Option, query_embedding: Option<&[f32]>, - ) -> OpenFangResult> { + ) -> OMTAEResult> { self.semantic .recall_with_embedding(query, limit, filter, query_embedding) } /// Update the embedding for an existing memory. - pub fn update_embedding(&self, id: MemoryId, embedding: &[f32]) -> OpenFangResult<()> { + pub fn update_embedding(&self, id: MemoryId, embedding: &[f32]) -> OMTAEResult<()> { self.semantic.update_embedding(id, embedding) } @@ -423,7 +423,7 @@ impl MemorySubstrate { limit: usize, filter: Option, query_embedding: Option<&[f32]>, - ) -> OpenFangResult> { + ) -> OMTAEResult> { let store = self.semantic.clone(); let query = query.to_string(); let embedding_owned = query_embedding.map(|e| e.to_vec()); @@ -431,7 +431,7 @@ impl MemorySubstrate { store.recall_with_embedding(&query, limit, filter, embedding_owned.as_deref()) }) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } /// Async wrapper for `remember_with_embedding` — runs in a blocking thread. @@ -443,7 +443,7 @@ impl MemorySubstrate { scope: &str, metadata: HashMap, embedding: Option<&[f32]>, - ) -> OpenFangResult { + ) -> OMTAEResult { let store = self.semantic.clone(); let content = content.to_string(); let scope = scope.to_string(); @@ -459,7 +459,7 @@ impl MemorySubstrate { ) }) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } // ----------------------------------------------------------------- @@ -473,7 +473,7 @@ impl MemorySubstrate { description: &str, assigned_to: Option<&str>, created_by: Option<&str>, - ) -> OpenFangResult { + ) -> OMTAEResult { let conn = Arc::clone(&self.conn); let title = title.to_string(); let description = description.to_string(); @@ -483,26 +483,26 @@ impl MemorySubstrate { tokio::task::spawn_blocking(move || { let id = uuid::Uuid::new_v4().to_string(); let now = chrono::Utc::now().to_rfc3339(); - let db = conn.lock().map_err(|e| OpenFangError::Internal(e.to_string()))?; + let db = conn.lock().map_err(|e| OMTAEError::Internal(e.to_string()))?; db.execute( "INSERT INTO task_queue (id, agent_id, task_type, payload, status, priority, created_at, title, description, assigned_to, created_by) VALUES (?1, ?2, ?3, ?4, 'pending', 0, ?5, ?6, ?7, ?8, ?9)", rusqlite::params![id, &created_by, &title, b"", now, title, description, assigned_to, created_by], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(id) }) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } /// Claim the next pending task (optionally for a specific assignee). Returns task JSON or None. - pub async fn task_claim(&self, agent_id: &str) -> OpenFangResult> { + pub async fn task_claim(&self, agent_id: &str) -> OMTAEResult> { let conn = Arc::clone(&self.conn); let agent_id = agent_id.to_string(); tokio::task::spawn_blocking(move || { - let db = conn.lock().map_err(|e| OpenFangError::Internal(e.to_string()))?; + let db = conn.lock().map_err(|e| OMTAEError::Internal(e.to_string()))?; // Find first pending task assigned to this agent, or any unassigned pending task let mut stmt = db.prepare( "SELECT id, title, description, assigned_to, created_by, created_at @@ -510,7 +510,7 @@ impl MemorySubstrate { WHERE status = 'pending' AND (assigned_to = ?1 OR assigned_to = '') ORDER BY priority DESC, created_at ASC LIMIT 1" - ).map_err(|e| OpenFangError::Memory(e.to_string()))?; + ).map_err(|e| OMTAEError::Memory(e.to_string()))?; let result = stmt.query_row(rusqlite::params![agent_id], |row| { Ok(( @@ -529,7 +529,7 @@ impl MemorySubstrate { db.execute( "UPDATE task_queue SET status = 'in_progress', assigned_to = ?2 WHERE id = ?1", rusqlite::params![id, agent_id], - ).map_err(|e| OpenFangError::Memory(e.to_string()))?; + ).map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(Some(serde_json::json!({ "id": id, @@ -542,42 +542,42 @@ impl MemorySubstrate { }))) } Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(OpenFangError::Memory(e.to_string())), + Err(e) => Err(OMTAEError::Memory(e.to_string())), } }) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } /// Mark a task as completed with a result string. - pub async fn task_complete(&self, task_id: &str, result: &str) -> OpenFangResult<()> { + pub async fn task_complete(&self, task_id: &str, result: &str) -> OMTAEResult<()> { let conn = Arc::clone(&self.conn); let task_id = task_id.to_string(); let result = result.to_string(); tokio::task::spawn_blocking(move || { let now = chrono::Utc::now().to_rfc3339(); - let db = conn.lock().map_err(|e| OpenFangError::Internal(e.to_string()))?; + let db = conn.lock().map_err(|e| OMTAEError::Internal(e.to_string()))?; let rows = db.execute( "UPDATE task_queue SET status = 'completed', result = ?2, completed_at = ?3 WHERE id = ?1", rusqlite::params![task_id, result, now], - ).map_err(|e| OpenFangError::Memory(e.to_string()))?; + ).map_err(|e| OMTAEError::Memory(e.to_string()))?; if rows == 0 { - return Err(OpenFangError::Internal(format!("Task not found: {task_id}"))); + return Err(OMTAEError::Internal(format!("Task not found: {task_id}"))); } Ok(()) }) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } /// List tasks, optionally filtered by status. - pub async fn task_list(&self, status: Option<&str>) -> OpenFangResult> { + pub async fn task_list(&self, status: Option<&str>) -> OMTAEResult> { let conn = Arc::clone(&self.conn); let status = status.map(|s| s.to_string()); tokio::task::spawn_blocking(move || { - let db = conn.lock().map_err(|e| OpenFangError::Internal(e.to_string()))?; + let db = conn.lock().map_err(|e| OMTAEError::Internal(e.to_string()))?; let (sql, params): (&str, Vec>) = match &status { Some(s) => ( "SELECT id, title, description, status, assigned_to, created_by, created_at, completed_at, result FROM task_queue WHERE status = ?1 ORDER BY created_at DESC", @@ -589,7 +589,7 @@ impl MemorySubstrate { ), }; - let mut stmt = db.prepare(sql).map_err(|e| OpenFangError::Memory(e.to_string()))?; + let mut stmt = db.prepare(sql).map_err(|e| OMTAEError::Memory(e.to_string()))?; let params_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect(); let rows = stmt.query_map(params_refs.as_slice(), |row| { Ok(serde_json::json!({ @@ -603,27 +603,27 @@ impl MemorySubstrate { "completed_at": row.get::<_, Option>(7).unwrap_or(None), "result": row.get::<_, Option>(8).unwrap_or(None), })) - }).map_err(|e| OpenFangError::Memory(e.to_string()))?; + }).map_err(|e| OMTAEError::Memory(e.to_string()))?; let mut tasks = Vec::new(); for row in rows { - tasks.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + tasks.push(row.map_err(|e| OMTAEError::Memory(e.to_string()))?); } Ok(tasks) }) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } } #[async_trait] impl Memory for MemorySubstrate { - async fn get(&self, agent_id: AgentId, key: &str) -> OpenFangResult> { + async fn get(&self, agent_id: AgentId, key: &str) -> OMTAEResult> { let store = self.structured.clone(); let key = key.to_string(); tokio::task::spawn_blocking(move || store.get(agent_id, &key)) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } async fn set( @@ -631,20 +631,20 @@ impl Memory for MemorySubstrate { agent_id: AgentId, key: &str, value: serde_json::Value, - ) -> OpenFangResult<()> { + ) -> OMTAEResult<()> { let store = self.structured.clone(); let key = key.to_string(); tokio::task::spawn_blocking(move || store.set(agent_id, &key, value)) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } - async fn delete(&self, agent_id: AgentId, key: &str) -> OpenFangResult<()> { + async fn delete(&self, agent_id: AgentId, key: &str) -> OMTAEResult<()> { let store = self.structured.clone(); let key = key.to_string(); tokio::task::spawn_blocking(move || store.delete(agent_id, &key)) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } async fn remember( @@ -654,7 +654,7 @@ impl Memory for MemorySubstrate { source: MemorySource, scope: &str, metadata: HashMap, - ) -> OpenFangResult { + ) -> OMTAEResult { let store = self.semantic.clone(); let content = content.to_string(); let scope = scope.to_string(); @@ -662,7 +662,7 @@ impl Memory for MemorySubstrate { store.remember(agent_id, &content, source, &scope, metadata) }) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } async fn recall( @@ -670,55 +670,55 @@ impl Memory for MemorySubstrate { query: &str, limit: usize, filter: Option, - ) -> OpenFangResult> { + ) -> OMTAEResult> { let store = self.semantic.clone(); let query = query.to_string(); tokio::task::spawn_blocking(move || store.recall(&query, limit, filter)) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } - async fn forget(&self, id: MemoryId) -> OpenFangResult<()> { + async fn forget(&self, id: MemoryId) -> OMTAEResult<()> { let store = self.semantic.clone(); tokio::task::spawn_blocking(move || store.forget(id)) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } - async fn add_entity(&self, entity: Entity) -> OpenFangResult { + async fn add_entity(&self, entity: Entity) -> OMTAEResult { let store = self.knowledge.clone(); tokio::task::spawn_blocking(move || store.add_entity(entity)) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } - async fn add_relation(&self, relation: Relation) -> OpenFangResult { + async fn add_relation(&self, relation: Relation) -> OMTAEResult { let store = self.knowledge.clone(); tokio::task::spawn_blocking(move || store.add_relation(relation)) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } - async fn query_graph(&self, pattern: GraphPattern) -> OpenFangResult> { + async fn query_graph(&self, pattern: GraphPattern) -> OMTAEResult> { let store = self.knowledge.clone(); tokio::task::spawn_blocking(move || store.query_graph(pattern)) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } - async fn consolidate(&self) -> OpenFangResult { + async fn consolidate(&self) -> OMTAEResult { let engine = self.consolidation.clone(); tokio::task::spawn_blocking(move || engine.consolidate()) .await - .map_err(|e| OpenFangError::Internal(e.to_string()))? + .map_err(|e| OMTAEError::Internal(e.to_string()))? } - async fn export(&self, format: ExportFormat) -> OpenFangResult> { + async fn export(&self, format: ExportFormat) -> OMTAEResult> { let _ = format; Ok(Vec::new()) } - async fn import(&self, _data: &[u8], _format: ExportFormat) -> OpenFangResult { + async fn import(&self, _data: &[u8], _format: ExportFormat) -> OMTAEResult { Ok(ImportReport { entities_imported: 0, relations_imported: 0, diff --git a/crates/openfang-memory/src/usage.rs b/crates/openfang-memory/src/usage.rs index 277fef438d..2ca0545fe2 100644 --- a/crates/openfang-memory/src/usage.rs +++ b/crates/openfang-memory/src/usage.rs @@ -1,8 +1,8 @@ //! Usage tracking store — records LLM usage events for cost monitoring. use chrono::Utc; -use openfang_types::agent::AgentId; -use openfang_types::error::{OpenFangError, OpenFangResult}; +use omtae_types::agent::AgentId; +use omtae_types::error::{OMTAEError, OMTAEResult}; use rusqlite::Connection; use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex}; @@ -80,11 +80,11 @@ impl UsageStore { } /// Record a usage event. - pub fn record(&self, record: &UsageRecord) -> OpenFangResult<()> { + pub fn record(&self, record: &UsageRecord) -> OMTAEResult<()> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let id = uuid::Uuid::new_v4().to_string(); let now = Utc::now().to_rfc3339(); conn.execute( @@ -101,16 +101,16 @@ impl UsageStore { record.tool_calls as i64, ], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(()) } /// Query total cost in the last hour for an agent. - pub fn query_hourly(&self, agent_id: AgentId) -> OpenFangResult { + pub fn query_hourly(&self, agent_id: AgentId) -> OMTAEResult { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let cost: f64 = conn .query_row( "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events @@ -118,16 +118,16 @@ impl UsageStore { rusqlite::params![agent_id.0.to_string()], |row| row.get(0), ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(cost) } /// Query total cost today for an agent. - pub fn query_daily(&self, agent_id: AgentId) -> OpenFangResult { + pub fn query_daily(&self, agent_id: AgentId) -> OMTAEResult { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let cost: f64 = conn .query_row( "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events @@ -135,16 +135,16 @@ impl UsageStore { rusqlite::params![agent_id.0.to_string()], |row| row.get(0), ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(cost) } /// Query total cost in the current calendar month for an agent. - pub fn query_monthly(&self, agent_id: AgentId) -> OpenFangResult { + pub fn query_monthly(&self, agent_id: AgentId) -> OMTAEResult { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let cost: f64 = conn .query_row( "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events @@ -152,16 +152,16 @@ impl UsageStore { rusqlite::params![agent_id.0.to_string()], |row| row.get(0), ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(cost) } /// Query total cost across all agents for the current hour. - pub fn query_global_hourly(&self) -> OpenFangResult { + pub fn query_global_hourly(&self) -> OMTAEResult { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let cost: f64 = conn .query_row( "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events @@ -169,16 +169,16 @@ impl UsageStore { [], |row| row.get(0), ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(cost) } /// Query total cost across all agents for the current calendar month. - pub fn query_global_monthly(&self) -> OpenFangResult { + pub fn query_global_monthly(&self) -> OMTAEResult { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let cost: f64 = conn .query_row( "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events @@ -186,16 +186,16 @@ impl UsageStore { [], |row| row.get(0), ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(cost) } /// Query usage summary, optionally filtered by agent. - pub fn query_summary(&self, agent_id: Option) -> OpenFangResult { + pub fn query_summary(&self, agent_id: Option) -> OMTAEResult { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let (sql, params): (&str, Vec>) = match agent_id { Some(aid) => ( @@ -225,17 +225,17 @@ impl UsageStore { total_tool_calls: row.get::<_, i64>(4)? as u64, }) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(summary) } /// Query usage grouped by model. - pub fn query_by_model(&self) -> OpenFangResult> { + pub fn query_by_model(&self) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let mut stmt = conn .prepare( @@ -243,7 +243,7 @@ impl UsageStore { COALESCE(SUM(output_tokens), 0), COUNT(*) FROM usage_events GROUP BY model ORDER BY SUM(cost_usd) DESC", ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let rows = stmt .query_map([], |row| { @@ -255,21 +255,21 @@ impl UsageStore { call_count: row.get::<_, i64>(4)? as u64, }) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let mut results = Vec::new(); for row in rows { - results.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + results.push(row.map_err(|e| OMTAEError::Memory(e.to_string()))?); } Ok(results) } /// Query daily usage breakdown for the last N days. - pub fn query_daily_breakdown(&self, days: u32) -> OpenFangResult> { + pub fn query_daily_breakdown(&self, days: u32) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let mut stmt = conn .prepare(&format!( @@ -282,7 +282,7 @@ impl UsageStore { GROUP BY day ORDER BY day ASC" )) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let rows = stmt .query_map([], |row| { @@ -293,35 +293,35 @@ impl UsageStore { calls: row.get::<_, i64>(3)? as u64, }) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; let mut results = Vec::new(); for row in rows { - results.push(row.map_err(|e| OpenFangError::Memory(e.to_string()))?); + results.push(row.map_err(|e| OMTAEError::Memory(e.to_string()))?); } Ok(results) } /// Query the timestamp of the earliest usage event. - pub fn query_first_event_date(&self) -> OpenFangResult> { + pub fn query_first_event_date(&self) -> OMTAEResult> { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let result: Option = conn .query_row("SELECT MIN(timestamp) FROM usage_events", [], |row| { row.get(0) }) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(result) } /// Query today's total cost across all agents. - pub fn query_today_cost(&self) -> OpenFangResult { + pub fn query_today_cost(&self) -> OMTAEResult { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let cost: f64 = conn .query_row( "SELECT COALESCE(SUM(cost_usd), 0.0) FROM usage_events @@ -329,16 +329,16 @@ impl UsageStore { [], |row| row.get(0), ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(cost) } /// Delete usage events older than the given number of days. - pub fn cleanup_old(&self, days: u32) -> OpenFangResult { + pub fn cleanup_old(&self, days: u32) -> OMTAEResult { let conn = self .conn .lock() - .map_err(|e| OpenFangError::Internal(e.to_string()))?; + .map_err(|e| OMTAEError::Internal(e.to_string()))?; let deleted = conn .execute( &format!( @@ -346,7 +346,7 @@ impl UsageStore { ), [], ) - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; Ok(deleted) } } diff --git a/crates/openfang-migrate/Cargo.toml b/crates/openfang-migrate/Cargo.toml index db5c9888fa..8f7258c456 100644 --- a/crates/openfang-migrate/Cargo.toml +++ b/crates/openfang-migrate/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "openfang-migrate" +name = "omtae-migrate" version.workspace = true edition.workspace = true license.workspace = true -description = "Migration engine for importing from other agent frameworks into OpenFang" +description = "Migration engine for importing from other agent frameworks into OMTAE" [dependencies] -openfang-types = { path = "../openfang-types" } +omtae-types = { path = "../omtae-types" } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } diff --git a/crates/openfang-migrate/src/lib.rs b/crates/openfang-migrate/src/lib.rs index 3eecd5462d..289444b984 100644 --- a/crates/openfang-migrate/src/lib.rs +++ b/crates/openfang-migrate/src/lib.rs @@ -1,4 +1,4 @@ -//! Migration engine for importing from other agent frameworks into OpenFang. +//! Migration engine for importing from other agent frameworks into OMTAE. //! //! Supports importing agents, memory, sessions, skills, and channel configs //! from OpenClaw and other frameworks. @@ -36,7 +36,7 @@ pub struct MigrateOptions { pub source: MigrateSource, /// Path to the source workspace directory. pub source_dir: PathBuf, - /// Path to the OpenFang home directory. + /// Path to the OMTAE home directory. pub target_dir: PathBuf, /// If true, only report what would be done without making changes. pub dry_run: bool, diff --git a/crates/openfang-migrate/src/openclaw.rs b/crates/openfang-migrate/src/openclaw.rs index 483e9e710b..e68250353a 100644 --- a/crates/openfang-migrate/src/openclaw.rs +++ b/crates/openfang-migrate/src/openclaw.rs @@ -410,21 +410,21 @@ struct LegacyYamlChannelConfig { } // --------------------------------------------------------------------------- -// OpenFang output types (TOML) +// OMTAE output types (TOML) // --------------------------------------------------------------------------- -/// OpenFang config.toml structure for serialization. +/// OMTAE config.toml structure for serialization. #[derive(Serialize)] -struct OpenFangConfig { - default_model: OpenFangModelConfig, - memory: OpenFangMemorySection, - network: OpenFangNetworkSection, +struct OMTAEConfig { + default_model: OMTAEModelConfig, + memory: OMTAEMemorySection, + network: OMTAENetworkSection, #[serde(skip_serializing_if = "Option::is_none")] channels: Option, } #[derive(Serialize)] -struct OpenFangModelConfig { +struct OMTAEModelConfig { provider: String, model: String, api_key_env: String, @@ -433,12 +433,12 @@ struct OpenFangModelConfig { } #[derive(Serialize)] -struct OpenFangMemorySection { +struct OMTAEMemorySection { decay_rate: f32, } #[derive(Serialize)] -struct OpenFangNetworkSection { +struct OMTAENetworkSection { listen_addr: String, } @@ -486,7 +486,7 @@ fn write_secret_env(path: &Path, key: &str, value: &str) -> Result<(), std::io:: Ok(()) } -/// Map OpenClaw DM policy to OpenFang DM policy string. +/// Map OpenClaw DM policy to OMTAE DM policy string. fn map_dm_policy(oc: &str) -> &'static str { match oc.to_lowercase().as_str() { "open" => "respond", @@ -496,7 +496,7 @@ fn map_dm_policy(oc: &str) -> &'static str { } } -/// Map OpenClaw group policy to OpenFang group policy string. +/// Map OpenClaw group policy to OMTAE group policy string. fn map_group_policy(oc: &str) -> &'static str { match oc.to_lowercase().as_str() { "open" => "respond", @@ -665,12 +665,12 @@ fn find_config_file(dir: &Path) -> Option { } // Tool name mapping and recognition are shared with the skill system. -use openfang_types::tool_compat::{is_known_openfang_tool, map_tool_name}; +use omtae_types::tool_compat::{is_known_omtae_tool, map_tool_name}; -/// Map OpenClaw tool profile to OpenFang capability tool list. +/// Map OpenClaw tool profile to OMTAE capability tool list. /// Delegates to `ToolProfile` so the migration and kernel use identical definitions. fn tools_for_profile(profile: &str) -> Vec { - use openfang_types::agent::ToolProfile; + use omtae_types::agent::ToolProfile; let p = match profile { "minimal" => ToolProfile::Minimal, "coding" => ToolProfile::Coding, @@ -682,7 +682,7 @@ fn tools_for_profile(profile: &str) -> Vec { p.tools() } -/// Map OpenClaw provider name to OpenFang provider name. +/// Map OpenClaw provider name to OMTAE provider name. fn map_provider(openclaw_provider: &str) -> String { match openclaw_provider.to_lowercase().as_str() { "anthropic" | "claude" => "anthropic".to_string(), @@ -706,7 +706,7 @@ fn map_provider(openclaw_provider: &str) -> String { "xai" | "grok" => "xai".to_string(), "cerebras" => "cerebras".to_string(), "sambanova" => "sambanova".to_string(), - // Additional OpenFang-supported providers and aliases + // Additional OMTAE-supported providers and aliases "perplexity" => "perplexity".to_string(), "cohere" => "cohere".to_string(), "ai21" => "ai21".to_string(), @@ -754,7 +754,7 @@ fn default_api_key_env(provider: &str) -> String { } } -fn is_known_openfang_provider_id(provider: &str) -> bool { +fn is_known_omtae_provider_id(provider: &str) -> bool { matches!( provider, "anthropic" @@ -845,8 +845,8 @@ fn resolve_provider_with_models_context( .unwrap_or_default() .to_lowercase(); - let raw_is_known = is_known_openfang_provider_id(&raw); - let mapped_is_known = is_known_openfang_provider_id(&mapped); + let raw_is_known = is_known_omtae_provider_id(&raw); + let mapped_is_known = is_known_omtae_provider_id(&mapped); let provider = if api_hint.contains("anthropic") { "anthropic".to_string() @@ -1377,15 +1377,15 @@ fn migrate_config_from_json( // Extract channels (writes secrets.env) let channels = migrate_channels_from_json(root, target, dry_run, report); - let of_config = OpenFangConfig { - default_model: OpenFangModelConfig { + let of_config = OMTAEConfig { + default_model: OMTAEModelConfig { provider: resolved.provider, model: resolved.model, api_key_env, base_url: resolved.base_url, }, - memory: OpenFangMemorySection { decay_rate: 0.05 }, - network: OpenFangNetworkSection { + memory: OMTAEMemorySection { decay_rate: 0.05 }, + network: OMTAENetworkSection { listen_addr: "127.0.0.1:4200".to_string(), }, channels, @@ -1394,7 +1394,7 @@ fn migrate_config_from_json( let toml_str = toml::to_string_pretty(&of_config)?; let config_content = format!( - "# OpenFang Agent OS configuration\n\ + "# OMTAE Agent OS configuration\n\ # Migrated from OpenClaw on {}\n\n\ {toml_str}", chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"), @@ -1879,12 +1879,12 @@ fn migrate_channels_from_json( }); } - // --- BlueBubbles (skip — no OpenFang adapter) --- + // --- BlueBubbles (skip — no OMTAE adapter) --- if oc_channels.bluebubbles.is_some() { report.skipped.push(SkippedItem { kind: ItemKind::Channel, name: "bluebubbles".to_string(), - reason: "No OpenFang adapter available — consider using the iMessage channel instead" + reason: "No OMTAE adapter available — consider using the iMessage channel instead" .to_string(), }); } @@ -1894,7 +1894,7 @@ fn migrate_channels_from_json( report.skipped.push(SkippedItem { kind: ItemKind::Channel, name: key.clone(), - reason: format!("Unknown channel '{key}' — not mapped to any OpenFang adapter"), + reason: format!("Unknown channel '{key}' — not mapped to any OMTAE adapter"), }); } @@ -1952,7 +1952,7 @@ fn migrate_agents_from_json( for tool in &unmapped_tools { report.warnings.push(format!( - "Agent '{id}': tool '{tool}' has no OpenFang equivalent and was skipped" + "Agent '{id}': tool '{tool}' has no OMTAE equivalent and was skipped" )); } @@ -1999,7 +1999,7 @@ fn convert_agent_from_json( let allow = extract_string_list(allow_val); let mut mapped = Vec::new(); for t in &allow { - if is_known_openfang_tool(t) { + if is_known_omtae_tool(t) { mapped.push(t.clone()); } else if let Some(of_name) = map_tool_name(t) { mapped.push(of_name.to_string()); @@ -2011,7 +2011,7 @@ fn convert_agent_from_json( if let Some(ref also_val) = agent_tools.also_allow { let also = extract_string_list(also_val); for t in &also { - if is_known_openfang_tool(t) { + if is_known_omtae_tool(t) { mapped.push(t.clone()); } else if let Some(of_name) = map_tool_name(t) { mapped.push(of_name.to_string()); @@ -2050,14 +2050,14 @@ fn convert_agent_from_json( .or_else(|| defaults.and_then(|d| d.identity.clone())) .unwrap_or_else(|| { format!( - "You are {display_name}, an AI agent running on the OpenFang Agent OS. You are helpful, concise, and accurate." + "You are {display_name}, an AI agent running on the OMTAE Agent OS. You are helpful, concise, and accurate." ) }); // Build agent TOML let mut toml_str = String::new(); toml_str.push_str(&format!( - "# OpenFang agent manifest\n# Migrated from OpenClaw agent '{id}'\n\n" + "# OMTAE agent manifest\n# Migrated from OpenClaw agent '{id}'\n\n" )); toml_str.push_str(&format!( "name = \"{}\"\n", @@ -2067,7 +2067,7 @@ fn convert_agent_from_json( toml_str.push_str(&format!( "description = \"Migrated from OpenClaw agent '{id}'\"\n" )); - toml_str.push_str("author = \"openfang\"\n"); + toml_str.push_str("author = \"omtae\"\n"); toml_str.push_str("module = \"builtin:chat\"\n"); toml_str.push_str("\n[model]\n"); @@ -2152,7 +2152,7 @@ fn resolve_default_tools(defaults: Option<&OpenClawAgentDefaults>) -> Vec {} @@ -2960,7 +2960,7 @@ fn migrate_legacy_agents( for tool in &unmapped_tools { report.warnings.push(format!( - "Agent '{agent_name}': tool '{tool}' has no OpenFang equivalent and was skipped" + "Agent '{agent_name}': tool '{tool}' has no OMTAE equivalent and was skipped" )); } @@ -2993,7 +2993,7 @@ fn convert_legacy_agent( let tools: Vec = if !oc.tools.is_empty() { let mut mapped = Vec::new(); for t in &oc.tools { - if is_known_openfang_tool(t) { + if is_known_omtae_tool(t) { mapped.push(t.clone()); } else if let Some(of_name) = map_tool_name(t) { mapped.push(of_name.to_string()); @@ -3021,7 +3021,7 @@ fn convert_legacy_agent( let system_prompt = oc.system_prompt.unwrap_or_else(|| { format!( - "You are {}, an AI agent running on the OpenFang Agent OS. {}", + "You are {}, an AI agent running on the OMTAE Agent OS. {}", oc.name, if oc.description.is_empty() { "You are helpful, concise, and accurate.".to_string() @@ -3042,7 +3042,7 @@ fn convert_legacy_agent( let mut toml_str = String::new(); toml_str.push_str(&format!( - "# OpenFang agent manifest\n# Migrated from OpenClaw agent '{}'\n\n", + "# OMTAE agent manifest\n# Migrated from OpenClaw agent '{}'\n\n", oc.name )); toml_str.push_str(&format!("name = \"{}\"\n", oc.name)); @@ -3051,7 +3051,7 @@ fn convert_legacy_agent( "description = \"{}\"\n", oc.description.replace('"', "\\\"") )); - toml_str.push_str("author = \"openfang\"\n"); + toml_str.push_str("author = \"omtae\"\n"); toml_str.push_str("module = \"builtin:chat\"\n"); if !oc.tags.is_empty() { @@ -3234,7 +3234,7 @@ fn scan_legacy_skills(source: &Path, report: &mut MigrationReport) { report.skipped.push(SkippedItem { kind: ItemKind::Skill, name: name.clone(), - reason: "Node.js skill — run with `openfang skill install` after migration" + reason: "Node.js skill — run with `omtae skill install` after migration" .to_string(), }); } else { @@ -3379,7 +3379,7 @@ mod tests { host: "irc.libera.chat", port: 6697, tls: true, - nick: "openfang-bot", + nick: "omtae-bot", password: "irc-secret-pw", channels: ["#dev", "#general"] }, @@ -4245,7 +4245,7 @@ mod tests { assert_eq!(map_tool_name("WebSearch"), Some("web_search")); assert_eq!(map_tool_name("WebFetch"), Some("web_fetch")); assert_eq!(map_tool_name("sessions_send"), Some("agent_send")); - assert_eq!(map_tool_name("sessions_spawn"), Some("agent_send")); + assert_eq!(map_tool_name("sessions_spawn"), Some("agent_spawn")); } #[test] @@ -4407,12 +4407,12 @@ mod tests { } #[test] - fn test_is_known_openfang_tool() { - assert!(is_known_openfang_tool("file_read")); - assert!(is_known_openfang_tool("shell_exec")); - assert!(is_known_openfang_tool("web_fetch")); - assert!(!is_known_openfang_tool("Read")); - assert!(!is_known_openfang_tool("unknown")); + fn test_is_known_omtae_tool() { + assert!(is_known_omtae_tool("file_read")); + assert!(is_known_omtae_tool("shell_exec")); + assert!(is_known_omtae_tool("web_fetch")); + assert!(!is_known_omtae_tool("Read")); + assert!(!is_known_omtae_tool("unknown")); } #[test] diff --git a/crates/openfang-migrate/src/report.rs b/crates/openfang-migrate/src/report.rs index 93838b4420..97d706af54 100644 --- a/crates/openfang-migrate/src/report.rs +++ b/crates/openfang-migrate/src/report.rs @@ -72,7 +72,7 @@ impl MigrationReport { let mode = if self.dry_run { " (Dry Run)" } else { "" }; out.push_str(&format!( - "# Migration Report: {} -> OpenFang{}\n\n", + "# Migration Report: {} -> OMTAE{}\n\n", self.source, mode )); @@ -121,13 +121,13 @@ impl MigrationReport { // Next steps out.push_str("## Next Steps\n\n"); - out.push_str("1. Review imported agent manifests in `~/.openfang/agents/`\n"); + out.push_str("1. Review imported agent manifests in `~/.omtae/agents/`\n"); out.push_str( - "2. Review `~/.openfang/secrets.env` — verify tokens were migrated correctly\n", + "2. Review `~/.omtae/secrets.env` — verify tokens were migrated correctly\n", ); - out.push_str("3. Set any remaining API keys referenced in `~/.openfang/config.toml`\n"); - out.push_str("4. Start the daemon: `openfang start`\n"); - out.push_str("5. Test your agents: `openfang agent list`\n"); + out.push_str("3. Set any remaining API keys referenced in `~/.omtae/config.toml`\n"); + out.push_str("4. Start the daemon: `omtae start`\n"); + out.push_str("5. Test your agents: `omtae agent list`\n"); out } @@ -163,8 +163,8 @@ impl MigrationReport { if !self.dry_run { println!("\n Next steps:"); - println!(" openfang start"); - println!(" openfang agent list"); + println!(" omtae start"); + println!(" omtae agent list"); } } } @@ -192,7 +192,7 @@ mod tests { imported: vec![MigrateItem { kind: ItemKind::Agent, name: "coder".to_string(), - destination: "~/.openfang/agents/coder/agent.toml".to_string(), + destination: "~/.omtae/agents/coder/agent.toml".to_string(), }], skipped: vec![SkippedItem { kind: ItemKind::Skill, diff --git a/crates/openfang-migrate/tests/provider_json5_agents.rs b/crates/openfang-migrate/tests/provider_json5_agents.rs index 5a6d6ddefa..aa7b56bc5d 100644 --- a/crates/openfang-migrate/tests/provider_json5_agents.rs +++ b/crates/openfang-migrate/tests/provider_json5_agents.rs @@ -1,4 +1,4 @@ -use openfang_migrate::{run_migration, MigrateOptions, MigrateSource}; +use omtae_migrate::{run_migration, MigrateOptions, MigrateSource}; use tempfile::TempDir; fn migrate_with_json5(json5_content: &str) -> (TempDir, TempDir) { diff --git a/crates/openfang-migrate/tests/provider_json5_default_model.rs b/crates/openfang-migrate/tests/provider_json5_default_model.rs index e892e07e98..9c5a8bb49b 100644 --- a/crates/openfang-migrate/tests/provider_json5_default_model.rs +++ b/crates/openfang-migrate/tests/provider_json5_default_model.rs @@ -1,4 +1,4 @@ -use openfang_migrate::{openclaw, MigrateOptions, MigrateSource}; +use omtae_migrate::{openclaw, MigrateOptions, MigrateSource}; use tempfile::TempDir; fn assert_default_model_mapping( diff --git a/crates/openfang-migrate/tests/provider_json5_provider_catalog.rs b/crates/openfang-migrate/tests/provider_json5_provider_catalog.rs index 316c2361cd..c858801122 100644 --- a/crates/openfang-migrate/tests/provider_json5_provider_catalog.rs +++ b/crates/openfang-migrate/tests/provider_json5_provider_catalog.rs @@ -1,4 +1,4 @@ -use openfang_migrate::{run_migration, MigrateOptions, MigrateSource}; +use omtae_migrate::{run_migration, MigrateOptions, MigrateSource}; use tempfile::TempDir; #[test] diff --git a/crates/openfang-migrate/tests/provider_legacy_yaml.rs b/crates/openfang-migrate/tests/provider_legacy_yaml.rs index adb0602875..3400793b9f 100644 --- a/crates/openfang-migrate/tests/provider_legacy_yaml.rs +++ b/crates/openfang-migrate/tests/provider_legacy_yaml.rs @@ -1,7 +1,7 @@ use std::fs; use std::path::Path; -use openfang_migrate::{run_migration, MigrateOptions, MigrateSource}; +use omtae_migrate::{run_migration, MigrateOptions, MigrateSource}; use tempfile::TempDir; fn create_legacy_workspace( diff --git a/crates/openfang-runtime/Cargo.toml b/crates/openfang-runtime/Cargo.toml index aff9a80291..5d61c4dc78 100644 --- a/crates/openfang-runtime/Cargo.toml +++ b/crates/openfang-runtime/Cargo.toml @@ -1,14 +1,14 @@ [package] -name = "openfang-runtime" +name = "omtae-runtime" version.workspace = true edition.workspace = true license.workspace = true -description = "Agent runtime and execution environment for OpenFang" +description = "Agent runtime and execution environment for OMTAE" [dependencies] -openfang-types = { path = "../openfang-types" } -openfang-memory = { path = "../openfang-memory" } -openfang-skills = { path = "../openfang-skills" } +omtae-types = { path = "../omtae-types" } +omtae-memory = { path = "../omtae-memory" } +omtae-skills = { path = "../omtae-skills" } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/openfang-runtime/src/a2a.rs b/crates/openfang-runtime/src/a2a.rs index a11a408005..d897b17ce5 100644 --- a/crates/openfang-runtime/src/a2a.rs +++ b/crates/openfang-runtime/src/a2a.rs @@ -6,10 +6,10 @@ //! This module provides: //! - `AgentCard` — describes an agent's capabilities to external systems //! - `A2aTask` — unit of work exchanged between agents -//! - `build_agent_card` — expose OpenFang agents via A2A +//! - `build_agent_card` — expose OMTAE agents via A2A //! - `A2aClient` — discover and interact with external A2A agents -use openfang_types::agent::AgentManifest; +use omtae_types::agent::AgentManifest; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Mutex; @@ -35,7 +35,7 @@ pub struct AgentCard { pub version: String, /// Agent capabilities. pub capabilities: AgentCapabilities, - /// Skills this agent can perform (A2A skill descriptors, not OpenFang skills). + /// Skills this agent can perform (A2A skill descriptors, not OMTAE skills). pub skills: Vec, /// Supported input content types. #[serde(default)] @@ -57,7 +57,7 @@ pub struct AgentCapabilities { pub state_transition_history: bool, } -/// A2A skill descriptor (not an OpenFang skill — describes a capability). +/// A2A skill descriptor (not an OMTAE skill — describes a capability). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentSkill { /// Unique skill identifier. @@ -319,7 +319,7 @@ impl Default for A2aTaskStore { /// /// Called during kernel boot to populate the list of known external agents. pub async fn discover_external_agents( - agents: &[openfang_types::config::ExternalAgent], + agents: &[omtae_types::config::ExternalAgent], ) -> Vec<(String, AgentCard)> { let client = A2aClient::new(); let mut discovered = Vec::new(); @@ -354,10 +354,10 @@ pub async fn discover_external_agents( } // --------------------------------------------------------------------------- -// A2A Server — expose OpenFang agents via A2A +// A2A Server — expose OMTAE agents via A2A // --------------------------------------------------------------------------- -/// Build an A2A Agent Card from an OpenFang agent manifest. +/// Build an A2A Agent Card from an OMTAE agent manifest. pub fn build_agent_card(manifest: &AgentManifest, base_url: &str) -> AgentCard { let tools: Vec = manifest.capabilities.tools.clone(); @@ -418,7 +418,7 @@ impl A2aClient { let response = self .client .get(&agent_json_url) - .header("User-Agent", "OpenFang/0.1 A2A") + .header("User-Agent", "OMTAE/0.1 A2A") .send() .await .map_err(|e| format!("A2A discovery failed: {e}"))?; @@ -733,7 +733,7 @@ mod tests { #[test] fn test_a2a_config_serde() { - use openfang_types::config::{A2aConfig, ExternalAgent}; + use omtae_types::config::{A2aConfig, ExternalAgent}; let config = A2aConfig { enabled: true, diff --git a/crates/openfang-runtime/src/agent_context.rs b/crates/openfang-runtime/src/agent_context.rs index e43a022436..23b565a11f 100644 --- a/crates/openfang-runtime/src/agent_context.rs +++ b/crates/openfang-runtime/src/agent_context.rs @@ -132,7 +132,7 @@ mod tests { fn fresh_workspace(tag: &str) -> PathBuf { // Unique temp dir per test to avoid cross-test cache pollution. let dir = std::env::temp_dir().join(format!( - "openfang_ctx_{}_{}", + "omtae_ctx_{}_{}", tag, std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/crates/openfang-runtime/src/agent_loop.rs b/crates/openfang-runtime/src/agent_loop.rs index 615fbfdb73..a25d81600a 100644 --- a/crates/openfang-runtime/src/agent_loop.rs +++ b/crates/openfang-runtime/src/agent_loop.rs @@ -4,26 +4,30 @@ //! calling the LLM, executing tool calls, and saving the conversation. use crate::auth_cooldown::{CooldownVerdict, ProviderCooldown}; +use crate::compactor::cap_max_output_tokens; use crate::context_budget::{apply_context_guard, truncate_tool_result_dynamic, ContextBudget}; use crate::context_overflow::{recover_from_overflow, RecoveryStage}; use crate::embedding::EmbeddingDriver; use crate::kernel_handle::KernelHandle; use crate::llm_driver::{CompletionRequest, DriverConfig, LlmDriver, LlmError, StreamEvent}; use crate::llm_errors; -use crate::loop_guard::{LoopGuard, LoopGuardConfig, LoopGuardVerdict}; +use crate::loop_guard::{ + check_planning_loop, check_verification_gate, LoopGuard, LoopGuardConfig, LoopGuardVerdict, + PlanningLoopAction, +}; use crate::mcp::McpConnection; use crate::tool_runner; use crate::web_search::WebToolsContext; -use openfang_memory::session::Session; -use openfang_memory::MemorySubstrate; -use openfang_skills::registry::SkillRegistry; -use openfang_types::agent::{AgentManifest, FallbackModel}; -use openfang_types::error::{OpenFangError, OpenFangResult}; -use openfang_types::memory::{Memory, MemoryFilter, MemorySource}; -use openfang_types::message::{ +use omtae_memory::session::Session; +use omtae_memory::MemorySubstrate; +use omtae_skills::registry::SkillRegistry; +use omtae_types::agent::{AgentManifest, FallbackModel}; +use omtae_types::error::{OMTAEError, OMTAEResult}; +use omtae_types::memory::{Memory, MemoryFilter, MemorySource}; +use omtae_types::message::{ ContentBlock, Message, MessageContent, Role, StopReason, TokenUsage, }; -use openfang_types::tool::{ToolCall, ToolDefinition}; +use omtae_types::tool::{ToolCall, ToolDefinition}; use std::collections::HashMap; use std::path::Path; use std::sync::Arc; @@ -37,6 +41,25 @@ const MAX_ITERATIONS: u32 = 50; /// Maximum retries for rate-limited or overloaded API calls. const MAX_RETRIES: u32 = 3; +fn is_context_overflow_error(err: &OMTAEError) -> Option { + match err { + OMTAEError::LlmDriver(msg) => { + let msg_lower = msg.to_lowercase(); + if msg_lower.contains("context too long") + || msg_lower.contains("context length") + || msg_lower.contains("too long") + || msg_lower.contains("context window") + { + Some(msg.clone()) + } else { + None + } + } + _ => None, + } +} + + /// Base delay for exponential backoff (milliseconds). const BASE_RETRY_DELAY_MS: u64 = 1000; @@ -87,7 +110,7 @@ const MAX_CONTINUATIONS: u32 = 5; /// Default maximum message history size before auto-trimming to prevent context overflow. /// Per-agent overrides come from `AgentManifest::max_history_messages` (issue #871). #[allow(dead_code)] -const MAX_HISTORY_MESSAGES: usize = openfang_types::agent::DEFAULT_MAX_HISTORY_MESSAGES; +const MAX_HISTORY_MESSAGES: usize = omtae_types::agent::DEFAULT_MAX_HISTORY_MESSAGES; /// Detect when the LLM claims to have performed an action (sent, posted, emailed) /// without actually calling any tools. Prevents hallucinated completions. @@ -110,6 +133,149 @@ fn phantom_action_detected(text: &str) -> bool { has_action && has_channel } +const RESEARCH_INTEGRITY_DISCLAIMER: &str = "\n\n---\n⚠️ **Verification notice:** Some website or business details above were not confirmed by web_search/web_fetch tool results. Treat listed names, addresses, and URLs as unverified."; + +/// Extract https URLs from free text (tool results or agent replies). +fn extract_https_urls(text: &str) -> Vec { + let mut urls = Vec::new(); + for token in text.split_whitespace() { + if let Some(start) = token.find("https://") { + let raw = &token[start..]; + let end = raw + .char_indices() + .find(|(_, c)| !c.is_ascii_alphanumeric() && *c != '/' && *c != ':' && *c != '.' && *c != '-' && *c != '_' && *c != '?' && *c != '=' && *c != '&' && *c != '%') + .map(|(i, _)| i) + .unwrap_or(raw.len()); + let url = raw[..end].trim_end_matches(|c: char| c == ')' || c == ']' || c == ',' || c == '.'); + if !url.is_empty() && !urls.iter().any(|u| u == url) { + urls.push(url.to_string()); + } + } + } + urls +} + +/// Collect https URLs from ToolResult blocks in the conversation history. +fn collect_tool_result_urls(messages: &[Message]) -> Vec { + let mut urls = Vec::new(); + for msg in messages { + let blocks = match &msg.content { + MessageContent::Blocks(blocks) => blocks, + MessageContent::Text(text) => { + for url in extract_https_urls(text) { + if !urls.contains(&url) { + urls.push(url); + } + } + continue; + } + }; + for block in blocks { + if let ContentBlock::ToolResult { content, .. } = block { + for url in extract_https_urls(content) { + if !urls.contains(&url) { + urls.push(url); + } + } + } + } + } + urls +} + +fn url_cited_in_tool_results(url: &str, tool_urls: &[String]) -> bool { + let normalized = url.trim_end_matches('/'); + tool_urls.iter().any(|t| { + let t_norm = t.trim_end_matches('/'); + t_norm == normalized || t_norm.starts_with(normalized) || normalized.starts_with(t_norm) + }) +} + +/// Detect unverified website lines or high-confidence lists without tool backing. +fn research_integrity_issue( + text: &str, + tool_urls: &[String], + any_tools_executed: bool, + has_web_tools: bool, +) -> bool { + if !has_web_tools { + return false; + } + + for line in text.lines() { + if !line.to_lowercase().contains("website:") { + continue; + } + let line_urls = extract_https_urls(line); + if line_urls.is_empty() { + return true; + } + if tool_urls.is_empty() { + return true; + } + if !line_urls.iter().all(|u| url_cited_in_tool_results(u, tool_urls)) { + return true; + } + } + + let lower = text.to_lowercase(); + let claims_high_confidence = lower.contains("confidence level: high") + || lower.contains("confidence: high") + || lower.contains("confidence level:** high"); + if claims_high_confidence && tool_urls.is_empty() && !text.contains("https://") { + return true; + } + + if !any_tools_executed && looks_like_enumerated_research_list(text) { + return true; + } + + false +} + +fn looks_like_enumerated_research_list(text: &str) -> bool { + let lower = text.to_lowercase(); + if lower.contains("top ") && (lower.contains(" in ") || lower.contains(" list")) { + return true; + } + let numbered = text + .lines() + .filter(|line| { + let trimmed = line.trim_start(); + trimmed + .chars() + .next() + .is_some_and(|c| c.is_ascii_digit()) + && (trimmed.contains(". ") || trimmed.contains(") ")) + }) + .count(); + numbered >= 3 +} + +fn agent_has_web_tools(available_tools: &[ToolDefinition]) -> bool { + available_tools + .iter() + .any(|t| t.name == "web_search" || t.name == "web_fetch") +} + +fn apply_research_integrity_check( + text: String, + messages: &[Message], + any_tools_executed: bool, + available_tools: &[ToolDefinition], +) -> String { + if !agent_has_web_tools(available_tools) { + return text; + } + let tool_urls = collect_tool_result_urls(messages); + if research_integrity_issue(&text, &tool_urls, any_tools_executed, true) { + warn!("Research integrity check triggered — appending verification disclaimer"); + format!("{text}{RESEARCH_INTEGRITY_DISCLAIMER}") + } else { + text + } +} + /// Returns true when the agent response text indicates an intentional silent completion. /// Matches `NO_REPLY` (exact) and `[SILENT]` (case-insensitive). fn is_silent_token(text: &str) -> bool { @@ -119,7 +285,7 @@ fn is_silent_token(text: &str) -> bool { /// Extra guidance injected after failed tool calls to prevent fabricated follow-up actions. const TOOL_ERROR_GUIDANCE: &str = - "[System: One or more tool calls failed. Failed tools did not produce usable data. Do NOT invent missing results, cite nonexistent search results, or pretend failed tools succeeded. If your next steps depend on a failed tool, either retry with a materially different approach or explain the failure to the user and stop. Do not write files, store memory, or take downstream actions based on failed tool outputs.]"; + "[System: One or more tool calls failed. Failed tools did not produce usable data. Do NOT invent missing results, cite nonexistent search results, or pretend failed tools succeeded. For agent_send: if the result is not exactly \"agent_send OK\" with specialist text, report delegation FAILED with the verbatim error — never claim agents \"heard\" each other. If your next steps depend on a failed tool, either retry with a materially different approach or explain the failure to the user and stop. Do not write files, store memory, or take downstream actions based on failed tool outputs.]"; fn append_tool_error_guidance(tool_result_blocks: &mut Vec) { let has_tool_error = tool_result_blocks @@ -258,7 +424,7 @@ pub struct AgentLoopResult { /// True when the agent intentionally chose not to reply (NO_REPLY token or [[silent]]). pub silent: bool, /// Reply directives extracted from the agent's response. - pub directives: openfang_types::message::ReplyDirectives, + pub directives: omtae_types::message::ReplyDirectives, } /// Build the user-turn message, combining text with any image content blocks. @@ -287,7 +453,7 @@ fn build_user_turn_message(user_message: &str, blocks: Option> /// Run the agent execution loop for a single user message. /// -/// This is the core of OpenFang: it loads session context, recalls memories, +/// This is the core of OMTAE: it loads session context, recalls memories, /// runs the LLM in a tool-use loop, and saves the updated session. #[allow(clippy::too_many_arguments)] pub async fn run_agent_loop( @@ -307,12 +473,12 @@ pub async fn run_agent_loop( on_phase: Option<&PhaseCallback>, media_engine: Option<&crate::media_understanding::MediaEngine>, tts_engine: Option<&crate::tts::TtsEngine>, - docker_config: Option<&openfang_types::config::DockerSandboxConfig>, + docker_config: Option<&omtae_types::config::DockerSandboxConfig>, hooks: Option<&crate::hooks::HookRegistry>, context_window_tokens: Option, process_manager: Option<&crate::process_manager::ProcessManager>, user_content_blocks: Option>, -) -> OpenFangResult { +) -> OMTAEResult { info!(agent = %manifest.name, "Starting agent loop"); // Extract hand-allowed env vars from manifest metadata (set by kernel for hand settings) @@ -375,7 +541,7 @@ pub async fn run_agent_loop( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: agent_id_str.as_str(), - event: openfang_types::agent::HookEvent::BeforePromptBuild, + event: omtae_types::agent::HookEvent::BeforePromptBuild, data: serde_json::json!({ "system_prompt": &manifest.model.system_prompt, "user_message": user_message, @@ -478,8 +644,11 @@ pub async fn run_agent_loop( // assistant turn at position 0, which strict providers (e.g. Gemini) // reject with INVALID_ARGUMENT on function-call turns. messages = crate::session_repair::ensure_starts_with_user(messages); + messages = crate::session_repair::ensure_has_user_text(messages); } + messages = crate::session_repair::ensure_has_user_text(messages); + // Use autonomous config max_iterations if set, else default let max_iterations = manifest .autonomous @@ -493,10 +662,20 @@ pub async fn run_agent_loop( if max_iterations > cfg.global_circuit_breaker { cfg.global_circuit_breaker = max_iterations * 3; } + // Meta-agent: cap delegation churn (agent_send × nested loops → stuck "Generating…") + if manifest.name == "orchestrator" { + cfg.global_circuit_breaker = cfg.global_circuit_breaker.min(15); + cfg.max_agent_send_per_loop = Some(5); + cfg.warn_threshold = 2; + cfg.block_threshold = 4; + cfg.ping_pong_min_repeats = 2; + } cfg }; let mut loop_guard = LoopGuard::new(loop_guard_config); let mut consecutive_max_tokens: u32 = 0; + let mut planning_reprompts: u32 = 0; + let mut verification_reprompts: u32 = 0; // Build context budget from model's actual context window (or fallback to default) let ctx_window = context_window_tokens.unwrap_or(DEFAULT_CONTEXT_WINDOW); @@ -523,6 +702,7 @@ pub async fn run_agent_loop( // Context guard: compact oversized tool results before LLM call apply_context_guard(&mut messages, &context_budget, available_tools); + messages = crate::session_repair::ensure_has_user_text(messages); // Strip provider prefix: "openrouter/google/gemini-2.5-flash" → "google/gemini-2.5-flash" let api_model = strip_provider_prefix(&manifest.model.model, &manifest.model.provider); @@ -531,7 +711,7 @@ pub async fn run_agent_loop( model: api_model, messages: messages.clone(), tools: available_tools.to_vec(), - max_tokens: manifest.model.max_tokens, + max_tokens: cap_max_output_tokens(manifest.model.max_tokens, context_window_tokens), temperature: manifest.model.temperature, system: Some(system_prompt.clone()), thinking: None, @@ -550,14 +730,65 @@ pub async fn run_agent_loop( // Call LLM with retry, error classification, and circuit breaker let provider_name = manifest.model.provider.as_str(); - let mut response = call_with_retry( - &*driver, - request, - Some(provider_name), - None, - &manifest.fallback_models, - ) - .await?; + let mut response = { + let mut active_request = request.clone(); + let mut overflow_attempts = 0; + loop { + match call_with_retry( + &*driver, + active_request.clone(), + Some(provider_name), + None, + &manifest.fallback_models, + ) + .await + { + Ok(resp) => break resp, + Err(e) => { + if let Some(err_msg) = is_context_overflow_error(&e) { + if overflow_attempts < 3 { + overflow_attempts += 1; + warn!( + overflow_attempts, + "Context overflow detected in agent loop. Attempting recovery..." + ); + // 1. Attempt to extract a safe limit using the error message + let cap = crate::drivers::openai::extract_max_tokens_limit(&err_msg); + // 2. Update max_tokens + if let Some(safe_cap) = cap { + warn!(old = active_request.max_tokens, new = safe_cap, "Reducing request max_tokens"); + active_request.max_tokens = safe_cap; + } else { + // Otherwise reduce max_tokens by 25% + let new_max = (active_request.max_tokens * 3) / 4; + warn!(old = active_request.max_tokens, new = new_max, "Reducing request max_tokens by 25%"); + active_request.max_tokens = new_max.max(256); + } + // 3. Trim the messages using recover_from_overflow. + // We simulate a smaller context window to force truncation! + // Reduce the effective context window by 15% on each attempt. + let reduced_ctx = ((ctx_window as f64) * (1.0 - 0.15 * (overflow_attempts as f64))) as usize; + warn!(reduced_ctx, "Running recover_from_overflow with reduced context window"); + let recovery = recover_from_overflow( + &mut messages, + &system_prompt, + available_tools, + reduced_ctx, + ); + if recovery != RecoveryStage::None { + messages = crate::session_repair::validate_and_repair(&messages); + messages = crate::session_repair::ensure_starts_with_user(messages); + messages = crate::session_repair::ensure_has_user_text(messages); + } + active_request.messages = messages.clone(); + continue; + } + } + return Err(e); + } + } + } + }; total_usage.input_tokens += response.usage.input_tokens; total_usage.output_tokens += response.usage.output_tokens; @@ -611,14 +842,14 @@ pub async fn run_agent_loop( memory .save_session_async(session) .await - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; return Ok(AgentLoopResult { response: String::new(), total_usage, iterations: iteration + 1, cost_usd: None, silent: true, - directives: openfang_types::message::ReplyDirectives { + directives: omtae_types::message::ReplyDirectives { reply_to: parsed_directives.reply_to, current_thread: parsed_directives.current_thread, silent: true, @@ -703,7 +934,79 @@ pub async fn run_agent_loop( text }; - final_response = text.clone(); + // Verification gate: factual claims without tool evidence (ECC-inspired). + if let Some(action) = check_verification_gate( + &messages, + &text, + any_tools_executed, + verification_reprompts, + ) { + match action { + PlanningLoopAction::Reprompt(nudge) => { + warn!(agent = %manifest.name, verification_reprompts, "Verification gate — TOOL_REQUIRED reprompt"); + verification_reprompts += 1; + messages.push(Message::assistant(text)); + messages.push(Message::user(nudge.to_string())); + continue; + } + PlanningLoopAction::ForceEnd(response) => { + warn!(agent = %manifest.name, "Verification gate exhausted — ending turn"); + final_response = response; + session.messages.push(Message::assistant(final_response.clone())); + memory + .save_session_async(session) + .await + .map_err(|e| OMTAEError::Memory(e.to_string()))?; + return Ok(AgentLoopResult { + response: final_response, + total_usage, + iterations: iteration + 1, + cost_usd: None, + silent: false, + directives: omtae_types::message::ReplyDirectives::default(), + }); + } + } + } + + // Planning loop guard: repeated planning prose without tool calls. + if let Some(action) = + check_planning_loop(&messages, &text, any_tools_executed, planning_reprompts) + { + match action { + PlanningLoopAction::Reprompt(nudge) => { + warn!(agent = %manifest.name, planning_reprompts, "Planning loop detected — re-prompting for tool use"); + planning_reprompts += 1; + messages.push(Message::assistant(text)); + messages.push(Message::user(nudge.to_string())); + continue; + } + PlanningLoopAction::ForceEnd(response) => { + warn!(agent = %manifest.name, "Planning loop exhausted — ending turn"); + final_response = response; + session.messages.push(Message::assistant(final_response.clone())); + memory + .save_session_async(session) + .await + .map_err(|e| OMTAEError::Memory(e.to_string()))?; + return Ok(AgentLoopResult { + response: final_response, + total_usage, + iterations: iteration + 1, + cost_usd: None, + silent: false, + directives: omtae_types::message::ReplyDirectives::default(), + }); + } + } + } + + final_response = apply_research_integrity_check( + text, + &messages, + any_tools_executed, + available_tools, + ); // Issue #1098: persist Thinking blocks alongside the text so // reasoning models retain state across turns. When the // response carries any Thinking content (Anthropic extended @@ -712,7 +1015,7 @@ pub async fn run_agent_loop( // full content blocks; otherwise fall back to the legacy // Text shape so existing sessions/snapshots stay readable. let assistant_msg = - build_assistant_message_preserving_thinking(&response.content, &text); + build_assistant_message_preserving_thinking(&response.content, &final_response); session.messages.push(assistant_msg); // Prune NO_REPLY heartbeat turns to save context budget @@ -722,7 +1025,7 @@ pub async fn run_agent_loop( memory .save_session_async(session) .await - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; // Remember this interaction (with embedding if available) let interaction_text = format!( @@ -785,7 +1088,7 @@ pub async fn run_agent_loop( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: agent_id_str.as_str(), - event: openfang_types::agent::HookEvent::AgentLoopEnd, + event: omtae_types::agent::HookEvent::AgentLoopEnd, data: serde_json::json!({ "iterations": iteration + 1, "response_length": final_response.len(), @@ -857,7 +1160,7 @@ pub async fn run_agent_loop( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: agent_id_str.as_str(), - event: openfang_types::agent::HookEvent::AgentLoopEnd, + event: omtae_types::agent::HookEvent::AgentLoopEnd, data: serde_json::json!({ "reason": "circuit_break", "error": msg.as_str(), @@ -865,7 +1168,7 @@ pub async fn run_agent_loop( }; let _ = hook_reg.fire(&ctx); } - return Err(OpenFangError::Internal(msg.clone())); + return Err(OMTAEError::Internal(msg.clone())); } LoopGuardVerdict::Block(msg) => { warn!(tool = %tool_call.name, "Tool call blocked by loop guard"); @@ -900,7 +1203,7 @@ pub async fn run_agent_loop( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: &caller_id_str, - event: openfang_types::agent::HookEvent::BeforeToolCall, + event: omtae_types::agent::HookEvent::BeforeToolCall, data: serde_json::json!({ "tool_name": &tool_call.name, "input": &tool_call.input, @@ -956,7 +1259,7 @@ pub async fn run_agent_loop( Ok(result) => result, Err(_) => { warn!(tool = %tool_call.name, "Tool execution timed out after {}s", timeout_secs); - openfang_types::tool::ToolResult { + omtae_types::tool::ToolResult { tool_use_id: tool_call.id.clone(), content: format!( "Tool '{}' timed out after {}s.", @@ -975,7 +1278,7 @@ pub async fn run_agent_loop( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: caller_id_str.as_str(), - event: openfang_types::agent::HookEvent::AfterToolCall, + event: omtae_types::agent::HookEvent::AfterToolCall, data: serde_json::json!({ "tool_name": &tool_call.name, "result": &result.content, @@ -1090,7 +1393,7 @@ pub async fn run_agent_loop( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: agent_id_str.as_str(), - event: openfang_types::agent::HookEvent::AgentLoopEnd, + event: omtae_types::agent::HookEvent::AgentLoopEnd, data: serde_json::json!({ "iterations": iteration + 1, "reason": "max_continuations", @@ -1133,7 +1436,7 @@ pub async fn run_agent_loop( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: agent_id_str.as_str(), - event: openfang_types::agent::HookEvent::AgentLoopEnd, + event: omtae_types::agent::HookEvent::AgentLoopEnd, data: serde_json::json!({ "reason": "max_iterations_exceeded", "iterations": max_iterations, @@ -1142,7 +1445,7 @@ pub async fn run_agent_loop( let _ = hook_reg.fire(&ctx); } - Err(OpenFangError::MaxIterationsExceeded(max_iterations)) + Err(OMTAEError::MaxIterationsExceeded(max_iterations)) } /// Call an LLM driver with automatic retry on rate-limit and overload errors. @@ -1158,7 +1461,7 @@ async fn call_with_retry( provider: Option<&str>, cooldown: Option<&ProviderCooldown>, fallback_models: &[FallbackModel], -) -> OpenFangResult { +) -> OMTAEResult { // Check circuit breaker before calling if let (Some(provider), Some(cooldown)) = (provider, cooldown) { match cooldown.check(provider) { @@ -1166,7 +1469,7 @@ async fn call_with_retry( reason, retry_after_secs, } => { - return Err(OpenFangError::LlmDriver(format!( + return Err(OMTAEError::LlmDriver(format!( "Provider '{provider}' is in cooldown ({reason}). Retry in {retry_after_secs}s." ))); } @@ -1193,7 +1496,7 @@ async fn call_with_retry( if let (Some(provider), Some(cooldown)) = (provider, cooldown) { cooldown.record_failure(provider, false); } - return Err(OpenFangError::LlmDriver(format!( + return Err(OMTAEError::LlmDriver(format!( "Rate limited after {} retries", MAX_RETRIES ))); @@ -1212,7 +1515,7 @@ async fn call_with_retry( if let (Some(provider), Some(cooldown)) = (provider, cooldown) { cooldown.record_failure(provider, false); } - return Err(OpenFangError::LlmDriver(format!( + return Err(OMTAEError::LlmDriver(format!( "Model overloaded after {} retries", MAX_RETRIES ))); @@ -1320,12 +1623,12 @@ async fn call_with_retry( } else { classified.sanitized_message }; - return Err(OpenFangError::LlmDriver(user_msg)); + return Err(OMTAEError::LlmDriver(user_msg)); } } } - Err(OpenFangError::LlmDriver( + Err(OMTAEError::LlmDriver( last_error.unwrap_or_else(|| "Unknown error".to_string()), )) } @@ -1343,7 +1646,7 @@ async fn stream_with_retry( provider: Option<&str>, cooldown: Option<&ProviderCooldown>, fallback_models: &[FallbackModel], -) -> OpenFangResult { +) -> OMTAEResult { // Check circuit breaker before calling if let (Some(provider), Some(cooldown)) = (provider, cooldown) { match cooldown.check(provider) { @@ -1351,7 +1654,7 @@ async fn stream_with_retry( reason, retry_after_secs, } => { - return Err(OpenFangError::LlmDriver(format!( + return Err(OMTAEError::LlmDriver(format!( "Provider '{provider}' is in cooldown ({reason}). Retry in {retry_after_secs}s." ))); } @@ -1380,7 +1683,7 @@ async fn stream_with_retry( if let (Some(provider), Some(cooldown)) = (provider, cooldown) { cooldown.record_failure(provider, false); } - return Err(OpenFangError::LlmDriver(format!( + return Err(OMTAEError::LlmDriver(format!( "Rate limited after {} retries", MAX_RETRIES ))); @@ -1399,7 +1702,7 @@ async fn stream_with_retry( if let (Some(provider), Some(cooldown)) = (provider, cooldown) { cooldown.record_failure(provider, false); } - return Err(OpenFangError::LlmDriver(format!( + return Err(OMTAEError::LlmDriver(format!( "Model overloaded after {} retries", MAX_RETRIES ))); @@ -1501,12 +1804,12 @@ async fn stream_with_retry( } else { classified.sanitized_message }; - return Err(OpenFangError::LlmDriver(user_msg)); + return Err(OMTAEError::LlmDriver(user_msg)); } } } - Err(OpenFangError::LlmDriver( + Err(OMTAEError::LlmDriver( last_error.unwrap_or_else(|| "Unknown error".to_string()), )) } @@ -1535,12 +1838,12 @@ pub async fn run_agent_loop_streaming( on_phase: Option<&PhaseCallback>, media_engine: Option<&crate::media_understanding::MediaEngine>, tts_engine: Option<&crate::tts::TtsEngine>, - docker_config: Option<&openfang_types::config::DockerSandboxConfig>, + docker_config: Option<&omtae_types::config::DockerSandboxConfig>, hooks: Option<&crate::hooks::HookRegistry>, context_window_tokens: Option, process_manager: Option<&crate::process_manager::ProcessManager>, user_content_blocks: Option>, -) -> OpenFangResult { +) -> OMTAEResult { info!(agent = %manifest.name, "Starting streaming agent loop"); // Extract hand-allowed env vars from manifest metadata (set by kernel for hand settings) @@ -1603,7 +1906,7 @@ pub async fn run_agent_loop_streaming( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: agent_id_str.as_str(), - event: openfang_types::agent::HookEvent::BeforePromptBuild, + event: omtae_types::agent::HookEvent::BeforePromptBuild, data: serde_json::json!({ "system_prompt": &manifest.model.system_prompt, "user_message": user_message, @@ -1697,8 +2000,11 @@ pub async fn run_agent_loop_streaming( // assistant turn at position 0, which strict providers (e.g. Gemini) // reject with INVALID_ARGUMENT on function-call turns. messages = crate::session_repair::ensure_starts_with_user(messages); + messages = crate::session_repair::ensure_has_user_text(messages); } + messages = crate::session_repair::ensure_has_user_text(messages); + // Use autonomous config max_iterations if set, else default let max_iterations = manifest .autonomous @@ -1712,10 +2018,19 @@ pub async fn run_agent_loop_streaming( if max_iterations > cfg.global_circuit_breaker { cfg.global_circuit_breaker = max_iterations * 3; } + if manifest.name == "orchestrator" { + cfg.global_circuit_breaker = cfg.global_circuit_breaker.min(15); + cfg.max_agent_send_per_loop = Some(5); + cfg.warn_threshold = 2; + cfg.block_threshold = 4; + cfg.ping_pong_min_repeats = 2; + } cfg }; let mut loop_guard = LoopGuard::new(loop_guard_config); let mut consecutive_max_tokens: u32 = 0; + let mut planning_reprompts: u32 = 0; + let mut verification_reprompts: u32 = 0; // Build context budget from model's actual context window (or fallback to default) let ctx_window = context_window_tokens.unwrap_or(DEFAULT_CONTEXT_WINDOW); @@ -1756,10 +2071,12 @@ pub async fn run_agent_loop_streaming( messages = crate::session_repair::validate_and_repair(&messages); // Ensure history starts with a user turn after overflow recovery. messages = crate::session_repair::ensure_starts_with_user(messages); + messages = crate::session_repair::ensure_has_user_text(messages); } // Context guard: compact oversized tool results before LLM call apply_context_guard(&mut messages, &context_budget, available_tools); + messages = crate::session_repair::ensure_has_user_text(messages); // Strip provider prefix: "openrouter/google/gemini-2.5-flash" → "google/gemini-2.5-flash" let api_model = strip_provider_prefix(&manifest.model.model, &manifest.model.provider); @@ -1768,7 +2085,7 @@ pub async fn run_agent_loop_streaming( model: api_model, messages: messages.clone(), tools: available_tools.to_vec(), - max_tokens: manifest.model.max_tokens, + max_tokens: cap_max_output_tokens(manifest.model.max_tokens, context_window_tokens), temperature: manifest.model.temperature, system: Some(system_prompt.clone()), thinking: None, @@ -1793,15 +2110,66 @@ pub async fn run_agent_loop_streaming( // Stream LLM call with retry, error classification, and circuit breaker let provider_name = manifest.model.provider.as_str(); - let mut response = stream_with_retry( - &*driver, - request, - stream_tx.clone(), - Some(provider_name), - None, - &manifest.fallback_models, - ) - .await?; + let mut response = { + let mut active_request = request.clone(); + let mut overflow_attempts = 0; + loop { + match stream_with_retry( + &*driver, + active_request.clone(), + stream_tx.clone(), + Some(provider_name), + None, + &manifest.fallback_models, + ) + .await + { + Ok(resp) => break resp, + Err(e) => { + if let Some(err_msg) = is_context_overflow_error(&e) { + if overflow_attempts < 3 { + overflow_attempts += 1; + warn!( + overflow_attempts, + "Context overflow detected in agent loop (stream). Attempting recovery..." + ); + // 1. Attempt to extract a safe limit using the error message + let cap = crate::drivers::openai::extract_max_tokens_limit(&err_msg); + // 2. Update max_tokens + if let Some(safe_cap) = cap { + warn!(old = active_request.max_tokens, new = safe_cap, "Reducing request max_tokens"); + active_request.max_tokens = safe_cap; + } else { + // Otherwise reduce max_tokens by 25% + let new_max = (active_request.max_tokens * 3) / 4; + warn!(old = active_request.max_tokens, new = new_max, "Reducing request max_tokens by 25%"); + active_request.max_tokens = new_max.max(256); + } + // 3. Trim the messages using recover_from_overflow. + // We simulate a smaller context window to force truncation! + // Reduce the effective context window by 15% on each attempt. + let reduced_ctx = ((ctx_window as f64) * (1.0 - 0.15 * (overflow_attempts as f64))) as usize; + warn!(reduced_ctx, "Running recover_from_overflow with reduced context window"); + let recovery = recover_from_overflow( + &mut messages, + &system_prompt, + available_tools, + reduced_ctx, + ); + if recovery != RecoveryStage::None { + messages = crate::session_repair::validate_and_repair(&messages); + messages = crate::session_repair::ensure_starts_with_user(messages); + messages = crate::session_repair::ensure_has_user_text(messages); + } + active_request.messages = messages.clone(); + continue; + } + } + return Err(e); + } + } + } + }; total_usage.input_tokens += response.usage.input_tokens; total_usage.output_tokens += response.usage.output_tokens; @@ -1852,14 +2220,14 @@ pub async fn run_agent_loop_streaming( memory .save_session_async(session) .await - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; return Ok(AgentLoopResult { response: String::new(), total_usage, iterations: iteration + 1, cost_usd: None, silent: true, - directives: openfang_types::message::ReplyDirectives { + directives: omtae_types::message::ReplyDirectives { reply_to: parsed_directives_s.reply_to, current_thread: parsed_directives_s.current_thread, silent: true, @@ -1923,13 +2291,86 @@ pub async fn run_agent_loop_streaming( } else { text }; - final_response = text.clone(); + + // Verification gate (streaming path). + if let Some(action) = check_verification_gate( + &messages, + &text, + any_tools_executed, + verification_reprompts, + ) { + match action { + PlanningLoopAction::Reprompt(nudge) => { + warn!(agent = %manifest.name, verification_reprompts, "Verification gate (streaming) — TOOL_REQUIRED reprompt"); + verification_reprompts += 1; + messages.push(Message::assistant(text)); + messages.push(Message::user(nudge.to_string())); + continue; + } + PlanningLoopAction::ForceEnd(response) => { + warn!(agent = %manifest.name, "Verification gate exhausted (streaming) — ending turn"); + final_response = response; + session.messages.push(Message::assistant(final_response.clone())); + memory + .save_session_async(session) + .await + .map_err(|e| OMTAEError::Memory(e.to_string()))?; + return Ok(AgentLoopResult { + response: final_response, + total_usage, + iterations: iteration + 1, + cost_usd: None, + silent: false, + directives: omtae_types::message::ReplyDirectives::default(), + }); + } + } + } + + // Planning loop guard (streaming path). + if let Some(action) = + check_planning_loop(&messages, &text, any_tools_executed, planning_reprompts) + { + match action { + PlanningLoopAction::Reprompt(nudge) => { + warn!(agent = %manifest.name, planning_reprompts, "Planning loop detected (streaming) — re-prompting for tool use"); + planning_reprompts += 1; + messages.push(Message::assistant(text)); + messages.push(Message::user(nudge.to_string())); + continue; + } + PlanningLoopAction::ForceEnd(response) => { + warn!(agent = %manifest.name, "Planning loop exhausted (streaming) — ending turn"); + final_response = response; + session.messages.push(Message::assistant(final_response.clone())); + memory + .save_session_async(session) + .await + .map_err(|e| OMTAEError::Memory(e.to_string()))?; + return Ok(AgentLoopResult { + response: final_response, + total_usage, + iterations: iteration + 1, + cost_usd: None, + silent: false, + directives: omtae_types::message::ReplyDirectives::default(), + }); + } + } + } + + final_response = apply_research_integrity_check( + text, + &messages, + any_tools_executed, + available_tools, + ); // Issue #1098: preserve Thinking blocks (with Anthropic // signatures / Gemini thought signatures / inline-think / // reasoning_content) on the persisted assistant turn. See // build_assistant_message_preserving_thinking for details. let assistant_msg = - build_assistant_message_preserving_thinking(&response.content, &text); + build_assistant_message_preserving_thinking(&response.content, &final_response); session.messages.push(assistant_msg); // Prune NO_REPLY heartbeat turns to save context budget @@ -1938,7 +2379,7 @@ pub async fn run_agent_loop_streaming( memory .save_session_async(session) .await - .map_err(|e| OpenFangError::Memory(e.to_string()))?; + .map_err(|e| OMTAEError::Memory(e.to_string()))?; // Remember this interaction (with embedding if available) let interaction_text = format!( @@ -2001,7 +2442,7 @@ pub async fn run_agent_loop_streaming( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: agent_id_str.as_str(), - event: openfang_types::agent::HookEvent::AgentLoopEnd, + event: omtae_types::agent::HookEvent::AgentLoopEnd, data: serde_json::json!({ "iterations": iteration + 1, "response_length": final_response.len(), @@ -2066,7 +2507,7 @@ pub async fn run_agent_loop_streaming( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: agent_id_str.as_str(), - event: openfang_types::agent::HookEvent::AgentLoopEnd, + event: omtae_types::agent::HookEvent::AgentLoopEnd, data: serde_json::json!({ "reason": "circuit_break", "error": msg.as_str(), @@ -2074,7 +2515,7 @@ pub async fn run_agent_loop_streaming( }; let _ = hook_reg.fire(&ctx); } - return Err(OpenFangError::Internal(msg.clone())); + return Err(OMTAEError::Internal(msg.clone())); } LoopGuardVerdict::Block(msg) => { warn!(tool = %tool_call.name, "Tool call blocked by loop guard (streaming)"); @@ -2109,7 +2550,7 @@ pub async fn run_agent_loop_streaming( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: &caller_id_str, - event: openfang_types::agent::HookEvent::BeforeToolCall, + event: omtae_types::agent::HookEvent::BeforeToolCall, data: serde_json::json!({ "tool_name": &tool_call.name, "input": &tool_call.input, @@ -2165,7 +2606,7 @@ pub async fn run_agent_loop_streaming( Ok(result) => result, Err(_) => { warn!(tool = %tool_call.name, "Tool execution timed out after {}s (streaming)", timeout_secs); - openfang_types::tool::ToolResult { + omtae_types::tool::ToolResult { tool_use_id: tool_call.id.clone(), content: format!( "Tool '{}' timed out after {}s.", @@ -2184,7 +2625,7 @@ pub async fn run_agent_loop_streaming( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: caller_id_str.as_str(), - event: openfang_types::agent::HookEvent::AfterToolCall, + event: omtae_types::agent::HookEvent::AfterToolCall, data: serde_json::json!({ "tool_name": &tool_call.name, "result": &result.content, @@ -2205,7 +2646,12 @@ pub async fn run_agent_loop_streaming( }; // Notify client of tool execution result (detect dead consumer) - let preview: String = final_content.chars().take(300).collect(); + let preview_limit = if tool_call.name == "agent_send" { + 16_000 + } else { + 300 + }; + let preview: String = final_content.chars().take(preview_limit).collect(); if stream_tx .send(StreamEvent::ToolExecutionResult { id: tool_call.id.clone(), @@ -2311,7 +2757,7 @@ pub async fn run_agent_loop_streaming( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: agent_id_str.as_str(), - event: openfang_types::agent::HookEvent::AgentLoopEnd, + event: omtae_types::agent::HookEvent::AgentLoopEnd, data: serde_json::json!({ "iterations": iteration + 1, "reason": "max_continuations", @@ -2352,7 +2798,7 @@ pub async fn run_agent_loop_streaming( let ctx = crate::hooks::HookContext { agent_name: &manifest.name, agent_id: agent_id_str.as_str(), - event: openfang_types::agent::HookEvent::AgentLoopEnd, + event: omtae_types::agent::HookEvent::AgentLoopEnd, data: serde_json::json!({ "reason": "max_iterations_exceeded", "iterations": max_iterations, @@ -2361,7 +2807,7 @@ pub async fn run_agent_loop_streaming( let _ = hook_reg.fire(&ctx); } - Err(OpenFangError::MaxIterationsExceeded(max_iterations)) + Err(OMTAEError::MaxIterationsExceeded(max_iterations)) } /// Recover tool calls that LLMs output as plain text instead of the proper @@ -2950,15 +3396,164 @@ fn recover_text_tool_calls(text: &str, available_tools: &[ToolDefinition]) -> Ve } } + // Pattern 15: Pseudo-agent JSON (vLLM/Qwen: `{"name":"researcher","arguments":{"query":"..."}}`) + // Runs even when other patterns matched nothing useful — scan always if agent_send is granted. + if tool_names.contains(&"agent_send") { + let mut scan_from = 0; + while let Some(brace_start) = text[scan_from..].find('{') { + let abs_brace = scan_from + brace_start; + if let Some((tool_name, input)) = + try_parse_bare_json_tool_call(&text[abs_brace..], &tool_names) + { + if tool_name == "agent_send" + && !calls + .iter() + .any(|c| c.name == "agent_send" && c.input == input) + { + info!( + "Recovered agent_send from pseudo-agent or bare JSON delegation" + ); + calls.push(ToolCall { + id: format!("recovered_{}", uuid::Uuid::new_v4()), + name: tool_name, + input, + }); + } + } + scan_from = abs_brace + 1; + } + } + calls } +/// Bundled agent names that vLLM/Qwen often emit as fake tool names instead of `agent_send`. +const PSEUDO_AGENT_TOOL_NAMES: &[&str] = &[ + "analyst", + "architect", + "assistant", + "browser-hand", + "code-reviewer", + "coder", + "customer-support", + "data-scientist", + "debugger", + "devops-lead", + "doc-writer", + "email-assistant", + "health-tracker", + "home-automation", + "legal-assistant", + "meeting-assistant", + "ops", + "orchestrator", + "personal-finance", + "planner", + "recruiter", + "researcher", + "sales-assistant", + "security-auditor", + "social-media", + "test-engineer", + "translator", + "travel-planner", + "tutor", + "writer", +]; + +fn is_pseudo_agent_tool_name(name: &str, tool_names: &[&str]) -> bool { + if tool_names.contains(&name) || name.is_empty() { + return false; + } + if !name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + return false; + } + PSEUDO_AGENT_TOOL_NAMES.contains(&name) +} + +/// Normalize `agent_send` tool input: map alias target keys → `agent_id`, ensure `message`. +fn normalize_agent_send_input(target: &str, raw: serde_json::Value) -> Option { + let obj = raw.as_object()?; + const TARGET_KEYS: &[&str] = &["agent_id", "agent", "target", "to", "recipient", "name"]; + const MESSAGE_KEYS: &[&str] = &[ + "message", + "query", + "task", + "prompt", + "input", + "content", + "text", + "request", + ]; + + let message = MESSAGE_KEYS + .iter() + .find_map(|k| obj.get(*k)) + .and_then(|v| { + if let Some(s) = v.as_str() { + let t = s.trim(); + if !t.is_empty() { + return Some(t.to_string()); + } + } + None + }) + .or_else(|| { + if obj.len() == 1 { + obj.values().next().and_then(|v| v.as_str()).map(str::to_string) + } else { + None + } + })?; + + let agent_id = TARGET_KEYS + .iter() + .find_map(|k| obj.get(*k)) + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(str::to_string) + .or_else(|| { + if target.is_empty() { + None + } else { + Some(target.to_string()) + } + })?; + + Some(serde_json::json!({ + "agent_id": agent_id, + "message": message, + })) +} + +/// Coerce `{"name":"researcher","arguments":{"query":"..."}}` → `agent_send`. +fn coerce_pseudo_agent_to_agent_send( + name: &str, + args: serde_json::Value, + tool_names: &[&str], +) -> Option<(String, serde_json::Value)> { + if !tool_names.contains(&"agent_send") || !is_pseudo_agent_tool_name(name, tool_names) { + return None; + } + let input = normalize_agent_send_input(name, args)?; + info!( + pseudo = name, + "Coerced pseudo-agent JSON tool call → agent_send" + ); + Some(("agent_send".to_string(), input)) +} + /// Parse a JSON object that represents a tool call. /// Supports formats: /// - `{"name":"tool","arguments":{"key":"value"}}` /// - `{"name":"tool","parameters":{"key":"value"}}` /// - `{"function":"tool","arguments":{"key":"value"}}` /// - `{"tool":"tool_name","args":{"key":"value"}}` +/// - `{"name":"researcher","arguments":{"query":"..."}}` → `agent_send` (pseudo-agent, vLLM/Qwen) fn parse_json_tool_call_object( text: &str, tool_names: &[&str], @@ -2973,10 +3568,6 @@ fn parse_json_tool_call_object( .or_else(|| obj.get("tool")) .and_then(|v| v.as_str())?; - if !tool_names.contains(&name) { - return None; - } - // Extract arguments from various field names let args = obj .get("arguments") @@ -2993,6 +3584,16 @@ fn parse_json_tool_call_object( args }; + if !tool_names.contains(&name) { + return coerce_pseudo_agent_to_agent_send(name, args, tool_names); + } + + if name == "agent_send" { + if let Some(normalized) = normalize_agent_send_input("", args.clone()) { + return Some(("agent_send".to_string(), normalized)); + } + } + Some((name.to_string(), args)) } @@ -3231,7 +3832,7 @@ mod tests { use super::*; use crate::llm_driver::{CompletionResponse, LlmError}; use async_trait::async_trait; - use openfang_types::tool::ToolCall; + use omtae_types::tool::ToolCall; use std::sync::atomic::{AtomicU32, Ordering}; #[test] @@ -3239,6 +3840,41 @@ mod tests { assert_eq!(MAX_ITERATIONS, 50); } + #[test] + fn test_research_integrity_detects_website_without_url() { + let text = "1. Bulldog Haven\nWebsite: Bulldog Haven\nConfidence Level: High"; + assert!(research_integrity_issue(text, &[], false, true)); + } + + #[test] + fn test_research_integrity_passes_with_tool_urls() { + let text = "1. Example Kennel\nWebsite: https://example.com/kennel"; + let tool_urls = vec!["https://example.com/kennel".to_string()]; + assert!(!research_integrity_issue(text, &tool_urls, true, true)); + } + + #[test] + fn test_research_integrity_flags_unbacked_top_list() { + let text = "Top 10 English Bulldog breeders in Florida:\n1. A\n2. B\n3. C"; + assert!(research_integrity_issue(text, &[], false, true)); + } + + #[test] + fn test_apply_research_integrity_appends_disclaimer() { + let tools = vec![ToolDefinition { + name: "web_search".to_string(), + description: String::new(), + input_schema: serde_json::json!({}), + }]; + let result = apply_research_integrity_check( + "Top 3 breeders:\n1. A\n2. B\n3. C".to_string(), + &[], + false, + &tools, + ); + assert!(result.contains("Verification notice")); + } + /// Issue #1098: when a response carries Thinking blocks, the persisted /// assistant turn must keep them so the next turn round-trips reasoning /// state to the model. @@ -3538,7 +4174,7 @@ mod tests { fn test_max_history_messages() { assert_eq!(MAX_HISTORY_MESSAGES, 20); assert_eq!( - openfang_types::agent::DEFAULT_MAX_HISTORY_MESSAGES, + omtae_types::agent::DEFAULT_MAX_HISTORY_MESSAGES, MAX_HISTORY_MESSAGES ); } @@ -3546,7 +4182,7 @@ mod tests { /// Issue #871: an agent with a manifest override uses that value. #[test] fn test_effective_max_history_uses_manifest_override() { - let mut manifest = openfang_types::agent::AgentManifest { + let mut manifest = omtae_types::agent::AgentManifest { max_history_messages: Some(40), ..Default::default() }; @@ -3561,7 +4197,7 @@ mod tests { /// accidentally disabling history entirely. #[test] fn test_effective_max_history_falls_back_to_default() { - let mut manifest = openfang_types::agent::AgentManifest { + let mut manifest = omtae_types::agent::AgentManifest { max_history_messages: None, ..Default::default() }; @@ -3582,7 +4218,7 @@ mod tests { #[test] fn test_manifest_max_history_round_trip_json() { let json_no_override = r#"{"name":"worker","module":"builtin:chat"}"#; - let manifest: openfang_types::agent::AgentManifest = + let manifest: omtae_types::agent::AgentManifest = serde_json::from_str(json_no_override).unwrap(); assert_eq!(manifest.max_history_messages, None); assert_eq!( @@ -3592,7 +4228,7 @@ mod tests { let json_with_override = r#"{"name":"orchestrator","module":"builtin:chat","max_history_messages":40}"#; - let manifest: openfang_types::agent::AgentManifest = + let manifest: omtae_types::agent::AgentManifest = serde_json::from_str(json_with_override).unwrap(); assert_eq!(manifest.max_history_messages, Some(40)); assert_eq!(manifest.effective_max_history_messages(), 40); @@ -3674,7 +4310,7 @@ mod tests { fn test_manifest() -> AgentManifest { AgentManifest { name: "test-agent".to_string(), - model: openfang_types::agent::ModelConfig { + model: omtae_types::agent::ModelConfig { system_prompt: "You are a test agent.".to_string(), ..Default::default() }, @@ -3787,10 +4423,10 @@ mod tests { #[tokio::test] async fn test_empty_response_after_tool_use_returns_fallback() { - let memory = openfang_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); - let agent_id = openfang_types::agent::AgentId::new(); - let mut session = openfang_memory::session::Session { - id: openfang_types::agent::SessionId::new(), + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), agent_id, messages: Vec::new(), context_window_tokens: 0, @@ -3840,10 +4476,10 @@ mod tests { #[tokio::test] async fn test_tool_error_injects_no_fabrication_guidance() { - let memory = openfang_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); - let agent_id = openfang_types::agent::AgentId::new(); - let mut session = openfang_memory::session::Session { - id: openfang_types::agent::SessionId::new(), + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), agent_id, messages: Vec::new(), context_window_tokens: 0, @@ -3895,10 +4531,10 @@ mod tests { #[tokio::test] async fn test_empty_response_max_tokens_returns_fallback() { - let memory = openfang_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); - let agent_id = openfang_types::agent::AgentId::new(); - let mut session = openfang_memory::session::Session { - id: openfang_types::agent::SessionId::new(), + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), agent_id, messages: Vec::new(), context_window_tokens: 0, @@ -3948,10 +4584,10 @@ mod tests { #[tokio::test] async fn test_normal_response_not_replaced_by_fallback() { - let memory = openfang_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); - let agent_id = openfang_types::agent::AgentId::new(); - let mut session = openfang_memory::session::Session { - id: openfang_types::agent::SessionId::new(), + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), agent_id, messages: Vec::new(), context_window_tokens: 0, @@ -3992,10 +4628,10 @@ mod tests { #[tokio::test] async fn test_streaming_empty_response_after_tool_use_returns_fallback() { - let memory = openfang_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); - let agent_id = openfang_types::agent::AgentId::new(); - let mut session = openfang_memory::session::Session { - id: openfang_types::agent::SessionId::new(), + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), agent_id, messages: Vec::new(), context_window_tokens: 0, @@ -4118,10 +4754,10 @@ mod tests { #[tokio::test] async fn test_empty_first_response_retries_and_recovers() { - let memory = openfang_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); - let agent_id = openfang_types::agent::AgentId::new(); - let mut session = openfang_memory::session::Session { - id: openfang_types::agent::SessionId::new(), + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), agent_id, messages: Vec::new(), context_window_tokens: 0, @@ -4165,10 +4801,10 @@ mod tests { #[tokio::test] async fn test_empty_first_response_fallback_when_retry_also_empty() { - let memory = openfang_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); - let agent_id = openfang_types::agent::AgentId::new(); - let mut session = openfang_memory::session::Session { - id: openfang_types::agent::SessionId::new(), + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), agent_id, messages: Vec::new(), context_window_tokens: 0, @@ -4218,10 +4854,10 @@ mod tests { #[tokio::test] async fn test_streaming_empty_response_max_tokens_returns_fallback() { - let memory = openfang_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); - let agent_id = openfang_types::agent::AgentId::new(); - let mut session = openfang_memory::session::Session { - id: openfang_types::agent::SessionId::new(), + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), agent_id, messages: Vec::new(), context_window_tokens: 0, @@ -4864,6 +5500,43 @@ mod tests { assert_eq!(calls[0].input["command"], "ls"); } + #[test] + fn test_recover_pseudo_agent_researcher_to_agent_send() { + let tools = vec![ + ToolDefinition { + name: "agent_send".into(), + description: "Delegate".into(), + input_schema: serde_json::json!({}), + }, + ToolDefinition { + name: "agent_list".into(), + description: "List".into(), + input_schema: serde_json::json!({}), + }, + ]; + let text = r#"I'll delegate. {"name": "researcher", "arguments": {"query": "OMTAE architecture"}}"#; + let calls = recover_text_tool_calls(text, &tools); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].name, "agent_send"); + assert_eq!(calls[0].input["agent_id"], "researcher"); + assert_eq!(calls[0].input["message"], "OMTAE architecture"); + } + + #[test] + fn test_recover_agent_send_with_agent_alias() { + let tools = vec![ToolDefinition { + name: "agent_send".into(), + description: "Delegate".into(), + input_schema: serde_json::json!({}), + }]; + let text = r#"{"name":"agent_send","arguments":{"agent":"coder","message":"Fix the bug"}}"#; + let calls = recover_text_tool_calls(text, &tools); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].name, "agent_send"); + assert_eq!(calls[0].input["agent_id"], "coder"); + assert_eq!(calls[0].input["message"], "Fix the bug"); + } + // --- Pattern 9: XML-attribute style --- #[test] @@ -5184,10 +5857,10 @@ mod tests { // This is THE critical test: a model outputs a tool call as text, // the recovery code detects it, promotes it to ToolUse, executes the tool, // and the agent loop continues to produce a final response. - let memory = openfang_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); - let agent_id = openfang_types::agent::AgentId::new(); - let mut session = openfang_memory::session::Session { - id: openfang_types::agent::SessionId::new(), + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), agent_id, messages: Vec::new(), context_window_tokens: 0, @@ -5255,10 +5928,10 @@ mod tests { #[tokio::test] async fn test_nested_xml_text_tool_call_recovery_e2e() { - let memory = openfang_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); - let agent_id = openfang_types::agent::AgentId::new(); - let mut session = openfang_memory::session::Session { - id: openfang_types::agent::SessionId::new(), + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), agent_id, messages: Vec::new(), context_window_tokens: 0, @@ -5332,10 +6005,10 @@ mod tests { /// Verifies recovery does NOT interfere with normal flow. #[tokio::test] async fn test_normal_flow_unaffected_by_recovery() { - let memory = openfang_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); - let agent_id = openfang_types::agent::AgentId::new(); - let mut session = openfang_memory::session::Session { - id: openfang_types::agent::SessionId::new(), + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), agent_id, messages: Vec::new(), context_window_tokens: 0, @@ -5387,10 +6060,10 @@ mod tests { #[tokio::test] async fn test_text_tool_call_recovery_streaming_e2e() { - let memory = openfang_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); - let agent_id = openfang_types::agent::AgentId::new(); - let mut session = openfang_memory::session::Session { - id: openfang_types::agent::SessionId::new(), + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), agent_id, messages: Vec::new(), context_window_tokens: 0, @@ -5490,4 +6163,162 @@ mod tests { assert!(!is_silent_token("SILENT")); assert!(!is_silent_token("")); } + + struct OverflowRecoveryDriver { + calls: std::sync::atomic::AtomicU32, + } + + impl OverflowRecoveryDriver { + fn new() -> Self { + Self { + calls: std::sync::atomic::AtomicU32::new(0), + } + } + } + + #[async_trait::async_trait] + impl LlmDriver for OverflowRecoveryDriver { + async fn complete(&self, req: CompletionRequest) -> Result { + let attempt = self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + if attempt == 0 { + Err(LlmError::Api { + status: 400, + message: "This model's maximum context length is 1000 tokens. However, you requested 500 output tokens and your prompt contains at least 550 input tokens.".to_string(), + }) + } else { + assert!(req.max_tokens <= 434); + Ok(CompletionResponse { + content: vec![ContentBlock::Text { + text: "Recovered successfully!".to_string(), + provider_metadata: None, + }], + stop_reason: StopReason::EndTurn, + tool_calls: vec![], + usage: TokenUsage { + input_tokens: 30, + output_tokens: 12, + }, + }) + } + } + + async fn stream( + &self, + req: CompletionRequest, + tx: mpsc::Sender, + ) -> Result { + let attempt = self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + if attempt == 0 { + Err(LlmError::Api { + status: 400, + message: "This model's maximum context length is 1000 tokens. However, you requested 500 output tokens and your prompt contains at least 550 input tokens.".to_string(), + }) + } else { + assert!(req.max_tokens <= 434); + let _ = tx.send(StreamEvent::TextDelta { + text: "Recovered streaming successfully!".to_string(), + }).await; + Ok(CompletionResponse { + content: vec![ContentBlock::Text { + text: "Recovered streaming successfully!".to_string(), + provider_metadata: None, + }], + stop_reason: StopReason::EndTurn, + tool_calls: vec![], + usage: TokenUsage { + input_tokens: 30, + output_tokens: 12, + }, + }) + } + } + } + + #[tokio::test] + async fn test_context_overflow_recovery_sequential() { + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), + agent_id, + messages: Vec::new(), + context_window_tokens: 0, + label: None, + }; + let manifest = test_manifest(); + let driver: Arc = Arc::new(OverflowRecoveryDriver::new()); + + let result = run_agent_loop( + &manifest, + "Say hello", + &mut session, + &memory, + driver, + &[], + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(1000), // context_window_tokens override + None, + None, + ) + .await + .expect("Sequential loop with context overflow should recover and complete"); + + assert_eq!(result.response, "Recovered successfully!"); + } + + #[tokio::test] + async fn test_context_overflow_recovery_streaming() { + let memory = omtae_memory::MemorySubstrate::open_in_memory(0.01).unwrap(); + let agent_id = omtae_types::agent::AgentId::new(); + let mut session = omtae_memory::session::Session { + id: omtae_types::agent::SessionId::new(), + agent_id, + messages: Vec::new(), + context_window_tokens: 0, + label: None, + }; + let manifest = test_manifest(); + let driver: Arc = Arc::new(OverflowRecoveryDriver::new()); + let (tx, _rx) = mpsc::channel(64); + + let result = run_agent_loop_streaming( + &manifest, + "Say hello", + &mut session, + &memory, + driver, + &[], + None, + tx, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(1000), // context_window_tokens override + None, + None, + ) + .await + .expect("Streaming loop with context overflow should recover and complete"); + + assert_eq!(result.response, "Recovered streaming successfully!"); + } } diff --git a/crates/openfang-runtime/src/apply_patch.rs b/crates/openfang-runtime/src/apply_patch.rs index 4ea734e931..7f3e5a4f82 100644 --- a/crates/openfang-runtime/src/apply_patch.rs +++ b/crates/openfang-runtime/src/apply_patch.rs @@ -710,7 +710,7 @@ mod tests { #[tokio::test] async fn test_apply_patch_integration() { - let dir = std::env::temp_dir().join("openfang_patch_test"); + let dir = std::env::temp_dir().join("omtae_patch_test"); let _ = tokio::fs::remove_dir_all(&dir).await; tokio::fs::create_dir_all(&dir).await.unwrap(); @@ -758,7 +758,7 @@ mod tests { #[tokio::test] async fn test_apply_patch_delete() { - let dir = std::env::temp_dir().join("openfang_patch_del_test"); + let dir = std::env::temp_dir().join("omtae_patch_del_test"); let _ = tokio::fs::remove_dir_all(&dir).await; tokio::fs::create_dir_all(&dir).await.unwrap(); diff --git a/crates/openfang-runtime/src/auth_cooldown.rs b/crates/openfang-runtime/src/auth_cooldown.rs index d2ca95806d..047618e2f7 100644 --- a/crates/openfang-runtime/src/auth_cooldown.rs +++ b/crates/openfang-runtime/src/auth_cooldown.rs @@ -404,7 +404,7 @@ impl ProviderCooldown { pub fn select_profile( &self, provider: &str, - profiles: &[openfang_types::config::AuthProfile], + profiles: &[omtae_types::config::AuthProfile], ) -> Option<(String, String)> { if profiles.is_empty() { return None; diff --git a/crates/openfang-runtime/src/browser.rs b/crates/openfang-runtime/src/browser.rs index 1bc6c8f742..2d4b923504 100644 --- a/crates/openfang-runtime/src/browser.rs +++ b/crates/openfang-runtime/src/browser.rs @@ -13,7 +13,7 @@ use dashmap::DashMap; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; -use openfang_types::config::BrowserConfig; +use omtae_types::config::BrowserConfig; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; @@ -978,7 +978,7 @@ pub async fn tool_browser_screenshot( let mut image_urls: Vec = Vec::new(); if !b64.is_empty() { use base64::Engine; - let upload_dir = std::env::temp_dir().join("openfang_uploads"); + let upload_dir = std::env::temp_dir().join("omtae_uploads"); let _ = std::fs::create_dir_all(&upload_dir); let file_id = uuid::Uuid::new_v4().to_string(); if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(b64) { diff --git a/crates/openfang-runtime/src/compactor.rs b/crates/openfang-runtime/src/compactor.rs index 3602ff244a..223e2039ec 100644 --- a/crates/openfang-runtime/src/compactor.rs +++ b/crates/openfang-runtime/src/compactor.rs @@ -12,9 +12,9 @@ use crate::llm_driver::{CompletionRequest, LlmDriver}; use crate::str_utils::safe_truncate_str; -use openfang_memory::session::Session; -use openfang_types::message::{ContentBlock, Message, MessageContent, Role}; -use openfang_types::tool::ToolDefinition; +use omtae_memory::session::Session; +use omtae_types::message::{ContentBlock, Message, MessageContent, Role}; +use omtae_types::tool::ToolDefinition; use serde::Serialize; use std::sync::Arc; use tracing::{info, warn}; @@ -49,8 +49,8 @@ pub struct CompactionConfig { impl Default for CompactionConfig { fn default() -> Self { Self { - threshold: 30, - keep_recent: 10, + threshold: 20, + keep_recent: 8, max_summary_tokens: 1024, base_chunk_ratio: 0.4, min_chunk_ratio: 0.15, @@ -58,12 +58,77 @@ impl Default for CompactionConfig { summarization_overhead_tokens: 4096, max_chunk_chars: 80_000, max_retries: 3, - token_threshold_ratio: 0.7, + token_threshold_ratio: 0.65, context_window_tokens: 200_000, } } } +/// User overrides from `~/.omtae/config.toml` `[compaction]`. +#[derive(Debug, Clone, Default)] +pub struct CompactionUserSettings { + pub threshold: Option, + pub keep_recent: Option, + pub token_threshold_ratio: Option, + pub max_summary_tokens: Option, +} + +/// Apply optional `[compaction]` overrides from kernel config. +pub fn apply_user_compaction_settings(config: &mut CompactionConfig, user: &CompactionUserSettings) { + if let Some(t) = user.threshold.filter(|&v| v > 0) { + config.threshold = t; + } + if let Some(k) = user.keep_recent.filter(|&v| v > 0) { + config.keep_recent = k; + } + if let Some(r) = user.token_threshold_ratio.filter(|&v| v > 0.0 && v < 1.0) { + config.token_threshold_ratio = r; + } + if let Some(m) = user.max_summary_tokens.filter(|&v| v > 0) { + config.max_summary_tokens = m; + } +} + +/// Build compaction settings from the model's context window (catalog / custom_models.json). +pub fn compaction_config_for_context_window(ctx_window: Option) -> CompactionConfig { + let mut config = CompactionConfig::default(); + let Some(cw) = ctx_window.filter(|&w| w > 0) else { + return config; + }; + config.context_window_tokens = cw; + if cw <= 16_384 { + config.token_threshold_ratio = 0.55; + config.threshold = 15; + config.keep_recent = 6; + config.max_summary_tokens = 512.min(config.max_summary_tokens); + } else if cw <= 32_768 { + config.token_threshold_ratio = 0.60; + config.threshold = 18; + config.keep_recent = 7; + } + config +} + +/// Build compaction config with catalog context window and user overrides. +pub fn build_compaction_config( + ctx_window: Option, + user: &CompactionUserSettings, +) -> CompactionConfig { + let mut config = compaction_config_for_context_window(ctx_window); + apply_user_compaction_settings(&mut config, user); + config +} + +/// Cap completion tokens so input + output fit within the model context window. +pub fn cap_max_output_tokens(manifest_max: u32, ctx_window: Option) -> u32 { + let Some(cw) = ctx_window.filter(|&w| w > 0) else { + return manifest_max; + }; + // Reserve at least half the window for input, tools, and system prompt. + let ceiling = ((cw as u32) / 2).max(256); + manifest_max.min(ceiling) +} + /// Result of a compaction operation. #[derive(Debug)] pub struct CompactionResult { @@ -81,7 +146,16 @@ pub struct CompactionResult { /// Check whether a session needs compaction (message-count trigger). pub fn needs_compaction(session: &Session, config: &CompactionConfig) -> bool { - session.messages.len() > config.threshold + session.messages.len() >= config.threshold +} + +/// Message-count or token-count compaction trigger. +pub fn needs_compaction_any( + session: &Session, + estimated_tokens: usize, + config: &CompactionConfig, +) -> bool { + needs_compaction(session, config) || needs_compaction_by_tokens(estimated_tokens, config) } /// Estimate token count for a set of messages, optional system prompt, and tool definitions. @@ -90,7 +164,7 @@ pub fn needs_compaction(session: &Session, config: &CompactionConfig) -> bool { pub fn estimate_token_count( messages: &[Message], system_prompt: Option<&str>, - tools: Option<&[openfang_types::tool::ToolDefinition]>, + tools: Option<&[omtae_types::tool::ToolDefinition]>, ) -> usize { let mut chars: usize = 0; @@ -615,7 +689,7 @@ async fn summarize_in_chunks( /// and the message at `split` is a user message with matching ToolResult blocks, /// the split is pulled back by 1 so the pair stays in the "kept" portion. fn adjust_split_for_tool_pairs(messages: &[Message], split: usize) -> usize { - use openfang_types::message::{ContentBlock, Role}; + use omtae_types::message::{ContentBlock, Role}; if split == 0 || split >= messages.len() { return split; @@ -770,13 +844,13 @@ pub async fn compact_session( #[cfg(test)] mod tests { use super::*; - use openfang_types::message::TokenUsage; + use omtae_types::message::TokenUsage; #[test] fn test_needs_compaction_below_threshold() { let session = Session { - id: openfang_types::agent::SessionId::new(), - agent_id: openfang_types::agent::AgentId::new(), + id: omtae_types::agent::SessionId::new(), + agent_id: omtae_types::agent::AgentId::new(), messages: vec![Message::user("hello")], context_window_tokens: 0, label: None, @@ -791,8 +865,8 @@ mod tests { .map(|i| Message::user(format!("msg {i}"))) .collect(); let session = Session { - id: openfang_types::agent::SessionId::new(), - agent_id: openfang_types::agent::AgentId::new(), + id: omtae_types::agent::SessionId::new(), + agent_id: omtae_types::agent::AgentId::new(), messages, context_window_tokens: 0, label: None, @@ -804,13 +878,27 @@ mod tests { #[test] fn test_compaction_config_defaults() { let config = CompactionConfig::default(); - assert_eq!(config.threshold, 30); - assert_eq!(config.keep_recent, 10); + assert_eq!(config.threshold, 20); + assert_eq!(config.keep_recent, 8); assert_eq!(config.max_summary_tokens, 1024); - assert!((config.token_threshold_ratio - 0.7).abs() < f64::EPSILON); + assert!((config.token_threshold_ratio - 0.65).abs() < f64::EPSILON); assert_eq!(config.context_window_tokens, 200_000); } + #[test] + fn test_compaction_config_for_small_context_window() { + let config = compaction_config_for_context_window(Some(8192)); + assert_eq!(config.context_window_tokens, 8192); + assert!(config.token_threshold_ratio < 0.7); + } + + #[test] + fn test_cap_max_output_tokens() { + assert_eq!(cap_max_output_tokens(8192, Some(8192)), 4096); + assert_eq!(cap_max_output_tokens(2048, Some(8192)), 2048); + assert_eq!(cap_max_output_tokens(8192, None), 8192); + } + #[tokio::test] async fn test_compact_session_few_messages() { use crate::llm_driver::{CompletionResponse, LlmError}; @@ -829,7 +917,7 @@ mod tests { text: "Summary of conversation".to_string(), provider_metadata: None, }], - stop_reason: openfang_types::message::StopReason::EndTurn, + stop_reason: omtae_types::message::StopReason::EndTurn, tool_calls: vec![], usage: TokenUsage { input_tokens: 100, @@ -840,8 +928,8 @@ mod tests { } let session = Session { - id: openfang_types::agent::SessionId::new(), - agent_id: openfang_types::agent::AgentId::new(), + id: omtae_types::agent::SessionId::new(), + agent_id: omtae_types::agent::AgentId::new(), messages: vec![Message::user("hello"), Message::assistant("hi")], context_window_tokens: 0, label: None, @@ -891,7 +979,7 @@ mod tests { text: "Summary with tools".to_string(), provider_metadata: None, }], - stop_reason: openfang_types::message::StopReason::EndTurn, + stop_reason: omtae_types::message::StopReason::EndTurn, tool_calls: vec![], usage: TokenUsage { input_tokens: 100, @@ -929,8 +1017,8 @@ mod tests { }; let session = Session { - id: openfang_types::agent::SessionId::new(), - agent_id: openfang_types::agent::AgentId::new(), + id: omtae_types::agent::SessionId::new(), + agent_id: omtae_types::agent::AgentId::new(), messages, context_window_tokens: 0, label: None, @@ -986,7 +1074,7 @@ mod tests { text: "Summary: discussed topics 0 through 79".to_string(), provider_metadata: None, }], - stop_reason: openfang_types::message::StopReason::EndTurn, + stop_reason: omtae_types::message::StopReason::EndTurn, tool_calls: vec![], usage: TokenUsage { input_tokens: 500, @@ -1000,8 +1088,8 @@ mod tests { .map(|i| Message::user(format!("Message about topic {i}"))) .collect(); let session = Session { - id: openfang_types::agent::SessionId::new(), - agent_id: openfang_types::agent::AgentId::new(), + id: omtae_types::agent::SessionId::new(), + agent_id: omtae_types::agent::AgentId::new(), messages, context_window_tokens: 0, label: None, @@ -1093,8 +1181,8 @@ mod tests { #[test] fn test_compaction_config_new_defaults() { let config = CompactionConfig::default(); - assert_eq!(config.threshold, 30); - assert_eq!(config.keep_recent, 10); + assert_eq!(config.threshold, 20); + assert_eq!(config.keep_recent, 8); assert_eq!(config.max_summary_tokens, 1024); assert!((config.base_chunk_ratio - 0.4).abs() < f64::EPSILON); assert!((config.min_chunk_ratio - 0.15).abs() < f64::EPSILON); @@ -1102,7 +1190,7 @@ mod tests { assert_eq!(config.summarization_overhead_tokens, 4096); assert_eq!(config.max_chunk_chars, 80_000); assert_eq!(config.max_retries, 3); - assert!((config.token_threshold_ratio - 0.7).abs() < f64::EPSILON); + assert!((config.token_threshold_ratio - 0.65).abs() < f64::EPSILON); assert_eq!(config.context_window_tokens, 200_000); } @@ -1127,8 +1215,8 @@ mod tests { .map(|i| Message::user(format!("Message {i}"))) .collect(); let session = Session { - id: openfang_types::agent::SessionId::new(), - agent_id: openfang_types::agent::AgentId::new(), + id: omtae_types::agent::SessionId::new(), + agent_id: omtae_types::agent::AgentId::new(), messages, context_window_tokens: 0, label: None, @@ -1182,7 +1270,7 @@ mod tests { text: format!("Chunk summary {n}"), provider_metadata: None, }], - stop_reason: openfang_types::message::StopReason::EndTurn, + stop_reason: omtae_types::message::StopReason::EndTurn, tool_calls: vec![], usage: TokenUsage { input_tokens: 50, @@ -1327,7 +1415,7 @@ mod tests { #[test] fn test_estimate_token_count_with_tools() { - use openfang_types::tool::ToolDefinition; + use omtae_types::tool::ToolDefinition; let messages = vec![Message::user("hi")]; let tools = vec![ToolDefinition { name: "web_search".into(), diff --git a/crates/openfang-runtime/src/context_budget.rs b/crates/openfang-runtime/src/context_budget.rs index 14cd738312..11ac1fa376 100644 --- a/crates/openfang-runtime/src/context_budget.rs +++ b/crates/openfang-runtime/src/context_budget.rs @@ -6,8 +6,8 @@ //! and compacts oldest results when total exceeds 75% headroom. use crate::str_utils::safe_truncate_str; -use openfang_types::message::{ContentBlock, Message, MessageContent}; -use openfang_types::tool::ToolDefinition; +use omtae_types::message::{ContentBlock, Message, MessageContent}; +use omtae_types::tool::ToolDefinition; use tracing::debug; /// Budget parameters derived from the model's context window. @@ -283,7 +283,7 @@ mod tests { let big_result = "x".repeat(500); let mut messages = vec![ Message { - role: openfang_types::message::Role::User, + role: omtae_types::message::Role::User, content: MessageContent::Blocks(vec![ContentBlock::ToolResult { tool_use_id: "t1".to_string(), tool_name: String::new(), @@ -293,7 +293,7 @@ mod tests { ..Default::default() }, Message { - role: openfang_types::message::Role::User, + role: omtae_types::message::Role::User, content: MessageContent::Blocks(vec![ContentBlock::ToolResult { tool_use_id: "t2".to_string(), tool_name: String::new(), @@ -345,7 +345,7 @@ mod tests { // Chinese text: 500 chars * 3 bytes = 1500 bytes let big_chinese: String = "\u{4e2d}\u{6587}\u{6d4b}\u{8bd5}\u{6570}\u{636e}".repeat(83); let mut messages = vec![Message { - role: openfang_types::message::Role::User, + role: omtae_types::message::Role::User, content: MessageContent::Blocks(vec![ContentBlock::ToolResult { tool_use_id: "t1".to_string(), tool_name: String::new(), diff --git a/crates/openfang-runtime/src/context_overflow.rs b/crates/openfang-runtime/src/context_overflow.rs index 3d9fa8c48f..4a86b67139 100644 --- a/crates/openfang-runtime/src/context_overflow.rs +++ b/crates/openfang-runtime/src/context_overflow.rs @@ -8,8 +8,8 @@ //! 3. Truncate historical tool results to 2K chars each //! 4. Return error suggesting /reset or /compact -use openfang_types::message::{ContentBlock, Message, MessageContent, Role}; -use openfang_types::tool::ToolDefinition; +use omtae_types::message::{ContentBlock, Message, MessageContent, Role}; +use omtae_types::tool::ToolDefinition; use tracing::{debug, warn}; /// Adjust a drain boundary so it does not split a ToolUse/ToolResult pair. @@ -121,8 +121,18 @@ pub fn recover_from_overflow( context_window: usize, ) -> RecoveryStage { let estimated = estimate_tokens(messages, system_prompt, tools); - let threshold_70 = (context_window as f64 * 0.70) as usize; - let threshold_90 = (context_window as f64 * 0.90) as usize; + // Tighter recovery for smaller context windows (local vLLM models). + let (low_ratio, high_ratio) = if context_window <= 8_192 { + (0.50, 0.72) + } else if context_window <= 16_384 { + (0.55, 0.75) + } else if context_window <= 32_768 { + (0.60, 0.80) + } else { + (0.70, 0.90) + }; + let threshold_70 = (context_window as f64 * low_ratio) as usize; + let threshold_90 = (context_window as f64 * high_ratio) as usize; // No recovery needed if estimated <= threshold_70 { @@ -224,7 +234,7 @@ pub fn recover_from_overflow( #[cfg(test)] mod tests { use super::*; - use openfang_types::message::{Message, Role}; + use omtae_types::message::{Message, Role}; fn make_messages(count: usize, size_each: usize) -> Vec { (0..count) diff --git a/crates/openfang-runtime/src/docker_sandbox.rs b/crates/openfang-runtime/src/docker_sandbox.rs index 25444e54cd..018a320914 100644 --- a/crates/openfang-runtime/src/docker_sandbox.rs +++ b/crates/openfang-runtime/src/docker_sandbox.rs @@ -3,7 +3,7 @@ //! Provides secure command execution inside Docker containers with strict //! resource limits, network isolation, and capability dropping. -use openfang_types::config::DockerSandboxConfig; +use omtae_types::config::DockerSandboxConfig; use std::path::Path; use std::time::Duration; use tracing::{debug, warn}; @@ -434,8 +434,8 @@ mod tests { #[test] fn test_sanitize_container_name_valid() { - let result = sanitize_container_name("openfang-sandbox-abc123").unwrap(); - assert_eq!(result, "openfang-sandbox-abc123"); + let result = sanitize_container_name("omtae-sandbox-abc123").unwrap(); + assert_eq!(result, "omtae-sandbox-abc123"); } #[test] @@ -518,7 +518,7 @@ mod tests { let config = DockerSandboxConfig::default(); assert!(!config.enabled); assert_eq!(config.image, "python:3.12-slim"); - assert_eq!(config.container_prefix, "openfang-sandbox"); + assert_eq!(config.container_prefix, "omtae-sandbox"); assert_eq!(config.workdir, "/workspace"); assert_eq!(config.network, "none"); assert_eq!(config.memory_limit, "512m"); diff --git a/crates/openfang-runtime/src/drivers/anthropic.rs b/crates/openfang-runtime/src/drivers/anthropic.rs index 10a606e6a7..e6783c649b 100644 --- a/crates/openfang-runtime/src/drivers/anthropic.rs +++ b/crates/openfang-runtime/src/drivers/anthropic.rs @@ -6,10 +6,10 @@ use crate::llm_driver::{CompletionRequest, CompletionResponse, LlmDriver, LlmError, StreamEvent}; use async_trait::async_trait; use futures::StreamExt; -use openfang_types::message::{ +use omtae_types::message::{ ContentBlock, Message, MessageContent, Role, StopReason, TokenUsage, }; -use openfang_types::tool::ToolCall; +use omtae_types::tool::ToolCall; use serde::{Deserialize, Serialize}; use tracing::{debug, warn}; use zeroize::Zeroizing; @@ -678,7 +678,7 @@ fn ensure_object(v: &serde_json::Value) -> serde_json::Value { } } -/// Convert an OpenFang Message to an Anthropic API message. +/// Convert an OMTAE Message to an Anthropic API message. fn convert_message(msg: &Message) -> ApiMessage { let role = match msg.role { Role::User => "user", diff --git a/crates/openfang-runtime/src/drivers/bedrock.rs b/crates/openfang-runtime/src/drivers/bedrock.rs index 4920e0b3c3..8a0e24565d 100644 --- a/crates/openfang-runtime/src/drivers/bedrock.rs +++ b/crates/openfang-runtime/src/drivers/bedrock.rs @@ -4,8 +4,8 @@ use crate::llm_driver::{CompletionRequest, CompletionResponse, LlmDriver, LlmError}; use async_trait::async_trait; -use openfang_types::message::{ContentBlock, MessageContent, Role, StopReason, TokenUsage}; -use openfang_types::tool::{ToolCall, ToolDefinition}; +use omtae_types::message::{ContentBlock, MessageContent, Role, StopReason, TokenUsage}; +use omtae_types::tool::{ToolCall, ToolDefinition}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use tracing::{debug, warn}; @@ -223,7 +223,7 @@ struct BedrockErrorResponse { // ── Conversion helpers ──────────────────────────────────────────────────────── fn convert_messages( - messages: &[openfang_types::message::Message], + messages: &[omtae_types::message::Message], system: &Option, ) -> (Vec, Option>) { let system_blocks = extract_system(messages, system); @@ -252,7 +252,7 @@ fn convert_messages( } fn extract_system( - messages: &[openfang_types::message::Message], + messages: &[omtae_types::message::Message], system: &Option, ) -> Option> { let text = system.clone().or_else(|| { @@ -781,8 +781,8 @@ impl LlmDriver for BedrockDriver { #[cfg(test)] mod tests { use super::*; - use openfang_types::message::{Message, MessageContent, Role}; - use openfang_types::tool::ToolDefinition; + use omtae_types::message::{Message, MessageContent, Role}; + use omtae_types::tool::ToolDefinition; // ── Endpoint building ────────────────────────────────────────────────────── diff --git a/crates/openfang-runtime/src/drivers/claude_code.rs b/crates/openfang-runtime/src/drivers/claude_code.rs index 897d12ed4b..43ca88fb65 100644 --- a/crates/openfang-runtime/src/drivers/claude_code.rs +++ b/crates/openfang-runtime/src/drivers/claude_code.rs @@ -11,7 +11,7 @@ use crate::llm_driver::{CompletionRequest, CompletionResponse, LlmDriver, LlmError, StreamEvent}; use async_trait::async_trait; use dashmap::DashMap; -use openfang_types::message::{ContentBlock, MessageContent, Role, StopReason, TokenUsage}; +use omtae_types::message::{ContentBlock, MessageContent, Role, StopReason, TokenUsage}; use serde::Deserialize; use std::sync::Arc; use tokio::io::{AsyncBufReadExt, AsyncReadExt}; @@ -74,7 +74,7 @@ impl ClaudeCodeDriver { warn!( "Claude Code driver: --dangerously-skip-permissions enabled. \ The CLI will not prompt for tool approvals. \ - OpenFang's own capability/RBAC system enforces access control." + OMTAE's own capability/RBAC system enforces access control." ); } @@ -329,7 +329,7 @@ impl LlmDriver for ClaudeCodeDriver { Self::apply_env_filter(&mut cmd); // Inject HOME so the CLI can find its credentials (~/.claude/) when - // OpenFang runs as a service without a login shell. + // OMTAE runs as a service without a login shell. if let Some(home) = home_dir() { cmd.env("HOME", &home); } @@ -750,7 +750,7 @@ mod tests { #[test] fn test_build_prompt_simple() { - use openfang_types::message::{Message, MessageContent}; + use omtae_types::message::{Message, MessageContent}; let request = CompletionRequest { model: "claude-code/sonnet".to_string(), @@ -775,7 +775,7 @@ mod tests { #[test] fn test_build_prompt_renders_image_attachment_marker() { - use openfang_types::message::{ContentBlock, Message, MessageContent}; + use omtae_types::message::{ContentBlock, Message, MessageContent}; // ~12 KB of base64 — decoded ~9 KB. let fake_b64 = "A".repeat(12 * 1024); @@ -816,7 +816,7 @@ mod tests { #[test] fn test_build_prompt_image_only_still_emits_marker() { - use openfang_types::message::{ContentBlock, Message, MessageContent}; + use omtae_types::message::{ContentBlock, Message, MessageContent}; let request = CompletionRequest { model: "claude-code/sonnet".to_string(), diff --git a/crates/openfang-runtime/src/drivers/copilot.rs b/crates/openfang-runtime/src/drivers/copilot.rs index 9c1f15f341..a0f7d155e6 100644 --- a/crates/openfang-runtime/src/drivers/copilot.rs +++ b/crates/openfang-runtime/src/drivers/copilot.rs @@ -51,7 +51,7 @@ const ACCESS_TOKEN_REFRESH_BUFFER_SECS: u64 = 600; // 10 minutes /// Scopes requested during device flow. const OAUTH_SCOPES: &str = "copilot"; -/// File name for persisted OAuth tokens (inside ~/.openfang/). +/// File name for persisted OAuth tokens (inside ~/.omtae/). const TOKEN_FILE_NAME: &str = ".copilot-tokens.json"; /// Device flow polling interval (seconds) — GitHub default is 5. @@ -83,16 +83,16 @@ impl PersistedTokens { self.access_token_expires_at > now + ACCESS_TOKEN_REFRESH_BUFFER_SECS as i64 } - /// Load from the OpenFang data directory. - pub fn load(openfang_dir: &Path) -> Option { - let path = openfang_dir.join(TOKEN_FILE_NAME); + /// Load from the OMTAE data directory. + pub fn load(omtae_dir: &Path) -> Option { + let path = omtae_dir.join(TOKEN_FILE_NAME); let data = std::fs::read_to_string(&path).ok()?; serde_json::from_str(&data).ok() } - /// Persist to the OpenFang data directory with restricted permissions. - pub fn save(&self, openfang_dir: &Path) -> Result<(), String> { - let path = openfang_dir.join(TOKEN_FILE_NAME); + /// Persist to the OMTAE data directory with restricted permissions. + pub fn save(&self, omtae_dir: &Path) -> Result<(), String> { + let path = omtae_dir.join(TOKEN_FILE_NAME); let json = serde_json::to_string_pretty(self) .map_err(|e| format!("Failed to serialize tokens: {e}"))?; std::fs::write(&path, &json) @@ -336,7 +336,7 @@ pub async fn exchange_copilot_token( .get(COPILOT_TOKEN_URL) .header("Authorization", format!("token {access_token}")) .header("Accept", "application/json") - .header("User-Agent", "OpenFang/1.0") + .header("User-Agent", "OMTAE/1.0") .header("Editor-Version", "vscode/1.96.0") .header("Editor-Plugin-Version", "copilot/1.250.0") .send() @@ -420,7 +420,7 @@ pub async fn fetch_models( let resp = client .get(&url) .header("Authorization", format!("Bearer {copilot_token}")) - .header("User-Agent", "OpenFang/1.0") + .header("User-Agent", "OMTAE/1.0") .header("Editor-Version", "vscode/1.96.0") .send() .await @@ -463,7 +463,7 @@ pub async fn fetch_models( /// LLM driver that authenticates via GitHub OAuth device flow and proxies /// completions through the Copilot API (OpenAI-compatible). pub struct CopilotDriver { - openfang_dir: PathBuf, + omtae_dir: PathBuf, http_client: reqwest::Client, /// Persisted OAuth tokens (ghu_ + grt_). @@ -475,20 +475,20 @@ pub struct CopilotDriver { } impl CopilotDriver { - pub fn new(openfang_dir: PathBuf) -> Self { + pub fn new(omtae_dir: PathBuf) -> Self { let http_client = reqwest::Client::builder() .timeout(TOKEN_EXCHANGE_TIMEOUT) .build() .expect("Failed to build HTTP client"); // Try to load persisted tokens on construction. - let persisted = PersistedTokens::load(&openfang_dir); + let persisted = PersistedTokens::load(&omtae_dir); if persisted.is_some() { debug!("Loaded persisted Copilot OAuth tokens"); } Self { - openfang_dir, + omtae_dir, http_client, oauth_tokens: Mutex::new(persisted), copilot_token: Mutex::new(None), @@ -520,7 +520,7 @@ impl CopilotDriver { match refresh_access_token(&self.http_client, rt).await { Ok(new_tokens) => { info!("Copilot access token refreshed successfully"); - if let Err(e) = new_tokens.save(&self.openfang_dir) { + if let Err(e) = new_tokens.save(&self.omtae_dir) { warn!("Failed to persist refreshed tokens: {e}"); } let access_token = new_tokens.access_token.clone(); @@ -540,7 +540,7 @@ impl CopilotDriver { // No valid tokens and refresh failed — need device flow. // In daemon mode, we can't do interactive auth. Return a clear error. Err(crate::llm_driver::LlmError::AuthenticationFailed( - "Copilot OAuth tokens expired. Run `openfang config set-key github-copilot` to re-authenticate via device flow.".to_string(), + "Copilot OAuth tokens expired. Run `omtae config set-key github-copilot` to re-authenticate via device flow.".to_string(), )) } @@ -732,17 +732,17 @@ impl crate::llm_driver::LlmDriver for CopilotDriver { /// Run the interactive Copilot setup: execute the device flow. /// -/// Called from `openfang config set-key github-copilot`, `openfang init`, -/// `openfang onboard`, and `openfang configure`. -pub async fn run_interactive_setup(openfang_dir: &Path) -> Result { - run_device_flow(openfang_dir).await +/// Called from `omtae config set-key github-copilot`, `omtae init`, +/// `omtae onboard`, and `omtae configure`. +pub async fn run_interactive_setup(omtae_dir: &Path) -> Result { + run_device_flow(omtae_dir).await } /// Run the OAuth device flow using the Copilot client ID. /// /// Prints the user code and verification URL, attempts to open the browser, /// then polls until the user authorizes. -pub async fn run_device_flow(openfang_dir: &Path) -> Result { +pub async fn run_device_flow(omtae_dir: &Path) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(30)) .build() @@ -768,7 +768,7 @@ pub async fn run_device_flow(openfang_dir: &Path) -> Result bool { } /// Check if Copilot OAuth tokens exist on disk. -pub fn copilot_auth_available(openfang_dir: &Path) -> bool { - openfang_dir.join(TOKEN_FILE_NAME).exists() +pub fn copilot_auth_available(omtae_dir: &Path) -> bool { + omtae_dir.join(TOKEN_FILE_NAME).exists() } // --------------------------------------------------------------------------- diff --git a/crates/openfang-runtime/src/drivers/fallback.rs b/crates/openfang-runtime/src/drivers/fallback.rs index 63958ce375..f4ce266502 100644 --- a/crates/openfang-runtime/src/drivers/fallback.rs +++ b/crates/openfang-runtime/src/drivers/fallback.rs @@ -118,7 +118,7 @@ impl LlmDriver for FallbackDriver { mod tests { use super::*; use crate::llm_driver::CompletionResponse; - use openfang_types::message::{ContentBlock, StopReason, TokenUsage}; + use omtae_types::message::{ContentBlock, StopReason, TokenUsage}; struct FailDriver; diff --git a/crates/openfang-runtime/src/drivers/gemini.rs b/crates/openfang-runtime/src/drivers/gemini.rs index aee4e30c78..f322a50da9 100644 --- a/crates/openfang-runtime/src/drivers/gemini.rs +++ b/crates/openfang-runtime/src/drivers/gemini.rs @@ -11,10 +11,10 @@ use crate::llm_driver::{CompletionRequest, CompletionResponse, LlmDriver, LlmError, StreamEvent}; use async_trait::async_trait; use futures::StreamExt; -use openfang_types::message::{ +use omtae_types::message::{ ContentBlock, Message, MessageContent, Role, StopReason, TokenUsage, }; -use openfang_types::tool::ToolCall; +use omtae_types::tool::ToolCall; use serde::{Deserialize, Serialize}; use tracing::{debug, warn}; use zeroize::Zeroizing; @@ -228,7 +228,7 @@ fn parse_gemini_error(body: &str) -> String { // ── Message conversion ───────────────────────────────────────────────── -/// Convert OpenFang messages into Gemini content entries. +/// Convert OMTAE messages into Gemini content entries. fn convert_messages( messages: &[Message], system: &Option, @@ -524,7 +524,7 @@ fn convert_tools(request: &CompletionRequest) -> Vec { .map(|t| { // Normalize schema for Gemini (strips $schema, flattens anyOf) let normalized = - openfang_types::tool::normalize_schema_for_provider(&t.input_schema, "gemini"); + omtae_types::tool::normalize_schema_for_provider(&t.input_schema, "gemini"); GeminiFunctionDeclaration { name: t.name.clone(), description: t.description.clone(), @@ -1187,7 +1187,7 @@ impl LlmDriver for GeminiDriver { #[cfg(test)] mod tests { use super::*; - use openfang_types::tool::ToolDefinition; + use omtae_types::tool::ToolDefinition; #[test] fn test_gemini_driver_creation() { diff --git a/crates/openfang-runtime/src/drivers/mod.rs b/crates/openfang-runtime/src/drivers/mod.rs index ca1e0701c7..7c99ccff33 100644 --- a/crates/openfang-runtime/src/drivers/mod.rs +++ b/crates/openfang-runtime/src/drivers/mod.rs @@ -15,7 +15,7 @@ pub mod qwen_code; pub mod vertex; use crate::llm_driver::{DriverConfig, LlmDriver, LlmError}; -use openfang_types::model_catalog::{ +use omtae_types::model_catalog::{ AI21_BASE_URL, ANTHROPIC_BASE_URL, AZURE_OPENAI_BASE_URL, CEREBRAS_BASE_URL, CHUTES_BASE_URL, COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, GEMINI_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL, KIMI_CODING_BASE_URL, LEMONADE_BASE_URL, LMSTUDIO_BASE_URL, @@ -39,7 +39,7 @@ struct ProviderDefaults { /// well-known environment variables. Returns `None` if no override is set. /// /// This lets users point Ollama / LM Studio / vLLM / Lemonade at a remote host -/// (VPS, LXC, another box on the LAN) without editing `~/.openfang/config.toml`. +/// (VPS, LXC, another box on the LAN) without editing `~/.omtae/config.toml`. /// /// Recognised variables: /// - `ollama` → `OLLAMA_BASE_URL`, then `OLLAMA_HOST` (Ollama CLI convention) @@ -423,21 +423,21 @@ pub fn create_driver(config: &DriverConfig) -> Result, LlmErr // GitHub Copilot — OAuth device flow + OpenAI-compatible completions. // Authentication is handled automatically via persisted tokens from the device flow. - // Run `openfang config set-key github-copilot` to authenticate. + // Run `omtae config set-key github-copilot` to authenticate. if provider == "github-copilot" || provider == "copilot" { - let openfang_dir = std::env::var("HOME") + let omtae_dir = std::env::var("HOME") .or_else(|_| std::env::var("USERPROFILE")) - .map(|h| std::path::PathBuf::from(h).join(".openfang")) - .unwrap_or_else(|_| std::path::PathBuf::from(".openfang")); + .map(|h| std::path::PathBuf::from(h).join(".omtae")) + .unwrap_or_else(|_| std::path::PathBuf::from(".omtae")); - if !copilot::copilot_auth_available(&openfang_dir) { + if !copilot::copilot_auth_available(&omtae_dir) { return Err(LlmError::MissingApiKey( - "Copilot not authenticated. Run `openfang config set-key github-copilot` to sign in." + "Copilot not authenticated. Run `omtae config set-key github-copilot` to sign in." .to_string(), )); } - return Ok(Arc::new(copilot::CopilotDriver::new(openfang_dir))); + return Ok(Arc::new(copilot::CopilotDriver::new(omtae_dir))); } // Azure OpenAI — deployment-based URL with `api-key` header diff --git a/crates/openfang-runtime/src/drivers/openai.rs b/crates/openfang-runtime/src/drivers/openai.rs index 73210f2757..3732ab8558 100644 --- a/crates/openfang-runtime/src/drivers/openai.rs +++ b/crates/openfang-runtime/src/drivers/openai.rs @@ -6,9 +6,9 @@ use crate::llm_driver::{CompletionRequest, CompletionResponse, LlmDriver, LlmErr use crate::think_filter::{FilterAction, StreamingThinkFilter}; use async_trait::async_trait; use futures::StreamExt; -use openfang_types::message::{ContentBlock, MessageContent, Role, StopReason, TokenUsage}; -use openfang_types::model_catalog::MOONSHOT_KIMI_BASE_URL; -use openfang_types::tool::ToolCall; +use omtae_types::message::{ContentBlock, MessageContent, Role, StopReason, TokenUsage}; +use omtae_types::model_catalog::MOONSHOT_KIMI_BASE_URL; +use omtae_types::tool::ToolCall; use serde::{Deserialize, Serialize}; use tracing::{debug, warn}; use zeroize::Zeroizing; @@ -181,6 +181,31 @@ fn temperature_must_be_one(model: &str) -> bool { m.starts_with("kimi-k2") || m == "kimi-k2.5" || m == "kimi-k2.5-0711" } +/// True for self-hosted OpenAI-compatible endpoints (vLLM, Ollama, LM Studio, Lemonade). +fn is_local_openai_compatible(base_url: &str) -> bool { + let url = base_url.to_lowercase(); + url.contains("localhost") + || url.contains("127.0.0.1") + || url.contains(":8000") + || url.contains(":11434") + || url.contains(":1234") + || url.contains("vllm") + || url.contains("ollama") + || url.contains("lmstudio") + || url.contains("lemonade") +} + +/// Explicit tool_choice when tools are present. Local/vLLM servers often ignore the +/// tools array unless tool_choice is set (otherwise models emit fake JSON in text). +/// Use the string `"auto"` — vLLM rejects `{"type":"auto"}` (validation: function None). +fn tool_choice_for_tools(has_tools: bool, _base_url: &str) -> Option { + if has_tools { + Some(serde_json::json!("auto")) + } else { + None + } +} + #[derive(Debug, Serialize)] struct OaiMessage { role: String, @@ -565,7 +590,7 @@ impl LlmDriver for OpenAIDriver { function: OaiToolDef { name: t.name.clone(), description: t.description.clone(), - parameters: openfang_types::tool::normalize_schema_for_provider( + parameters: omtae_types::tool::normalize_schema_for_provider( &t.input_schema, "openai", ), @@ -573,11 +598,7 @@ impl LlmDriver for OpenAIDriver { }) .collect(); - let tool_choice = if oai_tools.is_empty() { - None - } else { - Some(serde_json::json!("auto")) - }; + let tool_choice = tool_choice_for_tools(!oai_tools.is_empty(), &self.base_url); let (mt, mct) = if uses_completion_tokens(&request.model) { (None, Some(request.max_tokens)) @@ -627,10 +648,18 @@ impl LlmDriver for OpenAIDriver { req_builder = req_builder.header(k, v); } - let resp = req_builder - .send() - .await - .map_err(|e| LlmError::Http(e.to_string()))?; + let resp = match req_builder.send().await { + Ok(r) => r, + Err(e) => { + if attempt < max_retries { + let retry_ms = (attempt + 1) as u64 * 1000; + warn!(error = %e, attempt, retry_ms, "HTTP request send failed, retrying"); + tokio::time::sleep(std::time::Duration::from_millis(retry_ms)).await; + continue; + } + return Err(LlmError::Http(e.to_string())); + } + }; let status = resp.status().as_u16(); if status == 429 { @@ -692,7 +721,7 @@ impl LlmDriver for OpenAIDriver { } // Auto-cap max_tokens when model rejects our value (e.g. Groq Maverick limit 8192) - if status == 400 && body.contains("max_tokens") && attempt < max_retries { + if status == 400 && (body.contains("max_tokens") || body.contains("context length") || body.contains("context_length") || body.contains("maximum context") || body.contains("too long") || body.contains("output tokens") || body.contains("max_completion_tokens")) && attempt < max_retries { let current = oai_request .max_tokens .or(oai_request.max_completion_tokens) @@ -992,7 +1021,7 @@ impl LlmDriver for OpenAIDriver { function: OaiToolDef { name: t.name.clone(), description: t.description.clone(), - parameters: openfang_types::tool::normalize_schema_for_provider( + parameters: omtae_types::tool::normalize_schema_for_provider( &t.input_schema, "openai", ), @@ -1000,11 +1029,7 @@ impl LlmDriver for OpenAIDriver { }) .collect(); - let tool_choice = if oai_tools.is_empty() { - None - } else { - Some(serde_json::json!("auto")) - }; + let tool_choice = tool_choice_for_tools(!oai_tools.is_empty(), &self.base_url); let (mt, mct) = if uses_completion_tokens(&request.model) { (None, Some(request.max_tokens)) @@ -1053,10 +1078,18 @@ impl LlmDriver for OpenAIDriver { req_builder = req_builder.header(k, v); } - let resp = req_builder - .send() - .await - .map_err(|e| LlmError::Http(e.to_string()))?; + let resp = match req_builder.send().await { + Ok(r) => r, + Err(e) => { + if attempt < max_retries { + let retry_ms = (attempt + 1) as u64 * 1000; + warn!(error = %e, attempt, retry_ms, "HTTP request send failed (stream), retrying"); + tokio::time::sleep(std::time::Duration::from_millis(retry_ms)).await; + continue; + } + return Err(LlmError::Http(e.to_string())); + } + }; let status = resp.status().as_u16(); if status == 429 { @@ -1119,7 +1152,7 @@ impl LlmDriver for OpenAIDriver { } // Auto-cap max_tokens when model rejects our value - if status == 400 && body.contains("max_tokens") && attempt < max_retries { + if status == 400 && (body.contains("max_tokens") || body.contains("context length") || body.contains("context_length") || body.contains("maximum context") || body.contains("too long") || body.contains("output tokens") || body.contains("max_completion_tokens")) && attempt < max_retries { let current = oai_request .max_tokens .or(oai_request.max_completion_tokens) @@ -1601,7 +1634,28 @@ fn extract_thinking_summary(thinking: &str) -> String { /// Parse Groq's `tool_use_failed` error and extract the tool call from `failed_generation`. /// Extract the max_tokens limit from an API error message. /// Looks for patterns like: `must be less than or equal to \`8192\`` -fn extract_max_tokens_limit(body: &str) -> Option { +pub(crate) fn extract_max_tokens_limit(body: &str) -> Option { + // Handle vLLM context length message: + // "This model's maximum context length is tokens. However, you requested output tokens and your prompt contains at least input tokens." + if let Some(idx) = body.find("maximum context length is ") { + let after_max = &body[idx + "maximum context length is ".len()..]; + if let Some(end_max) = after_max.find(' ') { + if let Ok(max_ctx) = after_max[..end_max].parse::() { + if let Some(idx_prompt) = body.find("prompt contains at least ") { + let after_prompt = &body[idx_prompt + "prompt contains at least ".len()..]; + if let Some(end_prompt) = after_prompt.find(' ') { + if let Ok(prompt_tokens) = after_prompt[..end_prompt].parse::() { + if max_ctx > prompt_tokens { + // Return the remaining tokens minus a small safety buffer (e.g. 16) + return Some(max_ctx.saturating_sub(prompt_tokens).saturating_sub(16)); + } + } + } + } + } + } + } + // Pattern: "must be <= `N`" or "must be less than or equal to `N`" let patterns = [ "less than or equal to `", @@ -1817,6 +1871,9 @@ mod tests { fn test_extract_max_tokens_limit() { let body = r#"max_tokens must be less than or equal to `8192`"#; assert_eq!(extract_max_tokens_limit(body), Some(8192)); + + let vllm_err = "This model's maximum context length is 32768 tokens. However, you requested 16384 output tokens and your prompt contains at least 16385 input tokens."; + assert_eq!(extract_max_tokens_limit(vllm_err), Some(32768 - 16385 - 16)); } #[test] diff --git a/crates/openfang-runtime/src/drivers/qwen_code.rs b/crates/openfang-runtime/src/drivers/qwen_code.rs index c68329e734..de0a31e597 100644 --- a/crates/openfang-runtime/src/drivers/qwen_code.rs +++ b/crates/openfang-runtime/src/drivers/qwen_code.rs @@ -7,7 +7,7 @@ use crate::llm_driver::{CompletionRequest, CompletionResponse, LlmDriver, LlmError, StreamEvent}; use async_trait::async_trait; -use openfang_types::message::{ContentBlock, Role, StopReason, TokenUsage}; +use omtae_types::message::{ContentBlock, Role, StopReason, TokenUsage}; use serde::Deserialize; use tokio::io::AsyncBufReadExt; use tracing::{debug, warn}; @@ -59,7 +59,7 @@ impl QwenCodeDriver { warn!( "Qwen Code driver: --yolo enabled. \ The CLI will not prompt for tool approvals. \ - OpenFang's own capability/RBAC system enforces access control." + OMTAE's own capability/RBAC system enforces access control." ); } @@ -450,7 +450,7 @@ mod tests { #[test] fn test_build_prompt_simple() { - use openfang_types::message::{Message, MessageContent}; + use omtae_types::message::{Message, MessageContent}; let request = CompletionRequest { model: "qwen-code/qwen3-coder".to_string(), diff --git a/crates/openfang-runtime/src/drivers/vertex.rs b/crates/openfang-runtime/src/drivers/vertex.rs index 9f04841634..86245b7fda 100644 --- a/crates/openfang-runtime/src/drivers/vertex.rs +++ b/crates/openfang-runtime/src/drivers/vertex.rs @@ -25,10 +25,10 @@ use crate::llm_driver::{CompletionRequest, CompletionResponse, LlmDriver, LlmError, StreamEvent}; use async_trait::async_trait; use futures::StreamExt; -use openfang_types::message::{ +use omtae_types::message::{ ContentBlock, Message, MessageContent, Role, StopReason, TokenUsage, }; -use openfang_types::tool::ToolCall; +use omtae_types::tool::ToolCall; use serde::{Deserialize, Serialize}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -421,7 +421,7 @@ fn convert_tools(request: &CompletionRequest) -> Vec { .iter() .map(|t| { let normalized = - openfang_types::tool::normalize_schema_for_provider(&t.input_schema, "gemini"); + omtae_types::tool::normalize_schema_for_provider(&t.input_schema, "gemini"); VertexFunctionDeclaration { name: t.name.clone(), description: t.description.clone(), diff --git a/crates/openfang-runtime/src/embedding.rs b/crates/openfang-runtime/src/embedding.rs index c3245d879c..d0d462dedd 100644 --- a/crates/openfang-runtime/src/embedding.rs +++ b/crates/openfang-runtime/src/embedding.rs @@ -5,7 +5,7 @@ //! Groq, Together, Fireworks, Ollama, etc.). use async_trait::async_trait; -use openfang_types::model_catalog::{ +use omtae_types::model_catalog::{ FIREWORKS_BASE_URL, GROQ_BASE_URL, LMSTUDIO_BASE_URL, MISTRAL_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL, TOGETHER_BASE_URL, VLLM_BASE_URL, }; @@ -92,11 +92,16 @@ impl OpenAIEmbeddingDriver { // Infer dimensions from model name (common models) let dims = infer_dimensions(&config.model); + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .unwrap_or_else(|_| reqwest::Client::new()); + Ok(Self { api_key: Zeroizing::new(config.api_key), base_url: config.base_url, model: config.model, - client: reqwest::Client::new(), + client, dims, }) } diff --git a/crates/openfang-runtime/src/graceful_shutdown.rs b/crates/openfang-runtime/src/graceful_shutdown.rs index dee47ad222..4aee285e97 100644 --- a/crates/openfang-runtime/src/graceful_shutdown.rs +++ b/crates/openfang-runtime/src/graceful_shutdown.rs @@ -1,6 +1,6 @@ //! Graceful shutdown — ordered subsystem teardown for clean exit. //! -//! When OpenFang receives a shutdown signal (SIGTERM, Ctrl+C, API call), this +//! When OMTAE receives a shutdown signal (SIGTERM, Ctrl+C, API call), this //! module orchestrates an ordered shutdown sequence to prevent data loss and //! ensure clean resource cleanup. //! diff --git a/crates/openfang-runtime/src/hooks.rs b/crates/openfang-runtime/src/hooks.rs index 7fb76b8935..83d28e1182 100644 --- a/crates/openfang-runtime/src/hooks.rs +++ b/crates/openfang-runtime/src/hooks.rs @@ -8,7 +8,7 @@ //! - `AgentLoopEnd`: Fires after the agent loop completes. Observe-only. use dashmap::DashMap; -use openfang_types::agent::HookEvent; +use omtae_types::agent::HookEvent; use std::sync::Arc; /// Context passed to hook handlers. diff --git a/crates/openfang-runtime/src/host_functions.rs b/crates/openfang-runtime/src/host_functions.rs index a3c3c29fb4..067ca5dfca 100644 --- a/crates/openfang-runtime/src/host_functions.rs +++ b/crates/openfang-runtime/src/host_functions.rs @@ -8,7 +8,7 @@ use crate::sandbox::GuestState; use crate::web_fetch; -use openfang_types::capability::{capability_matches, Capability}; +use omtae_types::capability::{capability_matches, Capability}; use serde_json::json; use std::path::{Component, Path}; use tracing::debug; @@ -392,7 +392,7 @@ fn host_agent_send(state: &GuestState, params: &serde_json::Value) -> serde_json }; match state .tokio_handle - .block_on(kernel.send_to_agent(target, message)) + .block_on(kernel.send_to_agent(target, message, None, None)) { Ok(response) => json!({"ok": response}), Err(e) => json!({"error": e}), diff --git a/crates/openfang-runtime/src/image_gen.rs b/crates/openfang-runtime/src/image_gen.rs index ce564b143e..cd979e527b 100644 --- a/crates/openfang-runtime/src/image_gen.rs +++ b/crates/openfang-runtime/src/image_gen.rs @@ -1,7 +1,7 @@ //! Image generation — DALL-E 3, DALL-E 2, GPT-Image-1 via OpenAI API. use base64::Engine; -use openfang_types::media::{GeneratedImage, ImageGenRequest, ImageGenResult}; +use omtae_types::media::{GeneratedImage, ImageGenRequest, ImageGenResult}; use tracing::warn; /// Generate images via OpenAI's image generation API. @@ -34,7 +34,7 @@ pub async fn generate_image( }); // DALL-E 3 specific fields - if request.model == openfang_types::media::ImageGenModel::DallE3 { + if request.model == omtae_types::media::ImageGenModel::DallE3 { body["quality"] = serde_json::json!(request.quality); } @@ -165,7 +165,7 @@ pub fn save_images_to_workspace( #[cfg(test)] mod tests { use super::*; - use openfang_types::media::ImageGenModel; + use omtae_types::media::ImageGenModel; #[test] fn test_validate_valid_request() { diff --git a/crates/openfang-runtime/src/kernel_handle.rs b/crates/openfang-runtime/src/kernel_handle.rs index ec57efe1f9..714c5884dc 100644 --- a/crates/openfang-runtime/src/kernel_handle.rs +++ b/crates/openfang-runtime/src/kernel_handle.rs @@ -1,6 +1,6 @@ //! Trait abstraction for kernel operations needed by the agent runtime. //! -//! This trait allows `openfang-runtime` to call back into the kernel for +//! This trait allows `omtae-runtime` to call back into the kernel for //! inter-agent operations (spawn, send, list, kill) without creating //! a circular dependency. The kernel implements this trait and passes //! it into the agent loop. @@ -35,7 +35,14 @@ pub trait KernelHandle: Send + Sync { ) -> Result<(String, String), String>; /// Send a message to another agent and get the response. - async fn send_to_agent(&self, agent_id: &str, message: &str) -> Result; + /// `caller_id` / `caller_name` annotate the inbound turn for the target agent. + async fn send_to_agent( + &self, + agent_id: &str, + message: &str, + caller_id: Option<&str>, + caller_name: Option<&str>, + ) -> Result; /// List all running agents. fn list_agents(&self) -> Vec; @@ -89,20 +96,20 @@ pub trait KernelHandle: Send + Sync { /// Add an entity to the knowledge graph. async fn knowledge_add_entity( &self, - entity: openfang_types::memory::Entity, + entity: omtae_types::memory::Entity, ) -> Result; /// Add a relation to the knowledge graph. async fn knowledge_add_relation( &self, - relation: openfang_types::memory::Relation, + relation: omtae_types::memory::Relation, ) -> Result; /// Query the knowledge graph with a pattern. async fn knowledge_query( &self, - pattern: openfang_types::memory::GraphPattern, - ) -> Result, String>; + pattern: omtae_types::memory::GraphPattern, + ) -> Result, String>; /// Create a cron job for the calling agent. async fn cron_create( @@ -260,7 +267,7 @@ pub trait KernelHandle: Send + Sync { &self, manifest_toml: &str, parent_id: Option<&str>, - parent_caps: &[openfang_types::capability::Capability], + parent_caps: &[omtae_types::capability::Capability], ) -> Result<(String, String), String> { // Default: delegate to spawn_agent (no enforcement) // The kernel MUST override this with real enforcement diff --git a/crates/openfang-runtime/src/lib.rs b/crates/openfang-runtime/src/lib.rs index bde54ab199..9031688663 100644 --- a/crates/openfang-runtime/src/lib.rs +++ b/crates/openfang-runtime/src/lib.rs @@ -5,7 +5,7 @@ /// Default User-Agent header sent with all outgoing HTTP requests. /// Some LLM providers (e.g. Moonshot, Qwen) reject requests without one. -pub const USER_AGENT: &str = "openfang/0.3.48"; +pub const USER_AGENT: &str = "omtae/0.3.48"; pub mod a2a; pub mod agent_context; diff --git a/crates/openfang-runtime/src/link_understanding.rs b/crates/openfang-runtime/src/link_understanding.rs index 640cc2d56d..3b3231fffd 100644 --- a/crates/openfang-runtime/src/link_understanding.rs +++ b/crates/openfang-runtime/src/link_understanding.rs @@ -3,7 +3,7 @@ use tracing::warn; /// Configuration for link understanding (re-exported from types). -pub use openfang_types::media::LinkConfig; +pub use omtae_types::media::LinkConfig; /// Summary of a fetched link. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/crates/openfang-runtime/src/llm_driver.rs b/crates/openfang-runtime/src/llm_driver.rs index 500192c839..1adb83cf84 100644 --- a/crates/openfang-runtime/src/llm_driver.rs +++ b/crates/openfang-runtime/src/llm_driver.rs @@ -3,8 +3,8 @@ //! Abstracts over multiple LLM providers (Anthropic, OpenAI, Ollama, etc.). use async_trait::async_trait; -use openfang_types::message::{ContentBlock, Message, StopReason, TokenUsage}; -use openfang_types::tool::{ToolCall, ToolDefinition}; +use omtae_types::message::{ContentBlock, Message, StopReason, TokenUsage}; +use omtae_types::tool::{ToolCall, ToolDefinition}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -64,7 +64,7 @@ pub struct CompletionRequest { /// System prompt (extracted from messages for APIs that need it separately). pub system: Option, /// Extended thinking configuration (if supported by the model). - pub thinking: Option, + pub thinking: Option, } /// A response from an LLM completion. @@ -183,9 +183,9 @@ pub struct DriverConfig { /// Skip interactive permission prompts (Claude Code provider only). /// /// When `true`, adds `--dangerously-skip-permissions` to the spawned - /// `claude` CLI. Defaults to `true` because OpenFang runs as a daemon + /// `claude` CLI. Defaults to `true` because OMTAE runs as a daemon /// with no interactive terminal, so permission prompts would block - /// indefinitely. OpenFang's own capability / RBAC layer already + /// indefinitely. OMTAE's own capability / RBAC layer already /// restricts what agents can do, making this safe. #[serde(default = "default_skip_permissions")] pub skip_permissions: bool, diff --git a/crates/openfang-runtime/src/llm_errors.rs b/crates/openfang-runtime/src/llm_errors.rs index aee3218ab5..a025aa601a 100644 --- a/crates/openfang-runtime/src/llm_errors.rs +++ b/crates/openfang-runtime/src/llm_errors.rs @@ -2,7 +2,7 @@ //! //! Classifies raw LLM API errors into 8 categories using pattern matching //! against error messages and HTTP status codes. Handles error formats from -//! all 19+ providers OpenFang supports: Anthropic, OpenAI, Gemini, Groq, +//! all 19+ providers OMTAE supports: Anthropic, OpenAI, Gemini, Groq, //! DeepSeek, Mistral, Together, Fireworks, Ollama, vLLM, LM Studio, //! Perplexity, Cohere, AI21, Cerebras, SambaNova, HuggingFace, XAI, Replicate. //! diff --git a/crates/openfang-runtime/src/loop_guard.rs b/crates/openfang-runtime/src/loop_guard.rs index 74e73d95c4..ac0e7651d9 100644 --- a/crates/openfang-runtime/src/loop_guard.rs +++ b/crates/openfang-runtime/src/loop_guard.rs @@ -17,10 +17,315 @@ //! warnings for the same call. //! - **Statistics snapshot**: exposes internal state for debugging and API. +use omtae_types::message::{ContentBlock, Message, MessageContent, Role}; use serde::Serialize; use sha2::{Digest, Sha256}; use std::collections::{HashMap, HashSet}; +/// Injected when the agent repeats the same planning text without tool calls. +pub const PLANNING_LOOP_NUDGE: &str = "[System: STOP PLANNING — call tools now. You repeated the same planning text without executing tools. Use shell_exec, file_list, memory_recall, or web_search immediately. Do not reply with more planning prose.]"; + +/// Injected on first planning-only response (no tools yet). +pub const PLANNING_WITHOUT_TOOLS_NUDGE: &str = "[System: You wrote planning text without calling tools. Execute tools NOW (shell_exec, file_list, memory_recall). Max one planning line, then a tool call. Never claim workspace/memory status without tool output.]"; + +/// Injected when the agent states factual claims without any tool evidence. +pub const TOOL_REQUIRED_NUDGE: &str = "[System: TOOL_REQUIRED — You stated factual claims (paths, URLs, status, lists, or counts) without tool evidence in this conversation. Call shell_exec, file_list, web_search, memory_recall, or curl /api/brain/* NOW. Do not guess workspace, brain vault, peer agents, or service status.]"; + +/// Outcome when the agent loops on planning text without tool calls. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanningLoopAction { + Reprompt(&'static str), + ForceEnd(String), +} + +/// Extract visible text from an assistant message for repetition checks. +pub fn assistant_message_text(msg: &Message) -> String { + match &msg.content { + MessageContent::Text(t) => t.clone(), + MessageContent::Blocks(blocks) => blocks + .iter() + .filter_map(|b| match b { + ContentBlock::Text { text, .. } => Some(text.as_str()), + _ => None, + }) + .collect::>() + .join(" "), + } +} + +/// Normalize assistant text to a prefix for repetition detection. +pub fn assistant_text_prefix(text: &str) -> String { + let t = text.trim().to_lowercase(); + let end = t.find('\n').unwrap_or(t.len()).min(80); + t[..end].trim().to_string() +} + +/// Topic stem for fuzzy planning-loop detection (same intent, wording drifts). +pub fn assistant_planning_stem(text: &str) -> String { + let t = text.trim().to_lowercase(); + if t.contains("obsidian") && t.contains("brain") { + return "topic:obsidian-brain".to_string(); + } + if t.contains("obsidian") { + return "topic:obsidian".to_string(); + } + for marker in [ + " - let me ", + " let me ", + " i'll ", + " i will ", + " looking at ", + " let me also ", + ] { + if let Some(idx) = t.find(marker) { + let stem = t[..idx].trim(); + if stem.len() >= 12 { + return stem.to_string(); + } + } + } + assistant_text_prefix(text) +} + +/// Count prior assistant turns whose opening prefix matches `prefix`. +pub fn count_matching_assistant_prefix(messages: &[Message], prefix: &str) -> u32 { + if prefix.len() < 15 { + return 0; + } + messages + .iter() + .filter(|m| m.role == Role::Assistant) + .map(|m| assistant_text_prefix(&assistant_message_text(m))) + .filter(|p| p == prefix) + .count() as u32 +} + +/// Count prior assistant turns with the same planning stem (fuzzy). +pub fn count_matching_planning_stem(messages: &[Message], stem: &str) -> u32 { + if stem.len() < 8 && !stem.starts_with("topic:") { + return 0; + } + messages + .iter() + .filter(|m| m.role == Role::Assistant) + .map(|m| assistant_planning_stem(&assistant_message_text(m))) + .filter(|s| s == stem) + .count() as u32 +} + +/// Count prior assistant turns that are planning prose without tools. +pub fn count_prior_planning_prose(messages: &[Message]) -> u32 { + messages + .iter() + .filter(|m| m.role == Role::Assistant) + .filter(|m| planning_without_tools_detected(&assistant_message_text(m))) + .count() as u32 +} + +/// True when the assistant turn includes ToolResult blocks from prior tool calls. +pub fn has_tool_results_in_context(messages: &[Message]) -> bool { + messages.iter().any(|m| { + matches!( + &m.content, + MessageContent::Blocks(blocks) + if blocks + .iter() + .any(|b| matches!(b, ContentBlock::ToolResult { .. })) + ) + }) +} + +/// Detect meta-narration about the user's intent (planning without action). +pub fn meta_narration_detected(text: &str) -> bool { + let lower = text.to_lowercase(); + [ + "the user is asking about", + "the user wants", + "the user is asking", + "based on the user's question", + "the user mentioned", + ] + .iter() + .any(|p| lower.contains(p)) +} + +/// Count assistant turns with meta-narration in this loop (including current text). +pub fn count_meta_narration(messages: &[Message], current: &str) -> u32 { + let prior = messages + .iter() + .filter(|m| m.role == Role::Assistant) + .filter(|m| meta_narration_detected(&assistant_message_text(m))) + .count() as u32; + prior + u32::from(meta_narration_detected(current)) +} + +/// Detect factual claims that require tool backing (URLs, paths, status, lists, counts). +pub fn contains_factual_claims(text: &str) -> bool { + let lower = text.to_lowercase(); + + if text.contains("https://") || text.contains("http://") { + return true; + } + + if text.contains("/home/") + || text.contains("~/.") + || text.contains("/etc/") + || text.contains("/api/brain/") + || lower.contains("vault at ") + { + return true; + } + + let status_patterns = [ + "is running", + "is healthy", + "status: ok", + "status: warning", + "status: critical", + "peer agent", + "agents listed", + "connected peers", + "daemon is", + "service is", + "vault contains", + "brain status", + "obsidian vault", + "found ", + "there are ", + "currently running", + "currently active", + ]; + if status_patterns.iter().any(|p| lower.contains(p)) { + return true; + } + + let numbered = text + .lines() + .filter(|line| { + let trimmed = line.trim_start(); + trimmed + .chars() + .next() + .is_some_and(|c| c.is_ascii_digit()) + && (trimmed.contains(". ") || trimmed.contains(") ")) + }) + .count(); + if numbered >= 3 { + return true; + } + + let words: Vec<&str> = lower.split_whitespace().collect(); + for window in words.windows(2) { + if let [a, b] = window { + if a.chars().all(|c| c.is_ascii_digit()) + && matches!( + *b, + "agents" | "files" | "peers" | "items" | "results" | "skills" | "agents." + ) + { + return true; + } + } + } + + false +} + +/// ECC-inspired verification gate: block factual claims without tool evidence. +pub fn check_verification_gate( + messages: &[Message], + text: &str, + any_tools_executed: bool, + verification_reprompts: u32, +) -> Option { + if text.trim().is_empty() { + return None; + } + if any_tools_executed || has_tool_results_in_context(messages) { + return None; + } + if !contains_factual_claims(text) { + return None; + } + if verification_reprompts < 2 { + return Some(PlanningLoopAction::Reprompt(TOOL_REQUIRED_NUDGE)); + } + Some(PlanningLoopAction::ForceEnd( + "TOOL_REQUIRED: Stopped — factual claims without tool evidence. Use shell_exec, file_list, web_search, or curl /api/brain/status.".to_string(), + )) +} + +/// Detect planning prose without tool execution (first-offense nudge). +pub fn planning_without_tools_detected(text: &str) -> bool { + let lower = text.to_lowercase(); + [ + "let me check", + "let me also check", + "i'll check", + "i will check", + "checking the workspace", + "checking workspace", + "check workspace and memory", + "let me look", + "let me look around", + "i need to check", + "first i'll", + "first i will", + "looking at the peer", + "peer agents listed", + "obsidian vault", + "obsidian skill", + "related setup", + "related files", + "the user is asking about", + ] + .iter() + .any(|p| lower.contains(p)) +} + +/// Detect repeated planning text (same prefix 3x) or planning without tools. +pub fn check_planning_loop( + messages: &[Message], + text: &str, + any_tools_executed: bool, + planning_reprompts: u32, +) -> Option { + if any_tools_executed || text.trim().is_empty() { + return None; + } + let prefix = assistant_text_prefix(text); + let stem = assistant_planning_stem(text); + let exact_repeat = count_matching_assistant_prefix(messages, &prefix) + 1; + let stem_repeat = count_matching_planning_stem(messages, &stem) + 1; + let planning_prose_repeat = count_prior_planning_prose(messages) + 1; + let meta_repeat = count_meta_narration(messages, text); + let repeat_count = exact_repeat + .max(stem_repeat) + .max(planning_prose_repeat) + .max(if meta_repeat >= 2 { meta_repeat } else { 0 }); + + if repeat_count >= 2 { + if planning_reprompts < 1 { + return Some(PlanningLoopAction::Reprompt(PLANNING_LOOP_NUDGE)); + } + return Some(PlanningLoopAction::ForceEnd(format!( + "Stopped after repeating the same planning text {repeat_count} times without tool calls. \ + Run shell_exec or file_list on your next message." + ))); + } + if planning_without_tools_detected(text) { + if planning_reprompts < 2 { + return Some(PlanningLoopAction::Reprompt(PLANNING_WITHOUT_TOOLS_NUDGE)); + } + return Some(PlanningLoopAction::ForceEnd( + "Stopped after planning prose without tool calls. Use shell_exec (curl /api/brain/status, \ + file_list) or web_search — do not repeat planning." + .to_string(), + )); + } + None +} + /// Tools that are expected to be polled repeatedly. const POLL_TOOLS: &[&str] = &[ "shell_exec", // checking command output @@ -51,6 +356,8 @@ pub struct LoopGuardConfig { pub ping_pong_min_repeats: u32, /// Max warnings per unique tool call hash before upgrading to Block. pub max_warnings_per_call: u32, + /// Cap `agent_send` calls per agent loop (orchestrator / meta-agents). + pub max_agent_send_per_loop: Option, } impl Default for LoopGuardConfig { @@ -64,6 +371,7 @@ impl Default for LoopGuardConfig { outcome_block_threshold: 3, ping_pong_min_repeats: 3, max_warnings_per_call: 3, + max_agent_send_per_loop: None, } } } @@ -119,6 +427,8 @@ pub struct LoopGuard { blocked_calls: u32, /// Map from call hash to tool name (for stats reporting). hash_to_tool: HashMap, + /// `agent_send` invocations in this loop (for delegation caps). + agent_send_calls: u32, } impl LoopGuard { @@ -135,6 +445,7 @@ impl LoopGuard { poll_counts: HashMap::new(), blocked_calls: 0, hash_to_tool: HashMap::new(), + agent_send_calls: 0, } } @@ -156,6 +467,21 @@ impl LoopGuard { )); } + // Per-turn delegation cap (orchestrator agent_send storms) + if tool_name == "agent_send" { + self.agent_send_calls += 1; + if let Some(max) = self.config.max_agent_send_per_loop { + if self.agent_send_calls > max { + self.blocked_calls += 1; + return LoopGuardVerdict::CircuitBreak(format!( + "Delegation limit: agent_send called {} times (max {max} per turn). \ + Synthesize results from specialists already contacted and reply to the user.", + self.agent_send_calls + )); + } + } + } + let hash = Self::compute_hash(tool_name, params); self.hash_to_tool .entry(hash.clone()) @@ -183,13 +509,18 @@ impl LoopGuard { // Determine effective thresholds (poll tools get relaxed thresholds) let is_poll = Self::is_poll_call(tool_name, params); - let multiplier = if is_poll { - self.config.poll_multiplier + let is_delegate = matches!(tool_name, "agent_send" | "agent_list" | "agent_spawn"); + let (effective_warn, effective_block) = if is_delegate { + (2, 4) + } else if is_poll { + let m = self.config.poll_multiplier; + ( + self.config.warn_threshold * m, + self.config.block_threshold * m, + ) } else { - 1 + (self.config.warn_threshold, self.config.block_threshold) }; - let effective_warn = self.config.warn_threshold * multiplier; - let effective_block = self.config.block_threshold * multiplier; // Check per-hash thresholds if count_val >= effective_block { @@ -946,4 +1277,121 @@ mod tests { assert_eq!(stats.total_calls, 50); assert_eq!(stats.unique_calls, 50); } + + // ======================================================================== + // Planning text loop detection + // ======================================================================== + + #[test] + fn test_planning_without_tools_nudge() { + let text = "Let me check workspace and memory first."; + let action = check_planning_loop(&[], text, false, 0); + assert!(matches!( + action, + Some(PlanningLoopAction::Reprompt(PLANNING_WITHOUT_TOOLS_NUDGE)) + )); + } + + #[test] + fn test_planning_loop_same_prefix_second_time() { + let msg = |t: &str| Message::assistant(t.to_string()); + let messages = vec![msg("Let me check workspace and memory for status.")]; + let action = check_planning_loop( + &messages, + "Let me check workspace and memory for status.", + false, + 0, + ); + assert!(matches!( + action, + Some(PlanningLoopAction::Reprompt(PLANNING_LOOP_NUDGE)) + )); + } + + #[test] + fn test_planning_loop_force_end_after_nudge() { + let msg = |t: &str| Message::assistant(t.to_string()); + let messages = vec![ + msg("Let me check workspace and memory for status."), + msg("Let me check workspace and memory for status."), + ]; + let action = check_planning_loop( + &messages, + "Let me check workspace and memory for status.", + false, + 1, + ); + assert!(matches!(action, Some(PlanningLoopAction::ForceEnd(_)))); + } + + #[test] + fn test_planning_loop_skipped_after_tools() { + let action = check_planning_loop( + &[], + "Let me check workspace and memory.", + true, + 0, + ); + assert!(action.is_none()); + } + + #[test] + fn test_obsidian_brain_stem_catches_wording_drift() { + let msg = |t: &str| Message::assistant(t.to_string()); + let messages = vec![msg( + "The user is asking about their Obsidian visual brain - let me check peer agents.", + )]; + let action = check_planning_loop( + &messages, + "The user is asking about their Obsidian visual brain - let me look around for vault files.", + false, + 0, + ); + assert!(matches!( + action, + Some(PlanningLoopAction::Reprompt(PLANNING_LOOP_NUDGE)) + )); + assert_eq!( + assistant_planning_stem("Obsidian visual brain - let me check"), + "topic:obsidian-brain" + ); + } + + #[test] + fn test_verification_gate_blocks_unbacked_claims() { + let text = "There are 5 peer agents listed. The brain vault at /home/jay/vaults/omtae-brain is running."; + let action = check_verification_gate(&[], text, false, 0); + assert!(matches!( + action, + Some(PlanningLoopAction::Reprompt(TOOL_REQUIRED_NUDGE)) + )); + } + + #[test] + fn test_verification_gate_allows_after_tools() { + let text = "There are 5 peer agents listed."; + let action = check_verification_gate(&[], text, true, 0); + assert!(action.is_none()); + } + + #[test] + fn test_verification_gate_allows_opinion_without_facts() { + let text = "Happy to help with your question about Obsidian."; + let action = check_verification_gate(&[], text, false, 0); + assert!(action.is_none()); + } + + #[test] + fn test_meta_narration_counts_toward_loop() { + let msg = |t: &str| Message::assistant(t.to_string()); + let messages = vec![msg("The user is asking about their brain vault.")]; + assert_eq!(count_meta_narration(&messages, "The user wants Obsidian info."), 2); + } + + #[test] + fn test_planning_force_end_after_two_nudges() { + let text = "Let me check the obsidian vault and related files."; + let action = check_planning_loop(&[], text, false, 2); + assert!(matches!(action, Some(PlanningLoopAction::ForceEnd(_)))); + } } diff --git a/crates/openfang-runtime/src/mcp.rs b/crates/openfang-runtime/src/mcp.rs index 13f5902845..87057dc3d8 100644 --- a/crates/openfang-runtime/src/mcp.rs +++ b/crates/openfang-runtime/src/mcp.rs @@ -8,7 +8,7 @@ //! All MCP tools are namespaced with `mcp_{server}_{tool}` to prevent collisions. use http::{HeaderName, HeaderValue}; -use openfang_types::tool::ToolDefinition; +use omtae_types::tool::ToolDefinition; use rmcp::model::{CallToolRequestParams, ClientCapabilities, ClientInfo, Implementation}; use rmcp::service::RunningService; use rmcp::{RoleClient, ServiceExt}; @@ -93,7 +93,7 @@ impl McpConnection { pub async fn connect(config: McpServerConfig) -> Result { let client_info = ClientInfo::new( ClientCapabilities::default(), - Implementation::new("openfang", env!("CARGO_PKG_VERSION")), + Implementation::new("omtae", env!("CARGO_PKG_VERSION")), ); let client = match &config.transport { diff --git a/crates/openfang-runtime/src/mcp_server.rs b/crates/openfang-runtime/src/mcp_server.rs index fc2cf7459a..770039bf84 100644 --- a/crates/openfang-runtime/src/mcp_server.rs +++ b/crates/openfang-runtime/src/mcp_server.rs @@ -1,12 +1,12 @@ -//! MCP Server — expose OpenFang tools via the Model Context Protocol. +//! MCP Server — expose OMTAE tools via the Model Context Protocol. //! //! Implements the server-side MCP protocol so external MCP clients -//! (Claude Desktop, VS Code, etc.) can use OpenFang's built-in tools. +//! (Claude Desktop, VS Code, etc.) can use OMTAE's built-in tools. //! //! This module provides a reusable handler function — the CLI team //! wires it into a stdio transport. -use openfang_types::tool::ToolDefinition; +use omtae_types::tool::ToolDefinition; use serde_json::json; /// MCP protocol version supported by this server. @@ -32,7 +32,7 @@ pub async fn handle_mcp_request( "tools": {} }, "serverInfo": { - "name": "openfang", + "name": "omtae", "version": env!("CARGO_PKG_VERSION") } }), @@ -181,6 +181,6 @@ mod tests { assert!(response["result"]["serverInfo"]["name"] .as_str() .unwrap() - .contains("openfang")); + .contains("omtae")); } } diff --git a/crates/openfang-runtime/src/media_understanding.rs b/crates/openfang-runtime/src/media_understanding.rs index b8ad7e6333..82e549cb42 100644 --- a/crates/openfang-runtime/src/media_understanding.rs +++ b/crates/openfang-runtime/src/media_understanding.rs @@ -2,7 +2,7 @@ //! //! Auto-cascades through available providers based on configured API keys. -use openfang_types::media::{ +use omtae_types::media::{ MediaAttachment, MediaConfig, MediaSource, MediaType, MediaUnderstanding, }; use std::sync::Arc; @@ -315,7 +315,7 @@ async fn transcribe_with_parakeet_mlx( _ => "wav", }; let path = std::env::temp_dir().join(format!( - "openfang_parakeet_{}.{}", + "omtae_parakeet_{}.{}", uuid::Uuid::new_v4(), ext )); @@ -427,7 +427,7 @@ fn default_audio_model(provider: &str) -> &str { #[cfg(test)] mod tests { use super::*; - use openfang_types::media::{MediaSource, MAX_IMAGE_BYTES}; + use omtae_types::media::{MediaSource, MAX_IMAGE_BYTES}; #[test] fn test_engine_creation() { diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index 4bf6fc7ec8..1aa646b78d 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -3,7 +3,7 @@ //! Provides a comprehensive catalog of 130+ builtin models across 28 providers, //! with alias resolution, auth status detection, and pricing lookups. -use openfang_types::model_catalog::{ +use omtae_types::model_catalog::{ AuthStatus, ModelCatalogEntry, ModelTier, ProviderInfo, AI21_BASE_URL, ANTHROPIC_BASE_URL, AZURE_OPENAI_BASE_URL, BEDROCK_BASE_URL, CEREBRAS_BASE_URL, CHUTES_BASE_URL, COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, GEMINI_BASE_URL, GITHUB_COPILOT_BASE_URL, GROQ_BASE_URL, @@ -77,12 +77,12 @@ impl ModelCatalog { // GitHub Copilot: check for persisted OAuth tokens if provider.id == "github-copilot" || provider.id == "copilot" { - let openfang_dir = std::env::var("HOME") + let omtae_dir = std::env::var("HOME") .or_else(|_| std::env::var("USERPROFILE")) - .map(|h| std::path::PathBuf::from(h).join(".openfang")) - .unwrap_or_else(|_| std::path::PathBuf::from(".openfang")); + .map(|h| std::path::PathBuf::from(h).join(".omtae")) + .unwrap_or_else(|_| std::path::PathBuf::from(".omtae")); provider.auth_status = - if crate::drivers::copilot::copilot_auth_available(&openfang_dir) { + if crate::drivers::copilot::copilot_auth_available(&omtae_dir) { AuthStatus::Configured } else { AuthStatus::Missing @@ -276,16 +276,25 @@ impl ModelCatalog { .collect() } - /// Return the default model ID for a provider (first model in catalog order). + /// Return the default model ID for a provider. + /// + /// Prefers real models over builtin placeholders like `vllm-local` / `lmstudio-local` + /// (used only until dynamic discovery or `custom_models.json` adds entries). pub fn default_model_for_provider(&self, provider: &str) -> Option { // Check aliases first — e.g. "minimax" alias resolves to "MiniMax-M2.5" if let Some(model_id) = self.aliases.get(provider) { return Some(model_id.clone()); } - // Fall back to the first model registered for this provider - self.models + let provider_models: Vec<&ModelCatalogEntry> = self + .models .iter() - .find(|m| m.provider == provider) + .filter(|m| m.provider == provider) + .collect(); + // Prefer custom/discovered models over *-local catalog placeholders + provider_models + .iter() + .find(|m| !m.id.ends_with("-local")) + .or_else(|| provider_models.first()) .map(|m| m.id.clone()) } @@ -2592,8 +2601,8 @@ fn builtin_models() -> Vec { // vLLM (1) — generic local entry + dynamic discovery // ══════════════════════════════════════════════════════════════ ModelCatalogEntry { - id: "vllm-local".into(), - display_name: "vLLM Local Model".into(), + id: "Qwen2.5-Coder-32B-Instruct-AWQ".into(), + display_name: "Qwen2.5 Coder 32B AWQ (vLLM)".into(), provider: "vllm".into(), tier: ModelTier::Local, context_window: 32_768, @@ -2603,7 +2612,7 @@ fn builtin_models() -> Vec { supports_tools: true, supports_vision: false, supports_streaming: true, - aliases: vec![], + aliases: vec!["vllm-local".into(), "local-model".into()], }, // ══════════════════════════════════════════════════════════════ // LM Studio (1) — generic local entry + dynamic discovery diff --git a/crates/openfang-runtime/src/prompt_builder.rs b/crates/openfang-runtime/src/prompt_builder.rs index dd28ae396e..de82b19361 100644 --- a/crates/openfang-runtime/src/prompt_builder.rs +++ b/crates/openfang-runtime/src/prompt_builder.rs @@ -87,6 +87,12 @@ pub fn build_system_prompt(ctx: &PromptContext) -> String { // Section 2 — Tool Call Behavior (skip for subagents) if !ctx.is_subagent { sections.push(TOOL_CALL_BEHAVIOR.to_string()); + if !ctx.granted_tools.is_empty() { + sections.push(TOOL_FIRST_RULE.to_string()); + } + if ctx.granted_tools.iter().any(|t| t == "agent_send") { + sections.push(AGENT_DELEGATION_BEHAVIOR.to_string()); + } } // Section 2.5 — Agent Behavioral Guidelines (skip for subagents) @@ -104,6 +110,16 @@ pub fn build_system_prompt(ctx: &PromptContext) -> String { sections.push(tools_section); } + // Section 3.5 — Research integrity (agents with web tools) + if !ctx.is_subagent + && ctx + .granted_tools + .iter() + .any(|t| t == "web_search" || t == "web_fetch") + { + sections.push(RESEARCH_INTEGRITY.to_string()); + } + // Section 4 — Memory Protocol (always present) let mem_section = build_memory_section(&ctx.recalled_memories); sections.push(mem_section); @@ -232,7 +248,7 @@ pub fn build_system_prompt(ctx: &PromptContext) -> String { fn build_identity_section(ctx: &PromptContext) -> String { if ctx.base_system_prompt.is_empty() { format!( - "You are {}, an AI agent running inside the OpenFang Agent OS.\n{}", + "You are {}, an AI agent running inside the OMTAE Agent OS.\n{}", ctx.agent_name, ctx.agent_description ) } else { @@ -240,6 +256,14 @@ fn build_identity_section(ctx: &PromptContext) -> String { } } +/// Tool-first discipline (ECC-inspired): verify before claiming facts. +const TOOL_FIRST_RULE: &str = "\ +## Tool-First Rule (mandatory) +- NEVER state file paths, URLs, service status, peer-agent lists, vault contents, or counts without tool output in this conversation. +- On status/brain/vault/workspace questions: FIRST action is a tool call (shell_exec, file_list, web_search, memory_recall, or curl /api/brain/*). +- Max ONE short planning line per turn, then IMMEDIATE tool call. Forbidden: \"The user is asking about…\" meta-narration loops. +- If you lack tool evidence, say \"I need to check\" and call a tool — do not guess or narrate peer agents."; + /// Static tool-call behavior directives. const TOOL_CALL_BEHAVIOR: &str = "\ ## Tool Call Behavior @@ -256,6 +280,26 @@ without sharing what you found. execute it via the appropriate tool call (shell_exec, file_write, etc.). Never output commands as \ code blocks — always call the tool instead."; +/// Anti-hallucination rules for agents with web_search / web_fetch. +const RESEARCH_INTEGRITY: &str = "\ +## Research Integrity (mandatory when using web tools) +- Before listing businesses, people, products, or ranked recommendations, call web_search and/or web_fetch first. +- NEVER invent business names, addresses, phone numbers, or websites. Every named entity must come from tool output. +- Each list item MUST include a real https:// URL from web_search or web_fetch results — not a placeholder like \"Website: Business Name\". +- Do not repeat the same entity twice. Deduplicate by name and URL before responding. +- Do NOT claim \"Google Search Results\" or cite search engines unless web_search returned those results in this conversation. +- Do NOT write \"Confidence Level: High\" unless every listed fact has a cited https:// URL from tool output. +- If tools fail or return no results, say \"could not verify\" — never fabricate top-N lists to fill the gap."; + +/// Delegation rules for agents with `agent_send` (vLLM/Qwen often emit fake JSON tools). +const AGENT_DELEGATION_BEHAVIOR: &str = "\ +## Agent Delegation (mandatory when using agent_send) +- NEVER write JSON in plain text such as `{\"name\":\"researcher\",...}` or `{\"name\":\"coder\",...}` — those are not tools. +- ALWAYS use the built-in `agent_send` function with parameters `agent_id` and `message`. +- Workflow: call `agent_list` once → copy the target's `id` (UUID) from the list → `agent_send(agent_id=\"\", message=\"...\")`. +- Specialist names (researcher, coder, analyst) are agents, not tool names. Delegate only via `agent_send`. +- Do not claim a specialist replied unless the tool result starts with `agent_send OK`."; + /// Build the grouped tools section (Section 3). pub fn build_tools_section(granted_tools: &[String]) -> String { if granted_tools.is_empty() { @@ -473,7 +517,7 @@ fn build_peer_agents_section(self_name: &str, peers: &[(String, String, String)] out.push_str(&format!("- **{}** ({}) — model: {}\n", name, state, model)); } out.push_str( - "\nYou can communicate with them using `agent_send` (by name) and see all agents with `agent_list`. \ + "\nYou can communicate with them using `agent_send` (use agent_id UUID from `agent_list`) and see all agents with `agent_list`. \ Delegate tasks to specialized agents when appropriate.", ); out @@ -577,7 +621,7 @@ pub fn tool_hint(name: &str) -> &'static str { "memory_list" => "list stored memory keys", // Agents - "agent_send" => "send a message to another agent", + "agent_send" => "send a message to another agent (agent_id UUID from agent_list + message)", "agent_spawn" => "create a new agent", "agent_list" => "list running agents", "agent_kill" => "terminate an agent", @@ -1007,6 +1051,34 @@ mod tests { assert_eq!(result, "👋🌍🚀..."); } + #[test] + fn test_research_integrity_section_for_web_tools() { + let prompt = build_system_prompt(&basic_ctx()); + assert!(prompt.contains("## Research Integrity")); + assert!(prompt.contains("could not verify")); + } + + #[test] + fn test_research_integrity_omitted_without_web_tools() { + let ctx = PromptContext { + agent_name: "coder".to_string(), + agent_description: "Coder".to_string(), + base_system_prompt: "You are Coder.".to_string(), + granted_tools: vec!["file_read".to_string(), "shell_exec".to_string()], + ..Default::default() + }; + let prompt = build_system_prompt(&ctx); + assert!(!prompt.contains("## Research Integrity")); + } + + #[test] + fn test_research_integrity_omitted_for_subagent() { + let mut ctx = basic_ctx(); + ctx.is_subagent = true; + let prompt = build_system_prompt(&ctx); + assert!(!prompt.contains("## Research Integrity")); + } + #[test] fn test_capitalize() { assert_eq!(capitalize("files"), "Files"); diff --git a/crates/openfang-runtime/src/provider_health.rs b/crates/openfang-runtime/src/provider_health.rs index 0e0b5eaec5..fcffdc86d3 100644 --- a/crates/openfang-runtime/src/provider_health.rs +++ b/crates/openfang-runtime/src/provider_health.rs @@ -96,7 +96,13 @@ impl Default for ProbeCache { /// /// `base_url` should be the provider's base URL from the catalog (e.g., /// `http://localhost:11434/v1` for Ollama, `http://localhost:8000/v1` for vLLM). -pub async fn probe_provider(provider: &str, base_url: &str) -> ProbeResult { +/// +/// When `api_key` is set, sends `Authorization: Bearer …` (required by some vLLM deployments). +pub async fn probe_provider( + provider: &str, + base_url: &str, + api_key: Option<&str>, +) -> ProbeResult { let start = Instant::now(); let client = match reqwest::Client::builder() @@ -129,7 +135,11 @@ pub async fn probe_provider(provider: &str, base_url: &str) -> ProbeResult { (format!("{trimmed}/models"), false) }; - let resp = match client.get(&url).send().await { + let mut req = client.get(&url); + if let Some(key) = api_key.filter(|k| !k.is_empty()) { + req = req.header("Authorization", format!("Bearer {key}")); + } + let resp = match req.send().await { Ok(r) => r, Err(e) => { return ProbeResult { @@ -204,12 +214,13 @@ pub async fn probe_provider(provider: &str, base_url: &str) -> ProbeResult { pub async fn probe_provider_cached( provider: &str, base_url: &str, + api_key: Option<&str>, cache: &ProbeCache, ) -> ProbeResult { if let Some(cached) = cache.get(provider) { return cached; } - let result = probe_provider(provider, base_url).await; + let result = probe_provider(provider, base_url, api_key).await; cache.insert(provider, result.clone()); result } @@ -302,7 +313,7 @@ mod tests { #[tokio::test] async fn test_probe_unreachable_returns_error() { // Probe a port that's almost certainly not running a server - let result = probe_provider("ollama", "http://127.0.0.1:19999").await; + let result = probe_provider("ollama", "http://127.0.0.1:19999", None).await; assert!(!result.reachable); assert!(result.error.is_some()); } diff --git a/crates/openfang-runtime/src/python_runtime.rs b/crates/openfang-runtime/src/python_runtime.rs index a338e3dbc3..5341fc3dd0 100644 --- a/crates/openfang-runtime/src/python_runtime.rs +++ b/crates/openfang-runtime/src/python_runtime.rs @@ -15,7 +15,7 @@ //! {"type": "response", "text": "...", "tool_calls": [...]} //! ``` //! -//! The Python SDK (`openfang_sdk.py`) provides a helper to handle this protocol. +//! The Python SDK (`omtae_sdk.py`) provides a helper to handle this protocol. use std::path::Path; use std::process::Stdio; diff --git a/crates/openfang-runtime/src/retry.rs b/crates/openfang-runtime/src/retry.rs index 4fdaae236a..987b569990 100644 --- a/crates/openfang-runtime/src/retry.rs +++ b/crates/openfang-runtime/src/retry.rs @@ -2,7 +2,7 @@ //! //! Provides a configurable, async-aware retry utility that can be used for //! LLM API calls, network operations, channel message delivery, and any -//! other fallible async operation across the OpenFang codebase. +//! other fallible async operation across the OMTAE codebase. //! //! Jitter uses `std::time::SystemTime` UNIX nanos as a seed to avoid //! requiring the `rand` crate as a dependency. diff --git a/crates/openfang-runtime/src/routing.rs b/crates/openfang-runtime/src/routing.rs index bf8086a0bd..d1e7305a63 100644 --- a/crates/openfang-runtime/src/routing.rs +++ b/crates/openfang-runtime/src/routing.rs @@ -5,7 +5,7 @@ //! model that can handle the task. use crate::llm_driver::CompletionRequest; -use openfang_types::agent::ModelRoutingConfig; +use omtae_types::agent::ModelRoutingConfig; /// Task complexity tier. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -166,8 +166,8 @@ impl ModelRouter { #[cfg(test)] mod tests { use super::*; - use openfang_types::message::{Message, MessageContent, Role}; - use openfang_types::tool::ToolDefinition; + use omtae_types::message::{Message, MessageContent, Role}; + use omtae_types::tool::ToolDefinition; fn default_config() -> ModelRoutingConfig { ModelRoutingConfig { diff --git a/crates/openfang-runtime/src/sandbox.rs b/crates/openfang-runtime/src/sandbox.rs index d8aced6359..4dccfea715 100644 --- a/crates/openfang-runtime/src/sandbox.rs +++ b/crates/openfang-runtime/src/sandbox.rs @@ -16,7 +16,7 @@ //! //! # Host ABI //! -//! The host provides (in the `"openfang"` import module): +//! The host provides (in the `"omtae"` import module): //! - `host_call(request_ptr: i32, request_len: i32) -> i64` — RPC dispatch //! - `host_log(level: i32, msg_ptr: i32, msg_len: i32)` — logging //! @@ -25,7 +25,7 @@ use crate::host_functions; use crate::kernel_handle::KernelHandle; -use openfang_types::capability::Capability; +use omtae_types::capability::Capability; use std::sync::Arc; use tracing::debug; use wasmtime::*; @@ -281,14 +281,14 @@ impl WasmSandbox { }) } - /// Register host function imports in the linker ("openfang" module). + /// Register host function imports in the linker ("omtae" module). fn register_host_functions(linker: &mut Linker) -> Result<(), SandboxError> { // host_call: single dispatch for all capability-checked operations. // Request: JSON bytes in guest memory → {"method": "...", "params": {...}} // Response: packed (ptr, len) pointing to JSON in guest memory. linker .func_wrap( - "openfang", + "omtae", "host_call", |mut caller: Caller<'_, GuestState>, request_ptr: i32, @@ -357,7 +357,7 @@ impl WasmSandbox { // host_log: lightweight logging — no capability check required. linker .func_wrap( - "openfang", + "omtae", "host_log", |mut caller: Caller<'_, GuestState>, level: i32, @@ -449,7 +449,7 @@ mod tests { /// Proxy module: forwards input to host_call and returns the response. const HOST_CALL_PROXY_WAT: &str = r#" (module - (import "openfang" "host_call" (func $host_call (param i32 i32) (result i64))) + (import "omtae" "host_call" (func $host_call (param i32 i32) (result i64))) (memory (export "memory") 2) (global $bump (mut i32) (i32.const 1024)) diff --git a/crates/openfang-runtime/src/session_repair.rs b/crates/openfang-runtime/src/session_repair.rs index a4af9b139e..6149c46a44 100644 --- a/crates/openfang-runtime/src/session_repair.rs +++ b/crates/openfang-runtime/src/session_repair.rs @@ -11,7 +11,7 @@ //! - Consecutive same-role messages (Anthropic API requires alternation) //! - Oversized or potentially malicious tool result content -use openfang_types::message::{ContentBlock, Message, MessageContent, Role}; +use omtae_types::message::{ContentBlock, Message, MessageContent, Role}; use std::collections::{HashMap, HashSet}; use tracing::{debug, warn}; @@ -224,6 +224,32 @@ pub fn ensure_starts_with_user(mut messages: Vec) -> Vec { messages } +fn message_has_user_text(msg: &Message) -> bool { + if msg.role != Role::User { + return false; + } + match &msg.content { + MessageContent::Text(t) => !t.trim().is_empty(), + MessageContent::Blocks(blocks) => blocks.iter().any(|b| { + matches!(b, ContentBlock::Text { text, .. } if !text.trim().is_empty()) + }), + _ => false, + } +} + +/// Ensure at least one user message with non-empty text exists. +/// +/// Qwen/vLLM chat templates error with "No user query found in messages" when +/// history contains only assistant/tool turns (e.g. after aggressive trim/repair). +pub fn ensure_has_user_text(mut messages: Vec) -> Vec { + if messages.iter().any(message_has_user_text) { + return messages; + } + warn!("No user text in message history — injecting placeholder for provider compatibility"); + messages.insert(0, Message::user("Hello")); + messages +} + /// Phase 2b: Reorder misplaced ToolResults -- ensure each result follows its use. /// /// Builds a map of tool_use_id to the index of the assistant message containing it. diff --git a/crates/openfang-runtime/src/subprocess_sandbox.rs b/crates/openfang-runtime/src/subprocess_sandbox.rs index 673d5d6cfc..a967e0d7c3 100644 --- a/crates/openfang-runtime/src/subprocess_sandbox.rs +++ b/crates/openfang-runtime/src/subprocess_sandbox.rs @@ -115,7 +115,7 @@ pub fn validate_executable_path(path: &str) -> Result<(), String> { // Shell/exec allowlisting // --------------------------------------------------------------------------- -use openfang_types::config::{ExecPolicy, ExecSecurityMode}; +use omtae_types::config::{ExecPolicy, ExecSecurityMode}; /// SECURITY: Check for shell metacharacters that enable command injection. /// @@ -553,7 +553,7 @@ async fn kill_tree_windows(pid: u32, grace_ms: u64) -> Result { /// Kill a tokio child process with tree kill. /// /// Extracts the PID from the `Child` handle and performs a tree kill. -/// This is the preferred way to clean up subprocesses spawned by OpenFang. +/// This is the preferred way to clean up subprocesses spawned by OMTAE. pub async fn kill_child_tree( child: &mut tokio::process::Child, grace_ms: u64, @@ -596,7 +596,7 @@ pub async fn wait_or_kill_with_idle( absolute_timeout: std::time::Duration, no_output_timeout: std::time::Duration, grace_ms: u64, -) -> Result<(openfang_types::config::TerminationReason, String), String> { +) -> Result<(omtae_types::config::TerminationReason, String), String> { use tokio::io::AsyncReadExt; let idle_enabled = !no_output_timeout.is_zero(); @@ -622,7 +622,7 @@ pub async fn wait_or_kill_with_idle( tracing::warn!("Process hit absolute timeout after {:?}", absolute_timeout); kill_child_tree(child, grace_ms).await?; return Ok(( - openfang_types::config::TerminationReason::AbsoluteTimeout, + omtae_types::config::TerminationReason::AbsoluteTimeout, output, )); } @@ -636,7 +636,7 @@ pub async fn wait_or_kill_with_idle( ); kill_child_tree(child, grace_ms).await?; return Ok(( - openfang_types::config::TerminationReason::NoOutputTimeout, + omtae_types::config::TerminationReason::NoOutputTimeout, output, )); } @@ -668,14 +668,14 @@ pub async fn wait_or_kill_with_idle( ).await { Ok(Ok(status)) => { return Ok(( - openfang_types::config::TerminationReason::Exited(status.code().unwrap_or(-1)), + omtae_types::config::TerminationReason::Exited(status.code().unwrap_or(-1)), output, )); } Ok(Err(e)) => return Err(format!("Wait error: {e}")), Err(_) => { kill_child_tree(child, grace_ms).await?; - return Ok((openfang_types::config::TerminationReason::AbsoluteTimeout, output)); + return Ok((omtae_types::config::TerminationReason::AbsoluteTimeout, output)); } } } @@ -726,7 +726,7 @@ pub async fn wait_or_kill_with_idle( match result { Ok(status) => { return Ok(( - openfang_types::config::TerminationReason::Exited(status.code().unwrap_or(-1)), + omtae_types::config::TerminationReason::Exited(status.code().unwrap_or(-1)), output, )); } @@ -771,7 +771,7 @@ mod tests { #[test] fn test_exec_policy_default_has_empty_passthrough() { - let policy = openfang_types::config::ExecPolicy::default(); + let policy = omtae_types::config::ExecPolicy::default(); assert!(policy.shell_env_passthrough.is_empty()); } diff --git a/crates/openfang-runtime/src/tool_runner.rs b/crates/openfang-runtime/src/tool_runner.rs index 16695616ee..fe0d458c90 100644 --- a/crates/openfang-runtime/src/tool_runner.rs +++ b/crates/openfang-runtime/src/tool_runner.rs @@ -6,10 +6,10 @@ use crate::kernel_handle::KernelHandle; use crate::mcp; use crate::web_search::{parse_ddg_results, WebToolsContext}; -use openfang_skills::registry::SkillRegistry; -use openfang_types::taint::{TaintLabel, TaintSink, TaintedValue}; -use openfang_types::tool::{ToolDefinition, ToolResult}; -use openfang_types::tool_compat::normalize_tool_name; +use omtae_skills::registry::SkillRegistry; +use omtae_types::taint::{TaintLabel, TaintSink, TaintedValue}; +use omtae_types::tool::{ToolDefinition, ToolResult}; +use omtae_types::tool_compat::normalize_tool_name; use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -120,13 +120,13 @@ pub async fn execute_tool( allowed_env_vars: Option<&[String]>, workspace_root: Option<&Path>, media_engine: Option<&crate::media_understanding::MediaEngine>, - exec_policy: Option<&openfang_types::config::ExecPolicy>, + exec_policy: Option<&omtae_types::config::ExecPolicy>, tts_engine: Option<&crate::tts::TtsEngine>, - docker_config: Option<&openfang_types::config::DockerSandboxConfig>, + docker_config: Option<&omtae_types::config::DockerSandboxConfig>, process_manager: Option<&crate::process_manager::ProcessManager>, ) -> ToolResult { // Normalize the tool name through compat mappings so LLM-hallucinated aliases - // (e.g. "fs-write" → "file_write") resolve to the canonical OpenFang name. + // (e.g. "fs-write" → "file_write") resolve to the canonical OMTAE name. let tool_name = normalize_tool_name(tool_name); // Capability enforcement: reject tools not in the allowed list @@ -151,8 +151,8 @@ pub async fn execute_tool( // the user already whitelisted is contradictory (GitHub issue #772). let exec_policy_bypasses_approval = is_shell_tool(tool_name) && exec_policy.is_some_and(|p| { - p.mode == openfang_types::config::ExecSecurityMode::Full - || (p.mode == openfang_types::config::ExecSecurityMode::Allowlist + p.mode == omtae_types::config::ExecSecurityMode::Full + || (p.mode == omtae_types::config::ExecSecurityMode::Allowlist && p.allowed_commands.iter().any(|c| c == "*")) }); @@ -170,7 +170,7 @@ pub async fn execute_tool( let summary = format!( "{}: {}", tool_name, - openfang_types::truncate_str(&input_str, 200) + omtae_types::truncate_str(&input_str, 200) ); match kh.request_approval(agent_id_str, tool_name, &summary).await { Ok(true) => { @@ -244,18 +244,23 @@ pub async fn execute_tool( "shell_exec" => { let command = input["command"].as_str().unwrap_or(""); - // SECURITY: Always check for shell metacharacters, even in Full mode. - // These enable command injection regardless of exec policy. - if let Some(reason) = crate::subprocess_sandbox::contains_shell_metacharacters(command) - { - return ToolResult { - tool_use_id: tool_use_id.to_string(), - content: format!( - "shell_exec blocked: command contains {reason}. \ - Shell metacharacters are never allowed." - ), - is_error: true, - }; + let is_full_exec = exec_policy + .is_some_and(|p| p.mode == omtae_types::config::ExecSecurityMode::Full); + + // SECURITY: Check for shell metacharacters unless in Full mode. + // In Full mode, the operator has explicitly requested unrestricted execution. + if !is_full_exec { + if let Some(reason) = crate::subprocess_sandbox::contains_shell_metacharacters(command) + { + return ToolResult { + tool_use_id: tool_use_id.to_string(), + content: format!( + "shell_exec blocked: command contains {reason}. \ + Shell metacharacters are never allowed." + ), + is_error: true, + }; + } } // Exec policy enforcement (allowlist / deny / full) @@ -275,8 +280,6 @@ pub async fn execute_tool( } } // Skip heuristic taint patterns for Full exec policy (e.g. hand agents that need curl) - let is_full_exec = exec_policy - .is_some_and(|p| p.mode == openfang_types::config::ExecSecurityMode::Full); if !is_full_exec { if let Some(violation) = check_taint_shell_exec(command) { return ToolResult { @@ -296,7 +299,7 @@ pub async fn execute_tool( } // Inter-agent tools (require kernel handle) - "agent_send" => tool_agent_send(input, kernel).await, + "agent_send" => tool_agent_send(input, kernel, caller_agent_id).await, "agent_spawn" => tool_agent_spawn(input, kernel, caller_agent_id).await, "agent_list" => tool_agent_list(kernel), "agent_kill" => tool_agent_kill(input, kernel), @@ -521,7 +524,7 @@ pub async fn execute_tool( else if let Some(registry) = skill_registry { if let Some(skill) = registry.find_tool_provider(other) { debug!(tool = other, skill = %skill.manifest.skill.name, "Dispatching to skill"); - match openfang_skills::loader::execute_skill_tool( + match omtae_skills::loader::execute_skill_tool( &skill.manifest, &skill.path, other, @@ -669,14 +672,32 @@ pub fn builtin_tool_definitions() -> Vec { // --- Inter-agent tools --- ToolDefinition { name: "agent_send".to_string(), - description: "Send a message to another agent and receive their response. Accepts UUID or agent name. Use agent_find first to discover agents.".to_string(), + description: "Send a message to another agent and receive their full response. REQUIRED workflow: (1) call agent_list once, (2) copy the target's id (UUID) from that output, (3) call agent_send with agent_id and message. Do NOT emit JSON like {\"name\":\"researcher\"} in plain text — use this function only. Returns agent_send OK with specialist text, or agent_send FAILED with the exact error.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { - "agent_id": { "type": "string", "description": "The target agent's UUID or name" }, - "message": { "type": "string", "description": "The message to send to the agent" } + "agent_id": { + "type": "string", + "description": "Target agent UUID from agent_list (preferred). Name match also works (e.g. researcher, coder)." + }, + "message": { + "type": "string", + "description": "Task or question for the specialist agent" + }, + "agent": { + "type": "string", + "description": "Alias for agent_id (deprecated — use agent_id)" + }, + "target": { + "type": "string", + "description": "Alias for agent_id" + }, + "name": { + "type": "string", + "description": "Alias for agent_id (agent name, not tool name)" + } }, - "required": ["agent_id", "message"] + "required": ["message"] }), }, ToolDefinition { @@ -1174,7 +1195,7 @@ pub fn builtin_tool_definitions() -> Vec { input_schema: serde_json::json!({ "type": "object", "properties": { - "url": { "type": "string", "description": "Base URL of the remote OpenFang/A2A-compatible agent (e.g., 'https://agent.example.com')" } + "url": { "type": "string", "description": "Base URL of the remote OMTAE/A2A-compatible agent (e.g., 'https://agent.example.com')" } }, "required": ["url"] }), @@ -1315,7 +1336,7 @@ pub fn builtin_tool_definitions() -> Vec { }, // --- Skill introspection tools (issue #1038) --- // These let the agent discover and read installed skills without - // touching the filesystem. Global skills live at ~/.openfang/skills/ + // touching the filesystem. Global skills live at ~/.omtae/skills/ // which is outside the workspace sandbox — file_read cannot reach them. ToolDefinition { name: "skill_list".to_string(), @@ -1463,10 +1484,12 @@ fn resolve_directory_path_for_create( resolved.push(segment); } - if !resolved.starts_with(&canon_root) { - return Err(format!( - "Access denied: path '{raw_path}' resolves outside workspace" - )); + if std::env::var("OMTAE_UNRESTRICTED_FS").as_deref() != Ok("1") { + if !resolved.starts_with(&canon_root) { + return Err(format!( + "Access denied: path '{raw_path}' resolves outside workspace" + )); + } } Ok(resolved) @@ -1592,7 +1615,7 @@ async fn tool_web_search_legacy(input: &serde_json::Value) -> Result, - exec_policy: Option<&openfang_types::config::ExecPolicy>, + exec_policy: Option<&omtae_types::config::ExecPolicy>, ) -> Result { let command = input["command"] .as_str() @@ -1649,7 +1672,7 @@ async fn tool_shell_exec( // In Full mode: User explicitly opted into unrestricted shell access, // so we use sh -c / cmd /C as before. let use_direct_exec = exec_policy - .map(|p| p.mode == openfang_types::config::ExecSecurityMode::Allowlist) + .map(|p| p.mode == omtae_types::config::ExecSecurityMode::Allowlist) .unwrap_or(true); // Default to safe mode let mut cmd = if use_direct_exec { @@ -1783,18 +1806,51 @@ fn require_kernel( }) } +/// Resolve the target agent from tool input (LLMs often use `agent` instead of `agent_id`). +fn parse_agent_target(input: &serde_json::Value) -> Result<&str, String> { + const KEYS: &[&str] = &["agent_id", "agent", "target", "to", "recipient", "name"]; + for key in KEYS { + if let Some(v) = input.get(*key).and_then(|v| v.as_str()) { + let trimmed = v.trim(); + if !trimmed.is_empty() { + return Ok(trimmed); + } + } + } + Err( + "Missing target agent: provide 'agent_id' (preferred) or 'agent' with the agent name or UUID." + .into(), + ) +} + async fn tool_agent_send( input: &serde_json::Value, kernel: Option<&Arc>, + caller_agent_id: Option<&str>, ) -> Result { let kh = require_kernel(kernel)?; - let agent_id = input["agent_id"] - .as_str() - .ok_or("Missing 'agent_id' parameter")?; + let agent_id = parse_agent_target(input)?; let message = input["message"] .as_str() .ok_or("Missing 'message' parameter")?; + // Block self-delegation (orchestrator → orchestrator loops) + if let Some(caller) = caller_agent_id { + if agent_id == caller { + return Err( + "agent_send refused: cannot send a message to yourself (same agent_id).".into(), + ); + } + for a in kh.list_agents() { + if a.id == caller && a.name.eq_ignore_ascii_case(agent_id) { + return Err(format!( + "agent_send refused: cannot delegate to yourself ('{}').", + a.name + )); + } + } + } + // Check + increment inter-agent call depth let current_depth = AGENT_CALL_DEPTH.try_with(|d| d.get()).unwrap_or(0); if current_depth >= MAX_AGENT_CALL_DEPTH { @@ -1805,9 +1861,17 @@ async fn tool_agent_send( )); } + let caller_name_owned = caller_agent_id.and_then(|cid| { + kh.list_agents() + .into_iter() + .find(|a| a.id == cid) + .map(|a| a.name) + }); + let caller_name = caller_name_owned.as_deref(); + AGENT_CALL_DEPTH .scope(std::cell::Cell::new(current_depth + 1), async { - kh.send_to_agent(agent_id, message).await + kh.send_to_agent(agent_id, message, caller_agent_id, caller_name).await }) .await } @@ -1848,9 +1912,7 @@ fn tool_agent_kill( kernel: Option<&Arc>, ) -> Result { let kh = require_kernel(kernel)?; - let agent_id = input["agent_id"] - .as_str() - .ok_or("Missing 'agent_id' parameter")?; + let agent_id = parse_agent_target(input)?; kh.kill_agent(agent_id)?; Ok(format!("Agent {agent_id} killed successfully.")) } @@ -1860,9 +1922,7 @@ fn tool_agent_activate( kernel: Option<&Arc>, ) -> Result { let kh = require_kernel(kernel)?; - let agent_id = input["agent_id"] - .as_str() - .ok_or("Missing 'agent_id' parameter")?; + let agent_id = parse_agent_target(input)?; let name = kh.activate_agent(agent_id)?; Ok(format!( "Agent '{name}' activated. It is now Running and ready to receive messages." @@ -2006,8 +2066,8 @@ async fn tool_event_publish( // Knowledge graph tools // --------------------------------------------------------------------------- -fn parse_entity_type(s: &str) -> openfang_types::memory::EntityType { - use openfang_types::memory::EntityType; +fn parse_entity_type(s: &str) -> omtae_types::memory::EntityType { + use omtae_types::memory::EntityType; match s.to_lowercase().as_str() { "person" => EntityType::Person, "organization" | "org" => EntityType::Organization, @@ -2021,8 +2081,8 @@ fn parse_entity_type(s: &str) -> openfang_types::memory::EntityType { } } -fn parse_relation_type(s: &str) -> openfang_types::memory::RelationType { - use openfang_types::memory::RelationType; +fn parse_relation_type(s: &str) -> omtae_types::memory::RelationType { + use omtae_types::memory::RelationType; match s.to_lowercase().as_str() { "works_at" | "worksat" => RelationType::WorksAt, "knows_about" | "knowsabout" | "knows" => RelationType::KnowsAbout, @@ -2053,7 +2113,7 @@ async fn tool_knowledge_add_entity( .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) .unwrap_or_default(); - let entity = openfang_types::memory::Entity { + let entity = omtae_types::memory::Entity { id: String::new(), // kernel/store assigns a real ID entity_type: parse_entity_type(entity_type_str), name: name.to_string(), @@ -2087,7 +2147,7 @@ async fn tool_knowledge_add_relation( .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) .unwrap_or_default(); - let relation = openfang_types::memory::Relation { + let relation = omtae_types::memory::Relation { source: source.to_string(), relation: parse_relation_type(relation_str), target: target.to_string(), @@ -2112,7 +2172,7 @@ async fn tool_knowledge_query( let relation = input["relation"].as_str().map(parse_relation_type); let max_depth = input["max_depth"].as_u64().unwrap_or(1) as u32; - let pattern = openfang_types::memory::GraphPattern { + let pattern = omtae_types::memory::GraphPattern { source, relation, target, @@ -2930,7 +2990,7 @@ async fn tool_location_get() -> Result { // Use ip-api.com (free, no API key, JSON response) let resp = client .get("https://ip-api.com/json/?fields=status,message,country,regionName,city,zip,lat,lon,timezone,isp,query") - .header("User-Agent", "OpenFang/0.1") + .header("User-Agent", "OMTAE/0.1") .send() .await .map_err(|e| format!("Location request failed: {e}"))?; @@ -3020,10 +3080,10 @@ async fn tool_media_describe( _ => return Err(format!("Unsupported image format: .{ext}")), }; - let attachment = openfang_types::media::MediaAttachment { - media_type: openfang_types::media::MediaType::Image, + let attachment = omtae_types::media::MediaAttachment { + media_type: omtae_types::media::MediaType::Image, mime_type: mime.to_string(), - source: openfang_types::media::MediaSource::Base64 { + source: omtae_types::media::MediaSource::Base64 { data: base64::engine::general_purpose::STANDARD.encode(&data), mime_type: mime.to_string(), }, @@ -3065,10 +3125,10 @@ async fn tool_media_transcribe( _ => return Err(format!("Unsupported audio format: .{ext}")), }; - let attachment = openfang_types::media::MediaAttachment { - media_type: openfang_types::media::MediaType::Audio, + let attachment = omtae_types::media::MediaAttachment { + media_type: omtae_types::media::MediaType::Audio, mime_type: mime.to_string(), - source: openfang_types::media::MediaSource::Base64 { + source: omtae_types::media::MediaSource::Base64 { data: base64::engine::general_purpose::STANDARD.encode(&data), mime_type: mime.to_string(), }, @@ -3095,9 +3155,9 @@ async fn tool_image_generate( let model_str = input["model"].as_str().unwrap_or("dall-e-3"); let model = match model_str { - "dall-e-3" | "dalle3" | "dalle-3" => openfang_types::media::ImageGenModel::DallE3, - "dall-e-2" | "dalle2" | "dalle-2" => openfang_types::media::ImageGenModel::DallE2, - "gpt-image-1" | "gpt_image_1" => openfang_types::media::ImageGenModel::GptImage1, + "dall-e-3" | "dalle3" | "dalle-3" => omtae_types::media::ImageGenModel::DallE3, + "dall-e-2" | "dalle2" | "dalle-2" => omtae_types::media::ImageGenModel::DallE2, + "gpt-image-1" | "gpt_image_1" => omtae_types::media::ImageGenModel::GptImage1, _ => { return Err(format!( "Unknown image model: {model_str}. Use 'dall-e-3', 'dall-e-2', or 'gpt-image-1'." @@ -3109,7 +3169,7 @@ async fn tool_image_generate( let quality = input["quality"].as_str().unwrap_or("hd").to_string(); let count = input["count"].as_u64().unwrap_or(1).min(4) as u8; - let request = openfang_types::media::ImageGenRequest { + let request = omtae_types::media::ImageGenRequest { prompt: prompt.to_string(), model, size, @@ -3140,7 +3200,7 @@ async fn tool_image_generate( let mut image_urls: Vec = Vec::new(); { use base64::Engine; - let upload_dir = std::env::temp_dir().join("openfang_uploads"); + let upload_dir = std::env::temp_dir().join("omtae_uploads"); let _ = std::fs::create_dir_all(&upload_dir); for img in &result.images { let file_id = uuid::Uuid::new_v4().to_string(); @@ -3245,7 +3305,7 @@ async fn tool_speech_to_text( _ => "audio/mpeg", }; - use openfang_types::media::{MediaAttachment, MediaSource, MediaType}; + use omtae_types::media::{MediaAttachment, MediaSource, MediaType}; let attachment = MediaAttachment { media_type: MediaType::Audio, mime_type: mime_type.to_string(), @@ -3276,7 +3336,7 @@ async fn tool_speech_to_text( async fn tool_docker_exec( input: &serde_json::Value, - docker_config: Option<&openfang_types::config::DockerSandboxConfig>, + docker_config: Option<&omtae_types::config::DockerSandboxConfig>, workspace_root: Option<&Path>, caller_agent_id: Option<&str>, ) -> Result { @@ -3340,7 +3400,7 @@ async fn tool_process_start( input: &serde_json::Value, pm: Option<&crate::process_manager::ProcessManager>, caller_agent_id: Option<&str>, - exec_policy: Option<&openfang_types::config::ExecPolicy>, + exec_policy: Option<&omtae_types::config::ExecPolicy>, ) -> Result { let pm = pm.ok_or("Process manager not available")?; let agent_id = caller_agent_id.unwrap_or("default"); @@ -3580,7 +3640,7 @@ async fn tool_canvas_present( // --------------------------------------------------------------------------- // Skill introspection tools (issue #1038) // -// Global skills live at ~/.openfang/skills/ which is outside the agent +// Global skills live at ~/.omtae/skills/ which is outside the agent // workspace sandbox. Without these tools the LLM falls back to file_read / // shell_exec to inspect SKILL.md files — which fail with path-resolution // errors because file_read is workspace-scoped. These tools surface the @@ -3595,7 +3655,7 @@ fn tool_skill_list(skill_registry: Option<&SkillRegistry>) -> Result`.".to_string()); + return Ok("No skills installed. Install skills via the dashboard or `omtae skill install `.".to_string()); } let entries: Vec = skills .iter() @@ -3710,7 +3770,7 @@ async fn tool_skill_execute( } }; - match openfang_skills::loader::execute_skill_tool( + match omtae_skills::loader::execute_skill_tool( &skill.manifest, &skill.path, &resolved_tool, @@ -3820,7 +3880,7 @@ mod tests { /// sandbox) are reachable by the agent. #[tokio::test] async fn test_skill_tools_no_filesystem_access() { - use openfang_skills::registry::SkillRegistry; + use omtae_skills::registry::SkillRegistry; use tempfile::TempDir; // Build a skills directory containing one prompt-only SKILL.md skill @@ -3905,7 +3965,7 @@ mod tests { #[tokio::test] async fn test_file_read_missing() { let bad_path = std::env::temp_dir() - .join("openfang_test_nonexistent_99999") + .join("omtae_test_nonexistent_99999") .join("file.txt"); let result = execute_tool( "test-id", @@ -4214,7 +4274,7 @@ mod tests { let allowed = vec!["file_read".to_string()]; // Use a cross-platform nonexistent path let bad_path = std::env::temp_dir() - .join("openfang_test_nonexistent_12345") + .join("omtae_test_nonexistent_12345") .join("file.txt"); let result = execute_tool( "test-id", @@ -4489,6 +4549,24 @@ mod tests { assert!(result.content.contains("Failed to read")); } + #[test] + fn test_parse_agent_target_prefers_agent_id() { + let input = serde_json::json!({"agent_id": "coder", "agent": "analyst"}); + assert_eq!(parse_agent_target(&input).unwrap(), "coder"); + } + + #[test] + fn test_parse_agent_target_accepts_agent_alias() { + let input = serde_json::json!({"agent": "researcher", "message": "hi"}); + assert_eq!(parse_agent_target(&input).unwrap(), "researcher"); + } + + #[test] + fn test_parse_agent_target_missing() { + let input = serde_json::json!({"message": "hi"}); + assert!(parse_agent_target(&input).is_err()); + } + #[test] fn test_depth_limit_constant() { assert_eq!(MAX_AGENT_CALL_DEPTH, 5); @@ -4611,7 +4689,7 @@ mod tests { "html": "

Test Canvas

Hello world

", "title": "Test" }); - let tmp = std::env::temp_dir().join("openfang_canvas_test"); + let tmp = std::env::temp_dir().join("omtae_canvas_test"); let _ = std::fs::create_dir_all(&tmp); let result = tool_canvas_present(&input, Some(tmp.as_path())).await; assert!(result.is_ok()); @@ -4639,7 +4717,7 @@ mod tests { #[tokio::test] async fn test_issue_919_process_start_rm_blocked_in_allowlist() { - use openfang_types::config::{ExecPolicy, ExecSecurityMode}; + use omtae_types::config::{ExecPolicy, ExecSecurityMode}; let pm = crate::process_manager::ProcessManager::new(5); let policy = ExecPolicy { @@ -4649,7 +4727,7 @@ mod tests { }; let input = serde_json::json!({ "command": "rm", - "args": ["/tmp/openfang_test_should_not_be_deleted.txt"], + "args": ["/tmp/omtae_test_should_not_be_deleted.txt"], }); let result = tool_process_start(&input, Some(&pm), Some("test-agent"), Some(&policy)).await; @@ -4677,7 +4755,7 @@ mod tests { #[tokio::test] async fn test_issue_919_process_start_metachar_in_command_blocked() { - use openfang_types::config::{ExecPolicy, ExecSecurityMode}; + use omtae_types::config::{ExecPolicy, ExecSecurityMode}; let pm = crate::process_manager::ProcessManager::new(5); let policy = ExecPolicy { @@ -4697,7 +4775,7 @@ mod tests { #[tokio::test] async fn test_issue_919_process_start_metachar_in_arg_blocked() { - use openfang_types::config::{ExecPolicy, ExecSecurityMode}; + use omtae_types::config::{ExecPolicy, ExecSecurityMode}; let pm = crate::process_manager::ProcessManager::new(5); let policy = ExecPolicy { @@ -4720,7 +4798,7 @@ mod tests { #[tokio::test] async fn test_issue_919_process_start_deny_mode_blocks_everything() { - use openfang_types::config::{ExecPolicy, ExecSecurityMode}; + use omtae_types::config::{ExecPolicy, ExecSecurityMode}; let pm = crate::process_manager::ProcessManager::new(5); let policy = ExecPolicy { @@ -4781,7 +4859,7 @@ mod tests { // Minimal in-memory KernelHandle used to verify schedule_* tool wiring. // Records every cron_* call so tests can assert what the tool pushed into - // the kernel, without booting a real OpenFangKernel. + // the kernel, without booting a real OMTAEKernel. struct FakeKernelHandle { created: std::sync::Mutex>, cancelled: std::sync::Mutex>, @@ -4812,7 +4890,13 @@ mod tests { ) -> Result<(String, String), String> { Err("not used".into()) } - async fn send_to_agent(&self, _agent_id: &str, _message: &str) -> Result { + async fn send_to_agent( + &self, + _agent_id: &str, + _message: &str, + _caller_id: Option<&str>, + _caller_name: Option<&str>, + ) -> Result { Err("not used".into()) } fn list_agents(&self) -> Vec { @@ -4857,20 +4941,20 @@ mod tests { } async fn knowledge_add_entity( &self, - _entity: openfang_types::memory::Entity, + _entity: omtae_types::memory::Entity, ) -> Result { Err("not used".into()) } async fn knowledge_add_relation( &self, - _relation: openfang_types::memory::Relation, + _relation: omtae_types::memory::Relation, ) -> Result { Err("not used".into()) } async fn knowledge_query( &self, - _pattern: openfang_types::memory::GraphPattern, - ) -> Result, String> { + _pattern: omtae_types::memory::GraphPattern, + ) -> Result, String> { Ok(vec![]) } @@ -4885,7 +4969,7 @@ mod tests { .unwrap() .push((agent_id.to_string(), job_json.clone())); // Mirror what the real kernel returns (see cron_create in - // openfang-kernel): `{ "job_id": "...", "status": "created" }`. + // omtae-kernel): `{ "job_id": "...", "status": "created" }`. let resp = serde_json::json!({ "job_id": id, "status": "created" }); Ok(resp.to_string()) } diff --git a/crates/openfang-runtime/src/tts.rs b/crates/openfang-runtime/src/tts.rs index 5d4272fb3b..5fe708fddb 100644 --- a/crates/openfang-runtime/src/tts.rs +++ b/crates/openfang-runtime/src/tts.rs @@ -2,7 +2,7 @@ //! //! Auto-cascades through available providers based on configured API keys. -use openfang_types::config::TtsConfig; +use omtae_types::config::TtsConfig; /// Maximum audio response size (10MB). const MAX_AUDIO_RESPONSE_BYTES: usize = 10 * 1024 * 1024; diff --git a/crates/openfang-runtime/src/web_fetch.rs b/crates/openfang-runtime/src/web_fetch.rs index 1fba47f718..d8690fcbfc 100644 --- a/crates/openfang-runtime/src/web_fetch.rs +++ b/crates/openfang-runtime/src/web_fetch.rs @@ -7,7 +7,7 @@ use crate::str_utils::safe_truncate_str; use crate::web_cache::WebCache; use crate::web_content::{html_to_markdown, wrap_external_content}; -use openfang_types::config::WebFetchConfig; +use omtae_types::config::WebFetchConfig; use std::net::{IpAddr, ToSocketAddrs}; use std::sync::Arc; use tracing::debug; diff --git a/crates/openfang-runtime/src/web_search.rs b/crates/openfang-runtime/src/web_search.rs index 11b2f5823f..ce756aad52 100644 --- a/crates/openfang-runtime/src/web_search.rs +++ b/crates/openfang-runtime/src/web_search.rs @@ -9,7 +9,7 @@ use crate::web_cache::WebCache; use crate::web_content::wrap_external_content; -use openfang_types::config::{SearchProvider, WebConfig}; +use omtae_types::config::{SearchProvider, WebConfig}; use std::sync::Arc; use tracing::{debug, warn}; use zeroize::Zeroizing; @@ -294,7 +294,7 @@ impl WebSearchEngine { .client .get("https://html.duckduckgo.com/html/") .query(&[("q", query)]) - .header("User-Agent", "Mozilla/5.0 (compatible; OpenFangAgent/0.1)") + .header("User-Agent", "Mozilla/5.0 (compatible; OMTAEAgent/0.1)") .send() .await .map_err(|e| format!("DuckDuckGo request failed: {e}"))?; @@ -368,7 +368,7 @@ impl WebSearchEngine { ("categories", category), ("page", &page.to_string()), ]) - .header("User-Agent", "Mozilla/5.0 (compatible; OpenFangAgent/0.1)") + .header("User-Agent", "Mozilla/5.0 (compatible; OMTAEAgent/0.1)") .send() .await .map_err(|e| format!("SearXNG request failed: {e}"))?; @@ -458,7 +458,7 @@ impl WebSearchEngine { "{}/config", self.config.searxng.url.trim_end_matches('/') )) - .header("User-Agent", "Mozilla/5.0 (compatible; OpenFangAgent/0.1)") + .header("User-Agent", "Mozilla/5.0 (compatible; OMTAEAgent/0.1)") .send() .await .map_err(|e| format!("SearXNG config request failed: {e}"))?; @@ -492,31 +492,27 @@ impl WebSearchEngine { // --------------------------------------------------------------------------- /// Parse DuckDuckGo HTML search results into (title, url, snippet) tuples. +/// +/// Only parses anchors with `class="result__a"`. The HTML before the first such +/// anchor contains `` and other +/// boilerplate; treating that preamble as a result produced junk URLs. pub fn parse_ddg_results(html: &str, max: usize) -> Vec<(String, String, String)> { let mut results = Vec::new(); - for chunk in html.split("class=\"result__a\"") { + for (i, chunk) in html.split("class=\"result__a\"").enumerate() { + if i == 0 { + continue; + } if results.len() >= max { break; } - if !chunk.contains("href=") { - continue; - } - let url = extract_between(chunk, "href=\"", "\"") - .unwrap_or_default() - .to_string(); - - let actual_url = if url.contains("uddg=") { - url.split("uddg=") - .nth(1) - .and_then(|u| u.split('&').next()) - .map(urldecode) - .unwrap_or(url) - } else { - url + let url = match extract_between(chunk, "href=\"", "\"") { + Some(u) => u, + None => continue, }; + let actual_url = resolve_ddg_result_url(url); let title = extract_between(chunk, ">", "") .map(strip_html_tags) .unwrap_or_default(); @@ -531,7 +527,7 @@ pub fn parse_ddg_results(html: &str, max: usize) -> Vec<(String, String, String) String::new() }; - if !title.is_empty() && !actual_url.is_empty() { + if is_valid_ddg_result(&title, &actual_url) { results.push((title, actual_url, snippet)); } } @@ -539,6 +535,53 @@ pub fn parse_ddg_results(html: &str, max: usize) -> Vec<(String, String, String) results } +/// Decode DDG redirect links (`/l/?uddg=...`) and normalize protocol-relative URLs. +fn resolve_ddg_result_url(url: &str) -> String { + let decoded = if url.contains("uddg=") { + url.split("uddg=") + .nth(1) + .and_then(|u| u.split('&').next()) + .map(urldecode) + .unwrap_or_else(|| url.to_string()) + } else { + url.to_string() + }; + if decoded.starts_with("//") { + format!("https:{decoded}") + } else { + decoded + } +} + +/// Reject opensearch boilerplate, DDG assets, and non-absolute external URLs. +fn is_valid_ddg_result(title: &str, url: &str) -> bool { + if title.trim().is_empty() { + return false; + } + let lower = url.to_lowercase(); + if !(lower.starts_with("http://") || lower.starts_with("https://")) { + return false; + } + if lower.contains("opensearch") { + return false; + } + if lower.contains("duckduckgo.com") { + let path = lower + .split("duckduckgo.com") + .nth(1) + .unwrap_or(""); + if path.starts_with("/l/?") + || path.starts_with("/html") + || path.contains("/assets/") + || path.contains("/dist/") + || path.ends_with("favicon.ico") + { + return false; + } + } + true +} + /// Extract text between two delimiters. pub fn extract_between<'a>(text: &'a str, start: &str, end: &str) -> Option<&'a str> { let start_idx = text.find(start)? + start.len(); @@ -636,4 +679,53 @@ mod tests { assert_eq!(results.len(), 1); assert_eq!(results[0].1, "https://example.com"); } + + #[test] + fn test_ddg_parser_skips_opensearch_in_head() { + let html = r#" + + + + + Top Solar Installers + Real snippet text +"#; + let results = parse_ddg_results(html, 5); + assert_eq!(results.len(), 1); + assert_eq!(results[0].0, "Top Solar Installers"); + assert_eq!(results[0].1, "https://www.example.com/solar"); + assert!(!results[0].1.contains("opensearch")); + } + + #[test] + fn test_ddg_parser_rejects_internal_ddg_links() { + let html = r#"x class="result__a" href="//duckduckgo.com/html/">DuckDuckGo class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.org">Example"#; + let results = parse_ddg_results(html, 5); + assert_eq!(results.len(), 1); + assert_eq!(results[0].1, "https://example.org"); + } + + /// Live DDG HTML smoke test (network). Run: `cargo test -p omtae-runtime live_ddg -- --ignored --nocapture` + #[tokio::test] + #[ignore = "network"] + async fn test_live_ddg_top_solar_installers() { + let resp = reqwest::Client::new() + .get("https://html.duckduckgo.com/html/") + .query(&[("q", "top solar installers in the US")]) + .header("User-Agent", "Mozilla/5.0 (compatible; OMTAEAgent/0.1)") + .send() + .await + .expect("DDG request"); + let body = resp.text().await.expect("DDG body"); + let results = parse_ddg_results(&body, 3); + assert!(!results.is_empty(), "expected organic results"); + assert!( + !results[0].1.contains("opensearch"), + "first result must not be opensearch junk: {}", + results[0].1 + ); + for (title, url, snippet) in &results { + eprintln!("{title}\n {url}\n {snippet}\n"); + } + } } diff --git a/crates/openfang-runtime/src/workspace_context.rs b/crates/openfang-runtime/src/workspace_context.rs index bab9fbab61..fb34f15210 100644 --- a/crates/openfang-runtime/src/workspace_context.rs +++ b/crates/openfang-runtime/src/workspace_context.rs @@ -1,7 +1,7 @@ //! Workspace context auto-detection. //! //! Scans the workspace root for project type indicators (Cargo.toml, package.json, etc.), -//! context files (AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, HEARTBEAT.md), and OpenFang +//! context files (AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, HEARTBEAT.md), and OMTAE //! state files. Provides mtime-cached file reads to avoid redundant I/O. use serde::{Deserialize, Serialize}; @@ -65,8 +65,8 @@ pub struct WorkspaceContext { pub project_type: ProjectType, /// Whether this is a git repository. pub is_git_repo: bool, - /// Whether .openfang/ directory exists. - pub has_openfang_dir: bool, + /// Whether .omtae/ directory exists. + pub has_omtae_dir: bool, /// Cached context files. cache: HashMap, } @@ -76,7 +76,7 @@ impl WorkspaceContext { pub fn detect(root: &Path) -> Self { let project_type = detect_project_type(root); let is_git_repo = root.join(".git").exists(); - let has_openfang_dir = root.join(".openfang").exists(); + let has_omtae_dir = root.join(".omtae").exists(); let mut cache = HashMap::new(); for &name in CONTEXT_FILES { @@ -91,7 +91,7 @@ impl WorkspaceContext { workspace_root: root.to_path_buf(), project_type, is_git_repo, - has_openfang_dir, + has_omtae_dir, cache, } } @@ -215,7 +215,7 @@ fn has_extension_in_dir(dir: &Path, ext: &str) -> bool { false } -/// Persistent workspace state, saved to `.openfang/workspace-state.json`. +/// Persistent workspace state, saved to `.omtae/workspace-state.json`. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct WorkspaceState { /// State format version. @@ -232,10 +232,10 @@ fn default_version() -> u32 { } impl WorkspaceState { - /// Load state from the workspace's `.openfang/workspace-state.json`. + /// Load state from the workspace's `.omtae/workspace-state.json`. pub fn load(workspace_root: &Path) -> Self { let path = workspace_root - .join(".openfang") + .join(".omtae") .join("workspace-state.json"); match std::fs::read_to_string(&path) { Ok(json) => serde_json::from_str(&json).unwrap_or_default(), @@ -243,11 +243,11 @@ impl WorkspaceState { } } - /// Save state to the workspace's `.openfang/workspace-state.json`. + /// Save state to the workspace's `.omtae/workspace-state.json`. pub fn save(&self, workspace_root: &Path) -> Result<(), String> { - let dir = workspace_root.join(".openfang"); + let dir = workspace_root.join(".omtae"); std::fs::create_dir_all(&dir) - .map_err(|e| format!("Failed to create .openfang dir: {e}"))?; + .map_err(|e| format!("Failed to create .omtae dir: {e}"))?; let path = dir.join("workspace-state.json"); let json = serde_json::to_string_pretty(self) .map_err(|e| format!("Failed to serialize state: {e}"))?; @@ -261,7 +261,7 @@ mod tests { #[test] fn test_detect_rust_project() { - let dir = std::env::temp_dir().join("openfang_ws_rust_test"); + let dir = std::env::temp_dir().join("omtae_ws_rust_test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap(); @@ -271,7 +271,7 @@ mod tests { #[test] fn test_detect_node_project() { - let dir = std::env::temp_dir().join("openfang_ws_node_test"); + let dir = std::env::temp_dir().join("omtae_ws_node_test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("package.json"), "{}").unwrap(); @@ -281,7 +281,7 @@ mod tests { #[test] fn test_detect_python_project() { - let dir = std::env::temp_dir().join("openfang_ws_py_test"); + let dir = std::env::temp_dir().join("omtae_ws_py_test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("pyproject.toml"), "[tool.poetry]").unwrap(); @@ -291,7 +291,7 @@ mod tests { #[test] fn test_detect_go_project() { - let dir = std::env::temp_dir().join("openfang_ws_go_test"); + let dir = std::env::temp_dir().join("omtae_ws_go_test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("go.mod"), "module example.com/test").unwrap(); @@ -301,7 +301,7 @@ mod tests { #[test] fn test_detect_unknown_project() { - let dir = std::env::temp_dir().join("openfang_ws_unk_test"); + let dir = std::env::temp_dir().join("omtae_ws_unk_test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); assert_eq!(detect_project_type(&dir), ProjectType::Unknown); @@ -310,7 +310,7 @@ mod tests { #[test] fn test_workspace_context_detect() { - let dir = std::env::temp_dir().join("openfang_ws_ctx_test"); + let dir = std::env::temp_dir().join("omtae_ws_ctx_test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("Cargo.toml"), "[package]").unwrap(); @@ -327,7 +327,7 @@ mod tests { #[test] fn test_get_file_cache_hit() { - let dir = std::env::temp_dir().join("openfang_ws_cache_test"); + let dir = std::env::temp_dir().join("omtae_ws_cache_test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("SOUL.md"), "I am a helpful agent.").unwrap(); @@ -343,7 +343,7 @@ mod tests { #[test] fn test_file_size_cap() { - let dir = std::env::temp_dir().join("openfang_ws_cap_test"); + let dir = std::env::temp_dir().join("omtae_ws_cap_test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); @@ -359,7 +359,7 @@ mod tests { #[test] fn test_build_context_section() { - let dir = std::env::temp_dir().join("openfang_ws_section_test"); + let dir = std::env::temp_dir().join("omtae_ws_section_test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("Cargo.toml"), "[package]").unwrap(); @@ -378,7 +378,7 @@ mod tests { #[test] fn test_workspace_state_round_trip() { - let dir = std::env::temp_dir().join("openfang_ws_state_test"); + let dir = std::env::temp_dir().join("omtae_ws_state_test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); @@ -402,7 +402,7 @@ mod tests { #[test] fn test_workspace_state_missing_file() { - let dir = std::env::temp_dir().join("openfang_ws_state_missing"); + let dir = std::env::temp_dir().join("omtae_ws_state_missing"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); diff --git a/crates/openfang-runtime/src/workspace_sandbox.rs b/crates/openfang-runtime/src/workspace_sandbox.rs index 306c49f2a5..35214c6a1b 100644 --- a/crates/openfang-runtime/src/workspace_sandbox.rs +++ b/crates/openfang-runtime/src/workspace_sandbox.rs @@ -54,15 +54,17 @@ pub fn resolve_sandbox_path(user_path: &str, workspace_root: &Path) -> Result" http://127.0.0.1:4200/api/brain/status` | +| Agent roster | `curl -s http://127.0.0.1:4200/api/agents` | +| Disk layout | `file_list` on `/home/jay/vaults/omtae-brain` or workspace | +| External research | `web_search` → `web_fetch` | + +## Anti-Patterns + +- Narrating peer agents without `agent_list` or `/api/agents` output +- "Let me check…" across multiple turns without a tool call +- Inventing `https://` URLs or business names without web tools +- Treating memory_recall as current daemon or vault state diff --git a/crates/openfang-skills/ecc/tool-discipline/SKILL.md b/crates/openfang-skills/ecc/tool-discipline/SKILL.md new file mode 100644 index 0000000000..d2ce4f6159 --- /dev/null +++ b/crates/openfang-skills/ecc/tool-discipline/SKILL.md @@ -0,0 +1,43 @@ +--- +name: ecc-tool-discipline +description: Agent architecture tool-discipline layer for OMTAE. Enforces tool-before-claim at every turn. Adapted from ECC agent-architecture-audit layer 7. +origin: ECC +--- + +# ECC Tool Discipline (OMTAE) + +Layer-7 fix from ECC agent-architecture-audit: tools declared in prompt must be **called**, not narrated. + +## Symptoms This Skill Fixes + +- "Must use tool X" in prompt but model answers without calling it +- Tool results look correct but were never executed +- Model describes peer agents, vault files, or health without `shell_exec` / `file_list` + +## Per-Turn Contract + +Before sending a user-visible answer, confirm: + +- [ ] Did I call a tool for every factual claim in this message? +- [ ] Are paths/URLs copied from tool output, not invented? +- [ ] Did I avoid "The user is asking about…" meta-narration? +- [ ] If blocked by TOOL_REQUIRED, did I call a **different** tool than last attempt? + +## OMTAE High-Risk Prompts + +| User says | First tool | +|-----------|------------| +| brain / Obsidian / vault | `curl /api/brain/status` or `file_list` on vault | +| system check / health | `curl /api/health` or `nvidia-smi` | +| how many agents | `curl /api/agents` | +| research / top N / list | `web_search` | + +## Runtime Integration + +OMTAE `loop_guard` enforces this in code: + +- `TOOL_REQUIRED` — factual claims without tool evidence +- Planning loop — repeated "let me check" without tools (threshold: 2) +- Research integrity — unverified URLs get a disclaimer + +Prompts alone are insufficient; the runtime gates are the backstop. diff --git a/crates/openfang-skills/ecc/verification-loop/SKILL.md b/crates/openfang-skills/ecc/verification-loop/SKILL.md new file mode 100644 index 0000000000..c81226ccb7 --- /dev/null +++ b/crates/openfang-skills/ecc/verification-loop/SKILL.md @@ -0,0 +1,46 @@ +--- +name: ecc-verification-loop +description: OMTAE verification gate — tool evidence required before factual claims. Adapted from ECC verification-loop. +origin: ECC +--- + +# ECC Verification Loop (OMTAE) + +Use when an OMTAE agent must prove claims with tools before answering. + +## When to Activate + +- User asks about brain vault, Obsidian, peer agents, service status, or file contents +- Agent is about to list paths, URLs, counts, or ranked recommendations +- Prior turn was blocked with `TOOL_REQUIRED` or planning-loop nudge + +## OMTAE Tool Checklist + +Run the lightest check that answers the question: + +| Claim type | Required tool | +|------------|---------------| +| Daemon / API health | `shell_exec`: `curl -s http://127.0.0.1:4200/api/health` | +| Brain vault status | `shell_exec`: `curl -s -H "X-OMTAE-Pin: " http://127.0.0.1:4200/api/brain/status` | +| Brain search | `shell_exec`: `curl -s -H "X-OMTAE-Pin: " "http://127.0.0.1:4200/api/brain/search?q=..."` | +| Workspace files | `file_list` on workspace root or vault path | +| File contents | `file_read` on specific path from `file_list` output | +| Public facts | `web_search` then `web_fetch` on top URLs | +| Prior context | `memory_recall` — never treat recall as live system status | + +## Rules + +1. **No tool output → no factual claim.** Say "checking…" and call a tool. +2. **Quote evidence.** Paste key lines from tool results; do not paraphrase into new facts. +3. **One command per shell_exec** when ops-fixer policy applies (no pipes). +4. **Stop on TOOL_REQUIRED.** Do not repeat the same planning sentence — change the tool or parameters. + +## Report Format + +``` +VERIFICATION +- Tools used: shell_exec, file_list, … +- Evidence: +- Answer: +- Gaps: +``` diff --git a/crates/openfang-skills/src/bundled.rs b/crates/openfang-skills/src/bundled.rs index a8186d7b5c..364cb05f1f 100644 --- a/crates/openfang-skills/src/bundled.rs +++ b/crates/openfang-skills/src/bundled.rs @@ -1,6 +1,6 @@ //! Bundled skills — compile-time embedded SKILL.md files. //! -//! Ships 60 prompt-only skills inside the OpenFang binary via `include_str!()`. +//! Ships 60 prompt-only skills inside the OMTAE binary via `include_str!()`. //! User-installed skills with the same name override bundled ones. use crate::openclaw_compat::convert_skillmd_str; diff --git a/crates/openfang-skills/src/clawhub.rs b/crates/openfang-skills/src/clawhub.rs index b844b07cf6..92d434155c 100644 --- a/crates/openfang-skills/src/clawhub.rs +++ b/crates/openfang-skills/src/clawhub.rs @@ -229,7 +229,7 @@ pub struct ClawHubInstallResult { pub slug: String, /// Security warnings from the scan pipeline. pub warnings: Vec, - /// Tool name translations applied (OpenClaw → OpenFang). + /// Tool name translations applied (OpenClaw → OMTAE). pub tool_translations: Vec<(String, String)>, /// Whether this is a prompt-only skill. pub is_prompt_only: bool, @@ -309,7 +309,7 @@ impl ClawHubClient { let result = self .client .get(url) - .header("User-Agent", "OpenFang/0.1") + .header("User-Agent", "OMTAE/0.1") .send() .await; @@ -514,7 +514,7 @@ impl ClawHubClient { /// Security pipeline: /// 1. Download skill zip and compute SHA256 /// 2. Detect format (SKILL.md vs package.json) - /// 3. Convert to OpenFang manifest + /// 3. Convert to OMTAE manifest /// 4. Run manifest security scan /// 5. If prompt-only: run prompt injection scan /// 6. Check binary dependencies @@ -672,7 +672,7 @@ impl ClawHubClient { all_warnings.extend(manifest_warnings); // Step 7: Write skill.toml - openclaw_compat::write_openfang_manifest(&skill_dir, &manifest)?; + openclaw_compat::write_omtae_manifest(&skill_dir, &manifest)?; // Step 8: Enforce --require-signed gate, if requested. if let Err(e) = enforce_require_signed(&skill_dir, opts) { diff --git a/crates/openfang-skills/src/config_injection.rs b/crates/openfang-skills/src/config_injection.rs index b08b0827de..938ee8f651 100644 --- a/crates/openfang-skills/src/config_injection.rs +++ b/crates/openfang-skills/src/config_injection.rs @@ -5,7 +5,7 @@ //! endpoint URLs, etc.). This module resolves those variables from three //! layers in priority order: //! -//! 1. User-supplied config (e.g., `[skills.]` in `~/.openfang/config.toml`) +//! 1. User-supplied config (e.g., `[skills.]` in `~/.omtae/config.toml`) //! 2. Environment variable named by `var.env` //! 3. `var.default` //! @@ -53,7 +53,7 @@ pub enum SkillConfigError { /// /// Resolution order per variable: /// 1. `user_config[name]` — usually from the `[skills.]` section -/// of `~/.openfang/config.toml`, passed in by the caller. +/// of `~/.omtae/config.toml`, passed in by the caller. /// 2. `std::env::var(var.env)` if `var.env` is set. /// 3. `var.default` if set. /// @@ -154,7 +154,7 @@ pub fn render_config_block(resolved: &HashMap) -> String { let mut keys: Vec<&String> = resolved.keys().collect(); keys.sort(); - let mut out = String::from("[Skill config from ~/.openfang/config.toml:\n"); + let mut out = String::from("[Skill config from ~/.omtae/config.toml:\n"); for key in keys { let raw = &resolved[key]; let shown = redact_value(key, raw); diff --git a/crates/openfang-skills/src/installer.rs b/crates/openfang-skills/src/installer.rs index fde591a717..a35c126b81 100644 --- a/crates/openfang-skills/src/installer.rs +++ b/crates/openfang-skills/src/installer.rs @@ -6,7 +6,7 @@ //! payload and verify cleanly before the install is considered complete. //! //! The signature envelope is a JSON serialisation of -//! [`openfang_types::manifest_signing::SignedManifest`]. The installer looks +//! [`omtae_types::manifest_signing::SignedManifest`]. The installer looks //! for it at one of these well-known names inside the freshly written skill //! directory: //! @@ -19,7 +19,7 @@ //! prompt-injection-blocked path in `clawhub.rs`. use crate::SkillError; -use openfang_types::manifest_signing::SignedManifest; +use omtae_types::manifest_signing::SignedManifest; use std::path::Path; /// Options controlling enforcement during skill install. @@ -497,7 +497,7 @@ entry = "rm-rf.py" #[test] fn require_signed_binds_to_package_json() { let dir = TempDir::new().unwrap(); - let pkg = r#"{"name":"x","version":"0.1.0","openfang":{"skill":"x"}}"#; + let pkg = r#"{"name":"x","version":"0.1.0","omtae":{"skill":"x"}}"#; std::fs::write(dir.path().join("package.json"), pkg).unwrap(); let signing_key = SigningKey::generate(&mut OsRng); diff --git a/crates/openfang-skills/src/lib.rs b/crates/openfang-skills/src/lib.rs index c349d4845f..5bb7c8c612 100644 --- a/crates/openfang-skills/src/lib.rs +++ b/crates/openfang-skills/src/lib.rs @@ -1,4 +1,4 @@ -//! Skill system for OpenFang. +//! Skill system for OMTAE. //! //! Skills are pluggable tool bundles that extend agent capabilities. //! They can be: @@ -73,9 +73,9 @@ pub enum SkillRuntime { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "type")] pub enum SkillSource { - /// Built into OpenFang or manually installed. + /// Built into OMTAE or manually installed. Native, - /// Bundled at compile time (ships with OpenFang binary). + /// Bundled at compile time (ships with OMTAE binary). Bundled, /// Converted from OpenClaw format. OpenClaw, @@ -208,7 +208,7 @@ mod tests { name = "web-summarizer" version = "0.1.0" description = "Summarizes any web page into bullet points" -author = "openfang-community" +author = "omtae-community" license = "MIT" tags = ["web", "summarizer", "research"] diff --git a/crates/openfang-skills/src/marketplace.rs b/crates/openfang-skills/src/marketplace.rs index bbb316bbc9..758416945c 100644 --- a/crates/openfang-skills/src/marketplace.rs +++ b/crates/openfang-skills/src/marketplace.rs @@ -21,7 +21,7 @@ impl Default for MarketplaceConfig { fn default() -> Self { Self { registry_url: "https://api.github.com".to_string(), - github_org: "openfang-skills".to_string(), + github_org: "omtae-skills".to_string(), } } } @@ -38,7 +38,7 @@ impl MarketplaceClient { Self { config, http: reqwest::Client::builder() - .user_agent("openfang-skills/0.1") + .user_agent("omtae-skills/0.1") .build() .expect("Failed to build HTTP client"), } @@ -219,12 +219,12 @@ mod tests { fn test_default_config() { let config = MarketplaceConfig::default(); assert!(config.registry_url.contains("github")); - assert_eq!(config.github_org, "openfang-skills"); + assert_eq!(config.github_org, "omtae-skills"); } #[test] fn test_client_creation() { let client = MarketplaceClient::new(MarketplaceConfig::default()); - assert_eq!(client.config.github_org, "openfang-skills"); + assert_eq!(client.config.github_org, "omtae-skills"); } } diff --git a/crates/openfang-skills/src/openclaw_compat.rs b/crates/openfang-skills/src/openclaw_compat.rs index 240012f1af..3fa9cc1271 100644 --- a/crates/openfang-skills/src/openclaw_compat.rs +++ b/crates/openfang-skills/src/openclaw_compat.rs @@ -4,14 +4,14 @@ //! 1. **Node.js/TypeScript modules** — `package.json` + `index.js` (code skills) //! 2. **SKILL.md Markdown files** — YAML frontmatter + Markdown body (prompt-only skills) //! -//! This module detects both formats and converts them to OpenFang `SkillManifest`. +//! This module detects both formats and converts them to OMTAE `SkillManifest`. use crate::config_injection::SkillConfigVar; use crate::{ SkillError, SkillManifest, SkillMeta, SkillRequirements, SkillRuntime, SkillRuntimeConfig, SkillSource, SkillToolDef, SkillTools, }; -use openfang_types::tool_compat; +use omtae_types::tool_compat; use serde::Deserialize; use std::collections::HashMap; use std::path::Path; @@ -90,14 +90,14 @@ pub struct OpenClawDispatch { pub disable_model_invocation: bool, } -/// Result of converting a SKILL.md into OpenFang format. +/// Result of converting a SKILL.md into OMTAE format. #[derive(Debug, Clone)] pub struct ConvertedSkillMd { /// The generated skill manifest. pub manifest: SkillManifest, /// Markdown body (prompt context for the LLM). pub prompt_context: String, - /// Tool name translations applied (openclaw_name → openfang_name). + /// Tool name translations applied (openclaw_name → omtae_name). pub tool_translations: Vec<(String, String)>, /// Required system binaries. pub required_bins: Vec, @@ -167,7 +167,7 @@ pub fn parse_skillmd_str(content: &str) -> Result<(SkillMdFrontmatter, String), Ok((frontmatter, body)) } -/// Full conversion of a SKILL.md directory to OpenFang format. +/// Full conversion of a SKILL.md directory to OMTAE format. /// /// Most SKILL.md skills are prompt-only (no executable code). The Markdown body /// is stored as `prompt_context` and injected into the LLM's system prompt. @@ -197,17 +197,17 @@ pub fn convert_skillmd(dir: &Path) -> Result { required_env = requires.env.clone(); } - // Convert commands to OpenFang tool definitions + // Convert commands to OMTAE tool definitions for cmd in &meta.commands { if cmd.name.is_empty() { continue; } // Translate tool name if it's a known OpenClaw name - let openfang_name = if let Some(mapped) = tool_compat::map_tool_name(&cmd.name) { + let omtae_name = if let Some(mapped) = tool_compat::map_tool_name(&cmd.name) { tool_translations.push((cmd.name.clone(), mapped.to_string())); mapped.to_string() - } else if tool_compat::is_known_openfang_tool(&cmd.name) { + } else if tool_compat::is_known_omtae_tool(&cmd.name) { cmd.name.clone() } else { // Custom command — keep original name, normalize to snake_case @@ -215,7 +215,7 @@ pub fn convert_skillmd(dir: &Path) -> Result { }; tools.push(SkillToolDef { - name: openfang_name, + name: omtae_name, description: if cmd.description.is_empty() { format!("Execute {} command", cmd.name) } else { @@ -279,7 +279,7 @@ pub fn convert_skillmd(dir: &Path) -> Result { }) } -/// Convert an in-memory SKILL.md string into OpenFang format. +/// Convert an in-memory SKILL.md string into OMTAE format. /// /// Same as `convert_skillmd()` but works from a string rather than a directory. /// Used by the bundled skills system for compile-time embedded content. @@ -308,17 +308,17 @@ pub fn convert_skillmd_str(name_hint: &str, content: &str) -> Result Result bool { || dir.join("dist").join("index.js").exists()) } -/// Convert an OpenClaw Node.js skill directory into an OpenFang SkillManifest. +/// Convert an OpenClaw Node.js skill directory into an OMTAE SkillManifest. /// /// Reads package.json to extract name, version, description, and infers tool definitions. pub fn convert_openclaw_skill(dir: &Path) -> Result { @@ -477,8 +477,8 @@ fn extract_tools_from_openclaw_meta(meta: &serde_json::Value) -> Vec Result<(), SkillError> { +/// Write an OMTAE skill.toml manifest for an OpenClaw skill. +pub fn write_omtae_manifest(dir: &Path, manifest: &SkillManifest) -> Result<(), SkillError> { let toml_str = toml::to_string_pretty(manifest) .map_err(|e| SkillError::InvalidManifest(format!("TOML serialize: {e}")))?; std::fs::write(dir.join("skill.toml"), toml_str)?; diff --git a/crates/openfang-skills/src/registry.rs b/crates/openfang-skills/src/registry.rs index a9fb42e4e5..bb1c57aea6 100644 --- a/crates/openfang-skills/src/registry.rs +++ b/crates/openfang-skills/src/registry.rs @@ -21,7 +21,7 @@ pub struct SkillRegistry { /// Number of workspace skills blocked for critical prompt injection. blocked_skills_count: usize, /// User-supplied config values per skill name (from `[skills.]` in - /// `~/.openfang/config.toml`). Used by the loader to resolve declared + /// `~/.omtae/config.toml`). Used by the loader to resolve declared /// `config:` vars before injecting prompt context. skill_configs: HashMap>, } @@ -230,10 +230,10 @@ impl SkillRegistry { info!( skill = %converted.manifest.skill.name, - "Auto-converting SKILL.md to OpenFang format" + "Auto-converting SKILL.md to OMTAE format" ); if let Err(e) = - openclaw_compat::write_openfang_manifest(&path, &converted.manifest) + openclaw_compat::write_omtae_manifest(&path, &converted.manifest) { warn!("Failed to write skill.toml for {}: {e}", path.display()); continue; @@ -418,7 +418,7 @@ impl SkillRegistry { } if let Err(e) = - openclaw_compat::write_openfang_manifest(&path, &converted.manifest) + openclaw_compat::write_omtae_manifest(&path, &converted.manifest) { warn!("Failed to write skill.toml for {}: {e}", path.display()); continue; diff --git a/crates/openfang-types/Cargo.toml b/crates/openfang-types/Cargo.toml index 0c4ba71290..b75f2a1e1f 100644 --- a/crates/openfang-types/Cargo.toml +++ b/crates/openfang-types/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "openfang-types" +name = "omtae-types" version.workspace = true edition.workspace = true license.workspace = true -description = "Core types and traits for the OpenFang Agent OS" +description = "Core types and traits for the OMTAE Agent OS" [dependencies] serde = { workspace = true } @@ -18,6 +18,7 @@ ed25519-dalek = { workspace = true } sha2 = { workspace = true } hex = { workspace = true } rand = { workspace = true } +subtle = { workspace = true } bitflags = "2" [dev-dependencies] diff --git a/crates/openfang-types/src/agent.rs b/crates/openfang-types/src/agent.rs index 5589b1e35e..026456c874 100644 --- a/crates/openfang-types/src/agent.rs +++ b/crates/openfang-types/src/agent.rs @@ -485,7 +485,7 @@ pub struct AgentManifest { pub workspace: Option, /// Agent private state directory. Stores identity files (SOUL.md, etc.), /// AGENT.json, sessions/, and the daily memory log. Always lives under - /// `~/.openfang/workspaces/{name}/` regardless of where the user-facing + /// `~/.omtae/workspaces/{name}/` regardless of where the user-facing /// workspace points. Auto-derived on spawn. See issue #1097. #[serde(default)] pub state_dir: Option, @@ -611,10 +611,10 @@ pub struct SessionLabel(String); impl SessionLabel { /// Create a new validated session label. - pub fn new(label: &str) -> Result { + pub fn new(label: &str) -> Result { let trimmed = label.trim(); if trimmed.is_empty() || trimmed.len() > 128 { - return Err(crate::error::OpenFangError::InvalidInput( + return Err(crate::error::OMTAEError::InvalidInput( "Session label must be 1-128 chars".into(), )); } @@ -622,7 +622,7 @@ impl SessionLabel { .chars() .all(|c| c.is_alphanumeric() || c == ' ' || c == '-' || c == '_') { - return Err(crate::error::OpenFangError::InvalidInput( + return Err(crate::error::OMTAEError::InvalidInput( "Session label contains invalid chars".into(), )); } diff --git a/crates/openfang-types/src/approval.rs b/crates/openfang-types/src/approval.rs index 157efed270..8cbd416973 100644 --- a/crates/openfang-types/src/approval.rs +++ b/crates/openfang-types/src/approval.rs @@ -1,4 +1,4 @@ -//! Execution approval types for the OpenFang agent OS. +//! Execution approval types for the OMTAE agent OS. //! //! When an agent attempts a dangerous operation (e.g. `shell_exec`), the kernel //! creates an [`ApprovalRequest`] and pauses the agent until a human operator diff --git a/crates/openfang-types/src/capability.rs b/crates/openfang-types/src/capability.rs index fb50d7fdc2..cedfeddb12 100644 --- a/crates/openfang-types/src/capability.rs +++ b/crates/openfang-types/src/capability.rs @@ -1,6 +1,6 @@ //! Capability-based security types. //! -//! OpenFang uses capability-based security: an agent can only perform actions +//! OMTAE uses capability-based security: an agent can only perform actions //! that it has been explicitly granted permission to do. Capabilities are //! immutable after agent creation and enforced at the kernel level. @@ -54,7 +54,7 @@ pub enum Capability { /// Read environment variables matching the pattern. EnvRead(String), - // -- OFP (OpenFang Wire Protocol) -- + // -- OFP (OMTAE Wire Protocol) -- /// Can discover remote agents. OfpDiscover, /// Can connect to remote peers matching the pattern. @@ -87,10 +87,10 @@ impl CapabilityCheck { } /// Returns an error if denied, Ok(()) if granted. - pub fn require(&self) -> Result<(), crate::error::OpenFangError> { + pub fn require(&self) -> Result<(), crate::error::OMTAEError> { match self { Self::Granted => Ok(()), - Self::Denied(reason) => Err(crate::error::OpenFangError::CapabilityDenied( + Self::Denied(reason) => Err(crate::error::OMTAEError::CapabilityDenied( reason.clone(), )), } diff --git a/crates/openfang-types/src/commands.rs b/crates/openfang-types/src/commands.rs index 3c65117569..91f1725a68 100644 --- a/crates/openfang-types/src/commands.rs +++ b/crates/openfang-types/src/commands.rs @@ -4,8 +4,8 @@ //! dispatched across CLI, channel adapters (Telegram/Slack/etc.), and the web //! chat (WebSocket). //! -//! Each dispatch site (there are three: `openfang-cli/src/tui/mod.rs`, -//! `openfang-channels/src/bridge.rs`, `openfang-api/src/ws.rs`) retains its own +//! Each dispatch site (there are three: `omtae-cli/src/tui/mod.rs`, +//! `omtae-channels/src/bridge.rs`, `omtae-api/src/ws.rs`) retains its own //! handler logic. The registry is added as a front-door so command names and //! aliases can be canonicalised once and help / autocomplete is generated from //! a single list. @@ -13,7 +13,7 @@ //! # Example //! //! ``` -//! use openfang_types::commands::{self, Surfaces}; +//! use omtae_types::commands::{self, Surfaces}; //! //! let def = commands::resolve("NEW").expect("new is registered"); //! assert_eq!(def.name, "new"); @@ -98,12 +98,12 @@ pub struct CommandDef { pub requires_agent: bool, } -/// Every slash command registered in OpenFang. +/// Every slash command registered in OMTAE. /// /// Keep this list in sync with the three dispatch sites: -/// - `openfang-cli/src/tui/mod.rs::handle_slash_command` -/// - `openfang-channels/src/bridge.rs::handle_command` -/// - `openfang-api/src/ws.rs::handle_command` +/// - `omtae-cli/src/tui/mod.rs::handle_slash_command` +/// - `omtae-channels/src/bridge.rs::handle_command` +/// - `omtae-api/src/ws.rs::handle_command` pub const COMMAND_REGISTRY: &[CommandDef] = &[ // ── General ──────────────────────────────────────────────────────────── CommandDef { diff --git a/crates/openfang-types/src/config.rs b/crates/openfang-types/src/config.rs index 25df9b0059..808949a0e1 100644 --- a/crates/openfang-types/src/config.rs +++ b/crates/openfang-types/src/config.rs @@ -1,4 +1,4 @@ -//! Configuration types for the OpenFang kernel. +//! Configuration types for the OMTAE kernel. use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -582,7 +582,7 @@ pub struct DockerSandboxConfig { pub enabled: bool, /// Docker image for exec sandbox. Default: "python:3.12-slim". pub image: String, - /// Container name prefix. Default: "openfang-sandbox". + /// Container name prefix. Default: "omtae-sandbox". pub container_prefix: String, /// Working directory inside container. Default: "/workspace". pub workdir: String, @@ -637,7 +637,7 @@ impl Default for DockerSandboxConfig { Self { enabled: false, image: "python:3.12-slim".to_string(), - container_prefix: "openfang-sandbox".to_string(), + container_prefix: "omtae-sandbox".to_string(), workdir: "/workspace".to_string(), network: "none".to_string(), memory_limit: "512m".to_string(), @@ -719,7 +719,7 @@ impl Default for ExtensionsConfig { pub struct VaultConfig { /// Whether the vault is enabled (auto-detected if vault.enc exists). pub enabled: bool, - /// Custom vault file path (default: ~/.openfang/vault.enc). + /// Custom vault file path (default: ~/.omtae/vault.enc). pub path: Option, } @@ -732,6 +732,32 @@ impl Default for VaultConfig { } } +/// Obsidian brain vault configuration (`[brain]` in config.toml). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct BrainConfig { + /// Enable brain vault API and dashboard integration. + pub enabled: bool, + /// Obsidian vault root directory (default: `~/vaults/omtae-brain`). + pub path: Option, +} + +impl Default for BrainConfig { + fn default() -> Self { + Self { + enabled: true, + path: None, + } + } +} + +/// Default Obsidian brain vault path: `~/vaults/omtae-brain`. +pub fn default_brain_vault_path() -> PathBuf { + dirs::home_dir() + .unwrap_or_else(std::env::temp_dir) + .join("vaults/omtae-brain") +} + /// Agent binding — routes specific channel/account/peer patterns to agents. /// /// `deny_unknown_fields` so typos at the binding level (e.g. `match_rule` → @@ -753,7 +779,7 @@ pub struct AgentBinding { /// Single source of truth shared between: /// - Config validation (warn the user when their `channel_id` binding targets /// an adapter that doesn't populate `ctx.channel_id`). -/// - `ChannelMessage::channel_id()` in `openfang-channels::types` (routing-time +/// - `ChannelMessage::channel_id()` in `omtae-channels::types` (routing-time /// accessor that reads from this list to decide where to source the ID). /// /// Adapters not listed fall back to `metadata["channel_id"]` if present, then @@ -969,7 +995,7 @@ pub struct ExecPolicy { /// produce no stdout/stderr output for this duration. Default: 30. #[serde(default = "default_no_output_timeout")] pub no_output_timeout_secs: u64, - /// Environment variables to forward from the OpenFang process into + /// Environment variables to forward from the OMTAE process into /// `shell_exec` subprocesses. /// /// By default, subprocesses run with `env_clear()` and only receive a @@ -1143,9 +1169,9 @@ impl Default for ThinkingConfig { #[derive(Clone, Serialize, Deserialize)] #[serde(default)] pub struct KernelConfig { - /// OpenFang home directory (default: ~/.openfang). + /// OMTAE home directory (default: ~/.omtae). pub home_dir: PathBuf, - /// Data directory for databases (default: ~/.openfang/data). + /// Data directory for databases (default: ~/.omtae/data). pub data_dir: PathBuf, /// Log level (trace, debug, info, warn, error). pub log_level: String, @@ -1200,7 +1226,10 @@ pub struct KernelConfig { /// Credential vault configuration. #[serde(default)] pub vault: VaultConfig, - /// Root directory for agent workspaces. Default: `~/.openfang/workspaces` + /// Obsidian brain vault (markdown knowledge base). + #[serde(default)] + pub brain: BrainConfig, + /// Root directory for agent workspaces. Default: `~/.omtae/workspaces` #[serde(default)] pub workspaces_dir: Option, /// Media understanding configuration. @@ -1275,13 +1304,28 @@ pub struct KernelConfig { /// Dashboard authentication (username/password login). #[serde(default)] pub auth: AuthConfig, + /// Dashboard PIN gate (simple access for personal tunnels). + #[serde(default)] + pub dashboard: DashboardConfig, /// Directory for auto-loading workflow JSON files on startup. - /// Defaults to `~/.openfang/workflows`. Set to empty string to disable. + /// Defaults to `~/.omtae/workflows`. Set to empty string to disable. #[serde(default)] pub workflows_dir: Option, /// Heartbeat monitor settings. #[serde(default)] pub heartbeat: HeartbeatSettings, + /// Session compaction (LLM context management). See `[compaction]` in config.toml. + #[serde(default)] + pub compaction: CompactionSettings, + /// Agent autospawn allowlist. See `[agents]` in config.toml. + #[serde(default)] + pub agents: AgentsConfig, + /// Runtime drift watchdog (`[watchdog]` in config.toml). + #[serde(default)] + pub watchdog: WatchdogConfig, + /// ECC harness integration (`[ecc]` in config.toml). See docs/ECC-OMTAE.md. + #[serde(default)] + pub ecc: EccConfig, /// Per-skill runtime config (from `[skills.]` sections). /// /// When a skill declares a `config:` section in its SKILL.md frontmatter, @@ -1290,7 +1334,7 @@ pub struct KernelConfig { /// 2. env var named by the var's `env` field, /// 3. the var's `default`. /// - /// Example `~/.openfang/config.toml`: + /// Example `~/.omtae/config.toml`: /// ```toml /// [skills.github-repo-helper] /// github_token = "ghp_..." @@ -1321,6 +1365,98 @@ impl Default for HeartbeatSettings { } } +/// Dashboard PIN authentication (`[dashboard]` in config.toml). +/// +/// Intended for personal Cloudflare tunnel access only — not a substitute for +/// API keys on the public internet. See `OMTAE-TUNNEL.md`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct DashboardConfig { + /// 4–6 digit PIN. Stored in plaintext in config; change from any default. + pub pin: String, + /// When true and `pin` is non-empty, require PIN login for the dashboard/API. + pub require_pin: bool, +} + +impl Default for DashboardConfig { + fn default() -> Self { + Self { + pin: String::new(), + require_pin: false, + } + } +} + +impl DashboardConfig { + /// True when PIN auth is active (`require_pin` and a non-empty `pin`). + pub fn pin_auth_active(&self) -> bool { + self.require_pin && !self.pin.trim().is_empty() + } +} + +impl KernelConfig { + /// Dashboard session or password login is required. + pub fn dashboard_auth_enabled(&self) -> bool { + self.dashboard.pin_auth_active() || self.auth.enabled + } + + /// API key used by HTTP/WS bearer middleware (empty when PIN mode replaces it). + pub fn effective_api_key_for_auth(&self) -> String { + if self.dashboard.pin_auth_active() { + String::new() + } else { + self.api_key.trim().to_string() + } + } + + /// Resolved Obsidian brain vault root path. + pub fn brain_vault_path(&self) -> PathBuf { + self.brain + .path + .clone() + .unwrap_or_else(default_brain_vault_path) + } + + /// HMAC secret for `omtae_session` cookies. + pub fn dashboard_session_secret(&self) -> String { + if !self.effective_api_key_for_auth().is_empty() { + return self.effective_api_key_for_auth(); + } + if self.dashboard.pin_auth_active() { + return Self::pin_session_secret(&self.dashboard.pin); + } + if self.auth.enabled { + return self.auth.password_hash.clone(); + } + String::new() + } + + /// Derive a stable session signing key from the configured PIN. + pub fn pin_session_secret(pin: &str) -> String { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(b"omtae-dashboard-pin-v1:"); + hasher.update(pin.trim().as_bytes()); + hex::encode(hasher.finalize()) + } + + /// Constant-time PIN check against `[dashboard].pin`. + pub fn verify_dashboard_pin(&self, provided: &str) -> bool { + if !self.dashboard.pin_auth_active() { + return false; + } + let pin = provided.trim(); + let stored = self.dashboard.pin.trim(); + use subtle::ConstantTimeEq; + let a = pin.as_bytes(); + let b = stored.as_bytes(); + if a.len() != b.len() { + return false; + } + a.ct_eq(b).into() + } +} + /// Dashboard authentication (username/password login). #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] @@ -1330,7 +1466,7 @@ pub struct AuthConfig { /// Admin username. pub username: String, /// Argon2id password hash (PHC string format). - /// Generate with: openfang auth hash-password + /// Generate with: omtae auth hash-password pub password_hash: String, /// Session token lifetime in hours (default: 168 = 7 days). pub session_ttl_hours: u64, @@ -1491,7 +1627,7 @@ fn default_thread_ttl() -> u64 { impl Default for KernelConfig { fn default() -> Self { - let home_dir = openfang_home_dir(); + let home_dir = omtae_home_dir(); Self { data_dir: home_dir.join("data"), home_dir, @@ -1514,6 +1650,7 @@ impl Default for KernelConfig { browser: BrowserConfig::default(), extensions: ExtensionsConfig::default(), vault: VaultConfig::default(), + brain: BrainConfig::default(), workspaces_dir: None, media: crate::media::MediaConfig::default(), links: crate::media::LinkConfig::default(), @@ -1537,9 +1674,114 @@ impl Default for KernelConfig { provider_api_keys: HashMap::new(), oauth: OAuthConfig::default(), auth: AuthConfig::default(), + dashboard: DashboardConfig::default(), workflows_dir: None, heartbeat: HeartbeatSettings::default(), skills: HashMap::new(), + compaction: CompactionSettings::default(), + agents: AgentsConfig::default(), + watchdog: WatchdogConfig::default(), + ecc: EccConfig::default(), + } + } +} + +/// ECC (Everything Claude Code) integration for OMTAE (`[ecc]`). +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct EccConfig { + /// Enable ECC integration (runtime gates always on; this flag logs + documents intent). + pub enabled: bool, +} + +/// Drift watchdog — periodic self-check and safe auto-remediation (`[watchdog]`). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct WatchdogConfig { + /// Enable boot + periodic drift checks. + pub enabled: bool, + /// Interval between periodic checks (seconds). Default: 900 (15 minutes). + pub interval_secs: u64, + /// Cancel LLM runs / pause continuous loops stuck longer than this (seconds). + pub stuck_run_secs: u64, + /// Substrings expected in running agents' model id when `[default_model]` uses vLLM/Qwen. + pub expected_model_substrings: Vec, + /// Active checks: `autospawn`, `allowlist`, `stuck_run`, `continuous`, `model`. + pub checks: Vec, + /// When false (default), agents running outside `[agents] autospawn` are reported only — + /// never killed by the allowlist check or boot-time enforcement. + pub kill_disallowed: bool, + /// Agents allowed to run on demand (manual spawn) without autospawn or allowlist warnings. + /// Example: `["orchestrator"]` — spawn from the Agents tab when you need delegation. + pub manual_allowlist: Vec, +} + +fn default_watchdog_interval_secs() -> u64 { + 900 +} + +fn default_stuck_run_secs() -> u64 { + 900 +} + +impl Default for WatchdogConfig { + fn default() -> Self { + Self { + // Off by default — opt in via `[watchdog] enabled = true` in config.toml. + enabled: false, + interval_secs: default_watchdog_interval_secs(), + stuck_run_secs: default_stuck_run_secs(), + // Empty = derive patterns from `[default_model]` when the model check is enabled. + expected_model_substrings: Vec::new(), + checks: vec![ + "autospawn".into(), + "allowlist".into(), + "stuck_run".into(), + ], + kill_disallowed: false, + manual_allowlist: Vec::new(), + } + } +} + +/// LLM session compaction settings (`[compaction]` in config.toml). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct CompactionSettings { + /// Compact when message count reaches this value (default: 20). + pub threshold: Option, + /// Recent messages kept verbatim after compaction (default: 8). + pub keep_recent: Option, + /// Trigger compaction when estimated tokens exceed this fraction of the context window (default: 0.55–0.65). + pub token_threshold_ratio: Option, + /// Max tokens for the LLM compaction summary (default: 1024). + pub max_summary_tokens: Option, +} + +impl Default for CompactionSettings { + fn default() -> Self { + Self { + threshold: None, + keep_recent: None, + token_threshold_ratio: None, + max_summary_tokens: None, + } + } +} + +/// Agent directory autospawn policy (`[agents]` in config.toml). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct AgentsConfig { + /// If non-empty, only these agent folder names are auto-spawned on boot. + /// Other running agents are left alone unless `[watchdog] kill_disallowed = true`. + pub autospawn: Vec, +} + +impl Default for AgentsConfig { + fn default() -> Self { + Self { + autospawn: Vec::new(), } } } @@ -1612,6 +1854,14 @@ impl std::fmt::Debug for KernelConfig { .field("browser", &self.browser) .field("extensions", &self.extensions) .field("vault", &format!("enabled={}", self.vault.enabled)) + .field( + "brain", + &format!( + "enabled={} path={}", + self.brain.enabled, + self.brain_vault_path().display() + ), + ) .field("workspaces_dir", &self.workspaces_dir) .field( "media", @@ -1658,21 +1908,25 @@ impl std::fmt::Debug for KernelConfig { &format!("{} mapping(s)", self.provider_api_keys.len()), ) .field("auth", &format!("enabled={}", self.auth.enabled)) + .field( + "dashboard", + &format!("require_pin={}", self.dashboard.require_pin), + ) .field("skills", &format!("{} skill config(s)", self.skills.len())) .finish() } } -/// Resolve the OpenFang home directory. +/// Resolve the OMTAE home directory. /// -/// Priority: `OPENFANG_HOME` env var > `~/.openfang`. -fn openfang_home_dir() -> PathBuf { +/// Priority: `OPENFANG_HOME` env var > `~/.omtae`. +fn omtae_home_dir() -> PathBuf { if let Ok(home) = std::env::var("OPENFANG_HOME") { return PathBuf::from(home); } dirs::home_dir() .unwrap_or_else(std::env::temp_dir) - .join(".openfang") + .join(".omtae") } /// Default LLM model configuration. @@ -2132,7 +2386,7 @@ impl Default for SignalConfig { pub struct MatrixConfig { /// Matrix homeserver URL (e.g., `"https://matrix.org"`). pub homeserver_url: String, - /// Bot user ID (e.g., "@openfang:matrix.org"). + /// Bot user ID (e.g., "@omtae:matrix.org"). pub user_id: String, /// Env var name holding the access token. pub access_token_env: String, @@ -2295,7 +2549,7 @@ pub struct IrcConfig { pub nick: String, /// Env var name holding the server password (optional). pub password_env: Option, - /// Channels to join (e.g., `["#openfang", "#general"]`). + /// Channels to join (e.g., `["#omtae", "#general"]`). #[serde(default, deserialize_with = "deserialize_string_or_int_vec")] pub channels: Vec, /// Use TLS (requires tokio-native-tls). @@ -2312,7 +2566,7 @@ impl Default for IrcConfig { Self { server: "irc.libera.chat".to_string(), port: 6667, - nick: "openfang".to_string(), + nick: "omtae".to_string(), password_env: None, channels: vec![], use_tls: false, @@ -2375,7 +2629,7 @@ impl Default for TwitchConfig { Self { oauth_token_env: "TWITCH_OAUTH_TOKEN".to_string(), channels: vec![], - nick: "openfang".to_string(), + nick: "omtae".to_string(), default_agent: None, overrides: ChannelOverrides::default(), } @@ -2803,7 +3057,7 @@ impl Default for MqttConfig { Self { broker_url: "tcp://broker.hivemq.com:1883".to_string(), client_id: String::new(), - subscribe_topic: "openfang/inbox".to_string(), + subscribe_topic: "omtae/inbox".to_string(), publish_topic: String::new(), username_env: "MQTT_USERNAME".to_string(), password_env: "MQTT_PASSWORD".to_string(), @@ -3132,7 +3386,7 @@ impl Default for MumbleConfig { Self { host: String::new(), port: 64738, - username: "openfang".to_string(), + username: "omtae".to_string(), password_env: "MUMBLE_PASSWORD".to_string(), channel: String::new(), default_agent: None, @@ -4246,7 +4500,7 @@ mod tests { let irc = IrcConfig::default(); assert_eq!(irc.server, "irc.libera.chat"); assert_eq!(irc.port, 6667); - assert_eq!(irc.nick, "openfang"); + assert_eq!(irc.nick, "omtae"); assert!(!irc.use_tls); } @@ -4261,7 +4515,7 @@ mod tests { fn test_twitch_config_defaults() { let tw = TwitchConfig::default(); assert_eq!(tw.oauth_token_env, "TWITCH_OAUTH_TOKEN"); - assert_eq!(tw.nick, "openfang"); + assert_eq!(tw.nick, "omtae"); } #[test] diff --git a/crates/openfang-types/src/error.rs b/crates/openfang-types/src/error.rs index 4f6be01422..0fca24fe12 100644 --- a/crates/openfang-types/src/error.rs +++ b/crates/openfang-types/src/error.rs @@ -1,10 +1,10 @@ -//! Shared error types for the OpenFang system. +//! Shared error types for the OMTAE system. use thiserror::Error; -/// Top-level error type for the OpenFang system. +/// Top-level error type for the OMTAE system. #[derive(Error, Debug)] -pub enum OpenFangError { +pub enum OMTAEError { /// The requested agent was not found. #[error("Agent not found: {0}")] AgentNotFound(String), @@ -100,5 +100,5 @@ pub enum OpenFangError { InvalidInput(String), } -/// Alias for Result with OpenFangError. -pub type OpenFangResult = Result; +/// Alias for Result with OMTAEError. +pub type OMTAEResult = Result; diff --git a/crates/openfang-types/src/event.rs b/crates/openfang-types/src/event.rs index c3ab13d8d6..3b7575ac64 100644 --- a/crates/openfang-types/src/event.rs +++ b/crates/openfang-types/src/event.rs @@ -1,4 +1,4 @@ -//! Event types for the OpenFang internal event bus. +//! Event types for the OMTAE internal event bus. //! //! All inter-agent and system communication flows through events. @@ -294,7 +294,7 @@ pub enum SystemEvent { }, } -/// A complete event in the OpenFang event system. +/// A complete event in the OMTAE event system. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Event { /// Unique event ID. diff --git a/crates/openfang-types/src/lib.rs b/crates/openfang-types/src/lib.rs index de9df5975d..e90f9a1e49 100644 --- a/crates/openfang-types/src/lib.rs +++ b/crates/openfang-types/src/lib.rs @@ -1,6 +1,6 @@ -//! Core types and traits for the OpenFang Agent Operating System. +//! Core types and traits for the OMTAE Agent Operating System. //! -//! This crate defines all shared data structures used across the OpenFang kernel, +//! This crate defines all shared data structures used across the OMTAE kernel, //! runtime, memory substrate, and wire protocol. It contains no business logic. pub mod agent; diff --git a/crates/openfang-types/src/manifest_signing.rs b/crates/openfang-types/src/manifest_signing.rs index d1dc551bf9..13529a2c90 100644 --- a/crates/openfang-types/src/manifest_signing.rs +++ b/crates/openfang-types/src/manifest_signing.rs @@ -125,9 +125,9 @@ shell = false network = false "#; - let signed = SignedManifest::sign(manifest, &signing_key, "test@openfang.dev"); + let signed = SignedManifest::sign(manifest, &signing_key, "test@omtae.dev"); assert_eq!(signed.content_hash, hash_manifest(manifest)); - assert_eq!(signed.signer_id, "test@openfang.dev"); + assert_eq!(signed.signer_id, "test@omtae.dev"); assert!(signed.verify().is_ok()); } diff --git a/crates/openfang-types/src/media.rs b/crates/openfang-types/src/media.rs index c8f767c091..85d425fc94 100644 --- a/crates/openfang-types/src/media.rs +++ b/crates/openfang-types/src/media.rs @@ -84,7 +84,7 @@ pub struct MediaConfig { /// `audio_provider` semantics ("openai" or "groq" still selects which /// `*_API_KEY` env var is used for the Authorization header). /// - /// Closes . + /// Closes . /// /// Example: /// ```toml @@ -106,7 +106,7 @@ pub struct MediaConfig { /// header is still built from `OPENAI_API_KEY` (local services /// usually accept any non-empty bearer token). /// - /// Closes . + /// Closes . #[serde(default)] pub tts_openai_base_url: Option, @@ -118,7 +118,7 @@ pub struct MediaConfig { /// gateway. The `xi-api-key` header still comes from /// `ELEVENLABS_API_KEY`. /// - /// Closes . + /// Closes . #[serde(default)] pub tts_elevenlabs_base_url: Option, @@ -131,7 +131,7 @@ pub struct MediaConfig { /// format. The Authorization header is still built from /// `OPENAI_API_KEY`. /// - /// Closes . + /// Closes . #[serde(default)] pub image_gen_base_url: Option, } diff --git a/crates/openfang-types/src/memory.rs b/crates/openfang-types/src/memory.rs index b4ff501b2f..876863a453 100644 --- a/crates/openfang-types/src/memory.rs +++ b/crates/openfang-types/src/memory.rs @@ -268,7 +268,7 @@ pub trait Memory: Send + Sync { &self, agent_id: AgentId, key: &str, - ) -> crate::error::OpenFangResult>; + ) -> crate::error::OMTAEResult>; /// Set a key-value pair for a specific agent. async fn set( @@ -276,10 +276,10 @@ pub trait Memory: Send + Sync { agent_id: AgentId, key: &str, value: serde_json::Value, - ) -> crate::error::OpenFangResult<()>; + ) -> crate::error::OMTAEResult<()>; /// Delete a key-value pair for a specific agent. - async fn delete(&self, agent_id: AgentId, key: &str) -> crate::error::OpenFangResult<()>; + async fn delete(&self, agent_id: AgentId, key: &str) -> crate::error::OMTAEResult<()>; // -- Semantic operations -- @@ -291,7 +291,7 @@ pub trait Memory: Send + Sync { source: MemorySource, scope: &str, metadata: HashMap, - ) -> crate::error::OpenFangResult; + ) -> crate::error::OMTAEResult; /// Semantic search for relevant memories. async fn recall( @@ -299,39 +299,39 @@ pub trait Memory: Send + Sync { query: &str, limit: usize, filter: Option, - ) -> crate::error::OpenFangResult>; + ) -> crate::error::OMTAEResult>; /// Soft-delete a memory fragment. - async fn forget(&self, id: MemoryId) -> crate::error::OpenFangResult<()>; + async fn forget(&self, id: MemoryId) -> crate::error::OMTAEResult<()>; // -- Knowledge graph operations -- /// Add an entity to the knowledge graph. - async fn add_entity(&self, entity: Entity) -> crate::error::OpenFangResult; + async fn add_entity(&self, entity: Entity) -> crate::error::OMTAEResult; /// Add a relation between entities. - async fn add_relation(&self, relation: Relation) -> crate::error::OpenFangResult; + async fn add_relation(&self, relation: Relation) -> crate::error::OMTAEResult; /// Query the knowledge graph. async fn query_graph( &self, pattern: GraphPattern, - ) -> crate::error::OpenFangResult>; + ) -> crate::error::OMTAEResult>; // -- Maintenance -- /// Consolidate and optimize memory. - async fn consolidate(&self) -> crate::error::OpenFangResult; + async fn consolidate(&self) -> crate::error::OMTAEResult; /// Export all memory data. - async fn export(&self, format: ExportFormat) -> crate::error::OpenFangResult>; + async fn export(&self, format: ExportFormat) -> crate::error::OMTAEResult>; /// Import memory data. async fn import( &self, data: &[u8], format: ExportFormat, - ) -> crate::error::OpenFangResult; + ) -> crate::error::OMTAEResult; } #[cfg(test)] diff --git a/crates/openfang-types/src/message.rs b/crates/openfang-types/src/message.rs index ba5e93438d..6b0fa7d484 100644 --- a/crates/openfang-types/src/message.rs +++ b/crates/openfang-types/src/message.rs @@ -18,7 +18,7 @@ fn new_msg_id() -> String { pub struct Message { /// Server-assigned unique message ID (UUID v4). /// - /// Always generated by OpenFang at message creation time. LLM providers' + /// Always generated by OMTAE at message creation time. LLM providers' /// own `id` fields are unreliable (different formats per provider, can /// collide across sessions, sometimes absent), so we never use them as the /// primary key. The provider-supplied ID, when present, is preserved in diff --git a/crates/openfang-types/src/model_catalog.rs b/crates/openfang-types/src/model_catalog.rs index 1eb5985ee4..e09bacfc32 100644 --- a/crates/openfang-types/src/model_catalog.rs +++ b/crates/openfang-types/src/model_catalog.rs @@ -5,7 +5,7 @@ use std::fmt; // --------------------------------------------------------------------------- // Canonical provider base URLs — single source of truth. -// Referenced by openfang-runtime drivers, model catalog, and embedding modules. +// Referenced by omtae-runtime drivers, model catalog, and embedding modules. // --------------------------------------------------------------------------- pub const ANTHROPIC_BASE_URL: &str = "https://api.anthropic.com"; diff --git a/crates/openfang-types/src/scheduler.rs b/crates/openfang-types/src/scheduler.rs index e3a494f29e..4dd0082471 100644 --- a/crates/openfang-types/src/scheduler.rs +++ b/crates/openfang-types/src/scheduler.rs @@ -1,4 +1,4 @@ -//! Cron/scheduled job types for the OpenFang scheduler. +//! Cron/scheduled job types for the OMTAE scheduler. //! //! Defines the core types for recurring and one-shot scheduled jobs that can //! trigger agent turns, system events, or webhook deliveries. diff --git a/crates/openfang-types/src/tool_compat.rs b/crates/openfang-types/src/tool_compat.rs index 6deee74083..571028cc60 100644 --- a/crates/openfang-types/src/tool_compat.rs +++ b/crates/openfang-types/src/tool_compat.rs @@ -1,12 +1,12 @@ -//! Shared tool name mappings between OpenClaw and OpenFang. +//! Shared tool name mappings between OpenClaw and OMTAE. //! //! These mappings are used by both the migration engine and the skill system -//! to normalize OpenClaw tool names into OpenFang equivalents. +//! to normalize OpenClaw tool names into OMTAE equivalents. -/// Map an OpenClaw tool name to its OpenFang equivalent. +/// Map an OpenClaw tool name to its OMTAE equivalent. /// /// Returns `None` if the name has no known mapping (may already be -/// an OpenFang tool name — check with [`is_known_openfang_tool`]). +/// an OMTAE tool name — check with [`is_known_omtae_tool`]). pub fn map_tool_name(openclaw_name: &str) -> Option<&'static str> { match openclaw_name { // Claude-style tool names (capitalized) @@ -26,7 +26,7 @@ pub fn map_tool_name(openclaw_name: &str) -> Option<&'static str> { "memory_save" | "memory_store" => Some("memory_store"), "sessions_send" | "agent_message" => Some("agent_send"), "sessions_list" | "agents_list" | "agent_list" => Some("agent_list"), - "sessions_spawn" => Some("agent_send"), + "sessions_spawn" | "spawn_agent" => Some("agent_spawn"), // LLM-hallucinated aliases (fs-* style names) "fs-read" | "fs_read" | "fsRead" | "readFile" => Some("file_read"), @@ -40,20 +40,20 @@ pub fn map_tool_name(openclaw_name: &str) -> Option<&'static str> { } } -/// Normalize a tool name to its canonical OpenFang form. +/// Normalize a tool name to its canonical OMTAE form. /// -/// If the name is already a known OpenFang tool, returns it as-is. +/// If the name is already a known OMTAE tool, returns it as-is. /// Otherwise, tries to map it through [`map_tool_name`]. /// Returns the original name if no mapping is found. pub fn normalize_tool_name(name: &str) -> &str { - if is_known_openfang_tool(name) { + if is_known_omtae_tool(name) { return name; } map_tool_name(name).unwrap_or(name) } -/// Check if a tool name is a known OpenFang built-in tool. -pub fn is_known_openfang_tool(name: &str) -> bool { +/// Check if a tool name is a known OMTAE built-in tool. +pub fn is_known_omtae_tool(name: &str) -> bool { matches!( name, "file_read" @@ -127,7 +127,8 @@ mod tests { assert_eq!(map_tool_name("sessions_list"), Some("agent_list")); assert_eq!(map_tool_name("agents_list"), Some("agent_list")); assert_eq!(map_tool_name("agent_list"), Some("agent_list")); - assert_eq!(map_tool_name("sessions_spawn"), Some("agent_send")); + assert_eq!(map_tool_name("sessions_spawn"), Some("agent_spawn")); + assert_eq!(map_tool_name("spawn_agent"), Some("agent_spawn")); // LLM-hallucinated fs-* aliases assert_eq!(map_tool_name("fs-read"), Some("file_read")); @@ -166,7 +167,7 @@ mod tests { #[test] fn test_normalize_tool_name() { - // Known OpenFang tools pass through unchanged + // Known OMTAE tools pass through unchanged assert_eq!(normalize_tool_name("file_read"), "file_read"); assert_eq!(normalize_tool_name("file_write"), "file_write"); assert_eq!(normalize_tool_name("shell_exec"), "shell_exec"); @@ -186,7 +187,7 @@ mod tests { } #[test] - fn test_is_known_openfang_tool() { + fn test_is_known_omtae_tool() { // All 23 built-in tools + location_get let known = [ "file_read", @@ -216,12 +217,12 @@ mod tests { "location_get", ]; for tool in &known { - assert!(is_known_openfang_tool(tool), "Expected {tool} to be known"); + assert!(is_known_omtae_tool(tool), "Expected {tool} to be known"); } // Unknown - assert!(!is_known_openfang_tool("unknown")); - assert!(!is_known_openfang_tool("Read")); - assert!(!is_known_openfang_tool("Bash")); + assert!(!is_known_omtae_tool("unknown")); + assert!(!is_known_omtae_tool("Read")); + assert!(!is_known_omtae_tool("Bash")); } } diff --git a/crates/openfang-wire/Cargo.toml b/crates/openfang-wire/Cargo.toml index 438363ba40..6a18dd8ba7 100644 --- a/crates/openfang-wire/Cargo.toml +++ b/crates/openfang-wire/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "openfang-wire" +name = "omtae-wire" version.workspace = true edition.workspace = true license.workspace = true -description = "OpenFang Protocol (OFP) — agent-to-agent networking" +description = "OMTAE Protocol (OFP) — agent-to-agent networking" [dependencies] -openfang-types = { path = "../openfang-types" } +omtae-types = { path = "../omtae-types" } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/openfang-wire/src/lib.rs b/crates/openfang-wire/src/lib.rs index 6ef2fa2639..623ee3760c 100644 --- a/crates/openfang-wire/src/lib.rs +++ b/crates/openfang-wire/src/lib.rs @@ -1,4 +1,4 @@ -//! OpenFang Wire Protocol (OFP) — agent-to-agent networking. +//! OMTAE Wire Protocol (OFP) — agent-to-agent networking. //! //! Provides cross-machine agent discovery, authentication, and communication //! over TCP connections using a JSON-RPC framed protocol. diff --git a/crates/openfang-wire/src/message.rs b/crates/openfang-wire/src/message.rs index 89901f9914..4bd5b47656 100644 --- a/crates/openfang-wire/src/message.rs +++ b/crates/openfang-wire/src/message.rs @@ -1,6 +1,6 @@ //! Wire protocol message types. //! -//! All communication between OpenFang peers uses JSON-framed messages +//! All communication between OMTAE peers uses JSON-framed messages //! over TCP. Each message is prefixed with a 4-byte big-endian length header. use serde::{Deserialize, Serialize}; diff --git a/crates/openfang-wire/src/peer.rs b/crates/openfang-wire/src/peer.rs index 677018dd38..616cd7057b 100644 --- a/crates/openfang-wire/src/peer.rs +++ b/crates/openfang-wire/src/peer.rs @@ -1,7 +1,7 @@ -//! PeerNode — TCP server and client for the OpenFang Wire Protocol. +//! PeerNode — TCP server and client for the OMTAE Wire Protocol. //! //! A [`PeerNode`] binds a local TCP listener and accepts incoming connections -//! from other OpenFang kernels. It also connects outward to known peers. Each +//! from other OMTAE kernels. It also connects outward to known peers. Each //! connection performs a handshake to exchange identity and agent lists, then //! enters a message dispatch loop. //! @@ -58,7 +58,7 @@ impl NonceTracker { if self.seen.contains_key(nonce) { return Err(format!( "Nonce replay detected: {}", - openfang_types::truncate_str(nonce, 16) + omtae_types::truncate_str(nonce, 16) )); } @@ -126,7 +126,7 @@ impl Default for PeerConfig { Self { listen_addr: "127.0.0.1:0".parse().unwrap(), node_id: uuid::Uuid::new_v4().to_string(), - node_name: "openfang-node".to_string(), + node_name: "omtae-node".to_string(), shared_secret: String::new(), } } @@ -1227,7 +1227,7 @@ mod tests { #[test] fn test_peer_config_default() { let config = PeerConfig::default(); - assert_eq!(config.node_name, "openfang-node"); + assert_eq!(config.node_name, "omtae-node"); assert!(!config.node_id.is_empty()); } diff --git a/deploy/openfang.service b/deploy/openfang.service index 682f94255a..9dd4406373 100644 --- a/deploy/openfang.service +++ b/deploy/openfang.service @@ -1,27 +1,27 @@ [Unit] -Description=OpenFang Agent OS Daemon -Documentation=https://github.com/openfang-ai/openfang +Description=OMTAE Agent OS Daemon +Documentation=https://github.com/omtae-ai/omtae After=network-online.target Wants=network-online.target [Service] Type=simple -User=openfang -Group=openfang -ExecStart=/usr/local/bin/openfang start +User=omtae +Group=omtae +ExecStart=/usr/local/bin/omtae start Restart=on-failure RestartSec=5 TimeoutStopSec=30 # Environment -EnvironmentFile=-/etc/openfang/env -WorkingDirectory=/var/lib/openfang +EnvironmentFile=-/etc/omtae/env +WorkingDirectory=/var/lib/omtae # Security hardening NoNewPrivileges=true ProtectSystem=strict ProtectHome=true -ReadWritePaths=/var/lib/openfang +ReadWritePaths=/var/lib/omtae PrivateTmp=true ProtectKernelTunables=true ProtectKernelModules=true diff --git a/docker-compose.yml b/docker-compose.yml index f5717fa2a2..d1a8620f8f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,16 @@ -# NOTE: The GHCR image (ghcr.io/rightnow-ai/openfang) is not yet public. +# NOTE: The GHCR image (ghcr.io/rightnow-ai/omtae) is not yet public. # For now, build from source using `docker compose up --build`. -# See: https://github.com/RightNow-AI/openfang/issues/12 +# See: https://github.com/RightNow-AI/omtae/issues/12 version: "3.8" services: - openfang: + omtae: build: . - # image: ghcr.io/rightnow-ai/openfang:latest # Uncomment when GHCR is public + # image: ghcr.io/rightnow-ai/omtae:latest # Uncomment when GHCR is public ports: - "4200:4200" volumes: - - openfang-data:/data + - omtae-data:/data # Uncomment to reach host services (Ollama, whisper.cpp, local Postgres) # from inside the container. Required on Linux and colima. See # docs/troubleshooting.md#connecting-to-host-services-from-docker. @@ -27,4 +27,4 @@ services: restart: unless-stopped volumes: - openfang-data: + omtae-data: diff --git a/docs/ECC-OMTAE.md b/docs/ECC-OMTAE.md new file mode 100644 index 0000000000..c1af186728 --- /dev/null +++ b/docs/ECC-OMTAE.md @@ -0,0 +1,82 @@ +# ECC + OMTAE Integration + +[ECC](https://github.com/affaan-m/ECC) (Everything Claude Code) is a harness toolkit for Cursor: skills, hooks, loop guards, and verification. OMTAE embeds ECC-inspired **runtime gates** plus adapted skills for the daemon on `:4200`. + +## What OMTAE Enforces in Code + +| Gate | Location | Behavior | +|------|----------|----------| +| **TOOL_REQUIRED** | `loop_guard.rs` + `agent_loop.rs` | Blocks factual claims (paths, URLs, status, lists, counts) without tool evidence | +| **Planning loop** | `loop_guard.rs` | Threshold 2 for repeated planning / meta-narration | +| **Tool-first prompt** | `prompt_builder.rs` | Injected for all agents with tools | +| **Research integrity** | `agent_loop.rs` | Disclaimer on unverified web lists | + +Runtime gates run on every agent message — not only when ECC skills are installed. + +## Install ECC Skills (OMTAE format) + +From the openfang-core repo: + +```bash +chmod +x scripts/install-ecc-skills.sh +./scripts/install-ecc-skills.sh +``` + +Skills land in `~/.omtae/skills/`: + +- `verification-loop` — tool evidence checklist +- `search-first` — local → API → web order +- `research-ops` — fact / inference labels +- `no-fake-tools` — no JSON-in-text or phantom execution +- `tool-discipline` — per-turn tool contract + +Restart the daemon after install: + +```bash +systemctl --user restart omtae-daemon +# or kill + start if binary is locked +``` + +## Enable in Kernel Config + +Add to `~/.omtae/config.toml` or your kernel TOML (e.g. `openfang-kernel.toml`): + +```toml +[ecc] +enabled = true +``` + +When `enabled = true`, the kernel logs ECC integration at boot. Skills still load from `~/.omtae/skills/` via the normal skill registry — run `install-ecc-skills.sh` once. + +## Cursor ECC + OMTAE Daemon Together + +| Layer | Cursor ECC | OMTAE Daemon | +|-------|------------|--------------| +| IDE sessions | ECC skills, hooks, rules in Cursor | — | +| Agent OS (`:4200`) | — | Runtime gates + `~/.omtae/skills/ecc-*` | +| Shared discipline | verification-loop, search-first | Same concepts, OMTAE tool names | + +**Recommended workflow** + +1. Use ECC in Cursor for coding (hooks, `/verify`, skills). +2. Run `./scripts/install-ecc-skills.sh` so daemon agents get matching SKILL.md guidance. +3. Set `[ecc] enabled = true` in kernel config. +4. Deploy agent manifests with TOOL-FIRST rules (`agents/researcher/agent.toml`, etc.). +5. Live-test: POST to researcher with a brain question — response must show `shell_exec` / `file_list` evidence or `TOOL_REQUIRED` stop. + +## Brain Vault Live Test + +```bash +AGENT_ID=$(curl -s http://127.0.0.1:4200/api/agents | python3 -c "import sys,json; print([a for a in json.load(sys.stdin) if a.get('name')=='researcher'][0]['id'])") + +curl -s -X POST "http://127.0.0.1:4200/api/agents/${AGENT_ID}/message" \ + -H "Content-Type: application/json" \ + -d '{"message": "What is in my Obsidian brain vault? Use tools — do not guess peer agents."}' +``` + +Pass: tool calls in the trace or response cites `/api/brain/*` / `file_list` output. +Fail: peer-agent narration, "let me check" loops, or invented paths without tools. + +## ecc-bridge Agent (optional) + +`agents/ecc-bridge/agent.toml` is a thin chat agent with ECC tool-discipline prompts preloaded. Copy to `~/.omtae/agents/` and spawn when you want a dedicated verification-focused session. diff --git a/docs/MODEL_SWITCHING.md b/docs/MODEL_SWITCHING.md new file mode 100644 index 0000000000..a8554de848 --- /dev/null +++ b/docs/MODEL_SWITCHING.md @@ -0,0 +1,86 @@ +# OMTAE local model switching (vLLM + desk) + +Jay's stack: **vLLM :8000**, **OMTAE desk :4200**, dual RTX 3090. Only one vLLM weights bundle can run at a time. + +## One command (full switch) + +```bash +omtae-model use qwen-coder-32b # default coder AWQ +omtae-model use qwen36-obliterated-27b # after HF download completes +``` + +This will: + +1. Write `~/.omtae/active_model.txt` +2. Set `[default_model].model` in `~/.omtae/config.toml` +3. Point `omtae-vllm.service` at the profile's `start_script` and restart vLLM +4. POST `/api/config/reload` when the desk API is up + +## List / status + +```bash +omtae-model list +omtae-model status +``` + +## Files + +| File | Purpose | +|------|---------| +| `~/.omtae/models.toml` | Profile definitions (paths, TP, served name) | +| `~/.omtae/active_model.txt` | Last `omtae-model use` profile id | +| `~/.omtae/custom_models.json` | Catalog entries for dashboard/chat | +| `~/.omtae/config.toml` | `[default_model]` used by agents without override | + +Install CLI helper: + +```bash +cp scripts/omtae-model ~/.local/bin/omtae-model && chmod +x ~/.local/bin/omtae-model +cp scripts/models.toml.example ~/.omtae/models.toml +``` + +## Dashboard (one click) + +**Runtime** page → **Local vLLM profile** card → choose profile → **Apply to OMTAE**. + +That updates the OMTAE default model immediately. For a **full** switch (restart vLLM on :8000), run the CLI command shown after Apply, or enable `OMTAE_ALLOW_VLLM_RESTART=1` on the daemon and check **Restart vLLM** before Apply. + +## API + +```bash +curl -s http://127.0.0.1:4200/api/models/profiles +curl -s http://127.0.0.1:4200/api/models/active +curl -s -X PUT http://127.0.0.1:4200/api/models/active \ + -H "Content-Type: application/json" \ + -H "X-OMTAE-Pin: YOUR_PIN" \ + -d '{"profile_id":"qwen-coder-32b"}' +``` + +## Agents (`agent.toml`) + +The `model` field must match the **served** id (same as `omtae_model_id` in `models.toml` / `custom_models.json`), or omit it to use `[default_model]` from config. + +```toml +# Matches vLLM --served-model-name / custom_models.json id +model = "Qwen2.5-Coder-32B-Instruct-AWQ" +``` + +After switching to Qwen3.6: + +```toml +model = "OBLITERATUS/Qwen3.6-27B-OBLITERATED" +``` + +Aliases from `custom_models.json` (e.g. `qwen36-obliterated`) also work in chat when registered in the catalog. + +## Qwen3.6 download (optional profile) + +```bash +hf download OBLITERATUS/Qwen3.6-27B-OBLITERATED \ + --local-dir ~/llm-models/Qwen3.6-27B-OBLITERATED \ + --exclude 'gguf/*' +``` + +BF16 weights (~51GB) need **runtime FP8** (`--quantization fp8`) on 2×3090 for stable load. Verified profile: **32k** context (`max_model_len=32768`, `gpu_memory_utilization=0.85`, TP=2). Tradeoffs: GPUs run hot/near-full (~24GB/GPU), KV pool allows ~2 concurrent 32k requests max; full BF16 without FP8 usually OOMs. Start: `~/start-vllm-qwen36-obliterated.sh` (env: `OMTAE_QWEN36_MAX_LEN`, `OMTAE_VLLM_GPU_MEM`, `OMTAE_VLLM_QUANT`). + +Qwen2.5 Coder AWQ stays at **16k** default in `~/start-vllm-qwen-coder.sh`; 32k on AWQ 32B is untested on this host—try `OMTAE_CODER_MAX_LEN=32768 OMTAE_VLLM_GPU_MEM=0.72` only if you accept OOM risk. diff --git a/docs/README.md b/docs/README.md index 4ea86d4bf9..40c7d6a4f9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ -# OpenFang Documentation +# OMTAE Documentation -Welcome to the OpenFang documentation. OpenFang is the open-source Agent Operating System -- 14 Rust crates, 40 channels, 60 skills, 20 LLM providers, 76 API endpoints, and 16 security systems in a single binary. +Welcome to the OMTAE documentation. OMTAE is the open-source Agent Operating System -- 14 Rust crates, 40 channels, 60 skills, 20 LLM providers, 76 API endpoints, and 16 security systems in a single binary. --- @@ -61,7 +61,7 @@ Welcome to the OpenFang documentation. OpenFang is the open-source Agent Operati ```bash export GROQ_API_KEY="your-key" -openfang init && openfang start +omtae init && omtae start # Open http://127.0.0.1:4200 ``` @@ -85,10 +85,10 @@ openfang init && openfang start | Path | Description | |------|-------------| -| `~/.openfang/config.toml` | Main configuration file | -| `~/.openfang/data/openfang.db` | SQLite database | -| `~/.openfang/skills/` | Installed skills | -| `~/.openfang/daemon.json` | Daemon PID and port info | +| `~/.omtae/config.toml` | Main configuration file | +| `~/.omtae/data/omtae.db` | SQLite database | +| `~/.omtae/skills/` | Installed skills | +| `~/.omtae/daemon.json` | Daemon PID and port info | | `agents/` | Agent template manifests | ### Key Environment Variables diff --git a/docs/VERTEX_AI_LOCAL_TESTING.md b/docs/VERTEX_AI_LOCAL_TESTING.md index 124636282a..89840a0a2d 100644 --- a/docs/VERTEX_AI_LOCAL_TESTING.md +++ b/docs/VERTEX_AI_LOCAL_TESTING.md @@ -11,7 +11,7 @@ ### Option 1: Use the Batch File ```batch -# Run this from the openfang directory: +# Run this from the omtae directory: start-vertex.bat ``` @@ -20,13 +20,13 @@ This automatically: - Sets `GOOGLE_APPLICATION_CREDENTIALS` - Pre-fetches OAuth token via `gcloud auth print-access-token` - Sets `VERTEX_AI_ACCESS_TOKEN` env var -- Starts OpenFang +- Starts OMTAE ### Option 2: Manual PowerShell Setup ```powershell # 1. Kill any existing instances -taskkill /F /IM openfang.exe 2>$null +taskkill /F /IM omtae.exe 2>$null # 2. Set environment variables (CRITICAL: clear proxy!) $env:HTTPS_PROXY = "" @@ -36,9 +36,9 @@ $env:GOOGLE_APPLICATION_CREDENTIALS = "C:\Users\at384\Downloads\osc\dbg-grcit-de # 3. Pre-fetch OAuth token (IMPORTANT: avoids subprocess issues on Windows) $env:VERTEX_AI_ACCESS_TOKEN = gcloud auth print-access-token -# 4. Start OpenFang -cd C:\Users\at384\Downloads\osc\dllm\openfang -.\target\debug\openfang.exe start +# 4. Start OMTAE +cd C:\Users\at384\Downloads\osc\dllm\omtae +.\target\debug\omtae.exe start ``` ## Testing the API @@ -65,7 +65,7 @@ $response = Invoke-RestMethod -Uri "http://127.0.0.1:50051/v1/chat/completions" Write-Host $response.choices[0].message.content ``` -### Direct Vertex AI Test (Bypass OpenFang) +### Direct Vertex AI Test (Bypass OMTAE) ```powershell $env:HTTPS_PROXY = "" @@ -83,7 +83,7 @@ Invoke-RestMethod -Uri $url -Method POST -Headers @{Authorization = "Bearer $tok ## Configuration -### ~/.openfang/config.toml +### ~/.omtae/config.toml ```toml [default_model] @@ -120,7 +120,7 @@ $env:VERTEX_AI_ACCESS_TOKEN = gcloud auth print-access-token ### "Connection refused" -**Cause:** OpenFang not running or wrong port. +**Cause:** OMTAE not running or wrong port. **Solution:** Ensure server is running on port 50051: ```powershell @@ -139,20 +139,20 @@ $env:VERTEX_AI_ACCESS_TOKEN = gcloud auth print-access-token ## Build Commands ```powershell -cd C:\Users\at384\Downloads\osc\dllm\openfang +cd C:\Users\at384\Downloads\osc\dllm\omtae $env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH" # Debug build (faster compilation) -cargo build -p openfang-cli +cargo build -p omtae-cli # Run tests -cargo test -p openfang-runtime --lib vertex +cargo test -p omtae-runtime --lib vertex # Check formatting -cargo fmt --check -p openfang-runtime +cargo fmt --check -p omtae-runtime # Run clippy -cargo clippy -p openfang-runtime --lib -- -W warnings +cargo clippy -p omtae-runtime --lib -- -W warnings ``` ## API Endpoints @@ -167,5 +167,5 @@ cargo clippy -p openfang-runtime --lib -- -W warnings ## Files Modified in PR -- `crates/openfang-runtime/src/drivers/vertex.rs` (NEW - ~790 lines) -- `crates/openfang-runtime/src/drivers/mod.rs` (+62 lines) +- `crates/omtae-runtime/src/drivers/vertex.rs` (NEW - ~790 lines) +- `crates/omtae-runtime/src/drivers/mod.rs` (+62 lines) diff --git a/docs/agent-templates.md b/docs/agent-templates.md index d7fcf28a1f..886d8e489a 100644 --- a/docs/agent-templates.md +++ b/docs/agent-templates.md @@ -1,15 +1,15 @@ # Agent Templates Catalog -OpenFang ships with **30 pre-built agent templates** organized into 4 performance tiers. Each template is a ready-to-spawn `agent.toml` manifest located in the `agents/` directory. Templates cover software engineering, business operations, personal productivity, and everyday tasks. +OMTAE ships with **30 pre-built agent templates** organized into 4 performance tiers. Each template is a ready-to-spawn `agent.toml` manifest located in the `agents/` directory. Templates cover software engineering, business operations, personal productivity, and everyday tasks. ## Quick Start Spawn any template from the CLI: ```bash -openfang spawn orchestrator -openfang spawn coder -openfang spawn --template agents/writer/agent.toml +omtae spawn orchestrator +omtae spawn coder +omtae spawn --template agents/writer/agent.toml ``` Spawn via the REST API: @@ -118,12 +118,13 @@ The orchestrator is the command center of the agent fleet. It analyzes user requ - **Temperature**: 0.3 - **Max tokens**: 8192 - **Token quota**: 500,000/hour -- **Schedule**: Continuous check every 120 seconds +- **Schedule**: Reactive (request-driven). Do not enable `[schedule] continuous` — it causes autonomous background ticks that look like an infinite loop. Use the Scheduler tab for timed runs. +- **Max iterations**: 25 tool rounds per message (via `[autonomous]`) - **Tools**: `agent_send`, `agent_spawn`, `agent_list`, `agent_kill`, `memory_store`, `memory_recall`, `file_read`, `file_write` - **Capabilities**: `agent_spawn = true`, `agent_message = ["*"]`, `memory_read = ["*"]`, `memory_write = ["*"]` ```bash -openfang spawn orchestrator +omtae spawn orchestrator # "Plan and execute a full security audit of the codebase" ``` @@ -145,7 +146,7 @@ Designs systems following principles of separation of concerns, performance-awar - **Capabilities**: `agent_message = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn architect +omtae spawn architect # "Design a microservices architecture for the payment processing system" ``` @@ -169,7 +170,7 @@ Focuses on OWASP Top 10, input validation, auth flaws, cryptographic misuse, inj - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn security-auditor +omtae spawn security-auditor # "Audit the authentication module for vulnerabilities" ``` @@ -193,7 +194,7 @@ Writes clean, production-quality code with a step-by-step reasoning approach. Re - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*"]` ```bash -openfang spawn coder +omtae spawn coder # "Implement a rate limiter using the token bucket algorithm in Rust" ``` @@ -216,7 +217,7 @@ Reviews code by priority: correctness, security, performance, maintainability, s - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn code-reviewer +omtae spawn code-reviewer # "Review the changes in the last 3 commits for production readiness" ``` @@ -239,7 +240,7 @@ Follows a structured methodology: understand the question, explore data (shape, - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn data-scientist +omtae spawn data-scientist # "Analyze this CSV dataset and identify the top 3 factors correlated with churn" ``` @@ -262,7 +263,7 @@ Follows a strict methodology: reproduce, isolate (binary search through code/dat - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn debugger +omtae spawn debugger # "The API returns 500 on POST /api/agents when the name contains unicode -- find the root cause" ``` @@ -284,7 +285,7 @@ Fetches web pages, reads documents, and synthesizes findings into clear, structu - **Capabilities**: `network = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn researcher +omtae spawn researcher # "Research the current state of WebAssembly component model and summarize the key proposals" ``` @@ -307,7 +308,7 @@ Analyzes data, finds patterns, generates insights, and creates structured report - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn analyst +omtae spawn analyst # "Analyze the server access logs and report traffic patterns by hour and endpoint" ``` @@ -330,7 +331,7 @@ Tests document behavior, not implementation. Prefers fast, deterministic tests. - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn test-engineer +omtae spawn test-engineer # "Write comprehensive tests for the rate limiter module covering edge cases" ``` @@ -353,7 +354,7 @@ Systematically reviews contracts covering parties, termination provisions, payme - **Capabilities**: `network = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn legal-assistant +omtae spawn legal-assistant # "Review this NDA and flag any one-sided or problematic clauses" ``` @@ -375,7 +376,7 @@ Follows a structured methodology: scope (in/out), decompose (epics to stories to - **Capabilities**: `agent_message = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn planner +omtae spawn planner # "Create a project plan for migrating our monolith to microservices over 6 months" ``` @@ -397,7 +398,7 @@ Excels at documentation, technical writing, blog posts, and clear communication. - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*"]` ```bash -openfang spawn writer +omtae spawn writer # "Write a blog post about the benefits of agent-based architectures" ``` @@ -419,7 +420,7 @@ Writes for the reader: starts with WHY, then WHAT, then HOW. Uses progressive di - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn doc-writer +omtae spawn doc-writer # "Write API documentation for all the /api/agents endpoints" ``` @@ -442,7 +443,7 @@ Covers CI/CD pipeline design, container orchestration (Docker, Kubernetes), Infr - **Capabilities**: `agent_message = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn devops-lead +omtae spawn devops-lead # "Design a CI/CD pipeline for our Rust workspace with staging and production environments" ``` @@ -452,7 +453,7 @@ openfang spawn devops-lead **Tier 3 -- Balanced** | `groq/llama-3.3-70b-versatile` | Fallback: `gemini/gemini-2.0-flash` -> General-purpose assistant. The default OpenFang agent for everyday tasks, questions, and conversations. +> General-purpose assistant. The default OMTAE agent for everyday tasks, questions, and conversations. The versatile default agent covering conversational intelligence, task execution, research and synthesis, writing and communication, problem solving, agent delegation (routes specialized tasks to the right specialist), knowledge management, and creative brainstorming. Acts as the user's trusted first point of contact -- handles most tasks directly and delegates to specialists when they would do better. @@ -466,7 +467,7 @@ The versatile default agent covering conversational intelligence, task execution - **Capabilities**: `network = ["*"]`, `agent_message = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn assistant +omtae spawn assistant # "Help me plan my week and draft replies to these three emails" ``` @@ -489,7 +490,7 @@ Rapidly triages incoming email by urgency, category, and required action. Drafts - **Capabilities**: `network = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn email-assistant +omtae spawn email-assistant # "Triage these 15 emails and draft responses for the urgent ones" ``` @@ -512,7 +513,7 @@ Crafts platform-optimized content for Twitter/X, LinkedIn, Instagram, Facebook, - **Capabilities**: `network = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn social-media +omtae spawn social-media # "Create a week of LinkedIn posts about our open-source launch" ``` @@ -535,7 +536,7 @@ Triages support tickets by category, severity, product area, and customer tier. - **Capabilities**: `network = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn customer-support +omtae spawn customer-support # "Triage this batch of support tickets and draft responses for the top 5 urgent ones" ``` @@ -558,7 +559,7 @@ Drafts personalized cold outreach emails using the AIDA framework. Manages CRM d - **Capabilities**: `network = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn sales-assistant +omtae spawn sales-assistant # "Draft a 3-touch outreach sequence for CTOs at mid-market SaaS companies" ``` @@ -581,7 +582,7 @@ Evaluates resumes against job requirements with structured match scoring. Writes - **Capabilities**: `network = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn recruiter +omtae spawn recruiter # "Screen these 10 resumes against the senior backend engineer job requirements" ``` @@ -604,7 +605,7 @@ Creates structured, time-boxed agendas. Transforms raw meeting notes or transcri - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn meeting-assistant +omtae spawn meeting-assistant # "Process this meeting transcript and extract all action items with owners and deadlines" ``` @@ -628,7 +629,7 @@ Monitors system health, runs diagnostics, and helps with deployments. Precise an - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*"]` ```bash -openfang spawn ops +omtae spawn ops # "Check disk usage, memory, and running containers" ``` @@ -650,7 +651,7 @@ The simplest agent template -- a minimal starter agent with basic read-only capa - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*"]`, `agent_spawn = false` ```bash -openfang spawn hello-world +omtae spawn hello-world # "Hello! What can you do?" ``` @@ -673,7 +674,7 @@ Translates between 20+ major languages with high fidelity to meaning, tone, and - **Capabilities**: `network = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn translator +omtae spawn translator # "Translate this README from English to Japanese and Spanish, preserving code blocks" ``` @@ -697,7 +698,7 @@ Explains concepts at the learner's level using the Feynman Technique. Uses Socra - **Capabilities**: `network = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn tutor +omtae spawn tutor # "Teach me how binary search trees work, starting from the basics" ``` @@ -721,7 +722,7 @@ Tracks weight, blood pressure, heart rate, sleep, water intake, steps, mood, and - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*"]` ```bash -openfang spawn health-tracker +omtae spawn health-tracker # "Log today's metrics: weight 175lbs, sleep 7.5 hours, mood 8/10, 8000 steps" ``` @@ -745,7 +746,7 @@ Creates detailed budgets using frameworks like 50/30/20, zero-based budgeting, a - **Capabilities**: `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn personal-finance +omtae spawn personal-finance # "Analyze this month's expense CSV and show me where I'm over budget" ``` @@ -768,7 +769,7 @@ Builds day-by-day itineraries with estimated times, transportation, meal recomme - **Capabilities**: `network = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn travel-planner +omtae spawn travel-planner # "Plan a 10-day trip to Japan for 2 people, mid-range budget, mix of culture and food" ``` @@ -792,7 +793,7 @@ Manages smart home devices (lights, thermostats, security, appliances, sensors). - **Capabilities**: `network = ["*"]`, `memory_read = ["*"]`, `memory_write = ["self.*", "shared.*"]` ```bash -openfang spawn home-automation +omtae spawn home-automation # "Create a bedtime automation: lock doors, arm cameras, dim lights, set thermostat to 68F" ``` @@ -896,22 +897,22 @@ shell = ["python *", "cargo *"] # Allowed shell command patterns (whitelist) ```bash # Spawn by template name -openfang spawn coder +omtae spawn coder # Spawn with a custom name -openfang spawn coder --name "backend-coder" +omtae spawn coder --name "backend-coder" # Spawn from a TOML file path -openfang spawn --template agents/custom/my-agent.toml +omtae spawn --template agents/custom/my-agent.toml # List running agents -openfang agents +omtae agents # Send a message -openfang message "Write a function to parse TOML files" +omtae message "Write a function to parse TOML files" # Kill an agent -openfang kill +omtae kill ``` ### REST API @@ -945,7 +946,7 @@ DELETE /api/agents/{id} # Use any agent through the OpenAI-compatible endpoint POST /v1/chat/completions { - "model": "openfang:coder", + "model": "omtae:coder", "messages": [{"role": "user", "content": "Write a Rust HTTP server"}], "stream": true } diff --git a/docs/api-reference.md b/docs/api-reference.md index a36a5149e0..5179fcbe51 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,8 +1,8 @@ # API Reference -OpenFang exposes a REST API, WebSocket endpoints, and SSE streaming when the daemon is running. The default listen address is `http://127.0.0.1:4200`. +OMTAE exposes a REST API, WebSocket endpoints, and SSE streaming when the daemon is running. The default listen address is `http://127.0.0.1:4200`. -All responses include security headers (CSP, X-Frame-Options, X-Content-Type-Options, HSTS) and are protected by a GCRA cost-aware rate limiter with per-IP token bucket tracking and automatic stale entry cleanup. OpenFang implements 16 security systems including Merkle audit trails, taint tracking, WASM dual metering, Ed25519 manifest signing, SSRF protection, subprocess sandboxing, and secret zeroization. +All responses include security headers (CSP, X-Frame-Options, X-Content-Type-Options, HSTS) and are protected by a GCRA cost-aware rate limiter with per-IP token bucket tracking and automatic stale entry cleanup. OMTAE implements 16 security systems including Merkle audit trails, taint tracking, WASM dual metering, Ed25519 manifest signing, SSRF protection, subprocess sandboxing, and secret zeroization. ## Table of Contents @@ -41,7 +41,7 @@ Authorization: Bearer ### Setting the API Key -Add to `~/.openfang/config.toml`: +Add to `~/.omtae/config.toml`: ```toml api_key = "your-secret-api-key" @@ -579,12 +579,12 @@ List available agent templates from the agents directory. { "name": "hello-world", "description": "A friendly greeting agent", - "path": "/home/user/.openfang/agents/hello-world/agent.toml" + "path": "/home/user/.omtae/agents/hello-world/agent.toml" }, { "name": "coder", "description": "Expert coding assistant", - "path": "/home/user/.openfang/agents/coder/agent.toml" + "path": "/home/user/.omtae/agents/coder/agent.toml" } ], "total": 30 @@ -667,7 +667,7 @@ Detailed kernel status including all agents. { "status": "running", "agent_count": 2, - "data_dir": "/home/user/.openfang/data", + "data_dir": "/home/user/.omtae/data", "default_provider": "groq", "default_model": "llama-3.3-70b-versatile", "uptime_seconds": 3600, @@ -692,7 +692,7 @@ Build and version information. ```json { - "name": "openfang", + "name": "omtae", "version": "0.1.0", "build_date": "2025-01-15", "git_sha": "abc1234", @@ -768,7 +768,7 @@ Retrieve current kernel configuration (secrets are redacted). ```json { - "data_dir": "/home/user/.openfang/data", + "data_dir": "/home/user/.omtae/data", "default_provider": "groq", "default_model": "llama-3.3-70b-versatile", "listen_addr": "127.0.0.1:4200", @@ -780,7 +780,7 @@ Retrieve current kernel configuration (secrets are redacted). ### GET /api/peers -List OFP (OpenFang Protocol) wire peers and their connection status. +List OFP (OMTAE Protocol) wire peers and their connection status. **Response** `200 OK`: @@ -835,7 +835,7 @@ Delete a specific session and its conversation history. ## Model Catalog Endpoints -OpenFang maintains a built-in catalog of 51+ models across 20 providers. These endpoints allow you to browse available models, check provider authentication status, and resolve model aliases. +OMTAE maintains a built-in catalog of 51+ models across 20 providers. These endpoints allow you to browse available models, check provider authentication status, and resolve model aliases. ### GET /api/models @@ -1127,7 +1127,7 @@ Create a new skill from a template. { "status": "created", "skill": "my-skill", - "path": "/home/user/.openfang/skills/my-skill" + "path": "/home/user/.omtae/skills/my-skill" } ``` @@ -1233,7 +1233,7 @@ Get detailed information about a specific ClawHub skill. ### POST /api/clawhub/install -Install a skill from ClawHub. Downloads, verifies SHA256 checksum, scans for prompt injection, and converts SKILL.md format to OpenFang skill.toml automatically. +Install a skill from ClawHub. Downloads, verifies SHA256 checksum, scans for prompt injection, and converts SKILL.md format to OMTAE skill.toml automatically. **Request Body**: @@ -1258,7 +1258,7 @@ Install a skill from ClawHub. Downloads, verifies SHA256 checksum, scans for pro ## MCP & A2A Protocol Endpoints -OpenFang supports both Model Context Protocol (MCP) for tool interoperability and Agent-to-Agent (A2A) protocol for cross-system agent communication. +OMTAE supports both Model Context Protocol (MCP) for tool interoperability and Agent-to-Agent (A2A) protocol for cross-system agent communication. ### GET /api/mcp/servers @@ -1293,7 +1293,7 @@ List configured and connected MCP servers with their available tools. ### POST /mcp -MCP HTTP transport endpoint. Accepts JSON-RPC 2.0 requests and exposes OpenFang tools via the MCP protocol to external clients. +MCP HTTP transport endpoint. Accepts JSON-RPC 2.0 requests and exposes OMTAE tools via the MCP protocol to external clients. **Request Body** (JSON-RPC 2.0): @@ -1336,8 +1336,8 @@ A2A agent card discovery endpoint. Returns the server's A2A agent card, which de ```json { - "name": "OpenFang", - "description": "OpenFang Agent Operating System", + "name": "OMTAE", + "description": "OMTAE Agent Operating System", "url": "http://127.0.0.1:4200", "version": "0.1.0", "capabilities": { @@ -1444,7 +1444,7 @@ Cancel a running A2A task. ## Audit & Security Endpoints -OpenFang maintains a Merkle hash chain audit trail for all security-relevant operations. These endpoints allow inspection and verification of the audit log integrity. +OMTAE maintains a Merkle hash chain audit trail for all security-relevant operations. These endpoints allow inspection and verification of the audit log integrity. ### GET /api/audit/recent @@ -2108,7 +2108,7 @@ data: {"done":true,"usage":{"input_tokens":150,"output_tokens":340}} ## OpenAI-Compatible API -OpenFang exposes an OpenAI-compatible API for drop-in integration with tools that support the OpenAI API format (Cursor, Continue, Open WebUI, etc.). +OMTAE exposes an OpenAI-compatible API for drop-in integration with tools that support the OpenAI API format (Cursor, Continue, Open WebUI, etc.). ### POST /v1/chat/completions @@ -2118,7 +2118,7 @@ Send a chat completion request using the OpenAI message format. ```json { - "model": "openfang:coder", + "model": "omtae:coder", "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"} @@ -2129,11 +2129,11 @@ Send a chat completion request using the OpenAI message format. } ``` -**Model resolution** (the `model` field maps to an OpenFang agent): +**Model resolution** (the `model` field maps to an OMTAE agent): | Format | Example | Behavior | |--------|---------|----------| -| `openfang:` | `openfang:coder` | Find agent by name | +| `omtae:` | `omtae:coder` | Find agent by name | | UUID | `a1b2c3d4-...` | Find agent by ID | | Plain string | `coder` | Try as agent name | | Any other | `gpt-4o` | Falls back to first registered agent | @@ -2142,7 +2142,7 @@ Send a chat completion request using the OpenAI message format. ```json { - "model": "openfang:analyst", + "model": "omtae:analyst", "messages": [ { "role": "user", @@ -2204,16 +2204,16 @@ List available models (agents) in OpenAI format. "object": "list", "data": [ { - "id": "openfang:coder", + "id": "omtae:coder", "object": "model", "created": 1708617600, - "owned_by": "openfang" + "owned_by": "omtae" }, { - "id": "openfang:researcher", + "id": "omtae:researcher", "object": "model", "created": 1708617600, - "owned_by": "openfang" + "owned_by": "omtae" } ] } diff --git a/docs/architecture.md b/docs/architecture.md index 5aff03f658..7df65c663e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ -# OpenFang Architecture +# OMTAE Architecture -This document describes the internal architecture of OpenFang, the open-source Agent Operating System built in Rust. It covers the crate structure, kernel boot sequence, agent lifecycle, memory substrate, LLM driver abstraction, capability-based security model, the OFP wire protocol, the security hardening stack, the channel and skill systems, and the agent stability subsystems. +This document describes the internal architecture of OMTAE, the open-source Agent Operating System built in Rust. It covers the crate structure, kernel boot sequence, agent lifecycle, memory substrate, LLM driver abstraction, capability-based security model, the OFP wire protocol, the security hardening stack, the channel and skill systems, and the agent stability subsystems. ## Table of Contents @@ -24,26 +24,26 @@ This document describes the internal architecture of OpenFang, the open-source A ## Crate Structure -OpenFang is organized as a Cargo workspace with 14 crates (13 code crates + xtask). Dependencies flow downward (lower crates depend on nothing above them). +OMTAE is organized as a Cargo workspace with 14 crates (13 code crates + xtask). Dependencies flow downward (lower crates depend on nothing above them). ``` -openfang-cli CLI interface, daemon auto-detect, MCP server +omtae-cli CLI interface, daemon auto-detect, MCP server | -openfang-desktop Tauri 2.0 desktop app (WebView + system tray) +omtae-desktop Tauri 2.0 desktop app (WebView + system tray) | -openfang-api REST/WS/SSE API server (Axum 0.8), 76 endpoints +omtae-api REST/WS/SSE API server (Axum 0.8), 76 endpoints | -openfang-kernel Kernel: assembles all subsystems, workflow engine, RBAC, metering +omtae-kernel Kernel: assembles all subsystems, workflow engine, RBAC, metering | - +-- openfang-runtime Agent loop, 3 LLM drivers, 23 tools, WASM sandbox, MCP, A2A - +-- openfang-channels 40 channel adapters, bridge, formatter, rate limiter - +-- openfang-wire OFP peer-to-peer networking with HMAC-SHA256 auth - +-- openfang-migrate Migration engine (OpenClaw YAML->TOML) - +-- openfang-skills 60 bundled skills, FangHub marketplace, ClawHub client + +-- omtae-runtime Agent loop, 3 LLM drivers, 23 tools, WASM sandbox, MCP, A2A + +-- omtae-channels 40 channel adapters, bridge, formatter, rate limiter + +-- omtae-wire OFP peer-to-peer networking with HMAC-SHA256 auth + +-- omtae-migrate Migration engine (OpenClaw YAML->TOML) + +-- omtae-skills 60 bundled skills, FangHub marketplace, ClawHub client | -openfang-memory SQLite memory substrate, sessions, semantic search, usage tracking +omtae-memory SQLite memory substrate, sessions, semantic search, usage tracking | -openfang-types Shared types: Agent, Capability, Event, Memory, Message, Tool, Config, +omtae-types Shared types: Agent, Capability, Event, Memory, Message, Tool, Config, Taint, ManifestSigning, ModelCatalog, MCP/A2A config, Web config ``` @@ -51,36 +51,36 @@ openfang-types Shared types: Agent, Capability, Event, Memory, Message, | Crate | Description | |-------|-------------| -| **openfang-types** | Core type definitions used across all crates. Defines `AgentManifest`, `AgentId`, `Capability`, `Event`, `ToolDefinition`, `KernelConfig`, `OpenFangError`, taint tracking (`TaintLabel`, `TaintSet`), Ed25519 manifest signing, model catalog types (`ModelCatalogEntry`, `ProviderInfo`, `ModelTier`), tool compatibility mappings (21 OpenClaw-to-OpenFang), MCP/A2A config types, and web config types. All config structs use `#[serde(default)]` for forward-compatible TOML parsing. | -| **openfang-memory** | SQLite-backed memory substrate (schema v5). Uses `Arc>` with `spawn_blocking` for async bridge. Provides structured KV storage, semantic search with vector embeddings, knowledge graph (entities and relations), session management, task board, usage event persistence (`usage_events` table, `UsageStore`), and canonical sessions for cross-channel memory. Five schema versions: V1 core, V2 collab, V3 embeddings, V4 usage, V5 canonical_sessions. | -| **openfang-runtime** | Agent execution engine. Contains the agent loop (`run_agent_loop`, `run_agent_loop_streaming`), 3 native LLM drivers (Anthropic, Gemini, OpenAI-compatible covering 20 providers), 23 built-in tools, WASM sandbox (Wasmtime with dual fuel+epoch metering), MCP client/server (JSON-RPC 2.0 over stdio/SSE), A2A protocol (AgentCard, task management), web search engine (4 providers: Tavily/Brave/Perplexity/DuckDuckGo), web fetch with SSRF protection, loop guard (SHA256-based tool loop detection), session repair (history validation), LLM session compactor (block-aware), Merkle hash chain audit trail, and embedding driver. Defines the `KernelHandle` trait that enables inter-agent tools without circular crate dependencies. | -| **openfang-kernel** | The central coordinator. `OpenFangKernel` assembles all subsystems: `AgentRegistry`, `AgentScheduler`, `CapabilityManager`, `EventBus`, `Supervisor`, `WorkflowEngine`, `TriggerEngine`, `BackgroundExecutor`, `WasmSandbox`, `ModelCatalog`, `MeteringEngine`, `ModelRouter`, `AuthManager` (RBAC), `HeartbeatMonitor`, `SetupWizard`, `SkillRegistry`, MCP connections, and `WebToolsContext`. Implements `KernelHandle` for inter-agent operations. Handles agent spawn/kill, message dispatch, workflow execution, trigger evaluation, capability inheritance validation, and graceful shutdown with state persistence. | -| **openfang-api** | HTTP API server built on Axum 0.8 with 76 endpoints. Routes for agents, workflows, triggers, memory, channels, templates, models, providers, skills, ClawHub, MCP, health, status, version, and shutdown. WebSocket handler for real-time agent chat with streaming. SSE endpoint for streaming responses. OpenAI-compatible endpoints (`POST /v1/chat/completions`, `GET /v1/models`). A2A endpoints (`/.well-known/agent.json`, `/a2a/*`). Middleware: Bearer token auth, request ID injection, structured request logging, GCRA rate limiter (cost-aware), security headers (CSP, X-Frame-Options, etc.), health endpoint redaction. | -| **openfang-channels** | Channel bridge layer with 40 adapters. Each adapter implements the `ChannelAdapter` trait. Includes: Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Email, SMS, Webhook, Teams, Mattermost, IRC, Google Chat, Twitch, Rocket.Chat, Zulip, XMPP, LINE, Viber, Messenger, Reddit, Mastodon, Bluesky, Feishu, Revolt, Nextcloud, Guilded, Keybase, Threema, Nostr, Webex, Pumble, Flock, Twist, Mumble, DingTalk, Discourse, Gitter, Ntfy, Gotify, LinkedIn. Features: `AgentRouter` for message routing, `BridgeManager` for lifecycle coordination, `ChannelRateLimiter` (per-user DashMap tracking), `formatter.rs` (Markdown to TelegramHTML/SlackMrkdwn/PlainText), `ChannelOverrides` (model/system_prompt/dm_policy/group_policy/rate_limit/threading/output_format), DM/group policy enforcement. | -| **openfang-wire** | OpenFang Protocol (OFP) for peer-to-peer agent communication. JSON-framed messages over TCP with HMAC-SHA256 mutual authentication (nonce + constant-time verify via `subtle`). `PeerNode` listens for connections and manages peers. `PeerRegistry` tracks known remote peers and their agents. | -| **openfang-cli** | Clap-based CLI. Supports all commands: `init`, `start`, `status`, `doctor`, `agent spawn/list/chat/kill`, `workflow list/create/run`, `trigger list/create/delete`, `migrate`, `skill install/list/remove/search/create`, `channel list/setup/test/enable/disable`, `config show/edit`, `chat`, `mcp`. Daemon auto-detect: checks `~/.openfang/daemon.json` and health pings; uses HTTP when a daemon is running, boots an in-process kernel as fallback. Built-in MCP server mode. | -| **openfang-desktop** | Tauri 2.0 native desktop application. Boots the kernel in-process, runs the axum server on a background thread, and points a WebView at `http://127.0.0.1:{random_port}`. Features: system tray (Show/Browser/Status/Quit), single-instance enforcement, desktop notifications, hide-to-tray on close. IPC commands: `get_port`, `get_status`. Mobile-ready with `#[cfg(desktop)]` guards. | -| **openfang-migrate** | Migration engine. Supports OpenClaw (`~/.openclaw/`). Converts YAML configs to TOML, maps tool names, maps provider names, imports agent manifests, copies memory files, converts channel configs. Produces a `MigrationReport` with imported items, skipped items, and warnings. | -| **openfang-skills** | Skill system for pluggable tool bundles. 60 bundled skills compiled via `include_str!()`. Skills are `skill.toml` + Python/WASM/Node.js/PromptOnly code. `SkillManifest` defines metadata, runtime config, provided tools, and requirements. `SkillRegistry` manages installed and bundled skills. `FangHubClient` connects to FangHub marketplace. `ClawHubClient` connects to clawhub.ai for cross-ecosystem skill discovery. `SKILL.md` parser for OpenClaw compatibility (YAML frontmatter + Markdown body). `SkillVerifier` with SHA256 verification. Prompt injection scanner (`scan_prompt_content()`) detects override attempts, data exfiltration, and shell references. | +| **omtae-types** | Core type definitions used across all crates. Defines `AgentManifest`, `AgentId`, `Capability`, `Event`, `ToolDefinition`, `KernelConfig`, `OMTAEError`, taint tracking (`TaintLabel`, `TaintSet`), Ed25519 manifest signing, model catalog types (`ModelCatalogEntry`, `ProviderInfo`, `ModelTier`), tool compatibility mappings (21 OpenClaw-to-OMTAE), MCP/A2A config types, and web config types. All config structs use `#[serde(default)]` for forward-compatible TOML parsing. | +| **omtae-memory** | SQLite-backed memory substrate (schema v5). Uses `Arc>` with `spawn_blocking` for async bridge. Provides structured KV storage, semantic search with vector embeddings, knowledge graph (entities and relations), session management, task board, usage event persistence (`usage_events` table, `UsageStore`), and canonical sessions for cross-channel memory. Five schema versions: V1 core, V2 collab, V3 embeddings, V4 usage, V5 canonical_sessions. | +| **omtae-runtime** | Agent execution engine. Contains the agent loop (`run_agent_loop`, `run_agent_loop_streaming`), 3 native LLM drivers (Anthropic, Gemini, OpenAI-compatible covering 20 providers), 23 built-in tools, WASM sandbox (Wasmtime with dual fuel+epoch metering), MCP client/server (JSON-RPC 2.0 over stdio/SSE), A2A protocol (AgentCard, task management), web search engine (4 providers: Tavily/Brave/Perplexity/DuckDuckGo), web fetch with SSRF protection, loop guard (SHA256-based tool loop detection), session repair (history validation), LLM session compactor (block-aware), Merkle hash chain audit trail, and embedding driver. Defines the `KernelHandle` trait that enables inter-agent tools without circular crate dependencies. | +| **omtae-kernel** | The central coordinator. `OMTAEKernel` assembles all subsystems: `AgentRegistry`, `AgentScheduler`, `CapabilityManager`, `EventBus`, `Supervisor`, `WorkflowEngine`, `TriggerEngine`, `BackgroundExecutor`, `WasmSandbox`, `ModelCatalog`, `MeteringEngine`, `ModelRouter`, `AuthManager` (RBAC), `HeartbeatMonitor`, `SetupWizard`, `SkillRegistry`, MCP connections, and `WebToolsContext`. Implements `KernelHandle` for inter-agent operations. Handles agent spawn/kill, message dispatch, workflow execution, trigger evaluation, capability inheritance validation, and graceful shutdown with state persistence. | +| **omtae-api** | HTTP API server built on Axum 0.8 with 76 endpoints. Routes for agents, workflows, triggers, memory, channels, templates, models, providers, skills, ClawHub, MCP, health, status, version, and shutdown. WebSocket handler for real-time agent chat with streaming. SSE endpoint for streaming responses. OpenAI-compatible endpoints (`POST /v1/chat/completions`, `GET /v1/models`). A2A endpoints (`/.well-known/agent.json`, `/a2a/*`). Middleware: Bearer token auth, request ID injection, structured request logging, GCRA rate limiter (cost-aware), security headers (CSP, X-Frame-Options, etc.), health endpoint redaction. | +| **omtae-channels** | Channel bridge layer with 40 adapters. Each adapter implements the `ChannelAdapter` trait. Includes: Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Email, SMS, Webhook, Teams, Mattermost, IRC, Google Chat, Twitch, Rocket.Chat, Zulip, XMPP, LINE, Viber, Messenger, Reddit, Mastodon, Bluesky, Feishu, Revolt, Nextcloud, Guilded, Keybase, Threema, Nostr, Webex, Pumble, Flock, Twist, Mumble, DingTalk, Discourse, Gitter, Ntfy, Gotify, LinkedIn. Features: `AgentRouter` for message routing, `BridgeManager` for lifecycle coordination, `ChannelRateLimiter` (per-user DashMap tracking), `formatter.rs` (Markdown to TelegramHTML/SlackMrkdwn/PlainText), `ChannelOverrides` (model/system_prompt/dm_policy/group_policy/rate_limit/threading/output_format), DM/group policy enforcement. | +| **omtae-wire** | OMTAE Protocol (OFP) for peer-to-peer agent communication. JSON-framed messages over TCP with HMAC-SHA256 mutual authentication (nonce + constant-time verify via `subtle`). `PeerNode` listens for connections and manages peers. `PeerRegistry` tracks known remote peers and their agents. | +| **omtae-cli** | Clap-based CLI. Supports all commands: `init`, `start`, `status`, `doctor`, `agent spawn/list/chat/kill`, `workflow list/create/run`, `trigger list/create/delete`, `migrate`, `skill install/list/remove/search/create`, `channel list/setup/test/enable/disable`, `config show/edit`, `chat`, `mcp`. Daemon auto-detect: checks `~/.omtae/daemon.json` and health pings; uses HTTP when a daemon is running, boots an in-process kernel as fallback. Built-in MCP server mode. | +| **omtae-desktop** | Tauri 2.0 native desktop application. Boots the kernel in-process, runs the axum server on a background thread, and points a WebView at `http://127.0.0.1:{random_port}`. Features: system tray (Show/Browser/Status/Quit), single-instance enforcement, desktop notifications, hide-to-tray on close. IPC commands: `get_port`, `get_status`. Mobile-ready with `#[cfg(desktop)]` guards. | +| **omtae-migrate** | Migration engine. Supports OpenClaw (`~/.openclaw/`). Converts YAML configs to TOML, maps tool names, maps provider names, imports agent manifests, copies memory files, converts channel configs. Produces a `MigrationReport` with imported items, skipped items, and warnings. | +| **omtae-skills** | Skill system for pluggable tool bundles. 60 bundled skills compiled via `include_str!()`. Skills are `skill.toml` + Python/WASM/Node.js/PromptOnly code. `SkillManifest` defines metadata, runtime config, provided tools, and requirements. `SkillRegistry` manages installed and bundled skills. `FangHubClient` connects to FangHub marketplace. `ClawHubClient` connects to clawhub.ai for cross-ecosystem skill discovery. `SKILL.md` parser for OpenClaw compatibility (YAML frontmatter + Markdown body). `SkillVerifier` with SHA256 verification. Prompt injection scanner (`scan_prompt_content()`) detects override attempts, data exfiltration, and shell references. | | **xtask** | Build automation tasks (cargo-xtask pattern). | --- ## Kernel Boot Sequence -When `OpenFangKernel::boot_with_config()` is called (either by the daemon or in-process by the CLI/desktop app), the following sequence executes: +When `OMTAEKernel::boot_with_config()` is called (either by the daemon or in-process by the CLI/desktop app), the following sequence executes: ``` 1. Load configuration - - Read ~/.openfang/config.toml (or specified path) + - Read ~/.omtae/config.toml (or specified path) - Apply #[serde(default)] defaults for missing fields - Validate config and log warnings (missing API keys, etc.) 2. Create data directory - - Ensure ~/.openfang/data/ exists + - Ensure ~/.omtae/data/ exists 3. Initialize memory substrate - - Open SQLite database (openfang.db) + - Open SQLite database (omtae.db) - Run schema migrations (up to v5) - Set memory decay rate @@ -279,7 +279,7 @@ The session compactor handles all content block types (Text, ToolUse, ToolResult ## Memory Substrate -The memory substrate (`openfang-memory`) provides six layers of storage: +The memory substrate (`omtae-memory`) provides six layers of storage: ### 1. Structured KV Store @@ -328,7 +328,7 @@ All memory operations go through `Arc>` with Tokio's `spawn_bl ## LLM Driver Abstraction -The `LlmDriver` trait (`openfang-runtime`) provides a unified interface for all LLM providers: +The `LlmDriver` trait (`omtae-runtime`) provides a unified interface for all LLM providers: ```rust #[async_trait] @@ -339,7 +339,7 @@ pub trait LlmDriver: Send + Sync { system_prompt: &str, messages: &[Message], tools: &[ToolDefinition], - ) -> Result; + ) -> Result; async fn send_message_streaming( &self, @@ -348,7 +348,7 @@ pub trait LlmDriver: Send + Sync { messages: &[Message], tools: &[ToolDefinition], tx: mpsc::Sender, - ) -> Result; + ) -> Result; fn key_required(&self) -> bool; } @@ -413,7 +413,7 @@ LLM calls use exponential backoff for rate-limited (429) and overloaded (529) re ## Model Catalog -The `ModelCatalog` (`openfang-runtime/src/model_catalog.rs`) provides a registry of all known models, providers, and aliases. +The `ModelCatalog` (`omtae-runtime/src/model_catalog.rs`) provides a registry of all known models, providers, and aliases. ### Registry Contents @@ -512,7 +512,7 @@ The tool runner also enforces capabilities by filtering the tool list before pas ## Security Hardening -OpenFang implements 16 security systems organized into critical fixes and state-of-the-art defenses: +OMTAE implements 16 security systems organized into critical fixes and state-of-the-art defenses: ### Path Traversal Prevention @@ -536,7 +536,7 @@ WASM sandbox uses both Wasmtime fuel metering (instruction count) and epoch inte ### Information Flow Taint Tracking -`taint.rs` in `openfang-types` implements taint labels and taint sets. Data from external sources carries taint labels that propagate through operations, enabling information flow analysis. +`taint.rs` in `omtae-types` implements taint labels and taint sets. Data from external sources carries taint labels that propagate through operations, enabling information flow analysis. ### Ed25519 Manifest Signing @@ -582,7 +582,7 @@ See [Agent Loop Stability](#agent-loop-stability) above. ## Channel System -The channel system (`openfang-channels`) provides 40 adapters for messaging platform integration. +The channel system (`omtae-channels`) provides 40 adapters for messaging platform integration. ### Adapter List @@ -606,7 +606,7 @@ The channel system (`openfang-channels`) provides 40 adapters for messaging plat ## Skill System -The skill system (`openfang-skills`) provides 60 bundled skills and supports external skill installation. +The skill system (`omtae-skills`) provides 60 bundled skills and supports external skill installation. ### Skill Types @@ -635,10 +635,10 @@ All skills pass through a security pipeline before activation: ### Ecosystem Bridges -- **FangHub**: Native OpenFang marketplace (`FangHubClient`). +- **FangHub**: Native OMTAE marketplace (`FangHubClient`). - **ClawHub**: Cross-ecosystem compatibility (`ClawHubClient` connects to clawhub.ai). - **SKILL.md Parser**: Auto-converts OpenClaw SKILL.md format (YAML frontmatter + Markdown body) to `skill.toml`. -- **Tool Compat**: 21 OpenClaw-to-OpenFang tool name mappings in `tool_compat.rs`. +- **Tool Compat**: 21 OpenClaw-to-OMTAE tool name mappings in `tool_compat.rs`. --- @@ -646,10 +646,10 @@ All skills pass through a security pipeline before activation: ### Model Context Protocol (MCP) -OpenFang implements both MCP client and server: +OMTAE implements both MCP client and server: - **MCP Client** (`mcp.rs`): JSON-RPC 2.0 over stdio or SSE transports. Connects to external MCP servers. Tools are namespaced as `mcp_{server}_{tool}` to prevent collisions. Background connection in `start_background_agents()`. -- **MCP Server** (`mcp_server.rs`): Exposes OpenFang's 23 built-in tools via the MCP protocol. Enables external tools to use OpenFang as a tool provider. +- **MCP Server** (`mcp_server.rs`): Exposes OMTAE's 23 built-in tools via the MCP protocol. Enables external tools to use OMTAE as a tool provider. - **Configuration**: `KernelConfig.mcp_servers` (Vec of `McpServerConfigEntry` with name, command, args, env, transport). - **API**: `/api/mcp/servers` returns configured and connected servers with their tool lists. @@ -666,7 +666,7 @@ Google's A2A protocol for inter-system agent communication: ## Wire Protocol (OFP) -The OpenFang Protocol (OFP) enables peer-to-peer agent communication across machines. +The OMTAE Protocol (OFP) enables peer-to-peer agent communication across machines. ### Architecture @@ -748,7 +748,7 @@ OFP operations require capabilities: ## Desktop Application -The desktop app (`openfang-desktop`) wraps the full OpenFang stack in a native Tauri 2.0 application. +The desktop app (`omtae-desktop`) wraps the full OMTAE stack in a native Tauri 2.0 application. ### Architecture @@ -766,7 +766,7 @@ The desktop app (`openfang-desktop`) wraps the full OpenFang stack in a native T | +---------------------------------------+ | | | Background Thread | | | | +- Own Tokio Runtime | | -| | +- OpenFangKernel (in-process) | | +| | +- OMTAEKernel (in-process) | | | | +- Axum Server (build_router()) | | | | +- ServerHandle { port, shutdown } | | | +---------------------------------------+ | @@ -789,7 +789,7 @@ The desktop app (`openfang-desktop`) wraps the full OpenFang stack in a native T ``` +-------------------------------------------------------------------+ -| openfang-cli | +| omtae-cli | | [init] [start] [agent] [workflow] [trigger] [skill] [channel] | | [migrate] [config] [chat] [status] [doctor] [mcp] | +-------------------------------------------------------------------+ @@ -797,7 +797,7 @@ The desktop app (`openfang-desktop`) wraps the full OpenFang stack in a native T | (HTTP/daemon) | (in-process) v v +-------------------------------------------------------------------+ -| openfang-api | +| omtae-api | | +-------------+ +----------+ +--------+ +------------------+ | | | REST Routes | | WS Chat | | SSE | | OpenAI /v1/ | | | | (76 endpts) | +----------+ +--------+ +------------------+ | @@ -811,7 +811,7 @@ The desktop app (`openfang-desktop`) wraps the full OpenFang stack in a native T | v +-------------------------------------------------------------------+ -| openfang-kernel | +| omtae-kernel | | +----------------+ +------------------+ +-------------------+ | | | AgentRegistry | | AgentScheduler | | CapabilityManager | | | | (DashMap) | | (quota+metering) | | (DashMap+inherit) | | @@ -842,7 +842,7 @@ The desktop app (`openfang-desktop`) wraps the full OpenFang stack in a native T | | | | v v v v +------------------+ +--------------+ +--------+ +-----------+ -| openfang-runtime | | openfang- | | open- | | openfang- | +| omtae-runtime | | omtae- | | open- | | omtae- | | | | channels | | fang- | | skills | | +------------+ | | | | wire | | | | | Agent Loop | | | +----------+| | | | +-------+ | @@ -883,7 +883,7 @@ The desktop app (`openfang-desktop`) wraps the full OpenFang stack in a native T | v +------------------+ -| openfang-memory | +| omtae-memory | | +------------+ | | | KV Store | | Per-agent + shared namespace | +------------+ | @@ -914,7 +914,7 @@ The desktop app (`openfang-desktop`) wraps the full OpenFang stack in a native T | v +------------------+ -| openfang-types | +| omtae-types | | Agent, Capability| | Event, Memory | | Message, Tool | diff --git a/docs/benchmarks/architecture-overview.svg b/docs/benchmarks/architecture-overview.svg index af6ea8d77c..2ec26372f9 100644 --- a/docs/benchmarks/architecture-overview.svg +++ b/docs/benchmarks/architecture-overview.svg @@ -30,7 +30,7 @@ - OpenFang — Agent Operating System Architecture + OMTAE — Agent Operating System Architecture 14 crates | 137K lines of Rust | single binary @@ -56,7 +56,7 @@ - API GATEWAY — openfang-api + API GATEWAY — omtae-api 140 REST endpoints | WebSocket streaming | SSE events | GCRA rate limiter Security headers (CSP/HSTS) | CORS | Health redaction | File uploads | Approval queue @@ -65,7 +65,7 @@ - KERNEL — openfang-kernel + KERNEL — omtae-kernel Agent Registry @@ -104,7 +104,7 @@ - RUNTIME — openfang-runtime + RUNTIME — omtae-runtime Agent loop (streaming) | 3 LLM drivers | 53 tools WASM sandbox (dual metering) | MCP client/server Loop guard | Session repair | Auth cooldown @@ -133,7 +133,7 @@ - MEMORY — openfang-memory + MEMORY — omtae-memory SQLite (KV + relational) | Vector embeddings | Usage tracking | Canonical sessions | JSONL mirror Schema v7 | Session compaction | Knowledge graphs @@ -147,15 +147,15 @@ FOUNDATION - openfang-types + omtae-types - openfang-wire + omtae-wire - openfang-migrate + omtae-migrate - openfang-hands + omtae-hands - openfang-extensions + omtae-extensions Taint tracking | Ed25519 manifests | Merkle audit | SSRF protection | Zeroizing secrets diff --git a/docs/benchmarks/feature-coverage.svg b/docs/benchmarks/feature-coverage.svg index edfb86d98c..13781620ce 100644 --- a/docs/benchmarks/feature-coverage.svg +++ b/docs/benchmarks/feature-coverage.svg @@ -23,7 +23,7 @@ - OpenFang + OMTAE OpenClaw @@ -46,7 +46,7 @@ max - + 40 @@ -57,7 +57,7 @@ 0 - + 60 @@ -68,7 +68,7 @@ 0 - + 53 @@ -79,7 +79,7 @@ ~8 - + 27 @@ -90,7 +90,7 @@ ~20 - + 140 @@ -101,7 +101,7 @@ N/A - + 1759 diff --git a/docs/benchmarks/performance.svg b/docs/benchmarks/performance.svg index 6e572af9db..a4f0971ba8 100644 --- a/docs/benchmarks/performance.svg +++ b/docs/benchmarks/performance.svg @@ -21,7 +21,7 @@ Cold Start Time Time from process launch to first API response - OpenFang + OMTAE <200ms @@ -37,7 +37,7 @@ Install Footprint Total disk space required for full installation - OpenFang + OMTAE ~32 MB (single binary) @@ -53,6 +53,6 @@ (Python + deps) - OpenFang compiled with Rust 1.93 (release profile, LTO enabled). Python frameworks measured with pip install + dependencies. + OMTAE compiled with Rust 1.93 (release profile, LTO enabled). Python frameworks measured with pip install + dependencies. Startup = time until HTTP health endpoint responds. Install size = du -sh after clean install. \ No newline at end of file diff --git a/docs/benchmarks/security-layers.svg b/docs/benchmarks/security-layers.svg index b426f08508..f4e8edac33 100644 --- a/docs/benchmarks/security-layers.svg +++ b/docs/benchmarks/security-layers.svg @@ -21,14 +21,14 @@ Measured: discrete, independently testable security mechanisms per framework - OpenFang + OMTAE OpenClaw LangChain AutoGPT CrewAI - + 16 diff --git a/docs/channel-adapters.md b/docs/channel-adapters.md index e6534b8ff8..fe0892defa 100644 --- a/docs/channel-adapters.md +++ b/docs/channel-adapters.md @@ -1,6 +1,6 @@ # Channel Adapters -OpenFang connects to messaging platforms through **40 channel adapters**, allowing users to interact with their agents across every major communication platform. Adapters span consumer messaging, enterprise collaboration, social media, community platforms, privacy-focused protocols, and generic webhooks. +OMTAE connects to messaging platforms through **40 channel adapters**, allowing users to interact with their agents across every major communication platform. Adapters span consumer messaging, enterprise collaboration, social media, community platforms, privacy-focused protocols, and generic webhooks. All adapters share a common foundation: graceful shutdown via `watch::channel`, exponential backoff on connection failures, `Zeroizing` for secrets, automatic message splitting for platform limits, per-channel model/prompt overrides, DM/group policy enforcement, per-user rate limiting, and output formatting (Markdown, TelegramHTML, SlackMrkdwn, PlainText). @@ -115,7 +115,7 @@ All adapters share a common foundation: graceful shutdown via `watch::channel`, ## Channel Configuration -All channel configurations live in `~/.openfang/config.toml` under the `[channels]` section. Each channel is a subsection: +All channel configurations live in `~/.omtae/config.toml` under the `[channels]` section. Each channel is a subsection: ```toml [channels.telegram] @@ -147,7 +147,7 @@ default_agent = "social-media" ### Common Fields -- `bot_token_env` / `token_env` -- The environment variable holding the bot/access token. OpenFang reads the token from this env var at startup. All secrets are stored as `Zeroizing` and wiped from memory on drop. +- `bot_token_env` / `token_env` -- The environment variable holding the bot/access token. OMTAE reads the token from this env var at startup. All secrets are stored as `Zeroizing` and wiped from memory on drop. - `default_agent` -- The agent name (or ID) that receives messages when no specific routing applies. - `allowed_users` -- Optional list of platform user IDs allowed to interact. Empty means allow all. - `overrides` -- Optional per-channel behavior overrides (see [Channel Overrides](#channel-overrides) below). @@ -202,7 +202,7 @@ usage_footer = "compact" ### Output Formatter -The `formatter` module (`openfang-channels/src/formatter.rs`) converts Markdown output from the LLM into platform-native formats: +The `formatter` module (`omtae-channels/src/formatter.rs`) converts Markdown output from the LLM into platform-native formats: | OutputFormat | Target | Notes | |-------------|--------|-------| @@ -213,7 +213,7 @@ The `formatter` module (`openfang-channels/src/formatter.rs`) converts Markdown ### Per-User Rate Limiter -The `ChannelRateLimiter` (`openfang-channels/src/rate_limiter.rs`) uses a `DashMap` to track per-user message counts. When `rate_limit_per_user` is set on a channel's overrides, the limiter enforces a sliding-window cap of N messages per minute. Excess messages receive a polite rejection. +The `ChannelRateLimiter` (`omtae-channels/src/rate_limiter.rs`) uses a `DashMap` to track per-user message counts. When `rate_limit_per_user` is set on a channel's overrides, the limiter enforces a sliding-window cap of N messages per minute. Excess messages receive a polite rejection. ### DM Policy @@ -275,7 +275,7 @@ default_agent = "assistant" 6. Restart the daemon: ```bash -openfang start +omtae start ``` ### How It Works @@ -301,7 +301,7 @@ Agents can read the raw metadata field via tool calls that expose `ChannelMessag ### Interactive Setup ```bash -openfang channel setup telegram +omtae channel setup telegram ``` This walks you through the setup interactively. @@ -496,8 +496,8 @@ Then configure Feishu event callback to: ### How It Works -- **websocket mode**: OpenFang obtains endpoint from Feishu and receives events via long connection (no public inbound webhook needed). -- **webhook mode**: OpenFang starts an HTTP callback server and receives Feishu push events. +- **websocket mode**: OMTAE obtains endpoint from Feishu and receives events via long connection (no public inbound webhook needed). +- **webhook mode**: OMTAE starts an HTTP callback server and receives Feishu push events. - **send path (both modes)**: outbound messages still go through Feishu OpenAPI HTTP `im/v1/messages`. --- @@ -551,7 +551,7 @@ export MATRIX_TOKEN=syt_... [channels.matrix] homeserver_url = "https://matrix.org" access_token_env = "MATRIX_TOKEN" -user_id = "@openfang-bot:matrix.org" +user_id = "@omtae-bot:matrix.org" default_agent = "assistant" ``` @@ -676,7 +676,7 @@ A binding's score is the sum of its set fields. `peer_id` and `channel_id` are e Adapters not on this list (Reddit, Bluesky, Mastodon, Signal, Email, ntfy, Discourse, etc.) carry a *user* ID in `platform_id` and have no per-conversation concept, or use a hybrid scheme (IRC, Zulip flip between channel and user based on `is_group`). Bindings targeting `channel_id` on those platforms will only match if the adapter writes a `channel_id` key into message metadata. -The kernel emits a startup warning when a binding sets `channel_id` for a non-supporting adapter, so misconfigurations surface early instead of silently routing nowhere. The single source of truth for this list is `CHANNELS_WITH_PLATFORM_ID_AS_CHANNEL` in `openfang-types::config`, consumed by both routing (`ChannelMessage::channel_id()`) and config validation. +The kernel emits a startup warning when a binding sets `channel_id` for a non-supporting adapter, so misconfigurations surface early instead of silently routing nowhere. The single source of truth for this list is `CHANNELS_WITH_PLATFORM_ID_AS_CHANNEL` in `omtae-types::config`, consumed by both routing (`ChannelMessage::channel_id()`) and config validation. **Strict parsing** — `AgentBinding` and `BindingMatchRule` use `#[serde(deny_unknown_fields)]`. Typos at the binding level (e.g. `match_rules` for `match_rule`, `channnel_id` for `channel_id`) fail config load with a clear error rather than parsing into a no-op rule that silently matches every message. Existing configs that work today are unaffected; only configs with stray/misspelled fields inside a `[[bindings]]` block need a fix. The top-level `KernelConfig` deliberately stays permissive so unrecognized top-level keys (forward-compat, downstream forks) don't break startup. @@ -684,7 +684,7 @@ The kernel emits a startup warning when a binding sets `channel_id` for a non-su ## Writing Custom Adapters -To add support for a new messaging platform, implement the `ChannelAdapter` trait. The trait is defined in `crates/openfang-channels/src/types.rs`. +To add support for a new messaging platform, implement the `ChannelAdapter` trait. The trait is defined in `crates/omtae-channels/src/types.rs`. ### The ChannelAdapter Trait @@ -735,7 +735,7 @@ pub trait ChannelAdapter: Send + Sync { ### 1. Define Your Adapter -Create `crates/openfang-channels/src/myplatform.rs`: +Create `crates/omtae-channels/src/myplatform.rs`: ```rust use crate::types::{ @@ -812,7 +812,7 @@ impl ChannelAdapter for MyPlatformAdapter { ### 2. Register the Module -In `crates/openfang-channels/src/lib.rs`: +In `crates/omtae-channels/src/lib.rs`: ```rust pub mod myplatform; @@ -820,11 +820,11 @@ pub mod myplatform; ### 3. Wire It Into the Bridge -In `crates/openfang-api/src/channel_bridge.rs`, add initialization logic for your adapter alongside the existing adapters. +In `crates/omtae-api/src/channel_bridge.rs`, add initialization logic for your adapter alongside the existing adapters. ### 4. Add Config Support -In `openfang-types`, add a config struct: +In `omtae-types`, add a config struct: ```rust #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -840,7 +840,7 @@ Add it to the `ChannelsConfig` struct and `config.toml` parsing. The `overrides` ### 5. Add CLI Setup Wizard -In `crates/openfang-cli/src/main.rs`, add a case to `cmd_channel_setup` with step-by-step instructions for your platform. +In `crates/omtae-cli/src/main.rs`, add a case to `cmd_channel_setup` with step-by-step instructions for your platform. ### 6. Test diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 8cff5c756b..800d782881 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -1,41 +1,41 @@ -# OpenFang CLI Reference +# OMTAE CLI Reference -Complete command-line reference for `openfang`, the CLI tool for the OpenFang Agent OS. +Complete command-line reference for `omtae`, the CLI tool for the OMTAE Agent OS. ## Overview -The `openfang` binary is the primary interface for managing the OpenFang Agent OS. It supports two modes of operation: +The `omtae` binary is the primary interface for managing the OMTAE Agent OS. It supports two modes of operation: -- **Daemon mode** -- When a daemon is running (`openfang start`), CLI commands communicate with it over HTTP. This is the recommended mode for production use. +- **Daemon mode** -- When a daemon is running (`omtae start`), CLI commands communicate with it over HTTP. This is the recommended mode for production use. - **In-process mode** -- When no daemon is detected, commands that support it will boot an ephemeral in-process kernel. Agents spawned in this mode are not persisted and will be lost when the process exits. -Running `openfang` with no subcommand launches the interactive TUI (terminal user interface) built with ratatui, which provides a full dashboard experience in the terminal. +Running `omtae` with no subcommand launches the interactive TUI (terminal user interface) built with ratatui, which provides a full dashboard experience in the terminal. ## Installation ### From source (cargo) ```bash -cargo install --path crates/openfang-cli +cargo install --path crates/omtae-cli ``` ### Build from workspace ```bash -cargo build --release -p openfang-cli -# Binary: target/release/openfang (or openfang.exe on Windows) +cargo build --release -p omtae-cli +# Binary: target/release/omtae (or omtae.exe on Windows) ``` ### Docker ```bash -docker run -it openfang/openfang:latest +docker run -it omtae/omtae:latest ``` ### Shell installer ```bash -curl -fsSL https://get.openfang.ai | sh +curl -fsSL https://get.omtae.ai | sh ``` ## Global Options @@ -44,42 +44,42 @@ These options apply to all commands. | Option | Description | |---|---| -| `--config ` | Path to a custom config file. Overrides the default `~/.openfang/config.toml`. | +| `--config ` | Path to a custom config file. Overrides the default `~/.omtae/config.toml`. | | `--help` | Print help information for any command or subcommand. | -| `--version` | Print the version of the `openfang` binary. | +| `--version` | Print the version of the `omtae` binary. | **Environment variables:** | Variable | Description | |---|---| -| `RUST_LOG` | Controls log verbosity (e.g. `info`, `debug`, `openfang_kernel=trace`). | +| `RUST_LOG` | Controls log verbosity (e.g. `info`, `debug`, `omtae_kernel=trace`). | | `OPENFANG_AGENTS_DIR` | Override the agent templates directory. | -| `EDITOR` / `VISUAL` | Editor used by `openfang config edit`. Falls back to `notepad` (Windows) or `vi` (Unix). | +| `EDITOR` / `VISUAL` | Editor used by `omtae config edit`. Falls back to `notepad` (Windows) or `vi` (Unix). | --- ## Command Reference -### openfang (no subcommand) +### omtae (no subcommand) Launch the interactive TUI dashboard. ``` -openfang [--config ] +omtae [--config ] ``` -The TUI provides a full-screen terminal interface with panels for agents, chat, workflows, channels, skills, settings, and more. Tracing output is redirected to `~/.openfang/tui.log` to avoid corrupting the terminal display. +The TUI provides a full-screen terminal interface with panels for agents, chat, workflows, channels, skills, settings, and more. Tracing output is redirected to `~/.omtae/tui.log` to avoid corrupting the terminal display. Press `Ctrl+C` to exit. A second `Ctrl+C` force-exits the process. --- -### openfang init +### omtae init -Initialize the OpenFang workspace. Creates `~/.openfang/` with subdirectories (`data/`, `agents/`) and a default `config.toml`. +Initialize the OMTAE workspace. Creates `~/.omtae/` with subdirectories (`data/`, `agents/`) and a default `config.toml`. ``` -openfang init [--quick] +omtae init [--quick] ``` **Options:** @@ -98,35 +98,35 @@ openfang init [--quick] ```bash # Interactive setup -openfang init +omtae init # Non-interactive (CI/scripts) export GROQ_API_KEY="gsk_..." -openfang init --quick +omtae init --quick ``` --- -### openfang start +### omtae start -Start the OpenFang daemon (kernel + API server). +Start the OMTAE daemon (kernel + API server). ``` -openfang start [--config ] +omtae start [--config ] ``` **Behavior:** - Checks if a daemon is already running; exits with an error if so. -- Boots the OpenFang kernel (loads config, initializes SQLite database, loads agents, connects MCP servers, starts background tasks). +- Boots the OMTAE kernel (loads config, initializes SQLite database, loads agents, connects MCP servers, starts background tasks). - Starts the HTTP API server on the address specified in `config.toml` (default: `127.0.0.1:4200`). -- Writes `daemon.json` to `~/.openfang/` so other CLI commands can discover the running daemon. +- Writes `daemon.json` to `~/.omtae/` so other CLI commands can discover the running daemon. - Blocks until interrupted with `Ctrl+C`. **Output:** ``` - OpenFang Agent OS v0.1.0 + OMTAE Agent OS v0.1.0 Starting daemon... @@ -139,7 +139,7 @@ openfang start [--config ] Provider: groq Model: llama-3.3-70b-versatile - hint: Open the dashboard in your browser, or run `openfang chat` + hint: Open the dashboard in your browser, or run `omtae chat` hint: Press Ctrl+C to stop the daemon ``` @@ -147,20 +147,20 @@ openfang start [--config ] ```bash # Start with default config -openfang start +omtae start # Start with custom config -openfang start --config /path/to/config.toml +omtae start --config /path/to/config.toml ``` --- -### openfang status +### omtae status Show the current kernel/daemon status. ``` -openfang status [--json] +omtae status [--json] ``` **Options:** @@ -177,19 +177,19 @@ openfang status [--json] **Example:** ```bash -openfang status +omtae status -openfang status --json | jq '.agent_count' +omtae status --json | jq '.agent_count' ``` --- -### openfang doctor +### omtae doctor -Run diagnostic checks on the OpenFang installation. +Run diagnostic checks on the OMTAE installation. ``` -openfang doctor [--json] [--repair] +omtae doctor [--json] [--repair] ``` **Options:** @@ -201,7 +201,7 @@ openfang doctor [--json] [--repair] **Checks performed:** -1. **OpenFang directory** -- `~/.openfang/` exists +1. **OMTAE directory** -- `~/.omtae/` exists 2. **.env file** -- exists and has correct permissions (0600 on Unix) 3. **Config TOML syntax** -- `config.toml` parses without errors 4. **Daemon status** -- whether a daemon is running @@ -209,7 +209,7 @@ openfang doctor [--json] [--repair] 6. **Stale daemon.json** -- leftover `daemon.json` from a crashed daemon 7. **Database file** -- SQLite magic bytes validation 8. **Disk space** -- warns if less than 100MB available (Unix only) -9. **Agent manifests** -- validates all `.toml` files in `~/.openfang/agents/` +9. **Agent manifests** -- validates all `.toml` files in `~/.omtae/agents/` 10. **LLM provider keys** -- checks env vars for 10 providers (Groq, OpenRouter, Anthropic, OpenAI, DeepSeek, Gemini, Google, Together, Mistral, Fireworks), performs live validation (401/403 detection) 11. **Channel tokens** -- format validation for Telegram, Discord, Slack tokens 12. **Config consistency** -- checks that `api_key_env` references in config match actual environment variables @@ -218,21 +218,21 @@ openfang doctor [--json] [--repair] **Example:** ```bash -openfang doctor +omtae doctor -openfang doctor --repair +omtae doctor --repair -openfang doctor --json +omtae doctor --json ``` --- -### openfang dashboard +### omtae dashboard Open the web dashboard in the default browser. ``` -openfang dashboard +omtae dashboard ``` **Behavior:** @@ -244,17 +244,17 @@ openfang dashboard **Example:** ```bash -openfang dashboard +omtae dashboard ``` --- -### openfang completion +### omtae completion Generate shell completion scripts. ``` -openfang completion +omtae completion ``` **Arguments:** @@ -267,28 +267,28 @@ openfang completion ```bash # Bash -openfang completion bash > ~/.bash_completion.d/openfang +omtae completion bash > ~/.bash_completion.d/omtae # Zsh -openfang completion zsh > ~/.zfunc/_openfang +omtae completion zsh > ~/.zfunc/_omtae # Fish -openfang completion fish > ~/.config/fish/completions/openfang.fish +omtae completion fish > ~/.config/fish/completions/omtae.fish # PowerShell -openfang completion powershell > openfang.ps1 +omtae completion powershell > omtae.ps1 ``` --- ## Agent Commands -### openfang agent new +### omtae agent new Spawn an agent from a built-in template. ``` -openfang agent new [