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:131

A conversation message (mirrors AparteChatMessage).

role used to be a bare string, justified by a comment saying the loop needed to push the envelope roles of a tool turn “without importing core’s union”. But core’s union already contained them — 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";

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

content: string | StreamContentPart[];

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

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:147

Present on an 'assistant' message — the whole turn’s calls, grouped. What it said before them is that message’s own content, '' when it said nothing.

optional toolCallId?: string;

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

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

optional toolName?: string;

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

Present on a 'tool' message — the name of the tool that ran.


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

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:181

modelId: string;

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

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:190

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:197

'none' makes the loop drop the tool inventory for that turn. An object forces a tool, and forcing is a turn-1 instruction: the loop sends it on the first transport call of the run and lifts it (to 'auto') for every turn after, so the model can answer once the tool has.

optional tools?: StreamTool[];

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

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


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

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

optional maxTurns?: number;

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

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

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

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:99

Called for every turn the loop appends to the history — the assistant message carrying the turn’s calls, each tool message answering one (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 assistant 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.

Two of its fields are finalised after you are notified, then: toolCalls gains the turn’s later calls, and content is re-read at the turn’s end (a provider that emits [tool, text] streams the rest of the sentence after the envelope is opened — see syncEnvelopeText). A host that must write BYTES at receipt — an append-only log, which cannot hold a reference — should therefore treat the envelope as provisional and write it only once the turn’s last tool message has arrived, or re-read the object it holds before serialising.

content is the field that moved here, and the failure is louder than it was: a snapshot no longer loses a trailing clause, it loses the whole sentence the assistant said before it called anything.

ParameterType
messageStreamAgentMessage

void

optional idGen?: (prefix) => string;

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

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;

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


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

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

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:236

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:279

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:302

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:137

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>