A self-evolving AI agent engine written in Rust that orchestrates multiple AI models to cross-audit answers and improve its own prompts over time. It runs as a terminal-first CLI, interactive TUI, or embeddable headless service, supporting 23+ AI providers with unified abstraction and built-in sandboxing for tool execution.
Multi-provider AI agent CLI written in Rust
At a glance
README

Wayland Core
The self-evolving AI agent. Brilliant today, smarter tomorrow.
Most AI tools are as good as they'll ever be the day you install them. Wayland Core isn't — it convenes a council of rival models on your hardest problems, fuses their best answer into one, and rewrites its own prompts to get sharper every single run. Terminal-first, on your keys, in Rust.
Terminal-first · Multi-provider · Self-evolving · MCP-native · Embeddable · Apache-2.0
Install · Quick start · Providers · Orchestration · Anvil · Crucible · Security · Channels · Browser · Memory · Evolution · Endurance · Embedding · Docs
Most agents are frozen the day you install them, and married to one model. Wayland Core is neither. Hand it a hard problem and it convenes a council of rival models that cross-audit into one answer (Crucible). It rewrites and scores its own prompts between runs (GEPA). Every tool runs in an OS-native sandbox behind a single egress gate, and it speaks MCP in both directions — all from one Rust binary, on your keys. It's the engine inside Wayland Desktop, but it stands alone: a one-shot command, a full-screen TUI, or a headless stream you embed.
Wayland Core is the engine, on its own, open (this repo, Apache-2.0). Wayland Desktop is the GUI product built on it. Core is the engine; Desktop is one application that embeds it.
The 30-second proof
npx @ferroxlabs/wayland-core@latest "read Cargo.toml, list the workspace crates, and explain the dependency layering"
One command. The agent reads the file, runs grep/glob across the tree, reasons, and answers, with every tool call gated and streamed. Or run wayland-core with no arguments and it detects your provider keys and drops you into the TUI:

Paste a key, get a provider. Paste an API key (or run /connect in the TUI) and the engine fingerprints the provider from the key's shape, validates it live, and stores it in your OS keyring. From there, /config exposes Essentials and Advanced editors, /doctor shows provider, key, and MCP health, and /effective prints the resolved config with secrets redacted.
Your whole MCP toolset stays reachable. ToolSearch is the hydration path
for deferred tools, so its result is treated as structured data end to end and
is never passed through a lossy text transform. A large MCP catalogue arrives
intact and parseable, which is what makes the tool genuinely callable on the
next turn rather than merely mentioned. Fixed in v0.13.0.
What it is
- A standalone engine. The engine is the product, not a feature bolted onto an editor and not a wrapper around one vendor's API.
- Terminal-first. A one-shot command, an interactive TUI, or a headless stream. The terminal is the primary home, not an afterthought.
- Embeddable. Drive it from your own app over a typed JSON-Lines protocol. It is exactly how Wayland Desktop uses it.
- Apache-2.0. Permissive. Build on it commercially without an AGPL obligation.
Install
npm (recommended, pulls the right prebuilt binary for your platform):
npm install -g @ferroxlabs/wayland-core
wayland-core --version
# or run it once, no install
npx @ferroxlabs/wayland-core@latest "summarize the TODOs in this repo and draft a triage plan"
Prebuilt binaries for macOS (arm64/x64), Linux (arm64/x64), and Windows (arm64/x64) are on the Releases page, each verifiable against wayland-core-checksums.txt.
From source (Rust 1.95+):
cargo install --git https://github.com/FerroxLabs/wayland-core wcore-cli
Quick start
# 1. Generate a config, then add an API key for any provider
wayland-core --init-config
wayland-core --config-path # shows where the config lives
# 2. One-shot: the agent reads files and uses tools to answer
wayland-core "Read Cargo.toml and explain the dependencies"
# 3. Interactive TUI (just run it)
wayland-core
# 4. Everything else
wayland-core --help
Provider-neutral core
The engine never knows which vendor it's talking to. It builds one neutral request type, LlmRequest, and reads one neutral event stream, LlmEvent — TextDelta, ToolUse, ThinkingDelta, Done, Error. That's the whole contract. Every provider adapter implements a single async trait, LlmProvider, whose core method is stream(&LlmRequest) -> Receiver<LlmEvent>. Wire-format translation happens inside the adapter, where it belongs. The agent loop above it stays vendor-blind.
Vendor quirks don't get hardcoded. There is no if base_url.contains("openai.com") branch anywhere. The differences — field names, message-shape rules, which API surface to hit, reasoning vs. thinking, tool-array caps, temperature support, cache markers — live in one configuration layer, ProviderCompat: 31 Option<T> fields where None means "use the provider's default." 24 preset constructors set those defaults per vendor, and a single map binds each of the 23 built-in providers to its preset. Your config layers on top. Every field resolves as user.or(default), so anything you set wins and anything you leave alone keeps the shipped default. Adapters then read compat instead of sniffing URLs: api_path(), max_tokens_field, uses_responses_api(), supports_temperature, include_usage_in_stream, and the rest.
- 23 built-in providers, one
--provider <slug>switch. The slug picks the wire, the base URL, and the compat preset. - Point any OpenAI-compatible backend at a built-in wire with a custom alias — set
provider,model,api_key,base_url, and you're done. No code. - Override a quirk in config, not in a fork. A self-hosted server that rejects
stream_options?include_usage_in_stream = false. - Data-driven pricing. A bundled
pricing.toml— 46 model rows across 25 provider tables — computes per-token cost in integer microcents from per-Mtok USD rates. Swap the whole catalog withWAYLAND_PRICING_PATH. - Resilience is built in. Transient failures retry automatically, with multi-key rotation on supported providers; opt into a circuit breaker plus same-provider model fallback with one
[provider_chain]block.
# Point a custom backend at the OpenAI wire, then bend one quirk
[providers.my-service]
provider = "openai"
model = "custom-model-v1"
base_url = "https://my-service.example.com/api/openai"
[providers.my-service.compat]
include_usage_in_stream = false # self-hosted server rejects stream_options

