Skip to content

@aparte/engine — API reference

Defined in: packages/engine/src/agent/stream-events.ts:34

Token usage. Structurally a superset-compatible mirror of AparteUsage: the five common fields plus an index signature that carries the provider-specific rest (ttft/decode/phases/…) opaquely — the loop transports usage, never reads past these five.

inputTokens: number;

Defined in: packages/engine/src/agent/stream-events.ts:35

outputTokens: number;

Defined in: packages/engine/src/agent/stream-events.ts:36

optional totalTokens?: number;

Defined in: packages/engine/src/agent/stream-events.ts:37

optional cacheReadTokens?: number;

Defined in: packages/engine/src/agent/stream-events.ts:38

optional durationMs?: number;

Defined in: packages/engine/src/agent/stream-events.ts:39


Defined in: packages/engine/src/agent/stream-events.ts:57

One tool call as surfaced by the provider stream (mirrors AparteToolCall).

id: string;

Defined in: packages/engine/src/agent/stream-events.ts:58

name: string;

Defined in: packages/engine/src/agent/stream-events.ts:59

input: Record<string, unknown>;

Defined in: packages/engine/src/agent/stream-events.ts:68

Record<string, unknown>, mirroring core exactly, not unknown.

unknown looked safer — the loop does not read tool input, it forwards it — but the seam needs assignability in BOTH directions (transportCall is contravariant), and unknown is not assignable to Record<string, unknown>. Core’s composition compiles against it.


Defined in: packages/engine/src/agent/stream-events.ts:90

A tool declaration, structurally mirroring core’s AparteTool.

The loop forwards the inventory to the transport and reads maxTurns and needsApproval from its own lookup — it never interprets the schema. Mirrored exactly all the same, because the seam needs assignability in both directions; core’s composition compiles against it.

name: string;

Defined in: packages/engine/src/agent/stream-events.ts:91

description: string;

Defined in: packages/engine/src/agent/stream-events.ts:92

inputSchema: Record<string, unknown>;

Defined in: packages/engine/src/agent/stream-events.ts:93

optional systemPrompt?: string;

Defined in: packages/engine/src/agent/stream-events.ts:94

optional maxTurns?: number;

Defined in: packages/engine/src/agent/stream-events.ts:95

optional needsApproval?: boolean;

Defined in: packages/engine/src/agent/stream-events.ts:96


Defined in: packages/engine/src/agent/stream-events.ts:113

Multimodal content parts — a structural mirror of core’s AparteContentPart.

Mirrored rather than imported for the same reason as everything else in this file: engine stays zero-import at runtime. Mirrored EXACTLY rather than loosened to unknown[], which was the first attempt: transportCall is contravariant, so the message type has to be assignable to core’s AND core’s to it, and unknown[] fails the second direction. Core compiles streamRunner: runStreamAgent, so a part added on one side and not the other is a typecheck error there rather than a silent divergence.

The loop never looks inside content — it carries messages to the transport and appends results. This exists to make the seam type-check, not to be read.

type: "text";

Defined in: packages/engine/src/agent/stream-events.ts:113

text: string;

Defined in: packages/engine/src/agent/stream-events.ts:113


Defined in: packages/engine/src/agent/stream-events.ts:114

type: "image";

Defined in: packages/engine/src/agent/stream-events.ts:114

image: string;

Defined in: packages/engine/src/agent/stream-events.ts:114

optional mimeType?: string;

Defined in: packages/engine/src/agent/stream-events.ts:114


Defined in: packages/engine/src/agent/stream-events.ts:115

type: "file";

Defined in: packages/engine/src/agent/stream-events.ts:115

data: string;

Defined in: packages/engine/src/agent/stream-events.ts:115

mimeType: string;

Defined in: packages/engine/src/agent/stream-events.ts:115

optional name?: string;

Defined in: packages/engine/src/agent/stream-events.ts:115


Defined in: packages/engine/src/agent/stream-events.ts:132

A conversation message (mirrors AparteChatMessage).

role used to be a bare string, justified by a comment saying the loop needed to push the 'tool_call' / 'tool_result' envelope roles “without importing core’s union”. But core’s union already contains both — so the reason had not been true for some time, and the looseness cost something real: transportCall is contravariant, so this message type must ALSO be assignable to core’s, and a string role is not assignable to a closed union. That was the last link in the chain that stopped streamRunner: runStreamAgent from compiling.

Mirrored structurally rather than imported, to keep this package zero-import at runtime; core, which depends on this package, compiles the two against each other.

role: "user" | "assistant" | "system" | "tool_call" | "tool_result";

Defined in: packages/engine/src/agent/stream-events.ts:133

content: string | StreamContentPart[];

