diff --git a/.agents/skills/deep-review/SKILL.md b/.agents/skills/deep-review/SKILL.md new file mode 100644 index 000000000..361c7d032 --- /dev/null +++ b/.agents/skills/deep-review/SKILL.md @@ -0,0 +1,156 @@ +--- +name: deep-review +description: This skill should be used when the user asks for a "deep review", "thorough review", "multi-agent review", "full review", or "review this PR/branch/diff" beyond a quick pass. Fans out parallel specialist reviewer agents (security, logic, conformity, quality, tests, contracts, docs, UI) over a shared change map and synthesizes one deduplicated, severity-ranked report. +--- + +# Deep Review + +Multi-agent code review. You are the orchestrator: you build the change map, fan out dedicated reviewer agents in one batch, and own the synthesized report. Reviewers advise; you decide. + +The reviewers are project agents defined in `.omp/agents/review-*.md`. Each is read-only, carries its own area brief and severity contract, and returns structured findings. Read an agent file to see exactly what a reviewer checks. + +## Principles + +- Reviewers NEVER edit code or run builds/gates/test suites. Their agent definitions enforce this; repeat it in the batch context. +- Fan out exactly as wide as the change surface justifies. Never pad the batch with areas the change does not touch. +- Subagents start blank. Each task item carries the change map (or a `local://` pointer to it) and its specific focus. + +## Severity Scheme + +| Severity | Meaning | Examples | +|---|---|---| +| P1 | Must fix before merge | Correctness bug, security hole, data loss, breaking change, race condition | +| P2 | Should fix | New contract without a test, convention violation with teeth, real maintainability debt | +| P3 | Optional | Nits, style preferences, speculative improvements | + +Every P1/P2 finding must include a targeted verification command. + +## Reviewer Roster + +Core reviewers run on every review. Conditional reviewers run only when their trigger surface appears in the change map. + +| Name | Agent | Trigger | +|---|---|---| +| SecurityReview | `review-security` | always | +| LogicReview | `review-logic` | always | +| ConformityReview | `review-conformity` | always | +| QualityReview | `review-quality` | always | +| TestReview | `review-tests` | always | +| ContractReview | `review-contracts` | public API, migration, config, or wire/event surface changed | +| DocsReview | `review-docs` | user-facing behavior, config, or feature changed | +| UiReview | `designer` (bundled) | UI/frontend files changed; task: point at changed UI files, ask for visual/UX/accessibility review | + +## Workflow + +### Phase 0 — Map the change surface (you, inline; never delegated) + +Establish WHAT is under review before spawning anyone: + +- PR: read `pr://` for intent and discussion; diff base..head. +- Branch: `git log ..HEAD --oneline` and `git diff ...HEAD --stat`, then the full diff. +- Working tree: `git status` + `git diff`. + +Build the change map: + +1. Changed files grouped by subsystem. +2. Exported symbols added/changed/removed. +3. Callers of changed exported symbols (`lsp references`). +4. Surface triggers: migrations? public API? config/env? UI? user-facing behavior? + +Write the map to `local://change-map.md` if it exceeds ~50 lines. Decide which roster areas trigger. Skipping an area requires a stated reason in the final report. + +### Phase 1 — Fan out (exactly one `task` batch call) + +Spawn ALL triggered reviewers in a SINGLE `tasks[]` batch, each with its roster `agent`. Never serialize reviewers across multiple calls. + +Shared `context`: + +``` +# Goal +Deep review of : . +# Constraints +- READ-ONLY: no edits, no writes, no builds, no test-suite or gate runs. +- Report only what the diff touches or directly affects. +# Contract +- Change map: +- Return your structured findings per your output schema; severity per P1/P2/P3; P1/P2 include a verification command. +``` + +Each task item: + +``` +# Target + +# Focus + +``` + +Attach this `outputSchema` to every item: + +```json +{ + "type": "object", + "required": ["summary", "findings"], + "properties": { + "summary": { "type": "string", "description": "2-3 sentence area verdict" }, + "findings": { + "type": "array", + "items": { + "type": "object", + "required": ["severity", "confidence", "title", "location", "evidence", "recommendation"], + "properties": { + "severity": { "enum": ["P1", "P2", "P3"] }, + "confidence": { "enum": ["high", "medium", "low"] }, + "title": { "type": "string" }, + "location": { "type": "string", "description": "file:line" }, + "evidence": { "type": "string", "description": "quoted code + why it is wrong" }, + "recommendation": { "type": "string" }, + "verification": { "type": "string", "description": "targeted command; required for P1/P2" } + } + } + } + } +} +``` + +A reviewer that finds nothing returns empty `findings`. That is a valid result — do not respawn to force findings. + +### Phase 2 — Synthesize (you) + +1. Collect structured outputs from all reviewers. +2. Dedupe: same root cause from multiple areas → one finding, highest severity, note all reporting areas. +3. Spot-check every P1 by reading the cited code yourself. A false P1 erodes the report's trust. +4. Contradictions: reviewers stay `idle` after yielding — message them via `hub` (`send` with `await: true`) instead of respawning. +5. Drop low-confidence P3s unless they corroborate another finding. + +### Phase 3 — Report + +``` +## Deep Review: + +### Verdict +merge | merge after P1 fixes | do not merge — one-sentence justification + +### Findings +| # | Severity | Area | Location | Finding | Recommendation | Verification | +(severity-ordered, deduplicated) + +### Coverage +Areas run / skipped + reasons. Reviewer disagreements and how resolved. + +### Residual Risk +What static review cannot see: runtime behavior, external systems, perf under load. + +### Open Questions +Low-confidence items and judgment calls needing a human. +``` + +## Guardrails + +- NEVER delegate Phase 0 or Phase 2 — decomposition and adjudication stay with you. +- NEVER spawn a second wave to re-review covered ground; use `hub` follow-ups with the idle reviewers. +- If the change surface is tiny (< ~3 files, no API/schema/security surface), say so and review it yourself instead of fanning out. + +## Model Routing (optional) + +Reviewer agents inherit the session model by default. For a dedicated review model, add `model: "@review"` to each `.omp/agents/review-*.md` frontmatter and set `modelRoles.review` in `~/.omp/agent/config.yml`. diff --git a/.gitignore b/.gitignore index c50cebe07..814573ba9 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ docs/phases/ docs/specs/ PROJECT-STATUS.md .worktrees/ + +# Deep-review output, kept out of the tree +review_report.*.md diff --git a/Cargo.lock b/Cargo.lock index 66afb6e99..55ea8d1da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -800,6 +800,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bit_field" version = "0.10.3" @@ -970,6 +985,12 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "brotli" version = "8.0.2" @@ -2788,6 +2809,9 @@ name = "email_address" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] [[package]] name = "emojis" @@ -2916,9 +2940,9 @@ dependencies = [ [[package]] name = "ethnum" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" [[package]] name = "euclid" @@ -2976,6 +3000,17 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fancy-regex" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fast-float2" version = "0.2.3" @@ -3090,6 +3125,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "flume" version = "0.11.1" @@ -3113,6 +3159,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -3137,6 +3189,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "fs4" version = "0.8.4" @@ -3614,7 +3676,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -3623,6 +3685,17 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + [[package]] name = "hashlink" version = "0.10.0" @@ -4546,6 +4619,61 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonschema" +version = "0.49.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "508004a5500f2e1f68af048f70feea2de86d35ab115d85716530860822aef397" +dependencies = [ + "ahash", + "async-trait", + "bytecount", + "data-encoding", + "email_address", + "fancy-regex", + "fraction", + "getrandom 0.3.4", + "idna", + "itoa", + "jsonschema-regex", + "jsonschema-value", + "num-cmp", + "num-traits", + "percent-encoding", + "referencing", + "regex", + "reqwest 0.13.2", + "serde", + "serde_json", + "strum 0.28.0", + "tokio", + "unicode-general-category", + "uuid-simd", +] + +[[package]] +name = "jsonschema-regex" +version = "0.49.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8b30cafa78358ae6cd1494a7d6410b89530e28bf567f862c869c667e900d9f" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "jsonschema-value" +version = "0.49.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5526bd381d230af94908d07e6835a33fd82a465e12f5f1e9c81f5c2aa23b3c21" +dependencies = [ + "ahash", + "bytecount", + "fraction", + "num-cmp", + "num-traits", + "serde_json", +] + [[package]] name = "kqueue" version = "1.1.1" @@ -4786,7 +4914,7 @@ dependencies = [ "prost-types", "rand 0.9.2", "snafu", - "strum", + "strum 0.26.3", "tokio", "tracing", "xxhash-rust", @@ -5555,6 +5683,12 @@ dependencies = [ "libc", ] +[[package]] +name = "micromap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" + [[package]] name = "mime" version = "0.3.17" @@ -5865,6 +5999,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -5891,6 +6039,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + [[package]] name = "num-complex" version = "0.4.6" @@ -6279,6 +6433,12 @@ dependencies = [ "ureq", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "ownedbytes" version = "0.9.0" @@ -7208,6 +7368,25 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "referencing" +version = "0.49.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7af3eb523cce0df0af3c30d624b829b2dabd233172b5bc2615fcd03ceae8f746" +dependencies = [ + "ahash", + "async-trait", + "fluent-uri", + "futures", + "getrandom 0.3.4", + "hashbrown 0.17.1", + "itoa", + "micromap", + "parking_lot", + "percent-encoding", + "serde_json", +] + [[package]] name = "regex" version = "1.12.3" @@ -7342,6 +7521,7 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2 0.4.13", @@ -8440,7 +8620,7 @@ dependencies = [ [[package]] name = "spacebot" -version = "0.5.0" +version = "0.6.0" dependencies = [ "aes-gcm", "anyhow", @@ -8471,6 +8651,7 @@ dependencies = [ "ignore", "imap", "indoc", + "jsonschema", "lance-index", "lancedb", "lettre", @@ -8870,7 +9051,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -8886,6 +9076,18 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "subtle" version = "2.6.1" @@ -10027,6 +10229,12 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + [[package]] name = "unicode-ident" version = "1.0.23" @@ -10228,6 +10436,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "v_frame" version = "0.3.9" @@ -10257,6 +10475,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index f6b6a0195..35fd849a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spacebot" -version = "0.5.0" +version = "0.6.0" edition = "2024" default-run = "spacebot" @@ -160,6 +160,7 @@ open = "5.3.3" urlencoding = "2.1.3" url = "2" moka = "0.12.13" +jsonschema = { version = "0.49.3", default-features = false, features = ["resolve-async"] } [features] metrics = ["dep:prometheus"] diff --git a/README.md b/README.md index db17399b0..1de216b5d 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,8 @@

spacebot.shHow It Works • - Goals & Tasks • + Task BoardQuick Start • - SpacedriveDocs

@@ -103,19 +102,23 @@ Channel context hits 80% → Channel never interrupted ``` -For process capabilities, tool access by type, memory internals, cron, and multi-agent isolation, see [ARCHITECTURE.md](ARCHITECTURE.md). +For process capabilities, tool access by type, memory internals, cron, and multi-agent isolation, see the [architecture guide](). --- -## Goals and Tasks +## The Task Board -Spacebot is built around a task system. Goals set direction. Tasks carry work. The agent executes, remembers, and improves whether or not you're present. +Spacebot is built around an instance-level task board. Every agent on the instance shares it, every task carries a globally unique number, and it is the substrate through which agents delegate work to each other. -On a configured interval, the **autonomy channel** wakes with full context: identity, memory, working memory, the complete task state, active goals, and a summary of its last few runs. It picks the most important ready task, executes it with full tool access, and exits. +A task moves through seven states — `pending_approval` → `backlog` → `ready` → `in_progress` → `done`, with `blocked` (waiting on a person) and `skipped` (settled without running) off the main line — at one of four priorities. Two agent IDs sit on every task: the **owner** (who created it) and the **assignee** (who executes it). Reassigning is how work changes hands. -State lives in tasks. Progress notes go on the task itself. After a crash, the next wake reads task metadata and picks up where things left off. At the end of each run the autonomy channel writes a summary of what happened. On next wake, that summary is the first thing it reads. +**Agents delegate by creating tasks.** When one agent calls `send_agent_message` on another, that is not a chat message — it opens a task on the target agent's board, owned by the sender and assigned to the recipient, at `ready`. The board is the inter-agent protocol, and who may message whom is constrained by the communication graph. -**The agent proposes. You decide.** Tasks the autonomy channel creates land in `pending_approval`. Nothing runs autonomously until you approve it. +**Pickup is autonomous.** The cortex runs a background loop that claims the highest-priority `ready` task assigned to its agent, flips it to `in_progress`, and hands it to a worker with full tool access. The claim is a conditional `UPDATE ... WHERE status = 'ready'`, so two agents racing the same task can't both win it. Progress notes go on the task itself, so after a crash the next pass reads task metadata and resumes. + +**The agent proposes. You decide.** Tasks the cortex creates land in `pending_approval` and raise a dashboard notification. Nothing runs autonomously until you approve it. + +Goals live in the memory graph as `Goal`-typed memories rather than as board rows — they set direction and get recalled into context, but they aren't scheduled or executed the way tasks are. --- @@ -167,6 +170,47 @@ Workers come loaded with tools for real work: - **[OpenCode](https://opencode.ai)** — spawn a full coding agent as a persistent worker with codebase exploration, LSP awareness, and deep context management - **[Brave](https://brave.com/search/api/) web search** — search the web with freshness filters, localization, and configurable result count +### Projects and Repos + +Projects are how agents get pointed at real codebases. A project is a root directory registered once at the instance level and usable by every agent — no re-registering the same repo per agent. + +- **Multi-repo by design** — one project holds many git repos, each tracked with its remote URL, default branch, current branch, and disk usage +- **Worktrees** — git worktrees are first-class rows linked to their repo, so parallel branch work gets its own directory instead of fighting over one checkout +- **Worker targeting** — `spawn_worker` accepts a `project_id` or `worktree_id`; the worker's working directory and prompt context are set from it automatically +- **Sandbox integration** — registering a project refreshes the sandbox allowlist so workers can reach project paths outside the agent workspace + +### Wiki + +An instance-wide, permanently versioned knowledge base that sits alongside the memory graph. Memories are atomic facts that decay and get auto-injected into prompts; wiki pages are authored long-form documents that don't decay and are read on demand. + +- **Versioned forever** — every edit writes a new version, nothing is destructive +- **Wiki-link navigation** — pages reference each other and resolve as a graph +- **Agent-writable** — `wiki_create`, `wiki_edit`, `wiki_read`, `wiki_search`, `wiki_list`, `wiki_history` are available to workers and branches +- **Shared** — one wiki per instance, across all agents and humans + +### Multi-Agent and the Org Graph + +Run many agents on one binary. Each gets its own soul, workspace, databases, cortex, conversations, and messaging bindings — fully isolated. + +- **Communication graph** — explicit directed links define which agents may message which. Org-level humans are nodes too, so agents know who manages them +- **Agent factory** — agents create and configure other agents at runtime from presets, via `factory_*` tools +- **Per-agent permissions** — a `[permissions]` block per agent controls tool access and inter-agent data boundaries, checked at tool registration, at execution, and again on output. Denials return a structured error the LLM can reason about rather than a hidden tool or a silent failure + +### Dashboard, Portal, and Desktop + +- **Web dashboard** — React SPA embedded directly in the binary via `rust-embed`. Channels, workers, memories, tasks, cron, projects, wiki, cortex, skills, ingest, and settings +- **Notifications** — task approvals, worker failures, and cortex observations surface as actionable items pushed over SSE, each deep-linked to its source entity +- **Portal** — embeddable public chat with SSE streaming and per-agent session isolation +- **Desktop app** — Tauri 2 shell wrapping the same interface, with the `spacebot` binary bundled as a sidecar it can launch locally +- **Self-update** — checks GitHub releases and can update in place, including rewriting its own Docker image tag when `/var/run/docker.sock` is mounted + +### Observability + +- **Prometheus metrics** — LLM cost, token usage, agent activity, and memory operations, behind the `metrics` cargo feature so it compiles out entirely when unused +- **Token accounting** — per-call usage and cost estimates persisted to SQLite and surfaced in the dashboard +- **Worker transcripts** — every worker's turns and tool calls are recorded and replayable from the Workers tab +- **Loop guard** — programmatic detection of stuck tool loops (repeated identical calls, ping-pong patterns, global circuit breaker) with thresholds tuned per process type, so a spinning worker gets interrupted instead of burning budget + ### Messaging Native adapters for Discord, Slack, Telegram, Twitch, Signal, Mattermost, Email, and Webchat, plus a generic Webhook receiver: @@ -180,9 +224,9 @@ Native adapters for Discord, Slack, Telegram, Twitch, Signal, Mattermost, Email, ### Model Routing -Four-level routing picks the right model for every call. Channels get the best conversational model. Workers get something fast and cheap. Coding workers upgrade automatically. Simple user messages are downgraded to cheaper models by a sub-millisecond prompt scorer with no external calls. Voice messages route to a dedicated voice model. +Three-level routing picks the right model for every call, decided explicitly from the process type and task type — no keyword scoring, no classifier, no content analysis. Channels get the best conversational model. Workers, the compactor, and the cortex get something fast and cheap. Task-type overrides upgrade coding workers automatically. Per-model fallback chains retry on a different model when one fails, with rate-limit cooldown. -Any OpenAI-compatible or Anthropic-compatible endpoint works, including Ollama for local models, Z.ai GLM models, Azure OpenAI, and custom providers. Built-in support for Kilo Gateway, NVIDIA, MiniMax, Moonshot AI, Gemini, GitHub Copilot, OpenCode Go, and more. +Two provider types, not twenty: Anthropic's Messages API natively — required for OAuth, prompt caching, and extended thinking — and any OpenAI-compatible endpoint for everything else. That second one covers LiteLLM, vLLM, Ollama, TGI, OpenRouter, OpenAI, and anything else that speaks `/chat/completions`. A provider is a `base_url` plus a key, so adding one never needs a Spacebot release. LiteLLM is the recommended way to fan out to many upstreams, but it is not required. ### MCP Integration @@ -217,42 +261,19 @@ Spacebot builds on itself over time through four specific mechanisms. **Memory deepens with every interaction.** Each conversation adds facts, preferences, decisions, and observations to a typed graph with importance scoring and graph edges. The cortex synthesizes this into a briefing every future conversation benefits from. -**Goals drive autonomous work between conversations.** The autonomy channel wakes on its interval, picks up ready tasks, and works through them. Working memory records what happened, so the next conversation picks up where things left off. +**The board drives autonomous work between conversations.** The cortex's pickup loop claims ready tasks and runs them through workers without anyone asking. Working memory records what happened, so the next conversation picks up where things left off. Dormant-mode agents skip the loop entirely and run only when an external trigger wakes them. Everything goes through typed tools into structured storage. Nothing drifts. --- -## Spacebot + Spacedrive - -Spacebot pairs with [Spacedrive](https://github.com/spacedriveapp/spacedrive), an open-source cross-platform file manager built on a virtual distributed filesystem. Neither requires the other. When paired, Spacebot is the only agent harness with direct integration into a cross-device filesystem. - -### What Pairing Enables Today - -**Multi-device access:** one Spacebot instance, all your devices. Talk to your agent from your phone while a worker executes on your server. Spacedrive's P2P layer (Iroh/QUIC) routes from every device through the paired node to Spacebot. No separate SDK, no separate auth. - -**Remote execution:** workers can target any device in your library. A task that needs your home server's GPU, your work laptop's local repos, or your phone's camera routes through Spacedrive's permission system to the target device. From the agent's perspective, the tool call is identical. - -**File system intelligence:** every directory can carry context nodes describing what it contains and what policies apply. When the agent navigates your filesystem it gets that context, not a blind listing. - -**Safe data access:** Spacedrive indexes external sources (Gmail, Slack, Obsidian, GitHub, Apple Notes, contacts, calendar, browser history) as searchable data the agent can query. Every record passes through a local prompt injection classifier (Prompt Guard 2) before reaching the agent. The agent can search your emails without a malicious email hijacking it. - -### Where This Is Going - -A company deploys Spacebot + Spacedrive on their infrastructure. Employees install Spacedrive on their devices and join the company library. The company agent has access to employee devices through Spacedrive's permission system, with individual-level controls. The org graph in Spacebot defines hierarchy and delegation: which agents report to which, who can approve what, how tasks flow. - -An employee talks to the company agent from their MacBook. The agent knows their projects, their device, their role, and can spawn workers on any authorized machine. They switch to their personal Spacedrive library and connect to their home Spacebot, with personal data and personal context. The app is the same. The agent is different. - -No other agent harness is building this. It's a category. - ---- - ## Quick Start ### Prerequisites - **Rust** 1.85+ ([rustup](https://rustup.rs/)) -- An LLM API key from any supported provider (Anthropic, OpenAI, OpenRouter, Kilo Gateway, Z.ai, Groq, Together, Fireworks, DeepSeek, xAI, Mistral, NVIDIA, MiniMax, Moonshot AI, Gemini, GitHub Copilot, OpenCode Zen, OpenCode Go), or use `spacebot auth login` for Anthropic OAuth +- **protoc** (protobuf compiler) — required by LanceDB's build scripts. `apt install protobuf-compiler`, `brew install protobuf`, or use the included nix flake +- An Anthropic API key, or any OpenAI-compatible endpoint (LiteLLM recommended for reaching multiple vendors) — or use `spacebot auth login` for Anthropic OAuth ### Build and Run @@ -312,7 +333,7 @@ OAuth tokens are stored in `anthropic_oauth.json` and auto-refresh before each A | --------------- | --------------------------------------------------------------------------------------------------------------- | | Language | **Rust** (edition 2024) — single binary, no runtime dependencies, no GC pauses | | Async runtime | **Tokio** | -| LLM framework | **[Rig](https://github.com/0xPlaygrounds/rig)** v0.31 — agentic loop, tool execution, hooks | +| LLM framework | **[Rig](https://github.com/0xPlaygrounds/rig)** v0.33 — agentic loop, tool execution, hooks | | Relational data | **SQLite** (sqlx) — conversations, memory graph, tasks, goals, cron jobs | | Vector + FTS | **[LanceDB](https://lancedb.github.io/lancedb/)** — embeddings (HNSW), full-text (Tantivy), hybrid search (RRF) | | Key-value | **[redb](https://github.com/cberner/redb)** — settings, encrypted secrets | @@ -323,6 +344,9 @@ OAuth tokens are stored in `anthropic_oauth.json` and auto-refresh before each A | Telegram | **teloxide** — long-poll, media attachments, group/DM support | | Twitch | **twitch-irc** — chat integration with trigger prefix | | Browser | **Chromiumoxide** — headless Chrome via CDP | +| HTTP API | **axum** + **utoipa** — control API with a generated OpenAPI spec | +| Dashboard | **React** + **Vite**, embedded into the binary via **rust-embed** | +| Desktop | **Tauri 2** — native shell with the server bundled as a sidecar | | CLI | **Clap** — command line interface | Single binary, no server dependencies. All data lives in embedded databases in a local directory. @@ -335,16 +359,32 @@ Single binary, no server dependencies. All data lives in embedded databases in a | ------------------------------------------------------------------- | --------------------------------------------------------- | | [Quick Start]() | Setup, config, first run | | [Config Reference]() | Full `config.toml` reference | -| [Architecture](ARCHITECTURE.md) | Process types, tool access, memory internals, multi-agent | +| [Architecture]() | Process types, tool access, memory internals, multi-agent | | [Memory]() | Memory system design | | [Tools]() | All available LLM tools | | [Routing]() | Model routing and fallback chains | | [Secrets]() | Credential storage, encryption, output scrubbing | | [Sandbox]() | Process containment and environment sanitization | | [Cron Jobs]() | Scheduled recurring tasks | -| [MCP]() | External tool servers via Model Context Protocol | | [OpenCode]() | OpenCode as a worker backend | | [Messaging]() | Adapter architecture and platform setup | +| [Agents]() | Multi-agent setup, isolation, and the communication graph | +| [Tasks]() | The instance-level task board and autonomous pickup | +| [Workers]() | Worker lifecycle, segments, timeouts, and transcripts | +| [Projects]() | Repos, worktrees, and pointing workers at codebases | +| [Wiki]() | Versioned instance-wide knowledge base | +| [Permissions]() | Per-agent tool access and inter-agent boundaries | +| [Cortex]() | The system observer and working-memory assembly | +| [Compaction]() | How context stays under the window | +| [Prompts]() | Prompt templates and per-agent overrides | +| [Browser]() | Headless Chrome automation | +| [Ingestion]() | Bulk file import into the memory graph | +| [Portal]() | Embeddable public chat | +| [Notifications]() | Actionable events and SSE delivery | +| [Skills]() | Authoring, capture, and the skills.sh registry | +| [Desktop]() | Tauri desktop app and the sidecar binary | +| [Docker]() | Container deployment and updates | +| [Metrics]() | Prometheus metrics behind the `metrics` feature | --- diff --git a/docs/content/docs/(configuration)/config.mdx b/docs/content/docs/(configuration)/config.mdx index f0115567d..60910942a 100644 --- a/docs/content/docs/(configuration)/config.mdx +++ b/docs/content/docs/(configuration)/config.mdx @@ -32,40 +32,32 @@ These environment variables control instance-level behavior and are not set in ` ## Full Reference ```toml -# --- LLM Provider Credentials --- -# Instance-level, shared by all agents. At least one key or provider is required. -[llm] -anthropic_key = "env:ANTHROPIC_API_KEY" -openai_key = "env:OPENAI_API_KEY" -openrouter_key = "env:OPENROUTER_API_KEY" -kilo_key = "env:KILO_API_KEY" -zhipu_key = "env:ZHIPU_API_KEY" -groq_key = "env:GROQ_API_KEY" -together_key = "env:TOGETHER_API_KEY" -fireworks_key = "env:FIREWORKS_API_KEY" -deepseek_key = "env:DEEPSEEK_API_KEY" -xai_key = "env:XAI_API_KEY" -mistral_key = "env:MISTRAL_API_KEY" -opencode_zen_key = "env:OPENCODE_ZEN_API_KEY" -opencode_go_key = "env:OPENCODE_GO_API_KEY" - -# Custom LLM providers (alternative to legacy keys) -[llm.provider.my_anthropic] +# --- LLM Providers --- +# Instance-level, shared by all agents. At least one provider is required. +# +# There are exactly two api_types. Anthropic's Messages API is native because +# OAuth, prompt caching, and extended thinking have no OpenAI-format equivalent. +# Everything else is an OpenAI-compatible /chat/completions endpoint that +# differs only by base_url. +[llm.provider.anthropic] api_type = "anthropic" base_url = "https://api.anthropic.com" -api_key = "env:MY_ANTHROPIC_KEY" -name = "My Custom Anthropic" - -[llm.provider.my_openai] -api_type = "openai_responses" -base_url = "https://api.openai.com" -api_key = "env:MY_OPENAI_KEY" - -[llm.provider.local_openai] -api_type = "openai_completions" -base_url = "http://localhost:8080" # do not include /v1; Spacebot appends endpoint paths -api_key = "env:LOCAL_OPENAI_KEY" -name = "Local OpenAI Compatible" +api_key = "secret:ANTHROPIC_API_KEY" # or omit and use `spacebot auth login` + +[llm.provider.litellm] +api_type = "openai_compatible" +base_url = "http://localhost:4000/v1" # full path prefix, including /v1 +api_key = "secret:LITELLM_API_KEY" +name = "LiteLLM" # optional display name + +# Anything else — vLLM, Ollama, TGI, OpenRouter, a raw OpenAI key — is the same +# shape with a different base_url. `extra_headers` covers upstreams that need +# app-attribution or custom headers. +[llm.provider.openrouter] +api_type = "openai_compatible" +base_url = "https://openrouter.ai/api/v1" +api_key = "secret:OPENROUTER_API_KEY" +extra_headers = { "HTTP-Referer" = "https://spacebot.sh/", "X-Title" = "Spacebot" } # --- Instance Defaults --- # All agents inherit these. Individual agents can override any field. @@ -105,7 +97,7 @@ emergency_threshold = 0.95 # drop oldest 50%, no LLM [defaults.cortex] tick_interval_secs = 30 worker_timeout_secs = 600 -branch_timeout_secs = 60 +branch_timeout_secs = 600 circuit_breaker_threshold = 3 # consecutive failures before auto-disable # Warmup controls for cold-start behavior and manual rewarm. @@ -194,21 +186,24 @@ Any string value in the config supports three resolution modes: | _(none)_ | Literal value | `"sk-ant-..."` | ```toml +[llm.provider.anthropic] +api_type = "anthropic" +base_url = "https://api.anthropic.com" # From the secret store (recommended) -anthropic_key = "secret:ANTHROPIC_API_KEY" +api_key = "secret:ANTHROPIC_API_KEY" +[llm.provider.litellm] +api_type = "openai_compatible" +base_url = "http://localhost:4000/v1" # From an environment variable -openai_key = "env:OPENAI_API_KEY" - -# Literal value (not recommended — use secret: or env: instead) -groq_key = "gsk_abc123..." +api_key = "env:LITELLM_API_KEY" ``` -The `secret:` prefix resolves from the agent's secret store at config load time. If the secret doesn't exist, the value is treated as missing and implicit env fallbacks are tried. +The `secret:` prefix resolves from the instance's secret store at config load time. There is no fallback: if a provider's `api_key` reference cannot be resolved (the secret doesn't exist, or the store is locked), config load fails with an error naming the provider rather than booting with a key that will 401 on first use. -LLM keys also have implicit env fallbacks — if no key is set in the TOML, Spacebot checks `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `KILO_API_KEY`, and `OPENCODE_GO_API_KEY` automatically. +Two environment variable pairs bootstrap a provider when the config file does not define one under that name: `ANTHROPIC_API_KEY` (or `ANTHROPIC_AUTH_TOKEN`) with `ANTHROPIC_BASE_URL`, and `LITELLM_API_KEY` with `LITELLM_BASE_URL`. No other `*_API_KEY` variable configures a provider — everything else needs an explicit `[llm.provider.]` block. -Use `POST /api/secrets/migrate` to automatically move plaintext credentials from `config.toml` into the secret store and replace them with `secret:` references. See [Secret Store -- Migration](/docs/secrets#migration) for details. +Use `POST /api/secrets/migrate` to automatically move plaintext credentials from `config.toml` into the secret store and replace them with `secret:` references. Provider keys (`[llm.provider.*].api_key`) are not scanned — they already accept `secret:` / `env:` references directly, so write the reference yourself. See [Secret Store -- Migration](/docs/secrets#migration) for details. ## Env-Only Mode @@ -238,13 +233,18 @@ An agent with no overrides inherits everything from `[defaults]`. An agent with Model names include the provider as a prefix: -| Provider | Format | Example | -|----------|--------|---------| -| Anthropic | `anthropic/` | `anthropic/claude-sonnet-4-20250514` | -| OpenAI | `openai/` | `openai/gpt-4o` | -| OpenRouter | `openrouter//` | `openrouter/anthropic/claude-sonnet-4-20250514` | -| Kilo Gateway | `kilo//` | `kilo/anthropic/claude-sonnet-4.5` | -| Custom provider | `/` | `my_openai/gpt-4o-mini` | +The prefix is the provider id you chose in `[llm.provider.]` — there is no +fixed vendor list. + +| Provider block | Format | Example | +|----------------|--------|---------| +| `[llm.provider.anthropic]` | `anthropic/` | `anthropic/claude-sonnet-4-20250514` | +| `[llm.provider.litellm]` | `litellm/` | `litellm/claude-sonnet-4` | +| `[llm.provider.openrouter]` | `openrouter/` | `openrouter/anthropic/claude-sonnet-4-20250514` | +| Any provider you define | `/` | `my-vllm/Qwen2.5-72B-Instruct` | + +Everything after the first `/` is passed to the upstream verbatim, so gateway +model names containing slashes work unchanged. You can mix providers across process types. See [Routing](/docs/routing) for the full routing system. @@ -333,128 +333,129 @@ System prompts (channel, branch, worker, compactor, cortex, etc.) are Jinja2 tem ## Sections Reference -### Migration from Legacy Keys - -Legacy keys (`anthropic_key`, `openai_key`, etc.) are still supported and automatically converted to provider entries internally. For example: - -**Legacy format:** -```toml -[llm] -anthropic_key = "env:ANTHROPIC_API_KEY" -openai_key = "env:OPENAI_API_KEY" -``` - -**Internal representation (auto-created):** -```toml -[llm.provider.anthropic] -api_type = "anthropic" -base_url = "https://api.anthropic.com" -api_key = "env:ANTHROPIC_API_KEY" - -[llm.provider.openai] -api_type = "openai_completions" -base_url = "https://api.openai.com" -api_key = "env:OPENAI_API_KEY" -``` - -If you define a custom provider with the same ID as a legacy key, your custom configuration takes precedence. - -#### Legacy Keys - -| Key | Type | Default | Description | -|-----|------|---------|-------------| -| `anthropic_key` | string | None | Anthropic API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `openai_key` | string | None | OpenAI API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `openrouter_key` | string | None | OpenRouter API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `kilo_key` | string | None | Kilo Gateway API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `zhipu_key` | string | None | Zhipu AI (GLM) API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `groq_key` | string | None | Groq API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `together_key` | string | None | Together AI API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `fireworks_key` | string | None | Fireworks AI API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `deepseek_key` | string | None | DeepSeek API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `xai_key` | string | None | XAI API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `mistral_key` | string | None | Mistral API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `opencode_zen_key` | string | None | OpenCode Zen API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `opencode_go_key` | string | None | OpenCode Go API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `gemini_key` | string | None | Gemini API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `nvidia_key` | string | None | NVIDIA API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `minimax_key` | string | None | MiniMax API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `moonshot_key` | string | None | Moonshot API key (`secret:NAME`, `env:VAR_NAME`, or literal) | -| `github_copilot_key` | string | None | GitHub Copilot PAT (`secret:NAME`, `env:VAR_NAME`, or literal) | - -#### Custom Providers - -Custom providers allow configuring LLM providers with custom endpoints and API types. Use either legacy keys **or** custom providers. +### Migrating from the old provider keys + +Spacebot used to ship 20 hardcoded providers and 7 `api_type` values. Both +collapsed: there are now two `api_type`s and no built-in provider list. + +**`llm._key` shorthands were removed.** They each implied a hidden +base URL and API dialect. A config still using one fails to load with a message +naming the replacement block — deliberately, because silently ignoring them +would leave you booted with zero providers and every LLM call failing at +runtime. + +| Removed | Replacement | +|---------|-------------| +| `anthropic_key` | `api_type = "anthropic"`, `base_url = "https://api.anthropic.com"` | +| `openai_key` | `api_type = "openai_compatible"`, `base_url = "https://api.openai.com/v1"` | +| `openrouter_key` | `api_type = "openai_compatible"`, `base_url = "https://openrouter.ai/api/v1"` | +| `kilo_key` | `api_type = "openai_compatible"`, `base_url = "https://api.kilo.ai/api/gateway/v1"` | +| `zhipu_key` | `api_type = "openai_compatible"`, `base_url = "https://api.z.ai/api/paas/v4"` | +| `zai_coding_plan_key` | `api_type = "openai_compatible"`, `base_url = "https://api.z.ai/api/coding/paas/v4"` | +| `groq_key` | `api_type = "openai_compatible"`, `base_url = "https://api.groq.com/openai/v1"` | +| `together_key` | `api_type = "openai_compatible"`, `base_url = "https://api.together.xyz/v1"` | +| `fireworks_key` | `api_type = "openai_compatible"`, `base_url = "https://api.fireworks.ai/inference/v1"` | +| `deepseek_key` | `api_type = "openai_compatible"`, `base_url = "https://api.deepseek.com/v1"` | +| `xai_key` | `api_type = "openai_compatible"`, `base_url = "https://api.x.ai/v1"` | +| `mistral_key` | `api_type = "openai_compatible"`, `base_url = "https://api.mistral.ai/v1"` | +| `gemini_key` | `api_type = "openai_compatible"`, `base_url = "https://generativelanguage.googleapis.com/v1beta/openai"` | +| `nvidia_key` | `api_type = "openai_compatible"`, `base_url = "https://integrate.api.nvidia.com/v1"` | +| `opencode_zen_key` | `api_type = "openai_compatible"`, `base_url = "https://opencode.ai/zen/v1"` | +| `opencode_go_key` | `api_type = "openai_compatible"`, `base_url = "https://opencode.ai/zen/go/v1"` | +| `minimax_key` | `api_type = "anthropic"`, `base_url = "https://api.minimax.io/anthropic"` | +| `minimax_cn_key` | `api_type = "anthropic"`, `base_url = "https://api.minimaxi.com/anthropic"` | +| `moonshot_key` | `api_type = "openai_compatible"`, `base_url = "https://api.moonshot.ai/v1"` | +| `ollama_base_url` / `ollama_key` | `api_type = "openai_compatible"`, `base_url = "/v1"` | +| `github_copilot_key` | No replacement. GitHub Copilot token exchange was removed. | + +**Retired `api_type` values still parse, for one release.** Keeping them working +matters because three of them appended `/v1` to `base_url` internally and +`openai_compatible` does not — collapsing them without rewriting the URL would +have 404'd every existing config. Spacebot applies the fixup at load time and +logs a warning telling you what to write instead. + +| Old `api_type` | Now | `base_url` fixup | +|----------------|-----|------------------| +| `openai_chat_completions` | `openai_compatible` | none | +| `openai_completions` | `openai_compatible` | `/v1` appended if absent | +| `gemini` | `openai_compatible` | `/v1` appended if absent | +| `kilo_gateway` | `openai_compatible` | `/v1` appended if absent; add gateway headers via `extra_headers` | +| `openai_responses` | **removed** | Use an endpoint that speaks `/chat/completions`, or put LiteLLM in front | +| `azure` | **removed** | `openai_compatible` against your deployment URL, or LiteLLM in front of Azure | + +**Provider env vars other than the two documented pairs are ignored.** A +deployment setting only `OPENROUTER_API_KEY` will boot with no providers. +Spacebot logs a warning naming every retired variable it finds set. + +**Removed auth flows.** GitHub Copilot token exchange and ChatGPT Plus device +OAuth are gone; neither can be proxied. `spacebot auth login` (Anthropic +Pro/Max OAuth) is unaffected. + +### `[llm.provider.]` ```toml [llm.provider.] -api_type = "anthropic" # Required - see supported values below -base_url = "https://api..." # Required - valid URL -api_key = "env:API_KEY" # Required - API key (supports env:VAR_NAME format) -name = "My Provider" # Optional - friendly name for display +api_type = "openai_compatible" # Required - "anthropic" or "openai_compatible" +base_url = "https://api..." # Required - full path prefix, valid URL +api_key = "secret:API_KEY" # Required - supports secret: and env: prefixes +name = "My Provider" # Optional - display name in logs and UI +use_bearer_auth = false # Optional - Anthropic path only, see below +extra_headers = { } # Optional - sent with every request ``` | Field | Type | Required | Description | |-------|------|----------|-------------| -| `api_type` | string | Yes | API protocol type. One of: `anthropic`, `openai_completions`, `openai_chat_completions`, `openai_responses`, `gemini`, `kilo_gateway`, or `azure` | -| `base_url` | string | Yes | Base URL of the API endpoint. Must be a valid URL (including protocol). For Azure, must end with `.openai.azure.com` | -| `api_key` | string | Yes | API key for authentication. Supports `secret:NAME` and `env:VAR_NAME` syntax | -| `name` | string | No | Optional friendly name for the provider (displayed in logs and UI) | -| `api_version` | string | Azure only | Azure API version (format: `YYYY-MM-DD` or `YYYY-MM-DD-preview`) | -| `deployment` | string | Azure only | Azure deployment name (alphanumeric, hyphens, and dots allowed) | - -> Note: -> - For `openai_completions`, `openai_chat_completions`, and `openai_responses`, configure `base_url` as the provider root URL (usually without a trailing `/v1`). -> - Spacebot appends the endpoint path automatically: -> - `openai_completions` -> `/v1/chat/completions` -> - `openai_chat_completions` -> `/chat/completions` -> - `openai_responses` -> `/v1/responses` -> - `kilo_gateway` -> `/chat/completions` plus Kilo-required `HTTP-Referer` / `X-Title` headers -> - If you include `/v1` in `base_url`, requests can end up with duplicated paths such as `/v1/v1/...`. - -**Provider ID Requirements:** +| `api_type` | string | Yes | `anthropic` or `openai_compatible` | +| `base_url` | string | Yes | Full path prefix. Only the endpoint (`/v1/messages` or `/chat/completions`) is appended | +| `api_key` | string | Yes | Supports `secret:NAME`, `env:VAR_NAME`, or a literal | +| `name` | string | No | Friendly name shown in logs and the settings UI | +| `use_bearer_auth` | bool | No | Send `Authorization: Bearer` instead of `x-api-key` on the Anthropic path. For Anthropic-compatible proxies | +| `extra_headers` | table | No | Extra HTTP headers, e.g. `{ "HTTP-Referer" = "https://example.com" }` | + +> **`base_url` is the complete path prefix.** Nothing is inferred. An +> OpenAI-compatible server that serves `/v1/chat/completions` needs +> `base_url = "http://host:8080/v1"`. One that serves `/chat/completions` at the +> root needs `base_url = "http://host:8080"`. This is why the retired +> `openai_completions` type gets a `/v1` appended on migration. + +**Provider ID requirements:** - 1-64 characters long - Cannot contain `/` or whitespace - Case-insensitive (stored as lowercase) +- Becomes the model routing prefix, so pick something short #### Examples -**Anthropic-compatible provider:** +**Anthropic, native:** ```toml -[llm.provider.custom_anthropic] +[llm.provider.anthropic] api_type = "anthropic" base_url = "https://api.anthropic.com" -api_key = "env:CUSTOM_ANTHROPIC_KEY" -name = "Anthropic EU" +api_key = "secret:ANTHROPIC_API_KEY" ``` -**Azure OpenAI provider:** +**LiteLLM (recommended for everything else):** ```toml -[llm.provider.azure] -api_type = "azure" -base_url = "https://my-resource.openai.azure.com" -api_key = "env:AZURE_API_KEY" -api_version = "2024-02-15" # Required for Azure -deployment = "gpt-4o" # Required for Azure (deployment name) -name = "Azure OpenAI" +[llm.provider.litellm] +api_type = "openai_compatible" +base_url = "http://localhost:4000/v1" +api_key = "secret:LITELLM_API_KEY" ``` -> **Azure Requirements:** -> - `base_url` must end with `.openai.azure.com` -> - `api_version` must match format: `YYYY-MM-DD` or `YYYY-MM-DD-preview` -> - `deployment` can contain alphanumeric characters, hyphens, and dots (e.g., `gpt-4o`, `gpt-5.2`) -> - Model names in routing should use the format: `azure/` +See [LiteLLM](/docs/litellm) for standing one up. LiteLLM is a recommendation, +not a dependency — Spacebot remains a single binary and talks to any +OpenAI-compatible endpoint directly. -**OpenAI Completions provider:** +**A local vLLM or Ollama server:** ```toml -[llm.provider.local_llm] -api_type = "openai_completions" -base_url = "http://localhost:8080" # no /v1 in base_url -api_key = "env:LOCAL_LLM_KEY" -name = "Local LLaMA Server" +[llm.provider.local] +api_type = "openai_compatible" +base_url = "http://localhost:11434/v1" +api_key = "" # many local servers need no key +name = "Local models" ``` -At least one provider (legacy key or custom provider) must be configured. +At least one provider must be configured. ### `[defaults]` @@ -472,29 +473,31 @@ At least one provider (legacy key or custom provider) must be configured. | Key | Type | Default | Description | |-----|------|---------|-------------| -| `channel` | string | `anthropic/claude-sonnet-4-20250514` | Model for user-facing channels | -| `branch` | string | `anthropic/claude-sonnet-4-20250514` | Model for thinking branches | -| `worker` | string | `anthropic/claude-haiku-4.5-20250514` | Model for task workers | -| `compactor` | string | `anthropic/claude-haiku-4.5-20250514` | Model for summarization | -| `cortex` | string | `anthropic/claude-haiku-4.5-20250514` | Model for system observation | +| `channel` | string | `anthropic/claude-sonnet-4` | Model for user-facing channels | +| `branch` | string | `anthropic/claude-sonnet-4` | Model for thinking branches | +| `worker` | string | `anthropic/claude-sonnet-4` | Model for task workers | +| `compactor` | string | `anthropic/claude-sonnet-4` | Model for summarization | +| `cortex` | string | `anthropic/claude-sonnet-4` | Model for system observation | | `rate_limit_cooldown_secs` | integer | 60 | How long to deprioritize a rate-limited model | +The defaults above apply only when an `anthropic` provider is configured — it is the one provider whose model catalog Spacebot can name with confidence. When `[defaults.routing]` is absent and no `anthropic` provider exists, routing is empty and the instance boots into setup mode: configure `[defaults.routing]` explicitly, pointing each role at a model your provider actually serves (e.g. `litellm/`). Spacebot never guesses a model name for a non-Anthropic provider — a guessed string would look right and fail on first use. + Routing selects providers by the prefix before the first `/` in the model name. ```toml [defaults.routing] -channel = "my_openai/gpt-4o-mini" -worker = "custom_anthropic/claude-3-5-sonnet" +channel = "litellm/gpt-4o-mini" +worker = "anthropic/claude-haiku-4.5" -[llm.provider.my_openai] -api_type = "openai_completions" -base_url = "https://api.openai.com" -api_key = "env:OPENAI_API_KEY" +[llm.provider.litellm] +api_type = "openai_compatible" +base_url = "http://localhost:4000/v1" +api_key = "secret:LITELLM_API_KEY" -[llm.provider.custom_anthropic] +[llm.provider.anthropic] api_type = "anthropic" base_url = "https://api.anthropic.com" -api_key = "env:ANTHROPIC_API_KEY" +api_key = "secret:ANTHROPIC_API_KEY" ``` If no prefix is provided (for example `claude-sonnet-4-20250514`), Spacebot defaults to the `anthropic` provider. @@ -534,10 +537,11 @@ Thresholds are fractions of `context_window`. |-----|------|---------|-------------| | `tick_interval_secs` | integer | 30 | How often the cortex runtime loop runs maintenance ticks while continuously observing events | | `worker_timeout_secs` | integer | 600 | Worker idle timeout before cancellation | -| `branch_timeout_secs` | integer | 60 | Branch timeout before cancellation | +| `branch_timeout_secs` | integer | 600 | Branch timeout before cancellation | | `detached_worker_timeout_retry_limit` | integer | 2 | Retry limit before quarantining detached workers to backlog | | `supervisor_kill_budget_per_tick` | integer | 8 | Max number of overdue processes supervisor may cancel per health tick | | `circuit_breaker_threshold` | integer | 3 | Consecutive failures before auto-disable | +| `worker_task_create` | bool | true | Give task workers the `task_create` tool so they can decompose work by filing new cards. Set to `false` to stop workers generating their own backlog | ### `[defaults.warmup]` diff --git a/docs/content/docs/(configuration)/litellm.mdx b/docs/content/docs/(configuration)/litellm.mdx new file mode 100644 index 000000000..35ed8d308 --- /dev/null +++ b/docs/content/docs/(configuration)/litellm.mdx @@ -0,0 +1,167 @@ +--- +title: LiteLLM +description: Point Spacebot at any model provider through a single OpenAI-compatible gateway. +--- + +# LiteLLM + +Spacebot speaks two APIs: Anthropic's Messages API natively, and +OpenAI-compatible `/chat/completions` for everything else. That second one is +where LiteLLM comes in — it is a proxy that puts an OpenAI-compatible face on +roughly a hundred upstreams (Bedrock, Vertex, Azure, Together, Groq, your own +vLLM box) and lets you add new ones without waiting on a Spacebot release. + +**LiteLLM is a recommendation, not a dependency.** Spacebot is a single binary +with no server requirements, and it talks to any OpenAI-compatible endpoint +directly — vLLM, Ollama, TGI, OpenRouter, or OpenAI itself. Use LiteLLM when you +want one place to manage keys, budgets, and fallbacks across several vendors. + +## Why not just add providers to Spacebot? + +Spacebot used to ship 20 hardcoded providers, each with a baked-in base URL and +one of 7 API dialects. Every new vendor meant a code change, a release, and a +URL that would eventually go stale. Vendor differences turn out to be almost +entirely `base_url` plus a couple of headers — which is exactly what a proxy is +for. See [Configuration → Migrating from the old provider +keys](/docs/config#migrating-from-the-old-provider-keys) if you are coming from +a config that used them. + +## Standing one up + +```bash +pip install 'litellm[proxy]' +``` + +Write a `config.yaml`. The `model_name` values are the aliases Spacebot will +use; everything under `litellm_params` is LiteLLM's business. + +```yaml +model_list: + - model_name: claude-sonnet-4 + litellm_params: + model: anthropic/claude-sonnet-4-20250514 + api_key: os.environ/ANTHROPIC_API_KEY + + - model_name: claude-haiku-4.5 + litellm_params: + model: anthropic/claude-haiku-4-5-20251001 + api_key: os.environ/ANTHROPIC_API_KEY + + - model_name: gpt-4.1 + litellm_params: + model: openai/gpt-4.1 + api_key: os.environ/OPENAI_API_KEY + + # A local vLLM server, exposed under the same gateway. + - model_name: qwen-72b + litellm_params: + model: openai/Qwen2.5-72B-Instruct + api_base: http://localhost:8000/v1 + api_key: none + +general_settings: + master_key: os.environ/LITELLM_MASTER_KEY +``` + +```bash +litellm --config config.yaml --port 4000 +``` + +## Pointing Spacebot at it + +```toml +[llm.provider.litellm] +api_type = "openai_compatible" +base_url = "http://localhost:4000/v1" +api_key = "secret:LITELLM_API_KEY" +name = "LiteLLM" + +[defaults.routing] +channel = "litellm/claude-sonnet-4" +branch = "litellm/claude-sonnet-4" +worker = "litellm/claude-haiku-4.5" +compactor = "litellm/claude-haiku-4.5" +cortex = "litellm/claude-haiku-4.5" +``` + +The provider id (`litellm` here) becomes the routing prefix, and everything +after the first `/` is passed to LiteLLM verbatim — so it must match a +`model_name` from your `config.yaml`. + +Store the key rather than inlining it: + +```bash +spacebot secrets set LITELLM_API_KEY +``` + +Or skip `config.toml` entirely and use environment variables: + +```bash +export LITELLM_API_KEY="sk-..." +export LITELLM_BASE_URL="http://localhost:4000/v1" # defaults to this +``` + +> `base_url` is the complete path prefix. Spacebot appends only +> `/chat/completions`, so the `/v1` has to be there. + +## Keep Anthropic native + +If you have Anthropic access, configure it as its own provider alongside +LiteLLM rather than routing it through the proxy: + +```toml +[llm.provider.anthropic] +api_type = "anthropic" +base_url = "https://api.anthropic.com" +api_key = "secret:ANTHROPIC_API_KEY" +``` + +Three things only work on the native path: + +- **`spacebot auth login`** — Claude Pro/Max subscription auth. LiteLLM requires + an API key, so proxying it forces you onto metered billing. +- **Prompt caching.** Spacebot places `cache_control` breakpoints on the system + prompt and history. A harness that re-sends a large system prompt every turn + pays real money for losing them, and LiteLLM's OpenAI-format translation is + lossy here. +- **Extended thinking.** Adaptive thinking budgets are set per request on the + Messages API and do not survive the round trip through + `/chat/completions`. + +Mixing is fine and expected — route the channel through Anthropic for caching, +and workers through LiteLLM for cost: + +```toml +[defaults.routing] +channel = "anthropic/claude-sonnet-4" +branch = "anthropic/claude-sonnet-4" +worker = "litellm/qwen-72b" +compactor = "litellm/claude-haiku-4.5" +cortex = "litellm/claude-haiku-4.5" +``` + +## Fallbacks + +Spacebot has its own fallback chains, which work across providers: + +```toml +[defaults.routing.fallbacks] +"anthropic/claude-sonnet-4" = ["litellm/claude-sonnet-4", "litellm/gpt-4.1"] +``` + +LiteLLM has router-level fallbacks too. Prefer Spacebot's when you want to fall +back *across* gateways (including off a dead LiteLLM), and LiteLLM's when you +want to fall back between upstreams that Spacebot sees as one model name. + +## Verifying + +```bash +curl http://localhost:4000/v1/chat/completions \ + -H "Authorization: Bearer $LITELLM_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"model": "claude-sonnet-4", "messages": [{"role": "user", "content": "ok?"}]}' +``` + +If that returns a completion, the same URL and key will work in +`[llm.provider.litellm]`. The settings UI also has a **Test model** button that +runs a real completion before saving. diff --git a/docs/content/docs/(configuration)/meta.json b/docs/content/docs/(configuration)/meta.json index ef915e0d0..31e33ab7a 100644 --- a/docs/content/docs/(configuration)/meta.json +++ b/docs/content/docs/(configuration)/meta.json @@ -1,4 +1,10 @@ { "title": "Configuration", - "pages": ["config", "secrets", "sandbox", "permissions"] + "pages": [ + "config", + "litellm", + "secrets", + "sandbox", + "permissions" + ] } diff --git a/docs/content/docs/(configuration)/secrets.mdx b/docs/content/docs/(configuration)/secrets.mdx index 0ae0c7d79..26d6f7828 100644 --- a/docs/content/docs/(configuration)/secrets.mdx +++ b/docs/content/docs/(configuration)/secrets.mdx @@ -50,9 +50,10 @@ anything else → literal value The `secret:` prefix is the recommended way to reference credentials in config: ```toml -[llm] -anthropic_key = "secret:ANTHROPIC_API_KEY" -openai_key = "secret:OPENAI_API_KEY" +[llm.provider.anthropic] +api_type = "anthropic" +base_url = "https://api.anthropic.com" +api_key = "secret:ANTHROPIC_API_KEY" [messaging.discord] token = "secret:DISCORD_BOT_TOKEN" @@ -62,15 +63,20 @@ This keeps `config.toml` free of plaintext credentials. The secret store resolve ### Resolution Order -For LLM keys specifically, the resolution chain is: +For a provider's `api_key`, the resolution chain is: ``` -config.toml value (secret: / env: / literal) - → implicit env fallback (ANTHROPIC_API_KEY, etc.) - → missing +[llm.provider.].api_key (secret: / env: / literal) + → missing (config load fails for that provider) ``` -If `anthropic_key` is set to `"secret:ANTHROPIC_API_KEY"` and the secret store has that key, it resolves to the stored value. If the store doesn't have it, the key is treated as missing and the implicit env fallback is tried. +If `api_key` is `"secret:ANTHROPIC_API_KEY"` and the secret store has that key, +it resolves to the stored value. + +Separately, `ANTHROPIC_API_KEY` (or `ANTHROPIC_AUTH_TOKEN`) and +`LITELLM_API_KEY` bootstrap an `anthropic` / `litellm` provider when the config +file does not define one under that id. No other `*_API_KEY` variable +configures a provider. ## Integration Setup @@ -248,11 +254,12 @@ Scans `config.toml` for literal (plaintext) key values in known credential field Scanned fields are driven by `SystemSecrets` trait implementations -- the same declarations used for auto-categorization. This covers: -- All `[llm]` provider keys - `[defaults]` search keys - Default messaging adapter tokens (e.g. `[messaging.discord].token`) - Named adapter instance tokens in `[[messaging.*.instances]]` arrays (e.g. `DISCORD_ALERTS_BOT_TOKEN` for an instance named `"alerts"`) +Provider keys (`[llm.provider.*].api_key`) are **not** scanned. They already accept `secret:` / `env:` references directly, so there is no flat `llm.*_key` field left to migrate -- write the reference yourself. + Values already using `env:` or `secret:` prefixes are skipped. This is a one-shot operation -- run it once after setting up the secret store to migrate existing plaintext credentials. ### Export / Import @@ -303,9 +310,15 @@ The secret store requires no configuration in `config.toml`. It initializes auto To use stored secrets in config, replace literal values or `env:` references with `secret:` references: ```toml -[llm] -anthropic_key = "secret:ANTHROPIC_API_KEY" -openai_key = "secret:OPENAI_API_KEY" +[llm.provider.anthropic] +api_type = "anthropic" +base_url = "https://api.anthropic.com" +api_key = "secret:ANTHROPIC_API_KEY" + +[llm.provider.litellm] +api_type = "openai_compatible" +base_url = "http://localhost:4000/v1" +api_key = "secret:LITELLM_API_KEY" [messaging.discord] token = "secret:DISCORD_BOT_TOKEN" diff --git a/docs/content/docs/(core)/cortex.mdx b/docs/content/docs/(core)/cortex.mdx index f2a77cf30..a707a912b 100644 --- a/docs/content/docs/(core)/cortex.mdx +++ b/docs/content/docs/(core)/cortex.mdx @@ -179,7 +179,7 @@ bulletin_max_words = 500 worker_timeout_secs = 600 # Branch is considered stale after this duration. -branch_timeout_secs = 60 +branch_timeout_secs = 600 # Consecutive failures before circuit breaker trips. circuit_breaker_threshold = 3 diff --git a/docs/content/docs/(core)/routing.mdx b/docs/content/docs/(core)/routing.mdx index bcced75f7..3c56b12e4 100644 --- a/docs/content/docs/(core)/routing.mdx +++ b/docs/content/docs/(core)/routing.mdx @@ -27,9 +27,9 @@ worker = "anthropic/claude-haiku-4.5-20250514" compactor = "anthropic/claude-haiku-4.5-20250514" cortex = "anthropic/claude-haiku-4.5-20250514" -# Azure example: -# channel = "azure/gpt-4o" -# worker = "azure/gpt-4o-mini" +# Via a LiteLLM gateway: +# channel = "litellm/gpt-4o" +# worker = "litellm/gpt-4o-mini" ``` | Process | Why this model tier | @@ -41,9 +41,9 @@ cortex = "anthropic/claude-haiku-4.5-20250514" | Cortex | System-level observation. Small context, simple signal processing. Cheapest tier. | **Model Format:** -- Standard providers: `/` (e.g., `anthropic/claude-sonnet-4-20250514`, `openai/gpt-4o`) -- Azure: `azure/` (e.g., `azure/gpt-4o`, `azure/gpt-5.2`) -- Custom providers: `/` (e.g., `my_anthropic/claude-3.5-sonnet`) +- Any provider: `/` (e.g., `anthropic/claude-sonnet-4-20250514`, `litellm/gpt-4o`, `my_anthropic/claude-3.5-sonnet`) + +The prefix is the provider id from your `[llm.provider.]` block — there is no fixed vendor list. For Azure OpenAI deployments, point an `openai_compatible` provider (or LiteLLM) at the deployment URL and route by that provider's id; the retired `azure` api_type is rejected at config load. ### Level 2: Task-Type Overrides diff --git a/docs/content/docs/(deployment)/roadmap.mdx b/docs/content/docs/(deployment)/roadmap.mdx index ee25d1a36..203879ef5 100644 --- a/docs/content/docs/(deployment)/roadmap.mdx +++ b/docs/content/docs/(deployment)/roadmap.mdx @@ -9,7 +9,7 @@ description: What's shipped, what's next, and what we decided against. Spacebot is a working multi-agent system. Five process types (channel, branch, worker, compactor, cortex) run concurrently with delegation as the core pattern. Six messaging platforms are supported (Discord, Slack, Telegram, Twitch, Email, webhooks). The hosted platform is live at spacebot.sh. -The core systems are stable: memory graph with hybrid search, model routing with fallback chains across 14 providers (including Azure OpenAI), OS-level sandboxing, secret store with encryption at rest, per-channel settings with hot-reload, conversation persistence, token-by-token streaming, and a full React dashboard embedded in the binary. +The core systems are stable: memory graph with hybrid search, model routing with fallback chains over user-defined providers (a native Anthropic API type plus an OpenAI-compatible type that covers everything else, with LiteLLM as the recommended gateway), OS-level sandboxing, secret store with encryption at rest, per-channel settings with hot-reload, conversation persistence, token-by-token streaming, and a full React dashboard embedded in the binary. Recent work has focused on expanding what agents can do and how they share knowledge: @@ -68,7 +68,6 @@ Token usage tracking is shipped. Remaining: warning thresholds, hard blocks when - **Hot reload agent topology** — adding/removing agents without restart - **Agent templates** — pre-built configurations for common use cases - **JsonSchema-derived tool definitions** — replace hand-written JSON schemas with the `JsonSchema` derive already on every Args struct -- **Spacedrive integration** — connect agents to terabytes of indexed, content-addressed file data across devices ## Decided Against diff --git a/docs/content/docs/(features)/tasks.mdx b/docs/content/docs/(features)/tasks.mdx index 5844b7915..980c96136 100644 --- a/docs/content/docs/(features)/tasks.mdx +++ b/docs/content/docs/(features)/tasks.mdx @@ -1,21 +1,21 @@ --- title: Tasks -description: Task management with structured tracking, cortex pickup, and worker execution. +description: The instance task board — dependencies, contracts, gates, fan-out, loops, and worker execution. --- # Tasks -A task management system built into every agent. Tasks are spec-driven documents — the description is a full markdown spec that evolves through conversation, with pre-filled subtasks as an execution plan. Each task has a short title, rich description, status, priority, subtasks, and a numeric reference (`#42`). Each agent has its own independent task store backed by its own SQLite database. +Spacebot is built around an instance-level task board. Every agent on the instance shares one store (a global `tasks.db`), every task carries a globally unique number, and the board is the substrate through which agents delegate work to each other. Each task has a short title, a rich markdown description, status, priority, subtasks, an **owner** agent (who filed it) and an **assignee** agent (who executes it), and a numeric reference (`#42`). Tasks are not tickets. They're living specs written for workers who have no conversation context. A good task description includes requirements, constraints, file paths, examples, and acceptance criteria. The branch refines the spec as the user clarifies scope, and moves it to `ready` when it's complete. The cortex picks it up and spawns a worker that executes against the spec. -## Creation Paths +A task can also declare what it waits on (dependencies, gates), what it consumes (input bindings wired to upstream outputs), and what it must produce (an output schema validated at completion). Those declarations are what turn a pile of cards into a graph. -Tasks enter the system three ways: +## Creation Paths ### 1. Conversational (via branch tools) -The primary path. A user manages tasks through natural conversation — creating, listing, updating, approving, and closing tasks by talking to the agent. The channel delegates to a branch, and the branch uses `task_create`, `task_list`, and `task_update` tools. +The primary path. A user manages tasks through natural conversation — the channel delegates to a branch, and the branch uses `task_create`, `task_list`, and `task_update`. ``` User: "Create a task to refactor the auth module, high priority" @@ -23,54 +23,55 @@ User: "Create a task to refactor the auth module, high priority" → Branch calls task_create( title: "Refactor auth module", priority: "high", - description: "## Goal\nExtract auth logic from ...\n\n## Requirements\n- ...\n## Constraints\n- ...", - subtasks: ["Audit current auth endpoints", "Extract shared middleware", "Update tests", "Verify CI passes"] + description: "## Goal\nExtract auth logic from ...", + subtasks: ["Audit current auth endpoints", "Extract shared middleware", ...] ) → Branch returns: "Created task #7 with 4 subtasks" - -User: "Actually, we also need to migrate the session store to Redis" - → Channel branches - → Branch calls task_update(task_number: 7, description: "") - → Branch returns: "Updated #7 — added Redis migration to the spec" - -User: "Looks good, run it" - → Channel branches - → Branch calls task_update(task_number: 7, status: "ready") - → Cortex ready-task loop picks it up → Worker executes against the spec → Done ``` -Tasks created conversationally default to `backlog` status. The branch writes a rich markdown description and pre-fills subtasks as an execution plan. The user refines scope through conversation, and the branch updates the spec accordingly. When the spec is complete, moving to `ready` triggers automatic execution. +Branch-created tasks land in `pending_approval` and raise a dashboard notification — nothing an agent proposes runs autonomously until a person approves it. Approving moves the task to `ready`, which makes it eligible for automatic execution. -### 2. Cortex promotion (from Todo memories) +`task_create` can also file structured work in one call: `depends_on` (task numbers that must settle first), `input_bindings` (inputs wired to an upstream task's outputs, or literals), `output_schema` (a JSON Schema the outputs must satisfy), `assigned_agent_id` (delegate to another agent), and `project_id` / `repo_id` / `worktree_id` (scope the task to a codebase). -The cortex bridges the gap between quick captures and structured work. A cortex loop scans recent `Todo` memories, evaluates whether they're actionable, and promotes them to tasks in `pending_approval` status. The human reviews and approves before execution begins. +### 2. Worker-filed cards -``` -Branch saves a Todo memory during conversation - → Cortex evaluates the todo (promotion loop) - → Cortex creates a Task in "pending_approval" - → Human approves on the kanban board - → Cortex ready-task loop picks it up - → Worker executes → Done -``` - -This path is for things the agent noticed were actionable but the user didn't explicitly ask to track — the cortex catches what falls through the cracks. +Workers executing a task can file follow-up cards with `task_create` (enabled by `[defaults.cortex] worker_task_create`, default true). A worker's cards skip approval and go straight to `ready` — filing them *is* the decomposition, and approving each one would make the mechanism useless. The safety bound is numeric instead: one task may file at most 10 cards (`MAX_TASKS_FILED_PER_TASK`), and filing chains are capped at 3 hops deep (`MAX_FILING_DEPTH`). Cards a worker files are reported at completion via `task_complete`'s `created_tasks` and verified server-side. ### 3. UI / API -Tasks can be created directly from the kanban board UI or via the REST API. These default to `backlog` status with `created_by: "human"`. +Tasks can be created directly from the board UI or via the REST API (`POST /api/tasks`), with `created_by: "human"`. The default status is `pending_approval`; pass `status` to file straight into `backlog` or `ready`. + +### 4. Workflows + +A workflow launch compiles a template into a set of tasks wired with edges, bindings, and gates. See [Workflows](#workflows) below. -## Status (Kanban Columns) +## Status -Five columns on the board: +Seven statuses: | Status | Description | |--------|-------------| -| `pending_approval` | Created by cortex, awaiting human sign-off | -| `backlog` | Captured but not ready for work. Default for conversational and UI-created tasks | -| `ready` | Approved and waiting for the cortex to pick up | +| `pending_approval` | Proposed by an agent, awaiting human sign-off. Default for branch- and API-created tasks | +| `backlog` | Captured but not eligible to run — either not promoted yet, or waiting on unsettled dependencies | +| `ready` | Eligible for the cortex to claim | | `in_progress` | A worker is actively executing this task | +| `blocked` | Stuck and waiting on a human. `block_kind` says why | | `done` | Completed | +| `skipped` | Settled without running, and never will — e.g. a fan-out or gate branch that was ruled out. Terminal by design | + +A task waiting on its dependencies lives in `backlog`, not `blocked` — `blocked` is reserved for "what needs a person?", which is the only question the board exists to answer. + +### Block Kinds + +When a task is parked in `blocked`, `block_kind` records why, because the kinds recover differently: + +| Kind | Recovery | +|------|----------| +| `dependency` | Waiting on an upstream task. Clears itself via the ready sweep | +| `transient` | Flaky failure or provider outage. Retried under the failure budget | +| `needs_input` | Something only a person can repair. Sticky — an explicit unblock releases it | +| `capability` | The agent lacks a tool, credential, or permission. Sticky | +| `awaiting_decision` | A decision step asked a question and is waiting for a person's answer. The pipeline working as designed, not a fault | ### Status Transitions @@ -79,14 +80,15 @@ Transitions are validated. You can't skip steps or go backwards (except to `back ``` pending_approval → ready (approval) pending_approval → backlog (shelve) -backlog → ready (manual promotion) -ready → in_progress (cortex pickup) -in_progress → done (worker success) -in_progress → ready (worker failure, re-queued) +backlog → ready (manual promotion, or the sweep settling dependencies) +ready → in_progress (cortex claim) +in_progress → done (worker success via task_complete) +in_progress → ready (worker failure, re-queued under the failure budget) +in_progress → blocked (failure budget exhausted, or a sticky park) done → backlog (reopen) ``` -Attempting an invalid transition (e.g., `pending_approval → in_progress`, `ready → done`) returns an error. +Attempting an invalid transition returns an error. `skipped` is terminal — a task that could come back would make "settled" mean nothing to the dependency rule below it. ## Priority @@ -96,41 +98,107 @@ Four levels, ordered by urgency: critical > high > medium > low ``` -Default: `medium`. The cortex ready-task loop respects priority — a critical task is picked up before a low-priority one, regardless of creation order. +Default: `medium`. The cortex claims the highest-priority eligible task first, regardless of creation order. -## Subtasks +## Dependencies -Simple checklist items stored as a JSON array. One level deep, no nesting. +`depends_on` creates an edge from this task to the tasks it waits on. A task with unsettled parents is not eligible: it sits in `backlog` (or `blocked` with `block_kind: dependency`), and the ready sweep promotes it once every parent is `done` or `skipped`. `done` and `skipped` are the two settled statuses — a parent that was ruled out still releases its children. -```json -[ - {"title": "Research existing API endpoints", "completed": false}, - {"title": "Draft schema changes", "completed": true}, - {"title": "Implement migration", "completed": false} -] -``` +Edges can also be managed after creation via the API (`POST/DELETE /api/tasks/{n}/dependencies`). -Subtasks are included in the worker's prompt as an execution plan. Workers can mark subtasks complete via the `task_update` tool as they progress. +## Contracts: Input and Output Schemas -## Metadata +A task can carry two JSON Schemas: -Arbitrary key-value pairs stored as a JSON object. Used for linking to external resources. +- `input_schema` — the task's resolved inputs must satisfy it before the task runs. +- `output_schema` — the task's outputs must validate against it at completion. A worker completes its task by calling `task_complete` with a human-readable `summary` and machine-readable `outputs`; the outputs are validated against the schema before the task settles. -```json -{ - "github_issue": "https://github.com/org/repo/issues/123", - "estimated_effort": "small", - "worker_type": "opencode", - "skill": "rust-dev", - "notes": "Depends on the auth refactor landing first" -} -``` +Validated outputs are what downstream tasks read from. They are stored on the task (`outputs`) and remain readable after upstream changes. + +## Input Bindings + +An input binding says where one of a task's inputs comes from: + +- **Direct** — a pointer into an upstream task's outputs: `{ input_key, source_task_number, source_pointer }`, where `source_pointer` is an RFC 6901 JSON Pointer like `/image/tag`. +- **Literal** — a fixed JSON value. +- **Fan-in** — collect one field from every branch of a fan-out, keyed by branch. + +At claim time the bindings are resolved and the result is persisted on the task (`inputs`), so the value the worker actually saw survives a crash. Bindings are embedded into the claiming worker's prompt. + +## Gates + +A dependency edge says "wait for that task". A gate says "wait for that *fact*" — CI is green, the branch merged, an upstream task returned a particular value. A task with an unsatisfied gate is not promotable, exactly as a task with an unfinished parent is not. + +Two kinds: + +| Kind | What it waits on | +|------|------------------| +| `http` | Poll a URL; assert on the response status (`expect_status`) and/or a JSON Pointer into the body (`pointer`, optionally with `equals`). Covers GitHub, GitLab, Buildkite, Jenkins without vendor SDKs. Optional `headers` for authentication | +| `task_output` | Read an upstream task's stored outputs and compare at a JSON Pointer. This is also where conditional steps live — "run the rollback only if deploy reported failure" | + +Gates are polled by the instance on an interval (`poll_interval_secs`, minimum 15 seconds) with a 10-second per-evaluation timeout. Every evaluation lands in one of five results: + +| Result | Meaning | +|--------|---------| +| `pending` | Not true yet. May become true on its own; keep polling | +| `satisfied` | True. Latched — never re-evaluated | +| `failed` | Definitively false. Polling will not fix it; a person must | +| `erroring` | We could not tell — the endpoint was unreachable or the assertion malformed. Our problem, not the graph's | +| `routed` | The condition did not hold, and this gate *routes* rather than waits — the task it guarded is settled as `skipped` | + +An erroring gate backs off (up to 15 minutes between polls) and gives up after 5 consecutive errors (`GATE_ERROR_LIMIT`) — an unreachable endpoint becomes visible instead of silently expensive. + +A gate's **disposition** decides what a false answer means: `wait` ("is CI green yet?" — keep polling) or `route` ("should this branch run?" — settle the task as `skipped`). Waiting forever is correct for the first and a deadlock for the second. + +## Fan-out + +A fan-out runs one task per item of a collection an upstream task produced. In a workflow, a step sets `for_each_step_key` plus a `for_each_pointer` selecting an array in that step's outputs; an optional `for_each_key` pointer labels each branch. + +At launch the step compiles to a **placeholder** task that carries the edges its branches will inherit. When the source task completes, the placeholder expands into one branch task per item, each with its item bound to the `item` input key, and is deleted. A fan-in binding downstream collects one field from every branch, keyed by branch name. + +Fan-out width is bounded: one fan-out may emit at most 50 branches (`MAX_FAN_OUT_BRANCHES`). A fan-out that would exceed the cap is refused outright rather than truncated — a truncated fan-out feeding a fan-in would report part of the collection as the whole of it. + +## Loops + +A loop is one or more workflow steps sharing a `loop_group`. The body runs, an exit predicate (`loop_until`, expressed as the same object a `task_output` gate takes: `{"pointer": "/tests/passed", "equals": true}`) is evaluated against the body's exit-point outputs, and the body either turns over for another pass or the loop settles. + +Iterations are bounded: `loop_max_iterations` per template (default 3, `DEFAULT_LOOP_MAX_ITERATIONS`), never more than 25 (`MAX_LOOP_ITERATIONS`). A task retried under the failure budget keeps its iteration — retrying is not looping. + +## Workflows + +A workflow is a reusable pipeline definition stored on the instance. Launching one compiles its steps into real tasks on the board — from that point on, the ordinary machinery (ready sweep, claim, failure budget, gates) runs them. + +Each step has a stable `step_key` and a kind: + +| Kind | What it is | +|------|-----------| +| `agent` | One task, claimed by a worker with a full tool loop | +| `command` | A process, an exit code, and its output — a deterministic check, so downstream loop and branch predicates read a trustworthy value. Requires `command` and `command_timeout_secs`; optional `expect_exit_code` | +| `decision` | A question put to a person, whose answer is the step's whole output. The run parks in `blocked` with `block_kind: awaiting_decision` until someone answers. Optional timeout behavior (`decision_timeout_action`: wait, fail, or apply a validated default) | + +Steps are wired with edges (dependencies), input bindings, and gates — the same concepts as ad-hoc tasks, declared on the template. Steps can fan out (`for_each_step_key`) and loop (`loop_group`). A step can also declare `worktree_mode` to run in its own git checkout: `per_run` (one worktree for the step, created at launch) or `per_branch` (one per fan-out branch). Dirty worktrees are never deleted by the reaper. -No enforced schema. The UI renders known keys with special formatting (e.g., GitHub links become clickable) and displays unknown keys as plain key-value pairs. +A run can be started three ways, and all three just call launch: + +- **Manually** — the UI, the API (`POST /api/workflows/{id}/run`), or the `launch_workflow` LLM tool +- **Schedule** — a stored cron expression (evaluated in UTC) firing the launch +- **Webhook** — an inbound POST authenticated with a shared secret header (`x-spacebot-webhook-secret`), with a stored payload mapping + +Run history is inspectable per workflow (`GET /api/workflows/{id}/runs`, `GET /api/workflow-runs/{run_id}`), and a running run can be cancelled. + +## Subtasks + +Simple checklist items stored as a JSON array. One level deep, no nesting. Subtasks are included in the worker's prompt as an execution plan, and workers can mark them complete via `task_update` as they progress. + +## Metadata + +Arbitrary key-value pairs stored as a JSON object. Used for linking to external resources (GitHub issues, PRs). No enforced schema; the UI renders known keys with special formatting. ## Task Numbering -Per-agent, monotonically increasing. The next number is `MAX(task_number) + 1` within the agent's task table. Tasks are referenced as `#1`, `#42`, etc. Numbers are never reused — deleting task `#5` doesn't free up the number. +Instance-wide and monotonically increasing, allocated from a sequence — tasks are referenced as `#1`, `#42`, etc., and the same number means the same task to every agent on the instance. Numbers are never reused. + +Older installs kept per-agent task stores; on first boot after upgrade, a one-time idempotent migration moves every per-agent task into the global store with new globally unique numbers (the original number is preserved in metadata as `legacy_task_number`). ## Execution @@ -138,20 +206,24 @@ Per-agent, monotonically increasing. The next number is `MAX(task_number) + 1` w The primary execution path. A background loop runs every `cortex.tick_interval_secs` (default 30 seconds): -1. **Claim** — Atomically finds the oldest `ready` task with the highest priority and moves it to `in_progress` -2. **Build prompt** — Renders the worker system prompt with the task title, description, and subtask checklist -3. **Spawn worker** — Creates a new worker with full tool access (shell, file, exec, browser) -4. **Bind** — Sets `worker_id` on the task, linking it to the executing worker -5. **Execute** — The worker runs its loop, using subtasks as an execution plan -6. **Complete** — On success, the task moves to `done` with `completed_at` set. On failure, the task moves back to `ready` with `worker_id` cleared, so it gets re-queued for another attempt +1. **Claim** — Atomically finds the highest-priority eligible `ready` task assigned to its agent and moves it to `in_progress` (a conditional `UPDATE`, so two agents racing the same task can't both win it) +2. **Resolve inputs** — Evaluates the task's input bindings and persists the resolved values +3. **Build prompt** — Renders the worker system prompt with the task title, description, subtask checklist, and resolved inputs +4. **Spawn worker** — Creates a worker with tool access, scoped to the task's project/repo/worktree when set +5. **Bind** — Sets `worker_id` on the task, linking it to the executing worker +6. **Complete** — The worker calls `task_complete` with a summary and outputs; outputs are validated against `output_schema` and stored -Worker success/failure is determined by whether `worker.run()` returns `Ok` or `Err`. The cortex doesn't evaluate the quality of the work — a worker that completes without errors is considered successful. +### Failure Budget -### API Execute Endpoint +On failure the task is re-queued to `ready` while it has budget left. The default budget is 2 consecutive failures (`DEFAULT_FAILURE_LIMIT`); a task can override it with `max_retries` (a failure limit, not a retry count — `max_retries = 1` allows one attempt and zero retries). When the budget is exhausted the task parks in `blocked` with `block_kind: transient`, carrying `last_error` so the board can show why without joining run history. An operator-initiated retry resets the counter. -The `/api/agents/tasks/:number/execute` endpoint moves a task to `ready` (if it's in `backlog` or `pending_approval`), letting the cortex loop pick it up. Tasks already in `ready` or `in_progress` are returned as-is. +### Pooled Tasks and Capabilities -This means execution always flows through the cortex — the API doesn't spawn workers directly. +A task normally names its assignee. Setting `required_capabilities` instead makes it *pooled*: any agent declaring all of those labels may claim it, and claiming stamps `assigned_agent_id` from that moment on. + +### API Execute Endpoint + +The `POST /api/tasks/{n}/execute` endpoint moves a task to `ready` so the cortex loop picks it up. Tasks already `ready` or `in_progress` are returned as-is; `pending_approval` tasks are rejected — they must be approved first. Execution always flows through the cortex — the API doesn't spawn workers directly. ### Worker Scope @@ -160,29 +232,19 @@ Workers executing a task get a restricted version of the `task_update` tool. The - Update subtasks (mark complete, replace the checklist) - Update metadata -They cannot change the task's status, priority, title, description, or worker binding. These fields are managed by the cortex and the API. This prevents a worker from marking its own task as `done` — only the cortex does that based on whether the worker succeeded or failed. +They cannot change the task's status, priority, title, description, or worker binding. A worker settles its own task exclusively through `task_complete`, whose outputs are validated against the task's output schema. ## Bulletin Integration -Active tasks (non-done) are included in the cortex memory bulletin under an "Active Tasks" section. Each task is listed with its number, status, priority, title, and subtask progress: - -``` -### Active Tasks - -- #3 [in_progress] (high) Implement auth refactor [2/5] -- #7 [ready] (medium) Update deployment docs -- #12 [backlog] (low) Clean up unused dependencies -``` - -This gives every channel and branch awareness of the agent's current task board without querying the task store directly. +Active (non-settled) tasks are included in the cortex memory bulletin under an "Active Tasks" section, with number, status, priority, title, and subtask progress. This gives every channel and branch awareness of the instance's task board without querying the store directly. ## LLM Tools -Three tools available to branches and cortex chat sessions: +Four task tools: ### task_create -Creates a new task. +Creates a new task. Available to branches, cortex chat, and (optionally, via `[defaults.cortex] worker_task_create`, default true) workers. | Argument | Type | Required | Default | |----------|------|----------|---------| @@ -191,7 +253,13 @@ Creates a new task. | `priority` | string | no | `"medium"` | | `subtasks` | string[] | no | `[]` | | `metadata` | object | no | `{}` | -| `status` | string | no | `"backlog"` | +| `project_id` | string | no | - | +| `repo_id` | string | no | - | +| `worktree_id` | string | no | - | +| `depends_on` | integer[] | no | `[]` | +| `assigned_agent_id` | string | no | filing agent | +| `output_schema` | object | no | - | +| `input_bindings` | object[] | no | `[]` | Returns the created task number and status. @@ -220,130 +288,91 @@ Updates an existing task. Available to branches (unrestricted) and workers (rest | `metadata` | object | no | Merged with existing | | `complete_subtask` | integer | no | Index to mark complete | -## API Endpoints +### task_complete + +Settles the calling worker's task as `done`. Worker-scope only. + +| Argument | Type | Required | Notes | +|----------|------|----------|-------| +| `task_number` | integer | yes | The task being settled | +| `summary` | string | yes | Human-readable account of what was done | +| `outputs` | object | yes | Machine-readable result, validated against the task's `output_schema` | +| `created_tasks` | integer[] | no | Tasks filed while working — verified server-side | -All endpoints require `agent_id` as a query parameter or in the request body. +## API Endpoints | Method | Path | Description | |--------|------|-------------| -| `GET` | `/api/agents/tasks` | List tasks (filterable by status, priority) | -| `GET` | `/api/agents/tasks/:number` | Get single task by number | -| `POST` | `/api/agents/tasks` | Create task | -| `PUT` | `/api/agents/tasks/:number` | Update task | -| `DELETE` | `/api/agents/tasks/:number` | Delete task | -| `POST` | `/api/agents/tasks/:number/approve` | Approve (moves to `ready`) | -| `POST` | `/api/agents/tasks/:number/execute` | Execute (moves to `ready` for cortex pickup) | +| `GET` | `/api/tasks` | List tasks (filterable by status, priority, agent) | +| `GET` | `/api/tasks/{n}` | Get single task | +| `GET` | `/api/tasks/transitions` | List valid status transitions | +| `POST` | `/api/tasks` | Create task | +| `PUT` | `/api/tasks/{n}` | Update task | +| `DELETE` | `/api/tasks/{n}` | Delete task | +| `POST` | `/api/tasks/{n}/approve` | Approve (moves to `ready`) | +| `POST` | `/api/tasks/{n}/execute` | Execute (moves to `ready` for cortex pickup) | +| `POST` | `/api/tasks/{n}/retry` | Retry (resets the failure counter, re-queues) | +| `POST` | `/api/tasks/{n}/assign` | Reassign to another agent | +| `POST` | `/api/tasks/{n}/block` | Park with a `block_kind` and reason | +| `POST` | `/api/tasks/{n}/unblock` | Release a sticky block | +| `POST` | `/api/tasks/{n}/decision` | Answer a decision step's question | +| `GET` | `/api/tasks/{n}/runs` | Attempt history for the task | +| `GET/POST` | `/api/tasks/{n}/dependencies` | List / add dependency edges | +| `DELETE` | `/api/tasks/{n}/dependencies/{parent}` | Remove a dependency edge | +| `GET/PUT` | `/api/tasks/{n}/contract` | Read / set input and output schemas | +| `PUT/DELETE` | `/api/tasks/{n}/bindings/{key}` | Set / remove an input binding | +| `GET` | `/api/tasks/{n}/provenance` | Where this task's inputs came from | +| `GET/POST` | `/api/tasks/{n}/gates` | List / create gates | +| `DELETE` | `/api/tasks/{n}/gates/{gate_id}` | Remove a gate | +| `GET` | `/api/tasks/{n}/graph` | The dependency graph centred on this task | + +Workflow endpoints live under `/api/workflows` and `/api/workflow-runs` — CRUD for templates and their steps/edges/bindings/gates, plus `run`, `runs`, and `cancel`. ### SSE Events -Task state changes emit `task_updated` SSE events to connected clients: - -```json -{ - "type": "task_updated", - "agent_id": "main", - "task_number": 42, - "status": "in_progress", - "action": "updated" -} -``` - -The `action` field is one of `"created"`, `"updated"`, or `"deleted"`. The kanban board UI uses these events for real-time updates. +Task state changes emit `task_updated` SSE events to connected clients, which the board UI uses for real-time updates. ## Interface -### Task List - -The **Tasks** tab renders a Linear-style task list. Each row shows: - -- `#N` task number and title -- Status indicator -- Priority badge (color-coded: red for critical, amber for high, default for medium, outline for low) -- Subtask progress (if subtasks exist) -- GitHub metadata badges (linked issues and PRs, rendered from metadata fields) -- Worker badge (if a worker is bound) -- Creation timestamp and author - -### Task Detail - -Clicking a task opens a detail view with the full markdown description, subtask checklist, metadata, timestamps, and action buttons (Approve, Execute, Mark Done). GitHub issue and PR links in metadata render as clickable badges with status indicators. - -### Create Task - -The create form has fields for title, description, priority, and initial status. Tasks created from the UI default to `backlog` status and `"human"` as the creator. - -### Real-Time Updates - -Task state changes are pushed to the UI via SSE events. When a worker completes a task, marks subtasks done, or a status transition occurs, the task list updates immediately without polling. +The **Tasks** tab renders the board as a list, a kanban, or a dependency graph. The detail view shows the full markdown description, subtask checklist, metadata, dependencies, gates and their latest results, run history, and action buttons (Approve, Execute, Retry, Block/Unblock). Task state changes are pushed to the UI via SSE — no polling. ## Storage -One SQLite table in the agent's database. - -```sql -CREATE TABLE IF NOT EXISTS tasks ( - id TEXT PRIMARY KEY, - agent_id TEXT NOT NULL, - task_number INTEGER NOT NULL, - title TEXT NOT NULL, - description TEXT, - status TEXT NOT NULL DEFAULT 'backlog', - priority TEXT NOT NULL DEFAULT 'medium', - subtasks TEXT, -- JSON array - metadata TEXT, -- JSON object - source_memory_id TEXT, - worker_id TEXT, - created_by TEXT NOT NULL, - approved_at TIMESTAMP, - approved_by TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - completed_at TIMESTAMP, - UNIQUE(agent_id, task_number) -); -``` - -Indexes on `agent_id`, `status`, `(agent_id, task_number)`, `source_memory_id`, and `worker_id`. +One global SQLite database (`data/tasks.db`) shared by every agent on the instance, holding the `tasks` table plus dependency edges, input bindings, gates, run attempts (`task_runs`), workflow templates, and workflow runs. ## Module Layout ``` src/ -├── tasks.rs → tasks/ -│ └── store.rs — TaskStore: CRUD, status transitions, claim_next_ready +├── tasks/ +│ ├── store.rs — TaskStore: CRUD, transitions, claim, dependencies, +│ │ contracts, bindings, fan-out, loops, run history +│ ├── gates.rs — gate storage, evaluation, polling, error backoff +│ └── migration.rs — one-time per-agent → global store migration +│ +├── workflows/ +│ ├── store.rs — WorkflowStore: templates, launch/compile, runs +│ ├── triggers.rs — schedules and webhooks firing launches +│ └── worktrees.rs — per-run / per-branch checkout lifecycle │ ├── tools/ -│ ├── task_create.rs — task_create LLM tool (branches + cortex chat) -│ ├── task_list.rs — task_list LLM tool (branches + cortex chat) -│ └── task_update.rs — task_update LLM tool (branches + workers, scoped) +│ ├── task_create.rs — task_create LLM tool +│ ├── task_list.rs — task_list LLM tool +│ ├── task_update.rs — task_update LLM tool (branches + workers, scoped) +│ └── task_complete.rs — task_complete LLM tool (workers) │ ├── api/ -│ └── tasks.rs — REST endpoints (list, get, create, update, delete, -│ approve, execute) with SSE event emission -│ -├── agent/ -│ └── cortex.rs — spawn_ready_task_loop, pickup_one_ready_task, -│ gather_active_tasks (bulletin integration) +│ ├── tasks.rs — task REST endpoints with SSE event emission +│ └── workflows.rs — workflow and run REST endpoints │ -└── migrations/ - └── 20260219000001_tasks.sql +└── agent/ + └── cortex.rs — ready sweep, claim, gate polling, run supervision ``` ## Prompt Integration The channel, branch, and cortex chat prompts are all task-aware: -- **Channel prompt** (`channel.md.j2`) — has a dedicated "Task Board" section explaining spec-driven tasks and the kanban board. The Delegation section tells the channel to branch for task management. Active tasks appear in the Memory Context via the bulletin. -- **Branch prompt** (`branch.md.j2`) — documents all three task tools (`task_create`, `task_list`, `task_update`) with spec-driven guidance. `task_create` emphasizes rich markdown descriptions and pre-filled subtasks. `task_update` is framed as iterative spec refinement. Moving to `ready` triggers cortex auto-pickup. -- **Cortex chat prompt** (`cortex_chat.md.j2`) — lists task board management as a core capability with spec-driven language. The cortex chat has all three task tools. -- **Tool descriptions** — each task tool has a description template in `prompts/en/tools/` that reinforces the spec-driven philosophy: `task_create` tells the LLM to write full markdown specs with subtask execution plans, `task_update` tells it to refine specs as scope evolves. - -The channel itself has no task tools — it always branches to manage tasks. This keeps the channel responsive and ensures task operations go through a thinking process. - -## What's Not Implemented Yet - -- **Cortex todo-promotion loop** — the cortex loop that scans `Todo` memories and promotes them to `pending_approval` tasks. The data model and execution path are ready; the promotion evaluation prompt and loop are not yet built. -- **Activity timeline** — the detail view doesn't show a history of status changes, approvals, and worker events. -- **Task count badge** — the Tasks tab doesn't show a badge with the pending approval count yet. -- **Task archival** — done tasks accumulate indefinitely. Options: auto-archive after N days, a separate `archived` status, or UI filtering. -- **Rejection feedback** — when a human deletes a pending task, that signal isn't fed back to the cortex. Saving it as a memory would help the cortex learn what's not actionable. +- **Channel prompt** (`channel.md.j2`) — has a dedicated "Task Board" section. The channel itself has no task tools; it always branches to manage tasks. +- **Branch prompt** (`branch.md.j2`) — documents the task tools with spec-driven guidance: rich markdown descriptions, pre-filled subtask execution plans, iterative spec refinement. +- **Tool descriptions** — each task tool has a description template in `prompts/en/tools/` reinforcing the spec-driven philosophy. diff --git a/docs/content/docs/(getting-started)/docker.mdx b/docs/content/docs/(getting-started)/docker.mdx index f710eb5b2..c230c2523 100644 --- a/docs/content/docs/(getting-started)/docker.mdx +++ b/docs/content/docs/(getting-started)/docker.mdx @@ -70,16 +70,21 @@ Available environment variables: | Variable | Description | | ------------------------ | ---------------------- | -| `ANTHROPIC_API_KEY` | Anthropic API key | -| `OPENAI_API_KEY` | OpenAI API key | -| `OPENROUTER_API_KEY` | OpenRouter API key | +| `ANTHROPIC_API_KEY` | Anthropic API key (or `ANTHROPIC_AUTH_TOKEN` for OAuth tokens) | +| `ANTHROPIC_BASE_URL` | Override the Anthropic endpoint | +| `LITELLM_API_KEY` | LiteLLM proxy key — the supported path to every non-Anthropic provider | +| `LITELLM_BASE_URL` | Override the LiteLLM endpoint (default `http://localhost:4000/v1`) | | `DISCORD_BOT_TOKEN` | Discord bot token | | `SLACK_BOT_TOKEN` | Slack bot token | | `SLACK_APP_TOKEN` | Slack app token | +| `TELEGRAM_BOT_TOKEN` | Telegram bot token | | `BRAVE_SEARCH_API_KEY` | Brave Search API key | +| `SPACEBOT_MODEL` | Override channel, branch, and worker model | | `SPACEBOT_CHANNEL_MODEL` | Override channel model | | `SPACEBOT_WORKER_MODEL` | Override worker model | +`ANTHROPIC_API_KEY` and `LITELLM_API_KEY` are the only two variables that bootstrap a provider. Other provider variables (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `GEMINI_API_KEY`, …) were retired and are ignored with a warning — configure those providers with an explicit `[llm.provider.]` block in `config.toml` instead. + ### Config File Mount a config file into the volume for full control: @@ -96,8 +101,10 @@ docker run -d \ Config values can reference environment variables with `env:VAR_NAME`: ```toml -[llm] -anthropic_key = "env:ANTHROPIC_API_KEY" +[llm.provider.anthropic] +api_type = "anthropic" +base_url = "https://api.anthropic.com" +api_key = "env:ANTHROPIC_API_KEY" ``` See [Configuration](/docs/config) for the full config reference. diff --git a/docs/content/docs/(getting-started)/quickstart.mdx b/docs/content/docs/(getting-started)/quickstart.mdx index 4233de41c..636479ed0 100644 --- a/docs/content/docs/(getting-started)/quickstart.mdx +++ b/docs/content/docs/(getting-started)/quickstart.mdx @@ -46,8 +46,9 @@ You can also manage updates from **Settings → Updates** in the web UI. ### Prerequisites - **Rust 1.85+** — `rustup update stable` +- **protoc** (protobuf compiler) — required by LanceDB's build scripts. `apt install protobuf-compiler` or `brew install protobuf` - **Bun** (optional, for the web UI) — `curl -fsSL https://bun.sh/install | bash` -- **An LLM API key** — Anthropic, OpenAI, OpenRouter, Kilo Gateway, or OpenCode Go +- **An LLM API key** — Anthropic, or any OpenAI-compatible endpoint (LiteLLM, vLLM, Ollama, OpenRouter, OpenAI) ### Install @@ -91,13 +92,18 @@ Just run `spacebot` with no config file and no API key env var set. It will walk Create `~/.spacebot/config.toml`: ```toml -[llm] -anthropic_key = "sk-ant-..." -# or: openrouter_key = "sk-or-..." -# or: kilo_key = "sk-..." -# or: opencode_go_key = "..." -# or: openai_key = "sk-..." -# Keys also support env references: anthropic_key = "env:ANTHROPIC_API_KEY" +# Anthropic, native. Required for OAuth, prompt caching, and extended thinking. +[llm.provider.anthropic] +api_type = "anthropic" +base_url = "https://api.anthropic.com" +api_key = "sk-ant-..." # also accepts "env:VAR" and "secret:NAME" + +# Anything else is an OpenAI-compatible endpoint. base_url is the full path +# prefix, so include /v1 when your server expects it. +# [llm.provider.litellm] +# api_type = "openai_compatible" +# base_url = "http://localhost:4000/v1" +# api_key = "env:LITELLM_API_KEY" [[agents]] id = "main" @@ -173,8 +179,10 @@ mkdir -p ~/.spacebot/dev Create `~/.spacebot/dev/config.toml`: ```toml -[llm] -anthropic_key = "env:ANTHROPIC_API_KEY" +[llm.provider.anthropic] +api_type = "anthropic" +base_url = "https://api.anthropic.com" +api_key = "env:ANTHROPIC_API_KEY" [[agents]] id = "main" diff --git a/docs/design-docs/command-steps.md b/docs/design-docs/command-steps.md new file mode 100644 index 000000000..9b8c9fa81 --- /dev/null +++ b/docs/design-docs/command-steps.md @@ -0,0 +1,170 @@ +# Command Steps + +A workflow step that runs a command instead of an agent. `bun run lint` is not a +question anybody needs a model to answer. + +## Problem + +Every step in a workflow compiles to a task, and every task is claimed by a worker — +an LLM agent with a full tool loop. For "summarise these findings" that is exactly +right. For "does this lint" it is wrong three ways: + +- **Cost.** A model turn to run a command and report what it printed. +- **Latency.** Roughly a minute against roughly a second. +- **Truth.** This is the one that matters. Asked whether the code lints, a model + reports *its account* of the exit code. A step whose whole purpose is to be an + objective check should not route its answer through something that can be + mistaken about it. + +That third point is what makes this more than an optimisation. The loop and branch +predicates read step outputs and decide what runs next. A predicate is only as +trustworthy as the value it reads, so a **deterministic check makes every downstream +decision trustworthy**. `{"exit_code": 0}` is ground truth; "I ran the linter and it +looked clean" is testimony. + +## Design + +### A step kind + +`workflow_steps` gains `kind`: `agent` (default, today's behaviour) or `command`. + +A command step carries a command line, and produces: + +```json +{"exit_code": 1, "stdout": "…", "stderr": "…", "duration_ms": 840} +``` + +That is its `output_schema`, implicitly — bindings, gates, `loop_until` and conditions +all read it with the pointers they already use. No new plumbing anywhere downstream, +because a command step is just a task that produces outputs like any other. + +### Where it runs + +Nowhere new. A task already binds to a project / repo / worktree, and +`resolve_worker_working_dir` already resolves that binding to a directory and refuses +anything the sandbox allowlist does not cover. A command step runs in exactly the +directory its binding names, under exactly the same rule, and a step with no binding +has no directory and is refused rather than silently defaulting to the workspace. + +### Exit code is data, not failure + +**The load-bearing decision.** These are two different events: + +| | | +|---|---| +| the command **ran and reported a problem** | `exit 1` from a linter — the *task* succeeded | +| the command **could not run** | binary missing, timeout, killed — the *task* failed | + +Conflating them breaks the entire feature. A lint step that treats `exit 1` as a task +failure burns two attempts of the failure budget and parks itself before the fix loop +has run twice — the loop would die of the very condition it exists to fix. + +The distinction is **derivable, not configured**, which is the same shape as the gate +disposition in `workflow-branching.md`. At the process level: + +- `spawn()` errored, the timeout fired, or the process was signalled → **task failure**, + charge the budget +- the process ran to completion and exited → **task success**, whatever the code, and + the code is data + +An optional `expect_exit_code` exists for steps where non-zero really is a failure — +`git push` should not quietly "succeed" with exit 1. Absent by default, because the +common case is a check whose answer is the point. + +### The loop this exists for + +``` + lint ──▶ fix ──▶ lint' + │ │ + │ loop_until /exit_code == 0 + │ max_iterations 3 + ▼ + (clean) +``` + +Concretely: a `command` step running the linter, an `agent` step bound to the previous +iteration's `stdout` with instructions to fix what it reports, and a `loop_group` over +the two with the check as the body's exit step. + +Two things fall out that were designed before this was on the table: + +- **`loop_until` reads the check, not the fixer.** The body's exit step is the + deterministic one, so the loop terminates on ground truth rather than on the fixer's + opinion of its own work. That is the whole reason the exit predicate belongs on the + body's terminal step. +- **`PreviousIteration` falling back to the entry binding on iteration 1** means the + fixer reads the pre-loop lint on its first pass and the previous iteration's lint + after that, with no special first-pass wiring. That fallback was specified for loops + in the abstract; this is the case that justifies it. + +### Output is capped, and says so + +`stdout` feeds the next step's prompt, so it is both the point and the risk. Capped at +a fixed size with an explicit marker when truncated — silently handing a model half a +log and no indication is how a fix loop ends up confidently fixing the wrong thing. +Head and tail are kept in preference to the head alone; the useful part of a failing +build log is usually at the end. + +### Security + +A command step is arbitrary code execution, stored in a template, run repeatedly and +unattended. That is a meaningfully different exposure from a worker choosing to run a +command in the moment, even though the capability is the same. + +Nothing new is invented for it. It reuses `Sandbox::wrap`, the read/write allowlists, +the cwd rule above, `kill_on_drop` so a timeout cannot orphan a process tree, and a +hard timeout that is a required field rather than an inherited default. Tool secrets +are *not* injected into the environment: a worker gets them because a worker is trusted +to use them, and a template command is authored once and run forever. + +**But `Sandbox::wrap` contains less than its name suggests, and this matters here.** +`containment_active()` is `mode_enabled() && backend != None`, where the backend is +bubblewrap on Linux or `sandbox-exec` on macOS. With no backend installed — which is +the case on the current preview host — `mode = "enabled"` in config yields *no OS-level +containment at all*: the read/write prompt allowlists come back empty and `wrap` builds +an ordinary `Command` with a `PATH` adjustment. What remains is `is_path_allowed`, +which the *file tools* consult voluntarily and which a shell command does not go +through. + +For a worker that is a considered risk: an agent runs a command in the moment, watched, +as part of work someone asked for. A command step is different in kind — stored, +repeated, and unattended — so it should not inherit that posture silently. A command +step must **refuse to run when containment is inert**, unless the instance explicitly +opts out. Failing closed is the only honest default for stored code execution, and it +also turns an invisible property into a visible one. + +That the two conditions read alike from the config surface — `mode_enabled()` says yes, +`containment_active()` says no — is the same one-label-two-conditions shape this +codebase keeps paying for, this time in the security layer. + +## What it does not become + +Not a general job runner. No retries of its own (the failure budget already exists), +no cron (that already exists), no long-running services. A command step runs, exits, +and produces outputs — anything that wants to be a daemon is not a step in a pipeline. + +Nor is it a way to avoid agents. The interesting pipelines are mixed: a deterministic +check, an agent that reasons about the output, a deterministic verification that it +worked. **The value is in the alternation** — each doing the thing the other cannot. + +## Build order + +1. `kind` on `workflow_steps`, command line and timeout, launch validation (a command + step must have a binding; an agent step must not carry a command line). +2. The pickup branch in `cortex.rs`: a command task executes rather than spawning a + worker. This is the only scheduler change. +3. Outputs, the ran-vs-failed distinction, the output cap. +4. UI: a visibly different node on the canvas, because a graph that draws a shell + command identically to an agent step is lying about what it does. + +## Risks + +- **The ran-vs-failed distinction is the whole feature.** Getting it backwards makes + every check step burn its failure budget on a working check. +- **A command step is not a worker**, so everything the worker path does incidentally — + attempt records, event emission, the reaper — has to be done deliberately or a + command task becomes invisible to the machinery that recovers dead work. +- **Prompt injection through stdout.** A fix step reads a build log written by + whatever the build touched. It arrives as task input, and task input is already + treated as data rather than instructions, but this is the first path where the + content is attacker-influenceable at scale. diff --git a/docs/design-docs/human-decisions.md b/docs/design-docs/human-decisions.md new file mode 100644 index 000000000..f29b3bd85 --- /dev/null +++ b/docs/design-docs/human-decisions.md @@ -0,0 +1,118 @@ +# Human Decisions + +A pipeline can stop and wait for a person. It cannot ask one a question and use the +answer. Task #30. + +## Problem + +`BlockKind::NeedsInput` parks a task for a human, and `POST /tasks/{n}/unblock` +releases it. So "wait for a person" works. + +But unblocking is a single undifferentiated act. The task resumes and **nothing +downstream learns what the person decided.** "Approve this deploy?" and "which of these +three options?" are not expressible, because there is no channel for the answer — only +for the fact that someone acted. + +Today the workaround is an agent step that asks in a channel and reports what it heard. +That is slower, costs a model call, and can be wrong about what was said. It also +launders a human decision through a model, so the run record cannot distinguish "the +operator approved this" from "a model believed the operator approved this". For a +deploy gate that distinction is the entire point. + +This pairs directly with branching (#19): a human decision is exactly the kind of value +a condition should route on. Approve → ship. Reject → roll back. Neither is reachable +while the answer has nowhere to live. + +## The constraint + +**A human must not be able to set an arbitrary task's outputs.** Decided earlier and +deliberately: the outputs of an agent task are a record of what that agent produced, +and a person editing them destroys the only honest account of what happened. Anything +built here has to respect that. + +So this cannot be "let humans fill in outputs on a blocked task". That would be the +same feature with the provenance quietly removed. + +## Design + +### A decision step + +A step kind — alongside `agent` and the proposed `command` — whose entire purpose is +the answer. Its `output_schema` is written by the template author and describes what is +being asked for; the person answering fills exactly that and nothing else. + +The distinction survives into the record: a decision step's outputs are *known* to have +come from a person, because that is the only thing that kind of step can produce. No +mixing, no ambiguity, and no need to trust a field saying who wrote what. + +``` +kind decision +prompt the question, as the person will read it +output_schema what a valid answer looks like +asked_of who may answer — a person, a group, or anyone +timeout optional; what happens if nobody answers +``` + +The schema doing double duty is the point. A yes/no gate is +`{"approved": {"type": "boolean"}}`; a three-way choice is an `enum`; a free-text reason +is a `string`. The existing contract validation already enforces it at completion, so a +malformed answer is refused the same way a malformed agent output is — no second +validation path. + +### It is a task, like everything else + +A decision step compiles to a task, sits in the graph, has parents and children, binds +inputs, and produces outputs. It parks itself waiting for a person instead of being +claimed by a worker. + +That means everything already built applies unchanged: the graph view shows it, gates +can hold it, a loop can contain it, branching can route on its answer, and a run's +`stuck` detector counts it as legitimately waiting rather than wedged — which is a +distinction the detector must actually make, since a decision waiting on a person is +not a stalled run. + +### Timeouts + +An unanswered decision is the common failure and needs an answer that is not "wait +forever". Options, declared per step: + +- **wait** — default; it parks until answered, and the run is legitimately blocked. +- **default after N** — a declared default answer applies, recorded *as* a default so + the record does not claim a person chose it. +- **fail after N** — the step fails and the failure path routes it. + +The middle one is the one to get right. A defaulted answer that looks identical to a +human answer in the run record is the provenance problem returning through a side door. + +### Asking + +A decision needs to reach someone. The notification machinery already exists — +`TaskApproval` notifications, an action URL, the task drawer — so the first cut is a +notification pointing at the task, answered in the drawer. + +Delivering the question into a chat channel and accepting the reply there is the +obvious follow-on, and it is where the provenance question gets genuinely hard: a +message in a channel is attributable to a person, but parsing an answer out of prose +puts a model back in the middle. Structured replies, or a link back to the drawer, +rather than free-text interpretation. + +## Build order + +1. The `decision` step kind, compiling to a task that parks for a person. +2. Answering in the task drawer, validated against the step's `output_schema`. +3. Notification on a decision becoming answerable. +4. `stuck` detection treating an unanswered decision as waiting, not wedged. +5. Timeouts, with a defaulted answer recorded as defaulted. +6. Answering from a channel — last, because provenance there is the hard part. + +## Risks + +- **Provenance erosion.** Every shortcut here — a default that looks like an answer, a + model parsing a reply, an operator editing outputs "just this once" — removes the one + property that makes a decision step worth having over an agent step that asks. +- **A decision inside a loop.** Each pass asks again, which may be right (re-approve + each attempt) or maddening (three prompts for one deploy). The author should say + which; defaulting silently to either will be wrong half the time. +- **Blocking a run indefinitely** is correct behaviour and looks identical to a bug on + any dashboard that does not distinguish them. It has to be visibly *waiting on a + person*, not merely not running. diff --git a/docs/design-docs/repo-dependencies.md b/docs/design-docs/repo-dependencies.md new file mode 100644 index 000000000..6998640b5 --- /dev/null +++ b/docs/design-docs/repo-dependencies.md @@ -0,0 +1,78 @@ +# Repo Dependencies + +Record that one repo depends on another, and use it to *suggest* — never to derive. +Task #29. + +## Problem + +A project holds many repos. Nothing records how they relate. "The web client is +generated from the api contract" exists only in the head of whoever wired a particular +workflow, and has to be re-remembered every time a pipeline touches both. + +The pipeline machinery to act on it is already there: a workflow step names a repo, so +"regenerate the clients in `web` after the contract lands in `api`" is two steps and an +edge today. What is missing is the system knowing that the relationship exists, so it +can help you build that and notice when you have not. + +## Design + +### Declare only + +```sql +CREATE TABLE repo_dependencies ( + project_id TEXT NOT NULL, + repo_id TEXT NOT NULL, -- depends on + depends_on_repo_id TEXT NOT NULL, + kind TEXT, -- generated_from | consumes | vendors | … + note TEXT, + PRIMARY KEY (project_id, repo_id, depends_on_repo_id) +); +``` + +`kind` is an opaque label, like agent capabilities. A closed vocabulary invented now +will be wrong for the second project that uses this. + +### Suggest, never derive + +**This is the whole design decision.** The system may offer an edge; it must not create +one. + +A wrongly derived edge makes work wait forever on something that was never going to +happen, and the author never asked for it — so when it stalls, nothing points at the +declaration that caused it. That failure is unrecoverable in the sense that matters: +the person debugging it has no reason to suspect a repo relationship they may not know +exists. + +A suggestion is recoverable. It appears, you take it or you don't, and what runs is +what you agreed to. + +Concretely: + +- Authoring a step in `api` when `web` declares a dependency on it → offer to add a + `web` step downstream. +- Adding an edge that contradicts a declared dependency → say so, and allow it. The + declaration describes the repos, not the pipeline, and a template may legitimately + disagree. +- Reviewing a template → note declared dependencies with no corresponding step, as a + hint rather than an error. + +### Where it shows + +The project view, as a small graph — the repos and their arrows. That is also the +cheapest way for someone to notice a declaration is wrong, which matters because a +stale declaration that only ever produces suggestions is a nuisance, while one that +produced edges would be a fault. + +## Build order + +1. The table, CRUD, and the project view showing declared dependencies. +2. Suggestions in the workflow step editor. +3. The contradiction hint when an edge disagrees with a declaration. + +## Risks + +- **Derivation creeping in.** "It already knows, why not just add the edge" is the + obvious next thought, and the reason not to belongs in a comment where the suggestion + is generated, not only here. +- **Stale declarations.** They cost nothing while suggestions are all they produce. + That property is worth keeping deliberately, not by accident. diff --git a/docs/design-docs/task-assignment.md b/docs/design-docs/task-assignment.md new file mode 100644 index 000000000..ea5d83888 --- /dev/null +++ b/docs/design-docs/task-assignment.md @@ -0,0 +1,108 @@ +# Task Assignment + +Say what a task needs, not who should do it. Task #28. + +## Problem + +Every task carries `assigned_agent_id`, set at creation or inherited from whoever +launched the run. A workflow step may name a different agent, but it must name *one* — +there is no way to say "whichever agent can do this". + +The hard part is already built. `claim_next_ready` is a race-safe conditional UPDATE: +several agents can compete for the same work and exactly one wins. The mechanism for +pull already exists and is tested. What is missing is any notion of **what an agent can +do**, so there is nothing to match a task against, and the claim is therefore filtered +by name. + +Three consequences: + +- **A second agent does not spread load.** Work is addressed by name, so adding + capacity means re-addressing work. +- **Specialisation is hard-coded.** "Run the Rust step on one agent and the design step + on another" means naming both in the template, and re-editing every template when the + fleet changes. +- **A busy or dead agent blocks its queue.** Its tasks wait for it specifically, even + when another agent could do them. The reaper returns crashed work to `ready`, and + `ready` still means ready *for that agent*. + +## Design + +### Capabilities are declared, not inferred + +An agent declares what it can do — a set of labels. Not inferred from its tools, its +model, or its history: inference here is a guess that fails silently, and the failure +looks like work quietly not happening. + +``` +agent "main" capabilities: [rust, typescript, review] +agent "designer" capabilities: [design, review] +``` + +Labels are opaque strings the operator chooses. Resisting a taxonomy is deliberate — +every scheme invented up front is wrong for the fleet that eventually exists, and an +opaque label can be renamed without a migration. + +### A task requires, or names + +`assigned_agent_id` stays and stays the default. Naming an agent is push, it works +today, and it must keep working — a fleet of one has no use for any of this, and that +is the common case. + +Alongside it, a task may instead declare `requires`: a set of capabilities. Such a task +is **unassigned** and sits in a pool. Any agent whose capabilities cover the +requirement may claim it, and the existing conditional UPDATE decides who does, exactly +as it decides today. + +A workflow step gains the same choice — name an agent, or state a requirement. + +### The claim changes in one place + +Today: + +```sql +WHERE assigned_agent_id = ? AND status = 'ready' +``` + +Becomes: that, **or** unassigned with every required capability held by the claiming +agent. Claiming stamps `assigned_agent_id`, so a claimed task looks exactly like a +pushed one from that moment on — the attempt log, the reaper, and the failure budget +need no changes, and a reaped task returns to the pool it came from rather than to a +named agent that may be gone. + +### Nothing capable + +A task requiring capabilities no agent holds sits in the pool forever, which is the +"parked and silent" failure this codebase keeps rediscovering. It should be visible: +the ready sweep already reports `stalled` and `gated` holds, and this is a third — +*nothing in the fleet can do this*. + +It is also knowable earlier. A workflow step requiring a capability no agent declares +can be refused at **launch**, the way an unknown step reference already is. That does +not cover an agent being deleted mid-run, which is what the sweep report is for. + +## What this is not + +Not scheduling, priority, or fairness. Not load balancing beyond "whoever asks first +and can do it". Not routing by cost or model. Those are all reasonable later and all +need a working capability model first — and each one added before it would have to be +rebuilt on top of one. + +## Build order + +1. Capabilities on agents, declared and surfaced. +2. `requires` on tasks, and the claim query accepting an unassigned match. +3. `requires` on workflow steps, with launch-time refusal when nothing can satisfy it. +4. The sweep reporting tasks nothing in the fleet can claim. +5. UI: capabilities in agent config, requirement in the step editor, the unclaimable + hold shown on the board. + +## Risks + +- **A pool nobody watches.** An unassigned task that no agent can claim is invisible + unless the sweep says so. Step 4 is not optional polish. +- **Capability drift.** Labels are free text, so `rust` and `Rust` are two capabilities + and one of them matches nothing. Offer the existing set when authoring rather than + validating a taxonomy into existence. +- **Reaped tasks losing their pool.** A claimed task is stamped with an agent; the + reaper must return it to *unassigned* if that is where it came from, or a crashed + agent takes the work with it — the exact failure this feature exists to prevent. diff --git a/docs/design-docs/unattended-operation.md b/docs/design-docs/unattended-operation.md new file mode 100644 index 000000000..6c404f33a --- /dev/null +++ b/docs/design-docs/unattended-operation.md @@ -0,0 +1,149 @@ +# Unattended Operation + +What a pipeline engine needs before it can be left alone: a run that knows how it is +going, a ceiling on what it can spend, and something other than a person to start it. + +Covers tasks #25 (spend), #26 (run state), #27 (triggers). One document because they +are one problem — a spend ceiling has to park a run, a trigger has to report one, and +neither is possible while a run has no state. + +## Problem + +The graph engine is complete: sequential, parallel, fan-in, dynamic fan-out, bounded +loops with separate success and give-up paths, external gating. All of it verified +against a live model. And none of it can be left running. + +Three things are missing, and each was survivable until the other two arrived. + +### A run has no state + +`workflow_runs` is `(id, workflow_id, inputs, launched_by, created_at)`. There is no +status and no `finished_at`. Every caller that wants to know how a run is going loads +all its tasks and reduces them, differently. + +The question that actually matters is not "did it succeed" but **"is it stuck"**, and +nothing can answer it. A loop whose body task is permanently blocked never reaches +"all done", so no boundary fires and nothing announces the loop is wedged — reported +by the loops implementation as its own known gap. A task parked behind a gate that +will never open looks exactly like one waiting normally. The run simply stops making +progress, silently, and stays that way until somebody looks. + +There is also no way to cancel a run, and no way to delete one — two separate agents +left empty run rows behind because cleanup had no endpoint to call. + +### Nothing bounds what a run costs + +Fan-out branch count is **uncapped**. Loops cap at 25 iterations. They multiply. + +This was fine while a template compiled to a fixed number of tasks at launch. Both +fan-out and loops grow the graph *after* launch, so the size of a run is now decided at +run time — by model output. A scan step that hallucinates a 900-element array is a +900-task fan-out, each one a model call. Inside a loop, again per pass. + +The per-task failure budget bounds *failures per task*. Nothing bounds the number of +tasks a run creates or what the run costs in total. The instance already runs +unattended on a timer, so this is live, not theoretical. + +### Nothing can start a workflow but a person + +The only non-test caller of `WorkflowStore::launch` is the HTTP handler. So: + +- **No schedule.** Cron exists for agents and cannot launch a workflow. +- **No external trigger.** Gates can *wait* on CI; CI cannot *start* anything. The + gitops loop this was built for does not close. +- **No agent can launch one.** There is a `task_create` tool and no `launch_workflow` + tool. The cortex can file a card and cannot run a pipeline. + +That third is the sharpest: workflows are reusable procedures that the part of the +system meant to be autonomous cannot reuse. An agent deciding "this needs the full +release process" must re-derive the steps by hand every time. + +## Design + +### Run state + +``` +running tasks outstanding, progress recent +succeeded every task settled, no failure path taken +failed a task exhausted its budget, or a loop routed to on_exhausted +stuck nothing running, nothing runnable, not finished +cancelled a person stopped it +``` + +`stuck` is the one worth building the rest around. It is not derivable from any single +task — every task can look individually reasonable while the run as a whole cannot +advance. It is a property of the run, which is exactly why the run needs state of its +own rather than a reduction over tasks. + +Detection is the same shape as the existing reaper: a periodic pass asking whether a +run has any task in flight, any task promotable, and any unsettled gate that could +still open. None of the three, and not finished, means stuck — with the reason +attached, because "stuck" alone sends someone reading rows. + +`finished_at` and a terminal status close a run for good. A cancelled run marks its +unstarted tasks `cancelled` and leaves running ones to finish or be reaped, because +killing work mid-flight loses whatever it had done. + +**Notify on transition, not on state.** A run that goes to `stuck` or `failed` should +say so once. Polling a status nobody watches is the same as having no status. + +### Spend ceilings + +Two limits, both refusing rather than truncating: + +- **Fan-out width.** A cap on branch count, refused at expansion with the pointer and + the count found. A silently truncated fan-out is worse than a refusal, because the + downstream fan-in aggregates a subset and reports it as the whole. +- **Run task ceiling.** Total tasks a run may create. Reaching it parks the run + `stuck` with the reason, rather than continuing. + +A cost ceiling is the one people actually want, and it is harder: token accounting per +run has to survive retries and cross the agent boundary. The task ceiling is a crude +proxy available now; the cost ceiling should follow rather than block it. + +**Every limit is declared, and every refusal names it.** A run stopped by a ceiling +that does not say which ceiling is indistinguishable from a bug. + +### Triggers + +All three want the same thing — a launch identity that is not a person, which +`launched_by` already accommodates. + +- **Cron.** A schedule attached to a workflow, launching with a fixed input. Reuses the + existing scheduler; the input is a stored literal because a schedule cannot prompt. +- **Webhook.** An inbound endpoint mapping a payload to a run input via a pointer, the + same JSON-Pointer vocabulary bindings and gates already use. This is what closes the + gitops loop. +- **`launch_workflow` tool.** So the cortex can invoke a procedure rather than + reconstruct it. Bounded by the same filing depth and fan-out caps that already stop a + worker filing cards without limit — an agent that can launch a workflow that launches + a workflow needs the same recursion guard `MAX_FILING_DEPTH` provides. + +**A webhook is an unauthenticated inbound trigger for arbitrary pipeline execution.** +It must not ship before #1, and it needs its own shared secret regardless. Noted here +rather than left for whoever builds it to discover. + +## Build order + +1. **Fan-out width cap.** Smallest, and the only item on this list that is a live + hazard rather than a missing capability. +2. **Run state**, plus `finished_at`, cancel, and delete. Everything else reports + through it. +3. **Stuck detection** and notification on terminal transition. +4. **Run task ceiling**, parking the run `stuck` with its reason. +5. **`launch_workflow` tool** — highest value per unit of work, since it makes existing + pipelines reachable by the autonomous path. +6. **Cron trigger.** +7. **Webhook**, gated on authentication existing. + +## Risks + +- **Stuck detection that is wrong in either direction.** A false positive parks a + healthy run and trains people to ignore the signal; a false negative is the silence + we have now. The detector must consider gates that are still pollable, not only tasks. +- **Cancellation racing a claim.** Cancelling a task another agent is claiming is the + same race `claim_next_ready` already solves with a conditional update; use that + pattern rather than inventing another. +- **Ceilings that truncate instead of refusing.** A partial fan-out feeding a fan-in + that reports a subset as complete is a wrong answer delivered confidently, which is + worse than a stopped run. diff --git a/docs/design-docs/workflow-branching.md b/docs/design-docs/workflow-branching.md new file mode 100644 index 000000000..dea251a59 --- /dev/null +++ b/docs/design-docs/workflow-branching.md @@ -0,0 +1,227 @@ +# Workflow Branching + +A step declares a condition under which it runs. From that one primitive: either/or +branches, optional steps, guards, switches, and error routing — with merge behaviour +falling out of the input schema that was already there. + +## Problem + +Three questions about a step are currently squeezed into two mechanisms: + +| question | mechanism | +|---|---| +| what order does this run in? | dependency edges | +| is the outside world ready? | gates | +| **should this run at all?** | **nothing** | + +The third has no home, so it gets expressed as a gate — and *is CI green yet?* and +*should this branch run?* have the same predicate but **opposite** failure modes. +Waiting forever is correct for the first and a deadlock for the second. + +That is not hypothetical. The `deploy-or-rollback` workflow on the dev board has a +`rollback` step gated on `deploy` reporting `red`. Deploy reported `green` and is +`done`, so the gate can never open. `rollback` sits in the backlog permanently, +displayed as though it might still run. Put a merge step below both branches and +the pipeline **deadlocks outright**: every promotion path asks `p.status <> 'done'` +for every parent, and one parent is never going to be done. + +This is the same one-label-two-conditions bug that has now caused three separate +incidents in this codebase — the promote/re-block loop, the two meanings of +`backlog`, and the three states a gate can be in. + +Separately, and more mundanely: **a template cannot declare a gate at all**. +`task_gates` is keyed by `task_number`, and a template has only step keys. Branching +on the dev board had to be assembled by a script that launched the run, read back the +emitted task numbers, and POSTed gates against them. Branching is therefore a property +of one *run*, not of the template — launch it again and there are no branches. + +## The stance + +In most workflow engines conditionals must be rich, because the engine is the only +intelligence in the system: the DSL grows expressions, functions, templating, and +eventually a debugger. + +Here, **a step can be the condition.** "Does this need legal review?" is a task; a +model answers it and returns `{"needs_legal": true}`. The routing predicate only ever +reads a value something smarter already computed. + +So the predicate language stays permanently small — RFC 6901 pointer plus `equals` / +`any_of`, exactly what `task_gates` and `loop_until` already use. **The model reasons; +the graph routes.** Wanting `AND`/`OR`/arithmetic in a predicate is the signal that a +step should be computing it instead. + +This is a scope boundary, not a limitation to be lifted later. + +## Design + +### The primitive + +A step declares a **condition**: a predicate plus a **disposition** saying what a false +answer means. + +``` +wait — not yet. Poll again. (gate semantics, today's behaviour) +route — no. This step does not apply; it is settled and will never run. +``` + +Same table, same evaluator, one new field. The disposition is the entire fix. + +### Deriving the disposition + +`disposition` is nullable, and null means *derive*: + +- source is a **task output** and that task is **terminal** — `done` *or* `skipped` → + nothing can change the answer → **route** +- source is **http**, or the source task is still able to change → **wait** + +This is not a heuristic. It is a fact about whether the input can still change, which +is precisely the thing that distinguishes the two questions. + +`skipped` counts as terminal here, and it has to. A condition reading a branch that +was itself skipped would otherwise derive `wait` and hold forever — the same deadlock +this feature exists to remove, one level further down. Terminality is the property +that matters; `done` was an earlier draft of this sentence naming only half of it. It is right nearly always, +which is what makes it a good default. + +The override exists for what the derivation cannot see: an `http` gate polling a +decision endpoint that really is final, or a `task_output` condition that should hold +the whole pipeline rather than skip past it. Set at authoring time on the step, because +that is when the author knows. + +### `skipped` is a task status + +A seventh status, terminal. + +Terminal is load-bearing: a task that could un-skip would make "settled" meaningless +and put the sweep straight back into the promote/re-block territory it escaped. There +is deliberately no un-skip operation in v1. + +Dependency satisfaction changes from *done* to **settled**: + +```sql +-- before +AND p.status <> 'done' +-- after +AND p.status NOT IN ('done', 'skipped') +``` + +That predicate appears in **9 places** in `src/tasks/store.rs`. Nine hand-copied +literals is how drift happens, so it becomes one shared SQL fragment referenced +everywhere rather than nine edits. + +A skipped task records `skip_reason` — its own column, not `block_reason`. Overloading +the block fields would tie skip to the block machinery (recurrence limits, sticky +kinds, unblock) that has nothing to do with it, and that overloading is the exact +pattern this document exists to stop repeating. + +### Propagation falls out of the input schema + +**A binding whose source task was skipped resolves to *absent*.** The step's own +`input_schema` then decides whether that is legal: + +- the input is **required** and its branch skipped → the contract cannot be satisfied → + **the step skips too** +- the input is **optional** → the step runs without it + +So `required` in the JSON Schema **is** the join rule. "Needs both branches" and "needs +at least one" are expressed by writing the schema the author had to write anyway. No +`all`/`any` trigger-rule vocabulary, no new concept, and it reuses validation that is +already implemented and already enforced at claim time. + +Absent rather than `null` matters: `null` is a value a model will reason about ("the +review returned null…"), absent means the review never happened. + +Propagation is therefore **lazy** — evaluated when a task is considered for promotion, +not as an eager cascade when something is skipped. No separate pass, no ordering bugs, +and a task whose branch skipped is examined exactly once, at the moment the answer +matters. + +Mechanically this is a fourth outcome from `resolve_inputs`, alongside `NotRequired`, +`Resolved`, `Unresolved`, and `Pending`: + +``` +Unreachable { reason } — a required input's source is settled and produced nothing +``` + +A step that binds nothing from a skipped parent still runs. It declared a dependency on +that parent's *ordering*, not on its output, and honouring exactly what was declared is +the right behaviour. + +### Templates can declare gates + +The mechanical half of the original issue, and a prerequisite for all of the above. + +`workflow_step_gates` mirrors `workflow_step_bindings` — addressed by `step_key`, +compiled into real `task_gates` rows by `launch()`, with a `task_output` gate's +`source_step_key` translated into that step's compiled task number. The translation +problem and its solution are identical to bindings; this is well-trodden ground. + +```sql +CREATE TABLE workflow_step_gates ( + workflow_id TEXT NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, + step_key TEXT NOT NULL, + gate_key TEXT NOT NULL, -- author-named, so an edit is idempotent + kind TEXT NOT NULL, -- http | task_output + source_step_key TEXT, -- task_output: by name, resolved at launch + config TEXT NOT NULL, + label TEXT, + poll_interval_secs INTEGER NOT NULL DEFAULT 60, + disposition TEXT, -- NULL = derive; wait | route + PRIMARY KEY (workflow_id, step_key, gate_key) +); +``` + +`task_gates` gains the same nullable `disposition`. + +### What the poller does + +The gate poller already runs each tick. One addition: a gate whose disposition resolves +to `route` and whose predicate is decidedly false sets its task to `skipped` with a +reason naming the pointer and what was found there. + +Everything else — backoff, the error limit, the four `GateResult` states — is unchanged. +`erroring` still means *we could not tell*, and must never route a branch; being unable +to reach CI is not the same as CI saying no. + +## What you can express + +| shape | how | +|---|---| +| either/or | two steps off one parent, mutually exclusive conditions | +| optional step | one conditional step; skip and continue past it | +| guard | "only if approved", "only on the release branch" | +| switch | N steps, N conditions | +| error routing | pairs with the loop `on_exhausted` edge | + +## Build order + +1. **`skipped` status + settled-instead-of-done.** The risky one. Nine call sites + collapsed into one shared fragment, `can_transition` updated, `skip_reason` added. + Everything else is additive on top. +2. **`workflow_step_gates`** + compilation at launch + API. Purely mechanical. +3. **`disposition`**, derived with override, and the poller acting on `route`. +4. **`Unreachable` from `resolve_inputs`** and skip propagation via required inputs. +5. **UI**: skipped rendering, conditions in the step editor, conditional edges drawn + distinctly on the canvas. + +Steps 1–4 are independently testable and 1 is the only one that touches the scheduler's +core predicate. + +## Risks + +- **A seventh task status.** `@spacedrive/ai` knows five and its `TaskStatusIcon` + *crashes* on anything else; `TaskList` silently drops unknown ones. + `interface/src/components/tasks/boardColumns.ts` already contains the pattern for + handling this, and `designSystemTask.ts` is the adapter that must not pass `skipped` + through. Every surface rendering a status needs checking before `skipped` can reach + the UI. +- **The 9 sites.** Missing one produces a task that waits on a skipped parent forever — + the deadlock this work exists to remove, reintroduced somewhere less obvious. The + shared fragment is not tidiness; it is the mitigation. +- **`erroring` must not route.** An unreachable endpoint is our problem, not an answer. + Conflating it with a decided negative would skip branches because DNS failed. + +## Out of scope + +Expression languages. Computed predicates. Dynamic re-routing beyond skip. Un-skipping. +Any of these arriving would be evidence that a step should have been doing the thinking. diff --git a/docs/design-docs/worktree-provisioning.md b/docs/design-docs/worktree-provisioning.md new file mode 100644 index 000000000..b46b96f0e --- /dev/null +++ b/docs/design-docs/worktree-provisioning.md @@ -0,0 +1,156 @@ +# Worktree Provisioning + +A step says it needs its own checkout, and gets one. Parallel work in one repo stops +trampling itself. + +## Problem + +Tasks bind to a project, a repo, and optionally a worktree, and that binding is +enforced: `resolve_worker_working_dir` refuses any directory the sandbox allowlist does +not cover, and a task whose binding points somewhere disallowed parks for a human +rather than running in the wrong tree. Workflow steps carry `repo_id`, so multi-repo +pipelines — "regenerate the clients in `web` after the contract lands in `api`" — are +already two steps and an edge. + +Three things are missing, and they became urgent together: + +- **`auto_create_worktrees` is dead config.** Ten references across `config/types.rs`, + `config/load.rs`, `api/config.rs` and `projects/store.rs`, every one of them + plumbing. Nothing creates a worktree. It is settable in the UI and does nothing. +- **A workflow step has `repo_id` but no worktree.** A step can name a repo. It cannot + say "run in a checkout of your own". +- **Fan-out made this sharp.** Before dynamic fan-out, concurrent same-repo work was + something you had to go out of your way to build. Now `for_each` over five repos — + or five branches of one repo — is a single field, and every branch lands in the same + checkout. Two agents editing one working tree is not a race that produces a bad + result; it produces an incoherent one. + +Command steps (`command-steps.md`) sharpen it again: `bun run lint` in a tree another +step is mid-edit reports on a state that never existed. + +## Design + +### A step declares what it needs + +`workflow_steps.worktree_mode`: + +| mode | meaning | +|---|---| +| `inherit` | default. Use whatever the task binding already says — today's behaviour, unchanged | +| `per_run` | one worktree for this step, created at launch, shared by nothing else | +| `per_branch` | one worktree per fan-out branch, created when the fan-out expands | + +`inherit` being the default matters: every existing template keeps working, and a +pipeline that genuinely wants one shared checkout can still have one. + +`per_branch` on a step that is not a fan-out is a template error, refused at launch. +Silently degrading it to `per_run` would give an author a pipeline that looks isolated +and is not. + +### Naming + +Deterministic, derived from the run and the branch key: + +``` +/.worktrees/-[-] +``` + +Deterministic so it is greppable, re-derivable after a crash, and obvious in `git +worktree list` at three in the morning. Not random, because a leftover worktree with a +uuid name tells nobody what made it. + +The branch git creates follows the same scheme. `create_worktree` already falls back to +attaching an existing branch when `-b` fails with "already exists" — good behaviour for +a human retrying, and something to watch for here, since a re-launch reusing a branch +name would silently share history between two runs. Run-scoped names avoid it. + +### Base ref + +A step names what it forks from — a branch, a tag, a sha — defaulting to the repo's +current `HEAD`. Explicit because "whatever was checked out when the run happened" is +not reproducible, and a pipeline whose starting point drifts under it is one whose +failures cannot be explained afterwards. + +### The sandbox already covers it + +Worktrees live under the project root, and `refresh_project_paths` injects project +roots into the allowlist, so a provisioned worktree is inside the boundary without any +new allowlist plumbing. `resolve_worker_working_dir` then enforces it exactly as it +does today. + +This is worth stating precisely because it is the part most likely to be "helpfully" +widened later. **No new writable path should ever be added for a worktree.** A worktree +outside the project root would need one, which is the reason not to put it there. + +## Lifecycle — the part with a real decision in it + +### Never delete a dirty worktree + +Uncommitted work from a failed run is **evidence**, not garbage. It is the thing you +want when the question is "what did it actually do before it broke". + +The good news is that this is already the behaviour and the job is to keep it: +`remove_worktree` runs `git worktree remove` **without `--force`**, and git refuses on +a dirty tree. So the rule is a prohibition rather than a feature — + +> **Never pass `--force`. Never add a flag that would.** + +— and the reaper below simply lets git's refusal stand, records it, and moves on. + +### What gets reaped, and when + +On run completion, each worktree the run provisioned is offered for removal. Git +accepts the clean ones and refuses the rest. A refusal is not an error: it is recorded +against the run as "left behind, has uncommitted changes", and surfaced. + +Committed-but-unmerged work needs no special handling — `git worktree remove` deletes +the checkout, not the branch, so commits survive in the repo regardless. + +### Orphans + +A crash between "worktree created" and "run recorded it" leaves a directory nothing +owns. The deterministic naming scheme is what makes these findable: anything under +`.worktrees/` whose run id is not a live run is an orphan, and can be listed for a +person without ever being deleted automatically. + +Deliberately a *report*, not a sweep. The one thing worse than a stale worktree is a +background process that deletes directories. + +### Disk + +Worktrees are cheap in git terms and not free on disk, and a fan-out of fifty is fifty +checkouts. A cap on concurrent worktrees per run, refusing at expansion with a clear +message, beats discovering it as ENOSPC in the middle of a pipeline. + +## Failure modes + +| failure | response | +|---|---| +| `git worktree add` fails (dirty index, bad ref, disk) | block the task `capability` with git's own stderr — a person must fix the repo | +| base ref does not exist | refuse at **launch**, not at run time — it is knowable from the template | +| worktree removal refused | expected, recorded, not an error | +| two branches provisioning concurrently | run-scoped names make collision impossible by construction | + +## Build order + +1. `worktree_mode` on the step, plus launch validation (`per_branch` requires a + fan-out; base ref must resolve). +2. Provisioning at launch for `per_run`, and inside the fan-out expansion transaction + for `per_branch` — the same transaction that emits the branches, so a branch never + exists without its checkout. +3. Reaping on run completion, with git's refusal respected and recorded. +4. Orphan listing. +5. **Delete `auto_create_worktrees`.** It has never done anything. Replacing dead + config with real config is not the same as leaving both. +6. UI: worktree mode in the step editor, and the provisioned path on the run view — + "which checkout did this actually run in" is the first question anyone asks. + +## Risks + +- **`--force` creeping in.** Someone will hit a stuck reaper and reach for it. The + prohibition needs to be in a comment at the call site, not only in this document. +- **Provisioning outside the fan-out transaction.** A branch task that exists without + its worktree runs in the wrong directory — precisely the failure the cwd enforcement + was built to prevent, reintroduced one layer up. +- **Reaping while a task is still running.** A retry after a reap would find no + checkout. Reaping is keyed on the *run* being finished, not on the step. diff --git a/docs/docker.md b/docs/docker.md index b2425d671..58e55f711 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -65,16 +65,21 @@ Available environment variables: | Variable | Description | | ------------------------ | ---------------------- | -| `ANTHROPIC_API_KEY` | Anthropic API key | -| `OPENAI_API_KEY` | OpenAI API key | -| `OPENROUTER_API_KEY` | OpenRouter API key | +| `ANTHROPIC_API_KEY` | Anthropic API key (or `ANTHROPIC_AUTH_TOKEN` for OAuth tokens) | +| `ANTHROPIC_BASE_URL` | Override the Anthropic endpoint | +| `LITELLM_API_KEY` | LiteLLM proxy key — the supported path to every non-Anthropic provider | +| `LITELLM_BASE_URL` | Override the LiteLLM endpoint (default `http://localhost:4000/v1`) | | `DISCORD_BOT_TOKEN` | Discord bot token | | `SLACK_BOT_TOKEN` | Slack bot token | | `SLACK_APP_TOKEN` | Slack app token | +| `TELEGRAM_BOT_TOKEN` | Telegram bot token | | `BRAVE_SEARCH_API_KEY` | Brave Search API key | +| `SPACEBOT_MODEL` | Override channel, branch, and worker model | | `SPACEBOT_CHANNEL_MODEL` | Override channel model | | `SPACEBOT_WORKER_MODEL` | Override worker model | +`ANTHROPIC_API_KEY` and `LITELLM_API_KEY` are the only two variables that bootstrap a provider. Other provider variables (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `GEMINI_API_KEY`, …) were retired and are ignored with a warning — configure those providers with an explicit `[llm.provider.]` block in `config.toml` instead. + ### Config File Mount a config file into the volume for full control: @@ -91,8 +96,10 @@ docker run -d \ Config values can reference environment variables with `env:VAR_NAME`: ```toml -[llm] -anthropic_key = "env:ANTHROPIC_API_KEY" +[llm.provider.anthropic] +api_type = "anthropic" +base_url = "https://api.anthropic.com" +api_key = "env:ANTHROPIC_API_KEY" ``` See [config.md](config.md) for the full config reference. diff --git a/interface/bun.lock b/interface/bun.lock index ae78b619a..26dc8e607 100644 --- a/interface/bun.lock +++ b/interface/bun.lock @@ -41,7 +41,6 @@ "graphology-layout-forceatlas2": "^0.10.1", "graphology-types": "^0.24.8", "ogl": "^1.0.11", - "openapi-fetch": "^0.17", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.71.1", @@ -59,6 +58,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", + "bun-types": "^1.3.14", "openapi-typescript": "^7", "tailwindcss": "^4.2.2", "tailwindcss-radix": "^4.0.2", @@ -762,6 +762,8 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@26.1.2", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg=="], + "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -824,6 +826,8 @@ "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], @@ -1382,12 +1386,8 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="], - "openapi-fetch": ["openapi-fetch@0.17.0", "", { "dependencies": { "openapi-typescript-helpers": "^0.1.0" } }, "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig=="], - "openapi-typescript": ["openapi-typescript@7.13.0", "", { "dependencies": { "@redocly/openapi-core": "^1.34.6", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", "parse-json": "^8.3.0", "supports-color": "^10.2.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "typescript": "^5.x" }, "bin": { "openapi-typescript": "bin/cli.js" } }, "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ=="], - "openapi-typescript-helpers": ["openapi-typescript-helpers@0.1.0", "", {}, "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw=="], - "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -1660,6 +1660,8 @@ "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], diff --git a/interface/bunfig.toml b/interface/bunfig.toml new file mode 100644 index 000000000..48dedb712 --- /dev/null +++ b/interface/bunfig.toml @@ -0,0 +1,5 @@ +[test] +# The API client reads `window.__SPACEBOT_BASE_PATH` at module scope, so any +# test importing a module that reaches `@/api/client` needs a window to exist +# before the import graph evaluates. +preload = ["./test/setup.ts"] diff --git a/interface/package.json b/interface/package.json index d87a9bf3c..ee2c26c98 100644 --- a/interface/package.json +++ b/interface/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "NODE_OPTIONS='--max-old-space-size=6144' vite build", + "test": "bun test", "preview": "vite preview" }, "dependencies": { @@ -45,7 +46,6 @@ "graphology-layout-forceatlas2": "^0.10.1", "graphology-types": "^0.24.8", "ogl": "^1.0.11", - "openapi-fetch": "^0.17", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.71.1", @@ -63,6 +63,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", + "bun-types": "^1.3.14", "openapi-typescript": "^7", "tailwindcss": "^4.2.2", "tailwindcss-radix": "^4.0.2", diff --git a/interface/src/api/client-typed.ts b/interface/src/api/client-typed.ts deleted file mode 100644 index 0f4427559..000000000 --- a/interface/src/api/client-typed.ts +++ /dev/null @@ -1,24 +0,0 @@ -import createClient from "openapi-fetch"; -import type { paths } from "./schema"; - -let baseUrl = ""; - -export function setServerUrl(url: string) { - baseUrl = url; -} - -function getClient() { - return createClient({ - baseUrl: baseUrl ? `${baseUrl}/api` : "/api", - headers: getAuthHeaders(), - }); -} - -function getAuthHeaders(): Record { - const token = localStorage.getItem("spacebot_auth_token"); - return token ? { Authorization: `Bearer ${token}` } : {}; -} - -// Re-export the typed client for direct use -export { getClient }; -export type { paths }; diff --git a/interface/src/api/client.ts b/interface/src/api/client.ts index 51f4038f8..c64d38a31 100644 --- a/interface/src/api/client.ts +++ b/interface/src/api/client.ts @@ -71,8 +71,7 @@ export type { ProvidersResponse, ProviderUpdateResponse, ProviderModelTestResponse, - OpenAiOAuthBrowserStartResponse, - OpenAiOAuthBrowserStatusResponse, + ProviderEntry, ModelInfo, ModelsResponse, // Ingest @@ -289,24 +288,9 @@ export type ApiEvent = // -- Timeline types (discriminated union parts) -- -export interface AttachmentMeta { - id: string; - filename: string; - saved_filename: string; - mime_type: string; - size_bytes: number; -} +export type AttachmentMeta = Types.SavedAttachmentMeta; -export interface TimelineMessage { - type: "message"; - id: string; - role: "user" | "assistant"; - sender_name: string | null; - sender_id: string | null; - content: string; - created_at: string; - attachments?: AttachmentMeta[]; -} +export type TimelineMessage = Types.TimelineItem; export interface TimelineBranchRun { type: "branch_run"; @@ -337,6 +321,38 @@ async function fetchJson(path: string): Promise { return response.json(); } +/** + * A mutating call whose refusal text is the point. + * + * Every workflow endpoint answers a rejection with a plain-text body that names + * what is actually wrong — "step `draft` cannot wait for itself", "no step + * `nope` in this workflow", "the steps form a cycle and cannot be ordered: + * draft -> publish -> review". Those sentences are the whole diagnosis, and + * flattening them into "API error: 409" leaves the author with a number and no + * idea which edge to remove. So the body is the message; the status code is + * only the fallback for the rare empty one. + */ +async function mutateJson( + path: string, + method: string, + body?: unknown, +): Promise { + const response = await fetch(`${getApiBase()}${path}`, { + method, + ...(body === undefined + ? {} + : { + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(body), + }), + }); + if (!response.ok) { + const text = (await response.text().catch(() => "")).trim(); + throw new Error(text || `API error: ${response.status}`); + } + return (await response.json()) as T; +} + /** channel_id -> StatusBlockSnapshot */ export type ChannelStatusResponse = Record; @@ -445,15 +461,7 @@ export interface PromptCaptureResponse { // --- Memory helper types (extended beyond schema) --- // Extended MemoryType with additional values not yet in schema -export type MemoryType = - | "fact" - | "preference" - | "decision" - | "identity" - | "event" - | "observation" - | "goal" - | "todo"; +export type MemoryType = Types.MemoryType; export const MEMORY_TYPES: MemoryType[] = [ "fact", "preference", "decision", "identity", @@ -463,34 +471,13 @@ export const MEMORY_TYPES: MemoryType[] = [ export type MemorySort = "recent" | "importance" | "most_accessed"; // Extended MemoryItem with forgotten field (not yet in schema) -export interface MemoryItem { - id: string; - content: string; - memory_type: MemoryType; - importance: number; - created_at: string; - updated_at: string; - last_accessed_at: string; - access_count: number; - source: string | null; - channel_id: string | null; - forgotten: boolean; -} +export type MemoryItem = Types.Memory; -export interface MemoriesListResponse { - memories: MemoryItem[]; - total: number; -} +export type MemoriesListResponse = Types.MemoriesListResponse; -export interface MemorySearchResultItem { - memory: MemoryItem; - score: number; - rank: number; -} +export type MemorySearchResultItem = Types.MemorySearchResult; -export interface MemoriesSearchResponse { - results: MemorySearchResultItem[]; -} +export type MemoriesSearchResponse = Types.MemoriesSearchResponse; export interface MemoryGraphParams { limit?: number; @@ -541,18 +528,9 @@ export const CORTEX_EVENT_TYPES: CortexEventType[] = [ "observation_created", "health_check", ]; -export interface CortexEvent { - id: string; - event_type: CortexEventType; - summary: string; - details: Record | null; - created_at: string; -} +export type CortexEvent = Types.CortexEvent; -export interface CortexEventsResponse { - events: CortexEvent[]; - total: number; -} +export type CortexEventsResponse = Types.CortexEventsResponse; export interface CortexEventsParams { limit?: number; @@ -571,19 +549,9 @@ export type CortexChatSSEEvent = // -- Factory Presets -- -export interface PresetDefaults { - max_concurrent_workers: number | null; - max_turns: number | null; -} +export type PresetDefaults = Types.PresetDefaults; -export interface PresetMeta { - id: string; - name: string; - description: string; - icon: string; - tags: string[]; - defaults: PresetDefaults; -} +export type PresetMeta = Types.PresetMeta; export interface PresetsResponse { presets: PresetMeta[]; @@ -591,262 +559,76 @@ export interface PresetsResponse { // -- Config types with frontend-specific extensions -- -export interface RoutingSection { - channel: string; - branch: string; - worker: string; - compactor: string; - cortex: string; - voice: string; - rate_limit_cooldown_secs: number; - channel_thinking_effort: string; - branch_thinking_effort: string; - worker_thinking_effort: string; - compactor_thinking_effort: string; - cortex_thinking_effort: string; -} +export type RoutingSection = Types.RoutingSection; -export interface TuningSection { - max_concurrent_branches: number; - max_concurrent_workers: number; - max_turns: number; - branch_max_turns: number; - context_window: number; - history_backfill_count: number; -} +export type TuningSection = Types.TuningSection; -export interface CompactionSection { - background_threshold: number; - aggressive_threshold: number; - emergency_threshold: number; -} +export type CompactionSection = Types.CompactionSection; -export interface CortexSection { - tick_interval_secs: number; - worker_timeout_secs: number; - branch_timeout_secs: number; - circuit_breaker_threshold: number; - bulletin_interval_secs: number; - bulletin_max_words: number; - bulletin_max_turns: number; -} +export type CortexSection = Types.CortexSection; -export interface CoalesceSection { - enabled: boolean; - debounce_ms: number; - max_wait_ms: number; - min_messages: number; - multi_user_only: boolean; -} +export type CoalesceSection = Types.CoalesceSection; -export interface MemoryPersistenceSection { - enabled: boolean; - message_interval: number; -} +export type MemoryPersistenceSection = Types.MemoryPersistenceSection; -export interface BrowserSection { - enabled: boolean; - headless: boolean; - evaluate_enabled: boolean; - persist_session: boolean; - close_policy: "close_browser" | "close_tabs" | "detach"; -} +export type BrowserSection = Types.BrowserSection; -export interface ChannelSection { - listen_only_mode: boolean; -} +export type ChannelSection = Types.ChannelSection; -export interface SandboxSection { - mode: "enabled" | "disabled"; - writable_paths: string[]; -} +export type SandboxSection = Types.SandboxSection; +/** + * What containment the host is actually providing, per agent. + * + * Three separate facts on purpose. `mode` is what the operator asked for and + * `containment_active` is what is in force; `requested_but_inert` is the state + * where those two disagree, and it is the one a command step refuses to run in. + */ +export type SandboxContainmentStatus = Types.SandboxContainmentStatus; -export interface ProjectsSection { - use_worktrees: boolean; - worktree_name_template: string; - auto_create_worktrees: boolean; - auto_discover_repos: boolean; - auto_discover_worktrees: boolean; - disk_usage_warning_threshold: number; -} +export type ProjectsSection = Types.ProjectsSection; -export interface DiscordSection { - enabled: boolean; - allow_bot_messages: boolean; -} +export type DiscordSection = Types.DiscordSection; -export interface AgentConfigResponse { - routing: RoutingSection; - tuning: TuningSection; - compaction: CompactionSection; - cortex: CortexSection; - coalesce: CoalesceSection; - memory_persistence: MemoryPersistenceSection; - browser: BrowserSection; - channel: ChannelSection; - discord: DiscordSection; - sandbox: SandboxSection; - projects: ProjectsSection; -} +export type AgentConfigResponse = Types.AgentConfigResponse; // Partial update types - all fields are optional -export interface RoutingUpdate { - channel?: string; - branch?: string; - worker?: string; - compactor?: string; - cortex?: string; - voice?: string; - rate_limit_cooldown_secs?: number; - channel_thinking_effort?: string; - branch_thinking_effort?: string; - worker_thinking_effort?: string; - compactor_thinking_effort?: string; - cortex_thinking_effort?: string; -} +export type RoutingUpdate = Types.RoutingUpdate; -export interface TuningUpdate { - max_concurrent_branches?: number; - max_concurrent_workers?: number; - max_turns?: number; - branch_max_turns?: number; - context_window?: number; - history_backfill_count?: number; -} +export type TuningUpdate = Types.TuningUpdate; -export interface CompactionUpdate { - background_threshold?: number; - aggressive_threshold?: number; - emergency_threshold?: number; -} +export type CompactionUpdate = Types.CompactionUpdate; -export interface CortexUpdate { - tick_interval_secs?: number; - worker_timeout_secs?: number; - branch_timeout_secs?: number; - circuit_breaker_threshold?: number; - bulletin_interval_secs?: number; - bulletin_max_words?: number; - bulletin_max_turns?: number; -} +export type CortexUpdate = Types.CortexUpdate; -export interface CoalesceUpdate { - enabled?: boolean; - debounce_ms?: number; - max_wait_ms?: number; - min_messages?: number; - multi_user_only?: boolean; -} +export type CoalesceUpdate = Types.CoalesceUpdate; -export interface MemoryPersistenceUpdate { - enabled?: boolean; - message_interval?: number; -} +export type MemoryPersistenceUpdate = Types.MemoryPersistenceUpdate; -export interface BrowserUpdate { - enabled?: boolean; - headless?: boolean; - evaluate_enabled?: boolean; - persist_session?: boolean; - close_policy?: "close_browser" | "close_tabs" | "detach"; -} +export type BrowserUpdate = Types.BrowserUpdate; -export interface ChannelUpdate { - listen_only_mode?: boolean; -} +export type ChannelUpdate = Types.ChannelUpdate; -export interface SandboxUpdate { - mode?: "enabled" | "disabled"; - writable_paths?: string[]; -} +export type SandboxUpdate = Types.SandboxUpdate; -export interface ProjectsUpdate { - use_worktrees?: boolean; - worktree_name_template?: string; - auto_create_worktrees?: boolean; - auto_discover_repos?: boolean; - auto_discover_worktrees?: boolean; - disk_usage_warning_threshold?: number; -} +export type ProjectsUpdate = Types.ProjectsUpdate; -export interface DiscordUpdate { - allow_bot_messages?: boolean; -} +export type DiscordUpdate = Types.DiscordUpdate; -export interface AgentConfigUpdateRequest { - agent_id: string; - routing?: RoutingUpdate; - tuning?: TuningUpdate; - compaction?: CompactionUpdate; - cortex?: CortexUpdate; - coalesce?: CoalesceUpdate; - memory_persistence?: MemoryPersistenceUpdate; - browser?: BrowserUpdate; - channel?: ChannelUpdate; - discord?: DiscordUpdate; - sandbox?: SandboxUpdate; - projects?: ProjectsUpdate; -} +export type AgentConfigUpdateRequest = Types.AgentConfigUpdateRequest; // -- Cron Types -- -export interface CronJobWithStats { - id: string; - prompt: string; - cron_expr: string | null; - interval_secs: number; - delivery_target: string; - enabled: boolean; - run_once: boolean; - active_hours: [number, number] | null; - timeout_secs: number | null; - execution_success_count: number; - execution_failure_count: number; - delivery_success_count: number; - delivery_failure_count: number; - delivery_skipped_count: number; - last_executed_at: string | null; -} +export type CronJobWithStats = Types.CronJobWithStats; -export interface CronExecutionEntry { - id: string; - cron_id: string | null; - executed_at: string; - success: boolean; - execution_succeeded: boolean; - delivery_attempted: boolean; - delivery_succeeded: boolean | null; - result_summary: string | null; - execution_error: string | null; - delivery_error: string | null; -} +export type CronExecutionEntry = Types.CronExecutionEntry; -export interface CronListResponse { - jobs: CronJobWithStats[]; - timezone: string; -} +export type CronListResponse = Types.CronListResponse; -export interface CronExecutionsResponse { - executions: CronExecutionEntry[]; -} +export type CronExecutionsResponse = Types.CronExecutionsResponse; -export interface CronActionResponse { - success: boolean; - message: string; -} +export type CronActionResponse = Types.CronActionResponse; -export interface CreateCronRequest { - id: string; - prompt: string; - cron_expr?: string; - interval_secs?: number; - delivery_target: string; - active_start_hour?: number; - active_end_hour?: number; - enabled: boolean; - run_once: boolean; - timeout_secs?: number; -} +export type CreateCronRequest = Types.CreateCronRequest; export interface CronExecutionsParams { cron_id?: string; @@ -855,21 +637,9 @@ export interface CronExecutionsParams { // -- Update Types -- -export type Deployment = "docker" | "hosted" | "native"; - -export interface UpdateStatus { - current_version: string; - latest_version: string | null; - update_available: boolean; - release_url: string | null; - release_notes: string | null; - deployment: Deployment; - can_apply: boolean; - cannot_apply_reason: string | null; - docker_image: string | null; - checked_at: string | null; - error: string | null; -} +export type Deployment = Types.Deployment; + +export type UpdateStatus = Types.UpdateStatus; export interface UpdateApplyResponse { status: "updating" | "error"; @@ -878,212 +648,160 @@ export interface UpdateApplyResponse { // -- Global Settings Types -- -export interface OpenCodePermissions { - edit: string; - bash: string; - webfetch: string; -} +export type OpenCodePermissions = Types.OpenCodePermissionsResponse; -export interface OpenCodeSettings { - enabled: boolean; - path: string; - max_servers: number; - server_startup_timeout_secs: number; - max_restart_retries: number; - permissions: OpenCodePermissions; -} +export type OpenCodeSettings = Types.OpenCodeSettingsResponse; -export interface OpenCodeSettingsUpdate { - enabled?: boolean; - path?: string; - max_servers?: number; - server_startup_timeout_secs?: number; - max_restart_retries?: number; - permissions?: Partial; -} +export type OpenCodeSettingsUpdate = Types.OpenCodeSettingsUpdate; -export interface GlobalSettingsUpdate { - company_name?: string; - brave_search_key?: string | null; - api_enabled?: boolean; - api_port?: number; - api_bind?: string; - worker_log_mode?: string; - opencode?: OpenCodeSettingsUpdate; -} +export type GlobalSettingsUpdate = Types.GlobalSettingsUpdate; // -- Skills Types -- -export interface SkillInfo { - name: string; - description: string; - file_path: string; - base_dir: string; - source: "builtin" | "instance" | "workspace"; - source_repo?: string; -} +export type SkillInfo = Types.SkillInfo; -export interface SkillsListResponse { - skills: SkillInfo[]; -} +export type SkillsListResponse = Types.SkillsListResponse; -export interface InstallSkillRequest { - agent_id: string; - spec: string; - instance?: boolean; -} +export type InstallSkillRequest = Types.InstallSkillRequest; -export interface InstallSkillResponse { - installed: string[]; -} +export type InstallSkillResponse = Types.InstallSkillResponse; -export interface RemoveSkillRequest { - agent_id: string; - name: string; -} +export type RemoveSkillRequest = Types.RemoveSkillRequest; -export interface RemoveSkillResponse { - success: boolean; - path: string | null; -} +export type RemoveSkillResponse = Types.RemoveSkillResponse; // -- Skills Registry Types (skills.sh) -- export type RegistryView = "all-time" | "trending" | "hot"; -export interface RegistrySkill { - source: string; - skillId: string; - name: string; - installs: number; - description?: string; - id?: string; -} +export type RegistrySkill = Types.RegistrySkill; -export interface RegistryBrowseResponse { - skills: RegistrySkill[]; - has_more: boolean; - total?: number; -} +export type RegistryBrowseResponse = Types.RegistryBrowseResponse; -export interface RegistrySearchResponse { - skills: RegistrySkill[]; - query: string; - count: number; -} +export type RegistrySearchResponse = Types.RegistrySearchResponse; -export interface SkillContentResponse { - name: string; - description: string; - content: string; - file_path: string; - base_dir: string; - source: string; - source_repo?: string; -} +export type SkillContentResponse = Types.SkillContentResponse; -export interface UploadSkillResponse { - installed: string[]; -} +export type UploadSkillResponse = Types.UploadSkillResponse; // -- Task Types -- - -export type TaskStatus = "pending_approval" | "backlog" | "ready" | "in_progress" | "done"; -export type TaskPriority = "critical" | "high" | "medium" | "low"; - -export interface TaskSubtask { - title: string; - completed: boolean; -} - -export interface TaskItem { - id: string; - task_number: number; - title: string; - description?: string; - status: TaskStatus; - priority: TaskPriority; - owner_agent_id: string; - assigned_agent_id: string; - subtasks: TaskSubtask[]; - metadata: Record; - source_memory_id?: string; - worker_id?: string; - created_by: string; - approved_at?: string; - approved_by?: string; - created_at: string; - updated_at: string; - completed_at?: string; -} - -export interface TaskListResponse { - tasks: TaskItem[]; -} - -export interface TaskResponse { - task: TaskItem; -} - -export interface TaskActionResponse { - success: boolean; - message: string; -} - -export interface CreateTaskRequest { - owner_agent_id: string; - assigned_agent_id?: string; - title: string; - description?: string; - status?: TaskStatus; - priority?: TaskPriority; - subtasks?: TaskSubtask[]; - metadata?: Record; - source_memory_id?: string; - created_by?: string; -} - -export interface UpdateTaskRequest { - title?: string; - description?: string; - status?: TaskStatus; - priority?: TaskPriority; - assigned_agent_id?: string; - subtasks?: TaskSubtask[]; - metadata?: Record; - complete_subtask?: number; - worker_id?: string; - approved_by?: string; -} +// +// Aliased straight from the generated OpenAPI schema rather than hand-written. +// These used to be duplicated by hand here, which `check-typegen` cannot catch: +// it only diffs `schema.d.ts` against the Rust, so a local redeclaration could +// drift from the server indefinitely and the build stayed green. +// +// `TaskItem` is kept as the name most call sites use. +export type Task = Types.Task; +export type TaskStatus = Types.TaskStatus; +export type TaskPriority = Types.TaskPriority; +export type TaskSubtask = Types.TaskSubtask; +export type TaskRun = Types.TaskRun; +export type TaskRunOutcome = Types.TaskRunOutcome; +export type TaskRunsResponse = Types.TaskRunsResponse; +export type TaskListResponse = Types.TaskListResponse; +export type TaskResponse = Types.TaskResponse; +export type TaskActionResponse = Types.TaskActionResponse; +export type BlockKind = Types.BlockKind; +export type TaskEdgeSummary = Types.TaskEdgeSummary; +export type TaskDependenciesResponse = Types.TaskDependenciesResponse; +export type TaskTransition = Types.TaskTransition; +export type TaskTransitionsResponse = Types.TaskTransitionsResponse; +export type TaskGraph = Types.TaskGraph; +export type TaskGraphEdge = Types.TaskGraphEdge; +export type TaskGate = Types.TaskGate; +export type TaskGatesResponse = Types.TaskGatesResponse; +export type GateKind = Types.GateKind; +export type GateResult = Types.GateResult; +/** What a *false* answer means: `wait` holds the task, `route` skips it. */ +export type GateDisposition = Types.GateDisposition; +export type ContractProblem = Types.ContractProblem; +export type ContractSide = Types.ContractSide; +export type TaskInputBinding = Types.TaskInputBinding; +export type TaskContractResponse = Types.TaskContractResponse; +export type TaskProvenanceResponse = Types.TaskProvenanceResponse; +export type TaskItem = Types.Task; + +export type CreateTaskRequest = Types.CreateTaskRequest; + +export type UpdateTaskRequest = Types.UpdateTaskRequest; + +// -- Workflow Types -- +// +// A workflow is the reusable template; a run is one launch of it, compiled into +// real tasks with real dependency edges. Same rule as above: everything with a +// server counterpart is aliased from the generated schema, never redeclared. +export type Workflow = Types.Workflow; +export type WorkflowStep = Types.WorkflowStep; +export type WorkflowEdge = Types.WorkflowEdge; +export type StepBinding = Types.StepBinding; +/** A condition declared on a step, compiled into a `TaskGate` at launch. */ +export type StepGate = Types.StepGate; +export type BindingSource = Types.BindingSource; +/** + * Whether a step runs a model or a process: `agent | command`. + * + * `agent` is the default and describes every step that predates command steps, + * which is why it is optional on the wire. + */ +export type StepKind = Types.StepKind; +/** + * Where a step gets its working directory: `inherit | per_run | per_branch`. + * + * `inherit` is today's behaviour — whatever the task binding already says. + */ +export type WorktreeMode = Types.WorktreeMode; +/** Which way out of a loop an edge — and the task behind it — is on. */ +export type LoopArm = Types.LoopArm; +export type LoopResolution = Types.LoopResolution; +export type WorkflowListResponse = Types.WorkflowListResponse; +export type WorkflowResponse = Types.WorkflowResponse; +export type WorkflowDetailResponse = Types.WorkflowDetailResponse; +export type WorkflowActionResponse = Types.WorkflowActionResponse; +export type WorkflowRun = Types.WorkflowRun; +/** + * How a run is going: `running | succeeded | failed | stuck | cancelled`. + * + * A property of the run, not a reduction over its tasks. `stuck` in particular + * is not derivable from any single task — every card in a wedged run looks + * individually reasonable — which is why the run carries a status of its own. + */ +export type RunStatus = Types.RunStatus; +export type RunDetailResponse = Types.RunDetailResponse; +export type RunListResponse = Types.RunListResponse; +export type CancelRunRequest = Types.CancelRunRequest; +export type CancelRunResponse = Types.CancelRunResponse; + +export type SaveWorkflowRequest = Types.SaveWorkflowRequest; +export type SaveStepRequest = Types.SaveStepRequest; +export type SaveBindingRequest = Types.SaveBindingRequest; +export type SaveStepGateRequest = Types.SaveStepGateRequest; +export type StepEdgeRequest = Types.StepEdgeRequest; +export type LaunchRequest = Types.LaunchRequest; +export type LaunchResponse = Types.LaunchResponse; // -- Notification Types -- -export type NotificationKind = "task_approval" | "worker_failed" | "cortex_observation"; +/** + * The kinds the inbox can style. + * + * `workflow_run_stopped` is its own kind rather than a `worker_failed`: that one + * is about a process dying and is answered by looking at the process, while this + * is a pipeline that will not continue on its own and is answered by looking at + * the run. Filtering the inbox for one must not drag in the other. + */ +export type NotificationKind = + | "task_approval" + | "worker_failed" + | "cortex_observation" + | "workflow_run_stopped"; export type NotificationSeverity = "info" | "warn" | "error"; -export interface NotificationItem { - id: string; - kind: NotificationKind; - severity: NotificationSeverity; - title: string; - body?: string; - agent_id?: string; - related_entity_type?: string; - related_entity_id?: string; - action_url?: string; - metadata?: string; - created_at: string; - read_at?: string; - dismissed_at?: string; -} +export type NotificationItem = Types.Notification; -export interface NotificationsResponse { - notifications: NotificationItem[]; -} +export type NotificationsResponse = Types.NotificationsResponse; -export interface UnreadCountResponse { - count: number; -} +export type UnreadCountResponse = Types.UnreadCountResponse; export interface NotificationCreatedEvent { type: "notification_created"; @@ -1099,97 +817,21 @@ export interface NotificationUpdatedEvent { // -- Messaging / Bindings Types -- -export interface BindingInfo { - agent_id: string; - channel: string; - adapter: string | null; - guild_id: string | null; - workspace_id: string | null; - chat_id: string | null; - channel_ids: string[]; - require_mention: boolean; - dm_allowed_users: string[]; -} +export type BindingInfo = Types.BindingResponse; -export interface BindingsListResponse { - bindings: BindingInfo[]; -} +export type BindingsListResponse = Types.BindingsListResponse; -export interface CreateBindingRequest { - agent_id: string; - channel: string; - adapter?: string; - guild_id?: string; - workspace_id?: string; - chat_id?: string; - channel_ids?: string[]; - require_mention?: boolean; - dm_allowed_users?: string[]; - platform_credentials?: { - discord_token?: string; - slack_bot_token?: string; - slack_app_token?: string; - telegram_token?: string; - email_imap_host?: string; - email_imap_port?: number; - email_imap_username?: string; - email_imap_password?: string; - email_smtp_host?: string; - email_smtp_port?: number; - email_smtp_username?: string; - email_smtp_password?: string; - email_from_address?: string; - email_from_name?: string; - twitch_username?: string; - twitch_oauth_token?: string; - twitch_client_id?: string; - twitch_client_secret?: string; - twitch_refresh_token?: string; - }; -} +export type CreateBindingRequest = Types.CreateBindingRequest; -export interface CreateBindingResponse { - success: boolean; - restart_required: boolean; - message: string; -} +export type CreateBindingResponse = Types.CreateBindingResponse; -export interface UpdateBindingRequest { - original_agent_id: string; - original_channel: string; - original_adapter?: string; - original_guild_id?: string; - original_workspace_id?: string; - original_chat_id?: string; - agent_id: string; - channel: string; - adapter?: string; - guild_id?: string; - workspace_id?: string; - chat_id?: string; - channel_ids?: string[]; - require_mention?: boolean; - dm_allowed_users?: string[]; -} +export type UpdateBindingRequest = Types.UpdateBindingRequest; -export interface UpdateBindingResponse { - success: boolean; - message: string; -} +export type UpdateBindingResponse = Types.UpdateBindingResponse; -export interface DeleteBindingRequest { - agent_id: string; - channel: string; - adapter?: string; - guild_id?: string; - workspace_id?: string; - chat_id?: string; -} +export type DeleteBindingRequest = Types.DeleteBindingRequest; -export interface DeleteBindingResponse { - success: boolean; - message: string; -} +export type DeleteBindingResponse = Types.DeleteBindingResponse; // -- Links & Topology Types -- @@ -1207,168 +849,67 @@ export interface LinksResponse { links: AgentLinkResponse[]; } -export interface CreateHumanRequest { - id: string; - display_name?: string; - role?: string; - bio?: string; - description?: string; - discord_id?: string; - telegram_id?: string; - slack_id?: string; - email?: string; -} +export type CreateHumanRequest = Types.CreateHumanRequest; -export interface UpdateHumanRequest { - display_name?: string; - role?: string; - bio?: string; - description?: string; - discord_id?: string; - telegram_id?: string; - slack_id?: string; - email?: string; -} +export type UpdateHumanRequest = Types.UpdateHumanRequest; -export interface CreateGroupRequest { - name: string; - agent_ids?: string[]; - color?: string; -} +export type CreateGroupRequest = Types.CreateGroupRequest; -export interface UpdateGroupRequest { - name?: string; - agent_ids?: string[]; - color?: string; -} +export type UpdateGroupRequest = Types.UpdateGroupRequest; -export interface CreateLinkRequest { - from: string; - to: string; - direction?: LinkDirection; - kind?: LinkKind; -} +export type CreateLinkRequest = Types.CreateLinkRequest; -export interface UpdateLinkRequest { - direction?: LinkDirection; - kind?: LinkKind; -} +export type UpdateLinkRequest = Types.UpdateLinkRequest; // -- Projects Types -- -export type ProjectStatus = "active" | "archived"; +export type ProjectStatus = Types.ProjectStatus; -export interface Project { - id: string; - name: string; - description: string; - icon: string; - tags: string[]; - root_path: string; - logo_path: string | null; - settings: Record; - status: ProjectStatus; - sort_order: number; - created_at: string; - updated_at: string; -} +export type Project = Types.Project; -export interface ProjectRepo { - id: string; - project_id: string; - name: string; - path: string; - remote_url: string; - default_branch: string; - current_branch: string | null; - description: string; - disk_usage_bytes: number | null; - created_at: string; - updated_at: string; -} +export type ProjectRepo = Types.ProjectRepo; -export interface ProjectWorktree { - id: string; - project_id: string; - repo_id: string; - name: string; - path: string; - branch: string; - created_by: string; - disk_usage_bytes: number | null; - created_at: string; - updated_at: string; -} +export type ProjectWorktree = Types.ProjectWorktree; -export interface ProjectWorktreeWithRepo extends ProjectWorktree { - repo_name: string; -} +export type ProjectWorktreeWithRepo = Types.ProjectWorktreeWithRepo; + +/** + * A checkout under `.worktrees/` that nothing alive owns. + * + * A report, not a sweep. There is deliberately no endpoint that deletes one — + * the one thing worse than a stale worktree is a background process that + * removes directories — so nothing here should offer to clean them up. + */ +export type OrphanWorktree = Types.OrphanWorktree; + +export type OrphanWorktreesResponse = Types.OrphanWorktreesResponse; /** GET /agents/projects response */ -export interface ProjectListResponse { - projects: Project[]; -} +export type ProjectListResponse = Types.ProjectListResponse; /** GET /agents/projects/:id response — project fields are flattened */ -export interface ProjectWithRelations extends Project { - repos: ProjectRepo[]; - worktrees: ProjectWorktreeWithRepo[]; -} +export type ProjectWithRelations = Types.ProjectWithRelations; export interface ProjectActionResponse { success: boolean; message: string; } -export interface DiskUsageEntry { - name: string; - bytes: number; - is_dir: boolean; -} +export type DiskUsageEntry = Types.DiskUsageEntry; -export interface DiskUsageResponse { - total_bytes: number; - entries: DiskUsageEntry[]; -} +export type DiskUsageResponse = Types.DiskUsageResponse; -export interface CreateProjectRequest { - name: string; - description?: string; - icon?: string; - tags?: string[]; - root_path: string; - settings?: Record; - auto_discover?: boolean; -} +export type CreateProjectRequest = Types.CreateProjectRequest; -export interface UpdateProjectRequest { - name?: string; - description?: string; - icon?: string; - tags?: string[]; - logo_path?: string | null; - settings?: Record; - status?: ProjectStatus; -} +export type UpdateProjectRequest = Types.UpdateProjectRequest; -export interface CreateRepoRequest { - name: string; - path: string; - remote_url?: string; - default_branch?: string; - description?: string; -} +export type CreateRepoRequest = Types.CreateRepoRequest; -export interface CreateWorktreeRequest { - repo_id: string; - branch: string; - worktree_name?: string; - start_point?: string; -} +export type CreateWorktreeRequest = Types.CreateWorktreeRequest; // -- Secrets Types -- -export type SecretCategory = "system" | "tool"; +export type SecretCategory = Types.SecretCategory; export type StoreState = "unencrypted" | "locked" | "unlocked"; export interface SecretStoreStatus { @@ -1380,33 +921,15 @@ export interface SecretStoreStatus { platform_managed: boolean; } -export interface SecretListItem { - name: string; - category: SecretCategory; - created_at: string; - updated_at: string; -} +export type SecretListItem = Types.SecretListItem; -export interface SecretListResponse { - secrets: SecretListItem[]; -} +export type SecretListResponse = Types.SecretListResponse; -export interface PutSecretResponse { - name: string; - category: SecretCategory; - reload_required: boolean; - message: string; -} +export type PutSecretResponse = Types.PutSecretResponse; -export interface DeleteSecretResponse { - deleted: string; - warning?: string; -} +export type DeleteSecretResponse = Types.DeleteSecretResponse; -export interface EncryptResponse { - master_key: string; - message: string; -} +export type EncryptResponse = Types.EncryptResponse; export interface UnlockResponse { state: string; @@ -1414,17 +937,9 @@ export interface UnlockResponse { message: string; } -export interface MigrationItem { - config_key: string; - secret_name: string; - category: SecretCategory; -} +export type MigrationItem = Types.MigrationItem; -export interface MigrateResponse { - migrated: MigrationItem[]; - skipped: string[]; - message: string; -} +export type MigrateResponse = Types.MigrateResponse; export const api = { status: () => fetchJson("/status"), @@ -1564,7 +1079,7 @@ export const api = { return response.json() as Promise<{ success: boolean; agent_id: string; message: string }>; }, - updateAgent: async (agentId: string, update: { display_name?: string; role?: string; gradient_start?: string; gradient_end?: string }) => { + updateAgent: async (agentId: string, update: { display_name?: string; role?: string; gradient_start?: string; gradient_end?: string; capabilities?: string[] }) => { const response = await fetch(`${getApiBase()}/agents`, { method: "PUT", headers: { "Content-Type": "application/json" }, @@ -1641,7 +1156,12 @@ export const api = { return fetchJson(`/agents/cron/executions?${search}`); }, - createCronJob: async (agentId: string, request: CreateCronRequest) => { + // `agent_id` is supplied by the caller's route context and injected here, + // so the request object itself never carries it. + createCronJob: async ( + agentId: string, + request: Omit, + ) => { const response = await fetch(`${getApiBase()}/agents/cron`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -1702,66 +1222,40 @@ export const api = { // Provider management providers: () => fetchJson("/providers"), - updateProvider: async (provider: string, apiKey: string, model: string, baseUrl?: string, apiVersion?: string, deployment?: string) => { + updateProvider: async ( + provider: string, + apiKey: string, + model: string, + apiType: string, + baseUrl?: string, + ) => { const response = await fetch(`${getApiBase()}/providers`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider, api_key: apiKey, model, base_url: baseUrl, api_version: apiVersion, deployment }), + body: JSON.stringify({ provider, api_key: apiKey, model, api_type: apiType, base_url: baseUrl }), }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } return response.json() as Promise; }, - testProviderModel: async (provider: string, apiKey: string, model: string, baseUrl?: string, apiVersion?: string, deployment?: string) => { + testProviderModel: async ( + provider: string, + apiKey: string, + model: string, + apiType: string, + baseUrl?: string, + ) => { const response = await fetch(`${getApiBase()}/providers/test-model`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider, api_key: apiKey, model, base_url: baseUrl, api_version: apiVersion, deployment }), + body: JSON.stringify({ provider, api_key: apiKey, model, api_type: apiType, base_url: baseUrl }), }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } return response.json() as Promise; }, - getProviderConfig: async (provider: string, options?: { signal?: AbortSignal }) => { - const response = await fetch(`${getApiBase()}/providers/${provider}/config`, { - method: "GET", - signal: options?.signal, - }); - if (!response.ok) { - throw new Error(`API error: ${response.status}`); - } - return response.json() as Promise<{ - success: boolean; - message: string; - base_url?: string | null; - api_version?: string | null; - deployment?: string | null; - }>; - }, - startOpenAiOAuthBrowser: async (params: {model: string}) => { - const response = await fetch(`${getApiBase()}/providers/openai/browser-oauth/start`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model: params.model, - }), - }); - if (!response.ok) { - throw new Error(`API error: ${response.status}`); - } - return response.json() as Promise; - }, - openAiOAuthBrowserStatus: async (state: string) => { - const response = await fetch( - `${getApiBase()}/providers/openai/browser-oauth/status?state=${encodeURIComponent(state)}`, - ); - if (!response.ok) { - throw new Error(`API error: ${response.status}`); - } - return response.json() as Promise; - }, removeProvider: async (provider: string) => { const response = await fetch(`${getApiBase()}/providers/${encodeURIComponent(provider)}`, { method: "DELETE", @@ -2334,6 +1828,302 @@ export const api = { if (!response.ok) throw new Error(`API error: ${response.status}`); return response.json() as Promise; }, + /** + * The whole connected component this task belongs to. + * + * Undirected: siblings of a fan-out are only reachable through the parent + * they share, and "what else is running beside this" is most of the question + * somebody has when they open one branch of three. + * + * Unlike the run view this owes nothing to a workflow template — the edges + * are the task edges themselves — so it still draws after the template has + * been deleted, and it draws graphs that never came from one. + */ + getTaskGraph: (taskNumber: number) => + fetchJson(`/tasks/${taskNumber}/graph`), + /** Per-attempt execution log for a task, oldest first. */ + listTaskRuns: (taskNumber: number) => + fetchJson(`/tasks/${taskNumber}/runs`), + /** Resolves live, so it shows what the task would get if it ran now. */ + getTaskContract: (taskNumber: number) => + fetchJson(`/tasks/${taskNumber}/contract`), + /** Where this card came from, and what it filed. */ + getTaskProvenance: (taskNumber: number) => + fetchJson(`/tasks/${taskNumber}/provenance`), + /** + * Declare what a task must produce (and may require). + * + * A human defines the *shape*; only a worker ever writes the *values*, via + * `task_complete`. Setting an output schema is what makes that submission + * checked rather than taken on trust. + */ + setTaskContract: async ( + taskNumber: number, + body: {input_schema?: unknown; output_schema?: unknown}, + ) => { + const response = await fetch( + `${getApiBase()}/tasks/${taskNumber}/contract`, + { + method: "PUT", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(body), + }, + ); + if (!response.ok) throw new Error(`API error: ${response.status}`); + return (await response.json()) as TaskContractResponse; + }, + /** + * Wire one input to where its value comes from. + * + * Keyed by input key rather than by an id, so setting the same key twice + * rewires it instead of leaving two bindings fighting over one input. + * + * Exactly one source is meaningful: an upstream task's output at a JSON + * Pointer, or a literal. The server rejects a body carrying neither. + */ + setTaskBinding: async ( + taskNumber: number, + inputKey: string, + body: { + source_task_number?: number; + source_pointer?: string; + literal_value?: unknown; + }, + ) => { + const response = await fetch( + `${getApiBase()}/tasks/${taskNumber}/bindings/${encodeURIComponent(inputKey)}`, + { + method: "PUT", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(body), + }, + ); + if (!response.ok) { + // 422 is the "neither a source nor a literal" rejection, and it comes + // back with an empty body — a bare status code would leave the caller + // with nothing to say, so name the rule that was broken. + if (response.status === 422) { + throw new Error( + "A binding must either read from a task or carry a literal value.", + ); + } + throw new Error((await response.text()) || `API error: ${response.status}`); + } + return (await response.json()) as TaskContractResponse; + }, + removeTaskBinding: async (taskNumber: number, inputKey: string) => { + const response = await fetch( + `${getApiBase()}/tasks/${taskNumber}/bindings/${encodeURIComponent(inputKey)}`, + {method: "DELETE"}, + ); + if (!response.ok) { + throw new Error((await response.text()) || `API error: ${response.status}`); + } + return (await response.json()) as TaskContractResponse; + }, + listTaskDependencies: (taskNumber: number) => + fetchJson(`/tasks/${taskNumber}/dependencies`), + /** The legal status moves, so the board never offers one the API rejects. */ + listTaskTransitions: () => + fetchJson("/tasks/transitions"), + addTaskDependency: async (taskNumber: number, parentTaskNumber: number) => { + const response = await fetch( + `${getApiBase()}/tasks/${taskNumber}/dependencies`, + { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({parent_task_number: parentTaskNumber}), + }, + ); + if (!response.ok) { + // The server explains cycles and self-loops in the body; surfacing + // only a status code would strip the one detail that helps. + throw new Error((await response.text()) || `API error: ${response.status}`); + } + return (await response.json()) as TaskDependenciesResponse; + }, + removeTaskDependency: async (taskNumber: number, parentTaskNumber: number) => { + const response = await fetch( + `${getApiBase()}/tasks/${taskNumber}/dependencies/${parentTaskNumber}`, + {method: "DELETE"}, + ); + if (!response.ok) throw new Error(`API error: ${response.status}`); + return (await response.json()) as TaskDependenciesResponse; + }, + blockTask: async (taskNumber: number, kind: BlockKind, reason: string) => { + const response = await fetch(`${getApiBase()}/tasks/${taskNumber}/block`, { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({kind, reason}), + }); + if (!response.ok) throw new Error(`API error: ${response.status}`); + return (await response.json()) as TaskResponse; + }, + unblockTask: async (taskNumber: number) => { + const response = await fetch(`${getApiBase()}/tasks/${taskNumber}/unblock`, { + method: "POST", + }); + if (!response.ok) throw new Error(`API error: ${response.status}`); + return (await response.json()) as TaskResponse; + }, + /** Clear the failure budget and requeue. Used by the manual retry action. */ + retryTask: async (taskNumber: number): Promise => { + const response = await fetch(`${getApiBase()}/tasks/${taskNumber}/retry`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + }); + if (!response.ok) throw new Error(`API error: ${response.status}`); + return response.json() as Promise; + }, + + // Task gates — the conditions on one live task. + // + // Separate from the template's `StepGate`s: these are the compiled rows the + // poller actually evaluates, and they carry the verdict (`last_result`) the + // template cannot have. Deleting one is the escape hatch for a condition + // that will never open. + listTaskGates: (taskNumber: number) => + fetchJson(`/tasks/${taskNumber}/gates`), + removeTaskGate: (taskNumber: number, gateId: string) => + mutateJson( + `/tasks/${taskNumber}/gates/${encodeURIComponent(gateId)}`, + "DELETE", + ), + + // Workflows API + // + // A workflow is a reusable template; launching one compiles it into real + // tasks with real dependency edges and hands them to the same scheduler the + // board already shows. Every mutation below goes through `mutateJson` so the + // server's refusal text reaches the editor intact. + listWorkflows: () => fetchJson("/workflows"), + /** The template plus its steps, edges and bindings — one round trip. */ + getWorkflow: (id: string) => + fetchJson(`/workflows/${encodeURIComponent(id)}`), + createWorkflow: (body: SaveWorkflowRequest) => + mutateJson("/workflows", "POST", body), + updateWorkflow: (id: string, body: SaveWorkflowRequest) => + mutateJson( + `/workflows/${encodeURIComponent(id)}`, + "PUT", + body, + ), + deleteWorkflow: (id: string) => + mutateJson( + `/workflows/${encodeURIComponent(id)}`, + "DELETE", + ), + /** + * Add or replace a step. + * + * Keyed by `step_key` rather than an id, so saving the same key twice edits + * the step instead of leaving two behind — the same rule task bindings use, + * and the reason edges and bindings can reference a step by name at all. + */ + saveWorkflowStep: (id: string, stepKey: string, body: SaveStepRequest) => + mutateJson( + `/workflows/${encodeURIComponent(id)}/steps/${encodeURIComponent(stepKey)}`, + "PUT", + body, + ), + deleteWorkflowStep: (id: string, stepKey: string) => + mutateJson( + `/workflows/${encodeURIComponent(id)}/steps/${encodeURIComponent(stepKey)}`, + "DELETE", + ), + addWorkflowEdge: (id: string, body: StepEdgeRequest) => + mutateJson( + `/workflows/${encodeURIComponent(id)}/edges`, + "POST", + body, + ), + // The pair being removed identifies the edge, and there is no edge id to put + // in a path — hence a body on DELETE. + removeWorkflowEdge: (id: string, body: StepEdgeRequest) => + mutateJson( + `/workflows/${encodeURIComponent(id)}/edges`, + "DELETE", + body, + ), + setWorkflowBinding: ( + id: string, + stepKey: string, + inputKey: string, + body: SaveBindingRequest, + ) => + mutateJson( + `/workflows/${encodeURIComponent(id)}/steps/${encodeURIComponent(stepKey)}/bindings/${encodeURIComponent(inputKey)}`, + "PUT", + body, + ), + removeWorkflowBinding: (id: string, stepKey: string, inputKey: string) => + mutateJson( + `/workflows/${encodeURIComponent(id)}/steps/${encodeURIComponent(stepKey)}/bindings/${encodeURIComponent(inputKey)}`, + "DELETE", + ), + /** + * Declare the condition under which a step runs. + * + * Keyed by `gate_key` for the same reason steps are keyed by `step_key`: + * saving the same condition twice has to be an edit. A generated id would + * leave the step held behind two copies of one condition, and the second + * would be invisible in the editor that created it. + */ + setWorkflowStepGate: ( + id: string, + stepKey: string, + gateKey: string, + body: SaveStepGateRequest, + ) => + mutateJson( + `/workflows/${encodeURIComponent(id)}/steps/${encodeURIComponent(stepKey)}/gates/${encodeURIComponent(gateKey)}`, + "PUT", + body, + ), + removeWorkflowStepGate: (id: string, stepKey: string, gateKey: string) => + mutateJson( + `/workflows/${encodeURIComponent(id)}/steps/${encodeURIComponent(stepKey)}/gates/${encodeURIComponent(gateKey)}`, + "DELETE", + ), + /** Compile the template into tasks. Returns the step → task number map. */ + launchWorkflow: (id: string, body: LaunchRequest) => + mutateJson( + `/workflows/${encodeURIComponent(id)}/run`, + "POST", + body, + ), + listWorkflowRuns: (id: string) => + fetchJson(`/workflows/${encodeURIComponent(id)}/runs`), + // Not nested under the workflow: a run outlives the template it came from. + getWorkflowRun: (runId: string) => + fetchJson(`/workflow-runs/${encodeURIComponent(runId)}`), + /** + * Stop a run. + * + * Settles the tasks it never started; anything already in flight is left to + * finish, because killing work mid-flight loses whatever it had done — which + * is why the response reports `settled` and `left_running` separately rather + * than one number the caller would have to interpret. + */ + cancelWorkflowRun: (runId: string, cancelledBy: string) => + mutateJson( + `/workflow-runs/${encodeURIComponent(runId)}/cancel`, + "POST", + {cancelled_by: cancelledBy} satisfies CancelRunRequest, + ), + /** + * Remove a run and every task it emitted. + * + * Refused with a `409` while the run is live or a worker still holds one of + * its cards. The body of that refusal is the whole diagnosis, and `mutateJson` + * throws it verbatim — so callers surface the server's sentence rather than + * re-deriving the rule and getting it subtly wrong. + */ + deleteWorkflowRun: (runId: string) => + mutateJson( + `/workflow-runs/${encodeURIComponent(runId)}`, + "DELETE", + ), // Secrets API secretsStatus: () => fetchJson("/secrets/status"), @@ -2465,6 +2255,18 @@ export const api = { if (!response.ok) throw new Error(`API error: ${response.status}`); }, + /** + * Checkouts under `.worktrees/` that no live run accounts for. + * + * Read-only by design. There is no companion endpoint that removes one, and + * there should not be: the deterministic naming scheme exists so a person + * can find these and decide, not so a sweeper can delete them. + */ + projectWorktreeOrphans: (projectId: string) => + fetchJson( + `/agents/projects/${encodeURIComponent(projectId)}/worktree-orphans`, + ), + projectDiskUsage: (projectId: string) => fetchJson( `/agents/projects/${encodeURIComponent(projectId)}/disk-usage`, @@ -2652,146 +2454,38 @@ export const api = { }, } -export interface UsageTotals { - input_tokens: number; - output_tokens: number; - cache_read_tokens: number; - cache_write_tokens: number; - reasoning_tokens: number; - request_count: number; - estimated_cost_usd: number | null; - cost_status: string; -} +export type UsageTotals = Types.UsageTotals; -export interface UsageByModel { - model: string; - input_tokens: number; - output_tokens: number; - cache_read_tokens: number; - cache_write_tokens: number; - reasoning_tokens: number; - request_count: number; - estimated_cost_usd: number | null; -} +export type UsageByModel = Types.UsageByModel; -export interface UsageResponse { - total: UsageTotals; - by_model?: UsageByModel[]; - by_day?: Array<{ date: string } & UsageTotals>; - by_agent?: Array<{ agent_id: string } & UsageTotals>; -}; +export type UsageResponse = Types.UsageResponse;; // Activity types -export interface ProcessTokens { - input: number; - output: number; - cache_read: number; - reasoning: number; - cost_usd: number; -} +export type ProcessTokens = Types.ProcessTokens; -export interface TokenSummary { - input: number; - output: number; - cache_read: number; - reasoning: number; - cost_usd: number; - by_process: Record; -} +export type TokenSummary = Types.TokenSummary; -export interface ActivityDay { - date: string; - messages: number; - branches: number; - workers: number; - cortex: number; - cron: number; - active_channels: number; - tokens: TokenSummary; -} +export type ActivityDay = Types.ActivityDay; -export interface ActivityTotals { - messages: number; - branches: number; - workers: number; - cortex: number; - cron: number; - active_channels: number; - tokens: TokenSummary; -} +export type ActivityTotals = Types.ActivityTotals; -export interface ActivityResponse { - daily: ActivityDay[]; - totals: ActivityTotals; -} +export type ActivityResponse = Types.ActivityResponse; // Wiki types export type WikiPageType = "entity" | "concept" | "decision" | "project" | "reference"; -export interface WikiPageSummary { - id: string; - slug: string; - title: string; - page_type: string; - version: number; - updated_at: string; - updated_by: string; -} +export type WikiPageSummary = Types.WikiPageSummary; -export interface WikiPage { - id: string; - slug: string; - title: string; - page_type: string; - content: string; - related: string[]; - created_by: string; - updated_by: string; - version: number; - archived: boolean; - created_at: string; - updated_at: string; -} +export type WikiPage = Types.WikiPage; -export interface WikiPageVersion { - id: string; - page_id: string; - version: number; - content: string; - edit_summary: string | null; - author_type: string; - author_id: string; - created_at: string; -} +export type WikiPageVersion = Types.WikiPageVersion; -export interface WikiListResponse { - pages: WikiPageSummary[]; - total: number; -} +export type WikiListResponse = Types.WikiListResponse; -export interface WikiPageResponse { - page: WikiPage; -} +export type WikiPageResponse = Types.WikiPageResponse; -export interface WikiHistoryResponse { - versions: WikiPageVersion[]; -} +export type WikiHistoryResponse = Types.WikiHistoryResponse; -export interface CreateWikiPageRequest { - title: string; - page_type: WikiPageType; - content: string; - related?: string[]; - edit_summary?: string; - author_id?: string; - author_type?: string; -} +export type CreateWikiPageRequest = Types.CreatePageRequest; -export interface EditWikiPageRequest { - old_string: string; - new_string: string; - replace_all?: boolean; - edit_summary?: string; - author_id?: string; - author_type?: string; -} +export type EditWikiPageRequest = Types.EditPageRequest; diff --git a/interface/src/api/schema.d.ts b/interface/src/api/schema.d.ts index 01ec025cd..ef31fcc59 100644 --- a/interface/src/api/schema.d.ts +++ b/interface/src/api/schema.d.ts @@ -438,6 +438,34 @@ export interface paths { patch?: never; trace?: never; }; + "/agents/projects/{id}/repo-dependencies": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * GET /agents/projects/{id}/repo-dependencies — declared edges in a project. + * @description The same list travels inside `GET /agents/projects/{id}`; this exists for + * callers refreshing only the graph. + */ + get: operations["list_repo_dependencies"]; + put?: never; + /** + * POST /agents/projects/{id}/repo-dependencies — declare that one repo + * depends on another. + * @description Refuses a self-dependency, an unknown repo, a repo from another project, + * and a duplicate. A cycle is allowed: mutual generation between two repos is + * a real arrangement, and nothing derives an execution order from these edges. + */ + post: operations["declare_repo_dependency"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/agents/projects/{id}/repos": { parameters: { query?: never; @@ -472,6 +500,30 @@ export interface paths { patch?: never; trace?: never; }; + "/agents/projects/{id}/worktree-orphans": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * GET /agents/projects/{id}/worktree-orphans — list, and only list. + * @description Deliberately a report rather than a sweep. The one thing worse than a stale + * worktree is a background process that deletes directories, so this endpoint + * has no counterpart that removes anything: a person reads the list, looks at + * the diff, and decides. Most entries will be checkouts a failed run left + * behind with uncommitted changes, which is exactly the work you want back. + */ + get: operations["list_worktree_orphans"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/agents/projects/{id}/worktrees": { parameters: { query?: never; @@ -489,6 +541,32 @@ export interface paths { patch?: never; trace?: never; }; + "/agents/projects/{project_id}/repo-dependencies/{repo_id}/{depends_on_repo_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * PUT /agents/projects/{project_id}/repo-dependencies/{repo_id}/{depends_on_repo_id} + * — relabel or annotate an existing declaration. + * @description Repointing an edge is a different statement about the repos, so it is a + * delete and a fresh declaration rather than an update. + */ + put: operations["update_repo_dependency"]; + post?: never; + /** + * DELETE /agents/projects/{project_id}/repo-dependencies/{repo_id}/{depends_on_repo_id} + * — withdraw a declaration. + */ + delete: operations["delete_repo_dependency"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/agents/projects/{project_id}/repos/{repo_id}": { parameters: { query?: never; @@ -506,6 +584,34 @@ export interface paths { patch?: never; trace?: never; }; + "/agents/projects/{project_id}/repos/{repo_id}/dependency-suggestions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * GET /agents/projects/{project_id}/repos/{repo_id}/dependency-suggestions + * — what is declared around this repo, in both directions. + * @description This is the endpoint the workflow step editor calls: you are adding a step + * that runs in `api`, and `dependents` is why it can say "`web` is generated + * from `api` — add a step there too?". + * + * It answers, and that is all it does. The reason this never becomes "and so + * the server added the step for you" is written where the answer is produced, + * in `ProjectStore::repo_dependency_suggestions`; read it before adding + * anything here that writes. + */ + get: operations["repo_dependency_suggestions"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/agents/projects/{project_id}/worktrees/{worktree_id}": { parameters: { query?: never; @@ -685,7 +791,7 @@ export interface paths { }; /** * Serve a saved attachment file. - * @description Streams the file from disk with the correct Content-Type. + * @description Reads the file from disk with the correct Content-Type. * Use `?download=true` to force a download prompt. * Use `?thumbnail=true` to request a thumbnail (currently serves full file). */ @@ -736,6 +842,28 @@ export interface paths { patch?: never; trace?: never; }; + "/agents/{agent_id}/wake": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Manually wake a (typically dormant) agent. + * @description Fires the same wake path that `send_agent_message`, cron, and other + * trigger sources use. Useful for debugging dormant deployments and + * recovering an agent stuck on a missed trigger. + */ + post: operations["wake_agent"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/bindings": { parameters: { query?: never; @@ -1655,38 +1783,6 @@ export interface paths { patch?: never; trace?: never; }; - "/providers/openai/browser-oauth/start": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post: operations["start_openai_browser_oauth"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/providers/openai/browser-oauth/status": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["openai_browser_oauth_status"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/providers/test-model": { parameters: { query?: never; @@ -1719,22 +1815,6 @@ export interface paths { patch?: never; trace?: never; }; - "/providers/{provider}/config": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["get_provider_config"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/secrets": { parameters: { query?: never; @@ -2128,6 +2208,27 @@ export interface paths { patch?: never; trace?: never; }; + "/tasks/transitions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * `GET /tasks/transitions` — every legal status move. + * @description The dashboard reads this instead of hand-maintaining a second transition + * table in TypeScript, so a board can never offer a move the API rejects. + */ + get: operations["list_task_transitions"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/tasks/{number}": { parameters: { query?: never; @@ -2181,7 +2282,7 @@ export interface paths { patch?: never; trace?: never; }; - "/tasks/{number}/execute": { + "/tasks/{number}/bindings/{key}": { parameters: { query?: never; header?: never; @@ -2189,45 +2290,50 @@ export interface paths { cookie?: never; }; get?: never; - put?: never; - /** - * `POST /tasks/{number}/execute` — move a task to ready for execution. - * Tasks already in `ready` or `in_progress` are returned as-is. - */ - post: operations["execute_task"]; - delete?: never; + /** `PUT /tasks/{number}/bindings/{key}` — point one input at its source. */ + put: operations["set_task_binding"]; + post?: never; + /** `DELETE /tasks/{number}/bindings/{key}` — unbind one input. */ + delete: operations["remove_task_binding"]; options?: never; head?: never; patch?: never; trace?: never; }; - "/tools": { + "/tasks/{number}/block": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** List the contents of the durable `tools/bin` directory. */ - get: operations["list_tools"]; + get?: never; put?: never; - post?: never; + /** `POST /tasks/{number}/block` — park a task with a typed reason. */ + post: operations["block_task"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/topology": { + "/tasks/{number}/contract": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** Get the full agent topology for graph rendering. */ - get: operations["topology"]; - put?: never; + /** + * `GET /tasks/{number}/contract` — the declared contract, its bindings, and + * what those bindings currently resolve to. + * @description Resolution runs live rather than being read back from the last claim, so the + * page shows what the task *would* get if it ran now. A graph that has drifted + * since the last attempt is exactly the case worth seeing. + */ + get: operations["get_task_contract"]; + /** `PUT /tasks/{number}/contract` — declare what a task needs and produces. */ + put: operations["set_task_contract"]; post?: never; delete?: never; options?: never; @@ -2235,7 +2341,7 @@ export interface paths { patch?: never; trace?: never; }; - "/update-apply": { + "/tasks/{number}/decision": { parameters: { query?: never; header?: never; @@ -2244,93 +2350,149 @@ export interface paths { }; get?: never; put?: never; - /** Pull the new Docker image and recreate this container. */ - post: operations["update_apply"]; + /** + * `POST /tasks/{number}/decision` — answer a decision step. + * @description The only path by which a human-chosen value ever reaches a task's outputs, + * and it works on exactly one kind of task. That is the constraint the whole + * feature rests on: a person must not be able to set an *arbitrary* task's + * outputs, because an agent task's outputs are the record of what that agent + * produced. A decision step's outputs are the answer and nothing else, which is + * what lets the run record distinguish "the operator approved this" from "a + * model believed the operator approved this". + * + * The answer is validated against the step's own `output_schema` by the same + * validator that checks an agent's outputs — there is no second contract path. + */ + post: operations["answer_decision"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/update-check": { + "/tasks/{number}/dependencies": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** Return the current update status (from background check). */ - get: operations["update_check"]; + /** `GET /tasks/{number}/dependencies` — the edges around a task. */ + get: operations["list_task_dependencies"]; put?: never; - /** Force an immediate update check against GitHub. */ - post: operations["update_check_now"]; + /** + * `POST /tasks/{number}/dependencies` — make this task wait on another. + * @description Rejects self-loops, unknown tasks, and any edge that would close a cycle. + * The cycle response names the path so the caller can see which existing edge + * conflicts, rather than being told only that something is wrong. + */ + post: operations["add_task_dependency"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/usage": { + "/tasks/{number}/dependencies/{parent}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** Aggregated token usage for the instance. */ - get: operations["get_usage"]; + get?: never; put?: never; post?: never; - delete?: never; + /** `DELETE /tasks/{number}/dependencies/{parent}` — drop an edge. */ + delete: operations["remove_task_dependency"]; options?: never; head?: never; patch?: never; trace?: never; }; - "/usage/conversation/{conversation_id}": { + "/tasks/{number}/execute": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** Aggregated token usage for a single conversation. */ - get: operations["get_conversation_usage"]; + get?: never; put?: never; - post?: never; + /** + * `POST /tasks/{number}/execute` — move a task to ready for execution. + * Tasks already in `ready` or `in_progress` are returned as-is. + */ + post: operations["execute_task"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/wiki": { + "/tasks/{number}/gates": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** GET /wiki — list all wiki pages */ - get: operations["list_pages"]; + /** `GET /tasks/{number}/gates` — what this task is waiting on outside the graph. */ + get: operations["list_task_gates"]; put?: never; - /** POST /wiki — create a new wiki page */ - post: operations["create_page"]; + /** + * `POST /tasks/{number}/gates` — hold this task until something outside says go. + * @description The config is validated here rather than at first poll. A malformed gate + * accepted now would error once a minute forever with nobody reading the log, + * so the rejection has to land while a person is still looking at the form. + */ + post: operations["create_task_gate"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/wiki/search": { + "/tasks/{number}/gates/{gate_id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** GET /wiki/search — search wiki pages */ - get: operations["search_pages"]; + get?: never; + put?: never; + post?: never; + /** + * `DELETE /tasks/{number}/gates/{gate_id}` — stop waiting on it. + * @description Removing a gate is the escape hatch for one that has failed or cannot be + * reached: the task becomes promotable again on the next sweep. It is a + * deliberate act by a person, which is exactly what a `failed` gate is asking + * for. + */ + delete: operations["delete_task_gate"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/tasks/{number}/graph": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * `GET /tasks/{number}/graph` — every task connected to this one, and the + * edges between them. + * @description Drawn from real dependency edges rather than from a workflow template, + * which is what makes it answer the question in the three cases that matter: + * the template has since been deleted, the step fanned out so one step is now + * many tasks, or there was never a template at all because the graph was built + * by hand or by a worker filing cards. + */ + get: operations["get_task_graph"]; put?: never; post?: never; delete?: never; @@ -2339,25 +2501,29 @@ export interface paths { patch?: never; trace?: never; }; - "/wiki/{slug}": { + "/tasks/{number}/provenance": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** GET /wiki/:slug — read a wiki page */ - get: operations["get_page"]; + /** + * `GET /tasks/{number}/provenance` — where this card came from and what it + * spawned. + * @description A worker-filed card is otherwise indistinguishable from one a human wrote, + * which makes a surprising board impossible to explain. + */ + get: operations["get_task_provenance"]; put?: never; post?: never; - /** DELETE /wiki/:slug — archive a page */ - delete: operations["archive_page"]; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/wiki/{slug}/edit": { + "/tasks/{number}/retry": { parameters: { query?: never; header?: never; @@ -2366,23 +2532,27 @@ export interface paths { }; get?: never; put?: never; - /** POST /wiki/:slug/edit — apply a partial edit */ - post: operations["edit_page"]; + /** + * `POST /tasks/{number}/retry` — clear the failure budget and requeue. + * @description A human looked at the task, so the budget starts over rather than + * immediately re-parking it on the next failure. + */ + post: operations["retry_task"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/wiki/{slug}/history": { + "/tasks/{number}/runs": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** GET /wiki/:slug/history — list version history */ - get: operations["get_history"]; + /** `GET /tasks/{number}/runs` — the per-attempt execution log for a task. */ + get: operations["list_task_runs"]; put?: never; post?: never; delete?: never; @@ -2391,7 +2561,7 @@ export interface paths { patch?: never; trace?: never; }; - "/wiki/{slug}/restore": { + "/tasks/{number}/unblock": { parameters: { query?: never; header?: never; @@ -2400,2153 +2570,5578 @@ export interface paths { }; get?: never; put?: never; - /** POST /wiki/:slug/restore — restore to a historical version */ - post: operations["restore_version"]; + /** + * `POST /tasks/{number}/unblock` — release a parked task. + * @description Lands in `ready` when nothing upstream is outstanding, `backlog` otherwise. + * + * Refuses an unanswered decision with 409 and a sentence naming the endpoint + * that does work. Unblocking says somebody acted, not what they decided — + * releasing a decision through it would put the task back in the queue with + * nothing in its outputs, which is the exact hole the decision step fills. + */ + post: operations["unblock_task"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; -} -export type webhooks = Record; -export interface components { - schemas: { - /** @description Content within an action step. */ - ActionContent: { - text: string; - /** @enum {string} */ - type: "text"; - } | { - args: string; - id: string; - name: string; - /** @enum {string} */ - type: "tool_call"; - }; + "/tools": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List the contents of the durable `tools/bin` directory. */ + get: operations["list_tools"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/topology": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get the full agent topology for graph rendering. */ + get: operations["topology"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/update-apply": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Pull the new Docker image and recreate this container. */ + post: operations["update_apply"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/update-check": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Return the current update status (from background check). */ + get: operations["update_check"]; + put?: never; + /** Force an immediate update check against GitHub. */ + post: operations["update_check_now"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/usage": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Aggregated token usage for the instance. */ + get: operations["get_usage"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/usage/conversation/{conversation_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Aggregated token usage for a single conversation. */ + get: operations["get_conversation_usage"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/webhooks/workflow/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * `POST /webhooks/workflow/{id}` — an inbound trigger firing. + * @description **The one route here that is not behind the instance bearer token.** It has + * to be: a webhook exists so that CI — which cannot be handed a token that + * grants the whole API — can start a pipeline, and that is the loop the gate + * machinery was built for and has never been able to close. + * + * So the authentication is the per-workflow shared secret and nothing else, + * and every part of this is arranged to fail closed: + * + * - No row for the workflow means no. That is the default state of every + * workflow that has ever existed, and it is not a check that can be + * forgotten — it is the absence of the thing that would allow it. + * - A configured webhook is off unless somebody set `enabled`. + * - The secret is compared as a digest, in constant time, before the payload + * is looked at, before the workflow is loaded, and before anything is + * written. A delivery that does not authenticate causes no work. + * - All three refusals render identically. See [`rejection_response`]. + */ + post: operations["workflow_webhook_delivery"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/wiki": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** GET /wiki — list all wiki pages */ + get: operations["list_pages"]; + put?: never; + /** POST /wiki — create a new wiki page */ + post: operations["create_page"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/wiki/search": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** GET /wiki/search — search wiki pages */ + get: operations["search_pages"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/wiki/{slug}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** GET /wiki/:slug — read a wiki page */ + get: operations["get_page"]; + put?: never; + post?: never; + /** DELETE /wiki/:slug — archive a page */ + delete: operations["archive_page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/wiki/{slug}/edit": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** POST /wiki/:slug/edit — apply a partial edit */ + post: operations["edit_page"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/wiki/{slug}/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** GET /wiki/:slug/history — list version history */ + get: operations["get_history"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/wiki/{slug}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** POST /wiki/:slug/restore — restore to a historical version */ + post: operations["restore_version"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflow-runs/{run_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * `GET /workflow-runs/{run_id}` — one run and the tasks it produced. + * @description Not nested under the workflow: a run outlives the template it came from, so + * requiring the template's id to look one up would make deleted templates take + * their history with them. + */ + get: operations["get_run"]; + put?: never; + post?: never; + /** + * `DELETE /workflow-runs/{run_id}` — remove a finished run and its tasks. + * @description The endpoint whose absence left empty run rows behind: cleanup had nothing + * to call. Refused while the run is still going — `409` with the sentence + * saying to cancel it first, because a delete is not a stop. + */ + delete: operations["delete_run"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflow-runs/{run_id}/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * `POST /workflow-runs/{run_id}/cancel` — stop a run. + * @description Unstarted tasks are settled; anything already running is left to finish, + * because killing work mid-flight loses whatever it had done. Cancelling a + * `stuck` or `failed` run is allowed and is how the cards it left parked get + * cleared; a `succeeded` run has nothing to clear. + */ + post: operations["cancel_run"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflow-schedules/{schedule_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * `DELETE /workflow-schedules/{schedule_id}` — remove a schedule. + * @description Not nested under the workflow, matching the run routes: a caller holding a + * schedule id from a listing should not have to also know which template it + * belongs to in order to delete it. + */ + delete: operations["delete_schedule"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflows": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** `GET /workflows` — list templates. */ + get: operations["list_workflows"]; + put?: never; + /** `POST /workflows` — create a template. */ + post: operations["create_workflow"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflows/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** `GET /workflows/{id}` — a template with its steps, edges, and bindings. */ + get: operations["get_workflow"]; + /** `PUT /workflows/{id}` — rename or re-describe a template. */ + put: operations["update_workflow"]; + post?: never; + /** + * `DELETE /workflows/{id}` — delete a template. + * @description Runs already launched from it keep running: tasks carry the run id as plain + * text, not a foreign key, so deleting the recipe never deletes the history of + * work that was done from it. + */ + delete: operations["delete_workflow"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflows/{id}/edges": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** `POST /workflows/{id}/edges` — make one step wait for another. */ + post: operations["add_edge"]; + /** `DELETE /workflows/{id}/edges` — drop a wait. */ + delete: operations["remove_edge"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflows/{id}/run": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** `POST /workflows/{id}/run` — launch a pipeline from one input. */ + post: operations["launch_workflow"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflows/{id}/runs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** `GET /workflows/{id}/runs` — launches of one template, newest first. */ + get: operations["list_runs"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflows/{id}/schedules": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** `GET /workflows/{id}/schedules` — the schedules attached to one template. */ + get: operations["list_schedules"]; + put?: never; + /** `POST /workflows/{id}/schedules` — create or replace a schedule. */ + post: operations["put_schedule"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflows/{id}/steps/{step_key}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** `PUT /workflows/{id}/steps/{step_key}` — add or replace a step. */ + put: operations["put_step"]; + post?: never; + /** `DELETE /workflows/{id}/steps/{step_key}` — remove a step and its references. */ + delete: operations["delete_step"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflows/{id}/steps/{step_key}/bindings/{input_key}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * `PUT /workflows/{id}/steps/{step_key}/bindings/{input_key}` — declare where + * one of a step's inputs comes from. + */ + put: operations["put_binding"]; + post?: never; + /** `DELETE /workflows/{id}/steps/{step_key}/bindings/{input_key}` */ + delete: operations["delete_binding"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflows/{id}/steps/{step_key}/gates/{gate_key}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * `PUT /workflows/{id}/steps/{step_key}/gates/{gate_key}` — declare the + * condition under which a step runs. + * @description Idempotent on `gate_key`, so an editor saving the same condition twice edits + * it. A generated id would leave the step held behind two copies of one gate, + * which on the board reads as a condition that cannot be satisfied. + */ + put: operations["put_step_gate"]; + post?: never; + /** + * `DELETE /workflows/{id}/steps/{step_key}/gates/{gate_key}` — the step runs + * unconditionally again. + */ + delete: operations["delete_step_gate"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/workflows/{id}/webhook": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** `GET /workflows/{id}/webhook` — the webhook config, without its secret. */ + get: operations["get_webhook"]; + /** + * `PUT /workflows/{id}/webhook` — configure the inbound trigger. + * @description The one place a secret is accepted, and the only way this endpoint can ever + * start accepting deliveries. Both halves are deliberate: there is no global + * enable, no default row, and no way to end up with a live webhook without + * having chosen a secret and set `enabled` in the same call. + */ + put: operations["put_webhook"]; + post?: never; + /** `DELETE /workflows/{id}/webhook` — remove the inbound trigger entirely. */ + delete: operations["delete_webhook"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** @description Content within an action step. */ + ActionContent: { + text: string; + /** @enum {string} */ + type: "text"; + } | { + args: string; + id: string; + name: string; + /** @enum {string} */ + type: "tool_call"; + }; ActionResponse: { message: string; success: boolean; }; - ActivityDay: { - /** Format: int64 */ - active_channels: number; - /** Format: int64 */ - branches: number; - /** Format: int64 */ - cortex: number; - /** Format: int64 */ - cron: number; - date: string; - /** Format: int64 */ - messages: number; - tokens: components["schemas"]["TokenSummary"]; - /** Format: int64 */ - workers: number; + ActivityDay: { + /** Format: int64 */ + active_channels: number; + /** Format: int64 */ + branches: number; + /** Format: int64 */ + cortex: number; + /** Format: int64 */ + cron: number; + date: string; + /** Format: int64 */ + messages: number; + tokens: components["schemas"]["TokenSummary"]; + /** Format: int64 */ + workers: number; + }; + ActivityDayCount: { + /** Format: int64 */ + branches: number; + date: string; + /** Format: int64 */ + workers: number; + }; + ActivityResponse: { + daily: components["schemas"]["ActivityDay"][]; + totals: components["schemas"]["ActivityTotals"]; + }; + ActivityTotals: { + /** Format: int64 */ + active_channels: number; + /** Format: int64 */ + branches: number; + /** Format: int64 */ + cortex: number; + /** Format: int64 */ + cron: number; + /** Format: int64 */ + messages: number; + tokens: components["schemas"]["TokenSummary"]; + /** Format: int64 */ + workers: number; + }; + AdapterInstanceStatus: { + binding_count: number; + configured: boolean; + enabled: boolean; + /** @description `None` means the default instance for the platform. */ + name?: string | null; + platform: string; + runtime_key: string; + }; + AddDependencyRequest: { + /** Format: int64 */ + parent_task_number: number; + }; + AgentConfigResponse: { + browser: components["schemas"]["BrowserSection"]; + channel: components["schemas"]["ChannelSection"]; + coalesce: components["schemas"]["CoalesceSection"]; + compaction: components["schemas"]["CompactionSection"]; + cortex: components["schemas"]["CortexSection"]; + discord: components["schemas"]["DiscordSection"]; + memory_persistence: components["schemas"]["MemoryPersistenceSection"]; + projects: components["schemas"]["ProjectsSection"]; + routing: components["schemas"]["RoutingSection"]; + sandbox: components["schemas"]["SandboxSection"]; + tuning: components["schemas"]["TuningSection"]; + warmup: components["schemas"]["WarmupSection"]; + }; + AgentConfigUpdateRequest: { + agent_id: string; + browser?: null | components["schemas"]["BrowserUpdate"]; + channel?: null | components["schemas"]["ChannelUpdate"]; + coalesce?: null | components["schemas"]["CoalesceUpdate"]; + compaction?: null | components["schemas"]["CompactionUpdate"]; + cortex?: null | components["schemas"]["CortexUpdate"]; + discord?: null | components["schemas"]["DiscordUpdate"]; + memory_persistence?: null | components["schemas"]["MemoryPersistenceUpdate"]; + projects?: null | components["schemas"]["ProjectsUpdate"]; + routing?: null | components["schemas"]["RoutingUpdate"]; + sandbox?: null | components["schemas"]["SandboxUpdate"]; + tuning?: null | components["schemas"]["TuningUpdate"]; + warmup?: null | components["schemas"]["WarmupUpdate"]; + }; + /** @description Summary of an agent's configuration, exposed via the API. */ + AgentInfo: { + /** + * @description What this agent declares it can do. Published so the step editor can + * offer the labels that already exist rather than inviting a fleet where + * `rust` and `Rust` are two capabilities and one of them matches nothing. + * + * Always present, empty included: a client that has to distinguish "no + * capabilities" from "this build does not report them" cannot, if the + * field disappears when it is empty. + */ + capabilities: string[]; + context_window: number; + display_name?: string | null; + gradient_end?: string | null; + gradient_start?: string | null; + id: string; + max_concurrent_branches: number; + max_concurrent_workers: number; + max_turns: number; + role?: string | null; + workspace: string; + }; + AgentMcpResponse: { + servers: components["schemas"]["McpServerStatus"][]; + }; + AgentOverviewResponse: { + activity_daily: components["schemas"]["ActivityDayCount"][]; + activity_heatmap: components["schemas"]["HeatmapCell"][]; + channel_count: number; + cron_jobs: components["schemas"]["CronJobInfo"][]; + last_bulletin_at?: string | null; + latest_bulletin?: string | null; + memory_counts: { + [key: string]: number; + }; + memory_daily: components["schemas"]["DayCount"][]; + /** Format: int64 */ + memory_total: number; + recent_cortex_events: components["schemas"]["CortexEvent"][]; + }; + /** @description Persisted agent profile generated by the cortex. */ + AgentProfile: { + agent_id: string; + avatar_seed?: string | null; + bio?: string | null; + display_name?: string | null; + generated_at: string; + status?: string | null; + updated_at: string; + }; + AgentProfileResponse: { + profile?: null | components["schemas"]["AgentProfile"]; + }; + AgentSummary: { + activity_sparkline: number[]; + channel_count: number; + cron_job_count: number; + id: string; + last_activity_at?: string | null; + last_bulletin_at?: string | null; + /** Format: int64 */ + memory_total: number; + profile?: null | components["schemas"]["AgentProfile"]; + }; + AgentsResponse: { + agents: components["schemas"]["AgentInfo"][]; + }; + /** @description An answer to a decision step. */ + AnswerDecisionRequest: { + /** + * @description The answer, in whatever shape the step's `output_schema` declares. It + * becomes the task's outputs verbatim, so a binding downstream reads it + * with the pointers it already uses. + */ + answer: unknown; + /** + * @description Who is answering. Required — an answer with no answerer has no + * provenance, and provenance is the only thing a decision step has that an + * agent step asking in a channel does not. + */ + answered_by: string; + }; + ApproveRequest: { + approved_by?: string | null; + }; + AssignRequest: { + assigned_agent_id: string; + }; + /** @description Association between memories. */ + Association: { + /** Format: date-time */ + created_at: string; + id: string; + relation_type: components["schemas"]["RelationType"]; + source_id: string; + target_id: string; + /** Format: float */ + weight: number; + }; + AttachmentInfo: { + created_at: string; + id: string; + mime_type: string; + original_filename: string; + /** Format: int64 */ + size_bytes: number; + }; + AttachmentListResponse: { + attachments: components["schemas"]["AttachmentInfo"][]; + }; + AttachmentUploadResponse: { + id: string; + mime_type: string; + original_filename: string; + /** Format: int64 */ + size_bytes: number; + }; + AuthorizedKeyRequest: { + public_key: string; + }; + AuthorizedKeyResponse: { + message: string; + success: boolean; + }; + BinaryEntry: { + modified?: string | null; + name: string; + /** Format: int64 */ + size: number; + }; + BindingResponse: { + adapter?: string | null; + agent_id: string; + channel: string; + channel_ids: string[]; + chat_id?: string | null; + dm_allowed_users: string[]; + guild_id?: string | null; + require_mention: boolean; + team_id?: string | null; + workspace_id?: string | null; + }; + /** + * @description Where a step's input comes from. + * + * Named rather than inferred from which column is populated. The task-level + * table infers "literal" from a NULL source, which works for rows the store + * writes and is a trap for rows a human edits: a malformed binding is + * indistinguishable from a deliberate one. + * @enum {string} + */ + BindingSource: "step" | "literal" | "run_input" | "fan_in" | "previous_iteration"; + BindingsListResponse: { + bindings: components["schemas"]["BindingResponse"][]; + }; + /** + * @description Why a task is parked. + * + * The kinds differ in how they recover, which is the entire reason the column + * exists. `dependency` and `transient` clear themselves; `needs_input` and + * `capability` are sticky and only an explicit unblock releases them. + * @enum {string} + */ + BlockKind: "dependency" | "needs_input" | "capability" | "transient" | "awaiting_decision"; + BlockTaskRequest: { + /** @description dependency | needs_input | capability | transient */ + kind: string; + reason: string; + }; + BrowserSection: { + close_policy: components["schemas"]["ClosePolicy"]; + enabled: boolean; + evaluate_enabled: boolean; + headless: boolean; + persist_session: boolean; + }; + BrowserUpdate: { + close_policy?: null | components["schemas"]["ClosePolicy"]; + enabled?: boolean | null; + evaluate_enabled?: boolean | null; + headless?: boolean | null; + persist_session?: boolean | null; + }; + CancelProcessRequest: { + channel_id: string; + process_id: string; + process_type: string; + }; + CancelProcessResponse: { + message: string; + success: boolean; + }; + CancelRunRequest: { + /** + * @description Who stopped it. Recorded on the run and on every card it settles, so + * "why did this stop" is answerable from the row rather than from memory. + */ + cancelled_by: string; + }; + /** @description A run that was stopped, and what that did to its tasks. */ + CancelRunResponse: { + /** + * Format: int64 + * @description Tasks left in flight. They are not killed: whatever they had already + * done would be lost, so they finish or are reaped normally. + */ + left_running: number; + run: components["schemas"]["WorkflowRun"]; + /** + * Format: int64 + * @description Unstarted tasks settled as `skipped`. + */ + settled: number; + }; + ChannelResponse: { + agent_id: string; + created_at: string; + display_name?: string | null; + id: string; + is_active: boolean; + last_activity_at: string; + model?: string | null; + platform: string; + response_mode?: string | null; + }; + ChannelSection: { + listen_only_mode: boolean; + }; + ChannelSettingsResponse: { + conversation_id: string; + settings: components["schemas"]["ConversationSettings"]; + }; + ChannelUpdate: { + listen_only_mode?: boolean | null; + }; + ChannelsResponse: { + channels: components["schemas"]["ChannelResponse"][]; + }; + /** + * @description What happens when a worker explicitly calls "close" on the browser. + * @enum {string} + */ + ClosePolicy: "close_browser" | "close_tabs" | "detach"; + CoalesceSection: { + /** Format: int64 */ + debounce_ms: number; + enabled: boolean; + /** Format: int64 */ + max_wait_ms: number; + min_messages: number; + multi_user_only: boolean; + }; + CoalesceUpdate: { + /** Format: int64 */ + debounce_ms?: number | null; + enabled?: boolean | null; + /** Format: int64 */ + max_wait_ms?: number | null; + min_messages?: number | null; + multi_user_only?: boolean | null; + }; + CompactionSection: { + /** Format: float */ + aggressive_threshold: number; + /** Format: float */ + background_threshold: number; + /** Format: float */ + emergency_threshold: number; + }; + CompactionUpdate: { + /** Format: float */ + aggressive_threshold?: number | null; + /** Format: float */ + background_threshold?: number | null; + /** Format: float */ + emergency_threshold?: number | null; + }; + /** + * @description A specific reason a contract could not be satisfied. + * + * Deliberately granular. "Validation failed" sends a human reading prompts and + * guessing; naming the key and the upstream task that should have supplied it + * points straight at the broken edge. + */ + ContractProblem: { + /** @enum {string} */ + kind: "task_missing"; + /** Format: int64 */ + task_number: number; + } | { + input_key: string; + /** @enum {string} */ + kind: "source_missing"; + /** Format: int64 */ + source_task_number: number; + } | { + input_key: string; + /** @enum {string} */ + kind: "source_has_no_outputs"; + /** Format: int64 */ + source_task_number: number; + } | { + input_key: string; + /** @enum {string} */ + kind: "pointer_missed"; + pointer: string; + /** Format: int64 */ + source_task_number: number; + } | { + input_key: string; + /** @enum {string} */ + kind: "empty_literal"; + } | { + /** @enum {string} */ + kind: "schema_violation"; + message: string; + /** @description JSON Pointer to the offending value, `""` for the document root. */ + path: string; + side: components["schemas"]["ContractSide"]; + } | { + /** @enum {string} */ + kind: "invalid_schema"; + message: string; + side: components["schemas"]["ContractSide"]; + } | { + input_key: string; + /** @enum {string} */ + kind: "storage"; + message: string; + } | { + input_key: string; + /** @enum {string} */ + kind: "fan_in_outside_run"; + step_key: string; + } | { + input_key: string; + /** @enum {string} */ + kind: "fan_in_no_branches"; + step_key: string; + }; + /** + * @description Which half of a contract a problem came from. + * @enum {string} + */ + ContractSide: "input" | "output"; + /** @description Response payload for conversation defaults endpoint. */ + ConversationDefaultsResponse: { + /** @description All available models. */ + available_models: components["schemas"]["ModelOption"][]; + /** @description Current default delegation mode. */ + delegation: components["schemas"]["DelegationMode"]; + /** @description Available delegation modes. */ + delegation_modes: string[]; + /** @description Current default memory mode. */ + memory: components["schemas"]["MemoryMode"]; + /** @description Available memory modes. */ + memory_modes: string[]; + /** @description Current default model name (from agent config). */ + model: string; + /** @description Current default worker context settings. */ + worker_context: components["schemas"]["WorkerContextMode"]; + /** @description Available worker history modes. */ + worker_history_modes: string[]; + /** @description Available worker memory modes. */ + worker_memory_modes: string[]; + }; + /** @description Per-conversation settings that control behavior. */ + ConversationSettings: { + /** @description How tools work in this conversation. */ + delegation?: components["schemas"]["DelegationMode"]; + /** @description How memory is used in this conversation. */ + memory?: components["schemas"]["MemoryMode"]; + /** + * @description Blanket model override — applies to all processes unless a per-process + * override is set in `model_overrides`. + */ + model?: string | null; + /** @description Per-process model overrides. Takes priority over `model`. */ + model_overrides?: components["schemas"]["ModelOverrides"]; + /** @description How the channel handles incoming messages. */ + response_mode?: components["schemas"]["ResponseMode"]; + /** @description Whether file attachments are saved to workspace. */ + save_attachments?: boolean | null; + /** @description What context workers spawned from this conversation receive. */ + worker_context?: components["schemas"]["WorkerContextMode"]; + }; + CortexChatDeleteThreadRequest: { + agent_id: string; + thread_id: string; + }; + /** @description A persisted cortex chat message. */ + CortexChatMessage: { + channel_context?: string | null; + content: string; + created_at: string; + id: string; + role: string; + thread_id: string; + /** @description Serialized JSON array of tool calls (for assistant messages). */ + tool_calls?: components["schemas"]["CortexChatToolCall"][] | null; + }; + CortexChatMessagesResponse: { + messages: components["schemas"]["CortexChatMessage"][]; + thread_id: string; + }; + CortexChatSendRequest: { + agent_id: string; + channel_id?: string | null; + message: string; + thread_id: string; + }; + /** @description Summary of a cortex chat thread (returned by list_threads). */ + CortexChatThread: { + first_message_at: string; + last_message_at: string; + /** Format: int64 */ + message_count: number; + preview: string; + thread_id: string; + }; + CortexChatThreadsResponse: { + threads: components["schemas"]["CortexChatThread"][]; + }; + /** @description A tool call + result pair persisted alongside assistant messages. */ + CortexChatToolCall: { + args: string; + id: string; + result?: string | null; + status: string; + tool: string; + }; + /** @description A persisted cortex action record. */ + CortexEvent: { + created_at: string; + details?: unknown; + event_type: string; + id: string; + summary: string; + }; + CortexEventsResponse: { + events: components["schemas"]["CortexEvent"][]; + /** Format: int64 */ + total: number; + }; + CortexSection: { + /** Format: int64 */ + branch_timeout_secs: number; + /** Format: int64 */ + bulletin_interval_secs: number; + bulletin_max_turns: number; + bulletin_max_words: number; + /** Format: int32 */ + circuit_breaker_threshold: number; + /** Format: int32 */ + detached_worker_timeout_retry_limit: number; + /** Format: float */ + maintenance_decay_rate: number; + /** Format: int64 */ + maintenance_interval_secs: number; + /** Format: float */ + maintenance_merge_similarity_threshold: number; + /** Format: int64 */ + maintenance_min_age_days: number; + /** Format: float */ + maintenance_prune_threshold: number; + supervisor_kill_budget_per_tick: number; + /** Format: int64 */ + tick_interval_secs: number; + /** Format: int64 */ + worker_timeout_secs: number; + }; + CortexUpdate: { + /** Format: int64 */ + branch_timeout_secs?: number | null; + /** Format: int64 */ + bulletin_interval_secs?: number | null; + bulletin_max_turns?: number | null; + bulletin_max_words?: number | null; + /** Format: int32 */ + circuit_breaker_threshold?: number | null; + /** Format: int32 */ + detached_worker_timeout_retry_limit?: number | null; + /** Format: float */ + maintenance_decay_rate?: number | null; + /** Format: int64 */ + maintenance_interval_secs?: number | null; + /** Format: float */ + maintenance_merge_similarity_threshold?: number | null; + /** Format: int64 */ + maintenance_min_age_days?: number | null; + /** Format: float */ + maintenance_prune_threshold?: number | null; + supervisor_kill_budget_per_tick?: number | null; + /** Format: int64 */ + tick_interval_secs?: number | null; + /** Format: int64 */ + worker_timeout_secs?: number | null; + }; + CreateAgentRequest: { + agent_id: string; + /** + * @description What this agent can do — the labels pooled tasks are matched against. + * + * Opaque strings the operator chooses. Omit for none, which is right for + * an agent that only ever takes work addressed to it by name. + */ + capabilities?: string[] | null; + display_name?: string | null; + role?: string | null; + }; + CreateBindingRequest: { + adapter?: string | null; + agent_id: string; + channel: string; + channel_ids?: string[]; + chat_id?: string | null; + dm_allowed_users?: string[]; + guild_id?: string | null; + platform_credentials?: null | components["schemas"]["PlatformCredentials"]; + require_mention?: boolean; + team_id?: string | null; + workspace_id?: string | null; + }; + CreateBindingResponse: { + message: string; + /** @description True if platform credentials were added/changed (adapter needs restart). */ + restart_required: boolean; + success: boolean; + }; + CreateCronRequest: { + /** Format: int32 */ + active_end_hour?: number | null; + /** Format: int32 */ + active_start_hour?: number | null; + agent_id: string; + cron_expr?: string | null; + delivery_target: string; + enabled?: boolean; + id: string; + /** Format: int64 */ + interval_secs?: number; + prompt: string; + run_once?: boolean; + /** Format: int64 */ + timeout_secs?: number | null; + }; + CreateGateRequest: { + /** @description Shape depends on `kind`. See `crate::tasks::gates`. */ + config: unknown; + /** + * @description `wait` | `route`, or omit to derive it. + * + * What a *false* answer means. `wait` is a gate in the original sense — + * poll again. `route` says the step does not apply and settles it as + * `skipped`. Omitted is the right answer nearly always: a `task_output` + * gate whose source has finished routes, everything else waits, and that + * is a fact about whether the input can still change rather than a guess. + */ + disposition?: string | null; + /** @description `http` | `task_output` */ + kind: string; + /** + * @description What the board should call this gate. "waiting for CI on main" beats a + * URL. + */ + label?: string | null; + /** Format: int64 */ + poll_interval_secs?: number | null; + }; + CreateGroupRequest: { + agent_ids?: string[]; + color?: string | null; + name: string; + }; + CreateHumanRequest: { + bio?: string | null; + description?: string | null; + discord_id?: string | null; + display_name?: string | null; + email?: string | null; + id: string; + role?: string | null; + slack_id?: string | null; + telegram_id?: string | null; + }; + CreateLinkRequest: { + direction?: string; + from: string; + kind?: string; + to: string; + }; + CreateMcpServerRequest: { + args?: string[]; + command?: string | null; + enabled?: boolean; + env?: { + [key: string]: string; + }; + headers?: { + [key: string]: string; + }; + name: string; + transport: string; + url?: string | null; + }; + CreateMessagingInstanceRequest: { + credentials?: components["schemas"]["InstanceCredentials"]; + enabled?: boolean | null; + name?: string | null; + platform: string; + }; + CreatePageRequest: { + /** @description Who is creating this page: agent_id or user identifier. */ + author_id?: string; + author_type?: string; + content?: string; + edit_summary?: string | null; + page_type: string; + related?: string[]; + title: string; + }; + CreatePortalConversationRequest: { + agent_id: string; + settings?: null | components["schemas"]["ConversationSettings"]; + title?: string | null; + }; + CreateProjectRequest: { + /** @description When true, scan root_path for git repos and register them automatically. */ + auto_discover?: boolean; + description?: string | null; + icon?: string | null; + name: string; + root_path: string; + settings?: unknown; + tags?: string[]; + }; + CreateRepoRequest: { + default_branch?: string | null; + description?: string | null; + name: string; + path: string; + remote_url?: string | null; + }; + CreateTaskRequest: { + /** + * @description Agent assigned to execute. Defaults to `owner_agent_id`, unless + * `required_capabilities` is set — then the task is pooled and nobody is + * named until an agent claims it. + */ + assigned_agent_id?: string | null; + created_by?: string | null; + /** @description Task numbers that must finish before this one may run. */ + depends_on?: number[]; + description?: string | null; + metadata?: unknown; + /** @description Agent that owns (created) this task. */ + owner_agent_id: string; + priority?: string | null; + /** @description Project this task acts on. */ + project_id?: string | null; + /** @description Repo within the project. A project holds many repos. */ + repo_id?: string | null; + /** + * @description What this task needs, instead of who should do it. + * + * Any agent declaring all of these may claim it. Mutually exclusive with + * `assigned_agent_id`; sending both is rejected rather than resolved. + */ + required_capabilities?: string[] | null; + source_memory_id?: string | null; + /** + * @description Status to create the task in. Defaults to `pending_approval`. + * + * The dashboard has always sent `backlog` here; the field simply did not + * exist, so serde dropped it and every task created from the UI came back + * awaiting an approval the creator had just given by clicking "create". + */ + status?: string | null; + subtasks?: components["schemas"]["TaskSubtask"][]; + title: string; + /** @description Worktree to execute in. */ + worktree_id?: string | null; + }; + CreateWorktreeRequest: { + branch: string; + repo_id: string; + start_point?: string | null; + worktree_name?: string | null; + }; + CronActionResponse: { + message: string; + success: boolean; + }; + /** @description Entry in the cron execution log. */ + CronExecutionEntry: { + cron_id?: string | null; + delivery_attempted: boolean; + delivery_error?: string | null; + delivery_succeeded?: boolean | null; + executed_at: string; + execution_error?: string | null; + execution_succeeded: boolean; + id: string; + result_summary?: string | null; + success: boolean; + }; + CronExecutionsResponse: { + executions: components["schemas"]["CronExecutionEntry"][]; + }; + CronJobInfo: { + active_hours?: [ + number, + number + ] | null; + cron_expr?: string | null; + delivery_target: string; + enabled: boolean; + id: string; + /** Format: int64 */ + interval_secs: number; + prompt: string; + run_once: boolean; + /** Format: int64 */ + timeout_secs?: number | null; + }; + CronJobWithStats: { + active_hours?: [ + number, + number + ] | null; + cron_expr?: string | null; + /** Format: int64 */ + delivery_failure_count: number; + /** Format: int64 */ + delivery_skipped_count: number; + /** Format: int64 */ + delivery_success_count: number; + delivery_target: string; + enabled: boolean; + /** Format: int64 */ + execution_failure_count: number; + /** Format: int64 */ + execution_success_count: number; + id: string; + /** Format: int64 */ + interval_secs: number; + last_executed_at?: string | null; + prompt: string; + run_once: boolean; + /** Format: int64 */ + timeout_secs?: number | null; + }; + CronListResponse: { + jobs: components["schemas"]["CronJobWithStats"][]; + timezone: string; + }; + DayCount: { + /** Format: int64 */ + count: number; + date: string; + }; + /** + * @description What a decision inside a loop does on the second pass. + * + * See the migration for the argument. The short version: `EachPass` is the + * default because pass 2 exists precisely because the artefact changed, and + * reusing pass 1's answer would credit a person with approving work they never + * saw. + * @enum {string} + */ + DecisionAsk: "each_pass" | "once"; + /** + * @description How a decision was settled — the column the whole feature turns on. + * + * A defaulted answer that looks identical to a human one in the run record is + * the provenance problem returning through a side door, so these are four + * values rather than a nullable `answered_by`. + * @enum {string} + */ + DecisionOutcome: "answered" | "defaulted" | "timed_out" | "carried"; + /** + * @description What happens to a decision nobody answers. + * + * Three values because they have three recoveries, and the middle one is the + * one the design doc says to get right. + * @enum {string} + */ + DecisionTimeoutAction: "wait" | "default" | "fail"; + DeclareRepoDependencyRequest: { + /** @description The repo depended upon. */ + depends_on_repo_id: string; + /** @description Free-text label (`generated_from`, `consumes`, `vendors`, …). */ + kind?: string | null; + note?: string | null; + /** @description The dependent repo — the one that has to change when the other does. */ + repo_id: string; + }; + /** + * @description Delegation mode controls how the conversation handles tools. + * @enum {string} + */ + DelegationMode: "standard" | "direct"; + DeleteBindingRequest: { + adapter?: string | null; + agent_id: string; + channel: string; + chat_id?: string | null; + guild_id?: string | null; + team_id?: string | null; + workspace_id?: string | null; + }; + DeleteBindingResponse: { + message: string; + success: boolean; + }; + DeleteMessagingInstanceRequest: { + name?: string | null; + platform: string; + }; + DeleteSecretResponse: { + deleted: string; + warning?: string | null; + }; + /** + * @description What came of one accepted webhook delivery. + * + * Only ever written for a delivery that authenticated. A rejected delivery + * writes nothing at all — see [`WorkflowTriggerStore::authenticate_webhook`]. + * @enum {string} + */ + DeliveryOutcome: "launched" | "unmapped" | "refused" | "errored"; + DeliveryResponse: { + detail: string; + outcome: string; + run_id?: string | null; + }; + /** + * @description Deployment environment, detected from SPACEBOT_DEPLOYMENT env var. + * @enum {string} + */ + Deployment: "docker" | "hosted" | "native"; + DisconnectPlatformRequest: { + adapter?: string | null; + platform: string; + }; + DiscordSection: { + allow_bot_messages: boolean; + enabled: boolean; + }; + DiscordUpdate: { + allow_bot_messages?: boolean | null; + }; + DiskUsageEntry: { + /** Format: int64 */ + bytes: number; + is_dir: boolean; + name: string; + }; + DiskUsageResponse: { + entries: components["schemas"]["DiskUsageEntry"][]; + /** Format: int64 */ + total_bytes: number; + }; + EditPageRequest: { + author_id?: string; + author_type?: string; + edit_summary?: string | null; + new_string: string; + old_string: string; + replace_all?: boolean; + }; + EncryptResponse: { + master_key: string; + message: string; + }; + /** @description Portable backup format for all secrets in a store. */ + ExportData: { + /** + * @description Whether the source store had encryption enabled (informational only — + * values in this struct are always plaintext). + */ + encrypted: boolean; + /** @description All secrets with their metadata. */ + entries: components["schemas"]["ExportEntry"][]; + /** + * Format: int32 + * @description Format version (currently 1). + */ + version: number; + }; + /** @description A single secret in the export format. */ + ExportEntry: { + category: components["schemas"]["SecretCategory"]; + /** Format: date-time */ + created_at: string; + name: string; + /** + * @description Scope this secret belongs to. Older exports without a `scope` field + * import as `InstanceShared` so existing backups continue to load. + */ + scope?: components["schemas"]["SecretScope"]; + /** Format: date-time */ + updated_at: string; + value: string; + }; + /** + * @description What a *false* answer from a gate means. + * + * The entire difference between "is CI green yet?" and "should this branch + * run?". The two ask the same predicate with opposite failure modes: waiting + * forever is correct for the first and a deadlock for the second. + * @enum {string} + */ + GateDisposition: "wait" | "route"; + /** + * @description What kind of fact a gate waits on. + * + * Deliberately no vendor SDKs. `Http` covers GitHub, GitLab, Buildkite, and + * Jenkins without knowing what any of them are. + * @enum {string} + */ + GateKind: "http" | "task_output"; + /** + * @description The state of a gate, and the reason the four are not three. + * + * See the module docs. Each variant answers a different question: should we + * poll again, should the task run, and whose problem is it? + * @enum {string} + */ + GateResult: "pending" | "satisfied" | "failed" | "erroring" | "routed"; + GlobalSettingsResponse: { + api_bind: string; + api_enabled: boolean; + /** Format: int32 */ + api_port: number; + brave_search_key?: string | null; + company_name: string; + opencode: components["schemas"]["OpenCodeSettingsResponse"]; + ssh_enabled: boolean; + worker_log_mode: string; + }; + GlobalSettingsUpdate: { + api_bind?: string | null; + api_enabled?: boolean | null; + /** Format: int32 */ + api_port?: number | null; + brave_search_key?: string | null; + company_name?: string | null; + opencode?: null | components["schemas"]["OpenCodeSettingsUpdate"]; + ssh_enabled?: boolean | null; + worker_log_mode?: string | null; + }; + GlobalSettingsUpdateResponse: { + message: string; + requires_restart: boolean; + success: boolean; + }; + HealthResponse: { + status: string; + }; + HeatmapCell: { + /** Format: int64 */ + count: number; + /** Format: int64 */ + day: number; + /** Format: int64 */ + hour: number; + }; + IdentityResponse: { + identity?: string | null; + role?: string | null; + soul?: string | null; + }; + IdentityUpdateRequest: { + agent_id: string; + identity?: string | null; + role?: string | null; + soul?: string | null; + }; + IdleResponse: { + active_branches: number; + active_workers: number; + idle: boolean; + }; + ImportBody: components["schemas"]["ExportData"] & { + /** @description Whether to overwrite existing secrets with the same name. */ + overwrite?: boolean; + }; + IngestDeleteResponse: { + success: boolean; + }; + IngestFileInfo: { + /** Format: int64 */ + chunks_completed: number; + completed_at?: string | null; + content_hash: string; + /** Format: int64 */ + file_size: number; + filename: string; + started_at: string; + status: string; + /** Format: int64 */ + total_chunks: number; + }; + IngestFilesResponse: { + files: components["schemas"]["IngestFileInfo"][]; + }; + IngestUploadResponse: { + uploaded: string[]; + }; + InstallSkillRequest: { + agent_id: string; + instance?: boolean; + spec: string; + }; + InstallSkillResponse: { + installed: string[]; + }; + InstanceCredentials: { + discord_token?: string | null; + email_from_address?: string | null; + email_imap_host?: string | null; + email_imap_password?: string | null; + /** Format: int32 */ + email_imap_port?: number | null; + email_imap_username?: string | null; + email_smtp_host?: string | null; + email_smtp_password?: string | null; + /** Format: int32 */ + email_smtp_port?: number | null; + email_smtp_username?: string | null; + mattermost_base_url?: string | null; + mattermost_token?: string | null; + signal_account?: string | null; + signal_dm_allowed_users?: string | null; + signal_http_url?: string | null; + slack_app_token?: string | null; + slack_bot_token?: string | null; + telegram_token?: string | null; + twitch_client_id?: string | null; + twitch_client_secret?: string | null; + twitch_oauth_token?: string | null; + twitch_refresh_token?: string | null; + twitch_username?: string | null; + webhook_auth_token?: string | null; + webhook_bind?: string | null; + /** Format: int32 */ + webhook_port?: number | null; + }; + InstanceOverviewResponse: { + agents: components["schemas"]["AgentSummary"][]; + /** Format: int32 */ + pid: number; + /** Format: int64 */ + uptime_seconds: number; + version: string; + }; + LaunchRequest: { + /** @description The single payload the whole pipeline is driven from. */ + inputs?: unknown; + /** + * @description Agent credited with the launch, and the default assignee for any step + * that does not name one. + */ + launched_by: string; + }; + LaunchResponse: { + run: components["schemas"]["WorkflowRun"]; + /** @description Emitted task numbers, keyed by the step they came from. */ + task_numbers: { + [key: string]: number; + }; + }; + /** + * @description Which arm of a loop's exit a downstream task is on. + * + * A loop's exit is a branch, not a join. Both arms wait on the same body and + * exactly one of them runs, so they are told apart by name rather than by + * which happens to be wired — "the loop finished" is not a condition anything + * can act on. + * @enum {string} + */ + LoopArm: "normal" | "on_exhausted"; + /** + * @description What the boundary decided at the end of one iteration. + * + * Four values rather than one "handled" flag, because they recover + * differently: a run that gave up must not read like a run that succeeded, and + * a run parked for a person must not read like one that took a branch. + * @enum {string} + */ + LoopResolution: "converged" | "iterated" | "exhausted_routed" | "exhausted_blocked"; + McpAgentStatus: { + agent_id: string; + servers: components["schemas"]["McpServerInfo"][]; + }; + McpConnectionState: "connecting" | "connected" | { + failed: string; + } | "disconnected"; + McpServerInfo: { + enabled: boolean; + name: string; + state: string; + transport: string; + }; + McpServerStatus: { + enabled: boolean; + name: string; + state: components["schemas"]["McpConnectionState"]; + transport: string; + }; + MemoriesListResponse: { + memories: components["schemas"]["Memory"][]; + total: number; + }; + MemoriesSearchResponse: { + results: components["schemas"]["MemorySearchResult"][]; + }; + /** @description Memory structure. */ + Memory: { + /** Format: int64 */ + access_count: number; + channel_id?: string | null; + content: string; + /** Format: date-time */ + created_at: string; + /** + * @description Soft-delete flag. Forgotten memories are excluded from search and recall + * but remain in the database. + */ + forgotten: boolean; + id: string; + /** Format: float */ + importance: number; + /** Format: date-time */ + last_accessed_at: string; + memory_type: components["schemas"]["MemoryType"]; + source?: string | null; + /** Format: date-time */ + updated_at: string; + }; + MemoryGraphNeighborsResponse: { + edges: components["schemas"]["Association"][]; + nodes: components["schemas"]["Memory"][]; + }; + MemoryGraphResponse: { + edges: components["schemas"]["Association"][]; + nodes: components["schemas"]["Memory"][]; + total: number; + }; + /** + * @description Memory mode controls how memory is used in a conversation. + * @enum {string} + */ + MemoryMode: "full" | "ambient" | "off"; + MemoryPersistenceSection: { + enabled: boolean; + message_interval: number; + }; + MemoryPersistenceUpdate: { + enabled?: boolean | null; + message_interval?: number | null; + }; + /** @description Search result combining memory with relevance score. */ + MemorySearchResult: { + memory: components["schemas"]["Memory"]; + rank: number; + /** Format: float */ + score: number; + }; + /** + * @description Memory types. + * @enum {string} + */ + MemoryType: "fact" | "preference" | "decision" | "identity" | "event" | "observation" | "goal" | "todo"; + MessagesResponse: { + has_more: boolean; + items: components["schemas"]["TimelineItem"][]; + }; + MessagingInstanceActionResponse: { + message: string; + success: boolean; + }; + MessagingStatusResponse: { + discord: components["schemas"]["PlatformStatus"]; + email: components["schemas"]["PlatformStatus"]; + instances: components["schemas"]["AdapterInstanceStatus"][]; + mattermost: components["schemas"]["PlatformStatus"]; + signal: components["schemas"]["PlatformStatus"]; + slack: components["schemas"]["PlatformStatus"]; + telegram: components["schemas"]["PlatformStatus"]; + twitch: components["schemas"]["PlatformStatus"]; + webhook: components["schemas"]["PlatformStatus"]; + }; + MigrateResponse: { + message: string; + migrated: components["schemas"]["MigrationItem"][]; + skipped: string[]; + }; + MigrationItem: { + category: components["schemas"]["SecretCategory"]; + config_key: string; + secret_name: string; + }; + ModelInfo: { + /** + * Format: int64 + * @description Context window size in tokens, if known + */ + context_window?: number | null; + /** @description Full routing string (e.g. "openrouter/anthropic/claude-sonnet-4") */ + id: string; + /** @description Whether this model accepts audio input. */ + input_audio: boolean; + /** @description Human-readable name */ + name: string; + /** @description Provider ID for routing ("anthropic", "openrouter", "openai", etc.) */ + provider: string; + /** @description Whether this model has reasoning/thinking capability */ + reasoning: boolean; + /** @description Whether this model supports tool/function calling */ + tool_call: boolean; + }; + /** @description Model option for the defaults response. */ + ModelOption: { + /** @description Context window size. */ + context_window: number; + /** @description Model ID (e.g. "anthropic/claude-sonnet-4"). */ + id: string; + /** @description Display name (e.g. "Claude Sonnet 4"). */ + name: string; + /** @description Provider name (e.g. "anthropic"). */ + provider: string; + /** @description Whether the model supports thinking/claude-style extended thinking. */ + supports_thinking: boolean; + /** @description Whether the model supports tools. */ + supports_tools: boolean; + }; + /** + * @description Per-process model overrides. Each field, when set, overrides the + * routing config for that specific process type within this conversation. + */ + ModelOverrides: { + branch?: string | null; + channel?: string | null; + compactor?: string | null; + worker?: string | null; + }; + ModelsResponse: { + models: components["schemas"]["ModelInfo"][]; + }; + MutationResponse: { + message: string; + success: boolean; + }; + /** @description A persisted notification row. */ + Notification: { + action_url?: string | null; + agent_id?: string | null; + body?: string | null; + created_at: string; + dismissed_at?: string | null; + id: string; + kind: string; + metadata?: string | null; + read_at?: string | null; + related_entity_id?: string | null; + related_entity_type?: string | null; + severity: string; + title: string; + }; + NotificationsResponse: { + notifications: components["schemas"]["Notification"][]; + }; + OpenCodePermissionsResponse: { + bash: string; + edit: string; + webfetch: string; + }; + OpenCodePermissionsUpdate: { + bash?: string | null; + edit?: string | null; + webfetch?: string | null; + }; + OpenCodeSettingsResponse: { + enabled: boolean; + /** Format: int32 */ + max_restart_retries: number; + max_servers: number; + path: string; + permissions: components["schemas"]["OpenCodePermissionsResponse"]; + /** Format: int64 */ + server_startup_timeout_secs: number; + }; + OpenCodeSettingsUpdate: { + enabled?: boolean | null; + /** Format: int32 */ + max_restart_retries?: number | null; + max_servers?: number | null; + path?: string | null; + permissions?: null | components["schemas"]["OpenCodePermissionsUpdate"]; + /** Format: int64 */ + server_startup_timeout_secs?: number | null; + }; + /** @description A directory under `.worktrees/` that nothing alive accounts for. */ + OrphanWorktree: { + branch: string; + path: string; + project_id: string; + /** @description Why we think nobody owns it, in words. */ + reason: string; + repo_id: string; + /** @description The run it appears to have belonged to, when the name still says so. */ + run_id?: string | null; + }; + /** @description Worktrees found on disk that no live run accounts for. */ + OrphanWorktreesResponse: { + orphans: components["schemas"]["OrphanWorktree"][]; + }; + PlatformCredentials: { + discord_token?: string | null; + email_from_address?: string | null; + email_from_name?: string | null; + email_imap_host?: string | null; + email_imap_password?: string | null; + /** Format: int32 */ + email_imap_port?: number | null; + email_imap_username?: string | null; + email_smtp_host?: string | null; + email_smtp_password?: string | null; + /** Format: int32 */ + email_smtp_port?: number | null; + email_smtp_username?: string | null; + slack_app_token?: string | null; + slack_bot_token?: string | null; + telegram_token?: string | null; + twitch_client_id?: string | null; + twitch_client_secret?: string | null; + twitch_oauth_token?: string | null; + twitch_refresh_token?: string | null; + twitch_username?: string | null; + }; + PlatformStatus: { + configured: boolean; + enabled: boolean; + }; + PortalConversation: { + agent_id: string; + archived: boolean; + /** Format: date-time */ + created_at: string; + id: string; + settings?: null | components["schemas"]["ConversationSettings"]; + title: string; + title_source: string; + /** Format: date-time */ + updated_at: string; + }; + PortalConversationResponse: { + conversation: components["schemas"]["PortalConversation"]; }; - ActivityDayCount: { - /** Format: int64 */ - branches: number; - date: string; + PortalConversationSummary: { + agent_id: string; + archived: boolean; + /** Format: date-time */ + created_at: string; + id: string; + /** Format: date-time */ + last_message_at?: string | null; + last_message_preview?: string | null; + last_message_role?: string | null; /** Format: int64 */ - workers: number; + message_count: number; + settings?: null | components["schemas"]["ConversationSettings"]; + title: string; + title_source: string; + /** Format: date-time */ + updated_at: string; }; - ActivityResponse: { - daily: components["schemas"]["ActivityDay"][]; - totals: components["schemas"]["ActivityTotals"]; + PortalConversationsResponse: { + conversations: components["schemas"]["PortalConversationSummary"][]; }; - ActivityTotals: { - /** Format: int64 */ - active_channels: number; + PortalHistoryMessage: { + content: string; + id: string; + role: string; + }; + PortalSendRequest: { + agent_id: string; + /** @description IDs of pre-uploaded attachments to include with this message. */ + attachment_ids?: string[]; + message: string; + sender_name?: string; + session_id: string; + }; + PortalSendResponse: { + ok: boolean; + }; + /** @description A fully loaded preset with all identity file content. */ + Preset: { + identity: string; + meta: components["schemas"]["PresetMeta"]; + role: string; + soul: string; + }; + /** + * @description Default operational parameters suggested by a preset. + * + * Model routing is intentionally excluded — presets are provider-agnostic. + * The factory conversation handles model selection at creation time when the + * user's available providers are known. + */ + PresetDefaults: { + /** Format: int32 */ + max_concurrent_workers?: number | null; + /** Format: int32 */ + max_turns?: number | null; + }; + /** @description Metadata for a preset archetype (returned in list responses). */ + PresetMeta: { + defaults?: components["schemas"]["PresetDefaults"]; + description: string; + icon: string; + id: string; + name: string; + tags?: string[]; + }; + ProcessTokens: { /** Format: int64 */ - branches: number; + cache_read: number; + /** Format: double */ + cost_usd: number; /** Format: int64 */ - cortex: number; + input: number; /** Format: int64 */ - cron: number; + output: number; /** Format: int64 */ - messages: number; - tokens: components["schemas"]["TokenSummary"]; + reasoning: number; + }; + Project: { + created_at: string; + description: string; + icon: string; + id: string; + logo_path?: string | null; + name: string; + root_path: string; + settings: unknown; /** Format: int64 */ - workers: number; + sort_order: number; + status: components["schemas"]["ProjectStatus"]; + tags: string[]; + updated_at: string; }; - AdapterInstanceStatus: { - binding_count: number; - configured: boolean; - enabled: boolean; - /** @description `None` means the default instance for the platform. */ - name?: string | null; - platform: string; - runtime_key: string; + ProjectListResponse: { + projects: components["schemas"]["Project"][]; }; - AgentConfigResponse: { - browser: components["schemas"]["BrowserSection"]; - channel: components["schemas"]["ChannelSection"]; - coalesce: components["schemas"]["CoalesceSection"]; - compaction: components["schemas"]["CompactionSection"]; - cortex: components["schemas"]["CortexSection"]; - discord: components["schemas"]["DiscordSection"]; - memory_persistence: components["schemas"]["MemoryPersistenceSection"]; - projects: components["schemas"]["ProjectsSection"]; - routing: components["schemas"]["RoutingSection"]; - sandbox: components["schemas"]["SandboxSection"]; - tuning: components["schemas"]["TuningSection"]; - warmup: components["schemas"]["WarmupSection"]; + ProjectRepo: { + created_at: string; + /** @description Currently checked-out branch (may differ from `default_branch`). */ + current_branch?: string | null; + default_branch: string; + description: string; + /** Format: int64 */ + disk_usage_bytes?: number | null; + id: string; + name: string; + path: string; + project_id: string; + remote_url: string; + updated_at: string; }; - AgentConfigUpdateRequest: { - agent_id: string; - browser?: null | components["schemas"]["BrowserUpdate"]; - channel?: null | components["schemas"]["ChannelUpdate"]; - coalesce?: null | components["schemas"]["CoalesceUpdate"]; - compaction?: null | components["schemas"]["CompactionUpdate"]; - cortex?: null | components["schemas"]["CortexUpdate"]; - discord?: null | components["schemas"]["DiscordUpdate"]; - memory_persistence?: null | components["schemas"]["MemoryPersistenceUpdate"]; - projects?: null | components["schemas"]["ProjectsUpdate"]; - routing?: null | components["schemas"]["RoutingUpdate"]; - sandbox?: null | components["schemas"]["SandboxUpdate"]; - tuning?: null | components["schemas"]["TuningUpdate"]; - warmup?: null | components["schemas"]["WarmupUpdate"]; + ProjectResponse: components["schemas"]["ProjectWithRelations"]; + /** @enum {string} */ + ProjectStatus: "active" | "archived"; + /** @description Full project with nested repos and worktrees for API responses. */ + ProjectWithRelations: components["schemas"]["Project"] & { + /** + * @description Declared repo-to-repo relationships (#29). Travels with the repos it + * describes so the project view can draw the arrows without a second + * request, and so a relationship is never something you have to know to + * go and ask for. + */ + repo_dependencies: components["schemas"]["RepoDependency"][]; + repos: components["schemas"]["ProjectRepo"][]; + worktrees: components["schemas"]["ProjectWorktreeWithRepo"][]; }; - /** @description Summary of an agent's configuration, exposed via the API. */ - AgentInfo: { - context_window: number; - display_name?: string | null; - gradient_end?: string | null; - gradient_start?: string | null; + ProjectWorktree: { + branch: string; + created_at: string; + created_by: string; + /** Format: int64 */ + disk_usage_bytes?: number | null; id: string; - max_concurrent_branches: number; - max_concurrent_workers: number; - max_turns: number; - role?: string | null; - workspace: string; + name: string; + path: string; + project_id: string; + repo_id: string; + updated_at: string; }; - AgentMcpResponse: { - servers: components["schemas"]["McpServerStatus"][]; + /** @description Worktree with the source repo name resolved. */ + ProjectWorktreeWithRepo: components["schemas"]["ProjectWorktree"] & { + repo_name: string; }; - AgentOverviewResponse: { - activity_daily: components["schemas"]["ActivityDayCount"][]; - activity_heatmap: components["schemas"]["HeatmapCell"][]; - channel_count: number; - cron_jobs: components["schemas"]["CronJobInfo"][]; - last_bulletin_at?: string | null; - latest_bulletin?: string | null; - memory_counts: { - [key: string]: number; - }; - memory_daily: components["schemas"]["DayCount"][]; + ProjectsSection: { + auto_discover_repos: boolean; + auto_discover_worktrees: boolean; /** Format: int64 */ - memory_total: number; - recent_cortex_events: components["schemas"]["CortexEvent"][]; + disk_usage_warning_threshold: number; + use_worktrees: boolean; + worktree_name_template: string; }; - /** @description Persisted agent profile generated by the cortex. */ - AgentProfile: { - agent_id: string; - avatar_seed?: string | null; - bio?: string | null; - display_name?: string | null; - generated_at: string; - status?: string | null; - updated_at: string; + ProjectsUpdate: { + auto_discover_repos?: boolean | null; + auto_discover_worktrees?: boolean | null; + /** Format: int64 */ + disk_usage_warning_threshold?: number | null; + use_worktrees?: boolean | null; + worktree_name_template?: string | null; }; - AgentProfileResponse: { - profile?: null | components["schemas"]["AgentProfile"]; + PromptCaptureBody: { + channel_id: string; + enabled: boolean; }; - AgentSummary: { - activity_sparkline: number[]; - channel_count: number; - cron_job_count: number; + /** @description A configured provider, as reported to the UI. Never includes the API key. */ + ProviderEntry: { + /** @description `"anthropic"` or `"openai_compatible"`. */ + api_type: string; + base_url: string; + /** @description Optional human-readable label from `name`. */ + display_name?: string | null; + /** + * @description Whether an API key resolves for this provider. False means the block + * exists but its `secret:`/`env:` reference is unresolvable. + */ + has_key: boolean; + /** @description Provider id — the prefix in `provider/model` routing strings. */ id: string; - last_activity_at?: string | null; - last_bulletin_at?: string | null; - /** Format: int64 */ - memory_total: number; - profile?: null | components["schemas"]["AgentProfile"]; }; - AgentsResponse: { - agents: components["schemas"]["AgentInfo"][]; + ProviderModelTestRequest: { + api_key: string; + api_type?: string | null; + base_url?: string | null; + model: string; + provider: string; }; - ApproveRequest: { - approved_by?: string | null; + ProviderModelTestResponse: { + message: string; + model: string; + provider: string; + sample?: string | null; + success: boolean; }; - AssignRequest: { - assigned_agent_id: string; + ProviderUpdateRequest: { + api_key: string; + /** @description `"anthropic"` or `"openai_compatible"`. Defaults to `openai_compatible`. */ + api_type?: string | null; + /** @description Full path prefix. Required unless `api_type` is `anthropic`. */ + base_url?: string | null; + /** + * @description Routing string to apply to defaults and the default agent, e.g. + * `"litellm/claude-sonnet-4"`. Must be prefixed with `provider`. + */ + model: string; + /** @description Provider id to create or replace, e.g. `"litellm"`. */ + provider: string; }; - /** @description Association between memories. */ - Association: { - /** Format: date-time */ - created_at: string; - id: string; - relation_type: components["schemas"]["RelationType"]; - source_id: string; - target_id: string; - /** Format: float */ - weight: number; + ProviderUpdateResponse: { + message: string; + success: boolean; }; - AttachmentInfo: { - created_at: string; - id: string; - mime_type: string; - original_filename: string; - /** Format: int64 */ - size_bytes: number; + ProvidersResponse: { + /** + * @description Whether Anthropic OAuth credentials are on disk (`spacebot auth login`). + * This authenticates the `anthropic` provider without an API key. + */ + anthropic_oauth: boolean; + has_any: boolean; + providers: components["schemas"]["ProviderEntry"][]; }; - AttachmentListResponse: { - attachments: components["schemas"]["AttachmentInfo"][]; + PutSecretBody: { + category?: null | components["schemas"]["SecretCategory"]; + value: string; }; - AttachmentUploadResponse: { - id: string; - mime_type: string; - original_filename: string; - /** Format: int64 */ - size_bytes: number; + PutSecretResponse: { + category: components["schemas"]["SecretCategory"]; + message: string; + name: string; + reload_required: boolean; }; - AuthorizedKeyRequest: { - public_key: string; + RawConfigResponse: { + content: string; }; - AuthorizedKeyResponse: { + RawConfigUpdateRequest: { + content: string; + }; + RawConfigUpdateResponse: { message: string; success: boolean; }; - BinaryEntry: { - modified?: string | null; - name: string; - /** Format: int64 */ - size: number; - }; - BindingResponse: { - adapter?: string | null; + ReconnectMcpRequest: { agent_id: string; - channel: string; - channel_ids: string[]; - chat_id?: string | null; - dm_allowed_users: string[]; - guild_id?: string | null; - require_mention: boolean; - team_id?: string | null; - workspace_id?: string | null; + server_name: string; }; - BindingsListResponse: { - bindings: components["schemas"]["BindingResponse"][]; + RegistryBrowseResponse: { + has_more: boolean; + skills: components["schemas"]["RegistrySkill"][]; + /** Format: int64 */ + total?: number | null; }; - BrowserSection: { - close_policy: string; - enabled: boolean; - evaluate_enabled: boolean; - headless: boolean; - persist_session: boolean; + RegistrySearchResponse: { + count: number; + query: string; + skills: components["schemas"]["RegistrySkill"][]; }; - BrowserUpdate: { - close_policy?: null | components["schemas"]["ClosePolicy"]; - enabled?: boolean | null; - evaluate_enabled?: boolean | null; - headless?: boolean | null; - persist_session?: boolean | null; + RegistrySkill: { + description?: string | null; + id?: string | null; + /** Format: int64 */ + installs: number; + name: string; + skillId: string; + source: string; }; - CancelProcessRequest: { - channel_id: string; - process_id: string; - process_type: string; + RegistrySkillContentResponse: { + content?: string | null; + skill_id: string; + source: string; }; - CancelProcessResponse: { - message: string; + /** + * @description Relation types for memory associations. + * @enum {string} + */ + RelationType: "related_to" | "updates" | "contradicts" | "caused_by" | "result_of" | "part_of"; + RemoveSkillRequest: { + agent_id: string; + name: string; + }; + RemoveSkillResponse: { + path?: string | null; success: boolean; }; - ChannelResponse: { - agent_id: string; + ReorderProjectsRequest: { + /** @description Project IDs in the desired display order (first = sort_order 0). */ + ids: string[]; + }; + /** + * @description One declared edge, with both repo names resolved so a caller can render it + * without a second query. + */ + RepoDependency: { created_at: string; - display_name?: string | null; - id: string; - is_active: boolean; - last_activity_at: string; - model?: string | null; - platform: string; - response_mode?: string | null; + /** @description The repo depended upon. */ + depends_on_repo_id: string; + depends_on_repo_name: string; + /** + * @description Free-text label (`generated_from`, `consumes`, `vendors`, …). Nothing + * branches on it; it is shown to people. + */ + kind?: string | null; + /** @description Why the dependency exists, in the author's words. */ + note?: string | null; + project_id: string; + /** @description The dependent repo — the one that has to change when the other does. */ + repo_id: string; + repo_name: string; }; - ChannelSection: { - listen_only_mode: boolean; + RepoDependencyListResponse: { + dependencies: components["schemas"]["RepoDependency"][]; }; - ChannelSettingsResponse: { - conversation_id: string; - settings: components["schemas"]["ConversationSettings"]; + RepoDependencyResponse: { + dependency: components["schemas"]["RepoDependency"]; }; - ChannelUpdate: { - listen_only_mode?: boolean | null; + /** + * @description The declared neighbourhood of one repo, in both directions. + * + * Returned by the suggestion query. The step editor holds a repo and needs + * both halves: "you are editing a step in `api`; `web` depends on it" comes + * from `dependents`, and "this step is in `web`, which is generated from + * `api`" comes from `dependencies`. + */ + RepoDependencySuggestions: { + /** @description Repos this one declares a dependency **on** — upstream. */ + dependencies: components["schemas"]["RepoDependency"][]; + /** + * @description Repos that declare a dependency **on** this one — downstream. Editing + * this repo is a reason to offer a step in each of these. + */ + dependents: components["schemas"]["RepoDependency"][]; + /** @description The repo the question was asked about. */ + repo_id: string; }; - ChannelsResponse: { - channels: components["schemas"]["ChannelResponse"][]; + RepoResponse: { + repo: components["schemas"]["ProjectRepo"]; }; /** - * @description What happens when a worker explicitly calls "close" on the browser. + * @description Response mode controls how the channel handles incoming messages. * @enum {string} */ - ClosePolicy: "close_browser" | "close_tabs" | "detach"; - CoalesceSection: { - /** Format: int64 */ - debounce_ms: number; - enabled: boolean; + ResponseMode: "active" | "observe" | "mention_only"; + RestoreVersionRequest: { + author_id?: string; + author_type?: string; /** Format: int64 */ - max_wait_ms: number; - min_messages: number; - multi_user_only: boolean; + version: number; }; - CoalesceUpdate: { - /** Format: int64 */ - debounce_ms?: number | null; - enabled?: boolean | null; + RoutingSection: { + branch: string; + branch_thinking_effort: string; + channel: string; + channel_thinking_effort: string; + compactor: string; + compactor_thinking_effort: string; + cortex: string; + cortex_thinking_effort: string; /** Format: int64 */ - max_wait_ms?: number | null; - min_messages?: number | null; - multi_user_only?: boolean | null; - }; - CompactionSection: { - /** Format: float */ - aggressive_threshold: number; - /** Format: float */ - background_threshold: number; - /** Format: float */ - emergency_threshold: number; - }; - CompactionUpdate: { - /** Format: float */ - aggressive_threshold?: number | null; - /** Format: float */ - background_threshold?: number | null; - /** Format: float */ - emergency_threshold?: number | null; + rate_limit_cooldown_secs: number; + voice: string; + worker: string; + worker_thinking_effort: string; }; - /** @description Response payload for conversation defaults endpoint. */ - ConversationDefaultsResponse: { - /** @description All available models. */ - available_models: components["schemas"]["ModelOption"][]; - /** @description Current default delegation mode. */ - delegation: components["schemas"]["DelegationMode"]; - /** @description Available delegation modes. */ - delegation_modes: string[]; - /** @description Current default memory mode. */ - memory: components["schemas"]["MemoryMode"]; - /** @description Available memory modes. */ - memory_modes: string[]; - /** @description Current default model name (from agent config). */ - model: string; - /** @description Current default worker context settings. */ - worker_context: components["schemas"]["WorkerContextMode"]; - /** @description Available worker history modes. */ - worker_history_modes: string[]; - /** @description Available worker memory modes. */ - worker_memory_modes: string[]; + RoutingUpdate: { + branch?: string | null; + branch_thinking_effort?: string | null; + channel?: string | null; + channel_thinking_effort?: string | null; + compactor?: string | null; + compactor_thinking_effort?: string | null; + cortex?: string | null; + cortex_thinking_effort?: string | null; + /** Format: int64 */ + rate_limit_cooldown_secs?: number | null; + voice?: string | null; + worker?: string | null; + worker_thinking_effort?: string | null; }; - /** @description Per-conversation settings that control behavior. */ - ConversationSettings: { - /** @description How tools work in this conversation. */ - delegation?: components["schemas"]["DelegationMode"]; - /** @description How memory is used in this conversation. */ - memory?: components["schemas"]["MemoryMode"]; + /** @description A run and the tasks it produced. */ + RunDetailResponse: { + run: components["schemas"]["WorkflowRun"]; + tasks: components["schemas"]["Task"][]; + }; + RunListResponse: { + runs: components["schemas"]["WorkflowRun"][]; + }; + /** + * @description How a run is going. + * + * `stuck` is the value this enum exists for. The other four are reductions + * over tasks that a caller could have computed itself; `stuck` is not + * derivable from any single task, because every task in a wedged run looks + * individually reasonable — a loop body parked for a person, a step behind a + * gate that stopped polling, a placeholder that will never expand. Only the + * run can see that none of them will ever move. + * + * The distinction that matters most is the one this enum does *not* make: + * `stuck` versus still `running`. A run waiting on a gate that can still open + * is waiting, not stuck, and reporting it as stuck teaches people to ignore + * the status — which is worse than the silence it replaced. + * @enum {string} + */ + RunStatus: "running" | "succeeded" | "failed" | "stuck" | "cancelled"; + /** + * @description Sandbox reporting for one agent, as three separate facts. + * + * `mode` is what the operator asked for and `containment_active` is what the + * host is actually doing; they are not the same question, and reporting only + * the first is how an instance ends up running unconfined while its config + * says `mode = "enabled"`. `backend` names the mechanism so the answer is + * checkable rather than trusted. + */ + SandboxContainmentStatus: { + agent_id: string; + /** @description Backend enforcing containment, or null when none was detected. */ + backend?: string | null; + /** @description Whether OS-level containment is in force right now. */ + containment_active: boolean; + /** @description Configured `sandbox.mode`: "enabled" or "disabled". */ + mode: string; /** - * @description Blanket model override — applies to all processes unless a per-process - * override is set in `model_overrides`. + * @description Mode is enabled but no backend exists — the config claims containment + * this host is not providing. Reported explicitly rather than left to be + * derived, because deriving it wrong is the failure being fixed. */ - model?: string | null; - /** @description Per-process model overrides. Takes priority over `model`. */ - model_overrides?: components["schemas"]["ModelOverrides"]; - /** @description How the channel handles incoming messages. */ - response_mode?: components["schemas"]["ResponseMode"]; - /** @description Whether file attachments are saved to workspace. */ - save_attachments?: boolean | null; - /** @description What context workers spawned from this conversation receive. */ - worker_context?: components["schemas"]["WorkerContextMode"]; + requested_but_inert: boolean; + /** @description Whether `sandbox.require_containment` is set for this agent. */ + require_containment: boolean; }; - CortexChatDeleteThreadRequest: { - agent_id: string; - thread_id: string; + SandboxSection: { + mode: string; + passthrough_env: string[]; + writable_paths: string[]; }; - /** @description A persisted cortex chat message. */ - CortexChatMessage: { - channel_context?: string | null; - content: string; - created_at: string; - id: string; - role: string; - thread_id: string; - /** @description Serialized JSON array of tool calls (for assistant messages). */ - tool_calls?: components["schemas"]["CortexChatToolCall"][] | null; + SandboxUpdate: { + mode?: string | null; + passthrough_env?: string[] | null; + writable_paths?: string[] | null; }; - CortexChatMessagesResponse: { - messages: components["schemas"]["CortexChatMessage"][]; - thread_id: string; + SaveBindingRequest: { + /** @description Required when `source` is `literal`. */ + literal_value?: unknown; + /** @description `step` | `literal` | `run_input` | `fan_in` | `previous_iteration` */ + source: string; + /** @description RFC 6901 JSON Pointer. Empty selects the whole document. */ + source_pointer?: string | null; + /** @description Required when `source` is `step`. */ + source_step_key?: string | null; }; - CortexChatSendRequest: { + SaveScheduleRequest: { + /** @description The agent that owns and, absent a step assignment, executes the run. */ agent_id: string; - channel_id?: string | null; - message: string; - thread_id: string; + /** @description 5-field cron expression, read in UTC. Omit to use `interval_secs`. */ + cron_expr?: string | null; + enabled?: boolean; + /** @description Omit to create. Supplying an existing id replaces that schedule. */ + id?: string | null; + /** @description The launch payload. A literal, because a schedule cannot prompt. */ + inputs?: unknown; + /** Format: int64 */ + interval_secs?: number; + name: string; }; - /** @description Summary of a cortex chat thread (returned by list_threads). */ - CortexChatThread: { - first_message_at: string; - last_message_at: string; + /** @description A condition on a step: the predicate, and what a false answer means. */ + SaveStepGateRequest: { + /** + * @description The predicate — an RFC 6901 `pointer` plus `equals` or `any_of`, in the + * same shape a task gate takes. For `task_output`, `task_number` is filled + * in by the launch and must not be set here. + */ + config: unknown; + /** + * @description `wait` | `route`, or omit to derive it. + * + * `wait` holds the step until the condition becomes true — a gate in the + * original sense. `route` says a false answer means the step does not + * apply, and settles it as skipped. + * + * Omitting it is right nearly always: a `task_output` condition whose + * source has settled routes, and everything else waits. That is a fact + * about whether the answer can still change, not a guess. Set it for what + * the derivation cannot see — an http endpoint whose answer really is + * final, or a condition that should hold the pipeline rather than skip + * past it. + */ + disposition?: string | null; + /** @description `http` | `task_output` */ + kind: string; + /** @description What the board should call this. "needs legal review" beats a pointer. */ + label?: string | null; /** Format: int64 */ - message_count: number; - preview: string; - thread_id: string; + poll_interval_secs?: number | null; + /** + * @description Required when `kind` is `task_output`: whose output to read, by name. + * Becomes a task number at launch. + */ + source_step_key?: string | null; }; - CortexChatThreadsResponse: { - threads: components["schemas"]["CortexChatThread"][]; + SaveStepRequest: { + /** @description Omit to run the step as whoever launched the run. */ + assigned_agent_id?: string | null; + /** + * @description The command line for a command step. Refused on an agent step, where + * nothing would run it. + */ + command?: string | null; + /** + * Format: int64 + * @description Hard timeout for a command step, in seconds. Required on one. + */ + command_timeout_secs?: number | null; + /** + * @description `each_pass` (default) or `once`, for a decision inside a loop body. + * + * `each_pass` re-asks on every pass, because pass 2 exists precisely + * because the artefact changed and reusing pass 1's answer would credit a + * person with approving work they never saw. `once` carries the first + * answer forward, recorded as `carried` with the original answerer and + * timestamp, for gates that are a property of the run rather than the pass. + */ + decision_ask?: string | null; + /** + * @description Who may answer. Omit for anyone. + * + * **Advisory in v1.** It is recorded on the task and shown alongside the + * answerer, so an audit can compare them — but it is not enforced, because + * this layer has no authenticated caller identity to enforce it against and + * checking a self-declared name would be enforcement in name only. + */ + decision_asked_of?: string[] | null; + /** + * @description The answer that applies on a `default` timeout. Validated against this + * step's own `output_schema` at launch. + */ + decision_default_answer?: unknown; + /** + * @description The question a decision step asks, as the person answering reads it. + * Required on a decision step; refused on every other kind. + */ + decision_question?: string | null; + /** + * @description `wait` (default), `default`, or `fail`. + * + * `wait` parks until answered — the run is legitimately blocked and is not + * reported as stuck. `default` applies `decision_default_answer` after + * `decision_timeout_secs`, recorded *as* a default. `fail` fails the step + * and lets the failure path route it. + */ + decision_timeout_action?: string | null; + /** + * Format: int64 + * @description How long to wait, in seconds, from the moment the decision is asked — + * not from launch. Required by `default` and `fail`, refused by `wait`. + */ + decision_timeout_secs?: number | null; + description?: string | null; + /** + * Format: int64 + * @description The exit code that means success, for steps where non-zero really is a + * failure. Omit — the usual case — to treat the exit code as data: a + * command that ran and reported a problem is a step that succeeded. + */ + expect_exit_code?: number | null; + /** @description Pointer *within each item* naming its branch. Omit to key by index. */ + for_each_key?: string | null; + /** @description RFC 6901 pointer into that step's outputs. Must select an array. */ + for_each_pointer?: string | null; + /** + * @description Set to make this a fan-out: one task per item that step produced, + * instead of one task. + * + * Each branch receives its own item as the input key **`item`**. That is + * the name to declare in this step's `input_schema` and to bind against — + * there is no way to rename it, and a step that iterates without knowing + * the key would declare a contract it never receives. + */ + for_each_step_key?: string | null; + input_schema?: unknown; + /** + * @description `agent` (default) or `command`. + * + * A command step runs a process instead of a model. Its outputs are + * `{"exit_code", "stdout", "stderr", "duration_ms"}`, which bindings, + * gates, `loop_until` and conditions read with the pointers they already + * use. + */ + kind?: string | null; + /** + * @description Set to put this step in a loop body. Every step sharing the name is one + * body, and the whole body runs again until it converges or runs out. + */ + loop_group?: string | null; + /** + * Format: int64 + * @description How many passes the body may run. Omit for 3. + * + * Only read on the body's **exit step** — the one step with nothing after + * it inside the body. Set anywhere else, launch refuses rather than + * leaving a number that does nothing. + */ + loop_max_iterations?: number | null; + /** + * @description The exit predicate, in the same shape a `task_output` gate takes: + * `{"pointer": "/tests/passed", "equals": true}`. Required on the exit + * step of a loop body. + */ + loop_until?: unknown; + output_schema?: unknown; + /** + * Format: int64 + * @description Display order only — execution order comes from the edges. + */ + position?: number | null; + priority?: string | null; + repo_id?: string | null; + /** + * @description Say what the step needs instead of who should do it. + * + * Set, and the emitted task is unassigned: any agent declaring all of + * these claims it. Mutually exclusive with `assigned_agent_id`, and a + * requirement no agent in the fleet can satisfy is refused at launch. + */ + required_capabilities?: string[] | null; + /** @description Extra instructions appended to the worker prompt when this step runs. */ + system_prompt?: string | null; + title: string; + /** + * @description What a provisioned worktree forks from — a branch, tag or sha. Omit for + * the repo's current HEAD. + */ + worktree_base_ref?: string | null; + /** + * @description `inherit` (default), `per_run`, or `per_branch`. + * + * `per_branch` requires a fan-out and is refused at launch otherwise. + */ + worktree_mode?: string | null; }; - /** @description A tool call + result pair persisted alongside assistant messages. */ - CortexChatToolCall: { - args: string; - id: string; - result?: string | null; - status: string; - tool: string; + SaveWebhookRequest: { + /** @description The agent that owns and executes the run. */ + agent_id: string; + /** + * @description Off by default. An inbound trigger that turns itself on when configured + * would make "I set this up to test it" and "I want strangers able to run + * this pipeline" the same action. + */ + enabled?: boolean; + /** @description `{ "": "" }`. */ + input_pointers?: { + [key: string]: unknown; + }; + /** + * @description The shared secret, in plaintext, once. It is hashed before storage and + * there is no endpoint that reads it back. + */ + secret: string; }; - /** @description A persisted cortex action record. */ - CortexEvent: { - created_at: string; - details?: unknown; - event_type: string; - id: string; - summary: string; + SaveWorkflowRequest: { + description?: string | null; + /** @description JSON Schema for the input a whole run is launched with. */ + input_schema?: unknown; + name: string; }; - CortexEventsResponse: { - events: components["schemas"]["CortexEvent"][]; + /** @description Metadata for a saved attachment, returned after persisting to disk and DB. */ + SavedAttachmentMeta: { + filename: string; + id: string; + mime_type: string; + saved_filename: string; /** Format: int64 */ - total: number; + size_bytes: number; }; - CortexSection: { - /** Format: int64 */ - branch_timeout_secs: number; - /** Format: int64 */ - bulletin_interval_secs: number; - bulletin_max_turns: number; - bulletin_max_words: number; - /** Format: int32 */ - circuit_breaker_threshold: number; - /** Format: int32 */ - detached_worker_timeout_retry_limit: number; - /** Format: float */ - maintenance_decay_rate: number; - /** Format: int64 */ - maintenance_interval_secs: number; - /** Format: float */ - maintenance_merge_similarity_threshold: number; - /** Format: int64 */ - maintenance_min_age_days: number; - /** Format: float */ - maintenance_prune_threshold: number; - supervisor_kill_budget_per_tick: number; - /** Format: int64 */ - tick_interval_secs: number; - /** Format: int64 */ - worker_timeout_secs: number; + ScheduleListResponse: { + schedules: components["schemas"]["WorkflowSchedule"][]; }; - CortexUpdate: { - /** Format: int64 */ - branch_timeout_secs?: number | null; - /** Format: int64 */ - bulletin_interval_secs?: number | null; - bulletin_max_turns?: number | null; - bulletin_max_words?: number | null; - /** Format: int32 */ - circuit_breaker_threshold?: number | null; - /** Format: int32 */ - detached_worker_timeout_retry_limit?: number | null; - /** Format: float */ - maintenance_decay_rate?: number | null; - /** Format: int64 */ - maintenance_interval_secs?: number | null; - /** Format: float */ - maintenance_merge_similarity_threshold?: number | null; - /** Format: int64 */ - maintenance_min_age_days?: number | null; - /** Format: float */ - maintenance_prune_threshold?: number | null; - supervisor_kill_budget_per_tick?: number | null; - /** Format: int64 */ - tick_interval_secs?: number | null; - /** Format: int64 */ - worker_timeout_secs?: number | null; + /** + * @description What came of one schedule fire. + * + * Three values because there are three recoveries. See the module docs. + * @enum {string} + */ + ScheduleOutcome: "launched" | "refused" | "errored"; + ScheduleResponse: { + schedule: components["schemas"]["WorkflowSchedule"]; }; - CreateAgentRequest: { - agent_id: string; - display_name?: string | null; - role?: string | null; + /** + * @description Secret category determines subprocess exposure. + * + * All secrets are readable by Rust code via `SecretsStore::get()` regardless + * of category. The category answers one question: should this value be injected + * as an env var into worker subprocesses? + * @enum {string} + */ + SecretCategory: "system" | "tool"; + SecretInfoResponse: { + category: components["schemas"]["SecretCategory"]; + /** Format: date-time */ + created_at: string; + name: string; + /** Format: date-time */ + updated_at: string; + }; + SecretListItem: { + category: components["schemas"]["SecretCategory"]; + /** Format: date-time */ + created_at: string; + name: string; + /** + * @description Visibility scope. `InstanceShared` is the default for system / + * admin-managed secrets; `Agent` rows are per-agent tool credentials. + */ + scope: components["schemas"]["SecretScope"]; + /** Format: date-time */ + updated_at: string; + }; + SecretListResponse: { + secrets: components["schemas"]["SecretListItem"][]; }; - CreateBindingRequest: { - adapter?: string | null; + /** + * @description Secret scope determines visibility across agents on a shared instance. + * + * Orthogonal to `SecretCategory` — `System` secrets are always + * `InstanceShared` (singleton consumers like `LlmManager` / + * `MessagingManager`); `Tool` secrets default to `Agent(...)` for + * agentic-backend deployments where each tenant's worker subprocess must + * not see another tenant's credentials, but can also be `InstanceShared` + * when a single-tenant deployment legitimately wants every agent to share + * the same `Tool` secret (e.g. one repo-wide `GH_TOKEN`). + */ + SecretScope: { + /** @enum {string} */ + kind: "instance_shared"; + } | { agent_id: string; - channel: string; - channel_ids?: string[]; - chat_id?: string | null; - dm_allowed_users?: string[]; - guild_id?: string | null; - platform_credentials?: null | components["schemas"]["PlatformCredentials"]; - require_mention?: boolean; - team_id?: string | null; - workspace_id?: string | null; + /** @enum {string} */ + kind: "agent"; }; - CreateBindingResponse: { - message: string; - /** @description True if platform credentials were added/changed (adapter needs restart). */ - restart_required: boolean; - success: boolean; + SetBindingRequest: { + /** @description Literal JSON value, used when no source task is given. */ + literal_value?: unknown; + /** @description RFC 6901 JSON Pointer into that task's outputs. */ + source_pointer?: string | null; + /** + * Format: int64 + * @description Upstream task to read from. Omit for a literal. + */ + source_task_number?: number | null; }; - CreateCronRequest: { - /** Format: int32 */ - active_end_hour?: number | null; - /** Format: int32 */ - active_start_hour?: number | null; + SetChannelArchiveRequest: { agent_id: string; - cron_expr?: string | null; - delivery_target: string; - enabled?: boolean; - id: string; - /** Format: int64 */ - interval_secs?: number; - prompt: string; - run_once?: boolean; - /** Format: int64 */ - timeout_secs?: number | null; - }; - CreateGroupRequest: { - agent_ids?: string[]; - color?: string | null; - name: string; + archived: boolean; + channel_id: string; }; - CreateHumanRequest: { - bio?: string | null; - description?: string | null; - discord_id?: string | null; - display_name?: string | null; - email?: string | null; - id: string; - role?: string | null; - slack_id?: string | null; - telegram_id?: string | null; + SetContractRequest: { + input_schema?: unknown; + output_schema?: unknown; }; - CreateLinkRequest: { - direction?: string; - from: string; - kind?: string; - to: string; + SkillContentResponse: { + base_dir: string; + content: string; + description: string; + file_path: string; + name: string; + source: string; + source_repo?: string | null; }; - CreateMcpServerRequest: { - args?: string[]; - command?: string | null; - enabled?: boolean; - env?: { - [key: string]: string; - }; - headers?: { - [key: string]: string; - }; + SkillInfo: { + base_dir: string; + description: string; + file_path: string; name: string; - transport: string; - url?: string | null; + source: string; + source_repo?: string | null; }; - CreateMessagingInstanceRequest: { - credentials?: components["schemas"]["InstanceCredentials"]; - enabled?: boolean | null; - name?: string | null; - platform: string; + SkillsListResponse: { + skills: components["schemas"]["SkillInfo"][]; }; - CreatePageRequest: { - /** @description Who is creating this page: agent_id or user identifier. */ - author_id?: string; - author_type?: string; - content?: string; - edit_summary?: string | null; - page_type: string; - related?: string[]; - title: string; + SshStatusResponse: { + enabled: boolean; + has_authorized_key: boolean; + /** Format: int32 */ + port: number; }; - CreatePortalConversationRequest: { - agent_id: string; - settings?: null | components["schemas"]["ConversationSettings"]; - title?: string | null; + StatusResponse: { + /** Format: int32 */ + pid: number; + /** @description Per-agent process containment. One entry per agent with a live sandbox. */ + sandbox: components["schemas"]["SandboxContainmentStatus"][]; + status: string; + /** Format: int64 */ + uptime_seconds: number; + version: string; }; - CreateProjectRequest: { - /** @description When true, scan root_path for git repos and register them automatically. */ - auto_discover?: boolean; - description?: string | null; - icon?: string | null; - name: string; - root_path: string; - settings?: unknown; - tags?: string[]; + StepBinding: { + input_key: string; + literal_value?: unknown; + source: components["schemas"]["BindingSource"]; + /** @description RFC 6901 JSON Pointer. Empty selects the whole document. */ + source_pointer?: string | null; + source_step_key?: string | null; + step_key: string; + workflow_id: string; + }; + StepEdgeRequest: { + child_step_key: string; + /** + * @description `normal` (default) or `on_exhausted`. + * + * An `on_exhausted` edge is followed only when the loop ending at the + * parent runs out of attempts. Converging and giving up are opposite + * results, so they get different edges rather than one edge meaning both. + */ + kind?: string | null; + parent_step_key: string; }; - CreateRepoRequest: { - default_branch?: string | null; - description?: string | null; - name: string; - path: string; - remote_url?: string | null; + /** + * @description A gate declared by a *template*, addressed by step key. + * + * The template-level mirror of `task_gates`, exactly as [`StepBinding`] is the + * template-level mirror of `task_input_bindings`, and for the same reason: + * `task_gates` is keyed by task number and a template has only step keys. The + * translation at launch is the same one bindings already do. + * + * This is where a *condition* on a step lives. Whether the condition holds the + * step or settles it is [`StepGate::disposition`], and that one field is the + * whole of branching. + */ + StepGate: { + /** + * @description The predicate, in the shape `task_gates.config` takes: an RFC 6901 + * pointer plus `equals` / `any_of`. No second predicate language. + */ + config: unknown; + disposition?: null | components["schemas"]["GateDisposition"]; + /** + * @description Author-named, so saving the same gate twice is an edit rather than a + * second gate holding the step behind a duplicate of one condition. + */ + gate_key: string; + kind: components["schemas"]["GateKind"]; + /** @description What the board should call this. "needs legal review" beats a pointer. */ + label?: string | null; + /** Format: int64 */ + poll_interval_secs: number; + /** + * @description For `task_output`: whose output to read, by name. Becomes + * `config.task_number` at launch — the entire translation. + */ + source_step_key?: string | null; + /** @description The step this gate holds back. */ + step_key: string; + workflow_id: string; }; - CreateTaskRequest: { - /** @description Agent assigned to execute. Defaults to `owner_agent_id`. */ - assigned_agent_id?: string | null; - created_by?: string | null; + /** + * @description What a step *is*: something a model does, or something a process does. + * + * Named rather than inferred from "does `command` have a value". A NULL command + * on a step somebody meant to be a command step is a template bug, and + * inferring the kind would silently turn it into an agent step running a model + * against an empty instruction — expensive, slow, and wrong in a way nothing + * reports. With an explicit kind, launch refuses and names the missing field. + * @enum {string} + */ + StepKind: "agent" | "command" | "decision"; + StorageStatus: { + /** Format: int64 */ + available_bytes: number; + /** Format: int64 */ + total_bytes: number; + /** Format: int64 */ + used_bytes: number; + }; + Task: { + approved_at?: string | null; + approved_by?: string | null; + assigned_agent_id: string; + awaiting_loop_arm?: null | components["schemas"]["LoopArm"]; + /** + * @description This task is downstream of the named loop and waits on its verdict. + * + * The ready sweep skips it while this is set. That is what stops both arms + * of a loop's exit from running: the body finishes whether the loop + * converged or gave up, so completion alone cannot tell them apart. + */ + awaiting_loop_group?: string | null; + block_kind?: null | components["schemas"]["BlockKind"]; + /** @description Human-readable explanation shown on the card. */ + block_reason?: string | null; + /** + * Format: int64 + * @description Consecutive blocks for the same reason. See [`BLOCK_RECURRENCE_LIMIT`]. + */ + block_recurrences: number; + /** @description The command line, frozen from the step at launch. See [`TaskKind`]. */ + command?: string | null; + /** + * Format: int64 + * @description Hard wall-clock ceiling for that command, in seconds. + */ + command_timeout_secs?: number | null; + completed_at?: string | null; + /** + * Format: int64 + * @description Failures since the last success. Reset to 0 on completion and on an + * operator-initiated retry. + */ + consecutive_failures: number; + created_at: string; + created_by: string; + /** + * @description When it was settled. For a carried answer this is the *original* + * timestamp, not the moment it was reused. + */ + decision_answered_at?: string | null; + /** + * @description Who answered. `None` for a defaulted or timed-out decision, where the + * honest answer is nobody. + */ + decision_answered_by?: string | null; + /** @description What this decision does on a loop's second pass. */ + decision_ask: components["schemas"]["DecisionAsk"]; + /** + * @description When this decision became answerable. `None` means it has not been asked + * yet, and answering it is refused until it has been. + */ + decision_asked_at?: string | null; + /** + * @description Who may answer. Empty/absent means anyone. **Advisory in v1**: recorded + * alongside `decision_answered_by` so an audit can compare them, but not + * enforced, because this layer has no authenticated caller identity to + * enforce it against. + */ + decision_asked_of?: string[] | null; + /** @description The answer that applies on a `default` timeout. */ + decision_default_answer?: unknown; + decision_outcome?: null | components["schemas"]["DecisionOutcome"]; + /** + * @description The question a decision task asks, frozen from the step at launch. + * + * Frozen rather than read back from the template so that the question a + * person answered is the question on the record afterwards — a live read + * could be edited between the ask and the answer. + */ + decision_question?: string | null; + /** @description What happens if nobody answers. */ + decision_timeout_action: components["schemas"]["DecisionTimeoutAction"]; + /** + * Format: int64 + * @description How long, in seconds, from `decision_asked_at`. + */ + decision_timeout_secs?: number | null; description?: string | null; - metadata?: unknown; - /** @description Agent that owns (created) this task. */ + /** + * Format: int64 + * @description The exit code that means success. `None` means the code is *data*: a + * command that ran and reported a problem is a task that succeeded. + */ + expect_exit_code?: number | null; + /** + * @description Which branch of a fan-out this task is, once the fan-out has expanded. + * + * `None` on every ordinary task, and on the placeholder that holds the + * shape before expansion. This is the key a fan-in binding collects by. + */ + fan_out_branch_key?: string | null; + /** + * @description Whether this task is a fan-out placeholder rather than work. + * + * A placeholder carries exactly the edges its branches will inherit, so + * the steps downstream have something to wait on between launch and + * expansion. It is never promoted and never claimed — expansion replaces + * it with one task per item. + */ + fan_out_placeholder: boolean; + id: string; + /** @description JSON Schema this task's resolved inputs must satisfy before it runs. */ + input_schema?: unknown; + /** + * @description Inputs as resolved at claim time. Persisted so the value the worker + * actually saw survives a crash and stays readable after upstream changes. + */ + inputs?: unknown; + /** + * @description Whether this task is executed by a worker or by a process. + * + * `agent` on everything that predates command steps, which is why the + * column defaults to it: an unreadable or missing value must never be + * guessed as `command`, because that would execute a stored shell line on + * the strength of a corrupt row. + */ + kind: components["schemas"]["TaskKind"]; + last_block_kind?: null | components["schemas"]["BlockKind"]; + /** + * @description Text of the most recent failure, kept on the task so the board can + * show why it is parked without joining `task_runs`. + */ + last_error?: string | null; + /** @description Which loop body this task belongs to. `None` on every ordinary task. */ + loop_group?: string | null; + /** + * Format: int64 + * @description Which pass of that body this task is, 1-based. + * + * The pass, not the attempt: a task retried under the failure budget keeps + * its iteration, because retrying is not looping. + */ + loop_iteration?: number | null; + loop_resolution?: null | components["schemas"]["LoopResolution"]; + /** + * @description Whether this task is the body's exit point — the one whose outputs + * `loop_until` reads, and the one the iteration boundary is decided on. + */ + loop_terminal: boolean; + /** + * Format: int64 + * @description Per-task override of [`DEFAULT_FAILURE_LIMIT`]. + * + * Despite the name (inherited from the column), this is a *failure* limit, + * not a retry count: the task is parked once `consecutive_failures` + * reaches it, so `max_retries = 1` allows one attempt and zero retries. + */ + max_retries?: number | null; + metadata: unknown; + /** @description JSON Schema this task's outputs must satisfy to complete. */ + output_schema?: unknown; + /** @description Validated outputs. What downstream tasks read from. */ + outputs?: unknown; owner_agent_id: string; - priority?: string | null; + priority: components["schemas"]["TaskPriority"]; + /** @description Project this task acts on, if any. */ + project_id?: string | null; + /** + * @description Specific repo within the project. A project holds many repos, so this is + * what makes a task about `api-gateway` distinguishable from one about + * `web` in the same project. + */ + repo_id?: string | null; + /** + * @description What this task needs, instead of who should do it. + * + * `None` on a pushed task — the common case, and the one a fleet of one + * never leaves. `Some` makes the task *pooled*: any agent declaring all of + * these labels may claim it, and claiming stamps `assigned_agent_id` so + * that from that moment it is indistinguishable from a pushed task. + * + * This stays set for the life of the task, including while it is claimed. + * It answers "where did this come from", which is a different question + * from "who has it now" — and the reaper needs both to put a crashed + * pooled task back in the pool rather than back on the agent that died. + */ + required_capabilities?: string[] | null; + /** + * @description Why this task will never run, when its status is `skipped`. + * + * Its own field rather than a second meaning for `block_reason`: a block + * is a stop that recovers, and it drags in `block_kind`, the sticky kinds, + * the recurrence limiter, and the unblock path — none of which applies to + * a branch that was simply not taken. + */ + skip_reason?: string | null; source_memory_id?: string | null; - subtasks?: components["schemas"]["TaskSubtask"][]; + status: components["schemas"]["TaskStatus"]; + subtasks: components["schemas"]["TaskSubtask"][]; + /** + * @description Extra instructions appended to the worker prompt at pickup. Appended, + * never substituted — this is task guidance, not an identity override. + */ + system_prompt?: string | null; + /** Format: int64 */ + task_number: number; title: string; + updated_at: string; + worker_id?: string | null; + /** + * @description The workflow launch this task was compiled from, if any. + * + * Plain text rather than a foreign key: a task outlives its template, and + * deleting a workflow must not take the record of work that actually + * happened with it. + */ + workflow_run_id?: string | null; + /** @description Which step of that workflow produced this task. */ + workflow_step_key?: string | null; + /** @description What a provisioned worktree forks from. `None` means the repo's HEAD. */ + worktree_base_ref?: string | null; + /** + * @description Worktree to execute in. When set, the worker's working directory is + * resolved from it rather than from the repo or project root. + */ + worktree_id?: string | null; + /** + * @description What checkout this task runs in, frozen from the step at launch. + * + * A fan-out placeholder carries it to its branches, which is how expansion + * knows to provision one checkout per branch — inside the same transaction + * that emits them. + */ + worktree_mode: components["schemas"]["WorktreeMode"]; }; - CreateWorktreeRequest: { - branch: string; - repo_id: string; - start_point?: string | null; - worktree_name?: string | null; - }; - CronActionResponse: { - message: string; - success: boolean; - }; - /** @description Entry in the cron execution log. */ - CronExecutionEntry: { - cron_id?: string | null; - delivery_attempted: boolean; - delivery_error?: string | null; - delivery_succeeded?: boolean | null; - executed_at: string; - execution_error?: string | null; - execution_succeeded: boolean; - id: string; - result_summary?: string | null; + TaskActionResponse: { + message: string; success: boolean; }; - CronExecutionsResponse: { - executions: components["schemas"]["CronExecutionEntry"][]; + TaskContractResponse: { + bindings: components["schemas"]["TaskInputBinding"][]; + input_schema?: unknown; + /** @description Inputs as they were resolved at the last claim. */ + inputs?: unknown; + output_schema?: unknown; + outputs?: unknown; + /** @description Why resolution fails, if it does. Empty when the contract is satisfied. */ + problems: components["schemas"]["ContractProblem"][]; + /** + * @description What the bindings resolve to right now, which may differ from `inputs` + * if the graph changed since the last attempt. + */ + resolved_inputs?: unknown; }; - CronJobInfo: { - active_hours?: [ - number, - number - ] | null; - cron_expr?: string | null; - delivery_target: string; - enabled: boolean; - id: string; - /** Format: int64 */ - interval_secs: number; - prompt: string; - run_once: boolean; + TaskDependenciesResponse: { + /** + * @description The subset of `parents` that has not finished yet — what the board + * should name when explaining why a task is not moving. + */ + blocked_by: number[]; + /** @description Tasks waiting on this one. */ + children: number[]; + /** @description Tasks this one waits on. */ + parents: number[]; + }; + /** @description How many edges touch a task, and how many still gate it. */ + TaskEdgeSummary: { + /** + * Format: int64 + * @description The subset of `parents` that has not finished — why the task is waiting. + */ + blocked_by: number; + /** + * Format: int64 + * @description Tasks waiting on this one. + */ + children: number; + /** + * Format: int64 + * @description Tasks this one waits on. + */ + parents: number; /** Format: int64 */ - timeout_secs?: number | null; + task_number: number; }; - CronJobWithStats: { - active_hours?: [ - number, - number - ] | null; - cron_expr?: string | null; - /** Format: int64 */ - delivery_failure_count: number; - /** Format: int64 */ - delivery_skipped_count: number; - /** Format: int64 */ - delivery_success_count: number; - delivery_target: string; - enabled: boolean; - /** Format: int64 */ - execution_failure_count: number; + TaskGate: { + config: unknown; /** Format: int64 */ - execution_success_count: number; + consecutive_errors: number; + created_at: string; + disposition?: null | components["schemas"]["GateDisposition"]; id: string; + kind: components["schemas"]["GateKind"]; + /** + * @description What a person should read on the board. "waiting for CI on main" beats + * a URL. + */ + label?: string | null; + last_checked_at?: string | null; + last_detail?: string | null; + last_result: components["schemas"]["GateResult"]; /** Format: int64 */ - interval_secs: number; - last_executed_at?: string | null; - prompt: string; - run_once: boolean; + poll_interval_secs: number; /** Format: int64 */ - timeout_secs?: number | null; - }; - CronListResponse: { - jobs: components["schemas"]["CronJobWithStats"][]; - timezone: string; + task_number: number; }; - DayCount: { - /** Format: int64 */ - count: number; - date: string; + TaskGatesResponse: { + gates: components["schemas"]["TaskGate"][]; }; /** - * @description Delegation mode controls how the conversation handles tools. - * @enum {string} + * @description The connected graph a task belongs to. + * + * The unit a person actually asks about. "Show me this task" is nearly always + * "show me what this task is part of" — what it waits for, what waits on it, + * and what runs beside it. */ - DelegationMode: "standard" | "direct"; - DeleteBindingRequest: { - adapter?: string | null; - agent_id: string; - channel: string; - chat_id?: string | null; - guild_id?: string | null; - team_id?: string | null; - workspace_id?: string | null; - }; - DeleteBindingResponse: { - message: string; - success: boolean; + TaskGraph: { + edges: components["schemas"]["TaskGraphEdge"][]; + /** + * Format: int64 + * @description The task that was asked about, so a renderer can mark it. + */ + seed: number; + tasks: components["schemas"]["Task"][]; + /** + * @description Whether the walk hit its cap. Reported rather than swallowed: a partial + * graph presented as a whole one is worse than no graph. + */ + truncated: boolean; }; - DeleteMessagingInstanceRequest: { - name?: string | null; - platform: string; + /** @description One dependency edge, as a pair rather than a count. */ + TaskGraphEdge: { + /** Format: int64 */ + child_task_number: number; + /** Format: int64 */ + parent_task_number: number; }; - DeleteSecretResponse: { - deleted: string; - warning?: string | null; + /** + * @description Where one of a task's inputs comes from. + * + * Either a pointer into an upstream task's outputs, or a literal baked into + * the graph. `source_task_number` being `None` means literal. + */ + TaskInputBinding: { + /** Format: int64 */ + child_task_number: number; + /** + * @description Collect every branch of this workflow step into one object, keyed by + * branch key. + * + * Mutually exclusive with `source_task_number`, and it has to be: that one + * addresses a single upstream task by number, which cannot name a set that + * does not exist until the fan-out expands. + */ + fan_in_step_key?: string | null; + /** @description Key in the child's input object. */ + input_key: string; + /** @description JSON literal, used when `source_task_number` is `None`. */ + literal_value?: unknown; + /** + * @description RFC 6901 JSON Pointer into that task's outputs. Empty selects the whole + * outputs object. + */ + source_pointer?: string | null; + /** + * Format: int64 + * @description Upstream task to read from. `None` for a literal. + */ + source_task_number?: number | null; }; /** - * @description Deployment environment, detected from SPACEBOT_DEPLOYMENT env var. + * @description What executes a task. + * + * The task-level mirror of [`crate::workflows::StepKind`], and named rather + * than inferred from "does `command` have a value" for the same reason: a task + * meant to be a command and missing its command line must be *reported*, not + * quietly run as an agent task against an empty instruction. * @enum {string} */ - Deployment: "docker" | "hosted" | "native"; - DisconnectPlatformRequest: { - adapter?: string | null; - platform: string; + TaskKind: "agent" | "command" | "decision"; + TaskListResponse: { + /** + * Format: int64 + * @description How many consecutive failures a task tolerates when it sets no limit of + * its own. + * + * Published because the dashboard has to show what "default" *means* — a + * budget control that says "uses the default" without the number tells a + * reader nothing they can act on. The alternative was hard-coding it in + * TypeScript, which is the same silent-drift bug this codebase has already + * paid for four times over in dead config. + */ + default_failure_limit: number; + /** + * @description Edge counts for every task that has any. Tasks with no dependencies are + * absent rather than listed with zeroes. + */ + edges: components["schemas"]["TaskEdgeSummary"][]; + tasks: components["schemas"]["Task"][]; }; - DiscordSection: { - allow_bot_messages: boolean; - enabled: boolean; + /** @enum {string} */ + TaskPriority: "critical" | "high" | "medium" | "low"; + TaskProvenanceResponse: { + /** @description Cards this task filed. */ + filed: components["schemas"]["Task"][]; + /** + * Format: int64 + * @description The task that filed this one, when a worker did. + */ + filed_by_task_number?: number | null; + /** + * Format: int64 + * @description How many more this task may still file before hitting the cap. + */ + remaining_fan_out: number; }; - DiscordUpdate: { - allow_bot_messages?: boolean | null; + TaskResponse: { + task: components["schemas"]["Task"]; }; - DiskUsageEntry: { + /** @description A single execution attempt against a task. */ + TaskRun: { /** Format: int64 */ - bytes: number; - is_dir: boolean; - name: string; - }; - DiskUsageResponse: { - entries: components["schemas"]["DiskUsageEntry"][]; + attempt: number; + ended_at?: string | null; + error?: string | null; + id: string; + outcome?: null | components["schemas"]["TaskRunOutcome"]; + started_at: string; + summary?: string | null; /** Format: int64 */ - total_bytes: number; - }; - EditPageRequest: { - author_id?: string; - author_type?: string; - edit_summary?: string | null; - new_string: string; - old_string: string; - replace_all?: boolean; + task_number: number; + worker_id?: string | null; }; - EncryptResponse: { - master_key: string; - message: string; + /** + * @description Outcome of a single task execution attempt. + * @enum {string} + */ + TaskRunOutcome: "completed" | "failed" | "timeout" | "cancelled" | "blocked" | "rate_limited" | "abandoned"; + TaskRunsResponse: { + runs: components["schemas"]["TaskRun"][]; }; - /** @description Portable backup format for all secrets in a store. */ - ExportData: { - /** - * @description Whether the source store had encryption enabled (informational only — - * values in this struct are always plaintext). - */ - encrypted: boolean; - /** @description All secrets with their metadata. */ - entries: components["schemas"]["ExportEntry"][]; - /** - * Format: int32 - * @description Format version (currently 1). - */ - version: number; + /** @enum {string} */ + TaskStatus: "pending_approval" | "backlog" | "ready" | "in_progress" | "blocked" | "done" | "skipped"; + TaskSubtask: { + completed: boolean; + title: string; }; - /** @description A single secret in the export format. */ - ExportEntry: { - category: components["schemas"]["SecretCategory"]; - /** Format: date-time */ - created_at: string; - name: string; - /** Format: date-time */ - updated_at: string; - value: string; + TaskTransition: { + from: components["schemas"]["TaskStatus"]; + to: components["schemas"]["TaskStatus"]; }; - GlobalSettingsResponse: { - api_bind: string; - api_enabled: boolean; - /** Format: int32 */ - api_port: number; - brave_search_key?: string | null; - company_name: string; - opencode: components["schemas"]["OpenCodeSettingsResponse"]; - ssh_enabled: boolean; - worker_log_mode: string; + TaskTransitionsResponse: { + transitions: components["schemas"]["TaskTransition"][]; }; - GlobalSettingsUpdate: { - api_bind?: string | null; - api_enabled?: boolean | null; - /** Format: int32 */ - api_port?: number | null; - brave_search_key?: string | null; - company_name?: string | null; - opencode?: null | components["schemas"]["OpenCodeSettingsUpdate"]; - ssh_enabled?: boolean | null; - worker_log_mode?: string | null; + /** @description A unified timeline item combining messages, branch runs, and worker runs. */ + TimelineItem: { + attachments?: components["schemas"]["SavedAttachmentMeta"][]; + content: string; + created_at: string; + id: string; + role: string; + sender_id?: string | null; + sender_name?: string | null; + /** @enum {string} */ + type: "message"; + } | { + completed_at?: string | null; + conclusion?: string | null; + description: string; + id: string; + started_at: string; + /** @enum {string} */ + type: "branch_run"; + } | { + completed_at?: string | null; + id: string; + result?: string | null; + started_at: string; + status: string; + task: string; + /** @enum {string} */ + type: "worker_run"; + } | { + args: string; + completed_at?: string | null; + id: string; + result?: string | null; + started_at: string; + status: string; + tool_name: string; + /** @enum {string} */ + type: "tool_call_run"; }; - GlobalSettingsUpdateResponse: { - message: string; - requires_restart: boolean; - success: boolean; + ToggleCronRequest: { + agent_id: string; + cron_id: string; + enabled: boolean; }; - HealthResponse: { - status: string; + TogglePlatformRequest: { + adapter?: string | null; + enabled: boolean; + platform: string; }; - HeatmapCell: { + TokenSummary: { + by_process: { + [key: string]: components["schemas"]["ProcessTokens"]; + }; /** Format: int64 */ - count: number; + cache_read: number; + /** Format: double */ + cost_usd: number; /** Format: int64 */ - day: number; + input: number; /** Format: int64 */ - hour: number; + output: number; + /** Format: int64 */ + reasoning: number; }; - IdentityResponse: { - identity?: string | null; - role?: string | null; - soul?: string | null; + /** @enum {string} */ + ToolResultStatus: "pending" | "final" | "waiting_for_input"; + ToolsResponse: { + binaries: components["schemas"]["BinaryEntry"][]; + tools_bin: string; }; - IdentityUpdateRequest: { - agent_id: string; - identity?: string | null; + TopologyAgent: { + display_name?: string | null; + id: string; + name: string; role?: string | null; - soul?: string | null; }; - IdleResponse: { - active_branches: number; - active_workers: number; - idle: boolean; - }; - ImportBody: components["schemas"]["ExportData"] & { - /** @description Whether to overwrite existing secrets with the same name. */ - overwrite?: boolean; + TopologyGroup: { + agent_ids: string[]; + color?: string | null; + name: string; }; - IngestDeleteResponse: { - success: boolean; + TopologyHuman: { + bio?: string | null; + description?: string | null; + discord_id?: string | null; + display_name?: string | null; + email?: string | null; + id: string; + role?: string | null; + slack_id?: string | null; + telegram_id?: string | null; }; - IngestFileInfo: { - /** Format: int64 */ - chunks_completed: number; - completed_at?: string | null; - content_hash: string; - /** Format: int64 */ - file_size: number; - filename: string; - started_at: string; - status: string; - /** Format: int64 */ - total_chunks: number; + TopologyLink: { + direction: string; + from: string; + kind: string; + to: string; }; - IngestFilesResponse: { - files: components["schemas"]["IngestFileInfo"][]; + /** @description Topology response for graph rendering. */ + TopologyResponse: { + agents: components["schemas"]["TopologyAgent"][]; + groups: components["schemas"]["TopologyGroup"][]; + humans: components["schemas"]["TopologyHuman"][]; + links: components["schemas"]["TopologyLink"][]; }; - IngestUploadResponse: { - uploaded: string[]; + /** @description A single step in a worker transcript. */ + TranscriptStep: { + content: components["schemas"]["ActionContent"][]; + /** @enum {string} */ + type: "action"; + } | { + text: string; + /** @enum {string} */ + type: "user_text"; + } | { + text: string; + /** @enum {string} */ + type: "system_text"; + } | { + call_id: string; + /** @description Accumulated streaming output for live display. Cleared when tool completes. */ + live_output?: string | null; + name: string; + status?: components["schemas"]["ToolResultStatus"]; + text: string; + /** @enum {string} */ + type: "tool_result"; }; - InstallSkillRequest: { + TriggerCronRequest: { agent_id: string; - instance?: boolean; - spec: string; + cron_id: string; }; - InstallSkillResponse: { - installed: string[]; + TuningSection: { + branch_max_turns: number; + context_window: number; + history_backfill_count: number; + max_concurrent_branches: number; + max_concurrent_workers: number; + max_turns: number; }; - InstanceCredentials: { - discord_token?: string | null; - email_from_address?: string | null; - email_imap_host?: string | null; - email_imap_password?: string | null; - /** Format: int32 */ - email_imap_port?: number | null; - email_imap_username?: string | null; - email_smtp_host?: string | null; - email_smtp_password?: string | null; - /** Format: int32 */ - email_smtp_port?: number | null; - email_smtp_username?: string | null; - mattermost_base_url?: string | null; - mattermost_token?: string | null; - signal_account?: string | null; - signal_dm_allowed_users?: string | null; - signal_http_url?: string | null; - slack_app_token?: string | null; - slack_bot_token?: string | null; - telegram_token?: string | null; - twitch_client_id?: string | null; - twitch_client_secret?: string | null; - twitch_oauth_token?: string | null; - twitch_refresh_token?: string | null; - twitch_username?: string | null; - webhook_auth_token?: string | null; - webhook_bind?: string | null; - /** Format: int32 */ - webhook_port?: number | null; + TuningUpdate: { + branch_max_turns?: number | null; + context_window?: number | null; + history_backfill_count?: number | null; + max_concurrent_branches?: number | null; + max_concurrent_workers?: number | null; + max_turns?: number | null; }; - InstanceOverviewResponse: { - agents: components["schemas"]["AgentSummary"][]; - /** Format: int32 */ - pid: number; + UnlockBody: { + master_key: string; + }; + UnreadCountResponse: { /** Format: int64 */ - uptime_seconds: number; - version: string; + count: number; }; - McpAgentStatus: { + UpdateAgentRequest: { agent_id: string; - servers: components["schemas"]["McpServerInfo"][]; - }; - McpConnectionState: "connecting" | "connected" | { - failed: string; - } | "disconnected"; - McpServerInfo: { - enabled: boolean; - name: string; - state: string; - transport: string; - }; - McpServerStatus: { - enabled: boolean; - name: string; - state: components["schemas"]["McpConnectionState"]; - transport: string; - }; - MemoriesListResponse: { - memories: components["schemas"]["Memory"][]; - total: number; - }; - MemoriesSearchResponse: { - results: components["schemas"]["MemorySearchResult"][]; - }; - /** @description Memory structure. */ - Memory: { - /** Format: int64 */ - access_count: number; - channel_id?: string | null; - content: string; - /** Format: date-time */ - created_at: string; /** - * @description Soft-delete flag. Forgotten memories are excluded from search and recall - * but remain in the database. + * @description Replace what this agent declares it can do. Absent leaves it alone; an + * empty list clears it, which is how an agent is taken out of every pool + * without deleting it. */ - forgotten: boolean; - id: string; - /** Format: float */ - importance: number; - /** Format: date-time */ - last_accessed_at: string; - memory_type: components["schemas"]["MemoryType"]; - source?: string | null; - /** Format: date-time */ - updated_at: string; + capabilities?: string[] | null; + display_name?: string | null; + gradient_end?: string | null; + gradient_start?: string | null; + role?: string | null; }; - MemoryGraphNeighborsResponse: { - edges: components["schemas"]["Association"][]; - nodes: components["schemas"]["Memory"][]; + UpdateBindingRequest: { + adapter?: string | null; + agent_id: string; + channel: string; + channel_ids?: string[]; + chat_id?: string | null; + dm_allowed_users?: string[]; + guild_id?: string | null; + original_adapter?: string | null; + original_agent_id: string; + original_channel: string; + original_chat_id?: string | null; + original_guild_id?: string | null; + original_team_id?: string | null; + original_workspace_id?: string | null; + require_mention?: boolean; + team_id?: string | null; + workspace_id?: string | null; }; - MemoryGraphResponse: { - edges: components["schemas"]["Association"][]; - nodes: components["schemas"]["Memory"][]; - total: number; + UpdateBindingResponse: { + message: string; + success: boolean; }; - /** - * @description Memory mode controls how memory is used in a conversation. - * @enum {string} - */ - MemoryMode: "full" | "ambient" | "off"; - MemoryPersistenceSection: { - enabled: boolean; - message_interval: number; + UpdateChannelSettingsRequest: { + agent_id: string; + settings: components["schemas"]["ConversationSettings"]; }; - MemoryPersistenceUpdate: { - enabled?: boolean | null; - message_interval?: number | null; + UpdateGroupRequest: { + agent_ids?: string[] | null; + color?: string | null; + name?: string | null; }; - /** @description Search result combining memory with relevance score. */ - MemorySearchResult: { - memory: components["schemas"]["Memory"]; - rank: number; - /** Format: float */ - score: number; + UpdateHumanRequest: { + bio?: string | null; + description?: string | null; + discord_id?: string | null; + display_name?: string | null; + email?: string | null; + role?: string | null; + slack_id?: string | null; + telegram_id?: string | null; }; - /** - * @description Memory types. - * @enum {string} - */ - MemoryType: "fact" | "preference" | "decision" | "identity" | "event" | "observation" | "goal" | "todo"; - MessagesResponse: { - has_more: boolean; - items: components["schemas"]["TimelineItem"][]; + UpdateLinkRequest: { + direction?: string | null; + kind?: string | null; }; - MessagingInstanceActionResponse: { - message: string; - success: boolean; + UpdatePortalConversationRequest: { + agent_id: string; + archived?: boolean | null; + settings?: null | components["schemas"]["ConversationSettings"]; + title?: string | null; }; - MessagingStatusResponse: { - discord: components["schemas"]["PlatformStatus"]; - email: components["schemas"]["PlatformStatus"]; - instances: components["schemas"]["AdapterInstanceStatus"][]; - mattermost: components["schemas"]["PlatformStatus"]; - signal: components["schemas"]["PlatformStatus"]; - slack: components["schemas"]["PlatformStatus"]; - telegram: components["schemas"]["PlatformStatus"]; - twitch: components["schemas"]["PlatformStatus"]; - webhook: components["schemas"]["PlatformStatus"]; + UpdateProjectRequest: { + description?: string | null; + icon?: string | null; + logo_path?: string | null; + name?: string | null; + settings?: unknown; + status?: string | null; + tags?: string[] | null; }; - MigrateResponse: { - message: string; - migrated: components["schemas"]["MigrationItem"][]; - skipped: string[]; + UpdateRepoDependencyRequest: { + kind?: string | null; + note?: string | null; }; - MigrationItem: { - category: components["schemas"]["SecretCategory"]; - config_key: string; - secret_name: string; + /** @description Result of an update check. */ + UpdateStatus: { + /** @description Whether the Docker socket is accessible (enables one-click update). */ + can_apply: boolean; + /** @description Human-readable reason when one-click apply is unavailable. */ + cannot_apply_reason?: string | null; + /** Format: date-time */ + checked_at?: string | null; + current_version: string; + deployment: components["schemas"]["Deployment"]; + /** @description Current container image reference when running in Docker. */ + docker_image?: string | null; + error?: string | null; + latest_version?: string | null; + release_notes?: string | null; + release_url?: string | null; + update_available: boolean; }; - ModelInfo: { + UpdateTaskRequest: { + approved_by?: string | null; + assigned_agent_id?: string | null; + /** @description Unbind the task from its project/repo/worktree entirely. */ + clear_binding?: boolean; + complete_subtask?: number | null; + description?: string | null; /** * Format: int64 - * @description Context window size in tokens, if known + * @description How many failures this task tolerates before it is parked. + * + * Absent leaves it alone; explicit `null` returns it to the instance + * default. Distinguishing those needs the doubly-nested option — a plain + * `Option` cannot express "clear this". */ - context_window?: number | null; - /** @description Full routing string (e.g. "openrouter/anthropic/claude-sonnet-4") */ - id: string; - /** @description Whether this model accepts audio input. */ - input_audio: boolean; - /** @description Human-readable name */ - name: string; - /** @description Provider ID for routing ("anthropic", "openrouter", "openai", etc.) */ - provider: string; - /** @description Whether this model has reasoning/thinking capability */ - reasoning: boolean; - /** @description Whether this model supports tool/function calling */ - tool_call: boolean; - }; - /** @description Model option for the defaults response. */ - ModelOption: { - /** @description Context window size. */ - context_window: number; - /** @description Model ID (e.g. "anthropic/claude-sonnet-4"). */ - id: string; - /** @description Display name (e.g. "Claude Sonnet 4"). */ - name: string; - /** @description Provider name (e.g. "anthropic"). */ - provider: string; - /** @description Whether the model supports thinking/claude-style extended thinking. */ - supports_thinking: boolean; - /** @description Whether the model supports tools. */ - supports_tools: boolean; + max_retries?: number | null; + metadata?: unknown; + priority?: string | null; + project_id?: string | null; + repo_id?: string | null; + status?: string | null; + subtasks?: components["schemas"]["TaskSubtask"][] | null; + title?: string | null; + worker_id?: string | null; + worktree_id?: string | null; }; - /** - * @description Per-process model overrides. Each field, when set, overrides the - * routing config for that specific process type within this conversation. - */ - ModelOverrides: { - branch?: string | null; - channel?: string | null; - compactor?: string | null; - worker?: string | null; + UploadSkillResponse: { + installed: string[]; }; - ModelsResponse: { - models: components["schemas"]["ModelInfo"][]; + UsageByAgent: { + agent_id: string; + /** Format: int64 */ + cache_read_tokens: number; + /** Format: int64 */ + cache_write_tokens: number; + /** Format: double */ + estimated_cost_usd?: number | null; + /** Format: int64 */ + input_tokens: number; + /** Format: int64 */ + output_tokens: number; + /** Format: int64 */ + reasoning_tokens: number; + /** Format: int64 */ + request_count: number; }; - MutationResponse: { - message: string; - success: boolean; + UsageByDay: { + /** Format: int64 */ + cache_read_tokens: number; + /** Format: int64 */ + cache_write_tokens: number; + date: string; + /** Format: double */ + estimated_cost_usd?: number | null; + /** Format: int64 */ + input_tokens: number; + /** Format: int64 */ + output_tokens: number; + /** Format: int64 */ + reasoning_tokens: number; + /** Format: int64 */ + request_count: number; }; - /** @description A persisted notification row. */ - Notification: { - action_url?: string | null; - agent_id?: string | null; - body?: string | null; - created_at: string; - dismissed_at?: string | null; - id: string; - kind: string; - metadata?: string | null; - read_at?: string | null; - related_entity_id?: string | null; - related_entity_type?: string | null; - severity: string; - title: string; + UsageByModel: { + /** Format: int64 */ + cache_read_tokens: number; + /** Format: int64 */ + cache_write_tokens: number; + /** Format: double */ + estimated_cost_usd?: number | null; + /** Format: int64 */ + input_tokens: number; + model: string; + /** Format: int64 */ + output_tokens: number; + /** Format: int64 */ + reasoning_tokens: number; + /** Format: int64 */ + request_count: number; }; - NotificationsResponse: { - notifications: components["schemas"]["Notification"][]; + UsageResponse: { + by_agent?: components["schemas"]["UsageByAgent"][]; + by_day?: components["schemas"]["UsageByDay"][]; + by_model?: components["schemas"]["UsageByModel"][]; + total: components["schemas"]["UsageTotals"]; }; - OpenAiOAuthBrowserStartRequest: { - model: string; + UsageTotals: { + /** Format: int64 */ + cache_read_tokens: number; + /** Format: int64 */ + cache_write_tokens: number; + cost_status: string; + /** Format: double */ + estimated_cost_usd?: number | null; + /** Format: int64 */ + input_tokens: number; + /** Format: int64 */ + output_tokens: number; + /** Format: int64 */ + reasoning_tokens: number; + /** Format: int64 */ + request_count: number; }; - OpenAiOAuthBrowserStartResponse: { + WakeAgentResponse: { + agent_id: string; + fired: boolean; message: string; - state?: string | null; - success: boolean; - user_code?: string | null; - verification_url?: string | null; }; - OpenAiOAuthBrowserStatusResponse: { - done: boolean; - found: boolean; - message?: string | null; - success: boolean; + WarmupSection: { + eager_embedding_load: boolean; + enabled: boolean; + /** Format: int64 */ + refresh_secs: number; + /** Format: int64 */ + startup_delay_secs: number; + }; + /** + * @description Current warmup lifecycle state. + * @enum {string} + */ + WarmupState: "cold" | "warming" | "warm" | "degraded"; + /** @description Warmup runtime status snapshot for API and observability. */ + WarmupStatus: { + /** Format: int64 */ + bulletin_age_secs?: number | null; + embedding_ready: boolean; + last_error?: string | null; + /** Format: int64 */ + last_refresh_unix_ms?: number | null; + state: components["schemas"]["WarmupState"]; + }; + WarmupStatusEntry: { + agent_id: string; + status: components["schemas"]["WarmupStatus"]; }; - OpenCodePermissionsResponse: { - bash: string; - edit: string; - webfetch: string; + WarmupStatusResponse: { + statuses: components["schemas"]["WarmupStatusEntry"][]; }; - OpenCodePermissionsUpdate: { - bash?: string | null; - edit?: string | null; - webfetch?: string | null; + WarmupTriggerRequest: { + agent_id?: string | null; + force?: boolean; }; - OpenCodeSettingsResponse: { - enabled: boolean; - /** Format: int32 */ - max_restart_retries: number; - max_servers: number; - path: string; - permissions: components["schemas"]["OpenCodePermissionsResponse"]; - /** Format: int64 */ - server_startup_timeout_secs: number; + WarmupTriggerResponse: { + accepted_agents: string[]; + forced: boolean; + status: string; }; - OpenCodeSettingsUpdate: { + WarmupUpdate: { + eager_embedding_load?: boolean | null; enabled?: boolean | null; - /** Format: int32 */ - max_restart_retries?: number | null; - max_servers?: number | null; - path?: string | null; - permissions?: null | components["schemas"]["OpenCodePermissionsUpdate"]; /** Format: int64 */ - server_startup_timeout_secs?: number | null; + refresh_secs?: number | null; + /** Format: int64 */ + startup_delay_secs?: number | null; }; - PlatformCredentials: { - discord_token?: string | null; - email_from_address?: string | null; - email_from_name?: string | null; - email_imap_host?: string | null; - email_imap_password?: string | null; - /** Format: int32 */ - email_imap_port?: number | null; - email_imap_username?: string | null; - email_smtp_host?: string | null; - email_smtp_password?: string | null; - /** Format: int32 */ - email_smtp_port?: number | null; - email_smtp_username?: string | null; - slack_app_token?: string | null; - slack_bot_token?: string | null; - telegram_token?: string | null; - twitch_client_id?: string | null; - twitch_client_secret?: string | null; - twitch_oauth_token?: string | null; - twitch_refresh_token?: string | null; - twitch_username?: string | null; + /** + * @description A webhook as it is safe to describe. + * + * There is no `secret` field, and that is structural rather than a matter of + * remembering: the type the store hands back does not carry the secret either, + * so there is nothing here that a future edit could accidentally serialise. + */ + WebhookResponse: { + /** + * @description Where deliveries go, so an operator can paste it into a CI config + * without reconstructing it from the route table. + */ + delivery_path: string; + /** @description The header the shared secret goes in. */ + secret_header: string; + webhook: components["schemas"]["WorkflowWebhook"]; }; - PlatformStatus: { - configured: boolean; - enabled: boolean; + WikiActionResponse: { + message: string; + success: boolean; }; - PortalConversation: { - agent_id: string; + WikiHistoryResponse: { + versions: components["schemas"]["WikiPageVersion"][]; + }; + WikiListResponse: { + pages: components["schemas"]["WikiPageSummary"][]; + total: number; + }; + WikiPage: { archived: boolean; - /** Format: date-time */ + content: string; created_at: string; + created_by: string; id: string; - settings?: null | components["schemas"]["ConversationSettings"]; + page_type: string; + related: string[]; + slug: string; title: string; - title_source: string; - /** Format: date-time */ updated_at: string; + updated_by: string; + /** Format: int64 */ + version: number; }; - PortalConversationResponse: { - conversation: components["schemas"]["PortalConversation"]; + WikiPageResponse: { + page: components["schemas"]["WikiPage"]; }; - PortalConversationSummary: { - agent_id: string; - archived: boolean; - /** Format: date-time */ - created_at: string; + WikiPageSummary: { id: string; - /** Format: date-time */ - last_message_at?: string | null; - last_message_preview?: string | null; - last_message_role?: string | null; - /** Format: int64 */ - message_count: number; - settings?: null | components["schemas"]["ConversationSettings"]; + page_type: string; + slug: string; title: string; - title_source: string; - /** Format: date-time */ updated_at: string; + updated_by: string; + /** Format: int64 */ + version: number; }; - PortalConversationsResponse: { - conversations: components["schemas"]["PortalConversationSummary"][]; - }; - PortalHistoryMessage: { + WikiPageVersion: { + author_id: string; + author_type: string; content: string; + created_at: string; + edit_summary?: string | null; id: string; - role: string; - }; - PortalSendRequest: { - agent_id: string; - /** @description IDs of pre-uploaded attachments to include with this message. */ - attachment_ids?: string[]; - message: string; - sender_name?: string; - session_id: string; - }; - PortalSendResponse: { - ok: boolean; + page_id: string; + /** Format: int64 */ + version: number; }; - /** @description A fully loaded preset with all identity file content. */ - Preset: { - identity: string; - meta: components["schemas"]["PresetMeta"]; - role: string; - soul: string; + /** @description Worker context settings control what context workers receive when spawned. */ + WorkerContextMode: { + /** @description What conversation context the worker sees. */ + history: components["schemas"]["WorkerHistoryMode"]; + /** @description What memory context the worker gets. */ + memory: components["schemas"]["WorkerMemoryMode"]; + /** + * @description Whether the worker gets wiki tools (wiki_create, wiki_edit, wiki_read, wiki_list, + * wiki_search, wiki_history). Defaults to true so all workers can access the wiki. + */ + wiki_write?: boolean; }; - /** - * @description Default operational parameters suggested by a preset. - * - * Model routing is intentionally excluded — presets are provider-agnostic. - * The factory conversation handles model selection at creation time when the - * user's available providers are known. - */ - PresetDefaults: { - /** Format: int32 */ - max_concurrent_workers?: number | null; - /** Format: int32 */ - max_turns?: number | null; + WorkerDetailResponse: { + channel_id?: string | null; + channel_name?: string | null; + completed_at?: string | null; + /** @description Working directory for OpenCode workers. */ + directory?: string | null; + id: string; + /** @description Whether this worker accepts follow-up input via route. */ + interactive: boolean; + /** + * Format: int32 + * @description OpenCode server port (for workers with an embeddable web UI). + */ + opencode_port?: number | null; + /** @description OpenCode session ID (for workers with an embeddable web UI). */ + opencode_session_id?: string | null; + result?: string | null; + started_at: string; + status: string; + task: string; + /** Format: int64 */ + tool_calls: number; + transcript?: components["schemas"]["TranscriptStep"][] | null; + worker_type: string; }; - /** @description Metadata for a preset archetype (returned in list responses). */ - PresetMeta: { - defaults?: components["schemas"]["PresetDefaults"]; - description: string; - icon: string; + /** @description How much conversation history a worker receives. */ + WorkerHistoryMode: "none" | "summary" | { + /** + * Format: int32 + * @description Last N messages from the parent conversation. + */ + recent: number; + } | "full"; + WorkerListItem: { + channel_id?: string | null; + channel_name?: string | null; + completed_at?: string | null; + /** @description Working directory for OpenCode workers. */ + directory?: string | null; + has_transcript: boolean; id: string; - name: string; - tags?: string[]; + /** @description Whether this worker accepts follow-up input via route. */ + interactive: boolean; + /** @description Live status text from StatusBlock (running workers only). */ + live_status?: string | null; + /** + * Format: int32 + * @description OpenCode server port (for workers with an embeddable web UI). + */ + opencode_port?: number | null; + /** @description OpenCode session ID (for workers with an embeddable web UI). */ + opencode_session_id?: string | null; + /** @description Project ID this worker is linked to. */ + project_id?: string | null; + /** @description Project name (resolved via join). */ + project_name?: string | null; + started_at: string; + status: string; + task: string; + /** + * Format: int64 + * @description Total tool calls. From DB for completed workers, from StatusBlock for running. + */ + tool_calls: number; + worker_type: string; }; - ProcessTokens: { - /** Format: int64 */ - cache_read: number; - /** Format: double */ - cost_usd: number; - /** Format: int64 */ - input: number; - /** Format: int64 */ - output: number; + WorkerListResponse: { /** Format: int64 */ - reasoning: number; + total: number; + workers: components["schemas"]["WorkerListItem"][]; }; - Project: { + /** + * @description How much memory context a worker receives. + * @enum {string} + */ + WorkerMemoryMode: "none" | "ambient" | "tools" | "full"; + /** @description A reusable pipeline definition. */ + Workflow: { created_at: string; - description: string; - icon: string; + description?: string | null; id: string; - logo_path?: string | null; + /** @description JSON Schema for the input a whole run is launched with. */ + input_schema?: unknown; name: string; - root_path: string; - settings: unknown; - /** Format: int64 */ - sort_order: number; - status: components["schemas"]["ProjectStatus"]; - tags: string[]; updated_at: string; }; - ProjectListResponse: { - projects: components["schemas"]["Project"][]; + WorkflowActionResponse: { + message: string; + success: boolean; }; - ProjectRepo: { - created_at: string; - /** @description Currently checked-out branch (may differ from `default_branch`). */ - current_branch?: string | null; - default_branch: string; - description: string; - /** Format: int64 */ - disk_usage_bytes?: number | null; - id: string; - name: string; - path: string; - project_id: string; - remote_url: string; - updated_at: string; + /** + * @description A template and everything that references it. + * + * One response rather than four endpoints because the editor cannot render + * anything useful without all of it — a step list with no edges is not a + * pipeline — and four round trips can interleave with a save. + */ + WorkflowDetailResponse: { + bindings: components["schemas"]["StepBinding"][]; + edges: components["schemas"]["WorkflowEdge"][]; + /** + * @description Conditions on steps. Part of the same response for the same reason the + * edges are: a canvas that draws a step without its condition draws a + * pipeline that is not the one that runs. + */ + gates: components["schemas"]["StepGate"][]; + steps: components["schemas"]["WorkflowStep"][]; + workflow: components["schemas"]["Workflow"]; }; - ProjectResponse: components["schemas"]["ProjectWithRelations"]; - /** @enum {string} */ - ProjectStatus: "active" | "archived"; - /** @description Full project with nested repos and worktrees for API responses. */ - ProjectWithRelations: components["schemas"]["Project"] & { - repos: components["schemas"]["ProjectRepo"][]; - worktrees: components["schemas"]["ProjectWorktreeWithRepo"][]; + WorkflowEdge: { + child_step_key: string; + /** + * @description `normal` or `on_exhausted`. An editor that drew both alike would draw a + * pipeline that is not the one that runs. + */ + kind: string; + parent_step_key: string; }; - ProjectWorktree: { - branch: string; - created_at: string; - created_by: string; - /** Format: int64 */ - disk_usage_bytes?: number | null; - id: string; - name: string; - path: string; - project_id: string; - repo_id: string; - updated_at: string; + WorkflowListResponse: { + workflows: components["schemas"]["Workflow"][]; }; - /** @description Worktree with the source repo name resolved. */ - ProjectWorktreeWithRepo: components["schemas"]["ProjectWorktree"] & { - repo_name: string; + WorkflowResponse: { + workflow: components["schemas"]["Workflow"]; }; - ProjectsSection: { - auto_create_worktrees: boolean; - auto_discover_repos: boolean; - auto_discover_worktrees: boolean; + /** @description One launch of a workflow. */ + WorkflowRun: { + created_at: string; + /** + * @description When the run stopped, in any terminal sense. `None` exactly while + * `status` is `running`. + */ + finished_at?: string | null; + id: string; + inputs: unknown; + launched_by: string; + status: components["schemas"]["RunStatus"]; + /** @description Why the run reached its current status, in words. */ + status_reason?: string | null; + workflow_id: string; + }; + /** @description A schedule attached to a workflow, launching with a stored input. */ + WorkflowSchedule: { + /** @description Which agent owns and executes the emitted tasks. */ + agent_id: string; + created_at: string; + /** @description 5-field cron expression, read in UTC. `None` uses `interval_secs`. */ + cron_expr?: string | null; + enabled: boolean; + id: string; + /** @description The launch payload. A literal, because a schedule cannot prompt. */ + inputs: unknown; /** Format: int64 */ - disk_usage_warning_threshold: number; - use_worktrees: boolean; - worktree_name_template: string; + interval_secs: number; + last_detail?: string | null; + last_fired_at?: string | null; + last_outcome?: null | components["schemas"]["ScheduleOutcome"]; + last_run_id?: string | null; + name: string; + next_run_at?: string | null; + workflow_id: string; }; - ProjectsUpdate: { - auto_create_worktrees?: boolean | null; - auto_discover_repos?: boolean | null; - auto_discover_worktrees?: boolean | null; + /** @description One step of a pipeline. Becomes exactly one task per launch. */ + WorkflowStep: { + /** + * @description `None` means the agent that launched the run — unless + * `required_capabilities` is set, in which case nobody is named and the + * emitted task goes into the pool. + */ + assigned_agent_id?: string | null; + /** + * @description The command line, for a command step. `None` on every agent step, and + * launch refuses an agent step that carries one. + */ + command?: string | null; + /** + * Format: int64 + * @description Hard wall-clock ceiling for a command step, in seconds. + * + * Required rather than inherited from a default: a stored command runs + * unattended and forever, and the author is the only person who knows + * whether this is a two-second linter or a four-minute build. + */ + command_timeout_secs?: number | null; + /** @description What a decision inside a loop body does on the second pass. */ + decision_ask?: components["schemas"]["DecisionAsk"]; + /** + * @description Who may answer. Empty or `None` means anyone. **Advisory in v1** — see + * [`crate::tasks::Task::decision_asked_of`]. + */ + decision_asked_of?: string[] | null; + /** + * @description The answer that applies on a `default` timeout. Validated against this + * step's own `output_schema` at launch, not when the timeout fires. + */ + decision_default_answer?: unknown; + /** + * @description The question a decision step asks, as the person answering reads it. + * Required on a decision step and refused on every other kind. + */ + decision_question?: string | null; + /** @description What happens if nobody answers. */ + decision_timeout_action?: components["schemas"]["DecisionTimeoutAction"]; + /** + * Format: int64 + * @description How long, in seconds, from the moment the decision is asked. Required by + * `default` and `fail`, refused by `wait`. + */ + decision_timeout_secs?: number | null; + description?: string | null; + /** + * Format: int64 + * @description The exit code that means success, for the steps where non-zero really is + * a failure. Absent by default — see [`StepKind::Command`]. + */ + expect_exit_code?: number | null; + /** + * @description Pointer *within each item* naming its branch, e.g. `/name` over + * `{"name": "repo-a"}` labels the branch `repo-a`. + * + * This is what makes a fan-in keyed rather than positional. Without it the + * index is used and the keys come out `0`, `1`, `2` — honest, but far less + * useful in a report. + */ + for_each_key?: string | null; + /** @description RFC 6901 pointer into that step's outputs. Must select an array. */ + for_each_pointer?: string | null; + /** + * @description Which upstream step produces the collection this step iterates. + * + * Set, and the step is a fan-out: it becomes one task per item rather than + * one task, and the width is not known until that step finishes. + */ + for_each_step_key?: string | null; + input_schema?: unknown; + /** @description Agent step or command step. See [`StepKind`]. */ + kind?: components["schemas"]["StepKind"]; + /** + * @description Which loop body this step belongs to. + * + * A loop is one or more steps sharing this name. A body of one step is the + * degenerate case and needs no special handling. + */ + loop_group?: string | null; + /** + * Format: int64 + * @description How many passes the body may run before the loop gives up. + * + * `None` means [`crate::tasks::DEFAULT_LOOP_MAX_ITERATIONS`]. Read from the + * body's exit step only; set anywhere else it would be a number nothing + * consumes, so launch refuses that rather than letting it sit in a row. + */ + loop_max_iterations?: number | null; + /** + * @description The exit predicate, as the same object a `task_output` gate takes: + * `{"pointer": "/tests/passed", "equals": true}`. + * + * Deliberately not a second predicate language — conditional steps, + * external gating, and loop exit are one question asked in three places. + * Required on the body's exit step: a loop with no exit condition always + * burns its whole budget. + */ + loop_until?: unknown; + output_schema?: unknown; /** Format: int64 */ - disk_usage_warning_threshold?: number | null; - use_worktrees?: boolean | null; - worktree_name_template?: string | null; + position: number; + priority: components["schemas"]["TaskPriority"]; + repo_id?: string | null; + /** + * @description What this step needs, instead of who should do it. + * + * The step-level half of the same choice a task has. `None` on every step + * that exists today. Set, and the emitted task is unassigned and claimed + * by whichever capable agent asks first. + * + * A requirement nothing in the fleet can satisfy is refused at **launch**, + * the way an unknown step reference already is — a template is edited by + * somebody who is still watching, and a pooled task nothing can claim is + * otherwise only visible in the sweep report. + */ + required_capabilities?: string[] | null; + /** @description Stable name that edges and bindings reference. */ + step_key: string; + /** @description Per-step instructions appended to the worker prompt at pickup. */ + system_prompt?: string | null; + title: string; + workflow_id: string; + /** @description What a provisioned worktree forks from. `None` means the repo's HEAD. */ + worktree_base_ref?: string | null; + /** + * @description What checkout this step runs in. See + * [`crate::workflows::worktrees::WorktreeMode`]. + */ + worktree_mode?: components["schemas"]["WorktreeMode"]; }; - PromptCaptureBody: { - channel_id: string; + /** + * @description An inbound endpoint mapping a payload to a run input. + * + * Note what is *not* on this struct: the secret. It goes in as plaintext once, + * is hashed immediately, and is never read back out — there is no field here + * that could be serialised into a response by accident. + */ + WorkflowWebhook: { + agent_id: string; + created_at: string; enabled: boolean; + /** @description `{ "": "" }`. */ + input_pointers: { + [key: string]: unknown; + }; + last_delivery_at?: string | null; + last_detail?: string | null; + last_outcome?: null | components["schemas"]["DeliveryOutcome"]; + last_run_id?: string | null; + workflow_id: string; }; - ProviderConfigResponse: { - api_version?: string | null; - base_url?: string | null; - deployment?: string | null; - message: string; - success: boolean; - }; - ProviderModelTestRequest: { - api_key: string; - api_version?: string | null; - base_url?: string | null; - deployment?: string | null; - model: string; - provider: string; + /** + * @description Where a step gets its working directory from. + * + * `Inherit` is the default and is exactly today's behaviour, which is what lets + * every template that predates this feature keep working untouched. + * @enum {string} + */ + WorktreeMode: "inherit" | "per_run" | "per_branch"; + WorktreeResponse: { + worktree: components["schemas"]["ProjectWorktree"]; }; - ProviderModelTestResponse: { - message: string; - model: string; - provider: string; - sample?: string | null; - success: boolean; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + get_activity: { + parameters: { + query?: { + /** @description ISO 8601 lower bound (default: 30 days ago). */ + since?: string | null; + /** @description ISO 8601 upper bound. */ + until?: string | null; + }; + header?: never; + path?: never; + cookie?: never; }; - ProviderStatus: { - anthropic: boolean; - azure: boolean; - deepseek: boolean; - fireworks: boolean; - gemini: boolean; - github_copilot: boolean; - groq: boolean; - kilo: boolean; - minimax: boolean; - minimax_cn: boolean; - mistral: boolean; - moonshot: boolean; - nvidia: boolean; - ollama: boolean; - openai: boolean; - openai_chatgpt: boolean; - opencode_go: boolean; - opencode_zen: boolean; - openrouter: boolean; - together: boolean; - xai: boolean; - zai_coding_plan: boolean; - zhipu: boolean; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ActivityResponse"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - ProviderUpdateRequest: { - api_key: string; - api_version?: string | null; - base_url?: string | null; - deployment?: string | null; - model: string; - provider: string; + }; + list_agents: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - ProviderUpdateResponse: { - message: string; - success: boolean; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AgentsResponse"]; + }; + }; }; - ProvidersResponse: { - has_any: boolean; - providers: components["schemas"]["ProviderStatus"]; + }; + update_agent: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - PutSecretBody: { - category?: null | components["schemas"]["SecretCategory"]; - value: string; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateAgentRequest"]; + }; }; - PutSecretResponse: { - category: components["schemas"]["SecretCategory"]; - message: string; - name: string; - reload_required: boolean; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - RawConfigResponse: { - content: string; + }; + create_agent: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - RawConfigUpdateRequest: { - content: string; + requestBody: { + content: { + "application/json": components["schemas"]["CreateAgentRequest"]; + }; }; - RawConfigUpdateResponse: { - message: string; - success: boolean; + responses: { + /** @description Agent created successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Invalid request or agent limit reached */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Agent already exists */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - ReconnectMcpRequest: { - agent_id: string; - server_name: string; + }; + delete_agent: { + parameters: { + query: { + /** @description Agent ID to delete */ + agent_id: string; + }; + header?: never; + path?: never; + cookie?: never; }; - RegistryBrowseResponse: { - has_more: boolean; - skills: components["schemas"]["RegistrySkill"][]; - /** Format: int64 */ - total?: number | null; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - RegistrySearchResponse: { - count: number; - query: string; - skills: components["schemas"]["RegistrySkill"][]; + }; + get_avatar: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + }; + header?: never; + path?: never; + cookie?: never; }; - RegistrySkill: { - description?: string | null; - id?: string | null; - /** Format: int64 */ - installs: number; - name: string; - skillId: string; - source: string; + requestBody?: never; + responses: { + /** @description Avatar image */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Avatar or agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - RegistrySkillContentResponse: { - content?: string | null; - skill_id: string; - source: string; + }; + upload_avatar: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + }; + header?: never; + path?: never; + cookie?: never; }; - /** - * @description Relation types for memory associations. - * @enum {string} - */ - RelationType: "related_to" | "updates" | "contradicts" | "caused_by" | "result_of" | "part_of"; - RemoveSkillRequest: { - agent_id: string; - name: string; + requestBody?: never; + responses: { + /** @description Avatar uploaded successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Unsupported image type */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Payload too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - RemoveSkillResponse: { - path?: string | null; - success: boolean; + }; + delete_avatar: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + }; + header?: never; + path?: never; + cookie?: never; }; - ReorderProjectsRequest: { - /** @description Project IDs in the desired display order (first = sort_order 0). */ - ids: string[]; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - RepoResponse: { - repo: components["schemas"]["ProjectRepo"]; + }; + get_agent_config: { + parameters: { + query: { + agent_id: string; + }; + header?: never; + path?: never; + cookie?: never; }; - /** - * @description Response mode controls how the channel handles incoming messages. - * @enum {string} - */ - ResponseMode: "active" | "observe" | "mention_only"; - RestoreVersionRequest: { - author_id?: string; - author_type?: string; - /** Format: int64 */ - version: number; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AgentConfigResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - RoutingSection: { - branch: string; - channel: string; - compactor: string; - cortex: string; - /** Format: int64 */ - rate_limit_cooldown_secs: number; - voice: string; - worker: string; + }; + update_agent_config: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - RoutingUpdate: { - branch?: string | null; - channel?: string | null; - compactor?: string | null; - cortex?: string | null; - /** Format: int64 */ - rate_limit_cooldown_secs?: number | null; - voice?: string | null; - worker?: string | null; + requestBody: { + content: { + "application/json": components["schemas"]["AgentConfigUpdateRequest"]; + }; }; - SandboxSection: { - mode: string; - passthrough_env: string[]; - writable_paths: string[]; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AgentConfigResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - SandboxUpdate: { - mode?: string | null; - passthrough_env?: string[] | null; - writable_paths?: string[] | null; + }; + list_cron_jobs: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + }; + header?: never; + path?: never; + cookie?: never; }; - /** @description Metadata for a saved attachment, returned after persisting to disk and DB. */ - SavedAttachmentMeta: { - filename: string; - id: string; - mime_type: string; - saved_filename: string; - /** Format: int64 */ - size_bytes: number; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CronListResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - /** - * @description Secret category determines subprocess exposure. - * - * All secrets are readable by Rust code via `SecretsStore::get()` regardless - * of category. The category answers one question: should this value be injected - * as an env var into worker subprocesses? - * @enum {string} - */ - SecretCategory: "system" | "tool"; - SecretInfoResponse: { - category: components["schemas"]["SecretCategory"]; - /** Format: date-time */ - created_at: string; - name: string; - /** Format: date-time */ - updated_at: string; + }; + create_or_update_cron: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - SecretListItem: { - category: components["schemas"]["SecretCategory"]; - /** Format: date-time */ - created_at: string; - name: string; - /** Format: date-time */ - updated_at: string; + requestBody: { + content: { + "application/json": components["schemas"]["CreateCronRequest"]; + }; }; - SecretListResponse: { - secrets: components["schemas"]["SecretListItem"][]; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CronActionResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - SetChannelArchiveRequest: { - agent_id: string; - archived: boolean; - channel_id: string; + }; + delete_cron: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + /** @description Cron job ID to delete */ + cron_id: string; + }; + header?: never; + path?: never; + cookie?: never; }; - SkillContentResponse: { - base_dir: string; - content: string; - description: string; - file_path: string; - name: string; - source: string; - source_repo?: string | null; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CronActionResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - SkillInfo: { - base_dir: string; - description: string; - file_path: string; - name: string; - source: string; - source_repo?: string | null; + }; + cron_executions: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + /** @description Cron job ID (optional) */ + cron_id?: string; + /** @description Maximum number of executions to return (default 50) */ + limit: number; + }; + header?: never; + path?: never; + cookie?: never; }; - SkillsListResponse: { - skills: components["schemas"]["SkillInfo"][]; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CronExecutionsResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - SshStatusResponse: { - enabled: boolean; - has_authorized_key: boolean; - /** Format: int32 */ - port: number; + }; + toggle_cron: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - StatusResponse: { - /** Format: int32 */ - pid: number; - status: string; - /** Format: int64 */ - uptime_seconds: number; - version: string; + requestBody: { + content: { + "application/json": components["schemas"]["ToggleCronRequest"]; + }; }; - StorageStatus: { - /** Format: int64 */ - available_bytes: number; - /** Format: int64 */ - total_bytes: number; - /** Format: int64 */ - used_bytes: number; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CronActionResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - Task: { - approved_at?: string | null; - approved_by?: string | null; - assigned_agent_id: string; - completed_at?: string | null; - created_at: string; - created_by: string; - description?: string | null; - id: string; - metadata: unknown; - owner_agent_id: string; - priority: components["schemas"]["TaskPriority"]; - source_memory_id?: string | null; - status: components["schemas"]["TaskStatus"]; - subtasks: components["schemas"]["TaskSubtask"][]; - /** Format: int64 */ - task_number: number; - title: string; - updated_at: string; - worker_id?: string | null; + }; + trigger_cron: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - TaskActionResponse: { - message: string; - success: boolean; + requestBody: { + content: { + "application/json": components["schemas"]["TriggerCronRequest"]; + }; }; - TaskListResponse: { - tasks: components["schemas"]["Task"][]; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CronActionResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - /** @enum {string} */ - TaskPriority: "critical" | "high" | "medium" | "low"; - TaskResponse: { - task: components["schemas"]["Task"]; + }; + get_identity: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + }; + header?: never; + path?: never; + cookie?: never; }; - /** @enum {string} */ - TaskStatus: "pending_approval" | "backlog" | "ready" | "in_progress" | "done"; - TaskSubtask: { - completed: boolean; - title: string; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IdentityResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - /** @description A unified timeline item combining messages, branch runs, and worker runs. */ - TimelineItem: { - attachments?: components["schemas"]["SavedAttachmentMeta"][]; - content: string; - created_at: string; - id: string; - role: string; - sender_id?: string | null; - sender_name?: string | null; - /** @enum {string} */ - type: "message"; - } | { - completed_at?: string | null; - conclusion?: string | null; - description: string; - id: string; - started_at: string; - /** @enum {string} */ - type: "branch_run"; - } | { - completed_at?: string | null; - id: string; - result?: string | null; - started_at: string; - status: string; - task: string; - /** @enum {string} */ - type: "worker_run"; - } | { - args: string; - completed_at?: string | null; - id: string; - result?: string | null; - started_at: string; - status: string; - tool_name: string; - /** @enum {string} */ - type: "tool_call_run"; + }; + update_identity: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - ToggleCronRequest: { - agent_id: string; - cron_id: string; - enabled: boolean; + requestBody: { + content: { + "application/json": components["schemas"]["IdentityUpdateRequest"]; + }; }; - TogglePlatformRequest: { - adapter?: string | null; - enabled: boolean; - platform: string; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IdentityResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - TokenSummary: { - by_process: { - [key: string]: components["schemas"]["ProcessTokens"]; + }; + list_ingest_files: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; }; - /** Format: int64 */ - cache_read: number; - /** Format: double */ - cost_usd: number; - /** Format: int64 */ - input: number; - /** Format: int64 */ - output: number; - /** Format: int64 */ - reasoning: number; + header?: never; + path?: never; + cookie?: never; }; - ToolsResponse: { - binaries: components["schemas"]["BinaryEntry"][]; - tools_bin: string; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IngestFilesResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - TopologyAgent: { - display_name?: string | null; - id: string; - name: string; - role?: string | null; + }; + upload_ingest_file: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + }; + header?: never; + path?: never; + cookie?: never; }; - TopologyGroup: { - agent_ids: string[]; - color?: string | null; - name: string; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IngestUploadResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - TopologyHuman: { - bio?: string | null; - description?: string | null; - discord_id?: string | null; - display_name?: string | null; - email?: string | null; - id: string; - role?: string | null; - slack_id?: string | null; - telegram_id?: string | null; + }; + delete_ingest_file: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + /** @description Content hash of the file to delete */ + content_hash: string; + }; + header?: never; + path?: never; + cookie?: never; }; - TopologyLink: { - direction: string; - from: string; - kind: string; - to: string; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IngestDeleteResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - /** @description Topology response for graph rendering. */ - TopologyResponse: { - agents: components["schemas"]["TopologyAgent"][]; - groups: components["schemas"]["TopologyGroup"][]; - humans: components["schemas"]["TopologyHuman"][]; - links: components["schemas"]["TopologyLink"][]; + }; + instance_overview: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - /** @description A single step in a worker transcript. */ - TranscriptStep: { - content: components["schemas"]["ActionContent"][]; - /** @enum {string} */ - type: "action"; - } | { - text: string; - /** @enum {string} */ - type: "user_text"; - } | { - text: string; - /** @enum {string} */ - type: "system_text"; - } | { - call_id: string; - name: string; - text: string; - /** @enum {string} */ - type: "tool_result"; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["InstanceOverviewResponse"]; + }; + }; }; - TriggerCronRequest: { - agent_id: string; - cron_id: string; + }; + list_agent_mcp: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + }; + header?: never; + path?: never; + cookie?: never; }; - TuningSection: { - branch_max_turns: number; - context_window: number; - history_backfill_count: number; - max_concurrent_branches: number; - max_concurrent_workers: number; - max_turns: number; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AgentMcpResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - TuningUpdate: { - branch_max_turns?: number | null; - context_window?: number | null; - history_backfill_count?: number | null; - max_concurrent_branches?: number | null; - max_concurrent_workers?: number | null; - max_turns?: number | null; + }; + reconnect_agent_mcp: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - UnlockBody: { - master_key: string; + requestBody: { + content: { + "application/json": components["schemas"]["ReconnectMcpRequest"]; + }; }; - UnreadCountResponse: { - /** Format: int64 */ - count: number; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Failed to reconnect */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - UpdateAgentRequest: { - agent_id: string; - display_name?: string | null; - gradient_end?: string | null; - gradient_start?: string | null; - role?: string | null; + }; + list_memories: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + /** @description Maximum number of results to return (default 50, max 200) */ + limit: number; + /** @description Number of results to skip for pagination */ + offset: number; + /** @description Filter by memory type (fact, preference, decision, identity, event, observation, goal, todo) */ + memory_type?: string; + /** @description Sort order: recent, importance, most_accessed (default: recent) */ + sort: string; + }; + header?: never; + path?: never; + cookie?: never; }; - UpdateBindingRequest: { - adapter?: string | null; - agent_id: string; - channel: string; - channel_ids?: string[]; - chat_id?: string | null; - dm_allowed_users?: string[]; - guild_id?: string | null; - original_adapter?: string | null; - original_agent_id: string; - original_channel: string; - original_chat_id?: string | null; - original_guild_id?: string | null; - original_team_id?: string | null; - original_workspace_id?: string | null; - require_mention?: boolean; - team_id?: string | null; - workspace_id?: string | null; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MemoriesListResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - UpdateBindingResponse: { - message: string; - success: boolean; + }; + memory_graph: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + /** @description Maximum number of nodes to return (default 200, max 500) */ + limit: number; + /** @description Number of nodes to skip for pagination */ + offset: number; + /** @description Filter by memory type */ + memory_type?: string; + /** @description Sort order: recent, importance, most_accessed (default: recent) */ + sort: string; + }; + header?: never; + path?: never; + cookie?: never; }; - UpdateChannelSettingsRequest: { - agent_id: string; - settings: components["schemas"]["ConversationSettings"]; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MemoryGraphResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - UpdateGroupRequest: { - agent_ids?: string[] | null; - color?: string | null; - name?: string | null; + }; + memory_graph_neighbors: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + /** @description Memory ID to get neighbors for */ + memory_id: string; + /** @description Neighbor traversal depth (default 1, max 3) */ + depth: number; + /** @description Comma-separated list of memory IDs to exclude from results */ + exclude?: string; + }; + header?: never; + path?: never; + cookie?: never; }; - UpdateHumanRequest: { - bio?: string | null; - description?: string | null; - discord_id?: string | null; - display_name?: string | null; - email?: string | null; - role?: string | null; - slack_id?: string | null; - telegram_id?: string | null; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MemoryGraphNeighborsResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - UpdateLinkRequest: { - direction?: string | null; - kind?: string | null; + }; + search_memories: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + /** @description Search query string */ + q: string; + /** @description Maximum number of results to return (default 20, max 100) */ + limit: number; + /** @description Filter by memory type */ + memory_type?: string; + }; + header?: never; + path?: never; + cookie?: never; }; - UpdatePortalConversationRequest: { - agent_id: string; - archived?: boolean | null; - settings?: null | components["schemas"]["ConversationSettings"]; - title?: string | null; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MemoriesSearchResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - UpdateProjectRequest: { - description?: string | null; - icon?: string | null; - logo_path?: string | null; - name?: string | null; - settings?: unknown; - status?: string | null; - tags?: string[] | null; + }; + agent_overview: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + }; + header?: never; + path?: never; + cookie?: never; }; - /** @description Result of an update check. */ - UpdateStatus: { - /** @description Whether the Docker socket is accessible (enables one-click update). */ - can_apply: boolean; - /** @description Human-readable reason when one-click apply is unavailable. */ - cannot_apply_reason?: string | null; - /** Format: date-time */ - checked_at?: string | null; - current_version: string; - deployment: components["schemas"]["Deployment"]; - /** @description Current container image reference when running in Docker. */ - docker_image?: string | null; - error?: string | null; - latest_version?: string | null; - release_notes?: string | null; - release_url?: string | null; - update_available: boolean; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AgentOverviewResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - UpdateTaskRequest: { - approved_by?: string | null; - assigned_agent_id?: string | null; - complete_subtask?: number | null; - description?: string | null; - metadata?: unknown; - priority?: string | null; - status?: string | null; - subtasks?: components["schemas"]["TaskSubtask"][] | null; - title?: string | null; - worker_id?: string | null; + }; + get_agent_profile: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + }; + header?: never; + path?: never; + cookie?: never; }; - UploadSkillResponse: { - installed: string[]; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AgentProfileResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - UsageByAgent: { - agent_id: string; - /** Format: int64 */ - cache_read_tokens: number; - /** Format: int64 */ - cache_write_tokens: number; - /** Format: double */ - estimated_cost_usd?: number | null; - /** Format: int64 */ - input_tokens: number; - /** Format: int64 */ - output_tokens: number; - /** Format: int64 */ - reasoning_tokens: number; - /** Format: int64 */ - request_count: number; + }; + list_projects: { + parameters: { + query?: { + status?: string | null; + }; + header?: never; + path?: never; + cookie?: never; }; - UsageByDay: { - /** Format: int64 */ - cache_read_tokens: number; - /** Format: int64 */ - cache_write_tokens: number; - date: string; - /** Format: double */ - estimated_cost_usd?: number | null; - /** Format: int64 */ - input_tokens: number; - /** Format: int64 */ - output_tokens: number; - /** Format: int64 */ - reasoning_tokens: number; - /** Format: int64 */ - request_count: number; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProjectListResponse"]; + }; + }; + /** @description No project store available */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - UsageByModel: { - /** Format: int64 */ - cache_read_tokens: number; - /** Format: int64 */ - cache_write_tokens: number; - /** Format: double */ - estimated_cost_usd?: number | null; - /** Format: int64 */ - input_tokens: number; - model: string; - /** Format: int64 */ - output_tokens: number; - /** Format: int64 */ - reasoning_tokens: number; - /** Format: int64 */ - request_count: number; + }; + create_project: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - UsageResponse: { - by_agent?: components["schemas"]["UsageByAgent"][]; - by_day?: components["schemas"]["UsageByDay"][]; - by_model?: components["schemas"]["UsageByModel"][]; - total: components["schemas"]["UsageTotals"]; + requestBody: { + content: { + "application/json": components["schemas"]["CreateProjectRequest"]; + }; }; - UsageTotals: { - /** Format: int64 */ - cache_read_tokens: number; - /** Format: int64 */ - cache_write_tokens: number; - cost_status: string; - /** Format: double */ - estimated_cost_usd?: number | null; - /** Format: int64 */ - input_tokens: number; - /** Format: int64 */ - output_tokens: number; - /** Format: int64 */ - reasoning_tokens: number; - /** Format: int64 */ - request_count: number; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProjectResponse"]; + }; + }; + /** @description No project store available */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - WarmupSection: { - eager_embedding_load: boolean; - enabled: boolean; - /** Format: int64 */ - refresh_secs: number; - /** Format: int64 */ - startup_delay_secs: number; + }; + reorder_projects: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - /** - * @description Current warmup lifecycle state. - * @enum {string} - */ - WarmupState: "cold" | "warming" | "warm" | "degraded"; - /** @description Warmup runtime status snapshot for API and observability. */ - WarmupStatus: { - /** Format: int64 */ - bulletin_age_secs?: number | null; - embedding_ready: boolean; - last_error?: string | null; - /** Format: int64 */ - last_refresh_unix_ms?: number | null; - state: components["schemas"]["WarmupState"]; + requestBody: { + content: { + "application/json": components["schemas"]["ReorderProjectsRequest"]; + }; }; - WarmupStatusEntry: { - agent_id: string; - status: components["schemas"]["WarmupStatus"]; + responses: { + /** @description Sort order updated */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No project store available */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - WarmupStatusResponse: { - statuses: components["schemas"]["WarmupStatusEntry"][]; + }; + get_project: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project ID */ + id: string; + }; + cookie?: never; }; - WarmupTriggerRequest: { - agent_id?: string | null; - force?: boolean; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProjectResponse"]; + }; + }; + /** @description Project not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - WarmupTriggerResponse: { - accepted_agents: string[]; - forced: boolean; - status: string; + }; + update_project: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project ID */ + id: string; + }; + cookie?: never; }; - WarmupUpdate: { - eager_embedding_load?: boolean | null; - enabled?: boolean | null; - /** Format: int64 */ - refresh_secs?: number | null; - /** Format: int64 */ - startup_delay_secs?: number | null; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateProjectRequest"]; + }; }; - WikiActionResponse: { - message: string; - success: boolean; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProjectResponse"]; + }; + }; + /** @description Project not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - WikiHistoryResponse: { - versions: components["schemas"]["WikiPageVersion"][]; + }; + delete_project: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project ID */ + id: string; + }; + cookie?: never; }; - WikiListResponse: { - pages: components["schemas"]["WikiPageSummary"][]; - total: number; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ActionResponse"]; + }; + }; + /** @description Project not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - WikiPage: { - archived: boolean; - content: string; - created_at: string; - created_by: string; - id: string; - page_type: string; - related: string[]; - slug: string; - title: string; - updated_at: string; - updated_by: string; - /** Format: int64 */ - version: number; + }; + disk_usage: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project ID */ + id: string; + }; + cookie?: never; }; - WikiPageResponse: { - page: components["schemas"]["WikiPage"]; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DiskUsageResponse"]; + }; + }; + /** @description Project not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - WikiPageSummary: { - id: string; - page_type: string; - slug: string; - title: string; - updated_at: string; - updated_by: string; - /** Format: int64 */ - version: number; + }; + serve_logo: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project ID */ + id: string; + }; + cookie?: never; }; - WikiPageVersion: { - author_id: string; - author_type: string; - content: string; - created_at: string; - edit_summary?: string | null; - id: string; - page_id: string; - /** Format: int64 */ - version: number; + requestBody?: never; + responses: { + /** @description Logo image */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No logo found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - /** @description Worker context settings control what context workers receive when spawned. */ - WorkerContextMode: { - /** @description What conversation context the worker sees. */ - history: components["schemas"]["WorkerHistoryMode"]; - /** @description What memory context the worker gets. */ - memory: components["schemas"]["WorkerMemoryMode"]; - /** - * @description Whether the worker gets wiki tools (wiki_create, wiki_edit, wiki_read, wiki_list, - * wiki_search, wiki_history). Defaults to true so all workers can access the wiki. - */ - wiki_write?: boolean; + }; + list_repo_dependencies: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project ID */ + id: string; + }; + cookie?: never; }; - WorkerDetailResponse: { - channel_id?: string | null; - channel_name?: string | null; - completed_at?: string | null; - /** @description Working directory for OpenCode workers. */ - directory?: string | null; - id: string; - /** @description Whether this worker accepts follow-up input via route. */ - interactive: boolean; - /** - * Format: int32 - * @description OpenCode server port (for workers with an embeddable web UI). - */ - opencode_port?: number | null; - /** @description OpenCode session ID (for workers with an embeddable web UI). */ - opencode_session_id?: string | null; - result?: string | null; - started_at: string; - status: string; - task: string; - /** Format: int64 */ - tool_calls: number; - transcript?: components["schemas"]["TranscriptStep"][] | null; - worker_type: string; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RepoDependencyListResponse"]; + }; + }; + /** @description Project not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - /** @description How much conversation history a worker receives. */ - WorkerHistoryMode: "none" | "summary" | { - /** - * Format: int32 - * @description Last N messages from the parent conversation. - */ - recent: number; - } | "full"; - WorkerListItem: { - channel_id?: string | null; - channel_name?: string | null; - completed_at?: string | null; - /** @description Working directory for OpenCode workers. */ - directory?: string | null; - has_transcript: boolean; - id: string; - /** @description Whether this worker accepts follow-up input via route. */ - interactive: boolean; - /** @description Live status text from StatusBlock (running workers only). */ - live_status?: string | null; - /** - * Format: int32 - * @description OpenCode server port (for workers with an embeddable web UI). - */ - opencode_port?: number | null; - /** @description OpenCode session ID (for workers with an embeddable web UI). */ - opencode_session_id?: string | null; - /** @description Project ID this worker is linked to. */ - project_id?: string | null; - /** @description Project name (resolved via join). */ - project_name?: string | null; - started_at: string; - status: string; - task: string; - /** - * Format: int64 - * @description Total tool calls. From DB for completed workers, from StatusBlock for running. - */ - tool_calls: number; - worker_type: string; + }; + declare_repo_dependency: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project ID */ + id: string; + }; + cookie?: never; }; - WorkerListResponse: { - /** Format: int64 */ - total: number; - workers: components["schemas"]["WorkerListItem"][]; + requestBody: { + content: { + "application/json": components["schemas"]["DeclareRepoDependencyRequest"]; + }; }; - /** - * @description How much memory context a worker receives. - * @enum {string} - */ - WorkerMemoryMode: "none" | "ambient" | "tools" | "full"; - WorktreeResponse: { - worktree: components["schemas"]["ProjectWorktree"]; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RepoDependencyResponse"]; + }; + }; + /** @description Project or repo not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already declared */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Self-dependency, or a repo from another project */ + 422: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} -export type $defs = Record; -export interface operations { - get_activity: { + create_repo: { parameters: { - query?: { - /** @description ISO 8601 lower bound (default: 30 days ago). */ - since?: string | null; - /** @description ISO 8601 upper bound. */ - until?: string | null; + query?: never; + header?: never; + path: { + /** @description Project ID */ + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateRepoRequest"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RepoResponse"]; + }; + }; + /** @description Project not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; }; + }; + }; + scan_project: { + parameters: { + query?: never; header?: never; - path?: never; + path: { + /** @description Project ID */ + id: string; + }; cookie?: never; }; requestBody?: never; @@ -4556,11 +8151,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ActivityResponse"]; + "application/json": components["schemas"]["ProjectResponse"]; }; }; - /** @description Internal server error */ - 500: { + /** @description Project not found */ + 404: { headers: { [name: string]: unknown; }; @@ -4568,11 +8163,14 @@ export interface operations { }; }; }; - list_agents: { + list_worktree_orphans: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Project ID */ + id: string; + }; cookie?: never; }; requestBody?: never; @@ -4582,21 +8180,31 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AgentsResponse"]; + "application/json": components["schemas"]["OrphanWorktreesResponse"]; + }; + }; + /** @description Project not found */ + 404: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - update_agent: { + create_worktree: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Project ID */ + id: string; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["UpdateAgentRequest"]; + "application/json": components["schemas"]["CreateWorktreeRequest"]; }; }; responses: { @@ -4605,25 +8213,112 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["WorktreeResponse"]; }; }; - /** @description Invalid request */ - 400: { + /** @description Project or repo not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Agent not found */ + }; + }; + update_repo_dependency: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project ID */ + project_id: string; + /** @description Dependent repo ID */ + repo_id: string; + /** @description Depended-upon repo ID */ + depends_on_repo_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateRepoDependencyRequest"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RepoDependencyResponse"]; + }; + }; + /** @description Declaration not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete_repo_dependency: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project ID */ + project_id: string; + /** @description Dependent repo ID */ + repo_id: string; + /** @description Depended-upon repo ID */ + depends_on_repo_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ActionResponse"]; + }; + }; + /** @description Declaration not found */ 404: { headers: { [name: string]: unknown; }; - content?: never; + content?: never; + }; + }; + }; + delete_repo: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project ID */ + project_id: string; + /** @description Repository ID */ + repo_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ActionResponse"]; + }; }; - /** @description Internal server error */ - 500: { + /** @description Project or repo not found */ + 404: { headers: { [name: string]: unknown; }; @@ -4631,44 +8326,61 @@ export interface operations { }; }; }; - create_agent: { + repo_dependency_suggestions: { parameters: { query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateAgentRequest"]; + path: { + /** @description Project ID */ + project_id: string; + /** @description Repository ID */ + repo_id: string; }; + cookie?: never; }; + requestBody?: never; responses: { - /** @description Agent created successfully */ - 201: { + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["RepoDependencySuggestions"]; }; }; - /** @description Invalid request or agent limit reached */ - 400: { + /** @description Project or repo not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Agent already exists */ - 409: { + }; + }; + delete_worktree: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project ID */ + project_id: string; + /** @description Worktree ID */ + worktree_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ActionResponse"]; + }; }; - /** @description Internal server error */ - 500: { + /** @description Project or worktree not found */ + 404: { headers: { [name: string]: unknown; }; @@ -4676,10 +8388,10 @@ export interface operations { }; }; }; - delete_agent: { + list_skills: { parameters: { query: { - /** @description Agent ID to delete */ + /** @description Agent ID */ agent_id: string; }; header?: never; @@ -4693,18 +8405,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; - }; - }; - /** @description Invalid request */ - 400: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["SkillsListResponse"]; }; - content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Agent not found */ + 404: { headers: { [name: string]: unknown; }; @@ -4712,11 +8417,13 @@ export interface operations { }; }; }; - get_avatar: { + get_skill_content: { parameters: { query: { /** @description Agent ID */ agent_id: string; + /** @description Skill name */ + name: string; }; header?: never; path?: never; @@ -4724,14 +8431,15 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Avatar image */ 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["SkillContentResponse"]; + }; }; - /** @description Avatar or agent not found */ + /** @description Agent or skill not found */ 404: { headers: { [name: string]: unknown; @@ -4740,33 +8448,26 @@ export interface operations { }; }; }; - upload_avatar: { + install_skill: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["InstallSkillRequest"]; + }; + }; responses: { - /** @description Avatar uploaded successfully */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": unknown; - }; - }; - /** @description Unsupported image type */ - 400: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["InstallSkillResponse"]; }; - content?: never; }; /** @description Agent not found */ 404: { @@ -4775,13 +8476,6 @@ export interface operations { }; content?: never; }; - /** @description Payload too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; /** @description Internal server error */ 500: { headers: { @@ -4791,25 +8485,33 @@ export interface operations { }; }; }; - delete_avatar: { + remove_skill: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["RemoveSkillRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["RemoveSkillResponse"]; + }; + }; + /** @description Cannot remove instance-level skill */ + 403: { + headers: { + [name: string]: unknown; }; + content?: never; }; /** @description Agent not found */ 404: { @@ -4818,11 +8520,19 @@ export interface operations { }; content?: never; }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - get_agent_config: { + upload_skill: { parameters: { query: { + /** @description Agent ID */ agent_id: string; }; header?: never; @@ -4836,8 +8546,15 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AgentConfigResponse"]; + "application/json": components["schemas"]["UploadSkillResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; }; + content?: never; }; /** @description Agent not found */ 404: { @@ -4846,38 +8563,37 @@ export interface operations { }; content?: never; }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - update_agent_config: { + get_warmup_status: { parameters: { - query?: never; + query?: { + /** @description Optional agent ID to get status for a specific agent */ + agent_id?: string; + }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["AgentConfigUpdateRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AgentConfigResponse"]; - }; - }; - /** @description Invalid request */ - 400: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["WarmupStatusResponse"]; }; - content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Agent not found */ + 404: { headers: { [name: string]: unknown; }; @@ -4885,24 +8601,25 @@ export interface operations { }; }; }; - list_cron_jobs: { + trigger_warmup: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["WarmupTriggerRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CronListResponse"]; + "application/json": components["schemas"]["WarmupTriggerResponse"]; }; }; /** @description Agent not found */ @@ -4912,8 +8629,8 @@ export interface operations { }; content?: never; }; - /** @description Internal server error */ - 500: { + /** @description LLM manager not available */ + 503: { headers: { [name: string]: unknown; }; @@ -4921,33 +8638,31 @@ export interface operations { }; }; }; - create_or_update_cron: { + list_workers: { parameters: { - query?: never; + query: { + /** @description Agent ID */ + agent_id: string; + /** @description Maximum number of results to return */ + limit: number; + /** @description Number of results to skip */ + offset: number; + /** @description Filter by worker status */ + status?: string; + }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateCronRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CronActionResponse"]; - }; - }; - /** @description Invalid request */ - 400: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["WorkerListResponse"]; }; - content?: never; }; /** @description Agent not found */ 404: { @@ -4965,13 +8680,13 @@ export interface operations { }; }; }; - delete_cron: { + worker_detail: { parameters: { query: { /** @description Agent ID */ agent_id: string; - /** @description Cron job ID to delete */ - cron_id: string; + /** @description Worker ID */ + worker_id: string; }; header?: never; path?: never; @@ -4984,10 +8699,10 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CronActionResponse"]; + "application/json": components["schemas"]["WorkerDetailResponse"]; }; }; - /** @description Agent not found */ + /** @description Agent or worker not found */ 404: { headers: { [name: string]: unknown; @@ -5003,31 +8718,36 @@ export interface operations { }; }; }; - cron_executions: { + serve_attachment: { parameters: { - query: { + query?: { + /** @description When true, force Content-Disposition: attachment (download). */ + download?: boolean; + /** + * @description When true, serve a thumbnail-sized version (for display in the UI). + * Currently serves the full file — thumbnail generation is a future enhancement. + */ + thumbnail?: boolean; + }; + header?: never; + path: { /** @description Agent ID */ agent_id: string; - /** @description Cron job ID (optional) */ - cron_id?: string; - /** @description Maximum number of executions to return (default 50) */ - limit: number; + /** @description Attachment ID */ + attachment_id: string; }; - header?: never; - path?: never; cookie?: never; }; requestBody?: never; responses: { + /** @description File content */ 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["CronExecutionsResponse"]; - }; + content?: never; }; - /** @description Agent not found */ + /** @description Attachment not found */ 404: { headers: { [name: string]: unknown; @@ -5043,25 +8763,30 @@ export interface operations { }; }; }; - toggle_cron: { + list_attachments: { parameters: { - query?: never; + query?: { + /** @description Filter to attachments from a specific message. */ + message_id?: string | null; + limit?: number | null; + }; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ToggleCronRequest"]; + path: { + /** @description Agent ID */ + agent_id: string; + /** @description Channel ID */ + channel_id: string; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CronActionResponse"]; + "application/json": components["schemas"]["AttachmentListResponse"]; }; }; /** @description Agent not found */ @@ -5080,27 +8805,35 @@ export interface operations { }; }; }; - trigger_cron: { + upload_attachment: { parameters: { query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["TriggerCronRequest"]; + path: { + /** @description Agent ID */ + agent_id: string; + /** @description Channel / conversation ID */ + channel_id: string; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CronActionResponse"]; + "application/json": components["schemas"]["AttachmentUploadResponse"]; }; }; + /** @description Invalid or empty file */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; /** @description Agent not found */ 404: { headers: { @@ -5108,6 +8841,13 @@ export interface operations { }; content?: never; }; + /** @description File too large (max 50 MB) */ + 413: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; /** @description Internal server error */ 500: { headers: { @@ -5117,28 +8857,28 @@ export interface operations { }; }; }; - get_identity: { + wake_agent: { parameters: { - query: { + query?: never; + header?: never; + path: { /** @description Agent ID */ agent_id: string; }; - header?: never; - path?: never; cookie?: never; }; requestBody?: never; responses: { - 200: { + 202: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["IdentityResponse"]; + "application/json": components["schemas"]["WakeAgentResponse"]; }; }; - /** @description Agent not found */ - 404: { + /** @description Wake manager not running */ + 503: { headers: { [name: string]: unknown; }; @@ -5146,33 +8886,25 @@ export interface operations { }; }; }; - update_identity: { + list_bindings: { parameters: { - query?: never; + query?: { + /** @description Filter by agent ID */ + agent_id?: string; + }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["IdentityUpdateRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["IdentityResponse"]; - }; - }; - /** @description Agent not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["BindingsListResponse"]; }; - content?: never; }; /** @description Internal server error */ 500: { @@ -5183,27 +8915,28 @@ export interface operations { }; }; }; - list_ingest_files: { + update_binding: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateBindingRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["IngestFilesResponse"]; + "application/json": components["schemas"]["UpdateBindingResponse"]; }; }; - /** @description Agent not found */ + /** @description Binding not found or config not found */ 404: { headers: { [name: string]: unknown; @@ -5219,24 +8952,25 @@ export interface operations { }; }; }; - upload_ingest_file: { + create_binding: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["CreateBindingRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["IngestUploadResponse"]; + "application/json": components["schemas"]["CreateBindingResponse"]; }; }; /** @description Invalid request */ @@ -5246,13 +8980,6 @@ export interface operations { }; content?: never; }; - /** @description Agent not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; /** @description Internal server error */ 500: { headers: { @@ -5262,29 +8989,28 @@ export interface operations { }; }; }; - delete_ingest_file: { + delete_binding: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - /** @description Content hash of the file to delete */ - content_hash: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["DeleteBindingRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["IngestDeleteResponse"]; + "application/json": components["schemas"]["DeleteBindingResponse"]; }; }; - /** @description Agent not found */ + /** @description Binding not found or config not found */ 404: { headers: { [name: string]: unknown; @@ -5300,7 +9026,7 @@ export interface operations { }; }; }; - instance_overview: { + changelog: { parameters: { query?: never; header?: never; @@ -5314,16 +9040,20 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["InstanceOverviewResponse"]; + "application/json": unknown; }; }; }; }; - list_agent_mcp: { + list_channels: { parameters: { query: { - /** @description Agent ID */ - agent_id: string; + /** @description Include inactive channels */ + include_inactive: boolean; + /** @description Filter by agent ID */ + agent_id?: string; + /** @description Filter by active state */ + is_active?: boolean; }; header?: never; path?: never; @@ -5336,11 +9066,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AgentMcpResponse"]; + "application/json": components["schemas"]["ChannelsResponse"]; }; }; - /** @description Agent not found */ - 404: { + /** @description Internal server error */ + 500: { headers: { [name: string]: unknown; }; @@ -5348,18 +9078,19 @@ export interface operations { }; }; }; - reconnect_agent_mcp: { + delete_channel: { parameters: { - query?: never; + query: { + /** @description Agent ID that owns the channel */ + agent_id: string; + /** @description Channel ID to delete */ + channel_id: string; + }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["ReconnectMcpRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { @@ -5369,15 +9100,15 @@ export interface operations { "application/json": unknown; }; }; - /** @description Failed to reconnect */ - 400: { + /** @description Channel or agent not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Agent not found */ - 404: { + /** @description Internal server error */ + 500: { headers: { [name: string]: unknown; }; @@ -5385,35 +9116,28 @@ export interface operations { }; }; }; - list_memories: { + set_channel_archive: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - /** @description Maximum number of results to return (default 50, max 200) */ - limit: number; - /** @description Number of results to skip for pagination */ - offset: number; - /** @description Filter by memory type (fact, preference, decision, identity, event, observation, goal, todo) */ - memory_type?: string; - /** @description Sort order: recent, importance, most_accessed (default: recent) */ - sort: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["SetChannelArchiveRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MemoriesListResponse"]; + "application/json": unknown; }; }; - /** @description Agent not found */ + /** @description Channel or agent not found */ 404: { headers: { [name: string]: unknown; @@ -5429,35 +9153,35 @@ export interface operations { }; }; }; - memory_graph: { + cancel_process: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - /** @description Maximum number of nodes to return (default 200, max 500) */ - limit: number; - /** @description Number of nodes to skip for pagination */ - offset: number; - /** @description Filter by memory type */ - memory_type?: string; - /** @description Sort order: recent, importance, most_accessed (default: recent) */ - sort: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["CancelProcessRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["MemoryGraphResponse"]; - }; + content: { + "application/json": components["schemas"]["CancelProcessResponse"]; + }; + }; + /** @description Invalid process type or process ID */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; }; - /** @description Agent not found */ + /** @description Process or channel not found */ 404: { headers: { [name: string]: unknown; @@ -5473,17 +9197,15 @@ export interface operations { }; }; }; - memory_graph_neighbors: { + channel_messages: { parameters: { query: { - /** @description Agent ID */ - agent_id: string; - /** @description Memory ID to get neighbors for */ - memory_id: string; - /** @description Neighbor traversal depth (default 1, max 3) */ - depth: number; - /** @description Comma-separated list of memory IDs to exclude from results */ - exclude?: string; + /** @description Channel ID */ + channel_id: string; + /** @description Maximum number of messages to return (default: 20, max: 100) */ + limit: number; + /** @description Pagination cursor for fetching older messages */ + before?: string; }; header?: never; path?: never; @@ -5496,15 +9218,8 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MemoryGraphNeighborsResponse"]; - }; - }; - /** @description Agent not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["MessagesResponse"]; }; - content?: never; }; /** @description Internal server error */ 500: { @@ -5515,33 +9230,28 @@ export interface operations { }; }; }; - search_memories: { + set_prompt_capture: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - /** @description Search query string */ - q: string; - /** @description Maximum number of results to return (default 20, max 100) */ - limit: number; - /** @description Filter by memory type */ - memory_type?: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["PromptCaptureBody"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MemoriesSearchResponse"]; + "application/json": unknown; }; }; - /** @description Agent not found */ + /** @description Agent or settings not found */ 404: { headers: { [name: string]: unknown; @@ -5557,11 +9267,11 @@ export interface operations { }; }; }; - agent_overview: { + inspect_prompt: { parameters: { query: { - /** @description Agent ID */ - agent_id: string; + /** @description Channel ID to inspect */ + channel_id: string; }; header?: never; path?: never; @@ -5574,10 +9284,10 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AgentOverviewResponse"]; + "application/json": unknown; }; }; - /** @description Agent not found */ + /** @description Channel not found */ 404: { headers: { [name: string]: unknown; @@ -5593,11 +9303,13 @@ export interface operations { }; }; }; - get_agent_profile: { + list_prompt_snapshots: { parameters: { query: { - /** @description Agent ID */ - agent_id: string; + /** @description Channel ID to list snapshots for */ + channel_id: string; + /** @description Maximum number of snapshots to return (default: 50) */ + limit: number; }; header?: never; path?: never; @@ -5610,22 +9322,32 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AgentProfileResponse"]; + "application/json": unknown; }; }; - /** @description Agent not found */ + /** @description Snapshot store not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - list_projects: { + get_prompt_snapshot: { parameters: { - query?: { - status?: string | null; + query: { + /** @description Channel ID the snapshot belongs to */ + channel_id: string; + /** @description Snapshot timestamp in milliseconds */ + timestamp_ms: number; }; header?: never; path?: never; @@ -5638,41 +9360,18 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ProjectListResponse"]; + "application/json": unknown; }; }; - /** @description No project store available */ + /** @description Snapshot or store not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - }; - }; - create_project: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateProjectRequest"]; - }; - }; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ProjectResponse"]; - }; - }; - /** @description No project store available */ - 404: { + /** @description Internal server error */ + 500: { headers: { [name: string]: unknown; }; @@ -5680,42 +9379,34 @@ export interface operations { }; }; }; - reorder_projects: { + channel_status: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["ReorderProjectsRequest"]; - }; - }; + requestBody?: never; responses: { - /** @description Sort order updated */ - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; - }; - /** @description No project store available */ - 404: { - headers: { - [name: string]: unknown; + content: { + "application/json": unknown; }; - content?: never; }; }; }; - get_project: { + get_channel_settings: { parameters: { - query?: never; + query: { + agent_id: string; + }; header?: never; path: { - /** @description Project ID */ - id: string; + /** @description Channel conversation ID */ + channel_id: string; }; cookie?: never; }; @@ -5726,11 +9417,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ProjectResponse"]; + "application/json": components["schemas"]["ChannelSettingsResponse"]; }; }; - /** @description Project not found */ - 404: { + /** @description Internal server error */ + 500: { headers: { [name: string]: unknown; }; @@ -5738,19 +9429,19 @@ export interface operations { }; }; }; - update_project: { + update_channel_settings: { parameters: { query?: never; header?: never; path: { - /** @description Project ID */ - id: string; + /** @description Channel conversation ID */ + channel_id: string; }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["UpdateProjectRequest"]; + "application/json": components["schemas"]["UpdateChannelSettingsRequest"]; }; }; responses: { @@ -5759,11 +9450,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ProjectResponse"]; + "application/json": components["schemas"]["ChannelSettingsResponse"]; }; }; - /** @description Project not found */ - 404: { + /** @description Internal server error */ + 500: { headers: { [name: string]: unknown; }; @@ -5771,14 +9462,14 @@ export interface operations { }; }; }; - delete_project: { + conversation_defaults: { parameters: { - query?: never; - header?: never; - path: { - /** @description Project ID */ - id: string; + query: { + /** @description Agent ID */ + agent_id: string; }; + header?: never; + path?: never; cookie?: never; }; requestBody?: never; @@ -5788,26 +9479,37 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ActionResponse"]; + "application/json": components["schemas"]["ConversationDefaultsResponse"]; }; }; - /** @description Project not found */ + /** @description Agent not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - disk_usage: { + cortex_chat_messages: { parameters: { - query?: never; - header?: never; - path: { - /** @description Project ID */ - id: string; + query: { + /** @description Agent ID */ + agent_id: string; + /** @description Thread ID (omit for latest) */ + thread_id?: string; + /** @description Maximum messages to return (default: 50, max: 200) */ + limit: number; }; + header?: never; + path?: never; cookie?: never; }; requestBody?: never; @@ -5817,39 +9519,61 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["DiskUsageResponse"]; + "application/json": components["schemas"]["CortexChatMessagesResponse"]; }; }; - /** @description Project not found */ + /** @description Agent not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - serve_logo: { + cortex_chat_send: { parameters: { query?: never; header?: never; - path: { - /** @description Project ID */ - id: string; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["CortexChatSendRequest"]; + }; + }; responses: { - /** @description Logo image */ + /** @description SSE stream of chat events */ 200: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description No logo found */ - 404: { + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Cortex chat session busy */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { headers: { [name: string]: unknown; }; @@ -5857,47 +9581,50 @@ export interface operations { }; }; }; - create_repo: { + cortex_chat_delete_thread: { parameters: { query?: never; header?: never; - path: { - /** @description Project ID */ - id: string; - }; + path?: never; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["CreateRepoRequest"]; + "application/json": components["schemas"]["CortexChatDeleteThreadRequest"]; }; }; responses: { - 200: { + /** @description Thread deleted successfully */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["RepoResponse"]; - }; + content?: never; }; - /** @description Project not found */ + /** @description Agent or thread not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - scan_project: { + cortex_chat_threads: { parameters: { - query?: never; - header?: never; - path: { - /** @description Project ID */ - id: string; + query: { + /** @description Agent ID */ + agent_id: string; }; + header?: never; + path?: never; cookie?: never; }; requestBody?: never; @@ -5907,91 +9634,113 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ProjectResponse"]; + "application/json": components["schemas"]["CortexChatThreadsResponse"]; }; }; - /** @description Project not found */ + /** @description Agent not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - create_worktree: { + cortex_events: { parameters: { - query?: never; - header?: never; - path: { - /** @description Project ID */ - id: string; + query: { + /** @description Agent ID */ + agent_id: string; + /** @description Maximum events to return (default: 50, max: 200) */ + limit: number; + /** @description Offset for pagination (default: 0) */ + offset: number; + /** @description Filter by event type */ + event_type?: string; }; + header?: never; + path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateWorktreeRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WorktreeResponse"]; + "application/json": components["schemas"]["CortexEventsResponse"]; }; }; - /** @description Project or repo not found */ + /** @description Agent not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - delete_repo: { + events_sse: { parameters: { query?: never; header?: never; - path: { - /** @description Project ID */ - project_id: string; - /** @description Repository ID */ - repo_id: string; - }; + path?: never; cookie?: never; }; requestBody?: never; responses: { + /** @description SSE event stream */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ActionResponse"]; + "text/event-stream": unknown; }; }; - /** @description Project or repo not found */ - 404: { + }; + }; + list_presets: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["PresetMeta"][]; + }; }; }; }; - delete_worktree: { + get_preset: { parameters: { query?: never; header?: never; path: { - /** @description Project ID */ - project_id: string; - /** @description Worktree ID */ - worktree_id: string; + /** @description Preset ID */ + id: string; }; cookie?: never; }; @@ -6002,10 +9751,10 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ActionResponse"]; + "application/json": components["schemas"]["Preset"]; }; }; - /** @description Project or worktree not found */ + /** @description Preset not found */ 404: { headers: { [name: string]: unknown; @@ -6014,12 +9763,9 @@ export interface operations { }; }; }; - list_skills: { + health: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6031,26 +9777,14 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SkillsListResponse"]; - }; - }; - /** @description Agent not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["HealthResponse"]; }; - content?: never; }; }; }; - get_skill_content: { + idle: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - /** @description Skill name */ - name: string; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6062,56 +9796,31 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SkillContentResponse"]; - }; - }; - /** @description Agent or skill not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["IdleResponse"]; }; - content?: never; }; }; }; - install_skill: { + list_links: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["InstallSkillRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["InstallSkillResponse"]; - }; - }; - /** @description Agent not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; + "application/json": unknown; }; - content?: never; }; }; }; - remove_skill: { + create_link: { parameters: { query?: never; header?: never; @@ -6120,34 +9829,34 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["RemoveSkillRequest"]; + "application/json": components["schemas"]["CreateLinkRequest"]; }; }; responses: { - 200: { + 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["RemoveSkillResponse"]; + "application/json": unknown; }; }; - /** @description Cannot remove instance-level skill */ - 403: { + /** @description Invalid request */ + 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Agent not found */ + /** @description Agent or human not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Link already exists */ + 409: { headers: { [name: string]: unknown; }; @@ -6155,14 +9864,14 @@ export interface operations { }; }; }; - upload_skill: { + agent_links: { parameters: { - query: { + query?: never; + header?: never; + path: { /** @description Agent ID */ agent_id: string; }; - header?: never; - path?: never; cookie?: never; }; requestBody?: never; @@ -6172,54 +9881,60 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["UploadSkillResponse"]; - }; - }; - /** @description Invalid request */ - 400: { - headers: { - [name: string]: unknown; + "application/json": unknown; }; - content?: never; }; - /** @description Agent not found */ - 404: { + }; + }; + list_groups: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { headers: { [name: string]: unknown; }; - content?: never; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; + content: { + "application/json": unknown; }; - content?: never; }; }; }; - get_warmup_status: { + create_group: { parameters: { - query?: { - /** @description Optional agent ID to get status for a specific agent */ - agent_id?: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["CreateGroupRequest"]; + }; + }; responses: { - 200: { + 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WarmupStatusResponse"]; + "application/json": unknown; }; }; - /** @description Agent not found */ - 404: { + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Group already exists */ + 409: { headers: { [name: string]: unknown; }; @@ -6227,16 +9942,19 @@ export interface operations { }; }; }; - trigger_warmup: { + update_group: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Group name */ + group_name: string; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["WarmupTriggerRequest"]; + "application/json": components["schemas"]["UpdateGroupRequest"]; }; }; responses: { @@ -6245,18 +9963,25 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WarmupTriggerResponse"]; + "application/json": unknown; }; }; - /** @description Agent not found */ + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Group not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description LLM manager not available */ - 503: { + /** @description Group name conflict */ + 409: { headers: { [name: string]: unknown; }; @@ -6264,79 +9989,82 @@ export interface operations { }; }; }; - list_workers: { + delete_group: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - /** @description Maximum number of results to return */ - limit: number; - /** @description Number of results to skip */ - offset: number; - /** @description Filter by worker status */ - status?: string; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description Group name */ + group_name: string; + }; cookie?: never; }; requestBody?: never; responses: { - 200: { + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["WorkerListResponse"]; - }; + content?: never; }; - /** @description Agent not found */ + /** @description Group not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error */ - 500: { + }; + }; + list_humans: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": unknown; + }; }; }; }; - worker_detail: { + create_human: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - /** @description Worker ID */ - worker_id: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["CreateHumanRequest"]; + }; + }; responses: { - 200: { + 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WorkerDetailResponse"]; + "application/json": unknown; }; }; - /** @description Agent or worker not found */ - 404: { + /** @description Invalid request */ + 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Human ID already exists */ + 409: { headers: { [name: string]: unknown; }; @@ -6344,44 +10072,32 @@ export interface operations { }; }; }; - serve_attachment: { + update_human: { parameters: { - query?: { - /** @description When true, force Content-Disposition: attachment (download). */ - download?: boolean; - /** - * @description When true, serve a thumbnail-sized version (for display in the UI). - * Currently serves the full file — thumbnail generation is a future enhancement. - */ - thumbnail?: boolean; - }; + query?: never; header?: never; path: { - /** @description Agent ID */ - agent_id: string; - /** @description Attachment ID */ - attachment_id: string; + /** @description Human ID */ + human_id: string; }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateHumanRequest"]; + }; + }; responses: { - /** @description File content */ 200: { headers: { [name: string]: unknown; }; - content?: never; - }; - /** @description Attachment not found */ - 404: { - headers: { - [name: string]: unknown; + content: { + "application/json": unknown; }; - content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Human not found */ + 404: { headers: { [name: string]: unknown; }; @@ -6389,41 +10105,26 @@ export interface operations { }; }; }; - list_attachments: { + delete_human: { parameters: { - query?: { - /** @description Filter to attachments from a specific message. */ - message_id?: string | null; - limit?: number | null; - }; + query?: never; header?: never; path: { - /** @description Agent ID */ - agent_id: string; - /** @description Channel ID */ - channel_id: string; + /** @description Human ID */ + human_id: string; }; cookie?: never; }; requestBody?: never; responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AttachmentListResponse"]; - }; - }; - /** @description Agent not found */ - 404: { + 204: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Human not found */ + 404: { headers: { [name: string]: unknown; }; @@ -6431,80 +10132,70 @@ export interface operations { }; }; }; - upload_attachment: { + update_link: { parameters: { query?: never; header?: never; path: { - /** @description Agent ID */ - agent_id: string; - /** @description Channel / conversation ID */ - channel_id: string; + /** @description Source agent */ + from: string; + /** @description Target agent */ + to: string; }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateLinkRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AttachmentUploadResponse"]; + "application/json": unknown; }; }; - /** @description Invalid or empty file */ + /** @description Invalid request */ 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Agent not found */ + /** @description Link not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description File too large (max 50 MB) */ - 413: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; }; }; - list_bindings: { + delete_link: { parameters: { - query?: { - /** @description Filter by agent ID */ - agent_id?: string; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description Source agent */ + from: string; + /** @description Target agent */ + to: string; + }; cookie?: never; }; requestBody?: never; responses: { - 200: { + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BindingsListResponse"]; - }; + content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Link not found */ + 404: { headers: { [name: string]: unknown; }; @@ -6512,33 +10203,22 @@ export interface operations { }; }; }; - update_binding: { + list_mcp_servers: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateBindingRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["UpdateBindingResponse"]; - }; - }; - /** @description Binding not found or config not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["McpServerInfo"][]; }; - content?: never; }; /** @description Internal server error */ 500: { @@ -6549,7 +10229,7 @@ export interface operations { }; }; }; - create_binding: { + update_mcp_server: { parameters: { query?: never; header?: never; @@ -6558,7 +10238,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["CreateBindingRequest"]; + "application/json": components["schemas"]["CreateMcpServerRequest"]; }; }; responses: { @@ -6567,15 +10247,8 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CreateBindingResponse"]; - }; - }; - /** @description Invalid request */ - 400: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["MutationResponse"]; }; - content?: never; }; /** @description Internal server error */ 500: { @@ -6586,7 +10259,7 @@ export interface operations { }; }; }; - delete_binding: { + create_mcp_server: { parameters: { query?: never; header?: never; @@ -6595,7 +10268,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["DeleteBindingRequest"]; + "application/json": components["schemas"]["CreateMcpServerRequest"]; }; }; responses: { @@ -6604,15 +10277,8 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["DeleteBindingResponse"]; - }; - }; - /** @description Binding not found or config not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["MutationResponse"]; }; - content?: never; }; /** @description Internal server error */ 500: { @@ -6623,11 +10289,14 @@ export interface operations { }; }; }; - changelog: { + delete_mcp_server: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Server name */ + name: string; + }; cookie?: never; }; requestBody?: never; @@ -6637,23 +10306,26 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["MutationResponse"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - list_channels: { + reconnect_mcp_server: { parameters: { - query: { - /** @description Include inactive channels */ - include_inactive: boolean; - /** @description Filter by agent ID */ - agent_id?: string; - /** @description Filter by active state */ - is_active?: boolean; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description Server name */ + name: string; + }; cookie?: never; }; requestBody?: never; @@ -6663,7 +10335,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ChannelsResponse"]; + "application/json": components["schemas"]["MutationResponse"]; }; }; /** @description Internal server error */ @@ -6675,14 +10347,9 @@ export interface operations { }; }; }; - delete_channel: { + mcp_status: { parameters: { - query: { - /** @description Agent ID that owns the channel */ - agent_id: string; - /** @description Channel ID to delete */ - channel_id: string; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6694,15 +10361,31 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["McpAgentStatus"][]; }; }; - /** @description Channel or agent not found */ - 404: { + }; + }; + disconnect_platform: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DisconnectPlatformRequest"]; + }; + }; + responses: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": unknown; + }; }; /** @description Internal server error */ 500: { @@ -6713,7 +10396,7 @@ export interface operations { }; }; }; - set_channel_archive: { + create_messaging_instance: { parameters: { query?: never; header?: never; @@ -6722,7 +10405,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["SetChannelArchiveRequest"]; + "application/json": components["schemas"]["CreateMessagingInstanceRequest"]; }; }; responses: { @@ -6731,15 +10414,8 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; - }; - }; - /** @description Channel or agent not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["MessagingInstanceActionResponse"]; }; - content?: never; }; /** @description Internal server error */ 500: { @@ -6750,7 +10426,7 @@ export interface operations { }; }; }; - cancel_process: { + delete_messaging_instance: { parameters: { query?: never; header?: never; @@ -6759,7 +10435,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["CancelProcessRequest"]; + "application/json": components["schemas"]["DeleteMessagingInstanceRequest"]; }; }; responses: { @@ -6768,22 +10444,8 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CancelProcessResponse"]; - }; - }; - /** @description Invalid process type or process ID */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Process or channel not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["MessagingInstanceActionResponse"]; }; - content?: never; }; /** @description Internal server error */ 500: { @@ -6794,16 +10456,9 @@ export interface operations { }; }; }; - channel_messages: { + messaging_status: { parameters: { - query: { - /** @description Channel ID */ - channel_id: string; - /** @description Maximum number of messages to return (default: 20, max: 100) */ - limit: number; - /** @description Pagination cursor for fetching older messages */ - before?: string; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6815,7 +10470,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MessagesResponse"]; + "application/json": components["schemas"]["MessagingStatusResponse"]; }; }; /** @description Internal server error */ @@ -6827,7 +10482,7 @@ export interface operations { }; }; }; - set_prompt_capture: { + toggle_platform: { parameters: { query?: never; header?: never; @@ -6836,7 +10491,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["PromptCaptureBody"]; + "application/json": components["schemas"]["TogglePlatformRequest"]; }; }; responses: { @@ -6848,13 +10503,6 @@ export interface operations { "application/json": unknown; }; }; - /** @description Agent or settings not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; /** @description Internal server error */ 500: { headers: { @@ -6864,11 +10512,13 @@ export interface operations { }; }; }; - inspect_prompt: { + get_models: { parameters: { - query: { - /** @description Channel ID to inspect */ - channel_id: string; + query?: { + /** @description Filter by provider ID */ + provider?: string; + /** @description Filter by capability (input_audio, voice_transcription) */ + capability?: string; }; header?: never; path?: never; @@ -6881,15 +10531,8 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; - }; - }; - /** @description Channel not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["ModelsResponse"]; }; - content?: never; }; /** @description Internal server error */ 500: { @@ -6900,14 +10543,9 @@ export interface operations { }; }; }; - list_prompt_snapshots: { + refresh_models: { parameters: { - query: { - /** @description Channel ID to list snapshots for */ - channel_id: string; - /** @description Maximum number of snapshots to return (default: 50) */ - limit: number; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6919,15 +10557,8 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; - }; - }; - /** @description Snapshot store not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["ModelsResponse"]; }; - content?: never; }; /** @description Internal server error */ 500: { @@ -6938,13 +10569,17 @@ export interface operations { }; }; }; - get_prompt_snapshot: { + list_notifications: { parameters: { - query: { - /** @description Channel ID the snapshot belongs to */ - channel_id: string; - /** @description Snapshot timestamp in milliseconds */ - timestamp_ms: number; + query?: { + /** @description "unread" returns only unread notifications; anything else returns all. */ + filter?: string | null; + /** @description Filter by agent id. */ + agent_id?: string | null; + /** @description Filter by kind: "task_approval", "worker_failed", "cortex_observation". */ + kind?: string | null; + limit?: number; + offset?: number; }; header?: never; path?: never; @@ -6957,18 +10592,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; - }; - }; - /** @description Snapshot or store not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["NotificationsResponse"]; }; - content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Notification store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -6976,7 +10604,7 @@ export interface operations { }; }; }; - channel_status: { + dismiss_read: { parameters: { query?: never; header?: never; @@ -6985,26 +10613,52 @@ export interface operations { }; requestBody?: never; responses: { - 200: { + /** @description Read notifications dismissed */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": unknown; + content?: never; + }; + /** @description Notification store not initialized */ + 503: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - get_channel_settings: { + mark_all_read: { parameters: { - query: { - agent_id: string; - }; + query?: never; header?: never; - path: { - /** @description Channel conversation ID */ - channel_id: string; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description All marked as read */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Notification store not initialized */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; }; + }; + }; + unread_count: { + parameters: { + query?: never; + header?: never; + path?: never; cookie?: never; }; requestBody?: never; @@ -7014,11 +10668,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ChannelSettingsResponse"]; + "application/json": components["schemas"]["UnreadCountResponse"]; }; }; - /** @description Internal server error */ - 500: { + /** @description Notification store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -7026,32 +10680,34 @@ export interface operations { }; }; }; - update_channel_settings: { + dismiss_notification: { parameters: { query?: never; header?: never; path: { - /** @description Channel conversation ID */ - channel_id: string; + /** @description Notification id */ + id: string; }; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateChannelSettingsRequest"]; - }; - }; + requestBody?: never; responses: { - 200: { + /** @description Dismissed */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ChannelSettingsResponse"]; + content?: never; + }; + /** @description Not found or already dismissed */ + 404: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Notification store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -7059,35 +10715,34 @@ export interface operations { }; }; }; - conversation_defaults: { + mark_read: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description Notification id */ + id: string; + }; cookie?: never; }; requestBody?: never; responses: { - 200: { + /** @description Marked as read */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ConversationDefaultsResponse"]; - }; + content?: never; }; - /** @description Agent not found */ + /** @description Not found or already read */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Notification store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -7095,14 +10750,14 @@ export interface operations { }; }; }; - cortex_chat_messages: { + list_portal_conversations: { parameters: { query: { /** @description Agent ID */ agent_id: string; - /** @description Thread ID (omit for latest) */ - thread_id?: string; - /** @description Maximum messages to return (default: 50, max: 200) */ + /** @description Include archived conversations */ + include_archived: boolean; + /** @description Maximum number of conversations to return (default: 100, max: 500) */ limit: number; }; header?: never; @@ -7116,7 +10771,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CortexChatMessagesResponse"]; + "application/json": components["schemas"]["PortalConversationsResponse"]; }; }; /** @description Agent not found */ @@ -7135,7 +10790,7 @@ export interface operations { }; }; }; - cortex_chat_send: { + create_portal_conversation: { parameters: { query?: never; header?: never; @@ -7144,16 +10799,17 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["CortexChatSendRequest"]; + "application/json": components["schemas"]["CreatePortalConversationRequest"]; }; }; responses: { - /** @description SSE stream of chat events */ 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["PortalConversationResponse"]; + }; }; /** @description Agent not found */ 404: { @@ -7162,13 +10818,6 @@ export interface operations { }; content?: never; }; - /** @description Cortex chat session busy */ - 409: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; /** @description Internal server error */ 500: { headers: { @@ -7178,27 +10827,31 @@ export interface operations { }; }; }; - cortex_chat_delete_thread: { + update_portal_conversation: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Conversation session ID */ + session_id: string; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["CortexChatDeleteThreadRequest"]; + "application/json": components["schemas"]["UpdatePortalConversationRequest"]; }; }; responses: { - /** @description Thread deleted successfully */ - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["PortalConversationResponse"]; + }; }; - /** @description Agent or thread not found */ + /** @description Conversation not found */ 404: { headers: { [name: string]: unknown; @@ -7214,14 +10867,17 @@ export interface operations { }; }; }; - cortex_chat_threads: { + delete_portal_conversation: { parameters: { query: { /** @description Agent ID */ agent_id: string; }; header?: never; - path?: never; + path: { + /** @description Conversation session ID */ + session_id: string; + }; cookie?: never; }; requestBody?: never; @@ -7231,10 +10887,10 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CortexChatThreadsResponse"]; + "application/json": components["schemas"]["PortalSendResponse"]; }; }; - /** @description Agent not found */ + /** @description Conversation not found */ 404: { headers: { [name: string]: unknown; @@ -7250,17 +10906,15 @@ export interface operations { }; }; }; - cortex_events: { + portal_history: { parameters: { query: { /** @description Agent ID */ agent_id: string; - /** @description Maximum events to return (default: 50, max: 200) */ + /** @description Session ID */ + session_id: string; + /** @description Maximum number of messages to return (default: 100, max: 200) */ limit: number; - /** @description Offset for pagination (default: 0) */ - offset: number; - /** @description Filter by event type */ - event_type?: string; }; header?: never; path?: never; @@ -7273,7 +10927,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CortexEventsResponse"]; + "application/json": components["schemas"]["PortalHistoryMessage"][]; }; }; /** @description Agent not found */ @@ -7292,67 +10946,43 @@ export interface operations { }; }; }; - events_sse: { + portal_send: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["PortalSendRequest"]; + }; + }; responses: { - /** @description SSE event stream */ 200: { headers: { [name: string]: unknown; }; content: { - "text/event-stream": unknown; + "application/json": components["schemas"]["PortalSendResponse"]; }; }; - }; - }; - list_presets: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { + /** @description Invalid request */ + 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["PresetMeta"][]; - }; - }; - }; - }; - get_preset: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Preset ID */ - id: string; + content?: never; }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { + /** @description Agent not found */ + 404: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["Preset"]; - }; + content?: never; }; - /** @description Preset not found */ - 404: { + /** @description Messaging manager not available */ + 503: { headers: { [name: string]: unknown; }; @@ -7360,7 +10990,7 @@ export interface operations { }; }; }; - health: { + get_providers: { parameters: { query?: never; header?: never; @@ -7374,68 +11004,96 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["HealthResponse"]; + "application/json": components["schemas"]["ProvidersResponse"]; }; }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - idle: { + update_provider: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["ProviderUpdateRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["IdleResponse"]; + "application/json": components["schemas"]["ProviderUpdateResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - list_links: { + test_provider_model: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["ProviderModelTestRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["ProviderModelTestResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - create_link: { + delete_provider: { parameters: { query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateLinkRequest"]; + path: { + /** @description Provider ID to delete */ + provider: string; }; + cookie?: never; }; + requestBody?: never; responses: { - 201: { + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["ProviderUpdateResponse"]; }; }; /** @description Invalid request */ @@ -7445,15 +11103,8 @@ export interface operations { }; content?: never; }; - /** @description Agent or human not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Link already exists */ - 409: { + /** @description Provider not found */ + 404: { headers: { [name: string]: unknown; }; @@ -7461,14 +11112,11 @@ export interface operations { }; }; }; - agent_links: { + list_secrets: { parameters: { query?: never; header?: never; - path: { - /** @description Agent ID */ - agent_id: string; - }; + path?: never; cookie?: never; }; requestBody?: never; @@ -7478,12 +11126,19 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["SecretListResponse"]; + }; + }; + /** @description Secrets store not initialized */ + 503: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - list_groups: { + enable_encryption: { parameters: { query?: never; header?: never; @@ -7497,25 +11152,35 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["EncryptResponse"]; + }; + }; + /** @description Encryption already enabled */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Secrets store not initialized */ + 503: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - create_group: { + export_secrets: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateGroupRequest"]; - }; - }; + requestBody?: never; responses: { - 201: { + 200: { headers: { [name: string]: unknown; }; @@ -7523,15 +11188,15 @@ export interface operations { "application/json": unknown; }; }; - /** @description Invalid request */ - 400: { + /** @description Secret store is locked */ + 423: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Group already exists */ - 409: { + /** @description Secrets store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -7539,19 +11204,16 @@ export interface operations { }; }; }; - update_group: { + import_secrets: { parameters: { query?: never; header?: never; - path: { - /** @description Group name */ - group_name: string; - }; + path?: never; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["UpdateGroupRequest"]; + "application/json": components["schemas"]["ImportBody"]; }; }; responses: { @@ -7563,22 +11225,15 @@ export interface operations { "application/json": unknown; }; }; - /** @description Invalid request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Group not found */ - 404: { + /** @description Secret store is locked */ + 423: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Group name conflict */ - 409: { + /** @description Secrets store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -7586,82 +11241,65 @@ export interface operations { }; }; }; - delete_group: { + lock_secrets: { parameters: { query?: never; header?: never; - path: { - /** @description Group name */ - group_name: string; - }; + path?: never; cookie?: never; }; requestBody?: never; responses: { - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": unknown; + }; }; - /** @description Group not found */ - 404: { + /** @description Lock failed */ + 400: { headers: { [name: string]: unknown; }; content?: never; }; - }; - }; - list_humans: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { + /** @description Secrets store not initialized */ + 503: { headers: { [name: string]: unknown; }; - content: { - "application/json": unknown; - }; + content?: never; }; }; }; - create_human: { + migrate_secrets: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateHumanRequest"]; - }; - }; + requestBody?: never; responses: { - 201: { + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["MigrateResponse"]; }; }; - /** @description Invalid request */ - 400: { + /** @description Secret store is locked */ + 423: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Human ID already exists */ - 409: { + /** @description Secrets store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -7669,21 +11307,14 @@ export interface operations { }; }; }; - update_human: { + rotate_key: { parameters: { query?: never; header?: never; - path: { - /** @description Human ID */ - human_id: string; - }; + path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateHumanRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { @@ -7693,8 +11324,15 @@ export interface operations { "application/json": unknown; }; }; - /** @description Human not found */ - 404: { + /** @description Key rotation failed */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Secrets store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -7702,26 +11340,25 @@ export interface operations { }; }; }; - delete_human: { + secrets_status: { parameters: { query?: never; header?: never; - path: { - /** @description Human ID */ - human_id: string; - }; + path?: never; cookie?: never; }; requestBody?: never; responses: { - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": unknown; + }; }; - /** @description Human not found */ - 404: { + /** @description Secrets store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -7729,21 +11366,16 @@ export interface operations { }; }; }; - update_link: { + unlock_secrets: { parameters: { query?: never; header?: never; - path: { - /** @description Source agent */ - from: string; - /** @description Target agent */ - to: string; - }; + path?: never; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["UpdateLinkRequest"]; + "application/json": components["schemas"]["UnlockBody"]; }; }; responses: { @@ -7755,44 +11387,22 @@ export interface operations { "application/json": unknown; }; }; - /** @description Invalid request */ + /** @description Invalid master key format */ 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Link not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - delete_link: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Source agent */ - from: string; - /** @description Target agent */ - to: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 204: { + /** @description Invalid master key */ + 401: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Link not found */ - 404: { + /** @description Secrets store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -7800,25 +11410,39 @@ export interface operations { }; }; }; - list_mcp_servers: { + put_secret: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Secret name */ + name: string; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["PutSecretBody"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["McpServerInfo"][]; + "application/json": components["schemas"]["PutSecretResponse"]; }; }; - /** @description Internal server error */ - 500: { + /** @description Secret store is locked */ + 423: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Secrets store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -7826,29 +11450,35 @@ export interface operations { }; }; }; - update_mcp_server: { + delete_secret: { parameters: { query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateMcpServerRequest"]; + path: { + /** @description Secret name */ + name: string; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MutationResponse"]; + "application/json": components["schemas"]["DeleteSecretResponse"]; }; }; - /** @description Internal server error */ - 500: { + /** @description Secret store is locked */ + 423: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Secrets store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -7856,29 +11486,35 @@ export interface operations { }; }; }; - create_mcp_server: { + secret_info: { parameters: { query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateMcpServerRequest"]; + path: { + /** @description Secret name */ + name: string; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MutationResponse"]; + "application/json": components["schemas"]["SecretInfoResponse"]; }; }; - /** @description Internal server error */ - 500: { + /** @description Secret not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Secrets store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -7886,14 +11522,11 @@ export interface operations { }; }; }; - delete_mcp_server: { + get_global_settings: { parameters: { query?: never; header?: never; - path: { - /** @description Server name */ - name: string; - }; + path?: never; cookie?: never; }; requestBody?: never; @@ -7903,7 +11536,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MutationResponse"]; + "application/json": components["schemas"]["GlobalSettingsResponse"]; }; }; /** @description Internal server error */ @@ -7915,24 +11548,25 @@ export interface operations { }; }; }; - reconnect_mcp_server: { + update_global_settings: { parameters: { query?: never; header?: never; - path: { - /** @description Server name */ - name: string; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["GlobalSettingsUpdate"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MutationResponse"]; + "application/json": components["schemas"]["GlobalSettingsUpdateResponse"]; }; }; /** @description Internal server error */ @@ -7944,7 +11578,7 @@ export interface operations { }; }; }; - mcp_status: { + get_raw_config: { parameters: { query?: never; header?: never; @@ -7958,12 +11592,19 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["McpAgentStatus"][]; + "application/json": components["schemas"]["RawConfigResponse"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - disconnect_platform: { + update_raw_config: { parameters: { query?: never; header?: never; @@ -7972,7 +11613,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["DisconnectPlatformRequest"]; + "application/json": components["schemas"]["RawConfigUpdateRequest"]; }; }; responses: { @@ -7981,8 +11622,15 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["RawConfigUpdateResponse"]; + }; + }; + /** @description Validation error */ + 400: { + headers: { + [name: string]: unknown; }; + content?: never; }; /** @description Internal server error */ 500: { @@ -7993,29 +11641,30 @@ export interface operations { }; }; }; - create_messaging_instance: { + registry_browse: { parameters: { - query?: never; + query: { + /** @description View type (all-time, trending, hot) */ + view: string; + /** @description Page number */ + page: number; + }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateMessagingInstanceRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MessagingInstanceActionResponse"]; + "application/json": components["schemas"]["RegistryBrowseResponse"]; }; }; - /** @description Internal server error */ - 500: { + /** @description Bad gateway */ + 502: { headers: { [name: string]: unknown; }; @@ -8023,29 +11672,30 @@ export interface operations { }; }; }; - delete_messaging_instance: { + registry_skill_content: { parameters: { - query?: never; + query: { + /** @description GitHub owner/repo */ + source: string; + /** @description Skill identifier within the repo */ + skill_id: string; + }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["DeleteMessagingInstanceRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MessagingInstanceActionResponse"]; + "application/json": components["schemas"]["RegistrySkillContentResponse"]; }; }; - /** @description Internal server error */ - 500: { + /** @description Invalid request */ + 400: { headers: { [name: string]: unknown; }; @@ -8053,9 +11703,14 @@ export interface operations { }; }; }; - messaging_status: { + registry_search: { parameters: { - query?: never; + query: { + /** @description Search query */ + q: string; + /** @description Result limit */ + limit: number; + }; header?: never; path?: never; cookie?: never; @@ -8067,11 +11722,18 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MessagingStatusResponse"]; + "application/json": components["schemas"]["RegistrySearchResponse"]; }; }; - /** @description Internal server error */ - 500: { + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad gateway */ + 502: { headers: { [name: string]: unknown; }; @@ -8079,7 +11741,7 @@ export interface operations { }; }; }; - toggle_platform: { + set_authorized_key: { parameters: { query?: never; header?: never; @@ -8088,7 +11750,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["TogglePlatformRequest"]; + "application/json": components["schemas"]["AuthorizedKeyRequest"]; }; }; responses: { @@ -8097,8 +11759,15 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["AuthorizedKeyResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; }; + content?: never; }; /** @description Internal server error */ 500: { @@ -8109,14 +11778,9 @@ export interface operations { }; }; }; - get_models: { + ssh_status: { parameters: { - query?: { - /** @description Filter by provider ID */ - provider?: string; - /** @description Filter by capability (input_audio, voice_transcription) */ - capability?: string; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8128,8 +11792,15 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ModelsResponse"]; + "application/json": components["schemas"]["SshStatusResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; }; + content?: never; }; /** @description Internal server error */ 500: { @@ -8140,7 +11811,7 @@ export interface operations { }; }; }; - refresh_models: { + status: { parameters: { query?: never; header?: never; @@ -8154,45 +11825,30 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ModelsResponse"]; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["StatusResponse"]; }; - content?: never; }; }; }; - list_notifications: { + backup_export: { parameters: { - query?: { - /** @description "unread" returns only unread notifications; anything else returns all. */ - filter?: string | null; - /** @description Filter by agent id. */ - agent_id?: string | null; - /** @description Filter by kind: "task_approval", "worker_failed", "cortex_observation". */ - kind?: string | null; - limit?: number; - offset?: number; - }; + query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { + /** @description Backup archive */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["NotificationsResponse"]; + "application/zip": unknown; }; }; - /** @description Notification store not initialized */ + /** @description No runtime config available */ 503: { headers: { [name: string]: unknown; @@ -8201,23 +11857,34 @@ export interface operations { }; }; }; - dismiss_read: { + backup_restore: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/octet-stream": number[]; + }; + }; responses: { - /** @description Read notifications dismissed */ - 204: { + /** @description Backup restored successfully */ + 200: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Notification store not initialized */ + /** @description Empty payload */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No runtime config available */ 503: { headers: { [name: string]: unknown; @@ -8226,7 +11893,7 @@ export interface operations { }; }; }; - mark_all_read: { + storage_status: { parameters: { query?: never; header?: never; @@ -8235,25 +11902,37 @@ export interface operations { }; requestBody?: never; responses: { - /** @description All marked as read */ - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["StorageStatus"]; + }; }; - /** @description Notification store not initialized */ + /** @description No runtime config available */ 503: { headers: { [name: string]: unknown; }; content?: never; }; - }; - }; - unread_count: { - parameters: { - query?: never; + }; + }; + list_tasks: { + parameters: { + query?: { + /** @description Convenience filter: matches tasks where owner OR assigned equals this value. */ + agent_id?: string | null; + /** @description Filter by owner agent. Optional. */ + owner_agent_id?: string | null; + /** @description Filter by assigned agent. Optional. */ + assigned_agent_id?: string | null; + status?: string | null; + priority?: string | null; + created_by?: string | null; + limit?: number; + }; header?: never; path?: never; cookie?: never; @@ -8265,10 +11944,10 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["UnreadCountResponse"]; + "application/json": components["schemas"]["TaskListResponse"]; }; }; - /** @description Notification store not initialized */ + /** @description Task store not initialized */ 503: { headers: { [name: string]: unknown; @@ -8277,33 +11956,35 @@ export interface operations { }; }; }; - dismiss_notification: { + create_task: { parameters: { query?: never; header?: never; - path: { - /** @description Notification id */ - id: string; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["CreateTaskRequest"]; + }; + }; responses: { - /** @description Dismissed */ - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["TaskResponse"]; + }; }; - /** @description Not found or already dismissed */ - 404: { + /** @description Invalid request */ + 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Notification store not initialized */ + /** @description Task store not initialized */ 503: { headers: { [name: string]: unknown; @@ -8312,53 +11993,33 @@ export interface operations { }; }; }; - mark_read: { + list_task_transitions: { parameters: { query?: never; header?: never; - path: { - /** @description Notification id */ - id: string; - }; + path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Marked as read */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Not found or already read */ - 404: { + 200: { headers: { [name: string]: unknown; }; - content?: never; - }; - /** @description Notification store not initialized */ - 503: { - headers: { - [name: string]: unknown; + content: { + "application/json": components["schemas"]["TaskTransitionsResponse"]; }; - content?: never; }; }; }; - list_portal_conversations: { + get_task: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - /** @description Include archived conversations */ - include_archived: boolean; - /** @description Maximum number of conversations to return (default: 100, max: 500) */ - limit: number; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description Task number */ + number: number; + }; cookie?: never; }; requestBody?: never; @@ -8368,18 +12029,18 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PortalConversationsResponse"]; + "application/json": components["schemas"]["TaskResponse"]; }; }; - /** @description Agent not found */ + /** @description Task not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Task store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -8387,16 +12048,19 @@ export interface operations { }; }; }; - create_portal_conversation: { + update_task: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Task number */ + number: number; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["CreatePortalConversationRequest"]; + "application/json": components["schemas"]["UpdateTaskRequest"]; }; }; responses: { @@ -8405,18 +12069,25 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PortalConversationResponse"]; + "application/json": components["schemas"]["TaskResponse"]; }; }; - /** @description Agent not found */ + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Task not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Task store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -8424,39 +12095,35 @@ export interface operations { }; }; }; - update_portal_conversation: { + delete_task: { parameters: { query?: never; header?: never; path: { - /** @description Conversation session ID */ - session_id: string; + /** @description Task number */ + number: number; }; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdatePortalConversationRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PortalConversationResponse"]; + "application/json": components["schemas"]["TaskActionResponse"]; }; }; - /** @description Conversation not found */ + /** @description Task not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Task store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -8464,38 +12131,39 @@ export interface operations { }; }; }; - delete_portal_conversation: { + approve_task: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - }; + query?: never; header?: never; path: { - /** @description Conversation session ID */ - session_id: string; + /** @description Task number */ + number: number; }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["ApproveRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PortalSendResponse"]; + "application/json": components["schemas"]["TaskResponse"]; }; }; - /** @description Conversation not found */ + /** @description Task not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Task store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -8503,39 +12171,39 @@ export interface operations { }; }; }; - portal_history: { + assign_task: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - /** @description Session ID */ - session_id: string; - /** @description Maximum number of messages to return (default: 100, max: 200) */ - limit: number; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description Task number */ + number: number; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["AssignRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PortalHistoryMessage"][]; + "application/json": components["schemas"]["TaskResponse"]; }; }; - /** @description Agent not found */ + /** @description Task not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Task store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -8543,16 +12211,21 @@ export interface operations { }; }; }; - portal_send: { + set_task_binding: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Task number */ + number: number; + /** @description Input key */ + key: string; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["PortalSendRequest"]; + "application/json": components["schemas"]["SetBindingRequest"]; }; }; responses: { @@ -8561,24 +12234,17 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PortalSendResponse"]; - }; - }; - /** @description Invalid request */ - 400: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["TaskContractResponse"]; }; - content?: never; }; - /** @description Agent not found */ - 404: { + /** @description A binding must name either a source task or a literal */ + 422: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Messaging manager not available */ + /** @description Task store not initialized */ 503: { headers: { [name: string]: unknown; @@ -8587,11 +12253,16 @@ export interface operations { }; }; }; - get_providers: { + remove_task_binding: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Task number */ + number: number; + /** @description Input key */ + key: string; + }; cookie?: never; }; requestBody?: never; @@ -8601,11 +12272,18 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ProvidersResponse"]; + "application/json": components["schemas"]["TaskContractResponse"]; }; }; - /** @description Internal server error */ - 500: { + /** @description Binding not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Task store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -8613,16 +12291,19 @@ export interface operations { }; }; }; - update_provider: { + block_task: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Task number */ + number: number; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["ProviderUpdateRequest"]; + "application/json": components["schemas"]["BlockTaskRequest"]; }; }; responses: { @@ -8631,41 +12312,25 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ProviderUpdateResponse"]; + "application/json": components["schemas"]["TaskResponse"]; }; }; - /** @description Invalid request */ - 400: { + /** @description Task not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - }; - }; - start_openai_browser_oauth: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["OpenAiOAuthBrowserStartRequest"]; - }; - }; - responses: { - 200: { + /** @description Unknown block kind */ + 422: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["OpenAiOAuthBrowserStartResponse"]; - }; + content?: never; }; - /** @description Invalid request */ - 400: { + /** @description Task store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -8673,14 +12338,14 @@ export interface operations { }; }; }; - openai_browser_oauth_status: { + get_task_contract: { parameters: { - query: { - /** @description OAuth state parameter */ - state: string; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description Task number */ + number: number; + }; cookie?: never; }; requestBody?: never; @@ -8690,11 +12355,18 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["OpenAiOAuthBrowserStatusResponse"]; + "application/json": components["schemas"]["TaskContractResponse"]; }; }; - /** @description Invalid request */ - 400: { + /** @description Task not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Task store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -8702,16 +12374,19 @@ export interface operations { }; }; }; - test_provider_model: { + set_task_contract: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Task number */ + number: number; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["ProviderModelTestRequest"]; + "application/json": components["schemas"]["SetContractRequest"]; }; }; responses: { @@ -8720,11 +12395,18 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ProviderModelTestResponse"]; + "application/json": components["schemas"]["TaskContractResponse"]; }; }; - /** @description Invalid request */ - 400: { + /** @description Task not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Task store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -8732,35 +12414,53 @@ export interface operations { }; }; }; - delete_provider: { + answer_decision: { parameters: { query?: never; header?: never; path: { - /** @description Provider name to delete */ - provider: string; + /** @description Task number */ + number: number; }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["AnswerDecisionRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ProviderUpdateResponse"]; + "application/json": components["schemas"]["TaskResponse"]; }; }; - /** @description Invalid request */ - 400: { + /** @description Task not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Provider not found */ - 404: { + /** @description Already answered, defaulted, or not yet asked */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not a decision, or the answer does not match its schema */ + 422: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Task store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -8768,13 +12468,13 @@ export interface operations { }; }; }; - get_provider_config: { + list_task_dependencies: { parameters: { query?: never; header?: never; path: { - /** @description Provider ID */ - provider: string; + /** @description Task number */ + number: number; }; cookie?: never; }; @@ -8785,11 +12485,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ProviderConfigResponse"]; + "application/json": components["schemas"]["TaskDependenciesResponse"]; }; }; - /** @description Provider not found */ - 404: { + /** @description Task store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -8797,24 +12497,52 @@ export interface operations { }; }; }; - list_secrets: { + add_task_dependency: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Child task number */ + number: number; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["AddDependencyRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SecretListResponse"]; + "application/json": components["schemas"]["TaskDependenciesResponse"]; }; }; - /** @description Secrets store not initialized */ + /** @description Task not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Edge would create a cycle */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description A task cannot depend on itself */ + 422: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Task store not initialized */ 503: { headers: { [name: string]: unknown; @@ -8823,11 +12551,16 @@ export interface operations { }; }; }; - enable_encryption: { + remove_task_dependency: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Child task number */ + number: number; + /** @description Parent task number */ + parent: number; + }; cookie?: never; }; requestBody?: never; @@ -8837,17 +12570,17 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["EncryptResponse"]; + "application/json": components["schemas"]["TaskDependenciesResponse"]; }; }; - /** @description Encryption already enabled */ - 400: { + /** @description Edge not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Secrets store not initialized */ + /** @description Task store not initialized */ 503: { headers: { [name: string]: unknown; @@ -8856,31 +12589,45 @@ export interface operations { }; }; }; - export_secrets: { + execute_task: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Task number */ + number: number; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["ApproveRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["TaskResponse"]; }; }; - /** @description Secret store is locked */ - 423: { + /** @description Task not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Secrets store not initialized */ + /** @description Task pending approval */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Task store not initialized */ 503: { headers: { [name: string]: unknown; @@ -8889,35 +12636,27 @@ export interface operations { }; }; }; - import_secrets: { + list_task_gates: { parameters: { query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ImportBody"]; + path: { + /** @description Task number */ + number: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": unknown; - }; - }; - /** @description Secret store is locked */ - 423: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["TaskGatesResponse"]; }; - content?: never; }; - /** @description Secrets store not initialized */ + /** @description Task store not initialized */ 503: { headers: { [name: string]: unknown; @@ -8926,32 +12665,39 @@ export interface operations { }; }; }; - lock_secrets: { + create_task_gate: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Task number */ + number: number; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["CreateGateRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["TaskGatesResponse"]; }; }; - /** @description Lock failed */ - 400: { + /** @description Task not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Secrets store not initialized */ - 503: { + /** @description Gate config is not usable */ + 422: { headers: { [name: string]: unknown; }; @@ -8959,11 +12705,16 @@ export interface operations { }; }; }; - migrate_secrets: { + delete_task_gate: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Task number */ + number: number; + /** @description Gate id */ + gate_id: string; + }; cookie?: never; }; requestBody?: never; @@ -8973,18 +12724,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MigrateResponse"]; - }; - }; - /** @description Secret store is locked */ - 423: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["TaskGatesResponse"]; }; - content?: never; }; - /** @description Secrets store not initialized */ - 503: { + /** @description No such gate */ + 404: { headers: { [name: string]: unknown; }; @@ -8992,11 +12736,14 @@ export interface operations { }; }; }; - rotate_key: { + get_task_graph: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Task number to centre on */ + number: number; + }; cookie?: never; }; requestBody?: never; @@ -9006,17 +12753,17 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["TaskGraph"]; }; }; - /** @description Key rotation failed */ - 400: { + /** @description Task not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Secrets store not initialized */ + /** @description Task store not initialized */ 503: { headers: { [name: string]: unknown; @@ -9025,11 +12772,14 @@ export interface operations { }; }; }; - secrets_status: { + get_task_provenance: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Task number */ + number: number; + }; cookie?: never; }; requestBody?: never; @@ -9039,10 +12789,17 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": unknown; + "application/json": components["schemas"]["TaskProvenanceResponse"]; }; }; - /** @description Secrets store not initialized */ + /** @description Task not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Task store not initialized */ 503: { headers: { [name: string]: unknown; @@ -9051,42 +12808,34 @@ export interface operations { }; }; }; - unlock_secrets: { + retry_task: { parameters: { query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UnlockBody"]; + path: { + /** @description Task number */ + number: number; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": unknown; - }; - }; - /** @description Invalid master key format */ - 400: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["TaskResponse"]; }; - content?: never; }; - /** @description Invalid master key */ - 401: { + /** @description Task not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Secrets store not initialized */ + /** @description Task store not initialized */ 503: { headers: { [name: string]: unknown; @@ -9095,38 +12844,27 @@ export interface operations { }; }; }; - put_secret: { + list_task_runs: { parameters: { query?: never; header?: never; path: { - /** @description Secret name */ - name: string; + /** @description Task number */ + number: number; }; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["PutSecretBody"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["PutSecretResponse"]; - }; - }; - /** @description Secret store is locked */ - 423: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["TaskRunsResponse"]; }; - content?: never; }; - /** @description Secrets store not initialized */ + /** @description Task store not initialized */ 503: { headers: { [name: string]: unknown; @@ -9135,13 +12873,13 @@ export interface operations { }; }; }; - delete_secret: { + unblock_task: { parameters: { query?: never; header?: never; path: { - /** @description Secret name */ - name: string; + /** @description Task number */ + number: number; }; cookie?: never; }; @@ -9152,17 +12890,24 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["DeleteSecretResponse"]; + "application/json": components["schemas"]["TaskResponse"]; }; }; - /** @description Secret store is locked */ - 423: { + /** @description Task not found or not blocked */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Secrets store not initialized */ + /** @description This is a decision — answer it instead */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Task store not initialized */ 503: { headers: { [name: string]: unknown; @@ -9171,14 +12916,11 @@ export interface operations { }; }; }; - secret_info: { + list_tools: { parameters: { query?: never; header?: never; - path: { - /** @description Secret name */ - name: string; - }; + path?: never; cookie?: never; }; requestBody?: never; @@ -9188,26 +12930,38 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SecretInfoResponse"]; + "application/json": components["schemas"]["ToolsResponse"]; }; }; - /** @description Secret not found */ - 404: { + /** @description Internal server error */ + 500: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Secrets store not initialized */ - 503: { + }; + }; + topology: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["TopologyResponse"]; + }; }; }; }; - get_global_settings: { + update_apply: { parameters: { query?: never; header?: never; @@ -9221,7 +12975,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GlobalSettingsResponse"]; + "application/json": unknown; }; }; /** @description Internal server error */ @@ -9233,25 +12987,68 @@ export interface operations { }; }; }; - update_global_settings: { + update_check: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["GlobalSettingsUpdate"]; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UpdateStatus"]; + }; + }; + }; + }; + update_check_now: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UpdateStatus"]; + }; + }; + }; + }; + get_usage: { + parameters: { + query?: { + /** @description Filter to one agent. */ + agent_id?: string | null; + /** @description ISO 8601 lower bound (default: 30 days ago). */ + since?: string | null; + /** @description ISO 8601 upper bound. */ + until?: string | null; + /** @description Group by: day, agent, model (comma-separated for multiple). */ + group_by?: string | null; }; + header?: never; + path?: never; + cookie?: never; }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GlobalSettingsUpdateResponse"]; + "application/json": components["schemas"]["UsageResponse"]; }; }; /** @description Internal server error */ @@ -9263,11 +13060,17 @@ export interface operations { }; }; }; - get_raw_config: { + get_conversation_usage: { parameters: { - query?: never; + query?: { + /** @description Filter to one agent. */ + agent_id?: string | null; + }; header?: never; - path?: never; + path: { + /** @description Conversation ID */ + conversation_id: string; + }; cookie?: never; }; requestBody?: never; @@ -9277,7 +13080,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["RawConfigResponse"]; + "application/json": components["schemas"]["UsageTotals"]; }; }; /** @description Internal server error */ @@ -9289,16 +13092,19 @@ export interface operations { }; }; }; - update_raw_config: { + workflow_webhook_delivery: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Workflow id */ + id: string; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["RawConfigUpdateRequest"]; + "application/json": unknown; }; }; responses: { @@ -9307,18 +13113,18 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["RawConfigUpdateResponse"]; + "application/json": components["schemas"]["DeliveryResponse"]; }; }; - /** @description Validation error */ - 400: { + /** @description No usable webhook, or no valid shared secret */ + 401: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Authenticated, and the payload or the template was unusable */ + 422: { headers: { [name: string]: unknown; }; @@ -9326,13 +13132,10 @@ export interface operations { }; }; }; - registry_browse: { + list_pages: { parameters: { - query: { - /** @description View type (all-time, trending, hot) */ - view: string; - /** @description Page number */ - page: number; + query?: { + page_type?: string | null; }; header?: never; path?: never; @@ -9345,11 +13148,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["RegistryBrowseResponse"]; + "application/json": components["schemas"]["WikiListResponse"]; }; }; - /** @description Bad gateway */ - 502: { + /** @description Wiki store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -9357,44 +13160,48 @@ export interface operations { }; }; }; - registry_skill_content: { + create_page: { parameters: { - query: { - /** @description GitHub owner/repo */ - source: string; - /** @description Skill identifier within the repo */ - skill_id: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["CreatePageRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["RegistrySkillContentResponse"]; + "application/json": components["schemas"]["WikiPageResponse"]; }; }; - /** @description Invalid request */ + /** @description Invalid page_type */ 400: { headers: { [name: string]: unknown; }; content?: never; }; + /** @description Wiki store not initialized */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - registry_search: { + search_pages: { parameters: { query: { - /** @description Search query */ - q: string; - /** @description Result limit */ - limit: number; + query: string; + page_type?: string | null; }; header?: never; path?: never; @@ -9407,18 +13214,49 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["RegistrySearchResponse"]; + "application/json": components["schemas"]["WikiListResponse"]; }; }; - /** @description Invalid request */ - 400: { + /** @description Wiki store not initialized */ + 503: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Bad gateway */ - 502: { + }; + }; + get_page: { + parameters: { + query?: { + version?: number | null; + }; + header?: never; + path: { + /** @description Page slug */ + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WikiPageResponse"]; + }; + }; + /** @description Page not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wiki store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -9426,36 +13264,28 @@ export interface operations { }; }; }; - set_authorized_key: { + archive_page: { parameters: { query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AuthorizedKeyRequest"]; + path: { + /** @description Page slug */ + slug: string; }; + cookie?: never; }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AuthorizedKeyResponse"]; - }; - }; - /** @description Invalid request */ - 400: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["WikiActionResponse"]; }; - content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Wiki store not initialized */ + 503: { headers: { [name: string]: unknown; }; @@ -9463,77 +13293,76 @@ export interface operations { }; }; }; - ssh_status: { + edit_page: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Page slug */ + slug: string; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["EditPageRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SshStatusResponse"]; + "application/json": components["schemas"]["WikiPageResponse"]; }; }; - /** @description Invalid request */ + /** @description Edit match failed */ 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Page not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - }; - }; - status: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { + /** @description Wiki store not initialized */ + 503: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["StatusResponse"]; - }; + content?: never; }; }; }; - backup_export: { + get_history: { parameters: { - query?: never; + query?: { + limit?: number; + }; header?: never; - path?: never; + path: { + /** @description Page slug */ + slug: string; + }; cookie?: never; }; requestBody?: never; responses: { - /** @description Backup archive */ 200: { headers: { [name: string]: unknown; }; content: { - "application/zip": unknown; + "application/json": components["schemas"]["WikiHistoryResponse"]; }; }; - /** @description No runtime config available */ + /** @description Wiki store not initialized */ 503: { headers: { [name: string]: unknown; @@ -9542,34 +13371,38 @@ export interface operations { }; }; }; - backup_restore: { + restore_version: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Page slug */ + slug: string; + }; cookie?: never; }; requestBody: { content: { - "application/octet-stream": number[]; + "application/json": components["schemas"]["RestoreVersionRequest"]; }; }; responses: { - /** @description Backup restored successfully */ 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["WikiPageResponse"]; + }; }; - /** @description Empty payload */ - 400: { + /** @description Page or version not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description No runtime config available */ + /** @description Wiki store not initialized */ 503: { headers: { [name: string]: unknown; @@ -9578,11 +13411,14 @@ export interface operations { }; }; }; - storage_status: { + get_run: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Workflow run id */ + run_id: string; + }; cookie?: never; }; requestBody?: never; @@ -9592,11 +13428,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["StorageStatus"]; + "application/json": components["schemas"]["RunDetailResponse"]; }; }; - /** @description No runtime config available */ - 503: { + /** @description No such run */ + 404: { headers: { [name: string]: unknown; }; @@ -9604,22 +13440,14 @@ export interface operations { }; }; }; - list_tasks: { + delete_run: { parameters: { - query?: { - /** @description Convenience filter: matches tasks where owner OR assigned equals this value. */ - agent_id?: string | null; - /** @description Filter by owner agent. Optional. */ - owner_agent_id?: string | null; - /** @description Filter by assigned agent. Optional. */ - assigned_agent_id?: string | null; - status?: string | null; - priority?: string | null; - created_by?: string | null; - limit?: number; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description Workflow run id */ + run_id: string; + }; cookie?: never; }; requestBody?: never; @@ -9629,11 +13457,18 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskListResponse"]; + "application/json": components["schemas"]["WorkflowActionResponse"]; }; }; - /** @description Task store not initialized */ - 503: { + /** @description No such run */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description The run is still going, or a worker is still in it */ + 409: { headers: { [name: string]: unknown; }; @@ -9641,16 +13476,19 @@ export interface operations { }; }; }; - create_task: { + cancel_run: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Workflow run id */ + run_id: string; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["CreateTaskRequest"]; + "application/json": components["schemas"]["CancelRunRequest"]; }; }; responses: { @@ -9659,18 +13497,18 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskResponse"]; + "application/json": components["schemas"]["CancelRunResponse"]; }; }; - /** @description Invalid request */ - 400: { + /** @description No such run */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Task store not initialized */ - 503: { + /** @description The run has already finished */ + 409: { headers: { [name: string]: unknown; }; @@ -9678,34 +13516,52 @@ export interface operations { }; }; }; - get_task: { + delete_schedule: { parameters: { query?: never; header?: never; path: { - /** @description Task number */ - number: number; + /** @description Workflow schedule id */ + schedule_id: string; }; cookie?: never; }; requestBody?: never; responses: { - 200: { + /** @description Deleted */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["TaskResponse"]; - }; + content?: never; }; - /** @description Task not found */ + /** @description No such schedule */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Task store not initialized */ + }; + }; + list_workflows: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WorkflowListResponse"]; + }; + }; + /** @description Workflow store not initialized */ 503: { headers: { [name: string]: unknown; @@ -9714,19 +13570,16 @@ export interface operations { }; }; }; - update_task: { + create_workflow: { parameters: { query?: never; header?: never; - path: { - /** @description Task number */ - number: number; - }; + path?: never; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["UpdateTaskRequest"]; + "application/json": components["schemas"]["SaveWorkflowRequest"]; }; }; responses: { @@ -9735,25 +13588,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskResponse"]; - }; - }; - /** @description Invalid request */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Task not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["WorkflowResponse"]; }; - content?: never; }; - /** @description Task store not initialized */ - 503: { + /** @description A workflow with that name already exists */ + 409: { headers: { [name: string]: unknown; }; @@ -9761,13 +13600,13 @@ export interface operations { }; }; }; - delete_task: { + get_workflow: { parameters: { query?: never; header?: never; path: { - /** @description Task number */ - number: number; + /** @description Workflow id */ + id: string; }; cookie?: never; }; @@ -9778,38 +13617,31 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskActionResponse"]; + "application/json": components["schemas"]["WorkflowDetailResponse"]; }; }; - /** @description Task not found */ + /** @description No such workflow */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Task store not initialized */ - 503: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; }; }; - approve_task: { + update_workflow: { parameters: { query?: never; header?: never; path: { - /** @description Task number */ - number: number; + /** @description Workflow id */ + id: string; }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["ApproveRequest"]; + "application/json": components["schemas"]["SaveWorkflowRequest"]; }; }; responses: { @@ -9818,78 +13650,60 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskResponse"]; + "application/json": components["schemas"]["WorkflowResponse"]; }; }; - /** @description Task not found */ + /** @description No such workflow */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Task store not initialized */ - 503: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; }; }; - assign_task: { + delete_workflow: { parameters: { query?: never; header?: never; path: { - /** @description Task number */ - number: number; + /** @description Workflow id */ + id: string; }; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["AssignRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskResponse"]; + "application/json": components["schemas"]["WorkflowActionResponse"]; }; }; - /** @description Task not found */ + /** @description No such workflow */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Task store not initialized */ - 503: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; }; }; - execute_task: { + add_edge: { parameters: { query?: never; header?: never; path: { - /** @description Task number */ - number: number; + /** @description Workflow id */ + id: string; }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["ApproveRequest"]; + "application/json": components["schemas"]["StepEdgeRequest"]; }; }; responses: { @@ -9898,25 +13712,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskResponse"]; - }; - }; - /** @description Task not found */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["WorkflowDetailResponse"]; }; - content?: never; - }; - /** @description Task pending approval */ - 409: { - headers: { - [name: string]: unknown; - }; - content?: never; }; - /** @description Task store not initialized */ - 503: { + /** @description Self-loop, or a step that does not exist */ + 422: { headers: { [name: string]: unknown; }; @@ -9924,25 +13724,32 @@ export interface operations { }; }; }; - list_tools: { + remove_edge: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Workflow id */ + id: string; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["StepEdgeRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ToolsResponse"]; + "application/json": components["schemas"]["WorkflowDetailResponse"]; }; }; - /** @description Internal server error */ - 500: { + /** @description No such edge */ + 404: { headers: { [name: string]: unknown; }; @@ -9950,44 +13757,46 @@ export interface operations { }; }; }; - topology: { + launch_workflow: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Workflow id */ + id: string; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["LaunchRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TopologyResponse"]; + "application/json": components["schemas"]["LaunchResponse"]; }; }; - }; - }; - update_apply: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { + /** @description No such workflow */ + 404: { headers: { [name: string]: unknown; }; - content: { - "application/json": unknown; + content?: never; + }; + /** @description The steps form a cycle */ + 409: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Internal server error */ - 500: { + /** @description Bad input, or a reference that does not resolve */ + 422: { headers: { [name: string]: unknown; }; @@ -9995,11 +13804,14 @@ export interface operations { }; }; }; - update_check: { + list_runs: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Workflow id */ + id: string; + }; cookie?: never; }; requestBody?: never; @@ -10009,16 +13821,19 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["UpdateStatus"]; + "application/json": components["schemas"]["RunListResponse"]; }; }; }; }; - update_check_now: { + list_schedules: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Workflow id */ + id: string; + }; cookie?: never; }; requestBody?: never; @@ -10028,39 +13843,44 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["UpdateStatus"]; + "application/json": components["schemas"]["ScheduleListResponse"]; }; }; }; }; - get_usage: { + put_schedule: { parameters: { - query?: { - /** @description Filter to one agent. */ - agent_id?: string | null; - /** @description ISO 8601 lower bound (default: 30 days ago). */ - since?: string | null; - /** @description ISO 8601 upper bound. */ - until?: string | null; - /** @description Group by: day, agent, model (comma-separated for multiple). */ - group_by?: string | null; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description Workflow id */ + id: string; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["SaveScheduleRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["UsageResponse"]; + "application/json": components["schemas"]["ScheduleResponse"]; }; }; - /** @description Internal server error */ - 500: { + /** @description No such workflow */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unusable schedule */ + 422: { headers: { [name: string]: unknown; }; @@ -10068,31 +13888,41 @@ export interface operations { }; }; }; - get_conversation_usage: { + put_step: { parameters: { - query?: { - /** @description Filter to one agent. */ - agent_id?: string | null; - }; + query?: never; header?: never; path: { - /** @description Conversation ID */ - conversation_id: string; + /** @description Workflow id */ + id: string; + /** @description Stable step name */ + step_key: string; }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["SaveStepRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["UsageTotals"]; + "application/json": components["schemas"]["WorkflowDetailResponse"]; }; }; - /** @description Internal server error */ - 500: { + /** @description No such workflow */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unknown priority */ + 422: { headers: { [name: string]: unknown; }; @@ -10100,13 +13930,16 @@ export interface operations { }; }; }; - list_pages: { + delete_step: { parameters: { - query?: { - page_type?: string | null; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description Workflow id */ + id: string; + /** @description Stable step name */ + step_key: string; + }; cookie?: never; }; requestBody?: never; @@ -10116,11 +13949,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WikiListResponse"]; + "application/json": components["schemas"]["WorkflowDetailResponse"]; }; }; - /** @description Wiki store not initialized */ - 503: { + /** @description No such step */ + 404: { headers: { [name: string]: unknown; }; @@ -10128,16 +13961,23 @@ export interface operations { }; }; }; - create_page: { + put_binding: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description Workflow id */ + id: string; + /** @description Step being bound */ + step_key: string; + /** @description Name of the input */ + input_key: string; + }; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["CreatePageRequest"]; + "application/json": components["schemas"]["SaveBindingRequest"]; }; }; responses: { @@ -10146,18 +13986,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WikiPageResponse"]; - }; - }; - /** @description Invalid page_type */ - 400: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["WorkflowDetailResponse"]; }; - content?: never; }; - /** @description Wiki store not initialized */ - 503: { + /** @description Unknown source, or a source that does not match its kind */ + 422: { headers: { [name: string]: unknown; }; @@ -10165,14 +13998,18 @@ export interface operations { }; }; }; - search_pages: { + delete_binding: { parameters: { - query: { - query: string; - page_type?: string | null; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description Workflow id */ + id: string; + /** @description Step being bound */ + step_key: string; + /** @description Name of the input */ + input_key: string; + }; cookie?: never; }; requestBody?: never; @@ -10182,11 +14019,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WikiListResponse"]; + "application/json": components["schemas"]["WorkflowDetailResponse"]; }; }; - /** @description Wiki store not initialized */ - 503: { + /** @description No such binding */ + 404: { headers: { [name: string]: unknown; }; @@ -10194,37 +14031,43 @@ export interface operations { }; }; }; - get_page: { + put_step_gate: { parameters: { - query?: { - version?: number | null; - }; + query?: never; header?: never; path: { - /** @description Page slug */ - slug: string; + /** @description Workflow id */ + id: string; + /** @description Step being gated */ + step_key: string; + /** @description Author-chosen name for this condition */ + gate_key: string; }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["SaveStepGateRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WikiPageResponse"]; + "application/json": components["schemas"]["WorkflowDetailResponse"]; }; }; - /** @description Page not found */ + /** @description No such workflow */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Wiki store not initialized */ - 503: { + /** @description Unknown kind or disposition, a step that does not exist, or an unusable config */ + 422: { headers: { [name: string]: unknown; }; @@ -10232,13 +14075,17 @@ export interface operations { }; }; }; - archive_page: { + delete_step_gate: { parameters: { query?: never; header?: never; path: { - /** @description Page slug */ - slug: string; + /** @description Workflow id */ + id: string; + /** @description Step being gated */ + step_key: string; + /** @description Name of the condition */ + gate_key: string; }; cookie?: never; }; @@ -10249,11 +14096,11 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WikiActionResponse"]; + "application/json": components["schemas"]["WorkflowDetailResponse"]; }; }; - /** @description Wiki store not initialized */ - 503: { + /** @description No such condition */ + 404: { headers: { [name: string]: unknown; }; @@ -10261,77 +14108,68 @@ export interface operations { }; }; }; - edit_page: { + get_webhook: { parameters: { query?: never; header?: never; path: { - /** @description Page slug */ - slug: string; + /** @description Workflow id */ + id: string; }; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["EditPageRequest"]; - }; - }; + requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WikiPageResponse"]; - }; - }; - /** @description Edit match failed */ - 400: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["WebhookResponse"]; }; - content?: never; }; - /** @description Page not found */ + /** @description No webhook is configured */ 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Wiki store not initialized */ - 503: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; }; }; - get_history: { + put_webhook: { parameters: { - query?: { - limit?: number; - }; + query?: never; header?: never; path: { - /** @description Page slug */ - slug: string; + /** @description Workflow id */ + id: string; }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["SaveWebhookRequest"]; + }; + }; responses: { 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WikiHistoryResponse"]; + "application/json": components["schemas"]["WebhookResponse"]; }; }; - /** @description Wiki store not initialized */ - 503: { + /** @description No such workflow */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unusable webhook configuration */ + 422: { headers: { [name: string]: unknown; }; @@ -10339,39 +14177,27 @@ export interface operations { }; }; }; - restore_version: { + delete_webhook: { parameters: { query?: never; header?: never; path: { - /** @description Page slug */ - slug: string; + /** @description Workflow id */ + id: string; }; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["RestoreVersionRequest"]; - }; - }; + requestBody?: never; responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["WikiPageResponse"]; - }; - }; - /** @description Page or version not found */ - 404: { + /** @description Deleted */ + 204: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Wiki store not initialized */ - 503: { + /** @description No webhook is configured */ + 404: { headers: { [name: string]: unknown; }; diff --git a/interface/src/api/types.ts b/interface/src/api/types.ts index e41fe27bf..37712cf5c 100644 --- a/interface/src/api/types.ts +++ b/interface/src/api/types.ts @@ -231,6 +231,12 @@ export type MemoryPersistenceSection = export type BrowserSection = components["schemas"]["BrowserSection"]; export type ChannelSection = components["schemas"]["ChannelSection"]; export type SandboxSection = components["schemas"]["SandboxSection"]; +// What the host is *actually* doing, as opposed to what `sandbox.mode` asked +// for. The two read alike from the config surface and are not the same +// question — reporting only the first is how an instance ends up running +// unconfined while its config says `enabled`. +export type SandboxContainmentStatus = + components["schemas"]["SandboxContainmentStatus"]; export type ProjectsSection = components["schemas"]["ProjectsSection"]; export type DiscordSection = components["schemas"]["DiscordSection"]; @@ -297,7 +303,7 @@ export type TriggerCronRequest = components["schemas"]["TriggerCronRequest"]; // Provider/Model Types // ============================================================================= -export type ProviderStatus = components["schemas"]["ProviderStatus"]; +export type ProviderEntry = components["schemas"]["ProviderEntry"]; export type ProvidersResponse = components["schemas"]["ProvidersResponse"]; export type ProviderUpdateRequest = components["schemas"]["ProviderUpdateRequest"]; @@ -310,14 +316,6 @@ export type ProviderModelTestRequest = export type ProviderModelTestResponse = components["schemas"]["ProviderModelTestResponse"]; -// OAuth -export type OpenAiOAuthBrowserStartRequest = - components["schemas"]["OpenAiOAuthBrowserStartRequest"]; -export type OpenAiOAuthBrowserStartResponse = - components["schemas"]["OpenAiOAuthBrowserStartResponse"]; -export type OpenAiOAuthBrowserStatusResponse = - components["schemas"]["OpenAiOAuthBrowserStatusResponse"]; - // Models export type ModelInfo = components["schemas"]["ModelInfo"]; export type ModelsResponse = components["schemas"]["ModelsResponse"]; @@ -370,6 +368,33 @@ export type TaskSubtask = components["schemas"]["TaskSubtask"]; export type TaskListResponse = components["schemas"]["TaskListResponse"]; export type TaskResponse = components["schemas"]["TaskResponse"]; export type TaskActionResponse = components["schemas"]["TaskActionResponse"]; +export type TaskRun = components["schemas"]["TaskRun"]; +export type TaskRunOutcome = components["schemas"]["TaskRunOutcome"]; +export type TaskRunsResponse = components["schemas"]["TaskRunsResponse"]; +export type BlockKind = components["schemas"]["BlockKind"]; +export type TaskEdgeSummary = components["schemas"]["TaskEdgeSummary"]; +export type TaskDependenciesResponse = + components["schemas"]["TaskDependenciesResponse"]; +export type TaskTransition = components["schemas"]["TaskTransition"]; +export type ContractProblem = components["schemas"]["ContractProblem"]; +export type ContractSide = components["schemas"]["ContractSide"]; +export type TaskInputBinding = components["schemas"]["TaskInputBinding"]; +export type TaskContractResponse = + components["schemas"]["TaskContractResponse"]; +export type TaskProvenanceResponse = + components["schemas"]["TaskProvenanceResponse"]; +export type TaskTransitionsResponse = + components["schemas"]["TaskTransitionsResponse"]; +export type TaskGraph = components["schemas"]["TaskGraph"]; +export type TaskGraphEdge = components["schemas"]["TaskGraphEdge"]; + +// A condition on a live task: the predicate, its last verdict, and — via +// `disposition` — whether a false answer holds the task or rules it out. +export type TaskGate = components["schemas"]["TaskGate"]; +export type TaskGatesResponse = components["schemas"]["TaskGatesResponse"]; +export type GateKind = components["schemas"]["GateKind"]; +export type GateResult = components["schemas"]["GateResult"]; +export type GateDisposition = components["schemas"]["GateDisposition"]; // Requests export type CreateTaskRequest = components["schemas"]["CreateTaskRequest"]; @@ -377,6 +402,50 @@ export type UpdateTaskRequest = components["schemas"]["UpdateTaskRequest"]; export type ApproveRequest = components["schemas"]["ApproveRequest"]; export type AssignRequest = components["schemas"]["AssignRequest"]; +// ============================================================================= +// Workflow Types +// ============================================================================= + +export type Workflow = components["schemas"]["Workflow"]; +export type WorkflowStep = components["schemas"]["WorkflowStep"]; +export type WorkflowEdge = components["schemas"]["WorkflowEdge"]; +export type StepBinding = components["schemas"]["StepBinding"]; +// The template half of a condition. Addressed by `step_key` like a binding, +// compiled into a real `TaskGate` at launch. +export type StepGate = components["schemas"]["StepGate"]; +export type BindingSource = components["schemas"]["BindingSource"]; +// Whether a step runs a model or a process. `agent` is the default and is every +// step that predates command steps. +export type StepKind = components["schemas"]["StepKind"]; +// Where a step gets its working directory from: the task binding it already +// has, a checkout of its own, or one per fan-out branch. +export type WorktreeMode = components["schemas"]["WorktreeMode"]; +export type LoopArm = components["schemas"]["LoopArm"]; +export type LoopResolution = components["schemas"]["LoopResolution"]; +export type WorkflowListResponse = components["schemas"]["WorkflowListResponse"]; +export type WorkflowResponse = components["schemas"]["WorkflowResponse"]; +export type WorkflowDetailResponse = + components["schemas"]["WorkflowDetailResponse"]; +export type WorkflowActionResponse = + components["schemas"]["WorkflowActionResponse"]; +export type WorkflowRun = components["schemas"]["WorkflowRun"]; +// How a run is going, as a property of the run rather than a reduction over its +// tasks. `stuck` is the value the enum exists for: no single task can report it. +export type RunStatus = components["schemas"]["RunStatus"]; +export type RunDetailResponse = components["schemas"]["RunDetailResponse"]; +export type RunListResponse = components["schemas"]["RunListResponse"]; +export type CancelRunResponse = components["schemas"]["CancelRunResponse"]; + +// Requests +export type CancelRunRequest = components["schemas"]["CancelRunRequest"]; +export type SaveWorkflowRequest = components["schemas"]["SaveWorkflowRequest"]; +export type SaveStepRequest = components["schemas"]["SaveStepRequest"]; +export type SaveBindingRequest = components["schemas"]["SaveBindingRequest"]; +export type SaveStepGateRequest = components["schemas"]["SaveStepGateRequest"]; +export type StepEdgeRequest = components["schemas"]["StepEdgeRequest"]; +export type LaunchRequest = components["schemas"]["LaunchRequest"]; +export type LaunchResponse = components["schemas"]["LaunchResponse"]; + // ============================================================================= // Messaging Types // ============================================================================= @@ -457,6 +526,12 @@ export type ProjectWithRelations = export type ProjectListResponse = components["schemas"]["ProjectListResponse"]; export type ProjectResponse = components["schemas"]["ProjectResponse"]; +// A checkout under `.worktrees/` that no live run accounts for. Listed for a +// person to look at — there is deliberately no endpoint that removes one. +export type OrphanWorktree = components["schemas"]["OrphanWorktree"]; +export type OrphanWorktreesResponse = + components["schemas"]["OrphanWorktreesResponse"]; + // Disk usage export type DiskUsageEntry = components["schemas"]["DiskUsageEntry"]; export type DiskUsageResponse = components["schemas"]["DiskUsageResponse"]; @@ -509,3 +584,32 @@ export type StorageStatus = components["schemas"]["StorageStatus"]; // NOTE: Backup operations use raw binary data (zip/octet-stream), not JSON schema types: // - Backup export returns application/zip // - Backup restore accepts application/octet-stream + +// ============================================================================= +// Aliases consumed by client.ts +// ============================================================================= + +export type ActivityDay = components["schemas"]["ActivityDay"]; +export type ActivityResponse = components["schemas"]["ActivityResponse"]; +export type ActivityTotals = components["schemas"]["ActivityTotals"]; +export type Deployment = components["schemas"]["Deployment"]; +export type NotificationsResponse = components["schemas"]["NotificationsResponse"]; +export type PresetDefaults = components["schemas"]["PresetDefaults"]; +export type PresetMeta = components["schemas"]["PresetMeta"]; +export type ProcessTokens = components["schemas"]["ProcessTokens"]; +export type TokenSummary = components["schemas"]["TokenSummary"]; +export type UnreadCountResponse = components["schemas"]["UnreadCountResponse"]; +export type UpdateStatus = components["schemas"]["UpdateStatus"]; +export type UsageByModel = components["schemas"]["UsageByModel"]; +export type UsageResponse = components["schemas"]["UsageResponse"]; +export type UsageTotals = components["schemas"]["UsageTotals"]; +export type WikiHistoryResponse = components["schemas"]["WikiHistoryResponse"]; +export type WikiListResponse = components["schemas"]["WikiListResponse"]; +export type WikiPage = components["schemas"]["WikiPage"]; +export type WikiPageResponse = components["schemas"]["WikiPageResponse"]; +export type WikiPageSummary = components["schemas"]["WikiPageSummary"]; +export type WikiPageVersion = components["schemas"]["WikiPageVersion"]; +export type CreatePageRequest = components["schemas"]["CreatePageRequest"]; +export type EditPageRequest = components["schemas"]["EditPageRequest"]; +export type Notification = components["schemas"]["Notification"]; +export type SavedAttachmentMeta = components["schemas"]["SavedAttachmentMeta"]; diff --git a/interface/src/components/ApprovalModal.tsx b/interface/src/components/ApprovalModal.tsx index c17c594b5..25c63726e 100644 --- a/interface/src/components/ApprovalModal.tsx +++ b/interface/src/components/ApprovalModal.tsx @@ -1,4 +1,5 @@ import {useQuery, useMutation, useQueryClient} from "@tanstack/react-query"; +import {Link} from "@tanstack/react-router"; import { DialogRoot, DialogContent, @@ -9,7 +10,7 @@ import { Button, } from "@spacedrive/primitives"; import {TaskDetail} from "@spacedrive/ai"; -import {CheckCircle, XCircle, WarningCircle} from "@phosphor-icons/react"; +import {CheckCircle, XCircle, Warning, WarningCircle} from "@phosphor-icons/react"; import {api, type NotificationItem, type NotificationKind} from "@/api/client"; import {NOTIFICATIONS_QUERY_KEY} from "@/hooks/useNotifications"; @@ -22,6 +23,10 @@ const KIND_CONFIG: Record {if (!v) onClose();}}> @@ -122,6 +136,24 @@ export function ApprovalModal({notification, onClose}: ApprovalModalProps) { Task not found ) + ) : stoppedRunId ? ( + // The reason, set apart rather than run in as body text. It names + // the task and the hold — blocked for a person, a gate that can no + // longer open, a placeholder that will never expand, inputs that + // will never resolve — and it is the only thing here that says what + // to do next. +
+

+ Why it stopped +

+

+ {notification?.body ?? "No reason was recorded."} +

+

+ This run is not going to continue on its own. Open it to see + where it stopped, and to cancel or delete it. +

+
) : (
{notification?.body ? ( @@ -154,6 +186,17 @@ export function ApprovalModal({notification, onClose}: ApprovalModalProps) { {approveMutation.isPending ? "Approving…" : "Approve"} )} + {stoppedRunId && ( + + + + )}
diff --git a/interface/src/components/CapabilityPicker.tsx b/interface/src/components/CapabilityPicker.tsx new file mode 100644 index 000000000..0e511f48e --- /dev/null +++ b/interface/src/components/CapabilityPicker.tsx @@ -0,0 +1,272 @@ +import {useEffect, useMemo, useRef, useState, type KeyboardEvent} from "react"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faPlus, faXmark} from "@fortawesome/free-solid-svg-icons"; + +/** + * Picks capability labels, offering the ones the fleet already declares. + * + * This is a mitigation the design named, not decoration. Capabilities are + * opaque strings and case is deliberately not folded — `rust` and `Rust` are + * two capabilities and one of them matches nothing, silently, forever. The + * design's answer is to "offer the existing set when authoring rather than + * validating a taxonomy into existence", which is the whole reason + * `AgentInfo.capabilities` is published to the client at all. + * + * So the shape here is deliberate: existing labels are one click and are what + * Enter takes by default, while inventing a new one is a separate, differently + * coloured row that has to be chosen on purpose. Typing a novel label and + * blurring adds *nothing* — unlike the plain `TagInput` this is modelled on, + * where blur commits — because the expensive mistake is creating `Rust` while + * meaning to pick `rust`, and that mistake is exactly a typo followed by a + * click somewhere else. + */ +export interface CapabilityPickerProps { + value: string[]; + onChange: (next: string[]) => void; + /** Every label declared anywhere in the fleet, for suggestions. */ + suggestions: readonly string[]; + placeholder?: string; + className?: string; + /** Rendered under each suggestion — usually who declares it. */ + describeSuggestion?: (label: string) => string | undefined; + disabled?: boolean; + inputId?: string; +} + +export function CapabilityPicker({ + value, + onChange, + suggestions, + placeholder = "Add a capability…", + className, + describeSuggestion, + disabled, + inputId, +}: CapabilityPickerProps) { + const [draft, setDraft] = useState(""); + const [open, setOpen] = useState(false); + const [active, setActive] = useState(0); + const containerRef = useRef(null); + + const query = draft.trim(); + + // Existing labels not already chosen, filtered by what has been typed. + // Case-insensitive *matching* so a search finds `Rust` when you type `ru`; + // the label added is always the fleet's own spelling, never the query's. + const existing = useMemo(() => { + const chosen = new Set(value); + return suggestions + .filter((label) => !chosen.has(label)) + .filter((label) => + query === "" ? true : label.toLowerCase().includes(query.toLowerCase()), + ); + }, [suggestions, value, query]); + + // Offered only when the typed label is not already a fleet label *exactly*. + // An exact match that differs in case still offers creation, because that is + // a real and different capability — but the existing spelling is listed + // above it, which is the point. + const canCreate = + query !== "" && !suggestions.includes(query) && !value.includes(query); + + // A label that differs from the query only in case. This is the exact drift + // the design calls out — `rust` and `Rust` are two capabilities and one of + // them matches nothing — and filtering alone does not catch it: when the + // clashing label is one this agent already holds it is filtered out of the + // suggestions entirely, so the create row would be the *only* thing on + // screen and would look like the obvious choice. Named explicitly instead. + const caseClash = canCreate + ? [...suggestions, ...value].find( + (label) => + label !== query && label.toLowerCase() === query.toLowerCase(), + ) + : undefined; + + const options = useMemo( + () => [ + ...existing.map((label) => ({kind: "existing" as const, label})), + ...(canCreate ? [{kind: "create" as const, label: query}] : []), + ], + [existing, canCreate, query], + ); + + // Enter must land on an existing label whenever one matches, so the default + // path is reuse and creation costs an extra deliberate keystroke. + useEffect(() => { + setActive(0); + }, [query]); + + useEffect(() => { + if (!open) return; + const onDocMouseDown = (event: globalThis.MouseEvent) => { + if (!containerRef.current?.contains(event.target as Node)) setOpen(false); + }; + document.addEventListener("mousedown", onDocMouseDown); + return () => document.removeEventListener("mousedown", onDocMouseDown); + }, [open]); + + const add = (label: string) => { + const trimmed = label.trim(); + if (trimmed === "" || value.includes(trimmed)) return; + // Sorted to match the server's `normalise_capabilities`, so a set the UI + // shows and a set the server stores never differ only in order. + onChange([...value, trimmed].sort()); + setDraft(""); + setActive(0); + }; + + const remove = (label: string) => onChange(value.filter((it) => it !== label)); + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "ArrowDown") { + event.preventDefault(); + setOpen(true); + setActive((i) => Math.min(i + 1, options.length - 1)); + return; + } + if (event.key === "ArrowUp") { + event.preventDefault(); + setActive((i) => Math.max(i - 1, 0)); + return; + } + if (event.key === "Enter") { + event.preventDefault(); + const option = options[active]; + if (option) add(option.label); + return; + } + if (event.key === "Escape") { + setOpen(false); + return; + } + // Backspace on an empty box removes the last chip — the one gesture worth + // keeping from the plain tag input, because it undoes rather than creates. + if (event.key === "Backspace" && draft === "" && value.length > 0) { + remove(value[value.length - 1]); + } + }; + + return ( +
+
+ {value.map((label) => ( + + {label} + + + ))} + { + setDraft(event.target.value); + setOpen(true); + }} + onFocus={() => setOpen(true)} + onKeyDown={handleKeyDown} + placeholder={value.length === 0 ? placeholder : ""} + className="min-w-[120px] flex-1 border-none bg-transparent text-[11px] text-ink outline-none placeholder:text-ink-faint" + /> +
+ + {open && options.length > 0 && ( +
+
+ {existing.length > 0 && ( +
+ Declared in the fleet +
+ )} + {options.map((option, index) => { + const isActive = index === active; + if (option.kind === "create") { + return ( + + ); + } + const description = describeSuggestion?.(option.label); + return ( + + ); + })} +
+
+ )} +
+ ); +} diff --git a/interface/src/components/ChannelCard.tsx b/interface/src/components/ChannelCard.tsx index 37c95ecd7..5451c0e77 100644 --- a/interface/src/components/ChannelCard.tsx +++ b/interface/src/components/ChannelCard.tsx @@ -117,7 +117,7 @@ export function ChannelCard({ )} {channel.response_mode === "mention_only" && ( - + Mention Only )} diff --git a/interface/src/components/ChannelEditModal.tsx b/interface/src/components/ChannelEditModal.tsx index 7c10b6936..62146a0a7 100644 --- a/interface/src/components/ChannelEditModal.tsx +++ b/interface/src/components/ChannelEditModal.tsx @@ -945,8 +945,8 @@ export function ChannelEditModal({
{message.text} @@ -961,13 +961,13 @@ export function ChannelEditModal({ variant="bare" size="sm" onClick={() => setConfirmDisconnect(true)} - className="text-red-400 hover:text-red-300" + className="text-status-error hover:text-status-error/80" > Disconnect {name} ) : (
-

+

This will remove all credentials and bindings for {name}. The bot will stop responding immediately.

@@ -983,7 +983,7 @@ export function ChannelEditModal({ size="sm" onClick={() => disconnect.mutate()} loading={disconnect.isPending} - className="bg-red-500/20 text-red-400 hover:bg-red-500/30" + className="bg-status-error/20 text-status-error hover:bg-status-error/30" > Confirm Disconnect diff --git a/interface/src/components/ChannelSettingCard.tsx b/interface/src/components/ChannelSettingCard.tsx index 3f45baacd..98447884e 100644 --- a/interface/src/components/ChannelSettingCard.tsx +++ b/interface/src/components/ChannelSettingCard.tsx @@ -385,7 +385,7 @@ export function InstanceCard({ {instance.name || "default"} {instance.enabled ? "● Active" : "○ Disabled"} @@ -562,8 +562,8 @@ export function InstanceCard({
{message.text} @@ -582,7 +582,7 @@ export function InstanceCard({ ) : (
-

+

This will remove credentials and bindings for{" "} {instanceLabel}. The adapter will stop immediately.

@@ -598,7 +598,7 @@ export function InstanceCard({ size="sm" onClick={() => deleteInstance.mutate()} loading={deleteInstance.isPending} - className="bg-red-500/20 text-red-400 hover:bg-red-500/30" + className="bg-status-error/20 text-status-error hover:bg-status-error/30" > Confirm Remove @@ -1396,8 +1396,8 @@ export function AddInstanceCard({
{message.text} diff --git a/interface/src/components/ConnectionScreen.tsx b/interface/src/components/ConnectionScreen.tsx index d5f90864e..451236eed 100644 --- a/interface/src/components/ConnectionScreen.tsx +++ b/interface/src/components/ConnectionScreen.tsx @@ -205,7 +205,7 @@ export function ConnectionScreen() { )} {sidecarState === "error" && sidecarError && ( -

+

{sidecarError}

)} diff --git a/interface/src/components/ConversationsSidebar.tsx b/interface/src/components/ConversationsSidebar.tsx index 790551a68..edd5f9c30 100644 --- a/interface/src/components/ConversationsSidebar.tsx +++ b/interface/src/components/ConversationsSidebar.tsx @@ -162,7 +162,7 @@ export function ConversationsSidebar({ e.stopPropagation(); handleDelete(conv); }} - className="rounded p-0.5 text-ink-faint hover:bg-red-500/20 hover:text-red-400" + className="rounded p-0.5 text-ink-faint hover:bg-status-error/20 hover:text-status-error" title="Delete" > handleDelete(event, thread.thread_id)} - className="mt-0.5 shrink-0 rounded p-0.5 text-ink-faint opacity-0 transition-all hover:bg-red-500/10 hover:text-red-400 group-hover:opacity-100" + className="mt-0.5 shrink-0 rounded p-0.5 text-ink-faint opacity-0 transition-all hover:bg-status-error/10 hover:text-status-error group-hover:opacity-100" title="Delete thread" > @@ -509,7 +509,7 @@ export function CortexChatPanel({ )} {error && ( -
+
{error}
)} diff --git a/interface/src/components/DeleteAgentDialog.tsx b/interface/src/components/DeleteAgentDialog.tsx index 715f62249..9ee81aad5 100644 --- a/interface/src/components/DeleteAgentDialog.tsx +++ b/interface/src/components/DeleteAgentDialog.tsx @@ -66,7 +66,7 @@ export function DeleteAgentDialog({open, onOpenChange, agentId}: DeleteAgentDial />
{error && ( -
+
{error}
)} diff --git a/interface/src/components/ErrorBoundary.tsx b/interface/src/components/ErrorBoundary.tsx index e9c9883f4..9f5ef7be5 100644 --- a/interface/src/components/ErrorBoundary.tsx +++ b/interface/src/components/ErrorBoundary.tsx @@ -30,8 +30,8 @@ export class ErrorBoundary extends Component { return (
-
-

+
+

Something went wrong

@@ -46,7 +46,7 @@ export class ErrorBoundary extends Component { diff --git a/interface/src/components/MemoryGraph.tsx b/interface/src/components/MemoryGraph.tsx index 73aab6887..9c24ce64f 100644 --- a/interface/src/components/MemoryGraph.tsx +++ b/interface/src/components/MemoryGraph.tsx @@ -525,7 +525,7 @@ export function MemoryGraph({agentId, sort, typeFilter}: MemoryGraphProps) { )} {error && (

-

{error}

+

{error}

)} {!isLoading && !error && nodeCount === 0 && ( diff --git a/interface/src/components/ModelSelect.tsx b/interface/src/components/ModelSelect.tsx index 660d191f8..a0f081378 100644 --- a/interface/src/components/ModelSelect.tsx +++ b/interface/src/components/ModelSelect.tsx @@ -12,28 +12,6 @@ interface ModelSelectProps { capability?: "input_audio" | "voice_transcription"; } -const PROVIDER_LABELS: Record = { - anthropic: "Anthropic", - openrouter: "OpenRouter", - kilo: "Kilo Gateway", - openai: "OpenAI", - "openai-chatgpt": "ChatGPT Plus (OAuth)", - deepseek: "DeepSeek", - xai: "xAI", - mistral: "Mistral", - gemini: "Google Gemini", - groq: "Groq", - together: "Together AI", - fireworks: "Fireworks AI", - zhipu: "Z.ai (GLM)", - ollama: "Ollama", - "opencode-zen": "OpenCode Zen", - "opencode-go": "OpenCode Go", - minimax: "MiniMax", - "minimax-cn": "MiniMax CN", - "github-copilot": "GitHub Copilot", -}; - function formatContextWindow(tokens: number | null): string { if (!tokens) return ""; if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`; @@ -134,8 +112,6 @@ export function ModelSelect({ "kilo", "anthropic", "openai", - "openai-chatgpt", - "github-copilot", "ollama", "deepseek", "xai", @@ -178,7 +154,7 @@ export function ModelSelect({ {sortedProviders.map((provider) => (
- {PROVIDER_LABELS[provider] ?? provider} + {provider}
{grouped[provider].map((model) => (
)} {error && ( -
+
Failed to load prompt:{" "} {error instanceof Error ? error.message : "Unknown error"}
)} {data?.error && ( -
+
{data.message}
)} diff --git a/interface/src/components/SandboxContainment.tsx b/interface/src/components/SandboxContainment.tsx new file mode 100644 index 000000000..7090b92ab --- /dev/null +++ b/interface/src/components/SandboxContainment.tsx @@ -0,0 +1,149 @@ +import {useQuery} from "@tanstack/react-query"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import { + faCircleCheck, + faLockOpen, + faTriangleExclamation, +} from "@fortawesome/free-solid-svg-icons"; +import {api, type SandboxContainmentStatus} from "@/api/client"; + +/** + * What the host is actually doing about containment, per agent. + * + * `sandbox.mode` is what the operator asked for; `containment_active` is what + * is in force. They read alike from the config surface and are not the same + * question, and reporting only the first is how an instance ends up running + * unconfined while its config says `enabled` — the one-label-two-conditions + * shape this codebase keeps paying for, this time in the security layer. + * + * Until now `GET /status` returned all three facts and nothing displayed any of + * them. + */ +export function useContainmentStatus() { + const {data, isLoading} = useQuery({ + queryKey: ["status"], + queryFn: api.status, + // Installing a backend requires a restart, so this does not change under + // anyone. Polled slowly rather than never so a restarted instance is not + // misreported for the life of the tab. + staleTime: 30_000, + refetchInterval: 60_000, + }); + return {agents: data?.sandbox ?? [], isLoading}; +} + +/** Whether any agent's config claims containment the host is not providing. */ +export function anyInert(agents: SandboxContainmentStatus[]): boolean { + return agents.some((agent) => agent.requested_but_inert); +} + +/** + * One agent's containment, as the three facts rather than a green tick. + * + * The middle state is the one worth the space: mode says `enabled`, no backend + * exists, and the read/write allowlists come back empty. That is the state a + * command step refuses to run in, so this is also the explanation for a task + * parked as `capability` with nothing obviously wrong. + */ +export function ContainmentRow({status}: {status: SandboxContainmentStatus}) { + const inert = status.requested_but_inert; + const active = status.containment_active; + + return ( +
+
+ + {status.agent_id} + + {inert + ? "requested but inert" + : active + ? `contained by ${status.backend ?? "an unnamed backend"}` + : "not contained"} + + + · mode {status.mode} + {status.backend ? ( + <> + {" "} + · backend {status.backend} + + ) : ( + " · no backend detected" + )} + {status.require_containment && " · required"} + +
+ + {inert && ( +

+ This config claims containment the host is not providing: the + allowlists come back empty and a shell command runs with full host + access. Command steps refuse to run{" "} + in this state — they are stored, repeated and unattended, so they fail + closed rather than inheriting a worker's watched-in-the-moment risk. + Install a backend (on Linux, the bubblewrap{" "} + package) and restart, or set mode to{" "} + disabled to say out loud that this + host runs uncontained — enforcement is identical either way. +

+ )} +
+ ); +} + +/** Every agent's containment, for a settings screen. */ +export function ContainmentStatusList() { + const {agents, isLoading} = useContainmentStatus(); + + if (isLoading) { + return ( +

Checking what the host enforces…

+ ); + } + if (agents.length === 0) { + return ( +

+ No agent has a live sandbox to report on. +

+ ); + } + return ( +
+ {agents.map((status) => ( + + ))} +
+ ); +} diff --git a/interface/src/components/SetupBanner.tsx b/interface/src/components/SetupBanner.tsx index 0d214a206..3012d9382 100644 --- a/interface/src/components/SetupBanner.tsx +++ b/interface/src/components/SetupBanner.tsx @@ -15,7 +15,7 @@ export function SetupBanner() { return ( No LLM provider configured.{" "} - + Add an API key in Settings {" "} to get started. diff --git a/interface/src/components/Sidebar.tsx b/interface/src/components/Sidebar.tsx index 0f84a4712..f078e9967 100644 --- a/interface/src/components/Sidebar.tsx +++ b/interface/src/components/Sidebar.tsx @@ -31,6 +31,7 @@ import { TreeStructure, Wrench, CheckSquare, + FlowArrow, GearSix, DotsThree, ChatCircleDots, @@ -40,6 +41,7 @@ import { CalendarDots, SlidersHorizontal, BookBookmark, + FolderOpen, } from "@phosphor-icons/react"; import { CircleButton, @@ -120,7 +122,7 @@ function SortableAgentItem({ ? "bg-sidebar-selected/40 text-sidebar-ink" : isActive ? "text-sidebar-ink" - : "text-sidebar-inkDull hover:bg-sidebar-selected/20 hover:text-sidebar-ink" + : "text-sidebar-ink-dull hover:bg-sidebar-selected/20 hover:text-sidebar-ink" }`} style={{pointerEvents: isDragging ? "none" : "auto"}} {...attributes} @@ -153,7 +155,7 @@ function SortableAgentItem({ className={`flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left text-sm font-medium tracking-wide transition-colors ${ subActive ? "bg-sidebar-selected/40 text-sidebar-ink" - : "text-sidebar-inkDull hover:bg-sidebar-selected/20 hover:text-sidebar-ink" + : "text-sidebar-ink-dull hover:bg-sidebar-selected/20 hover:text-sidebar-ink" }`} >
@@ -174,6 +176,15 @@ const navItems = [ {to: "/", icon: TreeStructure, label: "Org Chart", exact: true}, {to: "/workbench", icon: Wrench, label: "Workbench", exact: true}, {to: "/tasks", icon: CheckSquare, label: "Tasks", exact: true}, + // Not `exact`: the editor and the run view live under this section too, and + // an exact match would unhighlight the moment you opened a template. + {to: "/workflows", icon: FlowArrow, label: "Workflows", exact: false}, + // In the main nav rather than only in the Projects section below, because + // that section is hidden while you have no projects — so the only route to + // the page where you create your first one appeared once you already had + // one. A fresh instance could not reach it at all, and projects are what + // repo and worktree bindings hang off. + {to: "/projects", icon: FolderOpen, label: "Projects", exact: false}, {to: "/wiki", icon: BookBookmark, label: "Wiki", exact: true}, ] as const; @@ -331,7 +342,7 @@ export function Sidebar({liveStates: _liveStates}: SidebarProps) { className={`flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left text-sm font-medium tracking-wide outline-none ring-inset ring-transparent transition-colors focus:ring-1 focus:ring-accent ${ isActive ? "bg-sidebar-selected/40 text-sidebar-ink" - : "text-sidebar-inkDull hover:bg-sidebar-selected/20 hover:text-sidebar-ink" + : "text-sidebar-ink-dull hover:bg-sidebar-selected/20 hover:text-sidebar-ink" }`} >
@@ -346,7 +357,7 @@ export function Sidebar({liveStates: _liveStates}: SidebarProps) { {projects.length > 0 && (
-
+
Projects
@@ -368,7 +379,7 @@ export function Sidebar({liveStates: _liveStates}: SidebarProps) { className={`flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left transition-colors ${ activeProjectId === project.id ? "bg-sidebar-selected/40 text-sidebar-ink" - : "text-sidebar-inkDull hover:bg-sidebar-selected/20 hover:text-sidebar-ink" + : "text-sidebar-ink-dull hover:bg-sidebar-selected/20 hover:text-sidebar-ink" }`} > {logoUrl ? ( @@ -413,7 +424,7 @@ export function Sidebar({liveStates: _liveStates}: SidebarProps) { {/* Agents section */}
-
+
Agents
{hasProvider && ( diff --git a/interface/src/components/TaskUtils.tsx b/interface/src/components/TaskUtils.tsx index 0bb1487ef..376f262c0 100644 --- a/interface/src/components/TaskUtils.tsx +++ b/interface/src/components/TaskUtils.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCodeBranch, faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"; import { Badge, Popover, SelectPill, OptionList, OptionListItem } from "@spacedrive/primitives"; +import { isRecord } from "@/lib/json"; // --------------------------------------------------------------------------- // GitHub metadata helpers @@ -13,10 +14,6 @@ interface GithubReference { url: string | null; } -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - function toSafeExternalUrl(value: unknown): string | null { if (typeof value !== "string") return null; try { @@ -52,7 +49,16 @@ function readGithubReference( return { kind, label, url }; } -export function getGithubReferences(metadata: Record): GithubReference[] { +/** + * Task metadata is a free-form JSON value on the server, so it arrives typed as + * `unknown` rather than as an object — it is not guaranteed to be one. Narrow + * here instead of asserting at each call site. + */ +export function getGithubReferences(metadata: unknown): GithubReference[] { + if (!isRecord(metadata)) { + return []; + } + return [ readGithubReference(metadata.github_issue, "issue"), readGithubReference(metadata.github_pr, "pr"), @@ -87,8 +93,8 @@ export function GithubMetadataBadges({ ); const className = compact - ? "cursor-pointer hover:border-blue-400/50 hover:text-blue-300" - : "cursor-pointer hover:border-blue-400/50 hover:bg-blue-500/20 hover:text-blue-300"; + ? "cursor-pointer hover:border-status-info/50 hover:text-status-info" + : "cursor-pointer hover:border-status-info/50 hover:bg-status-info/20 hover:text-status-info"; if (reference.url) { return ( diff --git a/interface/src/components/ToolCall.tsx b/interface/src/components/ToolCall.tsx index f2ee1658c..ffe826954 100644 --- a/interface/src/components/ToolCall.tsx +++ b/interface/src/components/ToolCall.tsx @@ -2,17 +2,15 @@ import {useState} from "react"; import {cx} from "class-variance-authority"; import type {OpenCodePart} from "@/api/client"; import type { TranscriptStep as SchemaTranscriptStep } from "@/api/types"; +import type { components } from "@/api/schema"; -// Extended TranscriptStep with live_output for streaming shell output -type ToolResultStatus = "pending" | "final" | "waiting_for_input"; +type ToolResultStatus = components["schemas"]["ToolResultStatus"]; -type ExtendedTranscriptStep = SchemaTranscriptStep & { - live_output?: string; - status?: ToolResultStatus; -}; - -// Use the extended type for pairing -type TranscriptStep = ExtendedTranscriptStep; +// `live_output` and `status` used to be declared here as a local augmentation +// because the generated schema predated the streaming-shell API. The schema now +// carries both, so the augmentation is gone — keeping it would contradict the +// generated types (`string | null` vs `string | undefined`) and break assignment. +type TranscriptStep = SchemaTranscriptStep; // --------------------------------------------------------------------------- // Types @@ -61,15 +59,16 @@ export function pairTranscriptSteps(steps: TranscriptStep[]): TranscriptItem[] { {name: string; text: string; status: ToolResultStatus; liveOutput?: string} >(); - // First pass: index all tool_result steps by call_id + // First pass: index all tool_result steps by call_id. + // `live_output` arrives as `string | null` from the API; normalize the null + // away here so downstream consumers only deal with `string | undefined`. for (const step of steps) { if (step.type === "tool_result") { - const liveOutput = step.live_output; resultsById.set(step.call_id, { name: step.name, text: step.text, status: step.status ?? "final", - liveOutput, + liveOutput: step.live_output ?? undefined, }); } } @@ -492,16 +491,16 @@ const toolRenderers: Record = {

{!!oldStr && (
-

Old

-
+							

Old

+
 								{truncate(String(oldStr), 500)}
 							
)} {!!newStr && (
-

New

-
+							

New

+
 								{truncate(String(newStr), 500)}
 							
@@ -668,16 +667,16 @@ const toolRenderers: Record = {

{!!oldStr && (
-

Old

-
+							

Old

+
 								{truncate(String(oldStr), 500)}
 							
)} {!!newStr && (
-

New

-
+							

New

+
 								{truncate(String(newStr), 500)}
 							
@@ -962,7 +961,7 @@ function ShellResultView({pair}: {pair: ToolCallPair}) { {/* Exit code badge for non-zero */} {isError && (
- + exit {exitCode}
@@ -982,7 +981,7 @@ function ShellResultView({pair}: {pair: ToolCallPair}) { stderr @@ -991,7 +990,7 @@ function ShellResultView({pair}: {pair: ToolCallPair}) {
 						{stderr.replace(/\n$/, "")}
@@ -1017,7 +1016,7 @@ const STATUS_COLORS: Record = {
 	running: "text-accent",
 	completed: "text-status-success",
 	error: "text-status-error",
-	waiting_for_input: "text-blue-500",
+	waiting_for_input: "text-status-info",
 };
 
 /** Human-readable tool name: browser_navigate → Navigate */
@@ -1069,7 +1068,7 @@ export function ToolCall({pair}: {pair: ToolCallPair}) {
 				pair.status === "error"
 					? "border-status-error/30"
 					: pair.status === "waiting_for_input"
-						? "border-blue-500/30"
+						? "border-status-info/30"
 						: "border-app-line/50",
 			)}
 		>
@@ -1097,7 +1096,7 @@ export function ToolCall({pair}: {pair: ToolCallPair}) {
 					
 				)}
 				{pair.status === "waiting_for_input" && !expanded && (
-					Waiting for input
+					Waiting for input
 				)}
 			
 
@@ -1179,8 +1178,8 @@ function renderResult(
 
 	if (pair.status === "waiting_for_input" && !pair.resultRaw) {
 		return (
-			
- +
+ Waiting for input
); diff --git a/interface/src/components/UpdatePill.tsx b/interface/src/components/UpdatePill.tsx index 7096114b4..9945d51f5 100644 --- a/interface/src/components/UpdatePill.tsx +++ b/interface/src/components/UpdatePill.tsx @@ -18,9 +18,9 @@ export function UpdatePill() { - + Update {data.latest_version ?? "available"} ); diff --git a/interface/src/components/WorkersPanel.tsx b/interface/src/components/WorkersPanel.tsx index 522742f9f..dd2735763 100644 --- a/interface/src/components/WorkersPanel.tsx +++ b/interface/src/components/WorkersPanel.tsx @@ -19,6 +19,7 @@ import {formatTimeAgo} from "@/lib/format"; import {LiveDuration} from "@/components/LiveDuration"; import {WorkerDetail, type LiveWorker} from "@/routes/AgentWorkers"; import {cx} from "class-variance-authority"; +import {copyText} from "@/lib/clipboard"; type Tab = "interactive" | "history"; @@ -304,7 +305,7 @@ function WorkerPanelRow({ )} {isIdle && ( - + )} {!isRunning && !isIdle && ( ("idle"); const [cancelling, setCancelling] = useState(false); const isActive = !!liveWorker; @@ -427,9 +428,11 @@ function WorkerDetailInline({ } if (detail?.result) lines.push(`\n---\nResult: ${detail.result}`); - navigator.clipboard.writeText(lines.join("\n")).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); + // copyText reports whether the copy landed; claiming "copied" on a + // refusal would send the operator off with an empty clipboard. + void copyText(lines.join("\n")).then((ok) => { + setCopyState(ok ? "copied" : "failed"); + setTimeout(() => setCopyState("idle"), 2000); }); }, [detail, liveTranscript]); @@ -454,8 +457,16 @@ function WorkerDetailInline({ /> )} diff --git a/interface/src/components/agent-config/ConfigSectionEditor.tsx b/interface/src/components/agent-config/ConfigSectionEditor.tsx index 0441877e8..fdf4443e4 100644 --- a/interface/src/components/agent-config/ConfigSectionEditor.tsx +++ b/interface/src/components/agent-config/ConfigSectionEditor.tsx @@ -10,6 +10,7 @@ import { Switch, } from "@spacedrive/primitives"; import {ModelSelect} from "@/components/ModelSelect"; +import {ContainmentStatusList} from "@/components/SandboxContainment"; import {TagInput} from "@/components/TagInput"; import {supportsAdaptiveThinking} from "./utils"; import {SANDBOX_DEFAULTS} from "./constants"; @@ -554,6 +555,23 @@ export function ConfigSectionEditor({
+ {/* What the host is *actually* doing, next to what was asked for. + `mode` and `containment_active` are two questions that read + alike from this form, and showing only the first is how an + instance ends up running unconfined while this control says + "Enabled". It is also the explanation for a command step + parked as `capability` with nothing else visibly wrong. */} +
+ +

+ The mode above is the request. This is the answer — and the two + can differ, because containment needs a backend the host may + not have. +

+ +
- handleChange("auto_create_worktrees", v)} - /> {description}
{localDirty ? ( - Unsaved changes + Unsaved changes ) : ( Changes saved to config.toml diff --git a/interface/src/components/agent-config/ConfigSidebar.tsx b/interface/src/components/agent-config/ConfigSidebar.tsx index 6eebdf316..cf9e40231 100644 --- a/interface/src/components/agent-config/ConfigSidebar.tsx +++ b/interface/src/components/agent-config/ConfigSidebar.tsx @@ -46,7 +46,7 @@ export function ConfigSidebar({ > {section.label} {!hasContent && ( - + empty )} diff --git a/interface/src/components/agent-config/GeneralEditor.tsx b/interface/src/components/agent-config/GeneralEditor.tsx index fd22904ab..ceecd9256 100644 --- a/interface/src/components/agent-config/GeneralEditor.tsx +++ b/interface/src/components/agent-config/GeneralEditor.tsx @@ -4,6 +4,8 @@ import {cx} from "class-variance-authority"; import {Button, Input} from "@spacedrive/primitives"; import {api} from "@/api/client"; import {ProfileAvatar, seedGradient} from "@/components/ProfileAvatar"; +import {CapabilityPicker} from "@/components/CapabilityPicker"; +import {fleetCapabilities} from "@/lib/capabilities"; import {GRADIENT_PRESETS} from "./constants"; import type {GeneralEditorProps} from "./types"; @@ -13,6 +15,8 @@ export function GeneralEditor({ role, gradientStart, gradientEnd, + capabilities, + agents, detail, onDirtyChange, saveHandlerRef, @@ -23,6 +27,7 @@ export function GeneralEditor({ const [localRole, setLocalRole] = useState(role); const [localGradientStart, setLocalGradientStart] = useState(gradientStart); const [localGradientEnd, setLocalGradientEnd] = useState(gradientEnd); + const [localCapabilities, setLocalCapabilities] = useState(capabilities); const [localDirty, setLocalDirty] = useState(false); const [avatarPreview, setAvatarPreview] = useState(null); const fileInputRef = useRef(null); @@ -66,8 +71,9 @@ export function GeneralEditor({ setLocalRole(role); setLocalGradientStart(gradientStart); setLocalGradientEnd(gradientEnd); + setLocalCapabilities(capabilities); } - }, [displayName, role, gradientStart, gradientEnd, localDirty]); + }, [displayName, role, gradientStart, gradientEnd, capabilities, localDirty]); useEffect(() => { onDirtyChange(localDirty); @@ -79,17 +85,31 @@ export function GeneralEditor({ role: localRole, gradient_start: localGradientStart || undefined, gradient_end: localGradientEnd || undefined, + // Always sent, empty included. `capabilities` replaces the set + // wholesale on the server, and an empty list is the documented way to + // take an agent out of every pool — so it must not be collapsed to + // `undefined` the way the gradients are, which would silently make + // "remove the last label" do nothing. + capabilities: localCapabilities, }); setLocalDirty(false); - }, [onSave, localDisplayName, localRole, localGradientStart, localGradientEnd]); + }, [ + onSave, + localDisplayName, + localRole, + localGradientStart, + localGradientEnd, + localCapabilities, + ]); const handleRevert = useCallback(() => { setLocalDisplayName(displayName); setLocalRole(role); setLocalGradientStart(gradientStart); setLocalGradientEnd(gradientEnd); + setLocalCapabilities(capabilities); setLocalDirty(false); - }, [displayName, role, gradientStart, gradientEnd]); + }, [displayName, role, gradientStart, gradientEnd, capabilities]); useEffect(() => { saveHandlerRef.current.save = handleSave; @@ -109,6 +129,24 @@ export function GeneralEditor({ uploadAvatarMutation.mutate(file); }; + // Suggestions come from the *other* agents. Offering this agent's own + // labels back to it would just be the chips it is already showing. + const otherFleetLabels = fleetCapabilities( + agents.filter((agent) => agent.id !== agentId), + ); + + // Naming who already declares a label is what makes reuse the easy path — + // "pick the one `main` has" is a decision, "pick from a list of strings" is + // a guess. + const describeLabel = (label: string) => { + const holders = agents + .filter( + (agent) => agent.id !== agentId && (agent.capabilities ?? []).includes(label), + ) + .map((agent) => agent.display_name ?? agent.id); + return holders.length > 0 ? `Declared by ${holders.join(", ")}` : undefined; + }; + const [seedC1, seedC2] = seedGradient(agentId); const previewC1 = localGradientStart || seedC1; const previewC2 = localGradientEnd || seedC2; @@ -121,7 +159,7 @@ export function GeneralEditor({ Agent metadata
{localDirty ? ( - Unsaved changes + Unsaved changes ) : ( Changes saved to config.toml @@ -174,6 +212,53 @@ export function GeneralEditor({ />
+ {/* Capabilities */} +
+ +

+ What this agent declares it can do. A task may name an agent, or + state a requirement instead and wait in a pool — any agent holding{" "} + every label a pooled task asks for may claim it. +

+ { + setLocalCapabilities(next); + setLocalDirty(true); + }} + suggestions={otherFleetLabels} + describeSuggestion={describeLabel} + placeholder="e.g. rust, typescript, review" + inputId="agent-capabilities" + /> + {localCapabilities.length === 0 && ( +

+ Declaring nothing is valid — this agent simply never claims + pooled work. Tasks that name it directly still run. +

+ )} + {/* Two authorities, and it is worth saying which wins, because the + obvious guess — that the UI and the file race — is wrong. + `PUT /agents` rewrites the `capabilities` array in config.toml + *and* the table the scheduler matches against, and a file the + watcher sees change is re-projected the same way. So the file + is always the record and neither path leaves a stale set + behind. */} +

+ Also settable as capabilities in + config.toml. Saving here rewrites that array, and editing the file + directly is picked up live — the file is the record either way, and + the last edit wins. Labels are case-sensitive:{" "} + rust and{" "} + Rust are two capabilities. +

+
+ {/* Avatar */}
@@ -207,7 +292,7 @@ export function GeneralEditor({ variant="bare" size="sm" onClick={() => deleteAvatarMutation.mutate()} - className="text-ink-faint hover:text-red-400" + className="text-ink-faint hover:text-status-error" > Remove diff --git a/interface/src/components/agent-config/IdentityEditor.tsx b/interface/src/components/agent-config/IdentityEditor.tsx index e111aa979..ae318649f 100644 --- a/interface/src/components/agent-config/IdentityEditor.tsx +++ b/interface/src/components/agent-config/IdentityEditor.tsx @@ -98,7 +98,7 @@ export function IdentityEditor({
{localDirty ? ( - Unsaved changes + Unsaved changes ) : ( Cmd+S to save )} diff --git a/interface/src/components/agent-config/types.ts b/interface/src/components/agent-config/types.ts index 45aa01ce5..01f0cb3e9 100644 --- a/interface/src/components/agent-config/types.ts +++ b/interface/src/components/agent-config/types.ts @@ -1,4 +1,8 @@ -import type {AgentConfigResponse, AgentConfigUpdateRequest} from "@/api/client"; +import type { + AgentConfigResponse, + AgentConfigUpdateRequest, + AgentInfo, +} from "@/api/client"; export type SectionId = | "general" @@ -35,6 +39,10 @@ export interface GeneralEditorProps { role: string; gradientStart: string; gradientEnd: string; + /** What this agent currently declares it can do. */ + capabilities: string[]; + /** The whole fleet, so the picker can offer labels that already exist. */ + agents: readonly AgentInfo[]; detail: string; onDirtyChange: (dirty: boolean) => void; saveHandlerRef: React.MutableRefObject; @@ -43,6 +51,7 @@ export interface GeneralEditorProps { role?: string; gradient_start?: string; gradient_end?: string; + capabilities?: string[]; }) => void; } diff --git a/interface/src/components/dashboard/ActionItemsCard.tsx b/interface/src/components/dashboard/ActionItemsCard.tsx index ae31cb1a7..773e91b3a 100644 --- a/interface/src/components/dashboard/ActionItemsCard.tsx +++ b/interface/src/components/dashboard/ActionItemsCard.tsx @@ -1,7 +1,9 @@ import {useState} from "react"; +import {Link} from "@tanstack/react-router"; import { CheckCircle, Clock, + Warning, WarningCircle, XCircle, } from "@phosphor-icons/react"; @@ -47,6 +49,18 @@ const TYPE_CONFIG: Record< label: "Alert", action: "Review", }, + // A pipeline that will not continue on its own. Its own row style rather than + // the generic `cortex_observation` fallback it used to land in, because that + // one is an agent's remark and this is a stopped run: the triangle separates + // it from the circle every other alert wears, and the action goes to the run + // rather than to a modal, because the run is where the recovery is. + workflow_run_stopped: { + icon: Warning, + iconClass: "text-status-warning", + badgeVariant: "warning", + label: "Run stopped", + action: "Open run", + }, }; function timeAgo(isoString: string): string { @@ -59,8 +73,21 @@ function timeAgo(isoString: string): string { return `${Math.floor(hours / 24)}d ago`; } +/** + * The run a notification is about, when it is about one. + * + * Read from `related_entity_*` rather than by parsing `action_url`: the pair is + * what the server sets deliberately, and a URL is a string that a route rename + * would quietly turn into a dead link. + */ +function stoppedRunId(item: NotificationItem): string | null { + return item.related_entity_type === "workflow_run" + ? (item.related_entity_id ?? null) + : null; +} + export function ActionItemsCard() { - const {notifications} = useNotifications("unread"); + const {notifications, dismiss} = useNotifications("unread"); const [activeNotification, setActiveNotification] = useState(null); const [atBottom, setAtBottom] = useState(false); @@ -105,10 +132,16 @@ export function ActionItemsCard() { ) as NotificationKind; const config = TYPE_CONFIG[kind]; const Icon = config.icon; + const runId = + kind === "workflow_run_stopped" ? stoppedRunId(item) : null; return (

{item.title}

+ {/* The reason, on the card. "Stuck" alone is what sends + somebody reading rows, and the whole point of putting a + reason on the transition was to stop that. Clipped to two + lines because these name a task and a hold and run long; + it opens the modal, which shows the whole sentence. */} + {kind === "workflow_run_stopped" && item.body && ( + + )}
{config.label} {item.agent_id && ( @@ -131,14 +179,36 @@ export function ActionItemsCard() {
- + {/* A stopped run is answered by looking at the run, so the + primary action goes straight there. Dismiss sits beside it + rather than inside the modal, because this row already shows + everything the modal would have been opened to read. */} + {runId ? ( +
+ + + + +
+ ) : ( + + )}
); }) diff --git a/interface/src/components/dashboard/RecentActivityCard.tsx b/interface/src/components/dashboard/RecentActivityCard.tsx index c2cc9f05b..a3501de46 100644 --- a/interface/src/components/dashboard/RecentActivityCard.tsx +++ b/interface/src/components/dashboard/RecentActivityCard.tsx @@ -23,10 +23,10 @@ const TYPE_CONFIG: Record< ActivityItem["type"], {icon: React.ElementType; iconClass: string} > = { - task_created: {icon: Circle, iconClass: "text-blue-400"}, + task_created: {icon: Circle, iconClass: "text-status-info"}, task_completed: {icon: CheckSquare, iconClass: "text-status-success"}, - cortex: {icon: Brain, iconClass: "text-violet-400"}, - worker_done: {icon: Robot, iconClass: "text-amber-400"}, + cortex: {icon: Brain, iconClass: "text-ink-dull"}, + worker_done: {icon: Robot, iconClass: "text-status-warning"}, }; const FILTERS: {key: FilterType; label: string}[] = [ diff --git a/interface/src/components/org/OrgGraphInner.tsx b/interface/src/components/org/OrgGraphInner.tsx index f838c1041..4859feb67 100644 --- a/interface/src/components/org/OrgGraphInner.tsx +++ b/interface/src/components/org/OrgGraphInner.tsx @@ -572,7 +572,7 @@ export function OrgGraphInner({activeEdges, agents}: OrgGraphInnerProps) { if (error) { return (
-

Failed to load topology

+

Failed to load topology

); } diff --git a/interface/src/components/org/ProfileNode.tsx b/interface/src/components/org/ProfileNode.tsx index 7715aeec2..386b1b3d6 100644 --- a/interface/src/components/org/ProfileNode.tsx +++ b/interface/src/components/org/ProfileNode.tsx @@ -96,7 +96,7 @@ export function ProfileNode({data, selected}: NodeProps) { {isAgent && (
)} diff --git a/interface/src/components/portal/PortalHeader.tsx b/interface/src/components/portal/PortalHeader.tsx index 2dbf3b557..d11091f3e 100644 --- a/interface/src/components/portal/PortalHeader.tsx +++ b/interface/src/components/portal/PortalHeader.tsx @@ -78,12 +78,12 @@ export function PortalHeader({ {modelLabel} )} {responseMode === "observe" && ( - + Observe )} {responseMode === "mention_only" && ( - + Mention Only )} @@ -160,7 +160,7 @@ export function PortalHeader({ saving={saving} /> ) : ( -
+
{defaultsError?.message ?? "Failed to load settings"}
)} diff --git a/interface/src/components/portal/PortalHistoryPopover.tsx b/interface/src/components/portal/PortalHistoryPopover.tsx index 3ac655a06..9d92c824c 100644 --- a/interface/src/components/portal/PortalHistoryPopover.tsx +++ b/interface/src/components/portal/PortalHistoryPopover.tsx @@ -153,7 +153,7 @@ function HistoryRow({ e.stopPropagation(); onDelete(); }} - className="rounded p-0.5 text-ink-faint hover:bg-red-500/20 hover:text-red-400" + className="rounded p-0.5 text-ink-faint hover:bg-status-error/20 hover:text-status-error" title="Delete" > diff --git a/interface/src/components/portal/PortalPanel.tsx b/interface/src/components/portal/PortalPanel.tsx index 8c82c8d4e..86a6da946 100644 --- a/interface/src/components/portal/PortalPanel.tsx +++ b/interface/src/components/portal/PortalPanel.tsx @@ -275,7 +275,7 @@ export function PortalPanel({ agentId }: PortalPanelProps) { /> {error && ( -
+
{error}
)} diff --git a/interface/src/components/portal/PortalTimeline.tsx b/interface/src/components/portal/PortalTimeline.tsx index f0fead486..3821b399b 100644 --- a/interface/src/components/portal/PortalTimeline.tsx +++ b/interface/src/components/portal/PortalTimeline.tsx @@ -6,6 +6,7 @@ import {api, type AttachmentMeta, type TimelineBranchRun, type TimelineItem, typ import {ToolCall, type ToolCallPair, tryParseJson, isErrorResult} from "@/components/ToolCall"; import {PortalWorkerCard} from "./PortalWorkerCard"; import clsx from "clsx"; +import {copyText} from "@/lib/clipboard"; function formatFileSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; @@ -245,7 +246,7 @@ export function PortalTimeline({ }, [sendCount]); const copyMessage = async (content: string) => { - await navigator.clipboard.writeText(content); + await copyText(content); }; return ( diff --git a/interface/src/components/portal/PortalWorkerCard.tsx b/interface/src/components/portal/PortalWorkerCard.tsx index deb5c37cf..e05ddac68 100644 --- a/interface/src/components/portal/PortalWorkerCard.tsx +++ b/interface/src/components/portal/PortalWorkerCard.tsx @@ -1,6 +1,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { InlineWorkerCard, type TranscriptStep } from "@spacedrive/ai"; import { api, type WorkerListItem } from "@/api/client"; +import {copyText} from "@/lib/clipboard"; interface PortalWorkerCardProps { agentId: string; @@ -38,7 +39,7 @@ export function PortalWorkerCard({ agentId, worker }: PortalWorkerCardProps) { .filter(Boolean) .join("\n\n"); - await navigator.clipboard.writeText(payload); + await copyText(payload); }; const cancelMutation = useMutation({ diff --git a/interface/src/components/settings/ApiKeysSection.tsx b/interface/src/components/settings/ApiKeysSection.tsx index e1af1697c..471d8028f 100644 --- a/interface/src/components/settings/ApiKeysSection.tsx +++ b/interface/src/components/settings/ApiKeysSection.tsx @@ -76,7 +76,7 @@ export function ApiKeysSection({settings, isLoading}: GlobalSettingsSectionProps Brave Search {settings?.brave_search_key && ( - + ● Configured )} @@ -117,8 +117,8 @@ export function ApiKeysSection({settings, isLoading}: GlobalSettingsSectionProps
{message.text} diff --git a/interface/src/components/settings/ChatGptOAuthDialog.tsx b/interface/src/components/settings/ChatGptOAuthDialog.tsx deleted file mode 100644 index 985e34006..000000000 --- a/interface/src/components/settings/ChatGptOAuthDialog.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { - Button, - DialogRoot, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from "@spacedrive/primitives"; -import {ProviderIcon} from "@/lib/providerIcons"; -import type {ChatGptOAuthDialogProps} from "./types"; - -export function ChatGptOAuthDialog({ - open, - onOpenChange, - isRequesting, - isPolling, - message, - deviceCodeInfo, - deviceCodeCopied, - onCopyDeviceCode, - onOpenDeviceLogin, - onRestart, -}: ChatGptOAuthDialogProps) { - return ( - - - - - - Sign in with ChatGPT Plus - - {!message && ( - - Copy the device code below, then sign in to your OpenAI account to - authorize access. You must first{" "} - - enable device code login - {" "} - in your ChatGPT security settings. - - )} - - -
- {message && !deviceCodeInfo ? ( - /* Completed state — success or error with no active flow */ -
- {message.text} -
- ) : isRequesting && !deviceCodeInfo ? ( -
-
- Requesting device code... -
- ) : deviceCodeInfo ? ( -
-
-
- - 1 - -

Copy this device code

-
-
- - {deviceCodeInfo.userCode} - - -
-
- -
-
- - 2 - -

- Open OpenAI and paste the code -

-
-
- -
-
- - {isPolling && !message && ( -
-
- Waiting for sign-in confirmation... -
- )} - - {message && ( -
- {message.text} -
- )} -
- ) : null} -
- - - {message && !deviceCodeInfo ? ( - /* Completed — show Done (or Retry for errors) */ - message.type === "success" ? ( - - ) : ( - <> - - - - ) - ) : ( - <> - - {deviceCodeInfo && ( - - )} - - )} - - - - ); -} diff --git a/interface/src/components/settings/ConfigFileSection.tsx b/interface/src/components/settings/ConfigFileSection.tsx index 3946a5a43..aca226d7c 100644 --- a/interface/src/components/settings/ConfigFileSection.tsx +++ b/interface/src/components/settings/ConfigFileSection.tsx @@ -196,10 +196,10 @@ export function ConfigFileSection() {
{validationError ? `Syntax error: ${validationError}` : message?.text} diff --git a/interface/src/components/settings/InstanceSection.tsx b/interface/src/components/settings/InstanceSection.tsx index 6e4c366d4..c5f6cb21b 100644 --- a/interface/src/components/settings/InstanceSection.tsx +++ b/interface/src/components/settings/InstanceSection.tsx @@ -88,8 +88,8 @@ export function InstanceSection({settings, isLoading}: GlobalSettingsSectionProp
{message.text} diff --git a/interface/src/components/settings/OpenCodeSection.tsx b/interface/src/components/settings/OpenCodeSection.tsx index e6b394676..323514fb8 100644 --- a/interface/src/components/settings/OpenCodeSection.tsx +++ b/interface/src/components/settings/OpenCodeSection.tsx @@ -291,8 +291,8 @@ export function OpenCodeSection({settings, isLoading}: GlobalSettingsSectionProp
{message.text} diff --git a/interface/src/components/settings/ProviderCard.tsx b/interface/src/components/settings/ProviderCard.tsx index daac315fe..817966e71 100644 --- a/interface/src/components/settings/ProviderCard.tsx +++ b/interface/src/components/settings/ProviderCard.tsx @@ -2,56 +2,67 @@ import {Button} from "@spacedrive/primitives"; import {ProviderIcon} from "@/lib/providerIcons"; import type {ProviderCardProps} from "./types"; +const API_TYPE_LABELS: Record = { + anthropic: "Anthropic Messages API", + openai_compatible: "OpenAI-compatible", +}; + export function ProviderCard({ provider, - name, - description, - configured, - defaultModel, + apiType, + baseUrl, + displayName, + hasKey, onEdit, onRemove, removing, - actionLabel, - showRemove, }: ProviderCardProps) { - const primaryLabel = actionLabel ?? (configured ? "Update" : "Add key"); - const shouldShowRemove = showRemove ?? configured; return (
-
+
- {name} - {configured && ( + + {displayName || provider} + + + {API_TYPE_LABELS[apiType] ?? apiType} + + {hasKey && ( )}
-

{description}

+

{baseUrl}

- Default model: {defaultModel} + Models route as{" "} + {provider}/<model> + {!hasKey && ( + + {" "} + · no API key resolved + + )}

+ - {shouldShowRemove && ( - - )}
diff --git a/interface/src/components/settings/SecretsSection.tsx b/interface/src/components/settings/SecretsSection.tsx index a2e17f2b4..a870e65bc 100644 --- a/interface/src/components/settings/SecretsSection.tsx +++ b/interface/src/components/settings/SecretsSection.tsx @@ -1,5 +1,6 @@ import {useState} from "react"; import {useQuery, useMutation, useQueryClient} from "@tanstack/react-query"; +import {copyText} from "@/lib/clipboard"; import { api, type SecretCategory, @@ -215,7 +216,7 @@ export function SecretsSection() { const handleCopyKey = async () => { if (!masterKeyDisplay) return; try { - await navigator.clipboard.writeText(masterKeyDisplay); + await copyText(masterKeyDisplay); setMasterKeyCopied(true); } catch { // Fallback @@ -262,10 +263,10 @@ export function SecretsSection() {
@@ -286,10 +287,10 @@ export function SecretsSection() { {/* Encryption banner (unencrypted stores) */} {state === "unencrypted" && !storeStatus?.platform_managed && ( -
+
-

+

Encryption not enabled

@@ -313,8 +314,8 @@ export function SecretsSection() { {/* Unlock prompt (locked stores) */} {isLocked && ( -

-

Secrets are locked

+
+

Secrets are locked

Enter your master key to unlock encrypted secrets. You can view secret names but cannot add, edit, or read values while locked. @@ -348,8 +349,8 @@ export function SecretsSection() {

{message.text} @@ -711,7 +712,7 @@ export function SecretsSection() {
- + {masterKeyDisplay}
-
+
If you lose this key and the OS credential store is cleared (e.g. after a Linux reboot), you will not be able to access your encrypted secrets. diff --git a/interface/src/components/settings/ServerSection.tsx b/interface/src/components/settings/ServerSection.tsx index fcd0f617e..848295073 100644 --- a/interface/src/components/settings/ServerSection.tsx +++ b/interface/src/components/settings/ServerSection.tsx @@ -140,13 +140,13 @@ export function ServerSection({settings, isLoading}: GlobalSettingsSectionProps)
{message.text} {message.requiresRestart && ( -
+
⚠️ Restart required for changes to take effect
)} diff --git a/interface/src/components/settings/UpdatesSection.tsx b/interface/src/components/settings/UpdatesSection.tsx index 811316a39..e057f1a91 100644 --- a/interface/src/components/settings/UpdatesSection.tsx +++ b/interface/src/components/settings/UpdatesSection.tsx @@ -2,6 +2,7 @@ import {useState} from "react"; import {useQuery, useMutation, useQueryClient} from "@tanstack/react-query"; import {api, type UpdateStatus} from "@/api/client"; import {Button} from "@spacedrive/primitives"; +import {copyText} from "@/lib/clipboard"; function formatCheckedAt(checkedAt: string | null): string { if (!checkedAt) return "Never"; @@ -77,18 +78,10 @@ export function UpdatesSection() { const handleCopy = async (label: string, content: string) => { try { - if (navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(content); - } else { - const textarea = document.createElement("textarea"); - textarea.value = content; - textarea.setAttribute("readonly", ""); - textarea.style.position = "absolute"; - textarea.style.left = "-9999px"; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand("copy"); - document.body.removeChild(textarea); + // `copyText` owns the secure-context fallback that used to be + // duplicated here, and reports whether the copy actually landed. + if (!(await copyText(content))) { + throw new Error("the browser refused the copy"); } setCopiedBlock(label); setTimeout( @@ -239,7 +232,7 @@ export function UpdatesSection() {

)} {!data?.can_apply && data?.cannot_apply_reason && ( -

+

{data.cannot_apply_reason}

)} @@ -334,7 +327,7 @@ export function UpdatesSection() {
{data?.error && ( -
+
Update check error: {data.error}
)} @@ -345,8 +338,8 @@ export function UpdatesSection() {
{message.text} diff --git a/interface/src/components/settings/WorkerLogsSection.tsx b/interface/src/components/settings/WorkerLogsSection.tsx index 544bfae4d..3c85f9a95 100644 --- a/interface/src/components/settings/WorkerLogsSection.tsx +++ b/interface/src/components/settings/WorkerLogsSection.tsx @@ -118,8 +118,8 @@ export function WorkerLogsSection({settings, isLoading}: GlobalSettingsSectionPr
{message.text} diff --git a/interface/src/components/settings/constants.ts b/interface/src/components/settings/constants.ts index 018bce707..14d445e93 100644 --- a/interface/src/components/settings/constants.ts +++ b/interface/src/components/settings/constants.ts @@ -80,186 +80,49 @@ export const SECTIONS = [ description: string; }[]; -export const PROVIDERS = [ - { - id: "openrouter", - name: "OpenRouter", - description: "Multi-provider gateway with unified API", - placeholder: "sk-or-...", - envVar: "OPENROUTER_API_KEY", - defaultModel: "openrouter/anthropic/claude-sonnet-4", - }, - { - id: "kilo", - name: "Kilo Gateway", - description: "OpenAI-compatible multi-provider gateway", - placeholder: "sk-...", - envVar: "KILO_API_KEY", - defaultModel: "kilo/anthropic/claude-sonnet-4.5", - }, - { - id: "opencode-zen", - name: "OpenCode Zen", - description: "Multi-format gateway (Kimi, GLM, MiniMax, Qwen)", - placeholder: "...", - envVar: "OPENCODE_ZEN_API_KEY", - defaultModel: "opencode-zen/kimi-k2.5", - }, - { - id: "opencode-go", - name: "OpenCode Go", - description: "Lite OpenCode model catalog and limits", - placeholder: "...", - envVar: "OPENCODE_GO_API_KEY", - defaultModel: "opencode-go/kimi-k2.5", - }, - { - id: "anthropic", - name: "Anthropic", - description: "Claude models (Sonnet, Opus, Haiku)", - placeholder: "sk-ant-...", - envVar: "ANTHROPIC_API_KEY", - defaultModel: "anthropic/claude-sonnet-4", - }, - { - id: "openai", - name: "OpenAI", - description: "GPT models", - placeholder: "sk-...", - envVar: "OPENAI_API_KEY", - defaultModel: "openai/gpt-4.1", - }, - { - id: "zai-coding-plan", - name: "Z.AI Coding Plan", - description: "GLM coding models (glm-4.7, glm-5, glm-4.5-air)", - placeholder: "...", - envVar: "ZAI_CODING_PLAN_API_KEY", - defaultModel: "zai-coding-plan/glm-5", - }, - { - id: "zhipu", - name: "Z.ai (GLM)", - description: "GLM models (GLM-4, GLM-4-Flash)", - placeholder: "...", - envVar: "ZHIPU_API_KEY", - defaultModel: "zhipu/glm-4-plus", - }, - { - id: "groq", - name: "Groq", - description: "Fast inference for Llama, Mixtral models", - placeholder: "gsk_...", - envVar: "GROQ_API_KEY", - defaultModel: "groq/llama-3.3-70b-versatile", - }, - { - id: "together", - name: "Together AI", - description: "Wide model selection with competitive pricing", - placeholder: "...", - envVar: "TOGETHER_API_KEY", - defaultModel: "together/meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", - }, - { - id: "fireworks", - name: "Fireworks AI", - description: "Fast inference for popular OSS models", - placeholder: "...", - envVar: "FIREWORKS_API_KEY", - defaultModel: "fireworks/accounts/fireworks/models/llama-v3p3-70b-instruct", - }, - { - id: "deepseek", - name: "DeepSeek", - description: "DeepSeek Chat and Reasoner models", - placeholder: "sk-...", - envVar: "DEEPSEEK_API_KEY", - defaultModel: "deepseek/deepseek-chat", - }, - { - id: "xai", - name: "xAI", - description: "Grok models", - placeholder: "xai-...", - envVar: "XAI_API_KEY", - defaultModel: "xai/grok-2-latest", - }, - { - id: "mistral", - name: "Mistral AI", - description: "Mistral Large, Small, Codestral models", - placeholder: "...", - envVar: "MISTRAL_API_KEY", - defaultModel: "mistral/mistral-large-latest", - }, - { - id: "gemini", - name: "Google Gemini", - description: "Google Gemini experimental and production models", - placeholder: "AIza...", - envVar: "GEMINI_API_KEY", - defaultModel: "gemini/gemini-2.5-flash", - }, - { - id: "nvidia", - name: "NVIDIA NIM", - description: "NVIDIA-hosted models via NIM API", - placeholder: "nvapi-...", - envVar: "NVIDIA_API_KEY", - defaultModel: "nvidia/meta/llama-3.1-405b-instruct", - }, - { - id: "minimax", - name: "MiniMax", - description: "MiniMax (Anthropic message format)", - placeholder: "sk-...", - envVar: "MINIMAX_API_KEY", - defaultModel: "minimax/MiniMax-M2.5", - }, - { - id: "minimax-cn", - name: "MiniMax CN", - description: "MiniMax China (Anthropic message format)", - placeholder: "sk-...", - envVar: "MINIMAX_CN_API_KEY", - defaultModel: "minimax-cn/MiniMax-M2.5", - }, - { - id: "moonshot", - name: "Moonshot AI", - description: "Kimi models (Kimi K2, Kimi K2.5)", - placeholder: "sk-...", - envVar: "MOONSHOT_API_KEY", - defaultModel: "moonshot/kimi-k2.5", - }, - { - id: "github-copilot", - name: "GitHub Copilot", - description: "GitHub Copilot API (uses GitHub PAT for token exchange)", - placeholder: "ghp_... or gh auth token", - envVar: "GITHUB_COPILOT_API_KEY", - defaultModel: "github-copilot/claude-sonnet-4", - }, - { - id: "azure", - name: "Azure OpenAI", - description: "Azure OpenAI Service with custom deployments", - placeholder: "Azure API key (alphanumeric string)", - envVar: "AZURE_API_KEY", - defaultModel: "azure/gpt-4o", - }, - { - id: "ollama", - name: "Ollama", - description: "Local or remote Ollama API endpoint", - placeholder: "http://localhost:11434", - envVar: "OLLAMA_BASE_URL", - defaultModel: "ollama/llama3.2", +/// The two API dialects a provider can speak. +/// +/// There is no per-vendor list any more. Adding OpenRouter, Groq, or a +/// self-hosted vLLM is the same form with a different base URL, so hardcoding +/// twenty vendors bought nothing but twenty things to keep current. +export const API_TYPES = [ + { + id: "openai_compatible" as const, + label: "OpenAI-compatible", + description: + "Any endpoint that speaks /chat/completions — LiteLLM, vLLM, Ollama, OpenRouter, OpenAI, TGI.", + baseUrlPlaceholder: "http://localhost:4000/v1", + baseUrlHint: + "Full path prefix. Nothing is appended but the endpoint, so include /v1 if your server expects it.", + keyPlaceholder: "sk-...", + }, + { + id: "anthropic" as const, + label: "Anthropic (native)", + description: + "Anthropic's Messages API. Required for prompt caching, extended thinking, and Claude Pro/Max OAuth.", + baseUrlPlaceholder: "https://api.anthropic.com", + baseUrlHint: "Defaults to https://api.anthropic.com.", + keyPlaceholder: "sk-ant-...", }, -] as const; +] satisfies { + id: string; + label: string; + description: string; + baseUrlPlaceholder: string; + baseUrlHint: string; + keyPlaceholder: string; +}[]; + +export type ApiTypeId = (typeof API_TYPES)[number]["id"]; -export const CHATGPT_OAUTH_DEFAULT_MODEL = "openai-chatgpt/gpt-5.3-codex"; +/// Starting point for a new provider. LiteLLM is the recommended way to reach +/// anything that is not Anthropic, but it is a suggestion, not a dependency. +export const DEFAULT_NEW_PROVIDER = { + id: "litellm", + apiType: "openai_compatible" as ApiTypeId, + baseUrl: "http://localhost:4000/v1", +}; export const PERMISSION_OPTIONS = [ { diff --git a/interface/src/components/settings/index.ts b/interface/src/components/settings/index.ts index ec98e9a78..fb4e5fdc0 100644 --- a/interface/src/components/settings/index.ts +++ b/interface/src/components/settings/index.ts @@ -10,13 +10,12 @@ export {UpdatesSection} from "./UpdatesSection"; export {ChangelogSection} from "./ChangelogSection"; export {ConfigFileSection} from "./ConfigFileSection"; export {ProviderCard} from "./ProviderCard"; -export {ChatGptOAuthDialog} from "./ChatGptOAuthDialog"; -export {SECTIONS, PROVIDERS, CHATGPT_OAUTH_DEFAULT_MODEL, PERMISSION_OPTIONS} from "./constants"; +export {SECTIONS, API_TYPES, DEFAULT_NEW_PROVIDER, PERMISSION_OPTIONS} from "./constants"; +export type {ApiTypeId} from "./constants"; export type { SectionId, Platform, GlobalSettingsSectionProps, ChangelogRelease, ProviderCardProps, - ChatGptOAuthDialogProps, } from "./types"; diff --git a/interface/src/components/settings/types.ts b/interface/src/components/settings/types.ts index 0f4d29ba0..b15a4597e 100644 --- a/interface/src/components/settings/types.ts +++ b/interface/src/components/settings/types.ts @@ -35,27 +35,14 @@ export interface ChangelogRelease { } export interface ProviderCardProps { + /** Provider id — the prefix in `provider/model` routing strings. */ provider: string; - name: string; - description: string; - configured: boolean; - defaultModel: string; + /** `"anthropic"` or `"openai_compatible"`. */ + apiType: string; + baseUrl: string; + displayName?: string | null; + hasKey: boolean; onEdit: () => void; onRemove: () => void; removing: boolean; - actionLabel?: string; - showRemove?: boolean; -} - -export interface ChatGptOAuthDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - isRequesting: boolean; - isPolling: boolean; - message: {text: string; type: "success" | "error"} | null; - deviceCodeInfo: {userCode: string; verificationUrl: string} | null; - deviceCodeCopied: boolean; - onCopyDeviceCode: () => void; - onOpenDeviceLogin: () => void; - onRestart: () => void; } diff --git a/interface/src/components/skills/RegistrySkillRow.tsx b/interface/src/components/skills/RegistrySkillRow.tsx index 341bbc8f2..4277158ac 100644 --- a/interface/src/components/skills/RegistrySkillRow.tsx +++ b/interface/src/components/skills/RegistrySkillRow.tsx @@ -63,7 +63,7 @@ export function RegistrySkillRow({ className={cx( "group shrink-0 rounded-md p-1.5 transition-colors", isInstalled - ? "text-green-400 hover:text-red-400" + ? "text-status-success hover:text-status-error" : "text-ink-faint hover:text-accent", )} title={isInstalled ? "Remove" : "Install"} diff --git a/interface/src/components/skills/SkillInspector.tsx b/interface/src/components/skills/SkillInspector.tsx index 0184d49ac..2ad368eca 100644 --- a/interface/src/components/skills/SkillInspector.tsx +++ b/interface/src/components/skills/SkillInspector.tsx @@ -166,7 +166,7 @@ export function SkillInspector({ size="sm" onClick={() => onRemove(selected.skill.name)} disabled={isRemoving && removingName === selected.skill.name} - className="w-full text-red-400 hover:text-red-400" + className="w-full text-status-error hover:text-status-error" > {isRemoving && removingName === selected.skill.name diff --git a/interface/src/components/skills/SkillsDirectory.tsx b/interface/src/components/skills/SkillsDirectory.tsx index 36f31ab7d..ddcea83fa 100644 --- a/interface/src/components/skills/SkillsDirectory.tsx +++ b/interface/src/components/skills/SkillsDirectory.tsx @@ -188,12 +188,12 @@ export function SkillsDirectory({ {githubInstallMutation.isError && ( -

+

Failed to install. Check the repository format.

)} {githubInstallMutation.isSuccess && ( -

+

Installed: {githubInstallMutation.data.installed.join(", ")}

)} diff --git a/interface/src/components/tasks/BlockKindChip.tsx b/interface/src/components/tasks/BlockKindChip.tsx new file mode 100644 index 000000000..79230916c --- /dev/null +++ b/interface/src/components/tasks/BlockKindChip.tsx @@ -0,0 +1,84 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faArrowRightToBracket, + faHourglassHalf, + faKey, + faRotate, +} from "@fortawesome/free-solid-svg-icons"; +import type { BlockKind } from "@/api/client"; + +/** + * Why a task is parked, and — more usefully — whether it is waiting on the + * system or on you. + * + * The two sticky kinds are styled as demanding attention because nothing will + * move them until a human acts. The two automatic kinds are muted: they clear + * themselves, and dressing them up as problems would train people to ignore the + * ones that are. + */ +const STYLES: Record< + BlockKind, + { label: string; icon: typeof faKey; className: string; actionable: boolean } +> = { + // Deliberately not styled like the sticky kinds it sits beside. A decision + // waiting on a person is the pipeline doing exactly what it was built to + // do, not a fault someone must repair — and dressing it as trouble is how + // a board teaches people to ignore the entries that are. + awaiting_decision: { + label: "Awaiting a decision", + icon: faKey, + className: "border-status-info/40 bg-status-info/10 text-status-info", + actionable: true, + }, + needs_input: { + label: "Needs input", + icon: faArrowRightToBracket, + className: "border-status-warning/40 bg-status-warning/10 text-status-warning", + actionable: true, + }, + capability: { + label: "Missing access", + icon: faKey, + className: "border-status-error/40 bg-status-error/10 text-status-error", + actionable: true, + }, + dependency: { + label: "Waiting upstream", + icon: faHourglassHalf, + className: "border-app-line bg-app-box/60 text-ink-faint", + actionable: false, + }, + transient: { + label: "Retrying", + icon: faRotate, + className: "border-app-line bg-app-box/60 text-ink-dull", + actionable: false, + }, +}; + +export interface BlockKindChipProps { + kind?: BlockKind | null; + /** Shown on hover — usually the server's `block_reason`. */ + reason?: string | null; +} + +export function BlockKindChip({ kind, reason }: BlockKindChipProps) { + if (!kind) return null; + const style = STYLES[kind]; + if (!style) return null; + + return ( + + + {style.label} + + ); +} + +/** Whether this block is one only a human can clear. */ +export function isActionableBlock(kind?: BlockKind | null): boolean { + return kind ? (STYLES[kind]?.actionable ?? false) : false; +} diff --git a/interface/src/components/tasks/BlockedBanner.tsx b/interface/src/components/tasks/BlockedBanner.tsx new file mode 100644 index 000000000..ef5a02fa5 --- /dev/null +++ b/interface/src/components/tasks/BlockedBanner.tsx @@ -0,0 +1,73 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faBan } from "@fortawesome/free-solid-svg-icons"; +import { Button } from "@spacedrive/primitives"; +import type { TaskItem } from "@/api/client"; +import { BlockKindChip } from "./BlockKindChip"; + +/** + * States the real status of a blocked task, above the shared detail panel. + * + * That panel comes from `@spacedrive/ai`, which has no `blocked` status and + * crashes when handed one, so the drawer passes it an adapted copy reading + * `pending_approval`. Without this banner the drawer would quietly assert a + * status the task does not have. The adapted label stays — it cannot be + * removed from a component we do not own — but it is no longer the only thing + * the reader sees, and the actions belong to this banner rather than to a + * status control that thinks the task is awaiting approval. + */ +export interface BlockedBannerProps { + task: TaskItem; + onUnblock?: (task: TaskItem) => void; + onRetry?: (task: TaskItem) => void; + busy?: boolean; +} + +export function BlockedBanner({ + task, + onUnblock, + onRetry, + busy, +}: BlockedBannerProps) { + if (task.status !== "blocked") return null; + + // A missing credential is not fixed by running the same task again, so the + // verb follows the kind — the same split the board makes. + const sticky = + task.block_kind === "needs_input" || task.block_kind === "capability"; + + return ( +
+
+ + + Blocked + + +
+ + {task.block_reason && ( +

+ {task.block_reason} +

+ )} + +

+ Not picked up automatically. The panel below labels this task + “pending approval” because it cannot render this state, and + the move buttons are hidden — every way out of blocked runs through + the action here. +

+ + {sticky && onUnblock && ( + + )} + {!sticky && onRetry && ( + + )} +
+ ); +} diff --git a/interface/src/components/tasks/BlockedTasksSection.tsx b/interface/src/components/tasks/BlockedTasksSection.tsx new file mode 100644 index 000000000..f326dcb59 --- /dev/null +++ b/interface/src/components/tasks/BlockedTasksSection.tsx @@ -0,0 +1,181 @@ +import { Badge, Button } from "@spacedrive/primitives"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faBan, + faChevronDown, + faLockOpen, + faRotateRight, +} from "@fortawesome/free-solid-svg-icons"; +import type { TaskEdgeSummary, TaskItem } from "@/api/client"; +import { RepoChip, type BindingNames } from "./RepoChip"; +import { BlockKindChip, isActionableBlock } from "./BlockKindChip"; +import { DependencyBadges } from "./DependencyBadges"; + +/** + * Blocked tasks, rendered locally rather than through `TaskList`. + * + * `@spacedrive/ai` hardcodes `TASK_STATUS_ORDER` to five statuses and drops any + * task whose status isn't in that list: + * + * for (const status of groups) map.set(status, []); + * for (const task of tasks) { + * const bucket = map.get(task.status); + * if (bucket) bucket.push(task); // unknown status => silently dropped + * } + * + * So a `blocked` task handed to `TaskList` disappears from the board entirely. + * Until the design system learns the status, blocked tasks get their own + * section — which is arguably where they belong anyway: blocked means *stuck + * and needing a human*, which is a different queue from backlog's *waiting its + * turn*. + */ +export interface BlockedTasksSectionProps { + tasks: TaskItem[]; + collapsed?: boolean; + onToggle?: () => void; + onRetry?: (task: TaskItem) => void; + onTaskClick?: (task: TaskItem) => void; + activeTaskId?: string | null; + retryingTaskNumber?: number | null; + resolveAgentName?: (agentId: string) => string; + bindingNames?: BindingNames; + /** Edge counts keyed by task number, from the list response. */ + edges?: Map; + /** Release a task a human has resolved. Shown for sticky blocks only. */ + onUnblock?: (task: TaskItem) => void; +} + +export function BlockedTasksSection({ + tasks, + collapsed = false, + onToggle, + onRetry, + onTaskClick, + activeTaskId, + retryingTaskNumber, + resolveAgentName, + bindingNames, + edges, + onUnblock, +}: BlockedTasksSectionProps) { + if (tasks.length === 0) return null; + + return ( +
+ + + {!collapsed && ( +
+ {tasks.map((task) => { + const isRetrying = retryingTaskNumber === task.task_number; + return ( +
+ + + {/* Always visible: on a queue that exists for human attention, + the primary action must not be hidden behind hover. */} + {onUnblock && isActionableBlock(task.block_kind) && ( + + )} + {onRetry && !isActionableBlock(task.block_kind) && ( + + )} +
+ ); + })} +
+ )} +
+ ); +} diff --git a/interface/src/components/tasks/CapabilityChips.tsx b/interface/src/components/tasks/CapabilityChips.tsx new file mode 100644 index 000000000..df7a4c8a9 --- /dev/null +++ b/interface/src/components/tasks/CapabilityChips.tsx @@ -0,0 +1,73 @@ +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faUsersGear} from "@fortawesome/free-solid-svg-icons"; + +/** + * What a pooled task asked for, as chips. + * + * A pooled task has no assignee until somebody claims it, and until this + * existed the board rendered that as an empty line — indistinguishable from a + * task whose agent had been deleted. "Addressed by capability" is a different + * thing from "addressed to nobody" and has to read as one. + * + * `unsatisfied` tints the labels that are the reason nothing can take it, so + * the chip row doubles as the diagnosis on a board where a banner may be + * scrolled away. + */ +export interface CapabilityChipsProps { + requires: readonly string[]; + /** Labels to mark as the problem. Usually the `undeclared` set. */ + unsatisfied?: readonly string[]; + /** Prefix the row with the pool icon and "Requires". */ + labelled?: boolean; + className?: string; +} + +export function CapabilityChips({ + requires, + unsatisfied, + labelled = true, + className, +}: CapabilityChipsProps) { + if (requires.length === 0) { + // A pooled task requiring nothing is claimable by anybody, which is a + // real state the server allows and not the same as a pushed task. + return ( + + + Pooled — requires nothing, any agent may claim it + + ); + } + + const problem = new Set(unsatisfied ?? []); + + return ( + + {labelled && ( + + + Requires + + )} + {requires.map((label) => ( + + {label} + + ))} + + ); +} diff --git a/interface/src/components/tasks/ContractSection.tsx b/interface/src/components/tasks/ContractSection.tsx new file mode 100644 index 000000000..5f7bc208e --- /dev/null +++ b/interface/src/components/tasks/ContractSection.tsx @@ -0,0 +1,793 @@ +import { useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Button } from "@spacedrive/primitives"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faCircleExclamation, + faPen, + faQuoteLeft, + faRightLong, + faXmark, +} from "@fortawesome/free-solid-svg-icons"; +import { + api, + type ContractProblem, + type TaskContractResponse, + type TaskInputBinding, +} from "@/api/client"; + +/** What `PUT /tasks/{n}/bindings/{key}` carries, minus the key in the path. */ +export interface BindingBody { + source_task_number?: number; + source_pointer?: string; + literal_value?: unknown; +} + +export interface ContractSectionProps { + taskNumber: number; + onSelectTask?: (taskNumber: number) => void; +} + +export function ContractSection({ taskNumber, onSelectTask }: ContractSectionProps) { + const queryClient = useQueryClient(); + const { data } = useQuery({ + queryKey: ["task-contract", taskNumber], + queryFn: () => api.getTaskContract(taskNumber), + }); + + const save = useMutation({ + mutationFn: (body: {input_schema?: unknown; output_schema?: unknown}) => + api.setTaskContract(taskNumber, body), + onSuccess: () => + void queryClient.invalidateQueries({queryKey: ["task-contract", taskNumber]}), + }); + + // Both binding calls return the whole contract, freshly resolved — so the + // response is written straight into the cache. Re-fetching would show the + // same thing one round trip later, and a rewired input is the moment you + // most want to see what it now resolves to. + const contractKey = ["task-contract", taskNumber]; + const saveBinding = useMutation({ + mutationFn: ({inputKey, body}: {inputKey: string; body: BindingBody}) => + api.setTaskBinding(taskNumber, inputKey, body), + onSuccess: (next) => queryClient.setQueryData(contractKey, next), + }); + const removeBinding = useMutation({ + mutationFn: (inputKey: string) => api.removeTaskBinding(taskNumber, inputKey), + onSuccess: (next) => queryClient.setQueryData(contractKey, next), + }); + + if (!data) return null; + return ( + save.mutate(body)} + saving={save.isPending} + saveError={save.error instanceof Error ? save.error.message : null} + onSaveBinding={(inputKey, body) => { + saveBinding.reset(); + saveBinding.mutate({inputKey, body}); + }} + onRemoveBinding={(inputKey) => { + removeBinding.reset(); + removeBinding.mutate(inputKey); + }} + bindingBusy={saveBinding.isPending || removeBinding.isPending} + bindingError={ + (saveBinding.error ?? removeBinding.error) instanceof Error + ? ((saveBinding.error ?? removeBinding.error) as Error).message + : null + } + /> + ); +} + +/** Split from the fetching wrapper so it renders against fixtures. */ +export function ContractSectionView({ + data, + onSelectTask, + onSaveSchemas, + saving, + saveError, + onSaveBinding, + onRemoveBinding, + bindingBusy, + bindingError, +}: { + data: TaskContractResponse; + onSelectTask?: (taskNumber: number) => void; + /** Omitted in read-only contexts such as the fixture harness. */ + onSaveSchemas?: (body: {input_schema?: unknown; output_schema?: unknown}) => void; + saving?: boolean; + saveError?: string | null; + onSaveBinding?: (inputKey: string, body: BindingBody) => void; + onRemoveBinding?: (inputKey: string) => void; + bindingBusy?: boolean; + bindingError?: string | null; +}) { + const hasContract = + data.input_schema != null || + data.output_schema != null || + data.bindings.length > 0 || + data.outputs != null; + + // Most tasks declare nothing, and an empty "Contract" heading on every one + // of them would be noise. But a task with no contract is exactly the one + // somebody needs to give a contract to, so the editor still gets a way in. + if (!hasContract && !onSaveSchemas) return null; + + // Which keys the graph currently cannot supply, so each row can say so + // rather than making the reader match a list of problems to a list of rows. + const failedKeys = new Set( + data.problems + .map((problem) => ("input_key" in problem ? problem.input_key : null)) + .filter((key): key is string => key !== null), + ); + + const resolved = (data.resolved_inputs ?? data.inputs ?? {}) as Record< + string, + unknown + >; + + return ( +
+

+ Contract +

+ + {/* Problems first and unmissable: a graph that cannot supply a task's + inputs is the single most common way a hand-built pipeline is + wrong, and it is silent everywhere else. */} + {data.problems.length > 0 && ( +
    + {data.problems.map((problem) => ( +
  • + + {describe(problem)} +
  • + ))} +
+ )} + + {(data.bindings.length > 0 || onSaveBinding) && ( + + )} + + {data.outputs != null ? ( + + ) : ( + data.output_schema != null && ( +
+

+ Outputs +

+

+ Not produced yet. Must match the declared schema. +

+
+ ) + )} + + {data.output_schema != null && ( + + )} + + {onSaveSchemas && ( + + )} +
+ ); +} + +/** + * Declare the shape a task must produce. + * + * Humans define the contract; only a worker writes values into it. Setting an + * output schema here is what turns `task_complete` from "record whatever the + * model said" into a checked submission that is rejected when it does not fit. + * + * The JSON is validated locally before being sent, because the server stores a + * schema it cannot compile and only surfaces the problem later, at the moment + * a task tries to run. + */ +function SchemaEditor({ + inputSchema, + outputSchema, + onSave, + saving, + saveError, +}: { + inputSchema: unknown; + outputSchema: unknown; + onSave: (body: {input_schema?: unknown; output_schema?: unknown}) => void; + saving?: boolean; + saveError?: string | null; +}) { + const [open, setOpen] = useState(false); + const [inputText, setInputText] = useState(() => format(inputSchema)); + const [outputText, setOutputText] = useState(() => format(outputSchema)); + const [localError, setLocalError] = useState(null); + + if (!open) { + return ( + + ); + } + + const submit = () => { + const input = parse(inputText); + const output = parse(outputText); + if (input.error || output.error) { + setLocalError( + input.error + ? `Input schema: ${input.error}` + : `Output schema: ${output.error}`, + ); + return; + } + setLocalError(null); + onSave({input_schema: input.value, output_schema: output.value}); + setOpen(false); + }; + + return ( +
+ + + + {(localError || saveError) && ( +

+ {localError ?? saveError} +

+ )} + +
+ + +
+
+ ); +} + +function SchemaField({ + label, + hint, + value, + onChange, +}: { + label: string; + hint: string; + value: string; + onChange: (next: string) => void; +}) { + return ( +
+ +

{hint}

+