Skip to main content

Context-aware Companion with SurfaceContext

Goal: Make your Companion instance aware of which page, entity, and surface it's on — so responses are relevant, not generic.

What is SurfaceContext?

SurfaceContext is a standardized input field injected into every Companion agent call. The agent prepends it to the user message before processing, giving it grounding about the embedding surface.

interface SurfaceContext {
  surface: string;        // 'customer-support' | 'developer-portal' | 'marketing-site'
  page: string;           // window.location.pathname
  page_title?: string;    // document.title
  entity_type?: string;   // 'support_ticket' | 'invoice' | 'agent'
  entity_id?: string;     // 'TKT-12345'
  entity_name?: string;   // 'Login loop issue'
  deployment_id?: string; // links to companion_deployments.id
}

The agent receives this as a prefixed line on the user message:

[Context: Surface — customer-support, page: ticket/TKT-12345, viewing support_ticket 'Login loop issue' (id: TKT-12345), deployment: dep_abc123]
Can you summarise this ticket and suggest next steps?

Basic usage

Pass buildAgentInput to the widget. It's called on every message send:

HUMAN.Companion.init({
  agentsCallUrl: '/api/companion/ask',
  buildAgentInput: () => ({
    deployment_id: 'dep_abc123',
    surface_context: {
      surface: 'developer-portal',
      page: window.location.pathname,
      page_title: document.title,
    },
  }),
});

Entity context — knowing what the user is viewing

When the user is on a specific entity page (a ticket, invoice, agent config), inject it:

// In a ticket detail page
HUMAN.Companion.init({
  agentsCallUrl: '/api/companion/ask',
  buildAgentInput: () => ({
    deployment_id: 'dep_support_portal',
    surface_context: {
      surface: 'customer-support',
      page: window.location.pathname,
      entity_type: 'support_ticket',
      entity_id: currentTicket.id,        // 'TKT-12345'
      entity_name: currentTicket.title,   // 'Login loop issue'
    },
  }),
});

The agent can now answer "summarise this ticket", "what's the priority?" or "similar tickets?" in context.

SPA route updates — no conversation loss

In a React SPA, the page changes without remounting the widget. Use setInputAugmenter to rewire the context on route change without destroying the conversation:

// Next.js example
import { useRef, useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { CompanionWidget, CompanionWidgetRef } from '@human/companion-widget/react';

export function Layout({ children }) {
  const pathname = usePathname();
  const widgetRef = useRef<CompanionWidgetRef>(null);

  // Rewire surface_context on route change — zero conversation loss
  useEffect(() => {
    widgetRef.current?.setInputAugmenter(() => ({
      deployment_id: 'dep_my_app',
      surface_context: {
        surface: 'my-app',
        page: pathname,
        page_title: document.title,
      },
    }));
  }, [pathname]);

  return (
    <>
      {children}
      <CompanionWidget
        ref={widgetRef}
        humanApiUrl={process.env.NEXT_PUBLIC_API_URL}
        agentsCallUrl="/api/companion/ask"
        buildAgentInput={() => ({
          deployment_id: 'dep_my_app',
          surface_context: { surface: 'my-app', page: pathname },
        })}
        ui={{ theme: 'dark', position: 'bottom-right' }}
      />
    </>
  );
}

Surface label conventions

Choose a stable, machine-readable surface string. It appears in the agent context and in analytics:

  • marketing-site — public marketing pages
  • developer-portal — docs and API reference
  • customer-support — support portal
  • admin-console — internal tools
  • ecommerce-checkout — transactional context

Match the surface_label in companion_deployments to your buildAgentInput surface field for consistency.

Next steps

← All guides