Defined in: packages/engine/src/agent/stream-events.ts:143

Message content — carried, never inspected by this loop.

Was plain string, because core’s AparteChatMessage.content became string | AparteContentPart[] for multimodal messages and this mirror never followed. The consequence was not subtle: new AparteClient({ streamRunner: runStreamAgent }) — the one composition the two packages exist to make, and the headline of five docs pages — did not typecheck.

optional toolCalls?: StreamToolCall[];

Defined in: packages/engine/src/agent/stream-events.ts:145

Present on a 'tool_call' envelope — the whole turn’s calls, grouped.

optional toolCallId?: string;

Defined in: packages/engine/src/agent/stream-events.ts:147

Present on a 'tool_result' message — which call it answers.

optional precedingText?: string;

Defined in: packages/engine/src/agent/stream-events.ts:149

Assistant text that preceded the tool call(s) this turn.


Defined in: packages/engine/src/agent/stream-events.ts:178

The request handed to the transport each turn (mirrors AparteChatRequest).

The two fields the loop actually reads are DECLARED rather than reached through an index signature. They used to be dynamic ([key: string]: unknown), which read like “extra fields pass through” but is a requirement on the target of an assignment, not a permission — it is what made core’s AparteChatRequest un-assignable here and broke streamRunner: runStreamAgent. Declaring them is both the fix and the more honest description: these are the fields the loop branches on. Anything else a host attaches still passes through untouched, because TypeScript only excess-property-checks object literals.

Types kept structural (not imported from core) so this package stays zero-import at runtime; core’s composition compiles the two against each other.

messages: StreamAgentMessage[];

Defined in: packages/engine/src/agent/stream-events.ts:179

modelId: string;

Defined in: packages/engine/src/agent/stream-events.ts:186

Required, because transportCall is contravariant: the runner hands this request to a transport typed against core’s AparteChatRequest, where modelId is required. A mirror that omitted it made the whole runner un-assignable — from the opposite direction to the messages mismatch.

optional _meta?: Record<string, unknown>;

Defined in: packages/engine/src/agent/stream-events.ts:188

Per-turn hints carried through for the host (prefixSegments, and whatever the consumer puts there); the loop reads none.

optional toolChoice?:
| {
name: string;
input?: Record<string, unknown>;
}
| "auto"
| "none";

Defined in: packages/engine/src/agent/stream-events.ts:190

'none' makes the loop drop the tool inventory for that turn.

optional tools?: StreamTool[];

Defined in: packages/engine/src/agent/stream-events.ts:192

The inventory the loop sends, and clears when toolChoice is 'none'.


Defined in: packages/engine/src/agent/stream-events.ts:207

Per-tool loop configuration (mirrors the AparteTool subset the loop reads).

optional maxTurns?: number;

Defined in: packages/engine/src/agent/stream-events.ts:208

optional needsApproval?: boolean | ((call) => boolean | "ask" | "deny");

Defined in: packages/engine/src/agent/stream-events.ts:222

Whether a call of this tool pauses for a decision. A boolean is the tool’s own declaration; a predicate decides PER CALL, from the arguments — the shape an approval policy needs (“a write to this path asks, a read does not”), and one a boolean could only approximate by gating every call and auto-approving most, which paints awaiting-approval on rows nobody was ever going to be asked about.

The predicate has three answers, because a policy has three: false runs the call, 'ask' (or true) puts it to someone — the loop announces the pause with tool-awaiting-approval — and 'deny' means the decision is already made: the resolver is still consulted (it is the one channel that carries the refusal sentence back), but nothing is announced, since nobody is being asked.


Defined in: packages/engine/src/agent/stream-run.ts:40

messageId: string;

Defined in: packages/engine/src/agent/stream-run.ts:42

Id of the assistant message being streamed (opaque; carried in events).

baseRequest: StreamChatRequest;

Defined in: packages/engine/src/agent/stream-run.ts:44

Turn-1 request; the loop clones its messages and enriches them per turn.

transportCall: (request) => Promise<
| string
| AsyncIterable<StreamChatEvent, any, any>>;

Defined in: packages/engine/src/agent/stream-run.ts:51

Calls the transport with the (possibly enriched) request. Returns the structured stream, or a plain string for a non-streaming provider. Mirrors getTransport().chat(provider, request, auth, ctx) with provider/auth/ctx closed over by the adapter.

ParameterType
requestStreamChatRequest

Promise< | string | AsyncIterable<StreamChatEvent, any, any>>

toolLookup: (name) => StreamToolHandler | undefined;

Defined in: packages/engine/src/agent/stream-run.ts:53

Resolves a tool’s handler by name (mirrors aparteGlobalConfig.getToolHandler).

