Conversation Message Processing#
Two packages collaborate to turn raw hierarchical message data into AI-model-compatible output:
conversation-flow— parses a flat message array into structured display representations (contextTreeandflatList). Its public API is a singleparse()function.context-engine— runs a configurable pipeline of processors over aPipelineContextto transform those messages before they are sent to an AI model. Orchestrated by theContextEngineclass.
The two packages serve complementary roles: conversation-flow builds the display model from raw DB messages; context-engine prepares those messages for the model API before each inference call.
conversation-flow: Three-Phase Parse Pipeline#
parse(messages, messageGroups?) is the single entry point . It runs three phases:
- Indexing —
buildHelperMapsbuilds amessageMapandchildrenMapfor O(1) access patterns. - Structuring —
buildIdTreeconverts flat parent-child relationships into a tree ofIdNodes. - Transformation — a
Transformerinstance produces two outputs :contextTree— semantic tree for navigation (node types:MessageNode,AssistantGroupNode,BranchNode,SignalCallbacksNode, etc.)flatList— flat array optimised for virtual list rendering
A pre-processing step before Phase 1 remaps sub_agent-scoped messages to use their subAgentId as agentId, so downstream collectors never merge messages from different agents into the same group .
Post-processing promotes metadata.usage to a top-level usage field on each flat message, normalising token data written by different executor paths .
Dual-Form Message Chain Support#
The parser handles two persisted parentage shapes that must produce identical output :
| Form | Next assistant's parentId |
|---|---|
| tool-anchored (old) | Last tool result message ID |
| assistant-anchored (new) | Previous assistant message ID (sibling of its tool results) |
Both forms, and any mix within a single turn, collapse to the same assistantGroup node in flatList . Key invariants enforced by the reader:
- A
toolmessage is always inline data of its assistant (both forms). - A branch is ≥ 2 non-tool siblings under one parent.
The test suite dualForm.test.ts documents seven fixture classes: old form, new form, mixed forms, parallel tools, regenerate-branch, assistant-anchored regenerated continuation, and async-task summary placement.
MessageCollector#
MessageCollector is the workhorse of the transformation phase. It is constructed with a messageMap, childrenMap, and a BranchResolver. Key methods:
| Method | Purpose |
|---|---|
collectToolMessages(assistant, messages) | Resolves tool results for an assistant. Prefers the explicit tool.result_msg_id; falls back to matching by parentId + tool_call_id. |
isToolChainHead(assistant) | Returns true when a toolless assistant is the head of a chain that eventually reaches a tool-using step — prevents that head from rendering as a standalone bubble. |
collectAssistantChain(...) | Recursively walks assistant → tools → assistant → …, staying within the same agentId. Handles toolless continuation steps (prose-only progress updates) without breaking the visual chain. |
findFlatChainContinuation(...) | Dual-form aware: gathers candidates from both the assistant's own non-tool children (new form) and each tool result's children (old form). Applies a fan-out guard (AgentCouncil or task children end the chain) and branch resolution for regenerated continuations. |
collectFlatSignalCallbacks(...) | Collects tool-stdout / tool-callback signal blocks, sorted by metadata.signal.sequence. |
collectFlatTaskCompletions(...) | Collects task-completion signal assistants (post-task summaries), sorted by createdAt. |
The signal lineage (metadata.signal) is read by getMessageSignal, which deliberately returns undefined when the assistant carries tools — the writer attaches the tag at stream start before knowing whether tools will be used, so the collector defangs that mismatch.
Phase 2 migration note: When a dedicated
messages.signalcolumn lands, swapmetadata?.signalfor(msg as any).signal ?? msg.metadata?.signalingetMessageSignal.
BranchResolver#
BranchResolver determines which branch is active when multiple non-tool siblings exist under one parent (e.g. a regenerated reply). It uses a three-tier priority :
metadata.activeBranchIndex— explicit integer index; ifindex === children.length, the branch is being optimistically created andundefinedis returned.- Infer from descendants — the child that already has descendants is the active one.
- Default — first child.
Two method variants exist for different traversal contexts :
getActiveBranchId(message, idNode)— used duringcontextTreebuilding (works onIdNodestructures).getActiveBranchIdFromMetadata(message, childIds, childrenMap)— used during flat-list building.
MessageCollector.resolveActiveContinuationId() calls getActiveBranchIdFromMetadata to pick the correct continuation when >1 non-tool same-agent siblings share a parent .
context-engine: ContextEngine Pipeline#
ContextEngine sequences an ordered array of ContextProcessor instances over a mutable PipelineContext :
{ messages, metadata, initialState, isAborted } → Processor₁ → Processor₂ → … → PipelineResult
- Early termination: any processor can set
context.isAborted = true; subsequent processors are skipped . - Error wrapping: processor failures are caught and re-thrown as
PipelineErrorwith the processor name and a 300-char cause summary for dashboard triage . - Stats:
PipelineResultincludes per-processorprocessorDurationsandtotalDuration. - Fluent API:
addProcessor,removeProcessor,clear,clone,validate.
Key Processors#
All processors extend BaseProcessor (abstract class implementing the ContextProcessor interface) and are exported from packages/context-engine/src/processors/index.ts.
Message Structure#
| Processor | What it does |
|---|---|
GroupMessageFlattenProcessor | Explodes role=assistantGroup / role=supervisor messages with a children array back into flat assistant + tool sequences that AI model APIs understand . Skips empty-children groups entirely . |
AgentCouncilFlattenProcessor | Handles AgentCouncil fan-out message structures. |
GroupRoleTransformProcessor | Transforms roles for group messages before API calls. |
SupervisorRoleRestoreProcessor | Restores supervisor role back to assistant before the model API call (mirrors the isSupervisor transform applied in parse()). |
Tool Handling#
| Processor | What it does |
|---|---|
ToolCallProcessor | Normalises tool call messages. |
ToolMessageReorder | Ensures tool result messages appear in model-expected order. |
DisabledToolCallFilter | Removes tool calls that are disabled. |
Async Task Support#
| Processor | What it does |
|---|---|
TasksFlattenProcessor | Flattens async task trees. |
TaskMessageProcessor | Transforms task messages. |
TaskCallbackMessageProcessor | Handles task callback/signal messages. |
Context Management#
| Processor | What it does |
|---|---|
HistoryTruncateProcessor | Enforces message window limits. |
MessageCleanupProcessor | Removes stale or invalid messages. |
MessageContentProcessor | Normalises message content formats. |
PlaceholderVariablesProcessor | Substitutes template variables. |
InputTemplateProcessor | Applies input templates. |
VerifyMessageProcessor | Validates message integrity before dispatch. |