Agent conversation history
An agent often works through several steps: read information, call a capability,
inspect its result, and continue. The SDK retains exchanges between these steps
in RAM by default. This applies to both structured and native interfaces
without changing the Touba program.
History, observations, and business memory
| State | Purpose | Owner |
|---|---|---|
| Runtime observations | Arguments and results of operations actually executed | Runtime |
| Conversation history | Provider messages, call IDs, and continuity metadata | SDK and host storage |
A memory resource | Business data shared within declared scopes | Touba program and host binding |
History grants no permissions. Every new call goes through runtime checks. A capability may be used repeatedly, including with identical arguments, within its declared authority and budgets.
Start with RAM
After installing @touba/sdk, this provider module uses the default RAM store.
Keep the OpenAI key in the environment:
import { createOpenAiDecisionProvider } from "@touba/sdk";
export default createOpenAiDecisionProvider({
model: "gpt-4.1-mini",
decisionInterface: "native",
history: {
maxMessages: 128,
maxBytes: 262144,
onEvent(event) {
console.info(event.kind, event.messageCount, event.bytes);
},
},
});
Omitting history enables the same defaults. Each invocation has a distinct
identity, including two calls to the same agent with identical inputs. Delegated
agents and parallel branches do not share their history. The provider and model
are also part of the conversation identity.
The RAM store retains at most 128 conversations, 32 MiB in total, and one hour of
inactivity. Configure these bounds with maxSessions, maxStoreBytes, and ttlMs
inside history. Completed conversations remain until expiration or deletion.
Expired entries are purged on the next access; this is not secure memory erasure.
Bound the size
maxBytes bounds UTF-8 JSON bytes of transmitted messages (262144 by default),
and maxMessages bounds their number (128). Storage is independent:
maxRetainedBytes bounds each retained conversation to 8 MiB by default, in
addition to the store-wide limits. Context compaction does not delete retained
exchanges. A full store refuses atomically instead of erasing older messages.
TTL and delete still apply; this is not a permanent audit log.
Byte and message bounds do not count tokens and exclude the catalogue and system instructions. The context guard below counts the full HTTP request with the provider.
The SDK removes the oldest complete exchanges when necessary. It keeps the
latest complete exchange and all current runtime observations. Calls and results
are never split. onEvent reports removals without exposing messages or
signatures. No replacement summary is invented by default. If the mandatory context exceeds
a limit, the SDK refuses with DecisionHistoryLimitExceeded.
Size limits do not establish business correctness. The application must still verify results and decide how to handle exhausted budgets.
A received provider response without a valid typed decision produces
AgentDecisionProviderSchemaViolation, rather than a service-outage diagnosis.
The trace retains the SDK diagnosis in provider_error_code when available.
AgentDecisionProviderUnavailable denotes a transport or provider failure.
Local history and context refusals keep their own codes, such as
DecisionHistoryBusy, DecisionHistoryLimitExceeded,
DecisionHistorySummaryInvalid and ProviderContextWindowExceeded.
They are not classified as malformed model output and do not retry the provider.
Count tokens before generation
With OpenAI Responses, add context: { contextWindowTokens: 32768 } and
maxOutputTokens: 1000 to the factory. Here 32768 is a deliberate application
ceiling: choose a value no greater than the model's verified context window.
The SDK does not guess a window from a model name. Without context, only
history byte/message bounds apply.
The guard counts the serialized request, including instructions, tools,
schemas and metadata. It reserves
ceil(input_tokens × 1.2) + 512 + maxOutputTokens by default.
Configure the margin with inputTokenMultiplier and safetyMarginTokens.
Provider counts may be estimates; a margin is not a mathematical guarantee
about an external tokenizer. The window remains an explicit host ceiling.
If the request exceeds this window, the SDK removes an old complete exchange
from the context and counts again, before any generation. It keeps the latest
exchange and all current observations. If this minimum does not fit, it raises
ProviderContextWindowExceeded. An unavailable count raises
ProviderContextCountUnavailable, without generation. A generation HTTP error
never triggers compaction. Completed model responses are never replayed to
obtain a better answer.
For Anthropic Messages or Gemini generateContent, attach
createProviderContextFetch to the AI SDK connector's fetch before passing its
model to createAiSdkDecisionProvider. Choose provider: "anthropic" or
"google". The guard supports official direct endpoints; proxies, Bedrock,
Vertex and other transports require a qualified adapter. Implicit server state,
OpenAI automatic truncation and external Gemini caches are refused by this
guard. Supply explicit messages.
provider_context_count events contain only provider, count, reservation,
window and fits. Counting adds requests and latency but generates no answer.
It does not replace runtime call budgets or a host monetary ceiling. See the
context and compaction tutorial.
Choose summary-based compaction
By default, no summarization LLM call occurs. The window keeps a suffix of
complete exchanges. A host may provide an async history.summarize callback
receiving { previousSummary, turns, maxBytes }. It returns nonempty text bounded
by maxSummaryBytes (4096 bytes by default). It receives only exchanges removed
since the preceding summary. With opaque_refs, these remain projected and
raw generated arguments are not reintroduced.
The summary is added as untrusted user data, never as system instructions
or permissions. It records the number and digest of covered exchanges;
decision_history_summary makes reduction observable without exposing text.
The resulting context is counted again, including the summary. Original messages
remain retained. An invalid summary raises DecisionHistorySummaryInvalid.
If the callback uses a model, the host must bound and account for those calls, protect their data and configure their timeout. They are not implicitly covered by the runtime's agent-provider call counter. The SDK does not guarantee summary fidelity: a task requiring exact details must use retained observations or a governed business resource. Never put authorization in a summary.
Persist with SQLite
To retain messages after restart, create a private directory excluded from Git, then use the SQLite store supplied with the SDK and Node.js 24:
import { mkdirSync } from "node:fs";
import { createOpenAiDecisionProvider } from "@touba/sdk";
import { createSqliteDecisionHistoryStore } from "@touba/sdk/history/sqlite";
mkdirSync(".private-state", { recursive: true, mode: 0o700 });
const store = createSqliteDecisionHistoryStore({
filename: ".private-state/conversations.sqlite",
});
export default createOpenAiDecisionProvider({
model: "gpt-4.1-mini",
decisionInterface: "native",
history: { store, namespace: "support-production" },
});
An existing directory keeps its permissions; check them before use. The store
also accepts an open DatabaseSync connection as database instead of
filename. The application then owns connection closure. SQLite defaults to
128 conversations, 32 MiB, and 24 hours of inactivity. Its maxSessions,
maxBytes, and ttlMs options configure storage limits.
A shared store requires a namespace supplied by trusted application
configuration, never by model input. Isolate applications and tenants where
required. Stored conversations are private application data: protect access,
backups, and retention. The SDK does not encrypt the database.
Approval resume retains the invocation identity. Reuse the same store,
namespace, model, and interface. After a RAM-only process restarts, checkpoint
observations remain available, but original provider messages cannot be
recreated. The resumedWithoutMessages event field reports this situation.
Two overlapping decisions for one conversation fail with DecisionHistoryBusy.
SQLite retains its reservation after a crash. Stop and verify the old worker
before calling store.recover(sessionKey, { workerStopped: true }). This releases
storage ownership, not an uncertain business effect. Effect reconciliation and
checkpoints remain the runtime's and application's responsibility.
store.delete(sessionKey) removes an idle conversation; store.close() closes
the connection created by the store.
store.read(sessionKey) returns a detached private snapshot, or null when
absent or expired, without renewing TTL. Reading an active conversation refuses.
The SQLite tutorial shows inspection after
process exit and distinguishes a new run from resume.
Connect another database
history.store accepts a host adapter implementing transact(key, operation).
It supplies a private record snapshot, or null, to operation, atomically
saves the returned record, and returns result. The operation is asynchronous
and may contact the provider. The adapter must exclude overlapping processing
of the same key, preserve the old record on exceptions, and handle capacity,
expiration, and crash recovery. An unprotected get/set pair is insufficient.
Continuity and content boundaries
In inline mode, the SDK keeps connector-returned messages, including call IDs
and opaque provider metadata. It adds a native call result only when the matching
runtime observation arrives. A pending proposal is never presented as an
executed operation. Structured calls also use message-retaining generation.
An injected generateObject implementation returning only a typed object allows
the SDK to retain that object, not metadata the implementation does not expose.
In opaque_refs mode, runtime projection takes priority: raw prior model
arguments are not reintroduced through history. Only projected inputs and
decision metadata are retained for the next request.
history: false explicitly selects a stateless provider. Real runtime observations
remain data, without invented provider call IDs or signatures. A custom host
enabling history must supply run_id,
invocation_id, and agent_ref; the Touba runtime supplies them automatically.
Private SQLite files
With filename, the SDK creates or restores the database and its SQLite sidecars
to POSIX mode 0600 before opening them. Symlinks, non-regular files and a parent
directory writable by other users are refused with DecisionHistoryStorageUnsafe.
Configure directory ACLs on Windows as well. If you provide an existing
DatabaseSync connection, the host owns file permissions and connection closure.
The SDK does not encrypt conversation history.