Files
md-files/.agents/skills/integrate-fusion-agent/SKILL.md
T
2026-05-31 20:25:41 +00:00

12 KiB

name, description, allowed-tools
name description allowed-tools
integrate-fusion-agent 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. 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:

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:

// 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<HostAppAPI | null>(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.

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.

{api && (
  <button onClick={() => api.sendAgentLayoutMode({ mode: 'sidebar' })}>
    Open Assistant
  </button>
)}

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.

// 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.

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.

// 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.

// 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.

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

// 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:

// src/App.tsx
function App() {
  const api = useHostApp();
  useAgentServer(api);   // registers resources + actions when api is ready

  return (
    <AppLayout>
      <MainContent />
      {api && (
        <ToolbarButton onClick={() => api.sendAgentLayoutMode({ mode: 'sidebar' })}>
          Open Assistant
        </ToolbarButton>
      )}
    </AppLayout>
  );
}

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:

// 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