Orchestration & swarms
A single agent is the floor, not the ceiling. Wayland Core fans one task out across many workers and brings the results back, with real isolation between them. Three distinct mechanisms ship in the code, and a four-tier topology model governs all of them: Spawn (5 agents), Swarm (100), Mesh (50), Fleet (100). Each tier fixes the agent cap, how much the parent sees, and the blackboard scope — and the caps are enforced, not advisory. Ask for 51 agents on a 50-cap tier and you get TopologyError::ExceedsCap, not a quietly-truncated run.
- Sub-agents (
Spawn) fan parallel work out from one tool call. Each sub-agent gets its own conversation context and its own tool access; the count is capped by the active topology (default Spawn, 5). - Worktree swarm runs N workers as OS subprocesses, each in a fresh
git worktreeon its own branch. A dirty-checkout guard runsgit status --porcelainfirst and refuses to dispatch on an uncommitted tree — that guard exists because a contamination incident in v0.2.2 taught us why it has to. Per-worker timeouts,kill_on_dropSIGKILL on expiry, and idempotentgit worktree remove --forcecleanup. Process isolation, not threads, so one bad worker can't corrupt another. - In-process dispatchers (
MeshDispatcher,FleetDispatcher) are library primitives: they coordinate caller-supplied agent closures over a shared blackboard, enforce the tier cap, apply a timeout, and reduce the reports. Fleet partitions agents into shards (default 10) under topic prefixes likefleet/<run-id>/shard-<i>/. They coordinate and reduce; spawning the agents is the orchestrator's job.
Every worker spawn goes through argv mode — Command::new(program).args(args), no shell interpreter — so worker commands are never re-parsed by a shell. Final stdout/stderr come back through collect(); opt-in heartbeats (.swarm-status.json, ~5s tick) give you liveness without consuming the result.
Roll the results up however the job needs. The wayland-core swarm CLI dispatches the worktree path and routes the collected results through one of four reducers:
# Run the test suite across 4 isolated worktrees, roll up pass/fail/total
wayland-core swarm --workers 4 --worker-command "cargo test" \
--base-branch main --branch-prefix swarm/ci --timeout 30m --reduce fleet
# Strict >50% majority over normalized worker stdout
wayland-core swarm --workers 5 --worker-command "pytest" --reduce consensus
mesh— verbatim passthrough of every worker result.fleet— succeeded / failed / total roll-up.consensus— strict majority: a bucket wins only if its votes are more than half of the successful workers, otherwise the top three are returned as disputed.debate— first round whose workers agree wins; at the CLI the batch is a single round (multi-round replay lives in the orchestrator, not the CLI path).
Topology is pure data with cap enforcement, the guards have tests behind them (58 across the swarm crate), and the live TUI labels the running tier by sub-agent count — 0-5 Spawn, 6-20 Swarm, 21-50 Mesh, 51+ Fleet. Those bands are a display heuristic (tui/agents/strip.rs:431) and are deliberately not the tier caps above: Swarm's own cap is MAX_DISPATCH_WORKERS = 100, so a 30-worker Swarm is within its cap while the TUI is labelling that count "Mesh". One note on reach: the standard monitored relay clamps Spawn fan-out to the Mesh cap of 50, so the 100-agent Fleet ceiling is the unmonitored library path, not the everyday Spawn call.


