| /** |
| * Canonical Runtime v2 event contract. |
| * |
| * This is the single internal runtime fact model. It is NOT a UI event |
| * (see ./events.ts `SessionEvent`) and NOT a trace row (RunTrace) or |
| * telemetry record. StoredMessage JSONL, renderer SessionEvent, |
| * AgentRunStore operational rows, RunTrace, and TelemetryRepo are all |
| * projections that should be written from -- or explicitly linked to -- these |
| * events. |
| * |
| * Architecture: docs/architecture/runtime-core-architecture-draft.md |
| * |
| * Phase 1 scope: types + small pure helpers only. No storage, runner, |
| * projection, or ledger logic lives here. Those arrive in later nodes. |
| */ |
| |
| import { |
| isMessageContent, |
| normalizeMessageContent, |
| type MessageContent, |
| type PermissionClosureReason, |
| } from './events.js'; |
| import { INTERACTION_ID_MAX_BYTES, INTERACTION_TOOL_NAME_MAX_BYTES } from './interaction.js'; |
| import type { PermissionRequestPayload, PermissionResponse } from './permission.js'; |
| import type { TurnOrigin } from './runtime-inputs.js'; |
| import type { UserQuestionRequest } from './user-question.js'; |
| import { |
| defineObjectShape, |
| hasExactShape, |
| isFiniteNumber, |
| isOptionalMember, |
| isOptionalString, |
| isRecord, |
| isStringArray, |
| } from './record-schema.js'; |
| import { |
| isPermissionDecisionFields, |
| isPermissionRequestPayload, |
| isUserQuestionRequest, |
| } from './interaction-record-schema.js'; |
| import { isTokenUsageFields, type TokenUsageFields } from './usage-record-schema.js'; |
| import { isToolRecoveryFactEnvelope, type ToolRecoveryFactEnvelope } from './tool-recovery-fact.js'; |
| import { |
| isRuntimeEventWorkspaceFactEnvelope, |
| type RuntimeEventWorkspaceFactEnvelope, |
| } from './workspace-version-authority.js'; |
| |
| // ============================================================================ |
| // Role / Author / Status |
| // ============================================================================ |
| |
| /** |
| * Conversation role the event plays in model history. Maps 1:1 with the |
| * roles providers expect in a message history (user / model / tool / |
| * system). Role is about *what lane* the content belongs to. |
| */ |
| export const RUNTIME_EVENT_ROLES = ['user', 'model', 'tool', 'system'] as const; |
| export type RuntimeEventRole = (typeof RUNTIME_EVENT_ROLES)[number]; |
| |
| export function isRuntimeEventRole(value: unknown): value is RuntimeEventRole { |
| return typeof value === 'string' && (RUNTIME_EVENT_ROLES as readonly string[]).includes(value); |
| } |
| |
| /** |
| * Who authored the event inside the runtime. `agent` covers the model + |
| * flow orchestration; `tool` covers tool execution; `system` covers the |
| * runner, gate, and recovery. Author is about *which subsystem* produced |
| * the fact, which is orthogonal to the model-history `role`. |
| * |
| * Not every (author, role) combination is meaningful, but the runtime — |
| * not this type module — owns the policy that constrains them. |
| */ |
| export const RUNTIME_EVENT_AUTHORS = ['user', 'host', 'agent', 'tool', 'system'] as const; |
| export type RuntimeEventAuthor = (typeof RUNTIME_EVENT_AUTHORS)[number]; |
| |
| export function isRuntimeEventAuthor(value: unknown): value is RuntimeEventAuthor { |
| return typeof value === 'string' && (RUNTIME_EVENT_AUTHORS as readonly string[]).includes(value); |
| } |
| |
| /** |
| * Lifecycle status an event asserts about its invocation/turn. Omitted on |
| * ordinary in-flight content events. Terminal values (completed / failed / |
| * aborted / cancelled) mark the last event of an invocation; `streaming` |
| * marks a non-terminal partial event that still carries lifecycle intent |
| * (e.g. a flow heartbeat) without being a content delta. |
| */ |
| export const RUNTIME_EVENT_STATUSES = [ |
| 'streaming', |
| 'completed', |
| 'failed', |
| 'aborted', |
| 'cancelled', |
| ] as const; |
| export type RuntimeEventStatus = (typeof RUNTIME_EVENT_STATUSES)[number]; |
| |
| export const TERMINAL_RUNTIME_EVENT_STATUSES: readonly RuntimeEventStatus[] = [ |
| 'completed', |
| 'failed', |
| 'aborted', |
| 'cancelled', |
| ]; |
| |
| export function isRuntimeEventStatus(value: unknown): value is RuntimeEventStatus { |
| return typeof value === 'string' && (RUNTIME_EVENT_STATUSES as readonly string[]).includes(value); |
| } |
| |
| /** Execution surface that produced a fact; absent on legacy ledgers. */ |
| export const RUNTIME_EVENT_ORIGINS = ['provider', 'code_mode'] as const; |
| export type RuntimeEventOrigin = (typeof RUNTIME_EVENT_ORIGINS)[number]; |
| |
| /** Explicit provider-history policy; absent means visible. */ |
| export const RUNTIME_EVENT_MODEL_VISIBILITIES = ['visible', 'hidden'] as const; |
| export type RuntimeEventModelVisibility = (typeof RUNTIME_EVENT_MODEL_VISIBILITIES)[number]; |
| |
| export function isTerminalRuntimeEventStatus(value: unknown): boolean { |
| return ( |
| typeof value === 'string' && |
| (TERMINAL_RUNTIME_EVENT_STATUSES as readonly string[]).includes(value) |
| ); |
| } |
| |
| // ============================================================================ |
| // Content (model-facing payload) |
| // ============================================================================ |
| |
| export interface RuntimeEventTextContent extends MessageContent { |
| kind: 'text'; |
| /** Provider-owned text metadata such as Responses URL citations. */ |
| providerOptions?: Record<string, unknown>; |
| /** Durable provenance for a host-authored user-role turn. */ |
| origin?: TurnOrigin; |
| /** |
| * Marks a user message steered into a running turn at a step boundary. |
| * `text` stays raw for UI/transcript projections; model-replay projections |
| * MUST wrap it in the steering envelope so the provider request has exactly |
| * one canonical form — bare text is not an identity (a steer can equal the |
| * current prompt or any historical user message verbatim). |
| */ |
| steering?: true; |
| } |
| |
| export interface RuntimeEventThinkingContent { |
| kind: 'thinking'; |
| text: string; |
| /** Anthropic signed thinking — MUST be re-sent on replay when present. */ |
| signature?: string; |
| /** Provider-owned replay metadata that must survive model replay. */ |
| providerOptions?: Record<string, unknown>; |
| } |
| |
| export interface RuntimeEventFunctionCallContent { |
| kind: 'function_call'; |
| /** Matches the tool-call id the provider issued and the matching response. */ |
| id: string; |
| name: string; |
| args: unknown; |
| /** Provider-owned opaque call metadata that must survive model replay. */ |
| providerOptions?: Record<string, unknown>; |
| providerExecuted?: boolean; |
| } |
| |
| export interface RuntimeEventFunctionResponseContent { |
| kind: 'function_response'; |
| /** Matches RuntimeEventFunctionCallContent.id. */ |
| id: string; |
| name: string; |
| result: unknown; |
| isError?: boolean; |
| providerExecuted?: boolean; |
| /** Raw provider result retained for provider-native replay; never rendered directly. */ |
| providerOutput?: unknown; |
| } |
| |
| export interface RuntimeEventErrorContent { |
| kind: 'error'; |
| code?: string; |
| /** Stable machine-readable reason for routing; mirrors ErrorEvent.reason. */ |
| reason?: string; |
| message: string; |
| /** Adapter MUST scrub secrets before populating this field. */ |
| details?: string[] | Record<string, unknown>; |
| } |
| |
| /** |
| * Content union for user/model text, model thinking, function call, |
| * function response, and error payloads. Discriminated by `kind` to |
| * match the existing ToolResultContent convention. |
| */ |
| export type RuntimeEventContent = |
| | RuntimeEventTextContent |
| | RuntimeEventThinkingContent |
| | RuntimeEventFunctionCallContent |
| | RuntimeEventFunctionResponseContent |
| | RuntimeEventErrorContent; |
| |
| export const RUNTIME_EVENT_CONTENT_KINDS = [ |
| 'text', |
| 'thinking', |
| 'function_call', |
| 'function_response', |
| 'error', |
| ] as const; |
| export type RuntimeEventContentKind = (typeof RUNTIME_EVENT_CONTENT_KINDS)[number]; |
| |
| // ============================================================================ |
| // Actions (control / side-effect intent) |
| // ============================================================================ |
| |
| /** |
| * Token usage carried as a runtime action rather than a content payload. |
| * Mirrors TokenUsageEvent / TokenUsageMessage so projections can map 1:1. |
| */ |
| export interface RuntimeEventTokenUsage extends TokenUsageFields {} |
| |
| /** |
| * Permission decision attached to an event. Runtime history may retain the |
| * originating tool identity for self-contained conversation copies. |
| */ |
| export interface RuntimeEventPermissionDecision extends PermissionResponse { |
| toolName?: string; |
| } |
| |
| export const TOOL_BOUNDARY_PROTOCOL_V1 = 't1_after_preflight_v1' as const; |
| export type ToolBoundaryProtocol = typeof TOOL_BOUNDARY_PROTOCOL_V1; |
| |
| /** |
| * Canonical fact that the runtime crossed the durable tool-dispatch boundary. |
| * Its presence means the implementation may have started; it does not assert |
| * that the implementation or any external side effect actually completed. |
| */ |
| export interface RuntimeEventToolDispatch { |
| protocol: ToolBoundaryProtocol; |
| operationId: string; |
| providerToolCallId: string; |
| toolName: string; |
| canonicalArgsHash: string; |
| recoveryMode: ToolRecoveryMode; |
| } |
| |
| export interface RuntimeEventProtocolMarker { |
| toolBoundary: ToolBoundaryProtocol; |
| } |
| |
| export interface RuntimeEventContinuationStartV2 { |
| protocol: 'continuation_start_v2'; |
| /** Mirrors store-owned claim state; recovery never trusts this field alone. */ |
| provenance: 'runtime_admission' | 'claim_repair'; |
| claimId: string; |
| boundaryDigest: `sha256:${string}`; |
| immediateSource: { |
| sessionId: string; |
| invocationId: string; |
| runId: string; |
| turnId: string; |
| highWater: number; |
| prefixDigest: `sha256:${string}`; |
| }; |
| replayManifestDigest: `sha256:${string}`; |
| providerProjectionVersion: 1; |
| providerReplayDigest: `sha256:${string}`; |
| } |
| |
| interface RuntimeEventAnswerAcceptedIdentity { |
| requestId: string; |
| } |
| |
| export interface RuntimeEventUserQuestionAnswerAccepted |
| extends RuntimeEventAnswerAcceptedIdentity {} |
| |
| export interface RuntimeEventPermissionAnswerAccepted extends RuntimeEventAnswerAcceptedIdentity {} |
| |
| export interface RuntimeEventPermissionClosureAccepted { |
| requestId: string; |
| reason: PermissionClosureReason; |
| } |
| |
| /** |
| * Control and side-effect intent carried alongside content. An event may |
| * carry content, actions, both, or (rarely) neither — but a terminal |
| * event without `actions.endInvocation` MUST assert a terminal `status`. |
| */ |
| export interface RuntimeEventActions { |
| /** Patch applied to invocation-scoped runtime state. */ |
| stateDelta?: Record<string, unknown>; |
| /** Artifact key → primitive delta (size/bytes/version counters, etc.). */ |
| artifactDelta?: Record<string, string | number | boolean>; |
| /** A permission prompt raised for a tool call. */ |
| permissionRequest?: PermissionRequestPayload; |
| /** A resolved permission decision (allow/deny) for a prior request. */ |
| permissionDecision?: RuntimeEventPermissionDecision; |
| /** Audit fact only; the canonical permission outcome remains in InteractionStore. */ |
| permissionAnswerAccepted?: RuntimeEventPermissionAnswerAccepted; |
| /** Audit fact that an unanswered hosted permission request was durably closed. */ |
| permissionClosureAccepted?: RuntimeEventPermissionClosureAccepted; |
| /** A bounded in-turn question raised by a tool call. */ |
| userQuestionRequest?: UserQuestionRequest; |
| /** Audit fact only; the canonical answer remains in InteractionStore. */ |
| userQuestionAnswerAccepted?: RuntimeEventUserQuestionAnswerAccepted; |
| /** Hand off the invocation to another agent (multi-agent transfer). */ |
| transferToAgent?: string; |
| /** Marks the event that closes the invocation. */ |
| endInvocation?: boolean; |
| /** Token accounting for the model call this event summarizes. */ |
| tokenUsage?: RuntimeEventTokenUsage; |
| /** Durable, non-model-visible T1 tool-dispatch fact. */ |
| toolDispatch?: RuntimeEventToolDispatch; |
| /** Reserved recovery fact; only the atomic recovery-bundle writer may persist it. */ |
| toolRecovery?: ToolRecoveryFactEnvelope; |
| /** Protocols that were actually active from the first event of this run. */ |
| runtimeProtocol?: RuntimeEventProtocolMarker; |
| /** Durable provider-call T1 for a claimed continuation. */ |
| continuationStart?: RuntimeEventContinuationStartV2; |
| /** Reserved workspace authority fact; only its atomic SQLite writer may persist it. */ |
| workspaceFact?: RuntimeEventWorkspaceFactEnvelope; |
| } |
| |
| // ============================================================================ |
| // Refs (links to projections / ledgers) |
| // ============================================================================ |
| |
| /** |
| * Links back to the projection/ledger rows written from (or correlated |
| * with) this event. Refs are diagnostics/audit pointers; a missing ref |
| * never changes runtime behavior. `toolCallId` doubles as the matching |
| * key for function_call ↔ function_response when provider ids differ. |
| */ |
| export interface RuntimeEventRefs { |
| storedMessageId?: string; |
| traceEventId?: string; |
| toolCallId?: string; |
| providerEventId?: string; |
| /** Canonical digest of a user submission before host-side preparation. */ |
| sourceMessageDigest?: `sha256:${string}`; |
| /** Trace-group id linking aggregate usage to physical provider attempts. */ |
| providerRequestTraceId?: string; |
| artifactId?: string; |
| /** Runtime-owned durable identity for one tool side-effect boundary. */ |
| operationId?: string; |
| /** Provider tool-call id of the enclosing exec operation. */ |
| parentToolCallId?: string; |
| /** Runtime-owned durable identity of the enclosing exec operation. */ |
| parentOperationId?: string; |
| /** |
| * Assistant step id for a function_call event: the id of the step's |
| * text/thinking messages (their `providerEventId`). Model replay pairs a |
| * step's signed thinking with its tool calls by this id. Absent on legacy |
| * (per-turn) events; a missing stepId marks history that cannot be paired |
| * and is replayed with the older degraded semantics. |
| */ |
| stepId?: string; |
| /** Source execution boundary for a safe-boundary continuation start fact. */ |
| sourceInvocationId?: string; |
| sourceRunId?: string; |
| sourceTurnId?: string; |
| sourceRuntimeEventHighWater?: number; |
| } |
| |
| /** Tool-owned contract for deciding what a later recovery phase may do. */ |
| export type ToolRecoveryMode = |
| | 'replay_safe' |
| | 'idempotent' |
| | 'reconcile' |
| | 'reattach' |
| | 'outcome_unknown' |
| | 'never_auto_retry'; |
| |
| // ============================================================================ |
| // RuntimeEvent |
| // ============================================================================ |
| |
| /** |
| * The canonical runtime fact. |
| * |
| * Phase 0-3 identity contract: one `invocationId` maps to one `runId`. |
| * Invocation identifies provider/tool execution while Run identifies its |
| * durable operational ledger. Store-owned control-plane streams (for example, |
| * workspace version authority) use reserved identities and have no AgentRun |
| * header. A continuation creates fresh values for both; |
| * `turnId` names the user turn and `ts` is Unix ms. |
| * |
| * `partial: true` marks a transient chunk (streaming text, progress) that |
| * is superseded by a later non-partial event. Projections decide whether |
| * to persist partials; model history MUST exclude them. |
| */ |
| export interface RuntimeEvent { |
| /** Event uuid — used for dedup on reconnect/replay. */ |
| id: string; |
| /** Durable invocation spine id; groups every run/turn of one request. */ |
| invocationId: string; |
| /** Durable operational run identity (maps to AgentRunHeader.runId). */ |
| runId: string; |
| sessionId: string; |
| /** Groups all events from one agent turn (maps to StoredMessage.turnId). */ |
| turnId: string; |
| /** Unix ms timestamp. */ |
| ts: number; |
| |
| /** Optional branch/agent lane for future multi-agent trees. */ |
| branch?: string; |
| /** True for transient streaming chunks superseded by a later event. */ |
| partial: boolean; |
| |
| role: RuntimeEventRole; |
| author: RuntimeEventAuthor; |
| /** Execution surface that produced this fact; absent on legacy ledgers. */ |
| origin?: RuntimeEventOrigin; |
| /** Explicit provider-history policy; absent means visible for legacy compatibility. */ |
| modelVisibility?: RuntimeEventModelVisibility; |
| /** Lifecycle assertion; omitted on ordinary in-flight content events. */ |
| status?: RuntimeEventStatus; |
| |
| content?: RuntimeEventContent; |
| actions?: RuntimeEventActions; |
| refs?: RuntimeEventRefs; |
| } |
| |
| /** |
| * Every key a RuntimeEvent envelope may carry. TypeScript forces this list to |
| * cover the interface, so it moves whenever the interface does — which makes it |
| * the thing anything re-implementing the envelope check must be pinned to. |
| * |
| * The Harbor trajectory exporter re-implements it in Python and drifted: it |
| * never learned `origin` or `modelVisibility`, so once the runtime started |
| * emitting them every event failed the check and all 89 cells of a benchmark |
| * run exported a one-line summary instead of a trajectory. The shared |
| * validation corpus was supposed to catch that and could not — it exercised |
| * the keys someone thought to write cases for, and those two were never among |
| * them. `runtime-event.test.ts` now holds the corpus to this list. |
| */ |
| export function runtimeEventEnvelopeKeys(): readonly string[] { |
| return [...RUNTIME_EVENT_SHAPE.allowed]; |
| } |
| |
| /** |
| * Every value each closed-domain envelope key may hold. |
| * |
| * Pinning the corpus to the key list closed one half of the drift the Harbor |
| * exporter fell through: a key nobody wrote a case for. The other half is a |
| * value nobody wrote a case for — the exporter spells these domains out again |
| * in Python, and a member added here that no case carries would leave the two |
| * disagreeing about what is valid with nothing red. Only the closed domains |
| * appear; `branch` is free text and has nothing to enumerate. |
| */ |
| export function runtimeEventEnvelopeValueDomains(): Readonly<Record<string, readonly string[]>> { |
| return { |
| role: RUNTIME_EVENT_ROLES, |
| author: RUNTIME_EVENT_AUTHORS, |
| origin: RUNTIME_EVENT_ORIGINS, |
| modelVisibility: RUNTIME_EVENT_MODEL_VISIBILITIES, |
| status: RUNTIME_EVENT_STATUSES, |
| }; |
| } |
| |
| const RUNTIME_EVENT_SHAPE = defineObjectShape<RuntimeEvent>()( |
| ['id', 'invocationId', 'runId', 'sessionId', 'turnId', 'ts', 'partial', 'role', 'author'], |
| ['branch', 'origin', 'modelVisibility', 'status', 'content', 'actions', 'refs'], |
| ); |
| const TEXT_CONTENT_SHAPE = defineObjectShape<RuntimeEventTextContent>()( |
| ['kind', 'text'], |
| [ |
| 'displayText', |
| 'origin', |
| 'attachments', |
| 'quotes', |
| 'inlineReferences', |
| 'steering', |
| 'providerOptions', |
| ], |
| ); |
| const THINKING_CONTENT_SHAPE = defineObjectShape<RuntimeEventThinkingContent>()( |
| ['kind', 'text'], |
| ['signature', 'providerOptions'], |
| ); |
| const FUNCTION_CALL_CONTENT_SHAPE = defineObjectShape<RuntimeEventFunctionCallContent>()( |
| ['kind', 'id', 'name', 'args'], |
| ['providerOptions', 'providerExecuted'], |
| ); |
| const FUNCTION_RESPONSE_CONTENT_SHAPE = defineObjectShape<RuntimeEventFunctionResponseContent>()( |
| ['kind', 'id', 'name', 'result'], |
| ['isError', 'providerExecuted', 'providerOutput'], |
| ); |
| const ERROR_CONTENT_SHAPE = defineObjectShape<RuntimeEventErrorContent>()( |
| ['kind', 'message'], |
| ['code', 'reason', 'details'], |
| ); |
| const RUNTIME_ACTIONS_SHAPE = defineObjectShape<RuntimeEventActions>()( |
| [], |
| [ |
| 'stateDelta', |
| 'artifactDelta', |
| 'permissionRequest', |
| 'permissionDecision', |
| 'permissionAnswerAccepted', |
| 'permissionClosureAccepted', |
| 'userQuestionRequest', |
| 'userQuestionAnswerAccepted', |
| 'transferToAgent', |
| 'endInvocation', |
| 'tokenUsage', |
| 'toolDispatch', |
| 'toolRecovery', |
| 'runtimeProtocol', |
| 'continuationStart', |
| 'workspaceFact', |
| ], |
| ); |
| const ANSWER_ACCEPTED_IDENTITY_SHAPE = defineObjectShape<RuntimeEventAnswerAcceptedIdentity>()( |
| ['requestId'], |
| [], |
| ); |
| const PERMISSION_CLOSURE_ACCEPTED_SHAPE = |
| defineObjectShape<RuntimeEventPermissionClosureAccepted>()(['requestId', 'reason'], []); |
| const RUNTIME_PERMISSION_DECISION_SHAPE = defineObjectShape<RuntimeEventPermissionDecision>()( |
| ['requestId', 'decision'], |
| ['rememberForTurn', 'reviewer', 'rationale', 'riskLevel', 'toolName'], |
| ); |
| const UTF8 = new TextEncoder(); |
| const RUNTIME_TOOL_DISPATCH_SHAPE = defineObjectShape<RuntimeEventToolDispatch>()( |
| [ |
| 'protocol', |
| 'operationId', |
| 'providerToolCallId', |
| 'toolName', |
| 'canonicalArgsHash', |
| 'recoveryMode', |
| ], |
| [], |
| ); |
| const RUNTIME_PROTOCOL_MARKER_SHAPE = defineObjectShape<RuntimeEventProtocolMarker>()( |
| ['toolBoundary'], |
| [], |
| ); |
| const RUNTIME_CONTINUATION_START_SHAPE = defineObjectShape<RuntimeEventContinuationStartV2>()( |
| [ |
| 'protocol', |
| 'provenance', |
| 'claimId', |
| 'boundaryDigest', |
| 'immediateSource', |
| 'replayManifestDigest', |
| 'providerProjectionVersion', |
| 'providerReplayDigest', |
| ], |
| [], |
| ); |
| const RUNTIME_CONTINUATION_SOURCE_SHAPE = defineObjectShape< |
| RuntimeEventContinuationStartV2['immediateSource'] |
| >()(['sessionId', 'invocationId', 'runId', 'turnId', 'highWater', 'prefixDigest'], []); |
| const RUNTIME_TOKEN_USAGE_SHAPE = defineObjectShape<RuntimeEventTokenUsage>()( |
| ['input', 'output'], |
| [ |
| 'cacheHitInput', |
| 'cacheMissInput', |
| 'cacheWriteInput', |
| 'cacheMissInputSource', |
| 'reasoning', |
| 'total', |
| 'rawFinishReason', |
| 'runtimeSteps', |
| 'cacheRead', |
| 'cacheCreation', |
| 'costUsd', |
| 'systemPromptHash', |
| 'contextRemaining', |
| 'prefixHash', |
| 'prefixChangeReason', |
| 'requestShapeHash', |
| 'requestShapeChangeReason', |
| 'promptSegments', |
| 'contextBudget', |
| 'providerRequestTraceId', |
| ], |
| ); |
| const RUNTIME_REFS_SHAPE = defineObjectShape<RuntimeEventRefs>()( |
| [], |
| [ |
| 'storedMessageId', |
| 'traceEventId', |
| 'toolCallId', |
| 'providerEventId', |
| 'sourceMessageDigest', |
| 'providerRequestTraceId', |
| 'artifactId', |
| 'operationId', |
| 'parentToolCallId', |
| 'parentOperationId', |
| 'stepId', |
| 'sourceInvocationId', |
| 'sourceRunId', |
| 'sourceTurnId', |
| 'sourceRuntimeEventHighWater', |
| ], |
| ); |
| |
| export function decodeRuntimeEvent(value: unknown): RuntimeEvent { |
| if ( |
| !isRecord(value) || |
| !hasExactShape(value, RUNTIME_EVENT_SHAPE) || |
| typeof value.id !== 'string' || |
| value.id.length === 0 || |
| typeof value.invocationId !== 'string' || |
| value.invocationId.length === 0 || |
| typeof value.runId !== 'string' || |
| value.runId.length === 0 || |
| typeof value.sessionId !== 'string' || |
| value.sessionId.length === 0 || |
| typeof value.turnId !== 'string' || |
| value.turnId.length === 0 || |
| !isFiniteNumber(value.ts) || |
| !isOptionalString(value.branch) || |
| typeof value.partial !== 'boolean' || |
| !isRuntimeEventRole(value.role) || |
| !isRuntimeEventAuthor(value.author) || |
| !isOptionalMember(value.origin, RUNTIME_EVENT_ORIGINS) || |
| !isOptionalMember(value.modelVisibility, RUNTIME_EVENT_MODEL_VISIBILITIES) || |
| (value.status !== undefined && !isRuntimeEventStatus(value.status)) || |
| (value.content !== undefined && !isRuntimeEventContent(value.content)) || |
| (value.actions !== undefined && !isRuntimeEventActions(value.actions)) || |
| (value.refs !== undefined && !isRuntimeEventRefs(value.refs)) |
| ) { |
| throw new Error('Invalid RuntimeEvent schema'); |
| } |
| if (isRecord(value.content) && value.content.kind === 'text') { |
| return { |
| ...value, |
| content: { |
| kind: 'text', |
| ...normalizeMessageContent(value.content as unknown as MessageContent), |
| ...(value.content.origin !== undefined |
| ? { origin: value.content.origin as TurnOrigin } |
| : {}), |
| ...(value.content.steering === true ? { steering: true as const } : {}), |
| }, |
| } as unknown as RuntimeEvent; |
| } |
| return value as unknown as RuntimeEvent; |
| } |
| |
| export function decodePersistedRuntimeEvent(value: unknown): RuntimeEvent { |
| return decodeRuntimeEvent(normalizeLegacyPermissionRequest(value)); |
| } |
| |
| function normalizeLegacyPermissionRequest(value: unknown): unknown { |
| if (!isRecord(value) || !isRecord(value.actions)) return value; |
| const request = value.actions.permissionRequest; |
| if ( |
| !isRecord(request) || |
| Object.hasOwn(request, 'kind') || |
| Object.hasOwn(request, 'rememberForTurnAllowed') |
| ) { |
| return value; |
| } |
| const normalized = { |
| ...request, |
| kind: 'tool_permission', |
| rememberForTurnAllowed: false, |
| }; |
| if (!isPermissionRequestPayload(normalized)) return value; |
| return { |
| ...value, |
| actions: { |
| ...value.actions, |
| permissionRequest: normalized, |
| }, |
| }; |
| } |
| |
| function isRuntimeEventContent(value: unknown): value is RuntimeEventContent { |
| if (!isRecord(value)) return false; |
| switch (value.kind) { |
| case 'text': |
| if ( |
| !hasExactShape(value, TEXT_CONTENT_SHAPE) || |
| (value.origin !== undefined && !isTurnOrigin(value.origin)) || |
| (value.steering !== undefined && value.steering !== true) || |
| (value.providerOptions !== undefined && !isRecord(value.providerOptions)) |
| ) { |
| return false; |
| } |
| return isMessageContent({ |
| text: value.text, |
| ...(value.displayText !== undefined ? { displayText: value.displayText } : {}), |
| ...(value.attachments !== undefined ? { attachments: value.attachments } : {}), |
| ...(value.quotes !== undefined ? { quotes: value.quotes } : {}), |
| ...(value.inlineReferences !== undefined |
| ? { inlineReferences: value.inlineReferences } |
| : {}), |
| }); |
| case 'thinking': |
| return ( |
| hasExactShape(value, THINKING_CONTENT_SHAPE) && |
| typeof value.text === 'string' && |
| isOptionalString(value.signature) && |
| (value.providerOptions === undefined || isRecord(value.providerOptions)) |
| ); |
| case 'function_call': |
| return ( |
| hasExactShape(value, FUNCTION_CALL_CONTENT_SHAPE) && |
| typeof value.id === 'string' && |
| typeof value.name === 'string' && |
| Object.hasOwn(value, 'args') && |
| (value.providerOptions === undefined || isRecord(value.providerOptions)) && |
| (value.providerExecuted === undefined || typeof value.providerExecuted === 'boolean') |
| ); |
| case 'function_response': |
| return ( |
| hasExactShape(value, FUNCTION_RESPONSE_CONTENT_SHAPE) && |
| typeof value.id === 'string' && |
| typeof value.name === 'string' && |
| Object.hasOwn(value, 'result') && |
| (value.isError === undefined || typeof value.isError === 'boolean') && |
| (value.providerExecuted === undefined || typeof value.providerExecuted === 'boolean') |
| ); |
| case 'error': |
| return ( |
| hasExactShape(value, ERROR_CONTENT_SHAPE) && |
| isOptionalString(value.code) && |
| isOptionalString(value.reason) && |
| typeof value.message === 'string' && |
| (value.details === undefined || isStringArray(value.details) || isRecord(value.details)) |
| ); |
| default: |
| return false; |
| } |
| } |
| |
| function isTurnOrigin(value: unknown): value is TurnOrigin { |
| if (!isRecord(value)) return false; |
| if (value.kind === 'automation') { |
| return Object.keys(value).length === 2 && typeof value.automationId === 'string'; |
| } |
| if (value.kind === 'goal') { |
| return Object.keys(value).length === 2 && typeof value.goalId === 'string'; |
| } |
| return ( |
| value.kind === 'agent_graph' && |
| Object.keys(value).length === 4 && |
| typeof value.graphId === 'string' && |
| typeof value.wakeId === 'string' && |
| typeof value.attemptId === 'string' |
| ); |
| } |
| |
| function isRuntimeEventActions(value: unknown): value is RuntimeEventActions { |
| if (!isRecord(value) || !hasExactShape(value, RUNTIME_ACTIONS_SHAPE)) return false; |
| if ( |
| value.permissionClosureAccepted !== undefined && |
| Object.keys(value).some((key) => key !== 'permissionClosureAccepted') |
| ) { |
| return false; |
| } |
| return ( |
| (value.stateDelta === undefined || isRecord(value.stateDelta)) && |
| (value.artifactDelta === undefined || |
| (isRecord(value.artifactDelta) && |
| Object.values(value.artifactDelta).every( |
| (item) => typeof item === 'string' || typeof item === 'boolean' || isFiniteNumber(item), |
| ))) && |
| (value.permissionRequest === undefined || |
| isPermissionRequestPayload(value.permissionRequest)) && |
| (value.permissionDecision === undefined || |
| isRuntimeEventPermissionDecision(value.permissionDecision)) && |
| (value.permissionAnswerAccepted === undefined || |
| isRuntimeEventAnswerAcceptedIdentity(value.permissionAnswerAccepted)) && |
| (value.permissionClosureAccepted === undefined || |
| isRuntimeEventPermissionClosureAccepted(value.permissionClosureAccepted)) && |
| (value.userQuestionRequest === undefined || isUserQuestionRequest(value.userQuestionRequest)) && |
| (value.userQuestionAnswerAccepted === undefined || |
| isRuntimeEventAnswerAcceptedIdentity(value.userQuestionAnswerAccepted)) && |
| isOptionalString(value.transferToAgent) && |
| (value.endInvocation === undefined || typeof value.endInvocation === 'boolean') && |
| (value.tokenUsage === undefined || isRuntimeTokenUsage(value.tokenUsage)) && |
| (value.toolDispatch === undefined || isRuntimeToolDispatch(value.toolDispatch)) && |
| (value.toolRecovery === undefined || isToolRecoveryFactEnvelope(value.toolRecovery)) && |
| (value.runtimeProtocol === undefined || isRuntimeProtocolMarker(value.runtimeProtocol)) && |
| (value.continuationStart === undefined || |
| isRuntimeContinuationStart(value.continuationStart)) && |
| (value.workspaceFact === undefined || isRuntimeEventWorkspaceFactEnvelope(value.workspaceFact)) |
| ); |
| } |
| |
| function isRuntimeEventPermissionDecision(value: unknown): value is RuntimeEventPermissionDecision { |
| return ( |
| isRecord(value) && |
| hasExactShape(value, RUNTIME_PERMISSION_DECISION_SHAPE) && |
| typeof value.requestId === 'string' && |
| isPermissionDecisionFields(value) && |
| (value.toolName === undefined || |
| (typeof value.toolName === 'string' && |
| value.toolName.length > 0 && |
| UTF8.encode(value.toolName).byteLength <= INTERACTION_TOOL_NAME_MAX_BYTES)) |
| ); |
| } |
| |
| function isRuntimeEventAnswerAcceptedIdentity( |
| value: unknown, |
| ): value is RuntimeEventAnswerAcceptedIdentity { |
| return ( |
| isRecord(value) && |
| hasExactShape(value, ANSWER_ACCEPTED_IDENTITY_SHAPE) && |
| typeof value.requestId === 'string' && |
| value.requestId.length > 0 && |
| UTF8.encode(value.requestId).byteLength <= INTERACTION_ID_MAX_BYTES |
| ); |
| } |
| |
| function isRuntimeEventPermissionClosureAccepted( |
| value: unknown, |
| ): value is RuntimeEventPermissionClosureAccepted { |
| return ( |
| isRecord(value) && |
| hasExactShape(value, PERMISSION_CLOSURE_ACCEPTED_SHAPE) && |
| typeof value.requestId === 'string' && |
| value.requestId.length > 0 && |
| UTF8.encode(value.requestId).byteLength <= INTERACTION_ID_MAX_BYTES && |
| value.reason === 'timed_out' |
| ); |
| } |
| |
| function isRuntimeToolDispatch(value: unknown): value is RuntimeEventToolDispatch { |
| return ( |
| isRecord(value) && |
| hasExactShape(value, RUNTIME_TOOL_DISPATCH_SHAPE) && |
| value.protocol === TOOL_BOUNDARY_PROTOCOL_V1 && |
| typeof value.operationId === 'string' && |
| typeof value.providerToolCallId === 'string' && |
| typeof value.toolName === 'string' && |
| typeof value.canonicalArgsHash === 'string' && |
| (value.recoveryMode === 'replay_safe' || |
| value.recoveryMode === 'idempotent' || |
| value.recoveryMode === 'reconcile' || |
| value.recoveryMode === 'reattach' || |
| value.recoveryMode === 'outcome_unknown' || |
| value.recoveryMode === 'never_auto_retry') |
| ); |
| } |
| |
| function isRuntimeProtocolMarker(value: unknown): value is RuntimeEventProtocolMarker { |
| return ( |
| isRecord(value) && |
| hasExactShape(value, RUNTIME_PROTOCOL_MARKER_SHAPE) && |
| value.toolBoundary === TOOL_BOUNDARY_PROTOCOL_V1 |
| ); |
| } |
| |
| function isRuntimeContinuationStart(value: unknown): value is RuntimeEventContinuationStartV2 { |
| return ( |
| isRecord(value) && |
| hasExactShape(value, RUNTIME_CONTINUATION_START_SHAPE) && |
| value.protocol === 'continuation_start_v2' && |
| (value.provenance === 'runtime_admission' || value.provenance === 'claim_repair') && |
| isNonEmptyString(value.claimId) && |
| isSha256Digest(value.boundaryDigest) && |
| isRecord(value.immediateSource) && |
| hasExactShape(value.immediateSource, RUNTIME_CONTINUATION_SOURCE_SHAPE) && |
| isNonEmptyString(value.immediateSource.sessionId) && |
| isNonEmptyString(value.immediateSource.invocationId) && |
| isNonEmptyString(value.immediateSource.runId) && |
| isNonEmptyString(value.immediateSource.turnId) && |
| Number.isSafeInteger(value.immediateSource.highWater) && |
| (value.immediateSource.highWater as number) > 0 && |
| isSha256Digest(value.immediateSource.prefixDigest) && |
| isSha256Digest(value.replayManifestDigest) && |
| value.providerProjectionVersion === 1 && |
| isSha256Digest(value.providerReplayDigest) |
| ); |
| } |
| |
| function isNonEmptyString(value: unknown): value is string { |
| return typeof value === 'string' && value.length > 0; |
| } |
| |
| function isSha256Digest(value: unknown): value is `sha256:${string}` { |
| return typeof value === 'string' && /^sha256:[0-9a-f]{64}$/.test(value); |
| } |
| |
| function isRuntimeTokenUsage(value: unknown): value is RuntimeEventTokenUsage { |
| return ( |
| isRecord(value) && hasExactShape(value, RUNTIME_TOKEN_USAGE_SHAPE) && isTokenUsageFields(value) |
| ); |
| } |
| |
| function isRuntimeEventRefs(value: unknown): value is RuntimeEventRefs { |
| return ( |
| isRecord(value) && |
| hasExactShape(value, RUNTIME_REFS_SHAPE) && |
| [ |
| value.storedMessageId, |
| value.traceEventId, |
| value.toolCallId, |
| value.providerEventId, |
| value.providerRequestTraceId, |
| value.artifactId, |
| value.operationId, |
| value.parentToolCallId, |
| value.parentOperationId, |
| value.stepId, |
| value.sourceInvocationId, |
| value.sourceRunId, |
| value.sourceTurnId, |
| ].every(isOptionalString) && |
| (value.sourceMessageDigest === undefined || isSha256Digest(value.sourceMessageDigest)) && |
| (value.sourceRuntimeEventHighWater === undefined || |
| (typeof value.sourceRuntimeEventHighWater === 'number' && |
| Number.isSafeInteger(value.sourceRuntimeEventHighWater) && |
| value.sourceRuntimeEventHighWater >= 0)) |
| ); |
| } |
| |
| // ============================================================================ |
| // Pure helpers |
| // ============================================================================ |
| |
| /** |
| * True if the event marks the end of its invocation — either by asserting |
| * a terminal `status` or by carrying `actions.endInvocation === true`. |
| * A single terminal event SHOULD carry exactly one of these signals. |
| */ |
| export function isTerminalRuntimeEvent(event: RuntimeEvent): boolean { |
| if (event.status !== undefined && isTerminalRuntimeEventStatus(event.status)) return true; |
| return event.actions?.endInvocation === true; |
| } |
| |
| /** True for transient streaming/progress chunks that a later event supersedes. */ |
| export function isPartialRuntimeEvent(event: RuntimeEvent): boolean { |
| return event.partial === true; |
| } |
| |
| /** |
| * True if the event carries content whose kind is eligible for model |
| * history projection: text, thinking, function_call, or function_response. |
| * Error-only content and pure action/refs events are NOT model-visible. |
| * |
| * This is a content-kind check only. Callers still apply `partial` |
| * filtering (partial chunks are never replayed into the next model call). |
| */ |
| export function runtimeEventHasModelVisibleContent(event: RuntimeEvent): boolean { |
| if (event.modelVisibility === 'hidden') return false; |
| const content = event.content; |
| if (!content) return false; |
| switch (content.kind) { |
| case 'text': |
| return content.text.length > 0; |
| case 'thinking': |
| case 'function_call': |
| case 'function_response': |
| return true; |
| case 'error': |
| return false; |
| } |
| } |
| |
| let __runtimeEventSeq = 0; |
| |
| /** |
| * Best-effort unique id for runtime events. Monotonic within a process so |
| * two ids never collide even when generated in the same millisecond. |
| * |
| * Runtime/runner layers MAY replace this with a stronger uuid source; it |
| * exists here only so early adopters and tests have a default. Tests that |
| * need deterministic ids SHOULD pass literal strings rather than rely on |
| * this helper's exact output. |
| */ |
| export function createRuntimeEventId(prefix = 'rt-event'): string { |
| __runtimeEventSeq += 1; |
| return `${prefix}_${Date.now().toString(36)}_${__runtimeEventSeq.toString(36)}`; |
| } |