entanglement 发布无头 Rust AI 编码智能体引擎,四种接口全部落地
xmiksay/entanglement
xmiksay/entanglement 是一个无头、基于 Rust 的 AI 编码智能体引擎,把推理加工具执行循环与 UI 解耦,以异步 actor 形式暴露为 InMsg 收件箱和 OutEvent 广播出口。
Headless, Rust-based AI coding agent engine. The reasoning + tool-execution
loop is decoupled from any UI and exposed as an async actor: a typed inbox of
InMsg and a broadcast outbox of OutEvent. Every interface is a thin adapter
over those two methods.
- Architecture & interfaces:
docs/architecture.md
Status
Actor core + stdio head + TUI + local WebSocket serve head, with real LLM
backends wired (entanglement-provider: z.ai/OpenAI/Ollama + Anthropic +
Gemini). All
four interfaces now ship. The three-layer re-architecture (core / provider /
runtime) has landed — see
docs/adr/0006 and the crate
table below.
The contract (one set of types, every head)
InMsg : Prompt | Approve | Reject | ToolResult | AnswerQuestion | RetractQuestion | ReplaceQuestion | Stop
| PauseSession | ResumeSession
| SetAgent | SetModel | SetGeneration | SetSessionMeta | SetToolOverlay | Oneshot | Spawn | ListSessions | ListQuestions | ReplayFrom | CloseSession
| McpList | McpAdd | McpRemove | McpAuth
| HibernateSession (trusted-only) | Resume (internal, not serialized) (harness → engine)
OutEvent : SessionStarted | SessionEnded | SessionHibernated | SessionList | QuestionList | History | Status
| AgentChanged | ModelChanged | GenerationChanged | SessionMetaChanged | ToolOverlayChanged
| McpList | McpChanged | McpAuthChanged | Throttle
| Plan | TextDelta | ReasoningDelta
| ToolCallDelta | ToolCall | ToolRequest | ToolExec | UserQuestion
| ToolOutput | TaskList | Usage | Error | Done | Compacted | FileChange | PlanChanged
| SkillActive | AmbiguousRetry | SearchResult | ReasoningBlock (engine → harness)
Every frame is session-scoped (one connection multiplexes many sessions via
SessionId) and content frames carry a monotonic seq for dedup/ordering.
Four interfaces, one ABI
| Head | Status | What it is |
|---|---|---|
| ABI (direct) | ✅ | Hold a Holly, call holly.send(InMsg) / holly.subscribe(). Zero serialization. The foundation. |
stdio (skutter run / skutter pipe) |
✅ | NDJSON over stdin/stdout — one-shot run (text or --format json, à la opencode run) and bidirectional pipe. skutter sessions lists past sessions; skutter inspect prompt --agent <name> [--parts] prints an agent's assembled system prompt (no engine); skutter inspect agents [name] shows the resolved agent registry with layer provenance — a table (name, mode, model, layer, source, mask) or one agent's full resolved profile + what lower layers it overrode; skutter inspect skills [name] [--disclosures] does the same for skills — a table (name, user_only, layer, root_dir, description), the exact tier-1 disclosure block the model gets (--disclosures), or a dry-run of the load_skill path substitution for one skill. |
TUI (skutter tui) |
✅ | opencode-style terminal UI streaming OutEvent, tool-approval prompts, plan/task panels. |
WebSocket (skutter serve --port <N>) |
✅ | axum /ws (+ /healthz), one broadcast fan-out per socket relayed as JSON text frames, inbound frames routed through the untrusted send_from_wire path, multiplexed by SessionId. Local, single-user, loopback-bound (127.0.0.1 only) — the WS is a general protocol interface (a raw local script is as valid a client as any future client), so the --allow-origin check is opt-in, never mandatory (ADR-0048, #153). |
Building your own head — a multi-tenant server embedding the engine as a
library, rather than one of the four above — is covered in
docs/embedding.md: session-per-tenant namespacing, the
send/send_from_wire trust split, pluggable persistence and tool-execution
policy, dynamic per-session tool/prompt resolvers, hibernate/resume, and
approval-across-restart semantics, backed by three compiling examples —
embedded.rs,
embedded_lifecycle.rs,
and mcp_http.rs.
Agent profiles (opencode-style)
A session runs under an agent profile — { name, description, mode, model, system_prompt, permission, tools, disallowed_tools, can_spawn, spawnable_agents }. Switch the primary profile with SetAgent; the cycle is
build → plan → research — explore and debug are mode: subagent, so they
are filtered out of the primary cycle and only reachable via spawn. Built-ins:
build, plan, explore, debug, research.
The permission profile (Allow | Ask | Deny per tool, name-or-* or
argument-scoped tool(pattern)) drives the approval flow. build allows
everything; plan asks by default and stays physically read-only apart
from one carve-out — write/edit succeed only under .entanglement/plans/
(#524, ADR-0142), the file a plan lives in (#513, ADR-0145) — explore is
the deny profile (read-only), the default spawn target; debug carries
build's own allow-everything permission (read/write/execute) for a spawned
sub-agent that actually needs to reproduce, fix, and verify a bug; research
is the read-only Q&A entry agent (ADR-0167) — write denied with no carve-out,
shell ask-graded per command, and it may spawn only read-only explore
leaves (re-engaging one with agent_send rather than respawning it), so its
delegation subtree can never widen into a write-capable profile. Permission
resolution and approval live entirely in the runtime (#59).
A session advertises its tool surface in one of two modes (ToolAdvertising,
default ToolSearch, ADR-0196): full sends every registered spec up
front; tool_search sends a lean kernel plus an always-on explore/
describe discovery pair, with the rest of the registry (MCP tools,
endpoints, skill tools, …) reachable by real name once discovered — no
router/envelope tool, every call by its native name either way.
A plan is a file (.entanglement/plans/<id>.md), not an in-memory
snapshot: the plan agent's one tool, propose_plan(content | path),
materializes or binds one, force-parks on approval, and — on approve —
switches the session's mode in place to the approver's pick (build, or
auto on a bare accept) so the same turn continues into implementation
(#513, ADR-0145; ADR-0207 §7). OutEvent::TaskList (markdown content) is a
separate, simpler snapshot from update_tasks: permission-gated but carrying
no host resource, so the runtime intercepts it out of the tool registry and
emits the snapshot instead of dispatching (#231, ADR-0049). Every head
renders both natively.
Definitions are data, layered (embedded < user < project, later wins).
Agents (ENTANGLEMENT_AGENTS_DIR) and skills (ENTANGLEMENT_SKILLS_DIR) are
.md files with YAML frontmatter — drop one in and it joins the registry with
no code change; skutter inspect agents|skills shows the resolved set with
layer provenance. Beyond the root-contained quintet (read/write/edit/
glob/grep), call (argv exec, no shell) is always registered too, and the
sandboxed rhai scripting tool ships in every default build (droppable via
--no-default-features, #502); only bash stays opt-in
(ENTANGLEMENT_ENABLE_BASH=1).
Crates
Three crates, two seams (core ↔ provider, core ↔ runtime). Dependency direction
is provider (leaf) ← core ← runtime (ADR-0053).
| Crate | Role | Hard rule |
|---|---|---|
entanglement-provider |
leaf crate owning the LLM ABI: the Llm trait + DTOs (LlmRequest/Event/Stream, LlmFactory, ToolCall/ToolSpec) + wire Message; z.ai/OpenAI/Ollama + Anthropic + native Gemini clients (#309); connection pool, retry, rate-limit, reasoning stream. Usable standalone for raw LLM queries. |
no entanglement-* deps; owns reqwest. |
entanglement-core |
actor engine: Holly, InMsg/OutEvent, agent turn loop, Context. Advertises tool schemas (ToolSpec) only — holds no executable tools. Depends on provider, drives dyn Llm, re-exports the ABI. |
No UI/web-server deps (clap/axum/crossterm/ratatui forbidden); reqwest is transitive via provider (ADR-0053). Enforced via make tree. |
entanglement-runtime |
the head crate (binary skutter): the Tool trait + ToolRegistry (moved from core, ADR-0059), host-tool impls, tool execution + permission dispatch (✅ #58/#59/#206), approval, user sessions, all transports (stdio ✅, TUI ✅, WS ✅ #153). Selects the concrete provider + glues it to core. Feature-gated cli/provider/tui/serve/mcp-http/rhai (default = ["tui", "serve", "mcp-http", "rhai"], #502); --no-default-features is a lean embeddable library (ADR-0025). |
--no-default-features stays CLI/TUI/transport-free; make check-lean enforces. |
Install
cargo install entanglement-runtime # installs the `skutter` binarySet a provider key
Provider API keys live in a managed env file
(${config_dir}/entanglement/.env, override ENTANGLEMENT_ENV_FILE), loaded at
startup for any var the real environment left unset (env > file). Set one without
hand-editing the file (#304, ADR-0073):
skutter config set-key zai # hidden prompt (never echoed) skutter config set-key openai --key sk-… # or pass it directly echo "sk-…" | skutter config set-key zai # or pipe it (scripting/CI)
z.ai has two catalog entries sharing ZAI_API_KEY, one per key type: zai
(the GLM Coding Plan endpoint, auto-detected) and zai_paas (the
pay-as-you-go endpoint). A pay-as-you-go key is rejected by the Coding Plan
URL — select it with ENTANGLEMENT_PROVIDER=zai_paas (or provider: zai_paas
in config.yml).
Inside the TUI, /key opens the same dialog (provider list → masked input); a key
set there is picked up on the next /model switch with no restart. No key set →
skutter falls back to the EchoLlm debug stub.
Build & develop
Requires stable Rust (pinned via rust-toolchain.toml). Build jobs capped at 4
in .cargo/config.toml.
make run # one dummy turn, text output make run-json # one dummy turn, NDJSON events make run-tui # launch the terminal UI make pipe # stdio pipe head — InMsg NDJSON on stdin, OutEvent NDJSON on stdout make serve # local WebSocket head on 127.0.0.1 (ARGS='--port 4517') make test # unit + integration make test-live # opt-in live z.ai probes (needs ZAI_API_KEY) make lint # clippy --all-targets -D warnings make verify # check-fmt + tree + check-lean + file-cap + userid + lint + test (CI-equivalent) make tree # cargo tree -p entanglement-core (UI/web-server dep hygiene gate) make check-lean # runtime --no-default-features stays CLI/TUI-free (ADR-0025 + ADR-0053) make coverage # cargo llvm-cov --workspace, fails under COV_MIN% (release gate) make install # cargo install --path entanglement-runtime → `skutter` in $CARGO_HOME/bin make build | check | fmt | clean
Drive commands through make, not raw cargo.
CI
GitHub Actions drives the same make targets (.github/workflows/):
ci.yml— runsmake verifyon every PR and every push tomaster, putting thetree(ADR-0006) andcheck-lean(ADR-0025) hygiene gates under automation.release.yml— on av*tag, runsmake verifyplusmake coverage(cargo llvm-cov --workspace, fails underCOV_MIN%) and uploads the lcov/Cobertura reports as an artifact, so a release is blocked on green tests.
make coverage needs cargo-llvm-cov locally: cargo install cargo-llvm-cov --locked.
License
MIT — see LICENSE.
来源:GitHub · github.com