@polyweave/dialog (1.0.8)
Installation
@polyweave:registry=https://hub.kl1.tenere.ai/api/packages/Polyweave/npm/npm install @polyweave/dialog@1.0.8"@polyweave/dialog": "1.0.8"About this package
@polyweave/dialog
Dialog and conversation history wrappers for the Polyweave platform. Provides bidirectional frame-level wrappers that accumulate conversation history, manage service parameters, support history compaction via replace instructions, handle streaming with interruption semantics, track voice playback via receipt-based "heard" events, and compress long conversations via observational memory (Observer/Reflector pattern with XML DSL).
Published as @polyweave/dialog.
Synopsis
import { createFrameDuplex, createFrameSink, createTextFrame, FrameType } from '@polyweave/core';
import {
createDialogWrapper,
createStreamingDialog,
createVoiceDialogWrapper,
createObservationalDialogWrapper,
estimateTokens,
countFrameTokens
} from '@polyweave/dialog';
// Basic dialog: wraps an LLM adapter, accumulates history
const dialog = createDialogWrapper(llmAdapter, { maxHistorySize: 50 });
dialog.source.pipe(mySink);
dialog.sink.write(createTextFrame('Hello!'));
// Streaming dialog: supports interruption and queued requests
const streamingDialog = createStreamingDialog(llmAdapter);
// While response is streaming, send an interrupt META frame:
streamingDialog.sink.write({ type: FrameType.META, content: { interrupt: true } });
// The next START/END request gets queued and replayed after interruption
// Voice dialog: tracks what was played back via META({ heard })
import { createVoiceDialogWrapper } from '@polyweave/dialog';
const voiceDialog = createVoiceDialogWrapper(llmAdapter);
// Observational dialog: compresses long conversations via Observer/Reflector
const obsDialog = createObservationalDialogWrapper(llmAdapter, {
observerFn: (opts, callback) => myLlm(opts.systemPrompt, opts.prompt, callback),
reflectorFn: (opts, callback) => myLlm(opts.systemPrompt, opts.prompt, callback),
messageThreshold: 30000, // token threshold to trigger observation
observationThreshold: 40000 // token threshold to trigger reflection
});
Install
npm install @polyweave/dialog
Why
Raw LLM adapters are stateless — each request is a blank slate. Real conversations require history accumulation, context management, and intelligent compaction when the context window overflows. This package provides five purpose-built bidirectional wrappers that sit between any LLM adapter and the application, transparently managing conversation state while remaining fully compatible with the Polyweave frame protocol. Each wrapper optimizes for a different interaction pattern: standard turn-taking, streaming with interruptions, voice with playback tracking, and long-running conversations with observational memory.
API
createDialogWrapper(innerDuplex, options)
Wraps an adapter to maintain full conversation history. Every user message is accumulated; every request is reconstructed with the complete history before being forwarded to the inner adapter.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
innerDuplex |
IFrameDuplex |
Yes | — | The LLM adapter to wrap. Must implement { source, sink }. |
options.maxHistorySize |
number |
No | undefined |
Maximum number of history entries (user + assistant + tool messages). Oldest entries are discarded when exceeded. |
options.durability.enabled |
boolean |
No | false |
Enable automatic META frame emission on state changes for crash recovery. |
options.durability.wrapperId |
string |
No | undefined |
Unique wrapper ID scoping durability META frames. |
options.name |
string |
No | null |
Human-readable name forwarded to createBidirectionalWrapper. |
Returns: { sink: IFrameSink, source: IFrameSource }
The returned duplex splits input to both the wrapper and its internal ObservableState, and joins output from both. This means durability META frames emitted by state changes appear on the source.
Frame protocol (request direction — sink receives, source emits downstream):
| Input → | Output → |
|---|---|
START |
Discarded (sets inRequestScope = true, clears buffer). |
PARAM |
Accumulated into serviceParams. Re-emitted in every reconstructed request. |
TEXT / IMAGE / AUDIO / VIDEO |
Buffered during request scope. |
TOOL_RESULT |
Treated as tool results — appended to history as { role: 'tool', frames: [toolResult] }. Reconstructed requests include them. |
META |
Routed to ObservableState for durability (persisted). If content.durability.restoreState matches wrapperId, it is consumed. If content.replace is present (array [startIdx, endIdx]), a replace instruction is set. Other META passes through. |
END (closing request scope) |
Reconstructs full history: START → PARAMs → all history messages (with role metadata) → END. If a replace instruction was set via META, history is compacted instead (range replaced by a summary message) and the request is NOT forwarded downstream. |
| Non-scoped frames | Pass through unchanged. |
Response direction:
| Input (from adapter) → | Output (to sink) → |
|---|---|
START |
Sets inResponseScope, clears buffer. Passed through. |
TEXT / IMAGE / AUDIO / VIDEO / TOOL_CALL |
Buffered. Passed through. |
END (closing response scope) |
Accumulates assistant message { role: 'assistant', frames: responseBuffer } into history. Increments turnCount. Enforces maxHistorySize. Passes END through. |
META |
Passed through unchanged. |
History compaction:
Send a META frame with content.replace set to an array of two indices (negative indices count from end, like [0, -2] to replace all but the last two messages). Optionally filter by content.tags (frame types) and content.roles (message roles). On the next request END, the wrapper replaces matching messages in the range with a single summary message assembled from the content frames of that request. The replacement request itself is not forwarded to the LLM.
Error conditions: None thrown synchronously. ObservableState writes are fire-and-forget from the wrapper's perspective.
createStreamingDialog(innerDuplex, options)
Like createDialogWrapper but adds streaming interruption support. While a response is streaming, an interrupt META frame causes the wrapper to discard buffered chunk frames, queue the next incoming request, and replay it after the interrupted response completes.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
innerDuplex |
IFrameDuplex |
Yes | — | The LLM adapter to wrap. |
options.durability.enabled |
boolean |
No | false |
Enable durability META emissions. |
options.durability.wrapperId |
string |
No | undefined |
Unique wrapper ID for durability. |
options.name |
string |
No | null |
Adapter name. |
Returns: { sink, source } — same split/join pattern as createDialogWrapper.
Interruption semantics:
| Step | What happens |
|---|---|
| 1. Response is streaming (inResponseScope = true) | TEXT/IMAGE/AUDIO/VIDEO frames are buffered in chunkBuffer. |
2. META { interrupt: true } arrives |
interrupted flag set. chunkBuffer cleared. queuedRequest initialized (empty array). |
| 3. Next START frame (new request) | Appended to queuedRequest array. Not forwarded. |
| 4. END of interrupted response arrives | interrupted cleared. The queued request is appended to history and a full reconstructed request is emitted after the END frame (as a multi-frame array). |
Otherwise identical to createDialogWrapper for request/response buffering, service params, history accumulation, and replace instructions.
createVoiceDialogWrapper(innerDuplex, options)
Conversation wrapper optimized for voice interactions. Assistant message content is built up incrementally via META({ heard }) receipts, reflecting what has actually been played back to the user.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
innerDuplex |
IFrameDuplex |
Yes | — | The LLM adapter. |
options.durability.enabled |
boolean |
No | false |
Enable durability META emissions. |
options.durability.wrapperId |
string |
No | undefined |
Unique wrapper ID. |
options.name |
string |
No | null |
Adapter name. |
Returns: { sink, source }
Heard tracking:
When a META frame with content.metadata.heard arrives (a text chunk that has been spoken/played):
- If a
pendingAssistantMessageexists, itstextproperty is appended with the heard chunk. - If the last history entry is an assistant message (no pending), its text is appended.
- If neither, a new pending assistant message is created with the heard text.
- The history is updated via
observableState.set(triggering durability META if enabled).
Request reconstruction includes text fields from history messages (voice messages are flattened to { role, text } rather than { role, frames }).
createObservationalDialogWrapper(innerDuplex, options)
Advanced dialog compaction using an Observer/Reflector pattern with XML DSL. When message tokens exceed a threshold, the Observer compresses recent messages into structured XML observations. When observations exceed a second threshold, the Reflector condenses them further. Observations are injected into every request as a system message for the LLM.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
innerDuplex |
IFrameDuplex |
Yes | — | The LLM adapter to wrap. |
options.observerFn |
function |
Yes | — | Async observer function. Called as observerFn({ systemPrompt, prompt, temperature }, callback). Must call callback(err, result) where result is XML text. |
options.reflectorFn |
function |
No | undefined |
Async reflector function. Same signature as observerFn. Must be provided for reflection to occur. |
options.messageThreshold |
number |
No | 30000 |
Estimated token count threshold. When messages exceed this, the Observer runs. |
options.observationThreshold |
number |
No | 40000 |
Estimated token count threshold. When observations exceed this and reflectorFn is provided, the Reflector runs. |
options.useXML |
boolean |
No | true |
Enable XML DSL for observations. When true, observations are parsed, validated, and predicate definitions are extracted. |
options.durability.enabled |
boolean |
No | false |
Enable durability META emissions. |
options.durability.wrapperId |
string |
No | undefined |
Unique wrapper ID. |
options.observerSystemPrompt |
string | function |
No | OBSERVER_XML_PROMPT(predicateContext) |
Override the Observer system prompt. If a function, receives { predicateContext, messages }. |
options.reflectorSystemPrompt |
string | function |
No | REFLECTOR_XML_PROMPT |
Override the Reflector system prompt. If a function, receives { observations }. |
options.includeObservationsInContext |
boolean |
No | true |
Whether to include observations in reconstructed requests. |
options.includeCurrentTaskInContext |
boolean |
No | true |
Whether to include the current task in reconstructed requests. |
options.includeMessageHistoryInContext |
boolean |
No | true |
Whether to include raw message history in reconstructed requests. |
options.memoryProvider |
function |
No | undefined |
Optional function returning a memory context string injected into each request. |
options.predicateStore |
PredicateStore |
No | createPredicateStore({}) |
Predicate store for tracking dynamically defined XML predicates. |
options.predicatePersistence |
object |
No | undefined |
Persistence config for the predicate store. |
options.name |
string |
No | null |
Adapter name. |
Returns:
{
sink: IFrameSink,
source: IFrameSource,
getState: () => snapshot,
getParsedObservations: () => parsed | null,
getPredicateRegistry: () => Map<string, predicate>,
observe: (callback) => void, // Manually trigger Observer
reflect: (callback) => void, // Manually trigger Reflector
getFacts: () => Facts[], // Extract facts from parsed observations
getPredicateStore: () => PredicateStore,
getPredicates: () => predicate[],
getPredicateStats: () => stats,
exportPredicates: () => definitions,
importPredicates: (defs) => void
}
Error conditions:
| Condition | Error |
|---|---|
observerFn not provided |
throw new Error('observerFn is required for observational dialog') |
| XML validation fails (Observer) | Calls callback(new Error('Invalid XML: ...')) if done callback provided |
| XML validation fails (Reflector) | Calls done(new Error('Invalid XML: ...')) if done callback provided |
| Observer LLM call fails | Emits META { type: 'observation-failed', error: '...' }. If done callback provided, calls done(err). |
| Reflector LLM call fails | Emits META { type: 'reflection-failed', error: '...' }. If done callback provided, calls done(err). |
XML DSL operations:
- Observer compresses messages into
<observations>XML containing<observation>elements with timestamps, entities, triples (subject-predicate-object), priorities, and metadata. - Reflector condenses accumulated observations, combining related entries and removing redundancy while preserving critical predicates.
- Predicate definitions extracted from XML are registered in both
predicateRegistry(Map) andpredicateStore. The predicate store tracks entity types, generates prompt context filtered by entity types detected in current messages, and records usage of each predicate. - Entity types are auto-detected from message content via heuristic keyword matching:
person,project,technology,date,feature,preference.
createDialogCompactWrapper(innerDuplex, options) — DEPRECATED
Replaced by createDialogWrapper which now includes built-in history compaction via META replace instructions. Calling this function emits a console warning and delegates to createDialogWrapper.
Aliases
export const createDialogTransform = createDialogWrapper;
export const createVoiceDialogTransform = createVoiceDialogWrapper;
export { createDialogCompactWrapper } from './dialogCompact.js'; // deprecated
estimateTokens(text) (exported from src/tokenCounter.js)
Estimates token count from text using a simple heuristic: Math.ceil(text.length / 4).
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
text |
string |
No | — | Input text. Returns 0 if not a string or falsy. |
Returns: number — estimated token count.
countFrameTokens(frames) (exported from src/tokenCounter.js)
Sums estimateTokens across all frames in an array that have content.text.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
frames |
Array |
No | — | Array of frame objects. Returns 0 if not an array. |
Returns: number — total estimated tokens.
Tests
npm test
Runs node --test test/dialog.test.js test/dialogDurability.test.js test/durableDialog.test.js test/rigor.test.js. Covering: dialog wrapper history accumulation, streaming interruption and queued request replay, voice dialog heard tracking, observational dialog Observer/Reflector cycles, replace instruction compaction, maxHistorySize enforcement, durability META emission, and token counting.
Requirements
- Node.js >= 22.0.0
- ESM only
- Peer dependencies:
@polyweave/core,@polyweave/durability,@polyweave/simplemem— must be installed alongside. @polyweave/simplememis required at runtime byobservationalDialog.jsforOBSERVER_XML_PROMPT,REFLECTOR_XML_PROMPT,parseXMLObservations,validateXMLObservations,observationsToFacts, andcreatePredicateStore.
Caveats
- All wrappers are stateful. For independent conversation threads, create separate wrapper instances.
- The observational dialog's
messageThresholdandobservationThresholduse the 4-char-per-token heuristic fromestimateTokens, not actual tokenization. Calibrate these thresholds accordingly. createDialogCompactWrapperis now an alias forcreateDialogWrapperand emits a deprecation warning. Replace support is built into the standard dialog wrapper.- The observational dialog requires both
observerFnand (optionally)reflectorFnto be injected — it does not create adapters internally. This follows the dependency injection pattern used throughout Polyweave. - When
useXMLis false, observations are stored as raw text and parsed observations return{ observations: [{ content, entities: [] }], metadata: {} }.
Dependencies
Development Dependencies
| ID | Version |
|---|---|
| @rigor/core | * |
Peer Dependencies
| ID | Version |
|---|---|
| @polyweave/core | * |
| @polyweave/simplemem | * |