跳到正文
GitHub·· 8 天前AI 评分65

entanglement 发布无头 Rust AI 编码智能体引擎,四种接口全部落地

xmiksay/entanglement

AI 导读

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.

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` binary

Set 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 — runs make verify on every PR and every push to master, putting the tree (ADR-0006) and check-lean (ADR-0025) hygiene gates under automation.
  • release.yml — on a v* tag, runs make verify plus make coverage (cargo llvm-cov --workspace, fails under COV_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