Skip to content

Function recipes

A grab-bag of handler patterns. Each is something a real integration tends to need.

Read DOM selection

ts
client.registerFunction({
  name: 'get_selection',
  description: 'Returns the text the user has currently selected.',
  handler: () => window.getSelection()?.toString() ?? '',
});
ts
import { useRouter } from 'vue-router';

const router = useRouter();

client.registerFunction({
  name: 'navigate',
  description: 'Navigates to a named route.',
  parameters: {
    type: 'object',
    properties: {
      name: { type: 'string' },
      params: { type: 'object' },
    },
    required: ['name'],
  },
  handler: async ({ name, params }: { name: string; params?: Record<string, string> }) => {
    await router.push({ name, params });
    return { ok: true, path: router.currentRoute.value.fullPath };
  },
});

Fetch from your own API

ts
client.registerFunction({
  name: 'find_orders',
  description: 'Searches the user\'s orders.',
  parameters: {
    type: 'object',
    properties: {
      q: { type: 'string' },
      limit: { type: 'number', default: 10 },
    },
    required: ['q'],
  },
  handler: async ({ q, limit }: { q: string; limit?: number }) => {
    const res = await fetch(`/api/orders?q=${encodeURIComponent(q)}&limit=${limit ?? 10}`);
    if (!res.ok) throw new Error(`API error ${res.status}`);
    return res.json();
  },
});

Confirm a destructive action with the user

Mark the function sensitive: true instead of rolling your own confirmation inside the handler. The client then holds execution until your confirmation handler resolves, so the handler only has to do the work:

ts
client.registerFunction({
  name: 'delete_account',
  description: 'Permanently deletes the current user account.',
  sensitive: true,
  handler: async () => {
    await fetch('/api/me', { method: 'DELETE' });
    return { deleted: true };
  },
});

client.setConfirmationHandler(async (req) => {
  const ok = window.confirm('Really delete your account? This cannot be undone.');
  return ok ? 'allow' : 'deny';
});

If the user denies (or no handler is set), the call is denied automatically — the autopilot receives a denied function error and handles it conversationally; the handler never runs. This is fail-closed, so sensitive functions never execute silently. See Sensitive functions.

In a Vue app you don't need to write the handler at all — the store registers one that drives the built-in ConfirmationPrompt UI (with "always allow" support). See the store's confirmation API.

Open a Vue dialog and resolve when it closes

ts
import { ref } from 'vue';

const dialogPromise = ref<{ resolve: (v: boolean) => void } | null>(null);
const dialogOpen = ref(false);
const dialogQuestion = ref('');

function openDialog(question: string) {
  dialogQuestion.value = question;
  dialogOpen.value = true;
  return new Promise<boolean>((resolve) => {
    dialogPromise.value = { resolve };
  });
}

export function resolveDialog(value: boolean) {
  dialogOpen.value = false;
  dialogPromise.value?.resolve(value);
  dialogPromise.value = null;
}

client.registerFunction({
  name: 'ask_user',
  description: 'Asks the user a yes/no question via a dialog.',
  parameters: {
    type: 'object',
    properties: { question: { type: 'string' } },
    required: ['question'],
  },
  handler: ({ question }: { question: string }) => openDialog(question),
});

Return a typed JSON object

The autopilot can read structured returns better than free-form strings. Whenever you can, return an object:

ts
handler: () => ({
  status: 'ok',
  user: { id: 42, name: 'Mo' },
  flags: { admin: true },
});

Avoid concatenating into a sentence ("User 42 is admin") — make the autopilot stringify it.

Long-running operation with progress

The function call is awaited synchronously; you cannot stream progress mid-call. Either:

  1. Return a job ID and register a follow-up function the autopilot can poll, or
  2. Update context as the work progresses so the autopilot sees state on its next turn:
ts
client.registerFunction({
  name: 'start_export',
  handler: async () => {
    const job = await startExport();
    client.updateContext({ export: { id: job.id, status: 'running' } });
    job.onProgress((p) => client.updateContext({ export: { id: job.id, status: 'running', progress: p } }));
    job.onDone(() => client.updateContext({ export: { id: job.id, status: 'done' } }));
    return { id: job.id };
  },
});