ParameterType
namestring

StreamToolHandler | undefined

optional toolConfigLookup?: (name) => StreamToolConfig | undefined;

Defined in: packages/engine/src/agent/stream-run.ts:55

Resolves a tool’s loop config by name (maxTurns / needsApproval).

ParameterType
namestring

StreamToolConfig | undefined

optional approvalResolver?: StreamApprovalResolver;

Defined in: packages/engine/src/agent/stream-run.ts:57

HITL approval resolver for needsApproval tools (default: never called).

emitter: StreamRunEmitter;

Defined in: packages/engine/src/agent/stream-run.ts:59

Synchronous, ordered event sink consumed by the adapter.

signal: AbortSignal;

Defined in: packages/engine/src/agent/stream-run.ts:61

Single abort signal composing _isAborted + the stream controller.

optional maxTurns?: number;

Defined in: packages/engine/src/agent/stream-run.ts:63

Global turn cap.

10
optional toolTimeoutMs?: number;

Defined in: packages/engine/src/agent/stream-run.ts:65

Per-tool-call handler timeout in ms.

300000
optional onHistoryAppend?: (message) => void;

Defined in: packages/engine/src/agent/stream-run.ts:86

Called for every turn the loop appends to the history — the grouped tool_call envelope, each tool_result (resolved or rejected), and a pipeline phase’s reply — in order, and always before the transport call that would carry it. Never called for the messages you passed in baseRequest.

For hosts that own their own transcript. The loop re-sends its messages array every turn, which suits stateless message APIs but not a prefix cache (llama.cpp slots, vLLM), where turn N+1 must EXTEND turn N byte for byte. Such a host builds its own request in transportCall — ignoring request.messages — and mirrors these notifications into its append-only log, instead of reimplementing the loop’s tool bookkeeping. Synchronous and ordered, like emitter.

One rule about the envelope: a turn’s tool_call message is reported ONCE, when its first call completes, and the calls that complete later in the same turn are already in that same object’s toolCalls — the array is shared by reference, not copied. Hold the reference, not a snapshot, and every result you receive after it is declared by it.

ParameterType
messageStreamAgentMessage

void

optional idGen?: (prefix) => string;

Defined in: packages/engine/src/agent/stream-run.ts:93

Generates the ids the loop mints itself — today only the synthetic call of a forced toolChoice (prefix is 'synthetic-tool'). The default is a deterministic per-run counter; core’s client injects a crypto-based one. Tool ids otherwise flow from the stream.

ParameterType
prefixstring

string

type StreamChatEvent =
| {
type: "text";
delta: string;
}
| {
type: "thinking";
delta: string;
}
| {
type: "tool_use";
} & StreamToolCall
| {
type: "error";
message: string;
}
| {
type: "done";
usage?: StreamUsage;
};

Defined in: packages/engine/src/agent/stream-events.ts:75

A structured stream event from the transport (mirrors AparteStreamEvent). The tool_use variant spreads StreamToolCall exactly like core’s does.


type StreamContentPart =
| StreamTextPart
| StreamImagePart
| StreamFilePart;

Defined in: packages/engine/src/agent/stream-events.ts:116


type StreamToolHandler = (call, signal) => Promise<{
content: string;
structuredContent?: unknown;
}>;

Defined in: packages/engine/src/agent/stream-events.ts:196

A tool handler (mirrors the resolved AparteToolHandler).

ParameterType
callStreamToolCall
signalAbortSignal

Promise<{ content: string; structuredContent?: unknown; }>


type StreamApprovalResolver = (call, signal) => Promise<{
approved: boolean;
payload?: unknown;
instruction?: string;
reason?: string;
}>;

Defined in: packages/engine/src/agent/stream-events.ts:229

Resolves a human-in-the-loop approval (mirrors core’s AparteToolApprovalResolver). Injected so the loop stays headless.

ParameterType
call{ id: string; name: string; input: Record<string, unknown>; }
call.idstring
call.namestring
call.inputRecord<string, unknown>
signalAbortSignal

Promise<{ approved: boolean; payload?: unknown; instruction?: string; reason?: string; }>


type StreamRunEvent =
| {
type: "run-start";
}
| {
type: "turn-start";
}
| {
type: "text-delta";
delta: string;
}
| {
type: "text-flush";
}
| {
type: "thinking-delta";
delta: string;
}
| {
type: "tool-start";
toolCallId: string;
name: string;
input: unknown;
}
| {
type: "tool-awaiting-approval";
toolCallId: string;
name: string;
input: unknown;
}
| {
type: "tool-approved";
toolCallId: string;
}
| {
type: "tool-rejected";
toolCallId: string;
reason: string;
}
| {
type: "tool-failed";
toolCallId: string;
error: string;
}
| {
type: "tool-resolved";
toolCallId: string;
result: string;
structuredResult?: unknown;
}
| {
type: "tool-aborted";
toolCallId: string;
}
| {
type: "turn-limit-exceeded";
scope: "global" | "tool";
limit: number;
toolCallId?: string;
}
| {
type: "run-aborted";
}
| {
type: "run-done";
usage?: StreamUsage;
};

