- name
- agenta-package-practices
- description
- Where to put frontend code (package vs the web/mobile app layer) and how to use the @agenta/* packages. Use when authoring or moving code in web/packages, choosing which @agenta/* package owns something (ui, entities, entity-ui, shared, chat, sessions, playground, the *-ui domain packages, ...), using molecules, the loadable bridge, the EntityPicker, or writing package unit tests.
# Agenta package practices
This skill is the source of truth for where frontend code lives and how the
`@agenta/*` workspace packages are used. Load it when you author or move code in
`web/packages/`, decide between the app layer and a package, or work with the entity
state primitives (molecules, bridges, pickers) or package unit tests.
## When to use this skill
- Deciding whether new code belongs in `web/mobile` (the app layer) or a package.
`web/oss` and `web/ee` are the abandoned desktop app: never add code there.
- Choosing which of the `@agenta/*` packages owns a piece of code, or importing from one.
- Building with molecules, the loadable bridge, or the EntityPicker.
- Writing or fixing unit tests inside a package under `web/packages/*/tests/`.
## Code placement: packages vs application code
### Quick heuristic
```text
Is the code used by 2+ features, or could be?
├─ NO → Keep it in the app layer (web/mobile/src/features/<feature>/)
└─ YES → Move it to a package, picking by purpose:
├─ Pure utility / type (no React) → @agenta/shared
├─ Reusable UI component / style util → @agenta/ui
├─ Entity state + API (molecule, atoms) → @agenta/entities
├─ Entity-specific UI (modals, pickers, drive) → @agenta/entity-ui
├─ Agent chat (model, transport, components) → @agenta/chat
├─ Domain logic, no screens → the domain's headless package
│ (annotation, auth, navigation, observability, playground,
│ sessions, settings, skills)
└─ Domain UI / a whole surface → the domain's -ui package
(annotation-ui, auth-ui, automation-ui, home-ui, navigation-ui,
observability-ui, playground-ui, sessions-ui, settings-ui, skills-ui)
```
A new domain gets a headless/`-ui` pair when it has both state and UI; see
`web/packages/README.md` for the full list with one-line purposes.
### Hard rules
- **Respect the layers.** Dependencies point one way, down this stack:
1. `@agenta/shared` (imports no other package); `@agentaai/api-client` ← `@agenta/sdk`
2. `@agenta/ui` (shared only)
3. `@agenta/entities` (sdk, shared, ui)
4. headless domain packages (entities, shared, sdk; `navigation` also reads
`sessions`, `settings` reads `navigation` and `ui`). They own state and API calls,
not screens, and never import `entity-ui`, `chat`, or a `-ui` package.
5. `@agenta/entity-ui`, `@agenta/chat`, and the `-ui` packages. These may import each
other (e.g. `entity-ui` → `sessions-ui`, `playground-ui` → `chat`), but never in
a cycle.
Declare every `@agenta/*` import in the package's `package.json`. Circular imports
break the build.
- **No legacy compat shims in packages.** Keep `OldFormat → NewFormat` adapters in the
app layer. Packages stay clean.
- **No `any` types.** Packages enforce `@typescript-eslint/no-explicit-any: error`.
- **Never import the `queryClient` singleton.** Reach the cache with
`getHostQueryClient()` from `@agenta/shared/api`, resolved per call. The singleton is the
host's to install; on a host that brought its own client, writes to it silently do nothing.
Lint-enforced. Contract: `web/AGENTS.md` § "The QueryClient host contract".
- **Use exported subpaths**, not internal paths:
`import {x} from "@agenta/entities/testcase"`, not
`from "@agenta/entities/src/testcase/state/molecule"`.
- **Verify your change builds AND lints before pushing:**
`pnpm turbo run build --filter=@agenta/<package>` and
`pnpm turbo run lint --filter=@agenta/<package>`.
## Package overview
There are 24 `@agenta/*` packages plus `@agentaai/api-client`. The full list, with a
one-line purpose each, is `web/packages/README.md`. The core ones:
| Package | Purpose | Key exports |
| --- | --- | --- |
| `@agenta/shared` | Utilities, types, schemas, hooks; host QueryClient API | `./api`, `./state`, `./utils`, `getHostQueryClient` |
| `@agenta/ui` | Shared UI kit | shadcn primitives (`./ui`), `EnhancedModal`, `InfiniteVirtualTable`, `cn`, `textColors`, presentational components |
| `@agenta/sdk` | Fern client wrapper | per-resource clients in `./resources` |
| `@agenta/entities` | Entity state and API calls | molecules (`workflowMolecule`, `testcaseMolecule`, ...), the loadable bridge |
| `@agenta/entity-ui` | Entity-specific UI | `EntityPicker`, modals, drawers, drill-in, the drive |
| `@agenta/chat` | Agent chat | model, transport, state, hooks, chat components |
| `@agenta/playground` | Playground state controllers | `./state`, `./snapshot`, `./agent-chat` |
| `@agenta/playground-ui` | Playground UI | comparison view, execution items, agent build, commit |
READMEs: `agenta-ui`, `agenta-entities`, `agenta-entity-ui`, `agenta-shared`,
`agenta-playground`, `agenta-playground-ui`, and `agenta-api-client` each have a
`README.md` at the package root.
## Subpath imports for tree-shaking
Always use subpath imports. Importing from a root barrel (`@agenta/shared`) pulls the
entire dependency graph and inflates the bundle.
`@agenta/shared`:
```typescript
import {axios, getAgentaApiUrl, getEnv, configureAxios} from "@agenta/shared/api"
import {projectIdAtom, setProjectIdAtom} from "@agenta/shared/state"
import {dayjs, isValidUUID, getValueAtPath, setValueAtPath, formatNumber, formatLatency} from "@agenta/shared/utils"
import {useDebounceInput} from "@agenta/shared/hooks"
import {MESSAGE_CONTENT_SCHEMA, CHAT_MESSAGE_SCHEMA} from "@agenta/shared/schemas"
import type {SimpleChatMessage, MessageContent, ToolCall} from "@agenta/shared/types"
```
`@agenta/ui`:
```typescript
import {...} from "@agenta/ui/ui" // shadcn primitives (Button, Dialog, Sheet, ...)
import {...} from "@agenta/ui" // presentational components, cn, textColors
import {...} from "@agenta/ui/table" // InfiniteVirtualTable, paginated stores
import {...} from "@agenta/ui/editor" // Editor, JSON parsing utilities
import {...} from "@agenta/ui/shared-editor" // SharedEditor, useDebounceInput
import {...} from "@agenta/ui/chat-message" // ChatMessageEditor, message types/schemas
import {...} from "@agenta/ui/llm-icons" // LLM provider icons
import {...} from "@agenta/ui/cell-renderers" // Table cell renderers, CellRendererRegistry
```
`@agenta/entities`:
```typescript
import {...} from "@agenta/entities/workflow" // workflowMolecule (apps, evaluators, agents)
import {...} from "@agenta/entities/shared" // molecule factories, transforms
import {...} from "@agenta/entities/trace" // trace/span molecule, schemas
import {...} from "@agenta/entities/testset" // testset/revision molecules
import {...} from "@agenta/entities/testcase" // testcase molecule
import {...} from "@agenta/entities/loadable" // loadable bridge
import {...} from "@agenta/entities/runnable" // execution utilities, port helpers
import {...} from "@agenta/entity-ui/selection" // EntityPicker and adapters
```
## Modals
`EnhancedModal` from `@agenta/ui` is an antd-`Modal`-compatible facade over the
`@agenta/ui` Radix `Dialog`, kept so existing call sites work unchanged. Use it
instead of raw antd `Modal` in package code. In `web/mobile`, and for new
surfaces, compose `Dialog`/`Sheet` from `@agenta/ui/ui` directly.
```typescript
import {EnhancedModal, ModalContent, ModalFooter} from "@agenta/ui"
function MyModal({open, onClose}: {open: boolean; onClose: () => void}) {
return (
<EnhancedModal open={open} onCancel={onClose} title="Modal Title" footer={null}>
<ModalContent>{/* Main content */}</ModalContent>
<ModalFooter>
<Button onClick={onClose}>Cancel</Button>
<Button type="primary">Confirm</Button>
</ModalFooter>
</EnhancedModal>
)
}
```
Why: consistent styling, proper scroll handling via `ModalContent`, standardized footer
via `ModalFooter`, theme integration.
## Style utilities and presentational components
When adding or changing UI elements, implement appearance and interaction states for both light and dark themes, and verify both before considering the work complete.
```typescript
import {cn, textColors, bgColors} from "@agenta/ui"
<div className={cn("base-class", isActive && "active-class")} />
<span className={textColors.secondary}>Secondary text</span>
<div className={bgColors.hover}>Hoverable area</div>
```
Section layout primitives from `@agenta/ui`:
```typescript
import {
SectionCard, SectionLabel, SectionHeaderRow, ConfigBlock, VersionBadge,
RevisionLabel, StatusTag, PanelHeader, SourceIndicator,
} from "@agenta/ui"
<SectionCard>
<SectionHeaderRow left={<SectionLabel>Configuration</SectionLabel>} right={<Button>Edit</Button>} />
<ConfigBlock title="Settings"><Input /></ConfigBlock>
</SectionCard>
```
## Package selection guide
```text
Need a primitive (button, dialog, ...)? → @agenta/ui/ui
Need a modal? → Dialog from @agenta/ui/ui (EnhancedModal for antd-style call sites)
Class-name utils or theme colors? → cn, textColors, bgColors from @agenta/ui
Section layout primitives? → SectionCard, SectionLabel, ConfigBlock from @agenta/ui
Entity state management (molecules)? → *Molecule from @agenta/entities/{entity}
Entity selection UI? → EntityPicker from @agenta/entity-ui/selection
Loadable bridge? → loadableBridge from @agenta/entities/loadable
Building playground features? → state from @agenta/playground, UI from @agenta/playground-ui
Chat, sessions, observability, ...? → the domain's headless + -ui package pair
```
## Molecule pattern (entity state management)
Full documentation: `web/packages/agenta-entities/src/shared/README.md`.
A molecule provides a unified API for entity state with draft, loading, and cache:
```typescript
molecule.atoms.* // atom families for reactive subscriptions
molecule.reducers.* // write operations
molecule.get.* / set.* // imperative reads/writes (snapshot from store)
molecule.useController // React hook combining atoms + dispatch
molecule.cleanup.* // memory management
```
Where to use which API:
```text
React component → useAtom / molecule.useController
Inside an atom → get(molecule.atoms.*)
Callback/effect → molecule.get.* / molecule.set.*
```
```typescript
import {testcaseMolecule} from "@agenta/entities/testcase"
function TestcaseEditor({id}: {id: string}) {
const [state, dispatch] = testcaseMolecule.useController(id)
if (state.isPending) return <Skeleton />
if (!state.data) return <NotFound />
return <Input value={state.data.input} onChange={(e) => dispatch.update({input: e.target.value})} />
}
// Fine-grained subscription: only re-renders when isDirty changes
function DirtyIndicator({id}: {id: string}) {
const isDirty = useAtomValue(testcaseMolecule.atoms.isDirty(id))
return isDirty ? <Badge>Modified</Badge> : null
}
```
Imperative API for callbacks:
```typescript
async function handleSave(id: string) {
const data = testcaseMolecule.get.data(id)
if (!data || !testcaseMolecule.get.isDirty(id)) return
await api.save(data)
testcaseMolecule.set.discard(id)
}
```
Examples: `workflowMolecule` (`@agenta/entities/workflow`), `testcaseMolecule`
(`@agenta/entities/testcase`), `traceSpanMolecule` (`@agenta/entities/trace`),
`testsetMolecule` and `revisionMolecule` (`@agenta/entities/testset`). Other entity
subpaths (`session`, `secret`, `gatewayTool`, ...) follow the same pattern.
Data flow: `Server → TanStack Query → atoms.serverData → atoms.draft (local changes) →
atoms.data (merged) → useController → component`.
Anti-patterns:
```typescript
// BAD - atoms require React context
async function handleSave(id) { const data = useAtomValue(molecule.atoms.data(id)) } // breaks
// GOOD - imperative API
async function handleSave(id) { const data = molecule.get.data(id) }
// BAD - new atom every render
const derived = atom((get) => get(molecule.atoms.data(id)))
// GOOD - memoize the atom
const derived = useMemo(() => atom((get) => get(molecule.atoms.data(id))), [id])
```
## Loadable bridge pattern
Full documentation: `web/packages/agenta-entities/src/loadable/README.md`.
A loadable represents a data source that provides input rows (local mode = manual entry,
connected mode = synced with a testset revision or trace).
```typescript
import {loadableBridge} from "@agenta/entities/loadable"
import {useAtomValue, useSetAtom} from "jotai"
const rows = useAtomValue(loadableBridge.selectors.rows(loadableId))
const addRow = useSetAtom(loadableBridge.actions.addRow)
addRow(loadableId, {prompt: "Hello, world!"})
const connect = useSetAtom(loadableBridge.actions.connectToSource)
connect(loadableId, testsetRevisionId, "MyTestset v1", "testcase")
```
Selectors: `rows`, `columns`, `activeRow`, `mode`, `isDirty`, `connectedSource`.
View on GitHub