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 pagesdeveloper-portal— docs and API referencecustomer-support— support portaladmin-console— internal toolsecommerce-checkout— transactional context
Match the surface_label in companion_deployments to your buildAgentInput surface field for consistency.