diff --git a/.gitignore b/.gitignore index 2a77479a..b3452621 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ tmp dist ts-dist .turbo +# Web/edge build artifacts (nitro .output, cloudflare .wrangler) — e.g. packages/console, packages/stats +.output +.wrangler +packages/console/app/public/sitemap.xml **/.serena .serena/ **/.omo diff --git a/README.md b/README.md index 616ccd9c..e211230d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

-

AI coding agent with persistent memory, autonomous goals, and a control plane

+

The AI coding agent that remembers, plans, collaborates, and finishes

English | @@ -14,95 +14,94 @@ Enterprise

-

Desktop v1.3

+

Desktop 1.4.1 · DeepAgent Core V4.1

--- -DeepAgent Code is an AI coding agent built on persistent document memory. It keeps [opencode](https://github.com/sst/opencode)'s runtime foundations and adds a control plane so the agent behaves less like a one-shot chat and more like a teammate that remembers your project, sharpens vague asks, and goes deep on hard problems. +DeepAgent Code is an AI coding workspace for work that lasts longer than one prompt. It combines a production coding-agent runtime with durable sessions, connected project memory, live planning, code intelligence, multi-agent collaboration, and human oversight. -The features below start from a real need — something a plain coding agent, opencode included, leaves on the table — and work down to the architecture we built to serve it. +You can ask for a small edit, guide a running task without interrupting it, hand over a migration with objective completion criteria, or bring several specialist agents into a decision. DeepAgent keeps the work coherent across turns, restarts, tools, people, and projects. -## What You Can Do +## One Workspace, Three Ways to Work -### Bring your history over — switch tools without starting from zero +Choose the collaboration style that fits the task: -**The need:** You've built up months of context in another agent — Codex or Claude Code — and switching tools normally means abandoning all of it: the conversations, the accumulated memory, the skills you tuned. That cost alone keeps people on tools they've outgrown. +| Mode | You provide | DeepAgent does | +|---|---|---| +| **Auto** | A request | Defines the objective, designs and plans as needed, then executes end to end | +| **Loop** | A goal | Writes an editable `goal+plan.md` and advances it through plan, execute, verify, and iterate ticks | +| **Design** | Your `goal+plan.md` | Executes your design faithfully without redefining its objective or completion criteria | -**What DeepAgent does:** One-click import of your existing history. Point it at a Codex or Claude Code installation and it hot-imports your chat sessions, memory, and skills straight into DeepAgent — reading each tool's on-disk format, normalizing it, and replaying it into the document graph so imported conversations behave like native ones. Secrets are redacted on the way in, imported projects stay isolated so nothing collides with your active work, and re-running an import converges instead of duplicating. Available from the Settings "Import history" panel, a History view in the sidebar, or the `import-history` CLI command. Migration is a few minutes, not a fresh start. +Autonomy and permission are independent. Use **Read-only**, **Request approval**, or **Full access** without changing the collaboration mode. -### Keep one conversation going indefinitely +## Stay in Control While It Works -**The need:** Long tasks overflow the context window. Most agents respond by truncating history or summarizing everything at a threshold — so mid-task the agent forgets a decision you made an hour ago, or the window fills with stale tool output and quality falls off a cliff. +DeepAgent is built for active collaboration, not fire-and-forget automation. -**What DeepAgent does:** Your conversation is treated as a continuously maintained work state, not a growing chat log. Before every turn, the agent rebuilds a working set — the task anchor, the most recent exchanges verbatim, active file references, and only the older facts that are relevant right now — while the full history is archived durably and stays queryable. The working set is held to a hard fraction of the model window, so there's always room for the model to actually think and respond. You just keep talking; the agent keeps focus. +- **Live steering:** send new guidance while a model turn or tool is running. The message is durably admitted and absorbed at the next safe provider-turn boundary without aborting in-flight work. +- **Goal steering:** guidance sent to an active goal is folded into the next tick, preserving the current tool and plan state. +- **Hot plan editing:** edit a running or paused goal. Stable step IDs, evidence, completed work, and the new plan version carry into the next tick. +- **Explicit queueing:** queue a future activity when the instruction should begin after the current activity instead of changing it. +- **Pause, resume, take over, or roll back:** every long-running workflow has a human control path and a durable audit trail. -### Switch windows, fork freely, never lose memory +## Memory You Can Inspect and Govern -**The need:** You want to try two approaches, or hand the work to a fresh conversation, without starting from amnesia and without polluting the original thread. +DeepAgent does not hide memory in an opaque prompt. Project state lives in typed, versioned documents with provenance, confidence, scope, status, and links. -**What DeepAgent does:** Fork any conversation from a chosen message. The fork opens carrying the parent's memory up to that point, shows a full-width "derived from" marker at the top of its transcript, and nests folder-style under its origin in the session tree (subagents and forks alike, up to three levels deep). Knowledge flows up a scope hierarchy — what one session learns can be promoted to the whole project, and cross-project preferences live at the user-global layer — so switching windows is a clean handoff, not a reset. +- Session-private working context stays with the current conversation. +- Project-shared facts and decisions follow the repository. +- User-global preferences can travel across projects. +- Built-in skills and domain packs remain versioned system knowledge. +- Sealed evaluator material stays audit-only and never enters model context. -### Pick how much autonomy you want, per task +Learning follows a governed lifecycle: evidence creates a candidate, isolated review or a human decision changes its status, and regression/ablation gates publish a reproducible knowledge snapshot. Rejection reasons remain durable so discarded patterns are not silently relearned. -**The need:** Some asks want a fast answer in your exact words; others want the agent to plan first, or to run on its own until the job is done. One fixed behavior can't serve all three. +The **Repo & Wiki** view makes this system readable. Browse knowledge and execution archives, search across the repository, follow docs-to-code links, inspect lineage, and promote useful run evidence into governed knowledge. -**What DeepAgent does:** Three modes on the composer. **Auto** decides how to plan and act on your request. **Design** explores the problem and proposes a design before building anything. **Loop** turns a request into a supervised goal the agent works toward autonomously — iterating plan → execute → verify until the criteria are met — while you stay in control. You choose the autonomy level per task, not once for the whole tool. +## Connected Context, Not a Larger Prompt -### Choose how much the agent drives — and who writes the plan +DeepAgent connects four views of the project: -**The need:** Sometimes you want the agent to just take a request and run; sometimes you want to steer it with a plan you control; sometimes you've already written the plan and just want it executed faithfully. +1. **Code graph:** files, symbols, imports, calls, diagnostics, and references. +2. **Knowledge graph:** strategies, methodologies, facts, skills, and failure dossiers. +3. **Project memory:** decisions, constraints, environment facts, and learned conventions. +4. **Document graph:** plans, designs, worklogs, evaluations, run context, and evidence. -**What DeepAgent does:** Three collaboration modes on the composer, picked from a single selector. **Auto** — the agent sets the objective, designs and plans as needed, and executes to completion. **Loop** — you describe the goal, the agent writes a `goal+plan.md` you can edit, then a supervised loop drives it to completion (plan → execute → verify per tick, with hard budget/step ceilings and objective completion checks). **Design** — you author `goal+plan.md` yourself and the agent executes your plan faithfully without redefining the goal. Orthogonal to mode, a permission control offers three presets — **Read-only**, **Request approval** (default), **Full access** — so autonomy and approval are separate, explicit choices. +The Session V2 runner assembles context from explicit sources under a durable Context Epoch. It selects linked evidence within budget, records why each reference was admitted or rejected, and preserves the current goal, constraints, decisions, open questions, next steps, and relevant files during compaction. -### Get a second opinion before high-risk decisions +Prompt caching remains effective across long runs: stable system instructions stay byte-stable, while plans, steering, budgets, round results, and other volatile state are appended in a dedicated tail block. -**The need:** Some decisions — a breaking migration, a security-sensitive change, an architecture call — deserve more than one confident pass agreeing with itself. +## Built for Difficult Work -**What DeepAgent does:** Convene an **Expert Panel** from the composer. Differentiated expert lenses (correctness, security, performance, architecture, repro) review the same frozen question independently, debate anonymously, and a deterministic (non-LLM) arbiter aggregates a verdict — with minority opinions preserved and a fail-closed bias toward escalating to you when the panel can't safely agree. +### AI IDE -### Read and govern what the agent knows +Query code by symbol and intent instead of guessing file locations. DeepAgent combines LSP definitions, references, call chains, type information, diagnostics, rename previews, and cross-file evidence. Unsaved editor buffers participate in LSP updates, so analysis follows the code you are actually editing. -**The need:** Persistent memory is only trustworthy if you can see it and correct it. +### Domain packs -**What DeepAgent does:** A **Repo & Wiki** view projects the four graphs into human-readable pages — browse and full-text-search the agent's knowledge, follow docs↔code cross-links, and edit governable Knowledge/Memory pages through the same evidence-gate the agent uses (Documents and Code stay read-only). A separate governance view lists learned facts grouped by project and global scope, so you approve what becomes durable. +Composable domain packs add language, framework, platform, hardware, business, and risk expertise without hardcoding it into the core. Packs activate from the problem profile, resolve conflicts with stricter-policy-wins semantics, and are snapshot-locked for reproducible runs. -### Go deep on genuinely hard problems +### Specialist agents and Expert Panel -**The need:** Complex work — an architecture decision, a tricky migration, a subtle bug — needs more than a single confident pass. It needs research, a second opinion, and someone actively trying to poke holes. +DeepAgent can partition independent work across bounded, isolated workers. Write-capable subagents receive dedicated worktrees, return compact summaries and artifact references, and leave their full transcripts available for inspection. -**What DeepAgent does:** At higher work strengths the primary agent decomposes the task, fans it out to focused subagents that research modules in parallel, synthesizes their findings, and then runs independent reviewers whose job is to *break* the plan rather than agree with it. Fan-out is bounded by a configurable concurrency ceiling, and live subagents surface in a session side panel and inline in the transcript so you can watch and jump into any of them. +For high-risk decisions, convene an **Expert Panel**. Correctness, security, performance, architecture, and reproducibility lenses review the same frozen question, debate anonymously for up to three rounds, and feed a deterministic arbiter that preserves minority opinions and fails closed to human review. -### Set a goal and let it run — supervised, not unsupervised +### Team and agent messaging -**The need:** Some work is a long haul — a migration, a green-the-suite push, a multi-step feature. You want to hand it off and walk away, but "walk away" can't mean "lose control." +Project IM brings people and agents into the same thread. Mention an agent to start a scoped run with project context, stream its progress, inspect its artifacts, and keep the answer attached to the conversation that requested it. -**What DeepAgent does:** Loop mode drives an autonomous goal loop against an objectively decidable finish line (tests pass, no diagnostics, reviewer clean, plan complete). It runs plan → execute → verify → iterate in the background, with hard ceilings on ticks, tokens, wall-clock, and cost so it can never run away. A status bar shows live progress; you can hot-edit the plan mid-run, pause and resume from exactly where it stopped, or take over — a takeover pauses autonomy escalation and hands control back to you. A goal with steps that need a human never reports "done"; it routes to you. Autonomy is a dial you hold, not a switch you flip and hope. +## DeepAgent Core V4.1 -### Get a second opinion that actually argues +V4.1 brings the complete DeepAgent control plane together: -**The need:** For a high-stakes decision, one confident answer isn't enough — you want independent experts who see the same evidence, disagree, and defend their positions. - -**What DeepAgent does:** Convene an Expert Panel on the current conversation from the composer. Pick single-round for a quick multi-lens review, or multi-round for a real debate: panelists (correctness, security, design, …) each render a verdict, then see each other's *anonymized* opinions and revise across up to three rounds — identity stripped so nobody anchors on "the security expert said." An arbiter synthesizes the surviving verdict. Fan-out and rounds are bounded, and every opinion (including the losers') is archived. The panel convenes on demand, or a running goal loop can convene it at high-risk decision points. - -### Chat with your team and your agents in one place - -**The need:** Coordinating with teammates and driving agents usually happens in two different tools. - -**What DeepAgent does:** A per-project group chat lives in the session side panel. @mention an agent as a chat member and it runs the full agent loop — query code, generate, fix — pulling project knowledge and recent messages for context, then replies inline with live progress streaming. - -## How It Works - -Each capability above is served by a control-plane primitive underneath. These are the parts a plain runtime doesn't have. - -**Four-graph unification** — Code, knowledge, project memory, and the document graph are unified into one typed, bidirectionally-linked store. When the agent pulls context, a change to a symbol surfaces the design decisions, past diagnoses, and knowledge actually linked to it — connected context, not four disconnected keyword searches. - -**Domain packs** — 140+ composable knowledge packages spanning languages, frameworks, platforms (cloud, Kubernetes, CI), hardware, and business/risk domains (security, privacy, compliance). Each pack bundles typed documents (strategies, methodologies, knowledge, skills, failure dossiers) with detectors that auto-activate the right packs for your task; conflicts resolve stricter-policy-wins, and the active set is version-locked so a run is reproducible. Core stays domain-neutral — expertise is data on disk, not hardcoded. - -**Tiered knowledge invocation** — A monotonic strength ladder (`general → high → xhigh → max → ultra`) gates how much control-plane machinery engages. `general` stays close to the plain runtime — fast and cheap. Higher rungs progressively unlock durable knowledge, project handoff summaries, heavier strategy/methodology tiers, and multi-agent orchestration. You pay for depth only when you dial it up. - -**Self-learning** — After work lands, the agent proposes candidate knowledge, facts, and methodologies. Promotion is evidence-gated (a test passed, a diagnostic cleared, a validation confirmed) and user-controllable — durable knowledge is carried over deliberately, not silently guessed. Session-stable conclusions consolidate into project memory over time, so the next session starts smarter about *your* codebase. - -**Supervised autonomy** — The goal loop, expert panel, and multi-agent fan-out run on an event-driven substrate with the guardrails autonomy needs: hard budget ceilings (ticks/tokens/wall-clock/cost), a stall detector that stops rather than spins, layered permission and safety gates that fail closed, human takeover that pauses escalation, and a full audit trail written back to the document graph. An Agent Dashboard surfaces task success rate, conflicts, and dead-letter events. Autonomy is bounded and observable by construction — never a black box you can't stop. +- **Durable Session V2:** prompt admission is persisted before execution; exact retries do not duplicate user intent; same-session wakes coalesce safely. +- **One provider-turn contract:** native and AI SDK providers share the same budget, permission, artifact, audit, learning, and close lifecycle. +- **Single durable truth:** DocumentStore owns documents, plans, learning candidates, governance state, and version conflicts through atomic, recoverable writes. +- **Event-driven Agent OS:** durable events, priority routing, backpressure, worker claims, leases, handoffs, retries, dead-letter recovery, and distributed placement coordinate autonomous work. +- **Consumer-driven goals:** `goal.tick.requested` claims and executes one idempotent tick, records facts, and schedules the next tick only when the durable goal remains eligible. +- **Human oversight:** approval queues, trace correlation, takeover, rollback, Wiki archives, notifications, and organization/workspace isolation remain part of the execution path. +- **Secure integrations:** MCP credentials use environment references or native OS secret storage; catalog risk, runtime permissions, trusted sources, and tool capability checks fail closed. ## Installation @@ -150,7 +149,7 @@ On your next session, when you ask to add rate limiting elsewhere, the agent alr ## Architecture -``` +```text ┌─────────────────────────────────────────────────────────────┐ │ Control Plane (DeepAgent additions) │ │ • Four-graph unified store (code + knowledge + memory + doc)│ @@ -179,24 +178,56 @@ On your next session, when you ask to add rate limiting elsewhere, the agent alr └─────────────────────────────────────────────────────────────┘ ``` -DeepAgent's control plane operates at provider-turn boundaries: it selects context before each model call and writes evidence back into the document graph afterward. It does not replace opencode's runtime — it layers on top. +The full architecture and its invariants are documented in [Architecture & Design](design/README.md). + +## Build From Source + +DeepAgent Code uses Bun 1.3.14. + +```bash +git clone https://github.com/deepagent-ltd/deepagent-code.git +cd deepagent-code +bun install +``` + +Start the Desktop app: + +```bash +bun run dev:desktop +``` + +Start the terminal experience: + +```bash +bun run dev +``` + +Run a one-shot task: + +```bash +bun run --cwd packages/deepagent-code dev run "add rate limiting to /api/users" +``` + +Import existing Codex or Claude Code history: + +```bash +bun run --cwd packages/deepagent-code dev import-history --from codex --dry-run +``` ## Documentation -- [Architecture & Design](design/README.md) — Control plane, code intelligence, MCP security model -- [Security Policy](SECURITY.md) — Vulnerability reporting, known limitations -- [Privacy Policy](PRIVACY.md) — Data handling and storage -- [Contributing](CONTRIBUTING.md) — Development setup and guidelines -- [Changelog](CHANGELOG.md) — Release history +- [Architecture & Design](design/README.md) +- [Security Policy](SECURITY.md) +- [Privacy Policy](PRIVACY.md) +- [Contributing](CONTRIBUTING.md) +- [Changelog](CHANGELOG.md) ## License & Attribution -DeepAgent Code is licensed under **AGPL-3.0-or-later**. If you modify and run it as a network service, you must make your source code available to users. +DeepAgent Code is licensed under **AGPL-3.0-or-later**. If you modify and run it as a network service, you must make the corresponding source available to its users. -This project is derived from [opencode](https://github.com/sst/opencode) (MIT License). See [NOTICE](NOTICE) for the upstream license and attribution. No endorsement by opencode or its contributors is implied. +DeepAgent Code is derived from [opencode](https://github.com/sst/opencode) under the MIT License. See [NOTICE](NOTICE) for upstream attribution. No endorsement by opencode or its contributors is implied. --- -

- Built by DeepAgent -

+

Built by DeepAgent

diff --git a/README.zh.md b/README.zh.md index e59e34c5..efe97275 100644 --- a/README.zh.md +++ b/README.zh.md @@ -6,7 +6,7 @@

-

具备持久记忆、自主目标与控制平面的 AI 编程智能体

+

会记忆、会规划、会协作,也能把工作真正做完的 AI 编程智能体

English | @@ -14,95 +14,94 @@ Enterprise 版本

-

桌面版 v1.3

+

桌面版 1.4.1 · DeepAgent Core V4.1

--- -DeepAgent Code 是一个构建在持久文档记忆之上的 AI 编程智能体。它保留了 [opencode](https://github.com/sst/opencode) 的运行时基座,并在其上叠加了一层控制平面——让智能体不再像一次性的对话工具,而更像一位记得住你项目、会替你打磨模糊需求、能对硬骨头深挖到底的队友。 +DeepAgent Code 是一套面向长期工作的 AI 编程工作区。它把生产级编程智能体运行时与持久会话、相互连通的项目记忆、实时计划、代码智能、多智能体协作和人类监督组合在一起。 -下面的每一项特性,都从一个真实的需求出发——一个普通编程智能体(包括 opencode)尚未满足的需求——再向下讲到我们为满足它而构建的底层架构。 +你可以让它完成一次小改动,在任务运行中继续补充指令,把一场迁移交给带客观完成判据的目标回路,或者召集多位专家共同审阅一项决策。DeepAgent 会让工作在多轮对话、进程重启、工具调用、团队成员和不同项目之间保持连贯。 -## 你可以做什么 +## 一个工作区,三种协作方式 -### 把历史一起带过来——换工具不必从零开始 +按任务选择最合适的协作方式: -**需求:** 你已经在另一个智能体里——Codex 或 Claude Code——积累了几个月的上下文,而换工具通常意味着把这一切统统丢下:那些对话、沉淀下来的记忆、你调好的技能。单是这份代价,就足以把人留在早该告别的工具上。 +| 模式 | 你提供 | DeepAgent 负责 | +|---|---|---| +| **自动(Auto)** | 一项需求 | 自行明确目标,按需设计和规划,再端到端执行 | +| **循环(Loop)** | 一个目标 | 生成可编辑的 `goal+plan.md`,按计划、执行、校验、迭代逐 tick 推进 | +| **设计(Design)** | 你编写的 `goal+plan.md` | 忠实执行你的设计,不重新定义目标或完成判据 | -**DeepAgent 的做法:** 一键导入你已有的历史。把它指向一个 Codex 或 Claude Code 的安装目录,它就会把你的会话、记忆和技能热导入 DeepAgent——读取各工具的本地格式,归一化后重放进文档图,让导入的对话表现得和原生对话一样。导入过程中会对密钥做脱敏,导入的项目彼此隔离、不会和你正在进行的工作冲突,重复导入会收敛而不是产生重复。入口有三处:设置里的"导入历史"面板、侧栏的历史视图,以及 `import-history` 命令行。迁移只需几分钟,而不是推倒重来。 +自主程度和权限相互独立。你可以在不改变协作模式的情况下选择**只读**、**请求批准**或**完全访问**。 -### 一直对话下去,不必换新 +## 它在工作,你仍然掌控全局 -**需求:** 长任务会撑爆上下文窗口。多数智能体的应对方式是截断历史,或在阈值处一刀切地整体摘要——于是任务进行到一半,智能体忘了你一小时前拍的板,或者窗口被陈旧的工具输出塞满,质量断崖式下跌。 +DeepAgent 为持续协作而设计,不是一个发出指令后只能等待的黑盒。 -**DeepAgent 的做法:** 你的对话被当作一份持续维护的工作状态,而不是不断变长的聊天记录。每一轮开始前,智能体都会重建一份工作面——任务锚点、最近若干轮的原文、活跃的文件引用,以及此刻真正相关的旧事实——同时完整历史被持久归档、随时可查。工作面被严格控制在模型窗口的一个固定比例以内,永远给模型留足思考和作答的余地。你只管一直说,智能体替你保持专注。 +- **实时 Steering:** 模型或工具仍在运行时继续发送指导。消息会先持久化,再在下一个安全的供应商轮次边界被吸收,不会中断在飞工作。 +- **Goal Steering:** 发给活跃目标的指导会进入下一个 tick,同时保留当前工具状态和计划状态。 +- **运行中计划热编辑:** 编辑正在运行或已暂停的目标。稳定的步骤 ID、证据、已完成工作和新计划版本会一起进入下一 tick。 +- **显式排队:** 当一条指令应该在当前 activity 结束后独立开始时,把它放入未来队列,而不是改变当前工作。 +- **暂停、恢复、接管或回滚:** 每个长跑流程都有清晰的人类控制路径和持久审计记录。 -### 随手分叉、切换窗口,记忆不丢 +## 看得见、管得住的记忆 -**需求:** 你想同时试两条思路,或把工作交给一个全新的对话,却不想从头失忆重来,也不想把原来的线程搅乱。 +DeepAgent 不会把记忆藏在不可见的提示词里。项目状态保存在带类型、版本、来源、置信度、作用域、状态和链接的文档中。 -**DeepAgent 的做法:** 从任意一条消息分叉当前对话。分叉出的新对话会继承父对话到该点为止的记忆,在时间线顶部显示一条贯穿窗口的"从对话派生"分割线,并像文件夹一样嵌套挂在来源对话之下(子 agent 与分叉同理,最多三层深)。知识沿作用域层级向上流动——单个会话学到的东西可提升到整个项目,跨项目的偏好则沉淀在用户全局层——所以换窗口是一次干净的交接,而非一次清零重来。 +- 会话私有工作上下文只属于当前对话。 +- 项目共享事实与决策跟随代码仓库。 +- 用户全局偏好可以跨项目使用。 +- 内置技能与领域包保持系统级版本管理。 +- 封存的评测材料仅用于审计,永不进入模型上下文。 -### 自主程度,按任务自己挑 +学习遵循可治理的生命周期:证据生成候选,隔离审阅或人工决策改变候选状态,回归与消融门发布可复现的知识快照。拒绝理由会持久保存,因此被淘汰的模式不会在后台被悄悄重新学习。 -**需求:** 有些提问想要一个快速、原话作答;有些希望智能体先规划;还有些希望它自己跑到把活干完为止。一种固定行为伺候不了这三种。 +**仓库与百科(Repo & Wiki)** 让这套系统对人可读。你可以浏览知识与执行档案、搜索整个仓库、沿文档到代码的链接探索上下文、检查来源链,并把有价值的运行证据升格为受治理知识。 -**DeepAgent 的做法:** 输入框上有三种模式。**自动(Auto)** 由智能体决定如何规划并执行你的请求。**设计(Design)** 先探索问题、给出设计方案,再动手构建。**循环(Loop)** 把请求变成一个受监督的目标,智能体朝它自主推进——不断地"规划 → 执行 → 校验"直到满足完成判据——而掌控权始终在你手里。自主程度按任务来选,而不是为整个工具一次性定死。 +## 相互连接的上下文,而不是更长的提示词 -### 选择智能体驱动的方式——以及由谁来写计划 +DeepAgent 把项目的四个视图连接在一起: -**需求:** 有时你希望智能体拿到需求就自己跑;有时你想用一份由自己掌控的计划来引导它;有时你已经把计划写好了,只需要它忠实执行。 +1. **代码图:** 文件、符号、导入、调用、诊断与引用。 +2. **知识图:** 策略、方法论、事实、技能与故障档案。 +3. **项目记忆:** 决策、约束、环境事实与已学习的项目约定。 +4. **文档图:** 计划、设计、工作日志、评测、运行上下文与证据。 -**DeepAgent 的做法:** 输入框上的协作模式选择器提供三档。**自动** ——智能体自行定目标、做计划、执行到完成。**目标** ——你说明需求,智能体生成一份你可以编辑的 `goal+plan.md`,再由监督循环驱动执行(计划→执行→验证逐步推进,有硬性预算/步数上限和客观完成判据)。**设计** ——你自己写好 `goal+plan.md`,智能体读取并忠实执行你的方案,不会重新定义目标。与协作模式正交,权限控制提供三个预设——**只读**、**请求批准**(默认)、**完全访问**——自主程度与审批方式是两个独立的、明确的选项。 +Session V2 运行器在持久 Context Epoch 下从明确的 Context Source 装配上下文。它在预算内选择相互关联的证据,记录每条引用为什么被准入或拒绝,并在压缩时保留当前目标、约束、决策、开放问题、后续步骤与相关文件。 -### 在高风险决策前听一次会诊 +长跑任务也能持续命中提示词缓存:稳定 system 指令保持字节级稳定,计划、Steering、预算、轮次结果等易变状态只追加到独立的尾部区块。 -**需求:** 有些决策——一次破坏性迁移、一个安全敏感的改动、一个架构抉择——值得不止一次自信的作答自我认同。 +## 为困难工作而生 -**DeepAgent 的做法:** 在输入框旁召集**专家团**。差异化的专家视角(正确性、安全、性能、架构、可复现)对同一个冻结问题各自独立审阅,匿名辩论,再由一个确定性(非 LLM)的仲裁者聚合裁定——保留少数派意见,并在专家团无法安全达成一致时偏向将决策权升级给你。 +### AI IDE -### 读懂并治理智能体所知道的 +按符号和意图查询代码,不再猜文件位置。DeepAgent 组合 LSP 定义、引用、调用链、类型信息、诊断、重命名预览与跨文件证据。未保存的编辑器 buffer 也会实时进入 LSP,因此分析看到的是你正在编辑的代码。 -**需求:** 持久记忆只有在你能看见、能纠错的情况下才值得信任。 +### 领域包 -**DeepAgent 的做法:** **仓库与百科**视图把四张图投影成人类可读的页面——浏览并全文检索智能体的知识,跟随文档↔代码的交叉链接,并通过与智能体相同的证据门编辑可治理的知识/记忆页面(文档与代码页面只读)。单独的知识治理视图按项目和全局分组列出已学事实,由你审批哪些成为永久知识。 +可组合的领域包提供语言、框架、平台、硬件、业务与风险知识,而不把专业逻辑硬编码进内核。领域包根据问题画像自动激活,以“更严格策略优先”解决冲突,并锁定快照以保证运行可复现。 -### 对真正的难题深挖到底 +### 专业子智能体与 Expert Panel -**需求:** 复杂的工作——一个架构决策、一次棘手的迁移、一个隐蔽的 bug——需要的不止一次自信的单程作答。它需要调研、需要第二意见、需要有人主动来挑刺。 +DeepAgent 可以把独立工作拆分给数量有界、相互隔离的 Worker。具备写权限的子智能体获得独立 worktree,只向父会话返回紧凑摘要和工件引用,完整执行记录仍可随时查看。 -**DeepAgent 的做法:** 在更高的工作强度下,主智能体会拆解任务,扇出给专注的子 agent 并行调研各个模块,综合它们的发现,再运行独立的审阅者——审阅者的职责是"击破"方案,而不是附和。扇出受可配置的并发上限约束;运行中的子 agent 会出现在会话侧栏面板和时间线内联卡片里,你可以旁观并随时跳进任意一个。 +高风险决策可以召集 **Expert Panel**。正确性、安全、性能、架构与可复现性等专家视角审阅同一个冻结问题,进行最多三轮匿名辩论,再由确定性仲裁器生成裁定。少数派意见会被保留,无法安全达成一致时会失败关闭并交给人类。 -### 定个目标让它自己跑——是受监督,而非放养 +### 团队与智能体消息 -**需求:** 有些工作是场持久战——一次迁移、一轮把测试全刷绿、一个多步骤的特性。你想把它交出去然后走开,但"走开"不能等于"失控"。 +项目 IM 把团队成员和智能体放进同一条讨论。@ 某个智能体即可启动有明确作用域的运行,使用项目上下文、流式展示进度、关联执行工件,并把答案留在发起任务的对话里。 -**DeepAgent 的做法:** 循环模式驱动一个自主目标回路,朝着一条可客观判定的终点线推进(测试通过、无诊断、审阅无异议、计划完成)。它在后台跑"规划 → 执行 → 校验 → 迭代",并对轮次、令牌、墙钟时间和成本设有硬上限,绝不会失控狂奔。状态条实时显示进度;你可以在运行中热编辑计划、暂停后从中断处精确恢复,或直接接管——接管会暂停自主升级、把控制权交还给你。一个包含需要人工处理步骤的目标绝不会谎报"完成",而是转交给你。自主是你握在手里的旋钮,不是拨一下就听天由命的开关。 +## DeepAgent Core V4.1 -### 要一个真会争论的第二意见 +V4.1 把完整的 DeepAgent 控制平面汇聚在一起: -**需求:** 面对高风险决策,一个自信的答案不够——你想要一批独立专家,看着同样的证据,各持己见、各自辩护。 - -**DeepAgent 的做法:** 从输入框就当前对话召集专家团。选单轮做一次快速的多视角评审,或选多轮进行真正的辩论:各位专家(正确性、安全、设计……)先各自给出裁定,然后看到彼此**匿名化**的意见并在最多三轮里修正——身份被抹去,谁都无法因"是安全专家说的"而锚定。一位仲裁者综合出最终存活的裁定。扇出与轮数都有界,每一条意见(包括落败的)都会被归档。专家团可按需召集,运行中的目标回路也能在高风险决策点上召集它。 - -### 团队与智能体,在同一处协作 - -**需求:** 和队友协调、驱动智能体,通常发生在两个不同的工具里。 - -**DeepAgent 的做法:** 每个项目的群聊就在会话侧栏里。把某个智能体 @ 进来当作聊天成员,它便会跑完整的智能体回路——查代码、生成、修复——拉取项目知识与最近消息作为上下文,然后带着实时进度在群里内联回复。 - -## 它是怎么做到的 - -上面每一项能力,底层都由一个控制平面原语来支撑。这些正是普通运行时所没有的部分。 - -**四图合一** — 代码图、知识图、项目记忆、文档图被统一进同一个带类型、双向链接的存储。当智能体拉取上下文时,对某个符号的改动会连带浮现出与它真正相连的设计决策、过往诊断和知识——是连通的上下文,而不是四次互不相干的关键词检索。 - -**领域包** — 140+ 个可组合的知识包,覆盖编程语言、框架、平台(云、Kubernetes、CI)、硬件,以及业务/风险领域(安全、隐私、合规)。每个包捆绑了带类型的文档(策略、方法论、知识、技能、故障档案)与探测器,能为你的任务自动激活相应的包;冲突按"更严策略优先"消解,激活的集合会被版本锁定,从而让一次运行可复现。内核保持领域中立——专业能力是磁盘上的数据,而非写死的代码。 - -**知识分级调用** — 一条单调递增的强度阶梯(`general → high → xhigh → max → ultra`)决定控制平面机器启动到什么程度。`general` 贴近原生运行时——又快又省。越往上,逐级解锁持久知识、项目交接摘要、更重的策略/方法论层,以及多智能体编排。只有当你上调档位时,才为深度付费。 - -**自学习** — 工作落地后,智能体会提出候选的知识、事实与方法论。晋升是证据门控的(一项测试通过、一条诊断清零、一次校验确认)且由用户掌控——持久知识是被有意结转的,而非后台悄悄猜出来的。会话中稳定的结论会随时间巩固进项目记忆,于是下一次会话对*你的*代码库上手更聪明。 - -**受监督的自主** — 目标回路、专家团与多智能体扇出都跑在一套事件驱动的底座上,并带着自主所必需的护栏:硬性预算上限(轮次/令牌/墙钟/成本)、宁停不空转的停滞检测、层层失败即关闭的权限与安全门、暂停升级的人工接管,以及写回文档图的完整审计轨迹。一个 Agent 面板会呈现任务成功率、冲突与死信事件。自主在构造上就是有界且可观测的——绝不是一个你停不下来的黑盒。 +- **持久 Session V2:** prompt 先持久准入、再调度执行;精确重试不会复制用户意图;同一 Session 的唤醒会安全合并。 +- **统一供应商轮次合同:** native 与 AI SDK provider 共享预算、权限、工件、审计、学习和关闭生命周期。 +- **单一持久真相:** DocumentStore 通过原子、可恢复写入统一管理文档、计划、学习候选、治理状态和版本冲突。 +- **事件驱动 Agent OS:** 持久事件、优先级路由、回压、Worker claim、租约、handoff、重试、死信恢复与分布式 placement 协调自主工作。 +- **消费者驱动 Goal:** `goal.tick.requested` 每次认领并执行一个幂等 tick,记录事实,并只在持久目标仍满足条件时调度下一 tick。 +- **人类监督:** 审批队列、全链路 trace、接管、回滚、Wiki 档案、通知,以及组织和 workspace 隔离始终位于执行路径上。 +- **安全集成:** MCP 凭据使用环境变量引用或原生操作系统 secret storage;目录风险、运行时权限、可信来源和工具 capability 逐层失败关闭。 ## 安装 @@ -150,7 +149,7 @@ deepagent-code "为 /api/users 端点添加限流" ## 架构 -``` +```text ┌─────────────────────────────────────────────────────────────┐ │ 控制平面(DeepAgent 新增) │ │ • 四图合一存储(代码 + 知识 + 记忆 + 文档) │ @@ -179,24 +178,56 @@ deepagent-code "为 /api/users 端点添加限流" └─────────────────────────────────────────────────────────────┘ ``` -DeepAgent 的控制平面在供应商轮次的边界上运作:在每次模型调用前挑选上下文,调用后把证据写回文档图。它不替换 opencode 的运行时——只是叠加在其之上。 +完整架构与不变量见 [架构与设计](design/README.md)。 + +## 从源码运行 + +DeepAgent Code 使用 Bun 1.3.14。 + +```bash +git clone https://github.com/deepagent-ltd/deepagent-code.git +cd deepagent-code +bun install +``` + +启动桌面应用: + +```bash +bun run dev:desktop +``` + +启动终端界面: + +```bash +bun run dev +``` + +执行一次性任务: + +```bash +bun run --cwd packages/deepagent-code dev run "为 /api/users 添加限流" +``` + +导入已有 Codex 或 Claude Code 历史: + +```bash +bun run --cwd packages/deepagent-code dev import-history --from codex --dry-run +``` ## 文档 -- [架构与设计](design/README.md) — 控制平面、代码智能、MCP 安全模型 -- [安全策略](SECURITY.md) — 漏洞上报、已知限制 -- [隐私政策](PRIVACY.md) — 数据处理与存储 -- [贡献指南](CONTRIBUTING.md) — 开发环境与规范 -- [更新日志](CHANGELOG.md) — 发布历史 +- [架构与设计](design/README.md) +- [安全策略](SECURITY.md) +- [隐私策略](PRIVACY.md) +- [贡献指南](CONTRIBUTING.md) +- [更新日志](CHANGELOG.md) ## 许可与署名 -DeepAgent Code 采用 **AGPL-3.0-or-later** 许可。如果你修改并将其作为网络服务运行,必须向用户提供你的源代码。 +DeepAgent Code 使用 **AGPL-3.0-or-later** 许可。如果你修改本项目并将其作为网络服务运行,必须向服务用户提供对应源代码。 -本项目衍生自 [opencode](https://github.com/sst/opencode)(MIT 许可)。完整的上游许可与署名见 [NOTICE](NOTICE)。本项目不暗示 opencode 或其贡献者的任何背书。 +DeepAgent Code 基于 [opencode](https://github.com/sst/opencode) 的 MIT 许可代码演进而来。上游署名见 [NOTICE](NOTICE)。本项目不暗示 opencode 或其贡献者的任何背书。 --- -

- 由 DeepAgent 打造 -

+

Built by DeepAgent

diff --git a/design/README.md b/design/README.md index 86c26ebc..bd41fcfb 100644 --- a/design/README.md +++ b/design/README.md @@ -1,120 +1,266 @@ -# DeepAgent Code — Architecture & Design +# DeepAgent Code Architecture & Design -> **Public design overview.** Internal implementation details and roadmap docs live in the private `docs/` tree (not version-controlled). This directory contains the publicly visible architectural narrative. +> Public architecture baseline for DeepAgent Core V4.1. ---- +DeepAgent Code is a document-centered, event-driven AI coding system. It combines a coding-agent runtime with a durable control plane that owns context, planning, learning, collaboration, safety, and human oversight. -## What is DeepAgent Code? +The architecture is designed around one requirement: a long-running agent must remain correct and governable after many model turns, tool calls, user interventions, process restarts, and worker handoffs. -DeepAgent Code is an AI coding agent that adds a **control plane** on top of the [opencode](https://github.com/sst/opencode) runtime. It keeps the proven opencode foundations (runtime, tool, MCP, session, provider stack) and layers in: +## Design Principles -- **Durable document memory** — knowledge base with retrieval gates, dedup, and merge -- **Context assembly** — selective, evidence-backed context building (not raw file dumps) -- **Plan system** — structured task planning with staleness detection and rollback -- **Failure triage** — three-tier classifier (auto-fixable / needs-narrowing / not-auto-fixable) -- **Domain adapters** — pluggable domain packs for specialized workflows -- **AI IDE microservice** — LSP-backed semantic code navigation via `code_intel` -- **MCP catalog** — curated, one-click-enable MCP servers with safety tiers +### One durable truth per concept ---- +Sessions, inputs, plans, documents, goals, events, approvals, and learning decisions each have one authoritative durable representation. In-memory state is a cache or an ownership hint, never a competing source of truth. -## Architectural Principles +### Admission is separate from execution -### 1. Enhance, don't replace +A user instruction is durably admitted before execution is scheduled. A successful API response therefore means the instruction is recorded, not merely present in a process-local queue. -DeepAgent is built **on top of** the opencode agent/runtime/session/tool/MCP stack. It does not rewrite the execution engine, tool system, or provider layer. The default agent behavior is not degraded. The lower-strength `general` mode stays close to the inherited runtime contract. +### One provider-turn contract -### 2. Control-plane only +Native and AI SDK providers pass through the same model-turn boundary. Budget, permissions, tool policy, prompt assembly, artifacts, audit, learning, and close semantics cannot be bypassed by selecting another provider implementation. -DeepAgent is responsible for **strategy / context / budget / audit / verification / document graph**. It does not directly spawn LSP processes or execute MCP tools — those go through the existing `LSP.Service` and `MCP.Service` respectively. +### Context is selected, not accumulated -### 3. Full tool output does not enter context +The model receives a bounded working set assembled from explicit Context Sources. Full tool output and durable history remain referenceable artifacts; only admitted summaries, evidence, and snippets enter the active context. -Per the deterministic task control contract: raw LSP results, diagnostic dumps, and capability indexes are written to **evidence artifacts** (ref-linked, tool-only visibility). Only summaries and `file:line` snippets appear in the model context. +### Safety fails closed -### 4. Fail-closed on safety +External events, credentials, tools, paths, autonomy, worker placement, and outbound messages are checked at their execution boundaries. Missing identity, trust, capability, or approval never widens access. -MCP catalog entries default to **not connected** (zero startup overhead). Dangerous write operations (force-push, DROP, file delete) require explicit approval. Read-only DB connections enforce restricted-mode at the server level. +### Humans can always intervene ---- +Steering, plan editing, approval, pause, resume, takeover, rollback, and review are part of the runtime contract. They are not dashboard-only controls layered over an autonomous black box. -## Component Map +## System Map -``` -┌─────────────────────────────────────────────────────────────┐ -│ DeepAgent Control Plane │ -│ │ -│ ┌──────────────┐ ┌─────────────┐ ┌───────────────────┐ │ -│ │ Plan System │ │ Doc Memory │ │ Failure Triage │ │ -│ │ (task/plan) │ │ (knowledge │ │ (3-tier classify)│ │ -│ └──────┬───────┘ │ store) │ └─────────┬─────────┘ │ -│ │ └──────┬──────┘ │ │ -│ ┌──────▼──────────────────▼──────────────────▼───────────┐ │ -│ │ Agent Gateway (core) │ │ -│ │ audit · budget · permission · capability index │ │ -│ └──────┬─────────────────────────────────────────────────┘ │ -└─────────│───────────────────────────────────────────────────┘ - │ -┌─────────▼──────────────────────────────────────────────────┐ -│ opencode Foundation (unchanged) │ -│ │ -│ Session ─── Tool Registry ─── MCP Service │ -│ │ │ │ │ -│ Provider LSP Service 38 lang servers │ -│ (Claude/…) code_intel tool + MCP catalog │ -└────────────────────────────────────────────────────────────┘ +```text +┌─────────────────────────────────────────────────────────────────────┐ +│ Experience │ +│ Desktop · Web · TUI · IM · Repo & Wiki · Expert Panel · Oversight │ +└──────────────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────────────▼──────────────────────────────────────┐ +│ Durable Session Runtime │ +│ Session V2 · System Context · Context Epoch · Steering · Queue │ +│ Prompt cache policy · Provider-turn lifecycle · Tool materialization│ +└──────────────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────────────▼──────────────────────────────────────┐ +│ DeepAgent Control Plane │ +│ PlanController · GoalController · Context Graph · Learning │ +│ Event Router · Scheduler · Worker Pool · Handoff · Security gates │ +└──────────────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────────────▼──────────────────────────────────────┐ +│ Durable State │ +│ DocumentStore · Session/Event database · Event Bus · Audit/Artifacts│ +└──────────────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────────────▼──────────────────────────────────────┐ +│ Execution Services │ +│ Provider · Tool · LSP · MCP · Git/Worktree · Debug · Profile │ +└─────────────────────────────────────────────────────────────────────┘ ``` ---- +## Session V2 -## code_intel — AI IDE Microservice +Session V2 separates durable prompt admission from model execution. -The `code_intel` tool wraps the LSP stack as a **symbol-driven semantic API**. The agent specifies a symbol name and an intent; `code_intel` resolves line/column coordinates internally and returns `file:line` + code snippets. +### Prompt admission -```typescript -code_intel({ symbol: "AgentGateway.open", intent: "overview" }) -// → definition + type + references + callers + callees + doc summary -// full detail → evidence artifact (ref only in context) -``` +- Each prompt creates one durable `session_input` before scheduling work. +- Reusing a Session ID adopts that Session rather than creating a parallel execution entity. +- Reusing a prompt message ID is accepted only for an exact retry with the same Session, content, and delivery mode. +- Conflicting ID reuse fails instead of silently reconciling different user intent. +- A prompt can be admitted without waking execution when the caller requests admit-only behavior. + +### Delivery vocabulary + +| Delivery | Meaning | +|---|---| +| normal turn | Start a normal activity when the Session is idle | +| `steer` | Add guidance to the active activity at the next safe provider-turn boundary | +| `goal_steer` | Add guidance to the next Goal tick | +| `queue` | Open a future FIFO activity after the active activity settles | +| interrupt | Target the active process-local ownership chain immediately | + +Steering never aborts an in-flight tool or stream. The input is persisted first, absorbed in stable order, and materialized into history exactly once. + +### Execution ownership + +`SessionExecution` is process-global and keyed by Session ID. A drain discovers placement from durable Session location only when it starts. SessionRunner, model resolution, tools, permissions, and filesystem services remain Location-scoped. + +Same-Session resumes join one coordinator; advisory wakes coalesce; different Sessions can run concurrently. Every provider turn performs one explicit `llm.stream(request)` call and reloads projected history before durable continuation. + +### Prompt cache invariant + +The system prompt is split into a byte-stable prefix and one append-only volatile tail: + +- Agent instructions, stable policy, and System Context baseline stay in the prefix. +- Round state, budgets, plan snapshots, prior results, fan-out decisions, and steering stay in the tail. +- OpenAI-compatible providers use a stable Session cache key; providers with cache markers use their protocol-native breakpoint. +- Prefix hash and cache outcomes distinguish normal compaction misses from accidental prefix drift. + +## Document System + +DocumentStore is the durable body for DeepAgent state. Documents carry: + +- a stable ID and monotonic version; +- type, scope, status, domain, tags, and description; +- provenance and evidence references; +- confidence, sensitivity, and approval risk; +- typed links such as `supports`, `blocks`, `conflicts`, `validates`, `supersedes`, `contains`, `imports`, and `calls`. + +Writes use atomic replacement and conflict detection. Concurrent handles observe one authority, process-level writers coordinate through lock/CAS semantics, and migrations are incremental, restartable, and integrity-checked. + +### Scope + +| Scope | Purpose | +|---|---| +| `session-private` | Current conversation and run-local state | +| `project-shared` | Knowledge and decisions shared by one project | +| `user-global` | Cross-project preferences and explicitly promoted knowledge | +| `public-system` | Built-in skills and domain-pack documents | +| `sealed` | Audit/evaluator material that cannot enter model context | + +Plans, run context, worklogs, designs, diagnoses, evaluations, knowledge, memory, strategies, methodologies, skills, and failure dossiers share this document algebra instead of maintaining separate storage models. + +## System Context and the Four Graphs + +The Context System connects four projections over the same durable knowledge surface: + +1. **Code:** files, symbols, imports, calls, definitions, references, diagnostics. +2. **Knowledge:** facts, strategies, methodologies, skills, and failure dossiers. +3. **Memory:** decisions, constraints, project conventions, environment facts, and handoffs. +4. **Documents:** plans, designs, run context, evidence, worklogs, and evaluations. + +Context Sources produce typed observations from their domains. Session-owned selection applies budget, relevance, scope, sensitivity, evidence strength, conflict, and snapshot rules. A Context Epoch records the selected baseline so a provider turn is reproducible and observable. + +The Event Router can attach a context strategy to an event. SessionRunner executes that strategy in the target Location, records query and admission decisions in trace, and degrades safely when an optional source is unavailable. + +Compaction preserves a stable structure: goal, constraints, completed and active work, blockers, decisions, next steps, critical facts/open questions, and relevant files. Durable references remain outside the prose summary and can be reloaded when needed. + +## Planning and Goal Execution + +### Plan authority + +A structural plan is a versioned DocumentStore document. Session hot state keeps only its plan pointer/version and stale latch. The model plan tool, human plan editor, Goal worker, UI, Grader, and archive all read and update the same plan through version-aware writes. + +The runtime derives plan staleness from facts it already observes: + +- the user adds new guidance; +- a tool or execution step fails; +- validation fails; +- repeated work makes no progress; +- the active domain-pack snapshot changes. + +Read and diagnosis tools remain available while stale. Mutating tools require the plan to be synchronized, with bounded anti-deadlock behavior. + +### Goal Loop + +A Goal has objective completion criteria, a plan, a durable run context, a budget ledger, and a bounded controller. Each tick: + +1. claims the expected durable Goal/plan version; +2. applies pending user plan edits and steering; +3. executes one coherent step; +4. records tools, tokens, cost, time, evidence, and progress; +5. evaluates objective criteria and stall state; +6. emits facts and schedules the next tick only when eligible. + +`goal.tick.requested` is a durable command, not a post-hoc trace marker. Duplicate delivery cannot repeat provider or tool side effects. Pause, stop, takeover, hard limits, quiet hours, needs-human, and terminal state all stop self-continuation. + +Hot plan edits preserve reusable step IDs and completed evidence, increment the plan version, and reset progress/stall baselines without interrupting the in-flight tick. + +## Event-Driven Agent Runtime + +The Event Bus provides persist-before-dispatch delivery, idempotency, priority, retry, acknowledgement, dead-letter handling, retention, replay, and correlation. Local deployments use the embedded backend; distributed deployments use Redis Streams or Kafka through the same backend contract. + +The Router combines event type, trusted source, actor identity, Agent trigger/capability metadata, autonomy ceiling, approval intent, context strategy, deduplication, priority, and workspace backpressure into one traceable route decision. + +The Worker Pool owns bounded concurrency, placement, claims, leases, renewal, recovery, and handoff. Independent DAG nodes run concurrently; dependency edges remain ordered. File and symbol claims are shared across Workers so conflicting writes cannot run together. + +Write-capable Agents use isolated worktrees by default. Parent Agents receive bounded summaries, status, artifact/session references, and necessary diffs; complete child transcripts remain in their own Sessions. + +## Human Collaboration and Oversight + +### Repo & Wiki + +Repo & Wiki is a human-facing projection, never a second source of truth. It exposes document and code navigation, full-text search, docs-to-code links, knowledge governance, and execution archives. Organization and workspace identity are enforced on query, index, archive, and promotion paths. + +### Expert Panel + +Panelists receive the same frozen question and evidence under differentiated lenses. Anonymous multi-round debate avoids identity anchoring. A deterministic Arbiter applies quorum, preserves minority opinions, and routes unsafe ambiguity to the Approval Queue. Distributed panelists run through the Worker Pool. + +### IM and proactive delivery + +Project IM supports groups, direct conversations, threads, search, attachments, agent mentions, progress streaming, and permission revalidation when project bindings change. Event-driven notifications and digests pass through content safety, path ACL, external-link, rate, and quiet-hours policies before delivery. + +### Oversight + +Correlation IDs connect event, route, context, worker claim, Session, provider turn, tool, artifact, approval, and outbound action. Operators can inspect Approval Queue items, dead letters, budgets, conflicts, takeovers, rollbacks, and final delivery without reconstructing state from logs. + +## Learning and Knowledge Governance + +Learning runs outside the interactive turn on idle, pause, project switch, and Session finalization triggers. Candidates enter the same DocumentStore lifecycle used by human governance. + +- Low-risk project candidates can pass deterministic auto-review. +- Medium-risk/global candidates use an isolated blank-thread reviewer with no Session history. +- Sensitive, strategic, regulated, or irreversible candidates require human review. +- Rejection status, reason, and fingerprint remain authoritative in DocumentStore; auxiliary indexes are rebuildable projections. +- Promotion changes the same document's status/version instead of copying it into a second identity. +- Released retrieval sets name a snapshot and carry evaluation matrix, baseline, repeats, and ablation verdict. + +A failed release gate restores the previous knowledge snapshot. Selected and rejected refs remain reproducible in run artifacts. + +## Code Intelligence + +The AI IDE surface combines: -Supported intents: `definition · references · implementations · type · calls_in · calls_out · supertypes · subtypes · type_hints · hover · rename_preview · quick_fix · outline · diagnostics · overview` +- semantic symbol lookup and intent-oriented `code_intel` queries; +- definitions, references, calls, imports, type hierarchy, diagnostics, and rename preview; +- unsaved-buffer `textDocument/didOpen`, `didChange`, `didSave`, and `didClose` synchronization; +- incremental code indexing with exact mtime I/O hints and content-SHA correctness authority; +- deterministic fallback to repository search/read when a language server is unavailable. -Graceful degradation: if no LSP server is configured for the file type, returns a hint to use `grep/read`. Capability only grows, never drops. +Full LSP output stays in evidence artifacts. The model receives bounded summaries and precise source references. ---- +## MCP and Credential Security -## MCP Catalog — Safety Model +MCP servers can be added from the curated catalog or configured manually through Desktop, HTTP, or CLI. Catalog risk tiers are derived from trusted templates rather than mutable user configuration. Servers default to disconnected; writes, external fetches, and privileged actions pass through runtime permission gates. -Each catalog entry carries a **risk tier** derived at load time from the catalog template. The tier is **not user-writable** — it is computed from the entry definition, preventing config-injection attacks. +Credentials use indirection rather than plaintext project configuration: -| Tier | Examples | Default behavior | -|------|----------|-----------------| -| `read_only` | postgres-readonly | All ops auto-allowed | -| `write_guarded` | filesystem, github, git | Write ops require explicit approval | -| `external_fetch` | fetch, browser (Playwright) | External requests require explicit approval | +- `${VAR}` and `${VAR:-default}` resolve at connection time; +- `secret://` handles resolve through macOS Keychain, Linux Secret Service, or Windows Credential Manager/DPAPI; +- environments without a native keyring require explicit approval for an audited local fallback outside the project repository; +- logs, artifacts, outbound messages, and config views redact resolved values. -**Credentials** are declared by key name in the catalog template (`CredentialSpec`). Values are filled at enable-time. +## Security Boundaries -> **Known limitation (V3.4):** credential values are stored in plaintext in the local config file. Do not commit config files containing credentials to version control. A secure-storage mechanism (OS keyring, aligned with the codex approach) is planned for V3.5. +Autonomous execution passes four independent gates: ---- +1. the event source is trusted for the workspace; +2. the actor has permission for the project and resource; +3. the Agent descriptor permits the trigger, capability, and autonomy level; +4. the runtime permits the concrete tool, path, network target, and side effect. -## Security Model Summary +Additional controls include durable budgets, rate limits, quiet hours, secret/path/link filtering, worktree isolation, hardened read-only Git, human approval, takeover, rollback, and complete audit correlation. -| Mechanism | Status | -|-----------|--------| -| MCP risk tier — catalog-derived, not config-injectable | ✅ V3.4 | -| MCP catalog defaults to not connected | ✅ V3.4 | -| Dangerous writes: approval gate (`ctx.ask`) | ✅ V3.4 | -| Read-only DB: restricted-mode enforced at server | ✅ V3.4 | -| Credential secure storage (OS keyring) | ⏳ V3.5 M-CRED | +## Repository Map ---- +| Area | Path | +|---|---| +| Core Session, documents, context, event, and policy algebra | `packages/core/src/` | +| CLI/server runtime, tools, Goal and event wiring | `packages/deepagent-code/src/` | +| Desktop/Web application UI | `packages/app/src/` | +| Electron host and isolated browser views | `packages/desktop/src/` | +| Provider abstraction | `packages/llm/` | +| Domain knowledge packs | `packages/domain-packs/` | +| Generated JavaScript SDK | `packages/sdk/js/` | -## License +## License and Source -DeepAgent Code is licensed under **AGPL-3.0-or-later**. -Source code: [github.com/lessweb/deepagent-code](https://github.com/lessweb/deepagent-code) +DeepAgent Code is licensed under **AGPL-3.0-or-later**. The canonical repository is [github.com/deepagent-ltd/deepagent-code](https://github.com/deepagent-ltd/deepagent-code). -DeepAgent Code is derived from [opencode](https://github.com/sst/opencode) (MIT). -See `NOTICE` in the repository root for the full upstream attribution. +The project is derived from [opencode](https://github.com/sst/opencode) under the MIT License. Upstream attribution is preserved in [NOTICE](../NOTICE). diff --git a/packages/app/package.json b/packages/app/package.json index 9435d21f..466955fe 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@deepagent-code/app", - "version": "1.4.0", + "version": "1.4.1", "description": "", "type": "module", "exports": { diff --git a/packages/app/src/components/dialog-custom-provider-form.ts b/packages/app/src/components/dialog-custom-provider-form.ts index 1294b436..c94ec521 100644 --- a/packages/app/src/components/dialog-custom-provider-form.ts +++ b/packages/app/src/components/dialog-custom-provider-form.ts @@ -1,5 +1,33 @@ +import { isOfficialProvider } from "@deepagent-code/core/provider-official" + const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/ const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible" +const ANTHROPIC = "@ai-sdk/anthropic" + +export type ProviderProtocol = "openai-compatible" | "anthropic" + +// The config payload written under `provider.`. `discovery` and `models` are mutually exclusive +// in practice (discovery mode emits an empty models map), but both are typed optional so the emitted +// object has one consistent shape instead of a union callers must narrow. +export type CustomProviderConfig = { + npm: string + name: string + env?: string[] + options: { + baseURL: string + apiKey?: string + headers?: Record + } + discovery?: boolean + models: Record +} + +const npmForProtocol = (kind: ProviderProtocol | undefined) => (kind === "anthropic" ? ANTHROPIC : OPENAI_COMPATIBLE) + +// Leading host labels that are generic service prefixes and make a poor provider id, so we skip past +// them to reach the brand label (api.deepseek.com -> "deepseek", not "api"). Kept deliberately small: +// only unambiguous service prefixes, never anything that could be a brand. +const GENERIC_HOST_LABELS = new Set(["api", "www", "app", "gateway", "proxy", "open"]) type Translator = (key: string, vars?: Record) => string @@ -46,29 +74,108 @@ type ValidateArgs = { t: Translator disabledProviders: string[] existingProviderIDs: Set + // Protocol detected during model discovery; decides the SDK npm written to config. Defaults to + // openai-compatible when omitted (backward compatible with the manual form). + protocol?: ProviderProtocol + // Runtime discovery mode: when true AND the user listed no manual models, persist `discovery: true` + // with an empty model list so the backend refreshes models from the provider's /models endpoint on + // every load instead of freezing them into config. Manual models always take precedence and turn + // this off for that provider. + discovery?: boolean +} + +// Turn a base URL into a stable, unique provider id + a human display name so the user only has to +// enter URL + key. Rules: +// - id is derived from the registrable host label (api.deepseek.com -> "deepseek", +// open.bigmodel.cn -> "bigmodel"), slugified to satisfy PROVIDER_ID. +// - reserved official ids (openai/deepseek/anthropic/zhipuai/xai/google/...) and any id already in +// use are avoided by appending a numeric suffix, since a third-party id that collides with an +// official one is rejected by the backend (THIRD_PARTY_PROVIDER_CONFLICT). +// - `disabledProviders` do NOT count as taken: re-adding a previously disabled provider should be +// able to reuse its id. +export function deriveProviderIdentity(input: { + baseURL: string + existingProviderIDs: Set + disabledProviders?: string[] +}): { providerID: string; name: string } { + const disabled = new Set(input.disabledProviders ?? []) + const taken = (id: string) => + (input.existingProviderIDs.has(id) && !disabled.has(id)) || (isOfficialProvider(id) && !disabled.has(id)) + + const base = baseSlug(input.baseURL) + let providerID = base + let n = 2 + while (taken(providerID)) { + providerID = `${base}-${n}` + n++ + } + return { providerID, name: displayName(base) } +} + +function baseSlug(baseURL: string): string { + let host = "" + try { + host = new URL(baseURL.trim()).hostname + } catch { + host = "" + } + const labels = host.split(".").filter(Boolean) + // Drop leading generic service labels (api., www., ...) so we land on the brand label. + while (labels.length > 1 && GENERIC_HOST_LABELS.has(labels[0].toLowerCase())) labels.shift() + // Prefer the registrable label: for a.b.com pick "b"; for single-label/localhost keep as-is. + const label = labels.length >= 2 ? labels[labels.length - 2] : (labels[0] ?? "") + const slug = label + .toLowerCase() + .replace(/[^a-z0-9-_]+/g, "-") + .replace(/^-+|-+$/g, "") + // Must satisfy PROVIDER_ID (starts alphanumeric). Fall back to a safe default. + return slug && PROVIDER_ID.test(slug) ? slug : "custom-provider" +} + +function displayName(slug: string): string { + const cleaned = slug.replace(/[-_]+/g, " ").trim() + if (!cleaned) return "Custom Provider" + return cleaned + .split(" ") + .map((word) => (word ? word[0].toUpperCase() + word.slice(1) : word)) + .join(" ") } export function validateCustomProvider(input: ValidateArgs) { - const providerID = input.form.providerID.trim() - const name = input.form.name.trim() + const typedID = input.form.providerID.trim() + const typedName = input.form.name.trim() const baseURL = input.form.baseURL.trim() const apiKey = input.form.apiKey.trim() const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim() const key = apiKey && !env ? apiKey : undefined + const urlError = !baseURL + ? input.t("provider.custom.error.baseURL.required") + : !/^https?:\/\//.test(baseURL) + ? input.t("provider.custom.error.baseURL.format") + : undefined + + // Zero-config path: when the user leaves id/name blank we derive them from the URL, so those + // fields are no longer required. Derivation needs a usable URL — if the URL itself is invalid we + // skip it and let urlError drive the failure instead of emitting a spurious id/name error. + const derived = !urlError && (!typedID || !typedName) ? deriveProviderIdentity({ + baseURL, + existingProviderIDs: input.existingProviderIDs, + disabledProviders: input.disabledProviders, + }) : undefined + const providerID = typedID || derived?.providerID || "" + const name = typedName || derived?.name || "" + + // Only the user's explicitly-typed id is format-checked; a derived id is always valid by + // construction. A blank id with no derivable URL still surfaces as "required". const idError = !providerID ? input.t("provider.custom.error.providerID.required") - : !PROVIDER_ID.test(providerID) + : typedID && !PROVIDER_ID.test(typedID) ? input.t("provider.custom.error.providerID.format") : undefined const nameError = !name ? input.t("provider.custom.error.name.required") : undefined - const urlError = !baseURL - ? input.t("provider.custom.error.baseURL.required") - : !/^https?:\/\//.test(baseURL) - ? input.t("provider.custom.error.baseURL.format") - : undefined const disabled = input.disabledProviders.includes(providerID) const existsError = idError @@ -77,6 +184,11 @@ export function validateCustomProvider(input: ValidateArgs) { ? input.t("provider.custom.error.providerID.exists") : undefined + // Discovery mode is only active when the user listed no manual models: the model list then comes + // from the backend at runtime, so the empty model rows must not fail validation. + const hasManualModels = input.form.models.some((m) => m.id.trim().length > 0) + const discoveryMode = !!input.discovery && !hasManualModels + const seenModels = new Set() const models = input.form.models.map((m) => { const id = m.id.trim() @@ -91,7 +203,7 @@ export function validateCustomProvider(input: ValidateArgs) { const nameError = !m.name.trim() ? input.t("provider.custom.error.required") : undefined return { id: idError, name: nameError } }) - const modelsValid = models.every((m) => !m.id && !m.name) + const modelsValid = discoveryMode || models.every((m) => !m.id && !m.name) const modelConfig = Object.fromEntries(input.form.models.map((m) => [m.id.trim(), { name: m.name.trim() }])) const seenHeaders = new Set() @@ -128,26 +240,25 @@ export function validateCustomProvider(input: ValidateArgs) { const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid if (!ok) return { err, models, headers } + const config: CustomProviderConfig = { + npm: npmForProtocol(input.protocol), + name, + ...(env ? { env: [env] } : {}), + options: { + baseURL, + ...(key ? { apiKey: key } : {}), + ...(Object.keys(headerConfig).length ? { headers: headerConfig } : {}), + }, + // Discovery mode: persist the opt-in flag and an empty model list (backend refreshes at runtime). + // Manual mode: freeze the listed models and leave discovery off. + ...(discoveryMode ? { discovery: true, models: {} } : { models: modelConfig }), + } + return { err, models, headers, - result: { - providerID, - name, - key, - config: { - npm: OPENAI_COMPATIBLE, - name, - ...(env ? { env: [env] } : {}), - options: { - baseURL, - ...(key ? { apiKey: key } : {}), - ...(Object.keys(headerConfig).length ? { headers: headerConfig } : {}), - }, - models: modelConfig, - }, - }, + result: { providerID, name, key, config }, } } diff --git a/packages/app/src/components/dialog-custom-provider.test.ts b/packages/app/src/components/dialog-custom-provider.test.ts index 07dd26ec..15c8689f 100644 --- a/packages/app/src/components/dialog-custom-provider.test.ts +++ b/packages/app/src/components/dialog-custom-provider.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { validateCustomProvider } from "./dialog-custom-provider-form" +import { deriveProviderIdentity, validateCustomProvider } from "./dialog-custom-provider-form" const t = (key: string) => key @@ -77,4 +77,175 @@ describe("validateCustomProvider", () => { value: undefined, }) }) + + test("derives provider id and name from the URL when left blank", () => { + const result = validateCustomProvider({ + form: { + providerID: "", + name: "", + baseURL: "https://api.moonshot.cn/v1", + apiKey: "secret", + // No manual models: discovery would fill these; validation must not require them here since + // the dialog auto-fills discovered models before calling validate. Simulate a filled row. + models: [{ row: "m0", id: "kimi", name: "Kimi", err: {} }], + headers: [{ row: "h0", key: "", value: "", err: {} }], + err: {}, + }, + t, + disabledProviders: [], + existingProviderIDs: new Set(), + }) + + expect(result.err.providerID).toBeUndefined() + expect(result.err.name).toBeUndefined() + expect(result.result?.providerID).toBe("moonshot") + expect(result.result?.name).toBe("Moonshot") + }) + + test("persists anthropic npm when protocol is anthropic", () => { + const result = validateCustomProvider({ + form: { + providerID: "claude-relay", + name: "Claude Relay", + baseURL: "https://relay.example.com", + apiKey: "secret", + models: [{ row: "m0", id: "claude-x", name: "Claude X", err: {} }], + headers: [{ row: "h0", key: "", value: "", err: {} }], + err: {}, + }, + t, + disabledProviders: [], + existingProviderIDs: new Set(), + protocol: "anthropic", + }) + + expect(result.result?.config.npm).toBe("@ai-sdk/anthropic") + }) + + test("defaults to openai-compatible npm when protocol omitted", () => { + const result = validateCustomProvider({ + form: { + providerID: "relay", + name: "Relay", + baseURL: "https://relay.example.com", + apiKey: "secret", + models: [{ row: "m0", id: "m", name: "M", err: {} }], + headers: [{ row: "h0", key: "", value: "", err: {} }], + err: {}, + }, + t, + disabledProviders: [], + existingProviderIDs: new Set(), + }) + + expect(result.result?.config.npm).toBe("@ai-sdk/openai-compatible") + }) + + test("discovery mode persists discovery flag and empty models when no manual models", () => { + const result = validateCustomProvider({ + form: { + providerID: "", + name: "", + baseURL: "https://api.moonshot.cn/v1", + apiKey: "secret", + // No manual models: the backend owns the list at runtime. + models: [{ row: "m0", id: "", name: "", err: {} }], + headers: [{ row: "h0", key: "", value: "", err: {} }], + err: {}, + }, + t, + disabledProviders: [], + existingProviderIDs: new Set(), + discovery: true, + }) + + expect(result.err.providerID).toBeUndefined() + expect(result.result?.providerID).toBe("moonshot") + expect(result.result?.config.discovery).toBe(true) + expect(result.result?.config.models).toEqual({}) + }) + + test("explicit anthropic protocol choice persists the anthropic npm even without detection", () => { + // Simulates the user picking Anthropic in the protocol selector: the dialog passes protocol + // "anthropic" to the form, which must win regardless of any auto-detection. + const result = validateCustomProvider({ + form: { + providerID: "relay", + name: "Relay", + baseURL: "https://relay.example.com", + apiKey: "secret", + models: [{ row: "m0", id: "m", name: "M", err: {} }], + headers: [{ row: "h0", key: "", value: "", err: {} }], + err: {}, + }, + t, + disabledProviders: [], + existingProviderIDs: new Set(), + protocol: "anthropic", + }) + + expect(result.result?.config.npm).toBe("@ai-sdk/anthropic") + }) + + test("manual models override discovery mode: freeze models, no discovery flag", () => { + const result = validateCustomProvider({ + form: { + providerID: "relay", + name: "Relay", + baseURL: "https://relay.example.com", + apiKey: "secret", + models: [{ row: "m0", id: "custom-model", name: "Custom Model", err: {} }], + headers: [{ row: "h0", key: "", value: "", err: {} }], + err: {}, + }, + t, + disabledProviders: [], + existingProviderIDs: new Set(), + // Even with discovery requested, a hand-listed model wins and turns discovery off. + discovery: true, + }) + + expect(result.result?.config.discovery).toBeUndefined() + expect(result.result?.config.models).toEqual({ "custom-model": { name: "Custom Model" } }) + }) +}) + +describe("deriveProviderIdentity", () => { + test("strips generic service labels and uses the brand label", () => { + expect(deriveProviderIdentity({ baseURL: "https://api.moonshot.cn/v1", existingProviderIDs: new Set() })).toEqual({ + providerID: "moonshot", + name: "Moonshot", + }) + expect( + deriveProviderIdentity({ baseURL: "https://open.bigmodel.cn/api/paas/v4", existingProviderIDs: new Set() }), + ).toEqual({ providerID: "bigmodel", name: "Bigmodel" }) + }) + + test("avoids reserved official provider ids by suffixing", () => { + // openai is a reserved official id; a third-party endpoint must not claim it. + const result = deriveProviderIdentity({ baseURL: "https://api.openai.com/v1", existingProviderIDs: new Set() }) + expect(result.providerID).toBe("openai-2") + }) + + test("avoids ids already in use", () => { + const result = deriveProviderIdentity({ + baseURL: "https://api.moonshot.cn/v1", + existingProviderIDs: new Set(["moonshot", "moonshot-2"]), + }) + expect(result.providerID).toBe("moonshot-3") + }) + + test("lets a disabled provider reclaim its id", () => { + const result = deriveProviderIdentity({ + baseURL: "https://api.moonshot.cn/v1", + existingProviderIDs: new Set(["moonshot"]), + disabledProviders: ["moonshot"], + }) + expect(result.providerID).toBe("moonshot") + }) + + test("falls back to a safe id for an unparseable URL", () => { + const result = deriveProviderIdentity({ baseURL: "not a url", existingProviderIDs: new Set() }) + expect(result.providerID).toBe("custom-provider") + }) }) diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index 90dfb92e..574fc9e3 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -6,14 +6,29 @@ import { ProviderIcon } from "@deepagent-code/ui/provider-icon" import { useMutation } from "@tanstack/solid-query" import { TextField } from "@deepagent-code/ui/text-field" import { showToast } from "@/utils/toast" -import { batch, For } from "solid-js" +import { batch, createSignal, For, Show } from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" -import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form" +import { + type FormState, + type ProviderProtocol, + deriveProviderIdentity, + headerRow, + modelRow, + validateCustomProvider, +} from "./dialog-custom-provider-form" import { DialogSelectProvider } from "./dialog-select-provider" +import { Select } from "@deepagent-code/ui/select" + +// "auto" keeps the zero-config behavior (probe openai-compatible then anthropic and persist whichever +// answered). The explicit choices let the user pin the protocol when auto-detection is wrong or the +// endpoint doesn't implement /models. The choice decides both the discovery probe `kind` and the SDK +// npm persisted to config. +type ProtocolChoice = "auto" | ProviderProtocol +const PROTOCOL_CHOICES: ProtocolChoice[] = ["auto", "openai-compatible", "anthropic"] type Props = { back?: "providers" | "close" @@ -35,6 +50,20 @@ export function DialogCustomProvider(props: Props) { err: {}, }) + // Advanced section (provider id / display name / headers / manual models) is collapsed by default: + // the zero-config path only needs Base URL + API key. Protocol detected during discovery decides + // which SDK npm gets persisted. + const [showAdvanced, setShowAdvanced] = createSignal(false) + const [detectedProtocol, setDetectedProtocol] = createSignal(undefined) + // User's explicit protocol choice. "auto" (default) preserves auto-detection; an explicit choice + // pins both the discovery probe kind and the persisted npm. + const [protocolChoice, setProtocolChoice] = createSignal("auto") + + // The protocol persisted to config: an explicit choice wins; otherwise the detected one (or + // openai-compatible default when nothing was detected). + const resolvedProtocol = (): ProviderProtocol | undefined => + protocolChoice() === "auto" ? detectedProtocol() : (protocolChoice() as ProviderProtocol) + const goBack = () => { if (props.back === "close") { dialog.close() @@ -109,15 +138,14 @@ export function DialogCustomProvider(props: Props) { .map((h) => [h.key, h.value]), ) - const discoveredRows = (models: Array<{ id: string; name: string }>) => - models.map((model) => ({ ...modelRow(), id: model.id, name: model.name || model.id })) - - const validate = (nextForm: FormState = form) => { + const validate = (nextForm: FormState = form, discovery = false) => { const output = validateCustomProvider({ form: nextForm, t: language.t, disabledProviders: serverSync.data.config.disabled_providers ?? [], existingProviderIDs: new Set(serverSync.data.provider.all.keys()), + protocol: resolvedProtocol(), + discovery, }) batch(() => { setForm("err", output.err) @@ -159,39 +187,55 @@ export function DialogCustomProvider(props: Props) { void (async () => { let nextForm: FormState = form - const providerID = form.providerID.trim() const baseURL = form.baseURL.trim() const key = form.apiKey.trim() const env = key.match(/^\{env:([^}]+)\}$/)?.[1]?.trim() // Whether the user already typed at least one model id by hand. const hasManualModels = form.models.some((m) => m.id.trim().length > 0) - if (providerID && baseURL && key && !env) { + // providerID is optional now (derived from URL when blank); discovery only needs URL + key. + // Pass a best-effort id so the backend has something to label the request with. + const discoverID = form.providerID.trim() || deriveProviderIdentity({ + baseURL, + existingProviderIDs: new Set(serverSync.data.provider.all.keys()), + disabledProviders: serverSync.data.config.disabled_providers ?? [], + }).providerID + // Runtime discovery mode: when the user provides only URL+key (no manual models) and the + // endpoint answers discovery, persist `discovery: true` with an empty model list so the backend + // refreshes models on every load instead of freezing this snapshot into config. + let discoveryMode = false + if (baseURL && key && !env) { // Model discovery is a convenience, not a requirement: many OpenAI-compatible servers // (some vLLM setups, local runtimes) don't implement GET /v1/models and return 400/404. - // Treat discovery as best-effort — only use its result to auto-fill models when the user - // hasn't entered any. If it fails (or returns nothing) we keep the user's manual models and - // let validation handle the empty case, instead of blocking save on a missing endpoint. - const discovered = await serverSDK.client.provider.models + // Treat discovery as best-effort — probe to detect the protocol (→ SDK npm) and confirm the + // endpoint works. If it fails we fall back to manual models and let validation handle the + // empty case instead of blocking save on a missing endpoint. + // Auto (kind omitted) lets the backend probe openai-compatible then anthropic and report which + // protocol answered. An explicit choice pins the probe to that protocol. + const choice = protocolChoice() + const res = await serverSDK.client.provider.models .discover( { - providerID, + providerID: discoverID, baseURL, apiKey: key, headers: headerConfig(), - kind: "openai-compatible", + ...(choice === "auto" ? {} : { kind: choice }), }, { throwOnError: true }, ) - .then((res) => res.data?.models ?? []) - .catch(() => []) + .then((res) => res.data) + .catch(() => undefined) + if (res?.kind) setDetectedProtocol(res.kind) + const discovered = res?.models ?? [] if (discovered.length > 0 && !hasManualModels) { - const rows = discoveredRows(discovered) - setForm("models", rows) - nextForm = { ...form, models: rows } + // Backend owns the model list at runtime, so we leave the form's manual-model rows empty + // and persist `discovery: true` instead of freezing this snapshot. (nextForm stays the + // empty-models form, keeping the validator in discovery mode.) + discoveryMode = true } } - const result = validate(nextForm) + const result = validate(nextForm, discoveryMode) if (!result) return saveMutation.mutate(result) })().catch((err) => { @@ -231,9 +275,55 @@ export function DialogCustomProvider(props: Props) {
setField("baseURL", v)} + validationState={form.err.baseURL ? "invalid" : undefined} + error={form.err.baseURL} + /> + setField("apiKey", v)} + /> +
+ +