@alma-harness/conversation
v0.12.0
Published
Governed buffered and settled-step streaming conversations for Alma.
Readme
@alma-harness/conversation
Governed buffered and settled-step streaming conversations. Use matching core, execution, loop
and single-call packages. createConversationRunner(config) composes durable
session admission, operation roots, governed model steps and final result storage.
It is the sole conversation entrypoint; the standalone agent and its legacy
lease/claim, streaming and financial writers have been removed (spec: retire-legacy-conversation-engine).
Supply step: GovernedStepConfig, admissions, rootResults, sessions, ordinary
agent prompt/policy/tools/profiles/hooks/media/recall options, caps, prices,
policyVersion, priceVersion, configRevision, resultContractVersion,
resultRetentionMs, runTimeoutMs, maxCalls, maxInputChars and consumers.
runTimeoutMs is at most five minutes. Clients must be single-dispatch. Ordinary
configuration is copied; store/client/policy/schema implementations and revision
mappings are trusted host capabilities. Tools use ConversationTool and receive
ConversationToolCtx, including models.direct({tier,sensitivity,prompt}) for a
single tool-free child call, and the narrowing models.delegate gateway.
runTurn({scope,sessionId,idempotencyKey,input,intent,trigger?,toolProfile?,signal?})
accepts an inline user Msg and a required stable delivery key. It snapshots bounded
plain JSON before I/O and derives scoped root identity and input/config bindings.
Retries return original outcomes without renewing deadlines or dispatching again;
changed content/options/config conflicts. Use immutable media references, not inline
wire content. Host configuration revisions must cover all behavior, and referenced
media bytes must remain bound to the original request. Undefined optional object
fields are omitted; arrays remain ordered. Non-JSON/accessor/cyclic/oversized input
is rejected before admission. The canonical payload ceiling includes 4 KiB metadata
overhead in addition to maxInputChars; traversal is bounded at 64 levels/100k nodes.
conversationRootKey(scope,sessionId,idempotencyKey) lets a host call read without
loading content. createConversationDescriptor(config,input,deadlineAt) is a pure
helper for explicit hosts interoperating with simple requests. runTurn also accepts
existing explicit input. Its reserved conversation-v1 identities use the same narrow
clock-race rule: differing proposed deadlines reuse the first descriptor only if all
other bindings match. This is a trusted-host convention, not proof of hashed content.
Arbitrary explicit roots keep exact binding semantics. Generic or lost claim ACKs
never grant ownership. Input signal is execution cancellation, not a socket signal.
The legacy run method is removed (spec: canonical-conversation-api-cutover).
Use runTurn for both simple and explicit input. It always returns a Promise;
invalid input rejects that promise and can be handled with .catch(). Input
normalization and snapshot happen immediately during the call, before any await.
Constructor and streamTurn validation remain synchronous.
@alma-harness/runtime/memory and /postgres provide storage composition.
Canonical APIs first observe the scoped admission and compare original bindings.
Matching retries never resolve policy again. For unseen work, policy.resolve
must be synchronous, pure and bounded: it receives only a copied RoutingIntent,
including task. A throw or absent selected client returns
{status:"not_admitted",reason:"routing_unavailable"} before any claim, context,
hook, provider validation or financial write. A final admission read takes
precedence if a matching competitor became visible; conflicts still reject.
Lookup failure or cancellation never claims a routing refusal.
This refusal is not durable and describes this invocation, not global absence.
read returns null for an unadmitted key and may observe a later competing claim.
Fix the configuration with a new configRevision before retrying; do not retry a
configuration refusal automatically. No receipt or session release is fabricated.
Successful validation snapshots the complete main choice for all main steps;
summary/direct/delegate routing remains independent. Post-admission uncertainty
still retains occupancy, including pre-existing failed admissions.
Settled-step streaming
streamTurn(input) accepts the same input as runTurn and starts the same engine
eagerly. It returns one async iterator and an independent result promise:
const stream = runner.streamTurn(input);
try {
for await (const event of stream) {
if (event.type === "text") showProgress(event.step, event.text);
if (event.type === "result") showStatus(event.view.status);
}
} catch (error) {
if (!(error instanceof ConversationStreamResyncError)) throw error;
// Resynchronize using runner.read(scope, originalRootKey).
} finally {
const view = await stream.result; // Worker completion is independent of the reader.
}Import ConversationStreamResyncError from this package. Text spans arrive in
main-step block order only after settlement and fresh authorization; there are no
provider-token deltas. A preamble may be visible before a later tool failure, cap
stop or uncertain root. Progress is not final completion and concatenated progress
is not generally the final reply. Final-message channels should use runTurn.
Progress tool/provider events contain metadata only; reasoning and child output
are withheld. Usage events are original main-call receipts; child costs can make
the final total larger than their sum. The terminal result event preserves the
existing host-facing ConversationView, including authorized tool/provider blocks
in its reply. Chat renderers select text blocks rather than serializing the envelope.
Fresh root authorization precedes final delivery and preserves owner/replay flags.
Break, return, throw and socket closure detach only the reader. Keep the worker
alive and await result separately; never wire socket close to input.signal.
Only that execution signal/deadline cancels execution. Detach is not durable
background scheduling, and uncertain execution still requires reconciliation.
Each stream permits one iterator and one pending next. Queues hold references,
not output, with limits of 128 references and 256 KiB metadata. Overflow, erased/
expired/denied step output or a failed authorization read detach with typed reasons
overflow, unavailable, or read_failed. Reconnect through read; there is no
durable intermediate replay. A late first iterator after result settlement receives
only a freshly authorized final view, unless already overflowed. That view may be
unavailable after erasure even if the earlier result promise completed. Detached
iterators cannot reattach, and pending store reads cannot deliver late events.
runTurn({root,intent,loadInput,trigger?,toolProfile?,signal?}) requires the root's
closed request descriptor. Its configuration/output versions and retention must
match construction, caps must match, maxCalls may narrow, and the declared root
sensitivity must equal the intent. The host assigns inputRevision to the complete
immutable request, including profile/trigger/intent. Changed metadata conflicts
before content access; never accept browser/model identities as authority.
The root ID is the turn ID; only the runner allocates child slots, IDs and parents.
Admission, root claim and pending final-result reservation precede loadInput, hooks, recall, history and media. Busy/replayed requests run nothing. Successful steps expose usable output only behind governed receipts. Main, direct, delegate and cold-start summary calls share the root; inclusive totals are not new charges. Explicit warn caps preserve results. Numeric/explicit block caps retain their stopping behavior, including zero-cost preflight refusal. The legacy SpendStore.add and AuditLog.cost writers are never called by this runner.
Final results and deployment
rootResults MUST be physically separate from step.results. For PostgreSQL,
create a dedicated schema and pool whose search_path resolves only that schema,
then use migrateExecutionResultStore/PostgresExecutionResultStore there. Configure
the same scoped roles and grant schema USAGE. Object inequality is checked but
cannot prove database isolation: two adapters pointing to the same table are an
invalid host composition. Final IDs are scoped root keys in this namespace; no
billable call or ordinal is fabricated. Do not use untrusted search_path entries.
Deploy all execution/accounting/admission migrations before the matching readers. Register both result namespaces and sessions in host erasure/retention workflows; createMemoryErasure's single results handle alone cannot cover both. Erasure or expiry of completed output returns unavailable and never regenerates content. Provider live smokes, publication and product delivery are separate evidence.
Completion and reconciliation
The result is a status view: completed with a TurnResult, busy with a root key,
in_progress, reconciliation_required, unavailable, or a non-durable not_admitted
routing refusal. read(scope,key) rechecks
admission, closed root, financial completion and authorized final content. It does
not dispatch, repair effects or load conversation context. Replays retain original
usage/cost and mark replayed: true; do not add them to new spend. Root warnings
are historical deduplicated evidence; notification delivery is a host concern.
Normal completion awaits handlers/children, append and turn:end, closes the root, persists and rechecks the final envelope, then finishes admission. A lost finish ACK is accepted only after reading back matching owner release, never operator resolution. Other uncertain writes, failures, cancellation and timeouts retain occupancy for explicit operator reconciliation. Available content alone is not proof of session/effect completion. No runner path calls resolve or retries append. If the public timeout races with an already-started finish after all work is quiescent, that finish may commit later; a subsequent read can verify completion. This exception never authorizes a finish after an earlier uncertain operation.
Handlers MUST await all external work. The harness tracks started model children, poisons swallowed child failures, and revokes gateways after each handler settles; it cannot discover arbitrary detached work hidden inside trusted host code. An ordinary handler rejection is uncertain even if labelled read-only. Hanging or failed context/hook work cannot authorize further effects. Timed-out dependencies may finish late; settlement evidence can drain in the background, but the turn never resumes automatically. Operators must reconcile effects/writes and establish quiescence before resolution. Use the step runner's recovery APIs for accounting; that does not complete a conversation.
Governed output preserves cross-category block order and omits reasoning from the reply. Refusal/truncation obey the governed-step output contract. There is no token streaming or automatic reactive context-window retry. Cold-start rotation is supported; uncertain provider failures require reconciliation. Batch/routines, history import and channel delivery remain separate adoption work.
The full PostgreSQL-backed gate exercises deterministic conversation composition, separate result namespaces, durable replay/erasure and lost append acknowledgements. No Prumo tour scene is added here: its source budget is nearly full, and the new capability's executable acceptance path is packages/conversation/test. A tour integration needs a separate budget-preserving scene slice.
Tools can use ctx.models.tryDelegate to handle structural delegation refusals.
Returning that exact invocation-local object emits an error tool result while the
parent continues. A refusal is not evidence that the handler had no effects;
throwing handlers, failed siblings, outstanding children and persistence failures
still require reconciliation. The existing delegate method retains its throwing
contract. S5 demonstrates explicit forwarding and settled parent/child accounting.
Safe context rotation
Cold rotation is unchanged. Reactive rotation requires a retained, authorized Anthropic pre-generation rejection plus its original zero call/root receipts. One summary and one replacement main call use new child identities under the same root/caps/deadline. Generic errors, ordinary zero usage, contradictory content, erased/expired proof, lost acknowledgements and cancellation cannot permit continuation. Earlier tools are never replayed. The OpenAI/OpenRouter parity exception remains for consolidated maintainer acceptance before cutover (spec: safe-context-rejection-rotation).
When an attested refusal finishes without another rotation (no policy, no removable
head, or a second refusal), the view is completed, result.terminalReason is
completed, result.stopReason is context_window_exceeded, and
result.reply.blocks is empty. Hosts must inspect stopReason, not just the
completed status: show a context-limit outcome instead of delivering a blank answer
as success. This replaces the legacy typed, retryable window error. The same admitted
key replays the original outcome; it does not retry the provider. A new attempt
requires a deliberate host decision and an appropriate context change. Generic
provider errors without attestation still require reconciliation.
The four product-visible differences (main-only progress usage, unpersisted input after a start-hook failure, withheld truncated tool closures, and completed context refusal) are enumerated for explicit owner acceptance in the review packet.
Private feature tests exercise volatile context, reasoning persistence and parent/delegate request-control isolation through this canonical API. Scripted usage remains explicit; missing evidence is never converted into a free call.
Deferred and provider-tool integration tests use the same canonical fixture, including grant isolation, settled search receipts, pause continuation and uncertainty when a search cannot be priced.
Media/wire tests also cover explicit lazy inline input, reference-only simple input, history replay and truncation suppression with retained receipts.
Financial feature tests pin requested versus served tier pricing, root warning attribution, sibling blocks and rejection of the removed per-turn override surface.
Output/text feature tests preserve tool ceilings and redaction, measured recall, Unicode repair for tools, invalid provider-text refusal and owning reply identity.
Recall feature tests distinguish normal content gating from source uncertainty, pre-admission cancellation and delegate sensitivity, with bounded timeout evidence.
Hook-boundary tests preserve request narrowing and shape-only trails, including retained uncertainty after sink failure or mutation of frozen hook input.
Recall uses the loop's single deadline with lifecycle-guarded source access. Cancellation clears the timer even if the source never settles; late source results cannot append, dispatch or release uncertain session ownership.
Basic engine feature tests cover schema/scope, profiles, nested grants and actual root/call correlation. Structural delegation refusals are explicit; execution failures retain uncertainty and suppress subsequent host lifecycle effects.
Construction rejects missing or noncallable spend readers. Persistent-cap tests use original governed receipts and counters, including tenant isolation and retained uncertainty when reads or settlement fail.
Lifecycle tests pin listener cleanup, frozen registration and awaited audit failures. A pre-aborted request is never admitted; a poisoned start hook leaves buffered input unpersisted. The final append is one user/assistant batch. Failure views withhold private errors and partial replies while original known receipts remain readable.
Final integration parity covers complete replay snapshots, daily budget-stop replay, no TTL takeover, main-only steps and duration before end hooks, cold summary history, and documented settled-stream consumption. The test ledger maps all remaining legacy cases and their explicitly superseded contracts.
Malformed cap configuration rejects with TypeError from the canonical financial normalizer, replacing legacy AgentConfigError. Invalid path scope rejects with InvalidScopeError before admission. Neither error authorizes execution.
Tool selection remains host-owned at the model-request boundary. The governed
step projection retains toolChoice in execution controls, while step:pre
cannot inject or change it, on parent or delegated steps. No global conversation
selection setting is added: one-call structured extraction uses the existing
createGovernedStepRunner from @alma-harness/single-call without session append,
recall or automatic memory extraction.
The text-only ctx.models.direct gateway rejects a supplied toolChoice rather
than discarding it; invalid child work retains the normal uncertain lifecycle.