Anvil — the gated forge (Smart Loops)
Anvil is where work with a real executable gate goes — tests, a build, a typecheck. Hand it a task with a provable finish line and it iterates until the gate actually passes, then hands you a machine-stamped receipt. Judgment work with no checkable reward — naming, prose, architecture opinions — goes to Crucible instead. That's the whole doctrine: the gate is the anvil, the models are the hammer.
It's on by default and needs zero config. In a repo with a test suite:
wayland-core forge "fix the failing auth tests"
Or just ask in a session — "iterate until it provably passes" — and the Forge tool routes the work to the forge. The gate is auto-detected from the workspace (cargo, npm, go, pytest, just, make); an explicit [anvil] gate always wins.
How a climb works: a builder sub-agent is forked into an isolated git worktree, the gate runs sandboxed (network-denied) after every round, and the climb accepts only strict improvement — a candidate that doesn't beat the current best is discarded, not merged. Your tree is never touched; the winning change lands on a review branch.
Every climb ends in a receipt:
Forged: verified · 1/1 checks · 3 iterations
Only machinery earns verified — a model's opinion of its own work never does. A climb that can't get there reports an honest terminal state (needs_escalation, timed_out) instead of a hopeful one, and cost is shown or marked "unpriced" — never $0.
The escalation valve. When consecutive rounds fail with the same fail-set, the climb buys exactly ONE read-only diagnostic turn from your frontier session model, feeds the guidance back to the cheap builder, and resumes. You pay for the unblocking, not the grinding.
Seat routing. The frontier model plans, a mid-tier driver iterates — or FluxRouter's flux-auto lane drives when a Flux key is connected — and machinery verifies. Three knobs in [anvil]:
[anvil]
enabled = true # kill-switch; a project config can disable, never re-enable (tighten-only)
# gate = ["cargo", "test"] # empty = auto-detect from the workspace
# driver_provider = "flux-router" # explicit driver seat; driver_model pins the model
Safety is structural, not advisory. The forge is invocation-only and refuses outright without a gate. Builders get edit tools but no shell — the gate does the executing. Trampoline gates (npm test, make test, just test) are content-pinned against tampering: a candidate that touches the dispatch manifest fails a Safety-class gate-integrity check that can't be traded away. Baseline probes run sandboxed in scratch worktrees, never in yours.
Shipped in v0.12.25 as the Smart Loops layer, live-proven end to end. → docs/advanced.md
Crucible — a Mixture-of-Providers council
Crucible is a council of rival providers. Hand it a hard task and it fans out to N sub-agents, each pinned to its own LLM provider — Anthropic, OpenAI, DeepSeek, GLM, Kimi, Gemini, Flux-routed models — that answer in parallel; a separate, read-only judge then fuses them into one. The diversity is the whole point: cross-vendor, not one family arguing with itself. We call it Mixture-of-Providers.

