Skip to main content
Version: 1.0.0

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​

StatePurposeOwner
Runtime observationsArguments and results of operations actually executedRuntime
Conversation historyProvider messages, call IDs, and continuity metadataSDK and host storage
A memory resourceBusiness data shared within declared scopesTouba 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.