Defined in: packages/engine/src/agent/stream-events.ts:272

High-level, DOM-free events isomorphic to the inline loop’s targetElement.* call sequence. Emitted synchronously and in order (see StreamRunEmitter) so the adapter reproduces the exact streaming update order.

Mapping to the inline loop it replaced, for the adapter:

  • run-start → updateMessage(status:‘streaming’) once at loop entry (the leading write before turn 1)
  • turn-start → reset the per-turn parser / thinking / streaming-segment state (no DOM); one per turn
  • text-delta → parser-driven addSegment/updateSegment, else typeName/updateLastMessage
  • text-flush → textParser.finalize() then addSegment/updateSegment the finalized segments; one per turn, after the inner SSE loop ends (surfaced by the spike — a turn-boundary flush)
  • thinking-delta → addSegment(‘thinking’) then updateSegment(content); first text-delta after thinking collapses it (updateSegment collapsed:true)
  • tool-start → renderer lookup + per-tool-name CSS inject into document.head + addSegment
  • tool-awaiting-approval → updateSegment(‘awaiting-approval’) + dispatch aparte-tool-approval-request
  • tool-approved → updateSegment(‘pending’)
  • tool-rejected → updateSegment(‘rejected’, result)
  • tool-failed → updateSegment(‘failed’, error) — the handler threw; the run then ends on that error
  • tool-resolved → updateSegment(‘resolved’, result)
  • tool-aborted → updateSegment(‘aborted’) (no-handler path, timeout/abort path, or per-tool maxTurns path)
  • turn-limit-exceeded scope:‘global’ → addSegment(error ‘MAX_TURNS_EXCEEDED’); scope:‘tool’ → updateSegment(‘aborted’)
  • run-aborted → dispatch aparte-message-aborted (from the inner-loop abort check or the outer turn-boundary abort check)
  • run-done → updateMessage(status:‘completed’) always + setUsage if usage
{
type: "run-start";
}

{
type: "turn-start";
}

{
type: "text-delta";
delta: string;
}

{
type: "text-flush";
}

{
type: "thinking-delta";
delta: string;
}

{
type: "tool-start";
toolCallId: string;
name: string;
input: unknown;
}

{
type: "tool-awaiting-approval";
toolCallId: string;
name: string;
input: unknown;
}

{
type: "tool-approved";
toolCallId: string;
}

{
type: "tool-rejected";
toolCallId: string;
reason: string;
}

{
type: "tool-failed";
toolCallId: string;
error: string;
}

The handler threw (not an abort): the row hears of the crash before the run ends on it.


{
type: "tool-resolved";
toolCallId: string;
result: string;
structuredResult?: unknown;
}

{
type: "tool-aborted";
toolCallId: string;
}

{
type: "turn-limit-exceeded";
scope: "global" | "tool";
limit: number;
toolCallId?: string;
}

{
type: "run-aborted";
}

{
type: "run-done";
usage?: StreamUsage;
}

type StreamRunEmitter = (event) => void;

Defined in: packages/engine/src/agent/stream-events.ts:295

Synchronous event sink — mirrors AGUIEmitter. Synchronous by contract: the loop must never yield between emitting an event and its ordered successor, or the adapter’s streaming updates would interleave out of order.

ParameterType
eventStreamRunEvent

void

function runStreamAgent(opts): Promise<StreamUsage | undefined>;

Defined in: packages/engine/src/agent/stream-run.ts:124

Run the structured-stream agent loop. Resolves the last turn’s usage (the done{usage} last-write-wins, mirroring _streamLoop’s return), or undefined. Throws on a stream error event or a non-abort tool failure — the caller (adapter) routes that to its lifecycle-error handler, exactly as _handleSend/_handleRetry/_handleEdit catch _streamLoop.

This block has to stay ADJACENT to the function. It used to sit above warnUnknownStreamEvent, six lines and a docblock away from what it describes, and a docblock separated from its declaration is attached to nothing: TypeDoc took the helper’s comment as the neighbouring one and @aparte/engine’s reference page carried the package’s headline export with no description at all — the very export its own frontmatter names. Same trap the custom-elements analyser has with a class docblock pushed above an import.

ParameterType
optsStreamRunOptions

Promise<StreamUsage | undefined>