Convening a council live in the TUI: two proposers pinned to different vendors, an independent judge from a third, the certified ceiling ($0.70) beside the single-model cost ($0.49), the daily envelope, and the gate's reasoning — all on the table before you approve a cent.

…and the fused output: a three-vendor council ranking the audit by severity — every proposal, provider, and cost on the table. Head-to-head benchmarks (Crucible vs. router-level mixtures vs. solo frontier models, cost-matched) are in flight.
It's off by default. List a roster in a [crucible] block:
[crucible]
enabled = true
proposers = ["anthropic:claude-opus-4-7", "openai:gpt-5", "deepseek:deepseek-v4-pro"]
aggregator = "anthropic:claude-opus-4-7" # optional; falls back to the first usable proposal
Then run it: wayland-core crucible "do a security audit of this deployment plan".
Each member pulls its own credentials from your [providers] map, so a council is genuinely keyed across vendors, not one key wearing hats. Routing prefixes don't defeat that — a Flux-pinned GPT-5 and a direct openai:gpt-5 collapse to the same vendor family, so the judge stays independent and an Auto roster stays diverse.
Then the cost discipline, because N models answering one question costs N times as much:
- A deterministic preflight gate decides whether to convene at all. A zero-LLM keyword/length classifier reads the leading instruction span and sizes the roster by stakes — low goes Direct (one call), medium pulls 3 members, high pulls 5. A high-stakes word buried in a pasted stack trace won't escalate it.
- Two roster modes. Manual: you list the providers. Auto: a deterministic Assembler picks a cost-effective, vendor-diverse roster per task, and you can
--denya vendor or force--deep. - Spend is gated before anything spawns. A judge-inclusive worst-case ceiling is certified up front. The per-run
max_cost_usdcap is strict — an unpriceable roster under a cap is refused, not run. A default-on $20/user/day envelope rides on top. - It fails closed. In a non-interactive session it refuses to spend unless you've explicitly opted in. On a TTY it prints a cost card and waits for Y/n.
- The judge can't touch your machine. The aggregator is a read-only sub-agent — no
Bash, noWrite, noEdit, by construction. Every proposal reaches it wrapped in untrusted-data fencing with forged section delimiters neutralized, so one poisoned proposer can't hijack the synthesis.
Fan-out is bounded by a per-route semaphore, and tail latency is capped: each proposer gets a hard deadline, and once quorum is met a global soft-deadline cancels the stragglers — timed-out members are kept as errored proposals so the provenance stays honest. The fused answer is either printed (Terminal mode) or injected as private guidance into the normal tool-using loop (Advisor mode, --advisor).
Shipped in v0.12.11. The council pipeline carries 84 unit tests plus 31 integration tests across the gate, resolver, roster validation, budget, fan-out, and injection-fencing paths.
Where it's honest about its edges. Some Flux-routed SKUs are unpriced today, so a Flux council can't always certify a hard ceiling — that's exactly why the per-run cap is opt-in and the daily envelope only soft-binds on Flux, accruing from actual usage instead of refusing up front. The daily envelope binds within a process, not yet across separate CLI runs. The convene-or-not gate is a deterministic heuristic, not a learned router. And the shipped invocation surface is the wayland-core crucible batch command; the slash command, natural-language tool, and full TUI/desktop approval cards are designed, not all shipped.
Security by default (fail-closed)
Security here is a posture, not a checkbox. When the safe thing and the convenient thing disagree, the engine picks safe and makes you opt out on purpose. Four mechanisms carry that, and they hold up when someone reads the source.
- No unsandboxed default. Model-driven shell and tools run inside an OS-native sandbox — bubblewrap on Linux,
sandbox-execon macOS, Docker if you opt in. When no real sandbox is available, execution is refused, not quietly downgraded to host permissions. Running with no isolation takes an explicitWAYLAND_ALLOW_NO_SANDBOX=1. A strayWAYLAND_SANDBOX=nonedoes nothing without it. Windows is different and we say so: the default there is a kill-on-close Job Object that owns the whole process tree and a scrubbed child environment, with no filesystem or network confinement — measured, AppContainer STRICT could not launch a usable shell, so it is now opt-in viaWAYLAND_SANDBOX=appcontainer. Because the Windows default cannot enforce a secret read-deny, the engine withholdsBashfrom remote/channel workspace sessions there rather than pretending otherwise. - One egress chokepoint, enforced by a lint. Every outbound HTTP request flows through a single client, and a clippy lint bans constructing a raw
reqwestclient anywhere else — so a missed migration fails the build instead of leaking a hole. On that seam sits a fail-closed host allowlist for untrusted URLs, an exfil-shape classifier that hard-denies suspicious POSTs and high-entropy paths to non-allowlisted hosts, a hard byte-cap body reader, and a resolve-once resolver that re-checks the IP at connect time to close DNS-rebinding races. Deny stops before the socket opens. Shared multi-tenant suffixes —amazonaws.com,*.workers.dev,*.vercel.app, around 45 of them — can never be apex-allowlisted. - SSRF and metadata floor, always on. Cloud-metadata endpoints (
169.254.169.254and the GCP, AWS, Alibaba, and Oracle equivalents) and lookalike hosts are rejected outright, independent of any allowlist you configure. - Output validation and secret scrubbing. Model output runs through a validator (refusal, credential-leak, and format checks) and a scrubber that redacts about 30 credential and PII shapes — AWS, OpenAI, Anthropic, GitHub, Slack, and Stripe keys, JWTs, PEM private keys, DB connection strings — down to
[REDACTED:KIND]. An optional LLM-judge validator is budget-capped and fails open: it skips when the budget is spent and never blocks your turn. - Default-deny permissions. A multi-actor policy engine gates every (actor, resource, action) tuple; no matching grant means denied. File globs reject any
..segment before matching, and bearer tokens are SHA-256 signed with a TTL, revocation, and rotation that cannot extend an expiry.
The egress gate is on out of the box. Tell it what to trust:
[security]
enabled = true # default; egress gate on
egress_allow = ["example.com", # registrable domain — covers subdomains
"myapp.workers.dev"] # shared host — exact match only, no apex
Turning the gate off takes enabled = false, and only the operator can write it. [security] enabled is read from your global config alone, so a project-level config.toml that arrives with a cloned repo cannot disable the boundary no matter what it says. No environment variable sets it, and there is no CLI flag: the switch is the global [security] block or nothing.
The source backs this: the sandbox crate alone carries 110+ test cases, with 90+ more across output safety and 50+ across permissions, and the threat model is written down in docs/security/permissions-threat-model.md.


