blob: 2e670cb1eb084388cef52b7581ad0ef8f20644b1 [file]
import type { AgentRunHeader, AgentRunStore, RuntimeEvent, RuntimeEventStore } from '@maka/core';
import { isSessionInlineRun } from '@maka/core';
import { stableHash, stableStringify } from './request-shape.js';
import { compareAgentGraphIdentity } from './stream-graph-identity.js';
export const AGENT_GRAPH_RECORD_SCHEMA_VERSION = 1 as const;
export const AGENT_GRAPH_RECORD_FACETS = [
'message',
'thinking',
'error',
'tool_call',
'tool_dispatch',
'tool_result',
'artifact_update',
'permission_request',
'permission_decision',
'user_question_request',
'transfer',
'usage',
'completed',
'failed',
'aborted',
'cancelled',
'runtime_fact',
] as const;
export type AgentGraphRecordFacet = (typeof AGENT_GRAPH_RECORD_FACETS)[number];
export type AgentGraphActivationStatus =
| 'running'
| 'completed'
| 'failed'
| 'aborted'
| 'cancelled';
export type AgentGraphSupervisorAttentionReason = 'permission_request' | 'user_question_request';
export type AgentGraphSupervisorSignal =
| {
kind: 'attention';
reason: AgentGraphSupervisorAttentionReason;
}
| {
kind: 'terminal';
status: Extract<AgentGraphActivationStatus, 'completed' | 'failed' | 'aborted' | 'cancelled'>;
};
/**
* Read-only binding between a graph operator and an existing durable Session.
*
* The graph does not own the Session or its RuntimeEvents. Each session-inline
* AgentRun is projected as one activation while the Session remains the stable
* operator execution identity across follow-ups and recovery.
*/
export interface AgentGraphOperatorBinding {
operatorId: string;
sessionId: string;
}
export interface AgentGraphRuntimeEventSource {
kind: 'runtime_event';
runtimeEventId: string;
sessionId: string;
runId: string;
turnId: string;
ts: number;
}
/**
* Stable tie-break metadata for records with the same immutable event time.
*
* `committedEventOrdinal` is counted after partial rows are excluded, so
* legacy partial-row retention or migration cannot renumber committed facts.
*/
export interface AgentGraphRecordOrderKey {
runCreatedAt: number;
operatorId: string;
runId: string;
committedEventOrdinal: number;
runtimeEventId: string;
}
/**
* A bounded reference-only record projected from one committed RuntimeEvent.
*
* The source RuntimeEvent remains authoritative. The graph record deliberately
* does not copy message/tool payloads, and `partial: true` events never enter
* this stream. Every record is also part of the always-on main-agent supervisor
* meta-stream; `supervisorSignals` marks facts that require semantic attention
* without putting the supervisor on the downstream data path.
*/
export interface AgentGraphRecord {
schemaVersion: typeof AGENT_GRAPH_RECORD_SCHEMA_VERSION;
recordId: string;
graphId: string;
operatorId: string;
activationId: string;
sessionId: string;
agentRunId: string;
eventTime: number;
orderKey: AgentGraphRecordOrderKey;
previousRecordId?: string;
type: 'agent_runtime_event';
facets: AgentGraphRecordFacet[];
supervisorSignals: AgentGraphSupervisorSignal[];
source: AgentGraphRuntimeEventSource;
}
/**
* Bounded always-on view consumed by the main-agent supervisor.
*
* It reuses the graph record identity and source reference, so it is a routing
* projection rather than a second fact. Empty `signals` still carries normal
* activity to the supervisor; non-empty signals mark semantic attention or a
* terminal milestone.
*/
export interface AgentGraphSupervisorMetaRecord {
recordId: string;
graphId: string;
operatorId: string;
activationId: string;
eventTime: number;
orderKey: AgentGraphRecordOrderKey;
facets: AgentGraphRecordFacet[];
signals: AgentGraphSupervisorSignal[];
source: AgentGraphRuntimeEventSource;
}
export interface AgentGraphActivationState {
activationId: string;
agentRunId: string;
status: AgentGraphActivationStatus;
recordCount: number;
firstEventTime: number;
lastEventTime: number;
lastRecordId: string;
terminalRecordId?: string;
}
export interface AgentGraphOperatorState {
operatorId: string;
sessionId: string;
status: AgentGraphActivationStatus;
currentActivationId: string;
activations: Record<string, AgentGraphActivationState>;
}
/**
* Deterministic trace state only. It intentionally has no graph-wide
* completion flag: topology closure and admission closure require a later
* control protocol and cannot be inferred from observed Agent runs alone.
*/
export interface AgentGraphReplayState {
graphId: string;
latestEventTime?: number;
appliedRecordIds: string[];
operators: Record<string, AgentGraphOperatorState>;
}
export interface AgentGraphRunStream {
operator: AgentGraphOperatorBinding;
run: AgentRunHeader;
events: readonly RuntimeEvent[];
}
export interface ProjectAgentGraphRecordsInput {
graphId: string;
streams: readonly AgentGraphRunStream[];
}
export interface AgentGraphProjection {
graphId: string;
operators: AgentGraphOperatorBinding[];
ignoredPartialEvents: number;
records: AgentGraphRecord[];
supervisorMetaStream: AgentGraphSupervisorMetaRecord[];
state: AgentGraphReplayState;
}
export interface ReadCommittedAgentGraphProjectionInput {
graphId: string;
operators: readonly AgentGraphOperatorBinding[];
runStore: Pick<AgentRunStore, 'listSessionRuns'>;
runtimeEventStore: Pick<RuntimeEventStore, 'readImmutableRuntimeEvents'>;
}
export interface AgentGraphProjectionWithRuns {
projection: AgentGraphProjection;
runs: AgentRunHeader[];
}
interface OrderedRuntimeEvent {
operator: AgentGraphOperatorBinding;
run: AgentRunHeader;
event: RuntimeEvent;
committedEventOrdinal: number;
}
interface MutableAgentGraphOperatorState extends Omit<AgentGraphOperatorState, 'activations'> {
activations: Map<string, AgentGraphActivationState>;
}
export async function readCommittedAgentGraphProjection(
input: ReadCommittedAgentGraphProjectionInput,
): Promise<AgentGraphProjection> {
return (await readCommittedAgentGraphProjectionWithRuns(input)).projection;
}
export async function readCommittedAgentGraphProjectionWithRuns(
input: ReadCommittedAgentGraphProjectionInput,
): Promise<AgentGraphProjectionWithRuns> {
assertGraphIdentity(input.graphId, input.operators);
const readImmutableRuntimeEvents = input.runtimeEventStore.readImmutableRuntimeEvents;
if (!readImmutableRuntimeEvents) {
throw new Error('Committed graph projection requires immutable RuntimeEvent reads');
}
const streams = (
await Promise.all(
input.operators.map(async (operator) => {
const runs = await input.runStore.listSessionRuns(operator.sessionId);
const orderedRuns = runs
.filter(isSessionInlineRun)
.sort((a, b) => a.createdAt - b.createdAt || compareAgentGraphIdentity(a.runId, b.runId));
return await Promise.all(
orderedRuns.map(async (run): Promise<AgentGraphRunStream> => {
if (run.sessionId !== operator.sessionId) {
throw new Error(
`Run ${run.runId} belongs to ${run.sessionId}, expected ${operator.sessionId}`,
);
}
return {
operator,
run,
events: await readImmutableRuntimeEvents.call(
input.runtimeEventStore,
operator.sessionId,
run.runId,
),
};
}),
);
}),
)
).flat();
const projected = projectAgentGraphRecords({ graphId: input.graphId, streams });
const state =
projected.records.length > 0
? replayAgentGraphRecords(projected.records)
: { graphId: input.graphId, appliedRecordIds: [], operators: {} };
return {
runs: streams.map((stream) => ({ ...stream.run })),
projection: {
graphId: input.graphId,
operators: input.operators.map((operator) => ({ ...operator })),
ignoredPartialEvents: projected.ignoredPartialEvents,
records: projected.records,
supervisorMetaStream: projected.supervisorMetaStream,
state,
},
};
}
export function projectAgentGraphRecords(input: ProjectAgentGraphRecordsInput): {
ignoredPartialEvents: number;
records: AgentGraphRecord[];
supervisorMetaStream: AgentGraphSupervisorMetaRecord[];
} {
const operators = uniqueBindings(input.streams.map((stream) => stream.operator));
assertGraphIdentity(input.graphId, operators);
const ordered: OrderedRuntimeEvent[] = [];
let ignoredPartialEvents = 0;
const sourceEventIds = new Set<string>();
for (const stream of input.streams) {
assertRunStream(stream);
let committedEventOrdinal = 0;
for (const event of stream.events) {
assertRuntimeEventIdentity(stream, event);
if (event.partial) {
ignoredPartialEvents += 1;
continue;
}
if (sourceEventIds.has(event.id)) {
throw new Error(
`RuntimeEvent ${event.id} is bound more than once in graph ${input.graphId}`,
);
}
sourceEventIds.add(event.id);
ordered.push({
operator: stream.operator,
run: stream.run,
event,
committedEventOrdinal,
});
committedEventOrdinal += 1;
}
}
ordered.sort(compareOrderedRuntimeEvents);
const previousByActivation = new Map<string, string>();
const records = ordered.map((item): AgentGraphRecord => {
const activationId = item.run.runId;
const activationKey = `${item.operator.operatorId}\0${activationId}`;
const previousRecordId = previousByActivation.get(activationKey);
const recordId = graphRecordId({
graphId: input.graphId,
operatorId: item.operator.operatorId,
sessionId: item.operator.sessionId,
runId: item.run.runId,
runtimeEventId: item.event.id,
});
previousByActivation.set(activationKey, recordId);
return {
schemaVersion: AGENT_GRAPH_RECORD_SCHEMA_VERSION,
recordId,
graphId: input.graphId,
operatorId: item.operator.operatorId,
activationId,
sessionId: item.operator.sessionId,
agentRunId: item.run.runId,
eventTime: item.event.ts,
orderKey: {
runCreatedAt: item.run.createdAt,
operatorId: item.operator.operatorId,
runId: item.run.runId,
committedEventOrdinal: item.committedEventOrdinal,
runtimeEventId: item.event.id,
},
...(previousRecordId ? { previousRecordId } : {}),
type: 'agent_runtime_event',
facets: runtimeEventFacets(item.event, item.run),
supervisorSignals: runtimeEventSupervisorSignals(item.event, item.run),
source: {
kind: 'runtime_event',
runtimeEventId: item.event.id,
sessionId: item.event.sessionId,
runId: item.event.runId,
turnId: item.event.turnId,
ts: item.event.ts,
},
};
});
return {
ignoredPartialEvents,
records,
supervisorMetaStream: records.map(projectSupervisorMetaRecord),
};
}
function projectSupervisorMetaRecord(record: AgentGraphRecord): AgentGraphSupervisorMetaRecord {
return {
recordId: record.recordId,
graphId: record.graphId,
operatorId: record.operatorId,
activationId: record.activationId,
eventTime: record.eventTime,
orderKey: { ...record.orderKey },
facets: [...record.facets],
signals: record.supervisorSignals.map((signal) => ({ ...signal })),
source: { ...record.source },
};
}
export function replayAgentGraphRecords(
records: readonly AgentGraphRecord[],
): AgentGraphReplayState {
if (records.length === 0) {
return { graphId: '', appliedRecordIds: [], operators: {} };
}
const uniqueRecords = new Map<string, AgentGraphRecord>();
for (const record of records) {
const existing = uniqueRecords.get(record.recordId);
if (existing) {
if (stableStringify(existing) !== stableStringify(record)) {
throw new Error(`Conflicting graph record ${record.recordId}`);
}
continue;
}
uniqueRecords.set(record.recordId, record);
}
const ordered = [...uniqueRecords.values()].sort(compareAgentGraphRecords);
const graphId = ordered[0]!.graphId;
const operatorsById = new Map<string, MutableAgentGraphOperatorState>();
const operatorBySession = new Map<string, string>();
for (const record of ordered) {
assertReplayRecord(record, graphId);
const sessionOwner = operatorBySession.get(record.sessionId);
if (sessionOwner && sessionOwner !== record.operatorId) {
throw new Error(
`Session ${record.sessionId} is bound to both ${sessionOwner} and ${record.operatorId}`,
);
}
operatorBySession.set(record.sessionId, record.operatorId);
let operator = operatorsById.get(record.operatorId);
if (!operator) {
operator = {
operatorId: record.operatorId,
sessionId: record.sessionId,
status: 'running',
currentActivationId: record.activationId,
activations: new Map(),
};
operatorsById.set(record.operatorId, operator);
} else if (operator.sessionId !== record.sessionId) {
throw new Error(
`Operator ${record.operatorId} is bound to both ${operator.sessionId} and ${record.sessionId}`,
);
}
let activation = operator.activations.get(record.activationId);
if (!activation) {
activation = {
activationId: record.activationId,
agentRunId: record.agentRunId,
status: 'running',
recordCount: 0,
firstEventTime: record.eventTime,
lastEventTime: record.eventTime,
lastRecordId: record.recordId,
};
operator.activations.set(record.activationId, activation);
} else {
if (activation.agentRunId !== record.agentRunId) {
throw new Error(`Activation ${record.activationId} references multiple AgentRuns`);
}
if (activation.terminalRecordId) {
throw new Error(
`Graph record ${record.recordId} appears after terminal record ${activation.terminalRecordId}`,
);
}
}
const status = activationStatusAfterRecord(record.facets);
activation.status = status;
activation.recordCount += 1;
activation.lastEventTime = record.eventTime;
activation.lastRecordId = record.recordId;
if (isTerminalActivationStatus(status)) {
activation.terminalRecordId = record.recordId;
}
operator.currentActivationId = activation.activationId;
operator.status = activation.status;
}
const operators: Record<string, AgentGraphOperatorState> = Object.fromEntries(
[...operatorsById].map(([operatorId, operator]) => {
const { activations, ...operatorState } = operator;
return [
operatorId,
{
...operatorState,
activations: Object.fromEntries(activations),
},
];
}),
);
return {
graphId,
latestEventTime: ordered.at(-1)!.eventTime,
appliedRecordIds: ordered.map((record) => record.recordId),
operators,
};
}
function runtimeEventFacets(event: RuntimeEvent, run: AgentRunHeader): AgentGraphRecordFacet[] {
const facets: AgentGraphRecordFacet[] = [];
switch (event.content?.kind) {
case 'text':
facets.push('message');
break;
case 'thinking':
facets.push('thinking');
break;
case 'error':
facets.push('error');
break;
case 'function_call':
facets.push('tool_call');
break;
case 'function_response':
facets.push('tool_result');
break;
}
const actions = event.actions;
if (actions?.toolDispatch) facets.push('tool_dispatch');
if (actions?.artifactDelta) facets.push('artifact_update');
if (actions?.permissionRequest) facets.push('permission_request');
if (actions?.permissionDecision) facets.push('permission_decision');
if (actions?.userQuestionRequest) facets.push('user_question_request');
if (actions?.transferToAgent) facets.push('transfer');
if (actions?.tokenUsage) facets.push('usage');
const terminalStatus = runtimeEventTerminalStatus(event, run);
if (terminalStatus) facets.push(terminalStatus);
if (facets.length === 0) facets.push('runtime_fact');
return facets;
}
function runtimeEventSupervisorSignals(
event: RuntimeEvent,
run: AgentRunHeader,
): AgentGraphSupervisorSignal[] {
const signals: AgentGraphSupervisorSignal[] = [];
if (event.actions?.permissionRequest) {
signals.push({ kind: 'attention', reason: 'permission_request' });
}
if (event.actions?.userQuestionRequest) {
signals.push({ kind: 'attention', reason: 'user_question_request' });
}
const terminalStatus = runtimeEventTerminalStatus(event, run);
if (terminalStatus) {
signals.push({ kind: 'terminal', status: terminalStatus });
}
return signals;
}
function runtimeEventTerminalStatus(
event: RuntimeEvent,
run: AgentRunHeader,
):
| Extract<AgentGraphActivationStatus, 'completed' | 'failed' | 'aborted' | 'cancelled'>
| undefined {
if (
event.status === 'completed' ||
event.status === 'failed' ||
event.status === 'aborted' ||
event.status === 'cancelled'
) {
return event.status;
}
return event.actions?.endInvocation ? terminalStatusFromRun(run) : undefined;
}
function terminalStatusFromRun(
run: AgentRunHeader,
): Extract<AgentGraphRecordFacet, 'completed' | 'failed' | 'cancelled'> {
switch (run.status) {
case 'completed':
case 'failed':
case 'cancelled':
return run.status;
default:
throw new Error(
`RuntimeEvent ended invocation ${run.runId} while its AgentRun is ${run.status}`,
);
}
}
function activationStatusAfterRecord(
facets: readonly AgentGraphRecordFacet[],
): AgentGraphActivationStatus {
const terminal = terminalStatusFromFacets(facets);
return terminal ?? 'running';
}
function terminalStatusFromFacets(
facets: readonly AgentGraphRecordFacet[],
):
| Extract<AgentGraphActivationStatus, 'completed' | 'failed' | 'aborted' | 'cancelled'>
| undefined {
const terminal = facets.filter(
(
facet,
): facet is Extract<AgentGraphRecordFacet, 'completed' | 'failed' | 'aborted' | 'cancelled'> =>
facet === 'completed' || facet === 'failed' || facet === 'aborted' || facet === 'cancelled',
);
if (terminal.length > 1) {
throw new Error(`Graph record carries conflicting terminal facets: ${terminal.join(', ')}`);
}
return terminal[0];
}
function isTerminalActivationStatus(status: AgentGraphActivationStatus): boolean {
return (
status === 'completed' || status === 'failed' || status === 'aborted' || status === 'cancelled'
);
}
function uniqueBindings(
operators: readonly AgentGraphOperatorBinding[],
): AgentGraphOperatorBinding[] {
const byOperator = new Map<string, AgentGraphOperatorBinding>();
for (const operator of operators) {
const existing = byOperator.get(operator.operatorId);
if (existing && existing.sessionId !== operator.sessionId) {
throw new Error(
`Operator ${operator.operatorId} is bound to both ${existing.sessionId} and ${operator.sessionId}`,
);
}
byOperator.set(operator.operatorId, operator);
}
return [...byOperator.values()];
}
function assertGraphIdentity(
graphId: string,
operators: readonly AgentGraphOperatorBinding[],
): void {
if (!graphId.trim()) throw new Error('Graph id must not be empty');
const operatorIds = new Set<string>();
const sessionIds = new Set<string>();
for (const operator of operators) {
if (!operator.operatorId.trim()) throw new Error('Operator id must not be empty');
if (!operator.sessionId.trim()) throw new Error('Operator session id must not be empty');
if (operatorIds.has(operator.operatorId)) {
throw new Error(`Duplicate graph operator ${operator.operatorId}`);
}
if (sessionIds.has(operator.sessionId)) {
throw new Error(`Session ${operator.sessionId} is bound to multiple graph operators`);
}
operatorIds.add(operator.operatorId);
sessionIds.add(operator.sessionId);
}
}
function assertRunStream(stream: AgentGraphRunStream): void {
if (stream.run.sessionId !== stream.operator.sessionId) {
throw new Error(
`Run ${stream.run.runId} belongs to ${stream.run.sessionId}, expected ${stream.operator.sessionId}`,
);
}
if (!isSessionInlineRun(stream.run)) {
throw new Error(`Graph activation ${stream.run.runId} must be a session-inline AgentRun`);
}
}
function assertRuntimeEventIdentity(stream: AgentGraphRunStream, event: RuntimeEvent): void {
if (
event.sessionId !== stream.operator.sessionId ||
event.runId !== stream.run.runId ||
event.turnId !== stream.run.turnId
) {
throw new Error(
`RuntimeEvent ${event.id} does not belong to ${stream.operator.sessionId}/${stream.run.runId}/${stream.run.turnId}`,
);
}
}
function compareOrderedRuntimeEvents(a: OrderedRuntimeEvent, b: OrderedRuntimeEvent): number {
return (
a.event.ts - b.event.ts ||
a.run.createdAt - b.run.createdAt ||
compareAgentGraphIdentity(a.operator.operatorId, b.operator.operatorId) ||
compareAgentGraphIdentity(a.run.runId, b.run.runId) ||
a.committedEventOrdinal - b.committedEventOrdinal ||
compareAgentGraphIdentity(a.event.id, b.event.id)
);
}
function compareAgentGraphRecords(a: AgentGraphRecord, b: AgentGraphRecord): number {
return (
a.eventTime - b.eventTime ||
a.orderKey.runCreatedAt - b.orderKey.runCreatedAt ||
compareAgentGraphIdentity(a.orderKey.operatorId, b.orderKey.operatorId) ||
compareAgentGraphIdentity(a.orderKey.runId, b.orderKey.runId) ||
a.orderKey.committedEventOrdinal - b.orderKey.committedEventOrdinal ||
compareAgentGraphIdentity(a.orderKey.runtimeEventId, b.orderKey.runtimeEventId) ||
compareAgentGraphIdentity(a.recordId, b.recordId)
);
}
function graphRecordId(input: {
graphId: string;
operatorId: string;
sessionId: string;
runId: string;
runtimeEventId: string;
}): string {
return `graph_record_${stableHash(input).slice('sha256:'.length, 'sha256:'.length + 32)}`;
}
function assertReplayRecord(record: AgentGraphRecord, graphId: string): void {
if (record.schemaVersion !== AGENT_GRAPH_RECORD_SCHEMA_VERSION) {
throw new Error(`Unsupported graph record schema ${record.schemaVersion}`);
}
if (record.graphId !== graphId) {
throw new Error(`Cannot replay records from graphs ${graphId} and ${record.graphId} together`);
}
if (!Number.isFinite(record.eventTime)) {
throw new Error(`Invalid event time on graph record ${record.recordId}`);
}
if (
record.source.sessionId !== record.sessionId ||
record.source.runId !== record.agentRunId ||
record.activationId !== record.agentRunId ||
record.source.ts !== record.eventTime ||
record.orderKey.operatorId !== record.operatorId ||
record.orderKey.runId !== record.agentRunId ||
record.orderKey.runtimeEventId !== record.source.runtimeEventId ||
!Number.isFinite(record.orderKey.runCreatedAt) ||
!Number.isSafeInteger(record.orderKey.committedEventOrdinal) ||
record.orderKey.committedEventOrdinal < 0
) {
throw new Error(`Invalid source identity on graph record ${record.recordId}`);
}
terminalStatusFromFacets(record.facets);
for (const signal of record.supervisorSignals) {
if (signal.kind === 'attention' && !record.facets.includes(signal.reason)) {
throw new Error(`Supervisor signal does not match graph record ${record.recordId}`);
}
if (signal.kind === 'terminal' && !record.facets.includes(signal.status)) {
throw new Error(`Supervisor terminal does not match graph record ${record.recordId}`);
}
}
}