Skip to content

v0.4.0

Choose a tag to compare

@gedaiu gedaiu released this 29 Sep 16:31
· 134 commits to main since this release
b219ecd

Durable sessions, provider retries, and three hook-reliability fixes. Nothing is removed and no existing signature changes, but three behaviours an existing consumer observes do change — see Upgrading from 0.3.x below.

Upgrading from 0.3.x

  • A closed socket no longer erases the conversation. The WebSocket close handler used to call sessionStore.delete(sessionId) unconditionally, so behind a durable store every conversation was written and then destroyed the moment the customer closed their tab. delete now has no engine-internal caller and is purely your erasure primitive. To restore the old behaviour, pass onDisconnect: sessionId => store.delete(sessionId) — but do not point that at MongoSessionStore, where delete destroys the document.
  • afterModelResponse now reports the whole turn's usage. It receives totalUsage, the sum across every provider call in the turn, rather than the last round alone. Billing and audit consumers will see totals rise on tool-heavy turns; that is the corrected figure.
  • onError now fires for rejections that are not Error instances. A non-Error value is wrapped in an Error whose cause is the original, so a hook typed against Error keeps compiling and a hook that wants the raw value can reach it.
  • SessionStore members are now Awaitable<T>. A synchronous store needs no change.
  • New optional peer dependency: mongodb, loaded only if MongoSessionStore actually connects.

Added

  • MongoSessionStore — a durable session store, exported as MongoSessionStore and createMongoSessionStore. Takes a collection, a client with a database name, or a connection URL; nothing reads the environment for you. No credential is ever written: authHeaders, authorization, cookie and host are stripped at any depth, and the same stripCredentialKeys is exported for stores you write yourself.
  • SessionStore.save(session) — the write signal a durable store needs, called once per processed user message from the same path that runs afterSession, so it fires for a turn that ended in a provider error as well as one that succeeded. What it throws is logged and the turn continues: a database outage is not a reason for the agent to stop answering.
  • SessionStore.evict(sessionId) and a bounded InMemorySessionStore, which now takes {maxAgeMs} (eight hours by default). There is no timer — a library-owned interval would keep your process alive.
  • withRetry(provider, policy) — wraps any AIProvider with per-attempt retry and two separate deadlines. Retries stop once the first chunk has been handed off, because streamed output cannot be un-sent; a stalled stream surfaces as an AIError with code TIMEOUT. Defaults: maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 5000, firstChunkTimeoutMs: 30000, idleChunkTimeoutMs: 30000, exponential backoff with full jitter.
  • HalEngineConfig.resilience?: RetryPolicy — when present, createHalEngine wraps the configured provider; when absent, behaviour is unchanged.

Fixed

  • onError and afterSession now run when beforeSession throws. The call sat above the try, so a failure during session setup skipped both hooks — and leaked whatever beforeSession had allocated on exactly the path where the allocation failed.

Full detail, including the entries not repeated here, is in CHANGELOG.md.