--- name: integrate-fusion-agent description: >- Integrates a Flows/Dune app with the Fusion built-in PAIA agent panel using @cognite/app-sdk. Use this skill whenever a developer wants to: open the agent panel from their app, send the agent a contextual message, let the agent read app state (resources), or let the agent call actions in the app. Triggers: "fusion agent", "PAIA", "agent panel", "sendAgentMessage", "sendAgentLayoutMode", "agent server", "registerAgentServer", "connectToHostApp", "agent integration", "agent sidebar", "app-sdk agent". Always use this skill instead of manually writing agent integration code — it sets up the correct lifecycle, graceful fallback, and recommended file structure. allowed-tools: Read, Glob, Grep, Edit, Write, Bash --- # Integrate Fusion Agent Panel Wire a Flows/Dune app into the Fusion built-in PAIA agent using `@cognite/app-sdk`. There are three independent capabilities — implement only the ones needed: 1. **Open the agent panel** — a button that shows the sidebar/fullscreen agent UI 2. **Send the agent a message** — inject context into the chat (e.g. on item click) 3. **Register an agent server** — expose app state (resources) and actions the agent can call --- ## Step 0 — Understand the app Before writing any code, read: - `package.json` — detect package manager and whether `@cognite/app-sdk` is already installed - `src/App.tsx` (or main entry) — understand current structure, existing SDK usage Ask the user which of the three capabilities they need if it's not clear from context. --- ## Step 1 — Install the SDK If `@cognite/app-sdk` is not already in `package.json`, install it: ```shell pnpm add @cognite/app-sdk # or npm/yarn depending on the app ``` Minimum required version: `0.3.1` --- ## Step 2 — Connect to the host app All capabilities require a `HostAppAPI` instance. Obtain it once on mount and store it in React state or context. Always catch the rejection — the SDK throws when running outside Fusion (e.g. standalone `vite dev`). **Pattern for React apps:** ```typescript // src/hooks/useHostApp.ts import { useState, useEffect } from 'react'; import { connectToHostApp, type HostAppAPI } from '@cognite/app-sdk'; export function useHostApp(): HostAppAPI | null { const [api, setApi] = useState(null); useEffect(() => { connectToHostApp({ applicationName: 'my-app' }) .then(({ api: resolvedApi }) => { // IMPORTANT: use the updater form here. Comlink proxies are callable // objects, so setApi(proxy) causes React to invoke the proxy as a // state-updater function — storing a Promise instead of the proxy. // setApi(() => proxy) returns the proxy as the new state value. setApi(() => resolvedApi); }) .catch(() => { // Running outside Fusion — agent features disabled, no-op }); }, []); return api; } ``` Call `useHostApp()` at the root of your app and pass `api` down (or put it in context). When `api` is `null`, all agent UI triggers should be hidden or disabled — not shown as broken. --- ## Step 3 — Opening the agent panel Wire a persistent toolbar button (or equivalent trigger) to `api.sendAgentLayoutMode`. ```typescript import { type AgentLayoutPayload } from '@cognite/app-sdk'; // Open as sidebar (most common) await api.sendAgentLayoutMode({ mode: 'sidebar' }); // Other modes await api.sendAgentLayoutMode({ mode: 'fullscreen' }); await api.sendAgentLayoutMode({ mode: 'closed' }); ``` The button should only render when `api` is not null — agent features are unavailable outside Fusion. ```tsx {api && ( )} ``` --- ## Step 4 — Sending the agent a message Use `sendAgentMessage` on contextual triggers (e.g. "Analyse this item" button). Always pair it with `sendAgentLayoutMode` so the panel is visible. ```typescript // Open sidebar then inject context await api.sendAgentLayoutMode({ mode: 'sidebar' }); await api.sendAgentMessage({ message: `Analyse the schedule for "${itemName}" and suggest how to reduce total duration.`, newSession: true, // clears previous conversation — appropriate for contextual entry points }); ``` Use `newSession: true` when the user is starting a new task from a specific item. Omit it when you want to continue an existing conversation. The message text should include relevant context the agent can act on immediately — item names, IDs, current state summary. --- ## Step 5 — Registering an agent server An agent server exposes **resources** (read-only app state the agent can read) and **actions** (tools the agent can invoke). Register once on mount, unregister on unmount. ### Recommended file structure Separate concerns so each piece is independently testable: ``` src/features/agent/ agentActions.ts — pure factory: (deps) => Action[] agentResources.ts — pure factory: (deps) => Resource[] useAgentServer.ts — useEffect lifecycle hook; calls the factories and registers ``` ### Resources Resources are the agent's window into app state. Write `description` as you would a function docstring — the agent reads it to decide when to fetch the resource. ```typescript // src/features/agent/agentResources.ts import { createAgentResource } from '@cognite/app-sdk'; import type { StorageService } from '../storage/StorageService'; export function buildAgentResources(storage: StorageService) { return [ createAgentResource({ uri: 'my-app://current-state', name: 'Current application state', description: 'The current list of items visible in the app, their statuses, and any active filters. Read this before answering questions about what the user is looking at.', async read() { const data = storage.getAll(); return [{ type: 'json', data }]; }, }), ]; } ``` Each resource's `read()` returns an array of content parts: - `{ type: 'json', data: unknown }` — structured data (preferred; agent reasons over it directly) - `{ type: 'text', text: string }` — free-form text ### Actions Actions are tools the agent can invoke. Use `snake_case` names and Zod for parameter schemas. The `.describe()` on each field is the agent's documentation. ```typescript // src/features/agent/agentActions.ts import { createAgentAction } from '@cognite/app-sdk'; import { z } from 'zod'; import type { DataService } from '../data/DataService'; export function buildAgentActions(dataService: DataService) { return [ createAgentAction({ name: 'get_item_details', description: 'Retrieve full details for a specific item by ID. Returns all fields including history.', parameters: z.object({ item_id: z.string().describe('The ID of the item to retrieve'), }), async handler({ item_id }) { const item = await dataService.getItem(item_id); return { content: [{ type: 'json', data: item }] }; }, }), ]; } ``` **Mutating actions:** The agent does NOT ask the user for confirmation before calling actions — so use caution with actions that write data. Be explicit in the `description` that the action is destructive, and require the user to have approved before the agent calls it. ```typescript createAgentAction({ name: 'update_item_status', description: 'Update the status of an item. Call this ONLY when the user has explicitly approved the change. The UI updates immediately.', parameters: z.object({ item_id: z.string().describe('The item to update'), status: z.enum(['active', 'closed', 'pending']).describe('The new status'), }), async handler({ item_id, status }) { storage.updateStatus(item_id, status); return { content: [{ type: 'json', data: { success: true } }] }; }, }) ``` ### Lifecycle hook ```typescript // src/features/agent/useAgentServer.ts import { useEffect } from 'react'; import { createAgentServer, registerAgentServer, type HostAppAPI } from '@cognite/app-sdk'; import { buildAgentActions } from './agentActions'; import { buildAgentResources } from './agentResources'; import { useStorageService } from '../storage/StorageServiceContext'; import { useDataService } from '../data/DataServiceContext'; export function useAgentServer(api: HostAppAPI | null): void { const storage = useStorageService(); const dataService = useDataService(); useEffect(() => { if (!api) return; const server = createAgentServer({ uri: 'my-app', // namespaced by Fusion with instance ID — no need to be globally unique actions: buildAgentActions(dataService), resources: buildAgentResources(storage), }); void registerAgentServer(api, server).catch((err: unknown) => { console.warn('[agent] registerAgentServer failed:', err); }); return () => { void api.unregisterAgentServer('my-app').catch((err: unknown) => { console.warn('[agent] unregisterAgentServer failed:', err); }); }; }, [api, storage, dataService]); } ``` Call `useAgentServer(api)` near the root of your component tree, after `api` is available. --- ## Step 6 — Wire it all together Call `useHostApp()` at the root, pass `api` to `useAgentServer`, and thread it down to any UI triggers: ```tsx // src/App.tsx function App() { const api = useHostApp(); useAgentServer(api); // registers resources + actions when api is ready return ( {api && ( api.sendAgentLayoutMode({ mode: 'sidebar' })}> Open Assistant )} ); } ``` --- ## Dev vs. production | Environment | `connectToHostApp` | Effect | |---|---|---| | Inside Fusion | Resolves with `{ api }` | All features work | | Standalone `vite dev` | Rejects | Agent features silently disabled | This is handled by the `useHostApp` hook above — no extra conditionals needed elsewhere. --- ## Testing Because `buildAgentActions` and `buildAgentResources` are pure factories that accept services as arguments, test them directly without mounting React: ```typescript // agentActions.test.ts const mockDataService = { getItem: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }) }; const [getItemAction] = buildAgentActions(mockDataService); const result = await getItemAction.handler({ item_id: '1' }); expect(result.content[0].data).toEqual({ id: '1', name: 'Test' }); ``` --- ## Known pitfalls ### `setApi(resolvedApi)` stores a Promise, not the proxy Comlink proxies are callable objects. React's `useState` setter, when given a function, calls it as `fn(prevState)` to compute the new state. Because a Comlink proxy responds to function calls (forwarding them to the remote), `setApi(proxy)` causes React to invoke the proxy, and the resulting Promise becomes the state value. **Symptom:** `api` appears non-null (a Promise is truthy), but calling `api.sendAgentLayoutMode(...)` or checking `typeof api.sendAgentLayoutMode` returns nonsense. **Fix:** Always use the updater form: `setApi(() => resolvedApi)`. ### `typeof proxy.method === 'function'` is always `true` Comlink Proxy objects return `'function'` for any property access via `typeof`. This means you cannot use `typeof` guards to detect whether a method is actually supported by the host. Use `try/catch` or `.catch()` on the call instead. --- ## Checklist - [ ] `@cognite/app-sdk@0.3.1+` installed - [ ] `useHostApp` hook uses `setApi(() => resolvedApi)` — NOT `setApi(resolvedApi)` - [ ] `useHostApp` hook catches rejection (outside Fusion), stores `api` in state - [ ] Agent UI buttons only render when `api` is not null - [ ] `useAgentServer` registered on mount, unregistered on unmount - [ ] `registerAgentServer` and `unregisterAgentServer` calls have `.catch()` handlers - [ ] Resource `description` fields explain what data is returned and when to read it - [ ] Action `name` fields are `snake_case` - [ ] Mutating actions warn in their `description` that confirmation is required - [ ] Services injected into action/resource factories (not imported directly) — enables unit testing