Files
open-design/docs/rfc-drafts/agent-ready.md
f1a0b60c6c chore: unify product name display to OpenDesign repo-wide (#6998)
* chore: unify product name display to OpenDesign repo-wide

Rename the user-facing product name from "Open Design" to "OpenDesign"
across documentation, web/desktop UI strings, i18n locales, prompts,
plugin/skill/design-system content, changelogs, and specs, matching the
landing-page rename already in flight.

Deliberately excluded so installed-app identity stays untouched:
- packages/release PRODUCT_NAME and everything derived from it
  (app bundle names, installer/registry identity, updater feeds)
- tools/pack, tools/release, apps/packaged, landing page (PR #6983)
- mocks/ recorded traces and LICENSE
- test fixtures and comments that encode real on-disk install paths
  (Open Design.app, Program Files\Open Design, AppData paths)

Validated with pnpm guard, pnpm typecheck, and targeted web/desktop/host
test runs over the renamed strings.

* fix(web): keep two-letter author monogram for CamelCase brand names

authorInitials split words on whitespace only, so the renamed single-token
author name "OpenDesign" produced a one-letter monogram. Treat lower-to-upper
CamelCase boundaries as word breaks so the avatar fallback stays "OD".

* fix(release): restore spaced Windows source artifact names in win.ps1

tools/pack still derives builder outputs from the unchanged PRODUCT_NAME
("Open Design"), so the asset script must look for the spaced filenames.
Matches the mac/mac-intel/linux asset scripts, which were already kept on
the original names.

---------

Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
2026-08-18 07:09:45 +00:00

276 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RFC: Agent-ready OpenDesign — one assistant that can drive the whole app
**Status:** Draft (umbrella design for review; only slice 1 ships code in the first PR)
**Author:** @leonaburime-ucla
**Related:** #5398 (make OpenDesign agent-ready)
## Summary
Today the chat assistant only helps with the page you are on. This RFC proposes
turning it into one assistant that can drive the whole app — including taking
actions in the browser UI on the user's behalf — built product-neutrally so the
same engine could power other products on this foundation.
This umbrella RFC presents the WHOLE arc so maintainers and product reviewers can
evaluate it at once (the issue is labeled `needs-product-direction`). Only
**slice 1** ships code in the first PR: the shared contracts
(`packages/contracts/src/agent-tools/`) plus this RFC. Everything past slice 1 is
described here as types + prose, and the genuinely open calls are presented as
**options considered → recommendation** rather than decrees, so reviewers judge
the design space.
## Problem
- **The chat is stuck on one page; real work isn't.** A real task touches several
places (design system → deck → media), which today means several chats and a
lot of clicking.
- **People can't find what OD can do.** Capabilities are discoverable only by
navigating to their page; there is no one catalog of "what can you do?".
- **The inbound half is missing.** OD already emits its runs in **AG-UI format —
the outbound half** (`packages/agui-adapter` → `/api/agui`). There is no
inbound path where the assistant *executes* an interface action and gets the
result back. That return path is the genuinely new seam.
Two execution models must both be served: (a) the **BYOK daemon-loop**, where the
daemon runs the model turn and orchestrates tools; and (b) a **local-CLI agent**
subprocess (Claude/Codex/etc.) that can only reach tools via MCP.
## The capability model (declare / list / search / call / result)
Every capability is one **tool**, declared once and reachable through an
MCP/WebMCP-congruent surface:
| Concern | Shape (in `contracts`) | MCP analog |
|---|---|---|
| **Declare** | `AgentToolDescriptor` (`browser` \| `api` union) | tool definition |
| **List** | `AgentToolManifest` (`protocolVersion`, `runId`, `tools[]`) | `tools/list` |
| **Search** | `AgentToolSearchQuery` → `AgentToolSearchResult` (paged, `api` only) | discovery over `tools/list` |
| **Call — `browser`** | `BrowserActionRequest` (`invocationId`, `runId`, `tool`, `input`) — dispatch toward a live session | `tools/call` (browser surface) |
| **Result — `browser`** | `BrowserActionResult` (`ok`-discriminated; `result: JsonValue`) | tool result |
| **Call — `api`** | the tool's own declared `/api/*` route + `od` subcommand (`ApiToolDescriptor.api` / `.cli`) — no round-trip envelope | `tools/call` (api surface) |
`AgentToolDescriptor` carries `name` (stable, dot-namespaced), `description`
(model- and catalog-facing), `inputSchema` (JSON Schema as `JsonValue`, so it
crosses the MCP wire verbatim — WebMCP / MCP-UI / A2UI ready), and a `surface`
discriminant. A `browser` tool asserts `viewStateOnly: true`; an `api` tool
carries both its `/api/*` route and its `od` subcommand, so the UI/CLI dual-track
law is **structural**, not a review checkbox.
**The two surfaces are called through two distinct request paths — the envelope is
not universal.** An `api` capability is a normal `/api/*` action: the daemon
invokes its declared route directly (and it is reachable as an `od` subcommand),
so it needs no dispatch-and-await round-trip and does **not** use
`BrowserActionRequest`. `BrowserActionRequest` / `BrowserActionResult` exist
**only** for the `browser` surface — the genuinely new inbound seam — because a
browser tool runs in a remote context (the live session) the daemon must dispatch
to and await a result from. On that browser path the action identity is a
daemon-minted `invocationId`; a provider `tool_call_id` or MCP request id maps to
it daemon-side and never enters the contract, so `result: JsonValue` means a
navigation acknowledgement and a future data-returning browser read serialize
identically — no navigation special case. `protocolVersion` on the manifest and
on the browser-action messages is the literal `typeof AGENT_ACTIONS_PROTOCOL_VERSION`,
so catalog and browser calls version together.
## Complex / multi-step workflows
The slice-1 shapes support complex work, not just one action:
- **Chaining.** The agent loop drives one step at a time: each step is a
`BrowserActionRequest` correlated to its `BrowserActionResult` by
`invocationId`; because `result` is `JsonValue`, one step's output feeds the
next step's `input`. No workflow object is required for sequential work.
- **Concurrency.** Each invocation has its own `invocationId`, so independent
invocations may be in flight at once; the daemon reconciles each result to its
own record. Nothing in the shapes forces serialization.
**Deferred:** streaming/progress for long-running steps (results are terminal in
slice 1), and a first-class task/workflow object that can span projects — slice 5,
with its own permissions RFC.
## Agent tool search — options
As OD accumulates tools across design systems, media, connectors, and Composio,
handing the model the full catalog gets expensive — the daemon's own MCP layer
notes a `tools/list` response costs on the order of ~150 tokens per call. So the
model should **find** relevant tools, not receive all of them. Two orthogonal
axes: how matches are computed (A/B/C), and how much of a tool enters model
context at all (D).
**Option A — Keyword / substring (SQL `LIKE`/FTS5 over name + description).**
Query the persisted registry over `name` and `description`.
*Pros:* zero new infra, deterministic, debuggable, no model dependency, cheap.
*Cons:* weak on vague intent ("make the images match the brand" won't match
`imagegen.recolor` by keyword); sensitive to naming discipline.
**Option B — Semantic / embedding search.**
Embed each descriptor; store vectors; rank the query by cosine similarity.
*Pros:* best recall on fuzzy intent; robust to vocabulary mismatch.
*Cons:* needs an embedding model + a vector store, adds cost + latency + a model
dependency, and non-determinism that complicates review and tests. Overkill until
tool count and fuzzy-intent misses justify it.
**Option C — Hybrid (keyword prefilter → semantic rank).**
`LIKE`/FTS narrows to a candidate set; embeddings rank within it.
*Pros:* bounds embedding cost to a small candidate set while keeping fuzzy recall.
*Cons:* two subsystems to build and keep consistent; premature before B is even
warranted.
**Option D — Deferred tools / progressive disclosure (context-budget strategy,
orthogonal to A/B/C).**
Only tool **names** (and one-line descriptions) enter model context; the full
`inputSchema` is fetched on demand via search — exactly how coding-agent harnesses
expose a "ToolSearch" over deferred tools. This is not a matching algorithm; it is
what the model *sees*, and it composes with whichever of A/B/C computes the match.
**Advertisement axis — PUSH vs PULL.**
- *PUSH:* the frontend advertises its live `AgentToolManifest` on mount / on tool
registration. **Required for browser-surface tools** — the daemon cannot know
which browser tools are live without the session telling it.
- *PULL:* the agent calls `capability.search` on demand against the persisted
registry. Transport-agnostic; works for local-CLI-via-MCP; the natural fit for
the large, persistent `api`-surface catalog.
**Recommended slice-3 default: A + D, with hybrid PUSH/PULL.** Ship keyword/FTS5
matching (A) under deferred/progressive disclosure (D): names-only in context,
schemas fetched on `capability.search`. Advertise **browser** tool availability by
PUSH (the session tells the daemon what is live) and discover the **api** catalog
by PULL (`capability.search`). Defer B/C until we have evidence of fuzzy-intent
misses at real tool counts — adding embedding ranking later is a
runtime/`providers` change behind the same `search` port and needs no contract
change (see "Non-breaking type hooks").
## Options considered → recommendation (key decisions)
**Return path.**
Options: (1) reuse GenUI `POST /api/runs/:runId/genui/:surfaceId/respond`;
(2) a NEW run-scoped `POST /api/runs/:runId/actions/:invocationId/result`;
(3) make `agui-adapter` inbound.
Tradeoff: (1) is live and proven but its surface/`respondedBy` lifecycle is shaped
for HITL surfaces, so every future action feature would mutate a table owned by
another concern; (3) welds the return path to one wire protocol and abandons the
native non-AG-UI chat path.
**Recommend (2)** — a dedicated endpoint *patterned on* GenUI's pending→responded
lifecycle without overloading `genui_surfaces`, keeping action semantics
(timeouts, consent, correlation) in their own home.
**Registry persistence.**
Options: in-memory; the daemon's SQLite store under the resolved data root
(`RUNTIME_DATA_DIR`); a flat file.
Tradeoff: in-memory loses the catalog on restart and can't scale to "too many
tools" or support search; a flat file re-implements indexing badly.
**Recommend SQLite** under the resolved data root — it scales, gives FTS5 for
Option A for free, and matches how the daemon already persists state. (Per the
repo's Daemon data directory contract, this RFC names no filesystem path; the
store derives from the resolved data root.)
**Advertisement scope.**
Options: per-run; per-session/tab; per-app-shell.
Tradeoff: browser tools are only live while their tab is mounted, so app-shell
scope over-claims availability and per-run alone can't see a tab that mounted
mid-run.
**Recommend per-session/tab availability advertised into the active run** — the
manifest granularity stays `manifest(runId)`, but browser-tool availability is
sourced from the live session, reconciled to the run.
**Action identity.**
Options: daemon-minted `invocationId`; provider `toolCallId`.
Tradeoff: `toolCallId` is free in the BYOK loop but absent in the MCP bridge and
scoped per-provider-turn, so using it as identity binds the contract to one
execution model.
**Recommend daemon-minted `invocationId`** as the sole contract identity; a
`toolCallId`/MCP request id maps to it daemon-side and never enters the contract.
**Result transport.**
Options: `POST` return; a websocket/SSE push channel for lower latency.
Tradeoff: `POST` is simplest and the browser already has an authenticated HTTP
path; a push channel lowers latency but adds a persistent connection and
reconnection semantics.
**Recommend `POST` for slices 2–3**; revisit a push channel only if action
round-trip latency becomes a measured problem. Dispatch (daemon→browser) already
rides the existing outbound run-event stream, so only the return leg is at issue.
## Registry boundary
The descriptor + manifest + search shapes and the `AgentToolRegistry` **port**
live in `packages/contracts` (this PR, pure types). The registry **runtime**
(registration + lookup + SQLite persistence) lands in slice 2 in a NEW standalone
`packages/agent-tools` package **forbidden from importing `apps/*` or any
project-specific code** — the guard against re-inventing a `ProjectView`-shaped
abstraction — enforced by a `scripts/check-cross-app-imports.ts`-style guard wired
into `pnpm guard`. Design tools register themselves as entries; the list itself
stays product-neutral.
## Return path
Transport-agnostic across both execution models, converging on a daemon-side
invocation record keyed by `invocationId`:
- **Dispatch** rides the existing outbound run-event stream as a new event kind
carrying a `BrowserActionRequest`.
- **Return** is a new run-scoped endpoint
`POST /api/runs/:runId/actions/:invocationId/result`, patterned on GenUI's
pending → responded lifecycle (see the return-path option above).
- **Local-CLI agents** reach browser tools via a per-run **frontend-bridge MCP
server** extending `buildLiveArtifactsMcpServersForAgent`; its handlers await
the same invocation record.
## Dual-track exemption
> A tool may be `surface: 'browser'` — and ship without an `od` CLI form — ONLY
> if its whole effect is an ephemeral browser view movement (navigate, scroll,
> focus, panel visibility, current selection) and nothing it produces outlives the
> browser session. If it creates, mutates, or reads anything that persists beyond
> the session, or could be meaningfully invoked with no browser attached, it is a
> capability: `surface: 'api'` with both an `/api/*` route and an `od` subcommand.
Litmus: *"could it be meaningfully invoked with no browser attached?"* must FAIL
for a browser tool. "Go to Media" fails it (exempt); "export the deck" passes it
(a capability — cannot hide behind the exemption).
## Slice roadmap
| Slice | Deliverable | Ships code? |
|---|---|---|
| **Prereq** | `ChatComposer` decomposition — the active vertical-slice canary, **in progress**; prerequisite for a global mount. | in progress |
| **1 (this PR)** | Contracts (`agent-tools/`: descriptor, actions, manifest, registry port) + this RFC. | ✅ types only |
| **2** | Guarded `packages/agent-tools` runtime + SQLite persistence + one browser nav tool AND one browser click tool + the return endpoint → prove one action round-trips live. | ✅ |
| **3** | Search (`capability.search`, default A + D) + PUSH/PULL advertisement handshake + "what can you do?" catalog view + frontend-bridge MCP server for local-CLI agents. | ✅ |
| **4** | Global chat mount (one assistant, not three page-bound ones). | ✅ |
| **5** | Cross-project task ownership + bounded permissions — see [companion RFC](./agent-ready-cross-project.md) (`AgentTask` + permissions options). | own RFC |
## Non-breaking type hooks — ruled out for now
Adding an optional field later is itself non-breaking, so slice 1 stays minimal:
- **`AgentToolSearchResult` item `score?`** — ruled OUT. Only Option B needs it;
add it (optional) when embedding ranking lands. Non-breaking then.
- **`AgentToolSearchQuery.tags?: string[]`** — ruled OUT. No taxonomy exists yet;
add optionally if a tag facet appears.
- **`AgentToolManifest` `hasMore?`/cursor** — ruled OUT. The manifest is the
*full* small-run catalog; the large-registry path is `search()`, which already
pages. Adding paging to the manifest would duplicate that seam.
## Deferred / Out of scope (this PR)
- Any behavior/feature code — slice 1 is contracts + this RFC only.
- The `packages/agent-tools` runtime, its guard, and SQLite persistence (slice 2).
- Search execution, the advertisement handshake, the catalog view, and the
frontend-bridge MCP server (slice 3).
- Global chat mount (slice 4); cross-project ownership + permissions (slice 5).
- Consent UX (`USER_DENIED` is reserved in the contract but unwired).
- Multi-tab / multi-session arbitration; streaming/progress results;
result-size limits; `resultSchema` validation; any `scope`/permission fields.
- No new chat framework, no changes to `agui-adapter` / `genui_surfaces` / the run
engine in this PR.
## Open questions
1. **Product direction** — is the browser-action bridge the right first
investment (the `needs-product-direction` question), ahead of global chat?
2. **Search default** — is A + D sufficient for slice 3, or do reviewers expect
semantic search (B/C) from day one?
3. **Consent** — when does a browser action need explicit confirmation, and should
`USER_DENIED` gate destructive `api`-surface tools too?
4. **Manifest freshness** — does a run re-fetch the manifest as tools register
(PUSH), or snapshot it at run start?