Handler & Wire Protocol
Every Sharely-compatible agent is one function shape — the Handler. This page documents the contract and every event type your handler can emit. Type definitions live in @sharelyai/protocol (opens in a new tab) (types only, no runtime).
The Handler Contract
type Handler = (input: AgentInput) => AsyncIterable<AgentEvent>;Your handler receives one chat turn and yields a stream of typed events. The server reduces them into the assistant message, persists it, and encodes each one as Server-Sent Events — you never touch the wire, persistence, or auth.
AgentInput
| Field | Type | Description |
|---|---|---|
message | string | The user's current turn |
history | AgentMessage[] | Prior conversation in this thread |
context | AgentContext | Request-scoped platform bindings (below) |
signal | AbortSignal | Fires when the client disconnects — stop work when it does |
AgentContext
| Field | Type | Description |
|---|---|---|
workspaceId | string | The workspace this turn belongs to |
threadId | string | The conversation thread |
userId | string | undefined | End user identifier |
roleId | string | null | undefined | The user's RBAC role — platform calls are already scoped to it |
languageId | string | undefined | Localization hint |
topK | number | undefined | Suggested retrieval result limit |
authorization | string | The user's bearer token (forwarded automatically by context.api) |
api | SharelyAPIClient | Request-scoped platform client — see Tools & Platform Client |
trace | TraceSpan | Observability span |
The AgentEvent Union
AgentEvent is a discriminated union of 12 in-band events. A well-formed turn starts with message_start and ends with message_end. The server appends a wire-only done event after your stream completes and wraps every event in a { threadId, messageId } envelope — you don't emit done yourself.
Turn lifecycle
message_start
Opens the assistant turn.
{ type: 'message_start', role: 'assistant', model: string }message_end
Closes the turn with the finish reason and token usage.
{
type: 'message_end',
finishReason: string, // e.g. 'stop', 'tool_calls', 'length'
tokenUsage: { inputTokens: number, outputTokens: number, totalTokens: number }
}error
Terminal error — emit instead of message_end when the turn fails.
{ type: 'error', error: string }Streamed text
content_delta / content_end
The assistant's visible answer, streamed incrementally.
{ type: 'content_delta', delta: string }
{ type: 'content_end' }Thinking (chain-of-thought transparency)
Web Control renders these as collapsible "thinking" steps while the agent works.
{ type: 'thinking_start', thinkingId: string, title: string }
{ type: 'thinking_delta', thinkingId: string, delta: string }
{ type: 'thinking_end', thinkingId: string, status: 'completed' | 'error', durationMs: number }Tool calls
Surface tool invocations to the user with inputs, outputs, and timing.
{ type: 'tool_call_start', toolCallId: string, name: string, input: Record<string, unknown> }
{ type: 'tool_call_end', toolCallId: string, output?: unknown, error?: string, durationMs: number }Citations and metadata
sources
Knowledge citations / retrieval results, rendered as source chips in Web Control.
{ type: 'sources', sources: Source[] }
interface Source {
id: string; // knowledgeId
type: string; // e.g. 'semantic'
title: string;
url?: string;
snippet: string;
}metadata_update
Attaches structured per-tool extras to the assistant message's metadata. Each event's metadata is shallow-merged into the accumulated metadata object.
{ type: 'metadata_update', metadata: Record<string, unknown> }A Complete Turn
A realistic handler that thinks, retrieves, cites, and answers:
import type { Handler } from '@sharelyai/protocol';
export const handler: Handler = async function* (input) {
yield { type: 'message_start', role: 'assistant', model: 'claude-sonnet-4-5' };
// 1. Show the user what we're doing
yield { type: 'thinking_start', thinkingId: 't1', title: 'Searching workspace knowledge' };
const started = Date.now();
// 2. Role-scoped retrieval through the platform client
const matches = await input.context.api.rag({ text: input.message, topK: 5 });
yield { type: 'thinking_delta', thinkingId: 't1', delta: `Found ${matches.length} passages` };
yield { type: 'thinking_end', thinkingId: 't1', status: 'completed', durationMs: Date.now() - started };
// 3. Cite what we retrieved
yield {
type: 'sources',
sources: matches.map((m) => ({
id: m.metadata.knowledgeId,
type: 'semantic',
title: m.metadata['pdf.info.Title'] ?? m.metadata.source,
snippet: m.metadata.text.slice(0, 200),
})),
};
// 4. Stream the answer (call your LLM here)
for await (const chunk of callYourModel(input.message, matches, input.signal)) {
yield { type: 'content_delta', delta: chunk };
}
yield { type: 'content_end' };
yield {
type: 'message_end',
finishReason: 'stop',
tokenUsage: { inputTokens: 1200, outputTokens: 350, totalTokens: 1550 },
};
};Validating Your Stream
@sharelyai/conformance validates any AgentEvent stream against the wire-protocol contract — useful in CI for custom integrations and adapters:
npm i -D @sharelyai/conformanceHow Events Are Persisted
The server reduces your event stream into a single assistant message stored on the thread:
| Events | Persisted as |
|---|---|
content_delta (concatenated) | content |
thinking_* | thinkingSteps[] |
tool_call_* | toolCalls[] |
sources | sources[] |
metadata_update (merged) | metadata |
message_start.model, message_end | model, finishReason, tokenUsage |
This is the same message shape returned by Get Thread in the Agent API.
Next Steps
- Tools & Platform Client — wire first-party Sharely tools into your agent loop
- Adapters & Examples — skip hand-rolling events: adapt Vercel AI, Temporal, LangGraph, and more