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

FieldTypeDescription
messagestringThe user's current turn
historyAgentMessage[]Prior conversation in this thread
contextAgentContextRequest-scoped platform bindings (below)
signalAbortSignalFires when the client disconnects — stop work when it does

AgentContext

FieldTypeDescription
workspaceIdstringThe workspace this turn belongs to
threadIdstringThe conversation thread
userIdstring | undefinedEnd user identifier
roleIdstring | null | undefinedThe user's RBAC role — platform calls are already scoped to it
languageIdstring | undefinedLocalization hint
topKnumber | undefinedSuggested retrieval result limit
authorizationstringThe user's bearer token (forwarded automatically by context.api)
apiSharelyAPIClientRequest-scoped platform client — see Tools & Platform Client
traceTraceSpanObservability 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/conformance

How Events Are Persisted

The server reduces your event stream into a single assistant message stored on the thread:

EventsPersisted as
content_delta (concatenated)content
thinking_*thinkingSteps[]
tool_call_*toolCalls[]
sourcessources[]
metadata_update (merged)metadata
message_start.model, message_endmodel, finishReason, tokenUsage

This is the same message shape returned by Get Thread in the Agent API.

Next Steps