@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
790 lines • 31.4 kB
JavaScript
import { actionProposalOutcomeFenceValid, actionProposalOutcomeText, } from '../action-proposal-outcome.js';
import { snapshotActionProposal } from '../action-proposal-transitions.js';
import { sumUsage } from '../spi/agent-store.js';
import { toolCallApprovalFromRow } from '../spi/approval-policy.js';
import { InMemoryActionProposalStore } from './in-memory-action-proposal-store.js';
/** A fully in-memory `AgentStore` for tests and the offline demo. */
export class InMemoryAgentStore extends InMemoryActionProposalStore {
threads = new Map();
/** Each thread's waiting messages, in run order. */
queues = new Map();
pauses = new Map();
/** Keyed by message and model-supplied id: two responses can be handed the same id (see `toolCallKey`). */
toolCalls = new Map();
usage = [];
genuiCalls = new Map();
runs = new Map();
now() {
return new Date().toISOString();
}
async createThread(input) {
const id = input.id ?? crypto.randomUUID();
if (this.threads.has(id)) {
throw new Error(`thread ${id} already exists`);
}
const ts = this.now();
const row = {
id,
actorRef: input.actor.id,
tenantRef: input.actor.tenantRef ?? null,
title: input.title ?? 'New chat',
persona: input.persona !== undefined && input.persona.length > 0 ? input.persona : null,
transient: input.transient ?? false,
createdAt: ts,
updatedAt: ts,
messages: [],
};
this.threads.set(id, row);
return this.toSummary(row);
}
async getThread(threadId) {
const row = this.threads.get(threadId);
if (row === undefined) {
return null;
}
return {
...this.toSummary(row),
messages: row.messages.map((message) => this.withApprovals(message)),
};
}
async getThreadActionProposalScope(threadId) {
const row = this.threads.get(threadId);
return row ? { threadId, actorRef: row.actorRef, tenantRef: row.tenantRef } : null;
}
async getThreadActorRef(threadId) {
return this.threads.get(threadId)?.actorRef ?? null;
}
async listThreads(actorRef, limit = 50) {
return [...this.threads.values()]
.filter((row) => row.actorRef === actorRef && !row.transient)
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
.slice(0, limit)
.map((row) => this.toSummary(row));
}
async promoteThread(threadId) {
const row = this.threads.get(threadId);
if (row?.transient === true) {
row.transient = false;
row.updatedAt = this.now();
}
}
async softDeleteThread(threadId) {
this.threads.delete(threadId);
}
async forkThread(threadId, fromMessageId) {
const source = this.threads.get(threadId);
if (source === undefined) {
throw new Error(`thread ${threadId} not found`);
}
const cutoff = source.messages.findIndex((message) => message.id === fromMessageId);
const kept = cutoff >= 0 ? source.messages.slice(0, cutoff + 1) : [...source.messages];
const id = crypto.randomUUID();
const ts = this.now();
const row = {
id,
actorRef: source.actorRef,
tenantRef: source.tenantRef,
title: source.title,
persona: source.persona,
transient: false,
createdAt: ts,
updatedAt: ts,
// Feedback rates a message in ITS thread; a fork starts unrated.
messages: kept.map(({ feedback: _feedback, ...message }) => ({ ...message })),
...(source.model != null ? { model: source.model } : {}),
...(source.defaultAgent != null ? { defaultAgent: source.defaultAgent } : {}),
};
this.threads.set(id, row);
return this.toSummary(row);
}
async setTitle(threadId, title) {
const row = this.threads.get(threadId);
if (row !== undefined) {
row.title = title;
row.updatedAt = this.now();
}
}
async updateThread(threadId, patch) {
const row = this.threads.get(threadId);
if (row === undefined) {
return;
}
if (patch.title !== undefined) {
row.title = patch.title;
}
if (patch.model !== undefined) {
row.model = patch.model;
}
if (patch.defaultAgent !== undefined) {
row.defaultAgent = patch.defaultAgent;
}
if (patch.persona !== undefined) {
row.persona = patch.persona;
}
row.updatedAt = this.now();
}
async defaultAgentForThread(threadId) {
return this.threads.get(threadId)?.defaultAgent ?? null;
}
async personaForThread(threadId) {
return this.threads.get(threadId)?.persona ?? null;
}
async clearActiveStream(threadId, runId) {
const row = this.threads.get(threadId);
if (row !== undefined && row.activeStreamId === runId) {
delete row.activeStreamId;
}
}
async activeRunForThread(threadId) {
return this.threads.get(threadId)?.activeStreamId ?? null;
}
async threadHeldByRun(runId) {
for (const [threadId, row] of this.threads) {
if (row.activeStreamId === runId) {
return threadId;
}
}
return null;
}
async claimActiveStream(threadId, runId, options = {}) {
const row = this.threads.get(threadId);
if (row === undefined) {
return false;
}
const holder = row.activeStreamId;
if (holder !== undefined && holder !== runId && holder !== options.replacing) {
return false;
}
row.activeStreamId = runId;
return true;
}
async releaseActiveStream(threadId, runId) {
const row = this.threads.get(threadId);
if (row?.activeStreamId !== runId) {
return false;
}
delete row.activeStreamId;
return true;
}
async enqueueMessage(input) {
const ts = this.now();
const message = {
id: crypto.randomUUID(),
threadId: input.threadId,
actor: input.actor,
content: input.content,
...(input.attachments !== undefined && input.attachments.length > 0
? { attachments: input.attachments }
: {}),
...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
...(input.persona !== undefined ? { persona: input.persona } : {}),
...(input.model !== undefined ? { model: input.model } : {}),
...(input.pageContext !== undefined ? { pageContext: input.pageContext } : {}),
...(input.uiCapabilities !== undefined ? { uiCapabilities: input.uiCapabilities } : {}),
...(input.hostContext !== undefined ? { hostContext: input.hostContext } : {}),
...(input.sentAt !== undefined ? { sentAt: input.sentAt } : {}),
...(input.interrupt === true ? { interrupt: true } : {}),
createdAt: ts,
updatedAt: ts,
};
const queue = this.queues.get(input.threadId) ?? [];
if (input.at === 'head') {
queue.unshift(message);
}
else {
queue.push(message);
}
this.queues.set(input.threadId, queue);
return { ...message };
}
async listQueue(threadId) {
return (this.queues.get(threadId) ?? []).map((message) => ({ ...message }));
}
async getQueuedMessage(id) {
const found = this.findQueued(id);
return found === undefined ? null : { ...found.message };
}
async updateQueuedMessage(id, patch) {
const found = this.findQueued(id);
if (found === undefined) {
return null;
}
const { message } = found;
if (patch.content !== undefined) {
message.content = patch.content;
}
if (patch.attachments !== undefined) {
if (patch.attachments === null || patch.attachments.length === 0) {
delete message.attachments;
}
else {
message.attachments = patch.attachments;
}
}
if (patch.interrupt === true) {
message.interrupt = true;
}
else if (patch.interrupt === false) {
delete message.interrupt;
}
message.updatedAt = this.now();
return { ...message };
}
async moveQueuedMessage(id, index) {
const found = this.findQueued(id);
if (found === undefined) {
return false;
}
const { queue, position, message } = found;
queue.splice(position, 1);
const target = Math.max(0, Math.min(queue.length, Math.trunc(index)));
queue.splice(target, 0, message);
return true;
}
async removeQueuedMessage(id) {
const found = this.findQueued(id);
if (found === undefined) {
return false;
}
found.queue.splice(found.position, 1);
return true;
}
async clearQueue(threadId) {
const count = this.queues.get(threadId)?.length ?? 0;
this.queues.delete(threadId);
return count;
}
async queuePause(threadId) {
return this.pauses.get(threadId) ?? null;
}
async setQueuePause(threadId, pause) {
if (pause === null) {
this.pauses.delete(threadId);
}
else {
this.pauses.set(threadId, pause);
}
}
findQueued(id) {
for (const queue of this.queues.values()) {
const position = queue.findIndex((message) => message.id === id);
const message = queue[position];
if (message !== undefined) {
return { queue, position, message };
}
}
return undefined;
}
async setActiveStream(threadId, runId) {
const row = this.threads.get(threadId);
if (row !== undefined) {
if (runId === null) {
delete row.activeStreamId;
}
else {
row.activeStreamId = runId;
}
}
}
async admitActionProposalOutcome(command) {
const row = [...this.rows.values()].find((row) => row.outcome?.id === command.outcomeId);
if (!row?.outcome || !row.outcomeDelivery)
return { status: 'not_found' };
if (row.outcomeDelivery.status === 'admitted')
return {
status: 'unchanged',
...(row.outcomeDelivery.messageId !== undefined
? { messageId: row.outcomeDelivery.messageId }
: {}),
};
if (row.outcomeDelivery.status === 'discarded')
return { status: 'discarded' };
if (!actionProposalOutcomeFenceValid(row, command, this.clock()))
return { status: 'conflict' };
const thread = this.threads.get(row.threadId);
if (!thread) {
this.rows.set(row.id, snapshotActionProposal({
...row,
outcomeDelivery: { ...row.outcomeDelivery, status: 'discarded', lease: null },
}));
return { status: 'discarded' };
}
if (thread.actorRef !== row.actorRef || thread.tenantRef !== row.tenantRef)
return { status: 'conflict' };
if (thread.activeStreamId)
return { status: 'busy' };
// No await between checking the thread holder and committing both in-memory mutations.
const message = {
id: crypto.randomUUID(),
role: 'assistant',
content: actionProposalOutcomeText(row.outcome),
createdAt: this.now(),
actionProposalOutcome: snapshotActionProposal(row.outcome),
ui: snapshotActionProposal(row.outcome.ui),
};
const next = snapshotActionProposal({
...row,
outcomeDelivery: {
...row.outcomeDelivery,
status: 'admitted',
lease: null,
messageId: message.id,
},
});
thread.messages.push(message);
thread.updatedAt = message.createdAt;
this.rows.set(row.id, next);
return { status: 'applied', messageId: message.id };
}
async appendMessage(input) {
const row = this.threads.get(input.threadId);
if (row === undefined) {
throw new Error(`thread ${input.threadId} not found`);
}
const message = {
id: crypto.randomUUID(),
role: input.role,
content: input.content,
createdAt: this.now(),
...(input.toolCalls !== undefined ? { toolCalls: input.toolCalls } : {}),
...(input.toolResults !== undefined ? { toolResults: input.toolResults } : {}),
...(input.attachments !== undefined ? { attachments: input.attachments } : {}),
...(input.followUps !== undefined ? { followUps: input.followUps } : {}),
...(input.usage !== undefined ? { usage: input.usage } : {}),
...(input.persona !== undefined ? { persona: input.persona } : {}),
...(input.runId !== undefined ? { runId: input.runId } : {}),
...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
...(input.reasoning !== undefined ? { reasoning: input.reasoning } : {}),
...(input.reasoningMs !== undefined ? { reasoningMs: input.reasoningMs } : {}),
...(input.ui !== undefined ? { ui: input.ui } : {}),
...(input.actionProposalOutcome !== undefined
? { actionProposalOutcome: snapshotActionProposal(input.actionProposalOutcome) }
: {}),
...(input.sentAt !== undefined ? { sentAt: input.sentAt } : {}),
};
row.messages.push(message);
row.updatedAt = message.createdAt;
return message;
}
async setMessageUi(messageId, ui) {
for (const row of this.threads.values()) {
const index = row.messages.findIndex((candidate) => candidate.id === messageId);
const message = row.messages[index];
if (message !== undefined) {
row.messages[index] = { ...message, ui };
return;
}
}
}
async setMessageToolResults(messageId, results) {
for (const row of this.threads.values()) {
const message = row.messages.find((candidate) => candidate.id === messageId);
if (message !== undefined) {
message.toolResults = results;
return;
}
}
}
async truncateFrom(threadId, messageId) {
const row = this.threads.get(threadId);
if (row === undefined) {
return;
}
const cutoff = row.messages.findIndex((message) => message.id === messageId);
if (cutoff >= 0) {
row.messages = row.messages.slice(0, cutoff);
}
}
/**
* Of `mediaIds`, the ones a surviving (or queued) message in one of this actor's threads still
* carries. Re-derived from the messages each call, so a media whose message was truncated away reads as
* unreferenced again.
*/
async referencedMediaIds(actorRef, mediaIds) {
if (mediaIds.length === 0) {
return [];
}
const wanted = new Set(mediaIds);
const found = new Set();
for (const thread of this.threads.values()) {
if (thread.actorRef !== actorRef) {
continue;
}
// A message waiting in the thread's queue carries its attachments too: it has been sent, it
// just has not run yet — collecting its media would fail the turn it is waiting to start.
const queued = this.queues.get(thread.id) ?? [];
for (const message of [...thread.messages, ...queued]) {
for (const attachment of message.attachments ?? []) {
if (wanted.has(attachment.mediaId)) {
found.add(attachment.mediaId);
}
}
}
}
return [...wanted].filter((mediaId) => found.has(mediaId));
}
async threadOfMessage(messageId) {
return this.threadIdForMessage(messageId) ?? null;
}
async setMessageFeedback(messageId, feedback) {
for (const row of this.threads.values()) {
const index = row.messages.findIndex((candidate) => candidate.id === messageId);
const message = row.messages[index];
if (message !== undefined) {
const { feedback: _previous, ...rest } = message;
row.messages[index] = feedback !== null ? { ...rest, feedback } : rest;
return;
}
}
}
/**
* A provider only promises a call id is unique within one response (one assistant message), so a
* call is keyed by its message too — as `LucidAgentStore` keys its rows.
*/
toolCallKey(toolCallId, messageId) {
return `${messageId}\u0000${toolCallId}`;
}
/**
* The newest call `runId` recorded under `toolCallId`, or without a run the newest overall — the
* same rule as `LucidAgentStore`. Insertion order is recording order.
*/
findToolCall(toolCallId, runId) {
let newest;
for (const row of this.toolCalls.values()) {
if (row.toolCallId === toolCallId && (runId === undefined || row.runId === runId)) {
newest = row;
}
}
return newest;
}
async toolCallInput(toolCallId, runId) {
return this.findToolCall(toolCallId, runId)?.input ?? null;
}
async getToolCallRunId(toolCallId) {
return this.findToolCall(toolCallId)?.runId ?? null;
}
async toolCallOutcomes(toolCallIds) {
const outcomes = [];
for (const id of toolCallIds) {
const row = this.findToolCall(id);
if (row !== undefined) {
outcomes.push({
id,
status: row.status,
...(row.output !== undefined ? { output: row.output } : {}),
...(row.error !== undefined ? { error: row.error } : {}),
});
}
}
return outcomes;
}
async failUnsettledToolCalls(runId, error) {
let settled = 0;
for (const row of this.toolCalls.values()) {
if (row.runId === runId && row.status === 'pending_approval') {
row.status = 'failed';
row.error = error;
settled += 1;
}
}
return settled;
}
async recordToolCall(input) {
this.toolCalls.set(this.toolCallKey(input.toolCallId, input.messageId), {
toolCallId: input.toolCallId,
messageId: input.messageId,
threadId: this.threadIdForMessage(input.messageId) ?? '',
toolName: input.toolName,
toolType: input.toolType,
input: input.input,
status: input.status,
...(input.proposalId !== undefined ? { proposalId: input.proposalId } : {}),
createdAt: this.now(),
...(input.runId !== undefined ? { runId: input.runId } : {}),
...(input.confirmation !== undefined ? { confirmation: { ...input.confirmation } } : {}),
...(input.approver !== undefined ? { approver: input.approver } : {}),
...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
});
}
async updateToolCall(input) {
const row = this.findToolCall(input.toolCallId, input.runId);
if (row === undefined) {
return;
}
row.status = input.status;
if (input.output !== undefined) {
row.output = input.output;
}
if (input.error !== undefined) {
row.error = input.error;
}
if (input.executionMs !== undefined) {
row.executionMs = input.executionMs;
}
if (input.executedByRef !== undefined) {
row.executedByRef = input.executedByRef;
}
if (input.remember !== undefined) {
row.remember = input.remember;
}
if (input.decidedVia !== undefined) {
row.decidedVia = input.decidedVia;
}
}
async rememberedApprovals(threadId) {
const names = new Set();
const scope = await this.getThreadActionProposalScope(threadId);
if (!scope)
return [];
for (const proposal of this.rows.values())
if (proposal.threadId === threadId &&
proposal.actorRef === scope.actorRef &&
proposal.tenantRef === scope.tenantRef &&
proposal.decision === 'approved' &&
(proposal.execution?.status === 'succeeded' || proposal.execution?.status === 'failed') &&
proposal.decisionAudit?.remember === true)
names.add(proposal.toolName);
for (const call of this.toolCalls.values()) {
if (call.threadId === threadId && call.remember === true) {
names.add(call.toolName);
}
}
return [...names];
}
async toolCallApproval(toolCallId, runId) {
const call = this.findToolCall(toolCallId, runId);
if (call === undefined) {
return null;
}
return {
status: call.status,
approver: call.approver ?? null,
expiresAt: call.expiresAt ?? null,
};
}
/** A message with the approval record of every call on it that was put to a person. */
withApprovals(message) {
const approvals = [];
for (const call of this.toolCalls.values()) {
if (call.messageId !== message.id) {
continue;
}
const approval = toolCallApprovalFromRow({
toolCallId: call.toolCallId,
proposalId: call.proposalId,
status: call.status,
approver: call.approver,
confirmation: call.confirmation,
expiresAt: call.expiresAt,
remember: call.remember,
executedByRef: call.executedByRef,
decidedVia: call.decidedVia,
error: call.error,
});
if (approval !== null) {
approvals.push(approval);
}
}
return approvals.length > 0 ? { ...message, approvals } : message;
}
async recordGenuiCalls(rows) {
for (const row of rows) {
const existing = this.genuiCalls.get(row.toolCallId);
this.genuiCalls.set(row.toolCallId, {
...row,
createdAt: existing?.createdAt ?? new Date(this.now()),
});
}
}
/** Generative-UI telemetry rows, oldest first (test helper; the governance feed). */
genuiCallRows() {
return [...this.genuiCalls.values()].map((row) => ({ ...row }));
}
async recordUsage(input) {
const createdAt = this.now();
this.usage.push({
actorRef: input.actorRef,
threadId: input.threadId,
modelId: input.modelId,
purpose: input.purpose,
inputTokens: input.usage.inputTokens,
outputTokens: input.usage.outputTokens,
day: createdAt.slice(0, 10),
createdAt,
...(input.runId !== undefined ? { runId: input.runId } : {}),
...(input.usage.cacheWriteTokens !== undefined
? { cacheWriteTokens: input.usage.cacheWriteTokens }
: {}),
...(input.usage.cacheReadTokens !== undefined
? { cacheReadTokens: input.usage.cacheReadTokens }
: {}),
...(input.costUsd !== undefined ? { costUsd: input.costUsd } : {}),
...(input.costUsd !== undefined && input.costSource !== undefined
? { costSource: input.costSource }
: {}),
});
}
async getRunActorRef(runId) {
return this.runs.get(runId)?.actorRef ?? null;
}
async recordRunStart(input) {
this.runs.set(input.runId, {
runId: input.runId,
threadId: input.threadId,
actorRef: input.actor.id,
status: 'running',
startedAt: this.now(),
stepCount: 0,
inputTokens: 0,
outputTokens: 0,
durable: input.durable ?? false,
...(input.actor.tenantRef !== undefined ? { tenantRef: input.actor.tenantRef } : {}),
...(input.agentName !== undefined ? { agentName: input.agentName } : {}),
...(input.parentRunId !== undefined ? { parentRunId: input.parentRunId } : {}),
});
}
async recordRunEnd(input) {
const row = this.runs.get(input.runId);
// First terminal wins: a no-op when the run is unknown or already settled (mirrors the Lucid twin).
if (row === undefined || row.status !== 'running') {
return;
}
row.status = input.status;
row.finishedAt =
input.finishedAt !== undefined ? new Date(input.finishedAt).toISOString() : this.now();
if (input.stepCount !== undefined)
row.stepCount = input.stepCount;
if (input.inputTokens !== undefined)
row.inputTokens = input.inputTokens;
if (input.outputTokens !== undefined)
row.outputTokens = input.outputTokens;
if (input.costUsd !== undefined)
row.costUsd = input.costUsd;
if (input.error !== undefined)
row.error = input.error;
if (input.ttftMs !== undefined)
row.ttftMs = input.ttftMs;
if (input.errorCode !== undefined)
row.errorCode = input.errorCode;
if (input.modelId !== undefined)
row.modelId = input.modelId;
}
async usageBetween(actorRef, fromDay, toDay) {
return sumUsage(this.usage.filter((row) => row.actorRef === actorRef && row.day >= fromDay && row.day <= toDay));
}
async quotaToday(actorRef, day) {
const usedTokens = this.usage
.filter((row) => row.actorRef === actorRef && row.day === day)
.reduce((sum, row) => sum + row.inputTokens + row.outputTokens, 0);
return { usedTokens };
}
/** Test helper: read the recorded usage rows (modelId + purpose + token totals). */
usageRows() {
return this.usage.map((row) => ({
actorRef: row.actorRef,
tokens: row.inputTokens + row.outputTokens,
modelId: row.modelId,
purpose: row.purpose,
}));
}
/** Governance read-model feed: recorded usage rows with the input/output split + thread/day. */
governanceUsage() {
return this.usage.map((row) => ({
actorRef: row.actorRef,
threadId: row.threadId,
modelId: row.modelId,
purpose: row.purpose,
inputTokens: row.inputTokens,
outputTokens: row.outputTokens,
day: row.day,
createdAt: row.createdAt,
...(row.runId !== undefined ? { runId: row.runId } : {}),
...(row.cacheWriteTokens !== undefined ? { cacheWriteTokens: row.cacheWriteTokens } : {}),
...(row.cacheReadTokens !== undefined ? { cacheReadTokens: row.cacheReadTokens } : {}),
...(row.costUsd !== undefined ? { costUsd: row.costUsd } : {}),
...(row.costSource !== undefined ? { costSource: row.costSource } : {}),
}));
}
/** Governance read-model feed: recorded tool calls with the resolved thread + timestamp. */
governanceToolCalls() {
return [...this.toolCalls.values()].map((row) => ({
toolCallId: row.toolCallId,
toolName: row.toolName,
toolType: row.toolType,
status: row.status,
threadId: row.threadId,
input: row.input,
createdAt: row.createdAt,
...(row.runId !== undefined ? { runId: row.runId } : {}),
...(row.output !== undefined ? { output: row.output } : {}),
...(row.error !== undefined ? { error: row.error } : {}),
...(row.executionMs !== undefined ? { executionMs: row.executionMs } : {}),
}));
}
/** Governance read-model feed: recorded runs (full lifecycle + rollups). */
governanceRuns() {
return [...this.runs.values()].map((row) => ({ ...row }));
}
/** Governance read-model feed: recorded messages with their thread + run correlation. */
governanceMessages() {
const rows = [];
for (const thread of this.threads.values()) {
for (const message of thread.messages) {
rows.push({
id: message.id,
threadId: thread.id,
role: message.role,
content: message.content,
createdAt: message.createdAt,
...(message.runId !== undefined ? { runId: message.runId } : {}),
});
}
}
return rows;
}
/** Governance read-model feed: thread metadata (title/actor/message count/last activity). */
governanceThreads() {
return [...this.threads.values()].map((row) => ({
threadId: row.id,
title: row.title,
actorRef: row.actorRef,
messageCount: row.messages.length,
updatedAt: row.updatedAt,
}));
}
threadIdForMessage(messageId) {
for (const [threadId, row] of this.threads) {
if (row.messages.some((message) => message.id === messageId)) {
return threadId;
}
}
return undefined;
}
/** Test helper: read the recorded tool-call rows. */
toolCallRows() {
return [...this.toolCalls.values()].map((row) => ({
toolCallId: row.toolCallId,
...(row.runId !== undefined ? { runId: row.runId } : {}),
toolName: row.toolName,
status: row.status,
...(row.output !== undefined ? { output: row.output } : {}),
}));
}
toSummary(row) {
const last = row.messages[row.messages.length - 1];
return {
id: row.id,
title: row.title,
persona: row.persona,
transient: row.transient,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
...(row.pinnedAt !== undefined ? { pinnedAt: row.pinnedAt } : {}),
...(last !== undefined ? { lastMessagePreview: last.content.slice(0, 120) } : {}),
model: row.model ?? null,
defaultAgent: row.defaultAgent ?? null,
activeRunId: row.activeStreamId ?? null,
};
}
}
//# sourceMappingURL=in-memory-store.js.map