docs: improve goose2 AGENTS.md (#9028)
This commit is contained in:
+50
-30
@@ -29,7 +29,40 @@ Treat this repo as partially Hermit-managed. Do not assume `just`, `pnpm`, `node
|
||||
- `just ci` is the main local verification gate.
|
||||
- `just clean` removes Rust build artifacts, `dist`, and `node_modules`, so `just setup` is required again before `just dev`.
|
||||
|
||||
## Architecture & File Structure
|
||||
## Architecture
|
||||
|
||||
### The frontend → ACP → goose core path
|
||||
|
||||
**All frontend ↔ backend communication in goose2 flows through a single path:**
|
||||
|
||||
```
|
||||
React UI ──► features/<feature>/api/ ──► @aaif/goose-sdk (TS) ──► goose-acp (WebSocket, ACP) ──► goose (core)
|
||||
```
|
||||
|
||||
**YOU MUST TREAT THE CLIENT as a THIN CLIENT****
|
||||
|
||||
- The Tauri shell spawns a long-lived `goose serve` process and exposes its WebSocket URL via the `get_goose_serve_url` Tauri command. That is essentially the only Tauri command the frontend needs for backend work — it is how the renderer discovers the ACP endpoint.
|
||||
- The frontend opens a WebSocket to `goose serve` and talks to it using `@aaif/goose-sdk` (published from `ui/sdk/`). The SDK is generated from the ACP custom-method definitions in `crates/goose-sdk/src/custom_requests.rs`, so every backend method has a typed TypeScript client method.
|
||||
- The goose2 TypeScript code should only be UI
|
||||
- `goose-acp` (`crates/goose-acp/src/server.rs`) is the server side of the WebSocket. It implements handlers for the custom ACP methods and calls into the `goose` core crate to do the actual work (providers, config, sessions, dictation, etc.).
|
||||
- `goose` is the pure domain crate. It knows nothing about Tauri or WebSockets — it just exposes Rust APIs that `goose-acp` handlers invoke.
|
||||
|
||||
**This is the pattern you must follow when adding any new backend-touching feature.** When you are vibecoding in this app, it is very tempting to reach for `invoke()` or add an HTTP fetch — don't. The rule is: if a feature needs to talk to `goose` core, it goes through the SDK → ACP → goose chain above.
|
||||
|
||||
### Don't build features entirely in the frontend
|
||||
|
||||
If a feature involves data, persistence, secrets, provider config, sessions, filesystem access, network calls to external services, or anything else that could plausibly be reused by the CLI or another goose surface, **the logic belongs in the `goose` core crate, exposed via a typed ACP method.** Do not:
|
||||
|
||||
- Stand up a feature whose business logic lives in a Zustand store, a React hook, or a `features/<feature>/api/` adapter that calls `localStorage`, `fetch`, or filesystem APIs directly.
|
||||
- Reach for `invoke()` to add a new Tauri command that proxies into `goose` — add an ACP custom method instead.
|
||||
- Do not use `localStorage` except for things it's absolutely needed for (specifically outlined in other parts of this AGENTS.md)
|
||||
- Duplicate types between TS and Rust — let the SDK generation produce them from `crates/goose-sdk/src/custom_requests.rs` and use the generated types ALWAYS. Do not create a shadow of a type you could use from the generated types.
|
||||
|
||||
The frontend's job is presentation, navigation, and orchestration of typed SDK calls. If you find a feature growing real logic on the React side, that's a signal to push it down into `goose-acp` + `goose`.
|
||||
|
||||
### File structure
|
||||
|
||||
The directory layout is organized to reinforce the path above. Every feature that touches the backend has an `api/` module that wraps `GooseClient` calls — UI and stores never touch the SDK directly.
|
||||
|
||||
```
|
||||
src/
|
||||
@@ -38,35 +71,33 @@ src/
|
||||
<feature>/
|
||||
ui/ — React components (required)
|
||||
hooks/ — Custom React hooks for feature logic (when needed)
|
||||
stores/ — Zustand state management (when feature needs shared state)
|
||||
api/ — Backend API integration (when feature calls backend)
|
||||
stores/ — Zustand state management (frontend-only UI state; not a substitute for goose core)
|
||||
api/ — Thin wrappers around GooseClient SDK calls (the ONLY place ACP is touched)
|
||||
types.ts — Feature-specific type definitions (when needed)
|
||||
shared/
|
||||
types/ — Canonical shared type definitions (single source of truth)
|
||||
agents.ts — Agent, Persona, Provider types
|
||||
chat.ts — ChatState, TokenState, Session, SSE events
|
||||
messages.ts — Message, MessageContent, type guards
|
||||
ui/ — Reusable UI components (button, etc.)
|
||||
lib/ — Utilities (cn.ts for class merging)
|
||||
theme/ — Theme provider, appearance settings
|
||||
styles/ — Global CSS, design tokens
|
||||
hooks/ — Shared hooks
|
||||
api/ — API integration
|
||||
api/ — Shared GooseClient wrappers used by multiple features (e.g. dictation)
|
||||
constants/ — Shared constants
|
||||
context/ — Shared contexts
|
||||
```
|
||||
|
||||
### Feature Organization
|
||||
|
||||
Not every feature needs every subdirectory. Use only what the feature requires:
|
||||
Not every feature needs every subdirectory. Use only what the feature requires. Note that anything beyond `ui/` only — i.e. anything with state or backend calls — should already have a corresponding ACP method on the goose side.
|
||||
|
||||
| Pattern | Structure | Examples |
|
||||
|----------------------|----------------------------------|----------------------|
|
||||
| **Full-featured** | `stores/` + `hooks/` + `ui/` | agents, chat |
|
||||
| **Data-driven** | `stores/` + `api/` + `ui/` | projects |
|
||||
| **API features** | `api/` + `ui/` | skills |
|
||||
| **Simple features** | `ui/` only | home, settings, sidebar, status |
|
||||
| **Tabs** | `ui/` + `types.ts` | tabs |
|
||||
| Pattern | Structure | Examples | Backend shape |
|
||||
|----------------------|----------------------------------|---------------------------------|------------------------------------------------|
|
||||
| **Full-featured** | `stores/` + `hooks/` + `ui/` + `api/` | agents, chat | Typed ACP methods drive the store |
|
||||
| **Data-driven** | `stores/` + `api/` + `ui/` | projects | CRUD via ACP, store caches results |
|
||||
| **API features** | `api/` + `ui/` | skills, providers | Pure pass-through to ACP |
|
||||
| **Simple features** | `ui/` only | home, settings, sidebar, status | No backend (pure presentation) |
|
||||
| **Tabs** | `ui/` + `types.ts` | tabs | No backend (frontend-only UI state) |
|
||||
|
||||
If you're tempted to build a "Full-featured" or "Data-driven" feature without a corresponding `api/` module that calls a typed ACP method, stop and add the ACP method first. See "The canonical example" below.
|
||||
|
||||
### Import Rules for Features
|
||||
|
||||
@@ -74,6 +105,7 @@ Not every feature needs every subdirectory. Use only what the feature requires:
|
||||
- There should be NO root-level `src/stores/` or `src/types/` directories.
|
||||
- Feature stores use feature-relative imports (e.g., `../stores/featureStore`).
|
||||
- Cross-feature imports use `@/features/*/stores/` or `@/shared/types/`.
|
||||
- Only `features/<feature>/api/` and `shared/api/` modules may import from `@aaif/goose-sdk` or call `getClient()`. UI components, hooks, and stores must go through those wrappers.
|
||||
|
||||
## Coding Conventions
|
||||
|
||||
@@ -132,20 +164,7 @@ ThemeProvider manages three axes:
|
||||
- Traffic light offset: `pl-20` (80px) to accommodate macOS window controls.
|
||||
- Distro bundle behavior, including feature flags, is documented in `distro/README.md`.
|
||||
|
||||
## Architecture
|
||||
|
||||
**All frontend ↔ backend communication in goose2 flows through a single path:**
|
||||
|
||||
```
|
||||
React UI ──► @aaif/goose-sdk (TS) ──► goose-acp (WebSocket, ACP) ──► goose (core)
|
||||
```
|
||||
|
||||
- The Tauri shell spawns a long-lived `goose serve` process and exposes its WebSocket URL via the `get_goose_serve_url` Tauri command. That is essentially the only Tauri command the frontend needs for backend work — it is how the renderer discovers the ACP endpoint.
|
||||
- The frontend opens a WebSocket to `goose serve` and talks to it using `@aaif/goose-sdk` (published from `ui/sdk/`). The SDK is generated from the ACP custom-method definitions in `crates/goose-sdk/src/custom_requests.rs`, so every backend method has a typed TypeScript client method.
|
||||
- `goose-acp` (`crates/goose-acp/src/server.rs`) is the server side of the WebSocket. It implements handlers for the custom ACP methods and calls into the `goose` core crate to do the actual work (providers, config, sessions, dictation, etc.).
|
||||
- `goose` is the pure domain crate. It knows nothing about Tauri or WebSockets — it just exposes Rust APIs that `goose-acp` handlers invoke.
|
||||
|
||||
**This is the pattern you must follow when adding any new backend-touching feature.** When you are vibecoding in this app, it is very tempting to reach for `invoke()` or add an HTTP fetch — don't. The rule is: if a feature needs to talk to `goose` core, it goes through the SDK → ACP → goose chain above.
|
||||
## Working in the ACP path: examples and rules
|
||||
|
||||
### The canonical example: skills-as-sources (PR #8675)
|
||||
|
||||
@@ -253,3 +272,4 @@ Additional tooling notes:
|
||||
- Never use `--no-verify` when pushing — fix the underlying lint/hook issues.
|
||||
- Don't create root-level `src/types/` or `src/stores/` directories — types belong in `src/shared/types/`, stores belong in `src/features/<feature>/stores/`.
|
||||
- Don't duplicate type definitions across files — each type has one canonical location.
|
||||
- Don't build features end-to-end in the frontend. If it talks to data, secrets, providers, sessions, or the filesystem, it goes through `goose` core via a typed ACP method. See **Architecture → Don't build features entirely in the frontend**.
|
||||
|
||||
Reference in New Issue
Block a user