Polyweave

@polyweave/dialog (1.0.8)

Published 2026-07-11 15:23:36 +00:00 by Dvorak

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 pendingAssistantMessage exists, its text property 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) and predicateStore. 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/simplemem is required at runtime by observationalDialog.js for OBSERVER_XML_PROMPT, REFLECTOR_XML_PROMPT, parseXMLObservations, validateXMLObservations, observationsToFacts, and createPredicateStore.

Caveats

  • All wrappers are stateful. For independent conversation threads, create separate wrapper instances.
  • The observational dialog's messageThreshold and observationThreshold use the 4-char-per-token heuristic from estimateTokens, not actual tokenization. Calibrate these thresholds accordingly.
  • createDialogCompactWrapper is now an alias for createDialogWrapper and emits a deprecation warning. Replace support is built into the standard dialog wrapper.
  • The observational dialog requires both observerFn and (optionally) reflectorFn to be injected — it does not create adapters internally. This follows the dependency injection pattern used throughout Polyweave.
  • When useXML is 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 *

Keywords

polyweave dialog conversation history
Details
npm
2026-07-11 15:23:36 +00:00
457
John Dvorak
SEE LICENSE IN LICENSE
latest
16 KiB
Assets (1)
Versions (5) View all
1.0.8 2026-07-11
1.0.5 2026-07-10
1.0.4 2026-07-10
1.0.3 2026-07-10
1.0.2 2026-07-10