Browser + computer use
The agent doesn't stop at the filesystem. Wayland Core ships two real desktop-automation tool families, both in-tree, both security-gated: a browser the agent drives, and synthesized control of the host desktop itself.
The Browser tool is an interactive browser with a fixed, locked surface of 18 operations — navigate, snapshot, read, click, fill, press, select, upload, download, screenshot, get_state, wait_for, network_log, console, new_tab, close_tab, back, forward. It is ARIA-tree-first by design: the model reasons over an accessibility snapshot with @e1/@e2 element refs, not raw DOM, so a page costs a bounded chunk of the prompt budget instead of dumping a megabyte of markup at the model. There is no JavaScript-evaluation op. The Evaluate variant is deliberately banned, and a test enforces its absence. For a plain read-only fetch the tool tells the model to use WebFetch instead.
The Cua tool is computer use: synthesized mouse, keyboard, screenshot, and accessibility-tree control of the host across macOS, Linux X11, Linux Wayland, and Windows. Eleven locked ops — left_click, right_click, double_click, mouse_move, scroll, type, key, screenshot, ax_tree, wait, frontmost_app. The defining invariant is that it stays out of your way: synthesized input must never move your cursor, raise a window, or steal foreground focus. On macOS that means input is posted at the HID layer (CGEventTapLocation::HID) and the code never calls an activate API — a focus_invariance_test locks it down.
- Multi-backend, runtime-selected. Camoufox is the default and talks to a sidecar over HTTP; Chromium (chromiumoxide/CDP) and Browserbase (cloud) are opt-in behind cargo features. The default build is Camoufox-only. CUA picks its backend from the platform: CGEvent on macOS, x11rb XTest on Linux X11 (on by default),
wlrctl+grimon Wayland (opt-in), UI Automation + SendInput on Windows. An unsupported target returns a typed error, never a silent no-op. - Fail-closed network policy.
BrowserPolicyruns pre-dispatch on every URL-bearing op,default_action = Denysince v0.2.1. Scheme allowlist is http/https only. Hardcoded blocks — regardless of your allow/deny lists — cover RFC1918 ranges, loopback, the169.254.169.254metadata endpoint, link-local, IPv6 ULA, and legacy IPv4 encodings (octal, hex, decimal). A TOFU cache pins host→first-IP to refuse DNS-rebinding, and the policy is re-evaluated on every redirect hop, capped at 10. - Filesystem confinement. Model-chosen download and upload paths must be absolute, no
.., no null bytes, no dotfile or OS-secret target, symlink-aware and confined to a downloads root. If you set none, it fails closed onto a temp directory so confinement always runs. - App-aware HITL gating.
CuaPolicyresolves the frontmost app and gates every op: forbidden apps are rejected, others suspend for human approval. Forbidden key combos are checked on bothKeyops andTypetext, with unicode/glyph normalization (Cmd+Q,command-q,^Q, glyphs all canonicalize tocmd+q). Unknown frontmost app plus any app-scoped rule routes to Suspend. - Screenshot redaction. Two passes: an always-on heuristic password-band blur, then OCR-backed sensitive-text blur (Apple Vision on macOS, Windows.Media.Ocr on Windows, Tesseract on Linux behind a feature). The matcher catches emails, SSNs, 13–19-digit cards, and key prefixes like
sk-,ghp_,aws_secret_. Best-effort, off by default. - Isolated, cancellable. Each sub-agent gets its own cookie jar and tab. Every op races a wall-clock deadline, the cancel token, and completion in a
tokioselect — Navigate gets 60s, Click/Fill 10s, and so on, with the browser running as a 120s-budget MCP-category tool, not a 600s one.
Both families register through a thin plugin shell. Per audit F2 the wayland-browser and wayland-cua crates carry no dependency on the real wcore-browser/wcore-cua engine crates — they register a spec mirror through the plugin API and the host reifies the actual tool, so the isolation boundary is structural, not a convention. The host has to advertise capabilities.browser_suite / capabilities.computer_use or neither family registers at all. On Linux Wayland the adapter goes further and refuses registration outright on a restricted compositor (GNOME mutter default, focus-steal-off Hyprland), probed live and re-checked mid-session.
[browser.policy]
# Disabled by default (fail-closed). Allow specific domains to turn it on:
allowed_origins = ["example.com", "*.mysite.com"]
# Or, not recommended (SSRF risk):
# default_action = "allow"
The depth shows up in the tests: 120+ #[test]/#[tokio::test] attributes across wcore-browser (the policy suite alone has 27), 99 across wcore-cua. → docs/tools.md
Omni-channel deployment & scheduled triggers
The same engine runs as a chat bot on ten messaging platforms, and fires itself on a schedule. Drop a TOML file in ~/.wayland/channels/, boot the engine, and every enabled channel auto-registers. From there the agent answers inbound DMs and group messages with a real agent turn — reading, tool-calling, reasoning — and sends the reply back through the same platform.
Each platform is its own crate, written against the platform-native API, not a generic webhook shim: Slack (Web API plus an Events webhook, HMAC-SHA256 signed with a 5-minute replay window), Discord (REST plus a Gateway WebSocket with heartbeat), Telegram (long-poll), WhatsApp (Cloud API plus Meta X-Hub-Signature-256), Signal (a signal-cli subprocess under a respawn supervisor), SMS (Twilio, HMAC-SHA1 webhook), email (SMTP out, IMAP poll in), iMessage (macOS, reads chat.db read-only, sends via AppleScript), Matrix (raw CS-API, deliberately no matrix-sdk), and MS Teams (Bot Framework, OAuth2 client-credentials).
- Three of the ten have been driven at the real platform. Slack, Discord and Matrix were each exercised against a real workspace, guild and room by the shipped binary, and two of them came back wrong: Slack and Discord had both claimed exactly-once delivery on the strength of a mock, and each produced two messages when a delivery key was finally replayed live. Both are now corrected to at-most-once; Matrix held, and is the only exactly-once adapter of the ten — conditionally, for a body that fits in one platform message (32,768 chars), above which it is chunked, sent unkeyed, and degrades to at-least-once. The other seven — Telegram, WhatsApp, Twilio SMS, email, Signal, iMessage, MS Teams — are implemented and carry their own test suites, but no replay has been driven at their real destination, which is real evidence about our code and none at all about the platform's behaviour. → docs/delivery-semantics.md has the per-adapter table, and a test fails the build if it drifts from the code.
- One file per channel, auto-registered on boot. The engine scans the channels dir, parses each TOML, and registers the rest. A disabled, unknown, or malformed config is skipped with a warn log — one bad file can never crash boot.
- Inbound is fail-closed. An unconfigured channel denies every message. The default DM policy is an empty allowlist, groups are disabled, and a mention is required. You name the stable platform sender ids that may drive the agent; everyone else is refused.
- A tool posture decides what the agent may touch.
conversational(default) gives the host no filesystem or shell. Opt up toworkspace(jailed to a workspace root) orfull(host-wide). It is enforced at the tool registry, so dropped tools are un-dispatchable, not merely hidden. - Secrets are handles, not tokens. The TOML carries a credential handle resolved from the OS credentials store at connection time. Over-long replies are chunked per the connector's own message-length limit, and reconnects back off and retry.
# ~/.wayland/channels/tg.toml
name = "tg"
platform = "telegram"
enabled = true
[options]
credential_handle = "telegram.acme.bot_token"
[inbound]
dm = "allowlist"
dm_allowlist = ["123456789"] # platform sender ids; "*" = anyone
group = "disabled" # open | allowlist | disabled
require_mention = true
tools = "conversational" # conversational (default) | workspace | full
ack = "both" # off | reactions | typing | both
Scheduled triggers are the other half. The cron subsystem parses standard 5-field crontab (and 6/7-field) expressions and fires one of three action types on a recurrence: run a slash command, post a message to a channel, or invoke a skill. Jobs persist to ~/.wayland/cron/jobs.json with an atomic write; outcomes append to a history ring buffer. It ticks every 30 seconds — either inline at engine boot, or as a detached daemon:
wayland-core cron add "0 9 * * *" --skill morning-brief
wayland-core cron add "*/15 * * * *" --slash "/status"
wayland-core cron add "0 8 * * 1" --channel team --text "Good morning"
wayland-core cron list # also: status · history · enable · disable · logs
wayland-core cron daemon # detached background runner
What we do not claim yet. Not every platform is full duplex. MS Teams is duplex: inbound arrives over the webhook host at /webhooks/msteams, authenticated by Bot Framework JWT validation (signature, issuer, audience, expiry) with the token's serviceUrl claim bound to the Activity's — though its inbound attachments are still not fetched. The inbound webhook host serves Slack, WhatsApp, Twilio SMS and MS Teams — the poll-based connectors (Telegram, Matrix, Signal) don't take webhooks. DM pairing is fail-closed: a pairing request is denied until you add the sender to the allowlist by hand. In the headless daemon, --skill and --channel jobs dispatch, but --slash jobs record as staged rather than executing. And inbound media enrichment (image-to-description, voice-to-transcript) is inert unless a vision or transcription key is configured. tools = "full" on a publicly reachable channel is host-wide access identical to a local CLI session — the code and docs both flag it as dangerous.
Ten platform crates, in-tree, each wired to a registry factory. More than 500 tests across the channels, registry, cron, and per-platform crates. → docs/channels.md
Memory, sessions & cost governance
An agent that forgets every session and spends without a ceiling is a liability. Wayland Core gives it a persistent brain that's on out of the box and hard money guardrails you set, both built to hold up when someone reads the source.
Cross-session memory. A SQLite-backed store so the agent remembers what it saw, did, learned, and concluded, instead of starting blank every run. It is organized as five partitions (Working, Episodic, Semantic, Procedural, and a Core user-model) across three durability tiers (Session, Project, Global), which is exactly nine valid cells, not fifteen. The dispatcher enforces that matrix and rejects a write to any invalid cell, with the count locked by unit tests. It is on by default — a fresh install gets a real memory backend, so self-evolution, skill routing, and the user-model all work out of the box; opt out with --no-memory or memory.enabled = false. It never retroactively ingests sessions from before it was active: memory is forward-only by design.
It also keeps itself from growing unbounded:
- Decay, not deletion. A relevance score of
exp(-age_days / 7.0)ages entries down, and episodes past ~30 days flip to Archived. Nothing is everDELETEd, so old context stays queryable while it stops dominating recall. - A dream cycle at session end runs four phases in order: Compress (summarize batches of Working entries into Episodes), Consolidate (Episodic into Semantic facts), Crystallize (a pattern seen three or more times becomes a staged Procedure), then Decay. It is throttled to once every 30 minutes by default.
- Deny-by-default access. Every read and write validates an access token against a partition-plus-tier ACL, and every access is logged to an audit DB. Facts are append-only; a correction is a new row that supersedes the old one, and secrets are stripped on the compaction path.
Sessions. Every run is saved to disk, the provider, model, working directory, token usage, and full message history, under a versioned schema with a migration ladder and WAL crash recovery, so a SIGKILL mid-turn does not corrupt the file. Resume the most recent with -c, jump to a specific one with --resume <id>, see them all with --list-sessions, or print what the agent remembers about one with --memory-show <session>.
Cost governance. Real spend caps reserve the next provider call before dispatch and settle it from returned usage afterward. Set per-session input, output, and dollar caps; concurrent reservations are atomic, so parallel calls cannot each spend the same remaining allowance. A separate execution budget tracks the whole session tree—wall time, tool runtime, process-spawning call concurrency, and agent depth—rolling child counters up to ancestors and checking caps in a fixed order. Per-turn cost resolves the provider and model actually dispatched against the bundled pricing catalog; an unpriceable route is rejected while a strict dollar cap is active rather than treated as free.
Two honest limits, stated up front. A transport failure can occur after a provider accepted a request but before usage returns; that ambiguous attempt consumes its conservative reservation so a retry ring cannot exceed the cap. The process concurrency cap limits admitted tool calls that spawn native processes, not every descendant PID inside one admitted shell command; descendant process-tree limits remain the platform sandbox's responsibility.
# .wayland-core.toml — memory is ON by default; spend caps are opt-in
[memory]
enabled = true # the default; set false (or --no-memory) to opt out
dream_cycle_throttle_secs = 1800 # min gap between dream cycles (30 min)
decay_interval_secs = 3600 # decay sweep cadence (1 hour)
[session_cap]
max_tokens_in = 200000
max_tokens_out = 16384
max_cost_usd = 1.50 # reserves before each provider dispatch
max_wall_time_secs = 600 # execution-tree caps
max_tool_runtime_secs = 120
max_concurrent_process_tools = 8 # legacy `max_processes` is still accepted
max_agent_depth = 4
[budget]
max_daily_cost_usd = 20.0 # UTC-day ceiling, ACROSS sessions and processes
Every cap above is per session, which means none of them bind a caller that
starts a fresh session per process — a crash-looping daemon, a cron job, a
channel gateway answering inbound messages. Each run is correctly inside its
own budget while the machine bills without limit. max_daily_cost_usd is the
one that binds that shape: it is backed by a small durable ledger holding only
the current UTC day's spend, mutated under an exclusive cross-process file lock
and published atomically, so concurrent processes serialize and a crash between
reserve and settle over-counts for one lease rather than reopening the hole. It
is opt-in and absent by default.
wayland-core --list-sessions
wayland-core -c # resume most-recen
Comments (0)
Sign in to join the discussion.
No comments yet
Be the first to share your take.