> ## Documentation Index
> Fetch the complete documentation index at: https://checkfu.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Add an agent to your app

> Walk the support-desk example from an ordinary Hono and React app to a governed, live agent experience.

<Warning>
  Agent creation and tool synchronization now use `checkfu.beta.agents`, but
  this guide's Session launch remains migration-fenced until the CMA-compatible
  Session surface lands. Do not use its Session example with a newly created
  CMA Agent yet.
</Warning>

The runnable [`examples/support-desk`](https://github.com/andyrewlee/checkfu/tree/main/examples/support-desk) demo starts as a plain support inbox: Hono on Node, Vite and React, JSON fixture data, its own cookie session. Adding a Checkfu agent replaces none of that.

This is the brownfield path. Your backend keeps its framework, data model, authentication, and application functions. Checkfu supplies the governed Session, the tool-serving SDK, the durable event log, and the React pieces you compose yourself.

<CardGroup cols={2}>
  <Card title="Server boundary" icon="server" href="/guides/serve-tools-from-your-app">
    Declare Custom tools over your existing functions and serve explicit Sessions from your backend worker.
  </Card>

  <Card title="React boundary" icon="window" href="/guides/embed-a-session-view">
    Proxy the event log through your backend and compose transcript, tool, status, and ActionApproval components.
  </Card>
</CardGroup>

## What the finished journey proves

1. The agent and the ticket HTTP routes call the same application functions.
2. An agent-authored draft appears in the open ticket without a reload.
3. Human and agent writes produce the same domain effect with different attribution.
4. `close_ticket` parks on a real Checkfu ActionApproval rendered inside the support app.
5. The selected ticket is supplied in the drive message; the agent does not scrape browser state.

The example's boundary test permits only `@checkfu/sdk` and `@checkfu/ui` imports from Checkfu. It rejects internal packages and platform application imports.

## Start from the host's functions

The plain app already owns `lookupTicket`, `draftReply`, and `closeTicket`. Each function accepts the operation input used by both surfaces. An optional serving context distinguishes an agent call from a direct human call for attribution; it does not fork the business behavior.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const actorFor = (context) => (context === undefined ? "human" : "agent")

const draftReply = (input, context) =>
  resultFor(input.ticket_id, store.saveDraft(input.ticket_id, input.draft, actorFor(context)))
```

The existing Hono reply route calls that function directly:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const result = actions.draftReply({
  ticket_id: context.req.param("ticketId"),
  draft: body.draft,
})
```

## Declare the same functions as tools

`defineTool` wraps those exact function references with a name, a description, and an input schema. It does not move execution into Checkfu.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { defineTool } from "@checkfu/sdk"

const tools = [
  defineTool({
    name: "lookup_ticket",
    description: "Read the current support ticket, including its latest draft and status.",
    schema: ticketIdSchema,
    handler: actions.lookupTicket,
  }),
  defineTool({
    name: "draft_reply",
    description: "Save a reply draft for the support teammate to review in the open ticket.",
    schema: draftReplySchema,
    handler: actions.draftReply,
  }),
  defineTool({
    name: "close_ticket",
    description: "Close a resolved support ticket after a human approves the frozen call.",
    schema: ticketIdSchema,
    needsActionApproval: true,
    handler: actions.closeTicket,
  }),
]
```

`needsActionApproval` maps to Checkfu ActionPolicy authority on `use_tool:close_ticket`. It is not a client-side confirmation flag. Sync updates the Agent and its SDK-managed ActionPolicy; the changed configuration automatically becomes the next immutable Version.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const synced = await checkfu.tools.sync(agentId, tools)
console.log(`Agent ${synced.id} is now at Version ${synced.version}`)
```

`sync` returns the current Agent. When the custom-tool set changes, the update
automatically creates the next Version. An unchanged tool set is a no-op.

See [Serve tools from your app](/guides/serve-tools-from-your-app) for validation errors, replay convergence, and approval sequencing.

## Create a Session with visible context

When a teammate delegates from an open case, the server creates a normal Session and starts serving that explicit Session:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const session = await checkfu.sessions.create({
  agent: agentId,
  principal: principalId,
  title: `${ticket.id}: ${ticket.subject}`,
  initial_events: [{
    type: "user.message",
    payload: {
      content: supportContextMessage(ticket, prompt),
      authored_by: principalId,
      caused_by: { kind: "api" },
    },
  }],
})

const servingTask = checkfu.tools.serve({ sessions: [session.id], tools, signal })
servingTasks.set(session.id, servingTask)

// On shutdown: abort every signal, then await all retained serving tasks.
```

The v1 `user.message` shape has free-text `content` and attribution but no structured host-context field. The demo therefore prepends a documented, bounded in-band block:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
--- support context v1 (provided by the host app) ---
{"open_ticket_id":"TKT-1042","customer":"Ari Delgado","subject":"Charged twice after upgrading",…}
--- end support context ---
Draft a concise reply and close the ticket if it is resolved.
```

The example README records a typed, bounded context field as a candidate product desire. The demo neither invents a private field nor lets the agent guess which ticket is open.

## Keep credentials behind the existing backend

The browser never receives a Checkfu Workspace key. The Hono server exposes only the event page and stream routes for an authenticated, authorized Session, attaching its key upstream:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const headers = new Headers({
  Authorization: `Bearer ${config.apiKey}`,
  "Checkfu-Version": "2026-08-27",
  Accept: stream ? "text/event-stream" : "application/json",
})

return fetch(upstream, { headers, signal: request.signal })
```

This is the proxy recipe from [Embed a Session view](/guides/embed-a-session-view), consumed in a non-Checkfu server stack. Authenticate the caller and verify its right to observe the requested Session before invoking the proxy. Never expose a generic credentialed passthrough.

## Project the log into React

The host binds `useSessionEvents` to its proxy with the Workspace-proxy transport. The hook consumes the stream as data — durable events append by `seq`, previews replace themselves, gaps get one bounded backfill — and the host's `fetch` keeps auth (cookies, sign-in retries) entirely host-owned.

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
const transport = useMemo(
  () =>
    workspaceProxyTransport({
      sessionId,
      routePrefix: "/api/agent",
      fetch: supportFetch,
    }),
  [sessionId],
)
const state = useSessionEvents({ sessionKey: sessionId, transport })
```

When the highest event sequence changes, the support app refetches the open ticket from its own API. A served `draft_reply` therefore appears in the existing reply box without a reload. The ticket store remains application truth; the Session log is the durable truth that the agent action occurred and the live invalidation source.

The right rail composes focused kit exports rather than adopting a Checkfu application shell:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
<SessionStatusBadge status={status} />
<ToolCallCard toolName={call.name} arguments={call.arguments} result={call.result} />
<TranscriptView items={items} mode="transcript" followOutput />
```

## Surface the consequential action

When the agent calls `close_ticket`, ActionPolicy emits `action_approval.pending` and the run waits. The backend lists the pending custom-tool ActionApproval for that Session and passes its frozen context to the kit:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
<ActionApprovalPrompt.Root approval={approval} onIntent={respondThroughBackend}>
  <ActionApprovalPrompt.Context />
  <ActionApprovalPrompt.ApproveAction />
  <ActionApprovalPrompt.DenyAction />
</ActionApprovalPrompt.Root>
```

Approve or deny goes to the host backend, which calls `checkfu.actionApprovals.decide`. On approval, ToolServing runs the same `closeTicket` function as the human button; on denial, the function never runs. Both decisions remain in the Session event log.

## Run the complete example locally

Start the owned mock stack and use its protected connection file. The file contains the local URL, credential, Workspace, AgentDefinition, and Principal expected by the example.

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm exec checkfu dev --harness checkfu-mock
export CHECKFU_CONNECTION_FILE="<printed connection path>"

pnpm --filter @checkfu-examples/support-desk sync:tools
pnpm --filter @checkfu-examples/support-desk publish:agent
pnpm --filter @checkfu-examples/support-desk dev
```

Open `http://127.0.0.1:4174`, select `TKT-1042`, and choose **Ask case agent**. The visible default prompt uses the mock harness's deterministic `Use <tool> with <JSON>` grammar, so the first Session drafts a reply through the host worker. When it completes, choose **Start close task**. That second Session calls `close_ticket` and parks on the frozen ActionApproval in the same right rail.

The mock executes one explicitly named Custom tool per Session, which is what makes both steps repeatable; a hosted model picks its tools from ordinary prose instead. Pointing the same application code at hosted Checkfu changes only the server-side connection values.

## Verify the customer boundary

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm --filter @checkfu-examples/support-desk typecheck
pnpm --filter @checkfu-examples/support-desk test
pnpm --filter @checkfu-examples/support-desk build
```

The journey test scripts the Checkfu event log on one boundary and jsdom on the other. One case drives context in, parks and serves the Custom tool, projects the draft live, escalates the close, and ends on `run.completed`. The other reconciles a delayed ActionApproval and projects a denial without running the host action. A live smoke swaps those two test doubles for `checkfu dev` and a browser, and the application code does not change.
