| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import { isWorkHubActionReceipt, type WorkHubActionReceipt } from './workhub-action-result.js'; |
| |
| import { |
| MODEL_FAILURE_MESSAGE_MAX_BYTES, |
| isModelRetryDecision, |
| type ModelRetryDecision, |
| } from './model-failure.js'; |
| |
| import { |
| decodeMessageContent, |
| TOOL_ACTIVITY_KINDS, |
| type MessageContent, |
| type AttachmentRef, |
| type ToolActivityKind, |
| type ToolResultContent, |
| } from './events.js'; |
| import { |
| isPermissionMode, |
| isToolCategory, |
| type PermissionMode, |
| type PolicyDecision, |
| type ToolCategory, |
| } from './permission.js'; |
| import type { CollaborationMode } from './collaboration.js'; |
| import type { OrchestrationMode } from './orchestration.js'; |
| import type { ToolMode } from './tool-mode.js'; |
| import { |
| defineObjectShape, |
| hasExactShape, |
| isFiniteNumber, |
| isOptionalString, |
| isRecord, |
| pickShape, |
| } from './record-schema.js'; |
| import { isPermissionDecisionFields } from './interaction-record-schema.js'; |
| import { isTokenUsageFields, type TokenUsageFields } from './usage-record-schema.js'; |
| import { |
| decodeCanonicalToolResultContent, |
| decodePersistedToolResultContent, |
| } from './tool-result-record-schema.js'; |
| import { markPersisted, type PersistedValue } from './persisted-value.js'; |
| import type { SubagentWorkspaceBinding } from './subagent-workspace.js'; |
| import { decodeTurnOrigin, type TurnOrigin } from './turn-origin.js'; |
| |
| export { DEEP_RESEARCH_SESSION_LABEL, isDeepResearchSession } from './deep-research.js'; |
| |
| /** Runtime execution states. Archive visibility is represented by `isArchived`. */ |
| export const SESSION_STATUSES = [ |
| 'active', |
| 'running', |
| 'waiting_for_user', |
| 'blocked', |
| 'aborted', |
| ] as const; |
| |
| export type SessionStatus = (typeof SESSION_STATUSES)[number]; |
| |
| export const SESSION_BLOCKED_REASONS = [ |
| 'NO_REAL_CONNECTION', |
| 'auth', |
| 'permission_required', |
| 'tool_failed', |
| 'unknown', |
| ] as const; |
| |
| export type SessionBlockedReason = (typeof SESSION_BLOCKED_REASONS)[number]; |
| |
| /** Reserved durable role for the one WorkHub coordination conversation owned by a Runtime Host. */ |
| export const WORKHUB_COORDINATION_SESSION_ROLE = 'workhub_coordination' as const; |
| export const WORKHUB_COORDINATION_SESSION_ID = 'maka_workhub_coordination' as const; |
| export type SessionRole = typeof WORKHUB_COORDINATION_SESSION_ROLE; |
| |
| /** Whether an identifier targets the reserved WorkHub Coordination Session. */ |
| export function isWorkHubCoordinationSessionId(sessionId: string): boolean { |
| return sessionId === WORKHUB_COORDINATION_SESSION_ID; |
| } |
| |
| export const TURN_STATUSES = ['running', 'completed', 'aborted', 'failed'] as const; |
| |
| export type TurnStatus = (typeof TURN_STATUSES)[number]; |
| |
| export const SUBAGENT_SESSION_LIFECYCLES = ['foreground'] as const; |
| |
| export type SubagentSessionLifecycle = (typeof SUBAGENT_SESSION_LIFECYCLES)[number]; |
| export const SUBAGENT_SESSION_RUNTIME_SCHEMA_VERSION = 1 as const; |
| export const SUBAGENT_SESSION_SPAWN_SCHEMA_VERSION = 1 as const; |
| |
| /** |
| * Durable control-plane lineage for a subagent session. |
| * |
| * The relation lives only on the child. Parents do not persist a reciprocal |
| * child-id array; reverse lookup is a read-model concern. Cross-session |
| * provenance deliberately stays out of AgentRun.parentRunId so runs inside the |
| * child session can retain normal session-inline history semantics. |
| */ |
| export interface SubagentSessionParent { |
| kind: 'subagent'; |
| parentSessionId: string; |
| spawnedBy: { |
| parentRunId: string; |
| parentTurnId: string; |
| toolCallId: string; |
| }; |
| swarm?: { |
| swarmId: string; |
| itemId: string; |
| }; |
| graph?: { |
| graphId: string; |
| workId: string; |
| operatorId: string; |
| }; |
| lifecycle: SubagentSessionLifecycle; |
| } |
| |
| /** |
| * Durable execution snapshot for a linked subagent session. |
| * |
| * The snapshot prevents a reopened child session from silently inheriting a |
| * wider tool surface from a later parent/default configuration. The concrete |
| * SessionHeader continues to own backend/model/cwd while ExecutionBoundary is |
| * the authoritative local execution authority. |
| */ |
| export interface SubagentSessionRuntime { |
| schemaVersion: typeof SUBAGENT_SESSION_RUNTIME_SCHEMA_VERSION; |
| definitionVersion: number; |
| agentId: string; |
| agentName: string; |
| profile: string; |
| /** User-approved model route selected at spawn time. Absent for legacy profile spawns. */ |
| presetId?: string; |
| systemPrompt: string; |
| toolNames: string[]; |
| categoryPolicy: Partial<Record<ToolCategory, PolicyDecision>>; |
| } |
| |
| /** |
| * Durable identity of the initial child invocation. |
| * |
| * The SQLite metadata control plane derives its unique spawn key from |
| * subagentParent. This block binds that key to the exact requested work and |
| * preallocates the first run identities so a retry can reuse or recover them. |
| */ |
| export interface SubagentSessionSpawn { |
| schemaVersion: typeof SUBAGENT_SESSION_SPAWN_SCHEMA_VERSION; |
| requestFingerprint: string; |
| initialTurnId: string; |
| initialRunId: string; |
| } |
| |
| /** |
| * Internal publication state for a Host-owned cross-Session conversation copy. |
| * |
| * Preparing copies are not product Sessions yet. The Host publishes them only |
| * after Messages, Runtime Events, Artifacts, and Task Ledger state are durable. |
| */ |
| export interface SessionConversationCopy { |
| kind: 'branch' | 'revision'; |
| sourceSessionId: string; |
| /** |
| * Settled turn the copy branches through. Absent marks an empty copy: a side |
| * conversation opened before the source has any completed turn copies NO |
| * source messages, inheriting only the source's model / cwd / permission and |
| * recording provenance (`parentSessionId`) without a `branchOfTurnId`. Only |
| * valid together with `intent: 'side_conversation'`. |
| */ |
| sourceTurnId?: string; |
| requestFingerprint: `sha256:${string}`; |
| state: 'preparing' | 'committed'; |
| intent?: 'side_conversation'; |
| } |
| |
| export type SubagentSessionRuntimeSummary = Omit< |
| SubagentSessionRuntime, |
| 'systemPrompt' | 'categoryPolicy' |
| >; |
| |
| /** |
| * Client-facing child-session relation when the durable spawn record remains |
| * inside the Runtime Host authority boundary. |
| */ |
| export interface SessionSubagentProjection { |
| parentSessionId: string; |
| agentId?: string; |
| agentName?: string; |
| profile?: string; |
| } |
| |
| export function isSessionStatus(value: unknown): value is SessionStatus { |
| return typeof value === 'string' && (SESSION_STATUSES as readonly string[]).includes(value); |
| } |
| |
| export function isSessionBlockedReason(value: unknown): value is SessionBlockedReason { |
| return ( |
| typeof value === 'string' && (SESSION_BLOCKED_REASONS as readonly string[]).includes(value) |
| ); |
| } |
| |
| export function isTurnStatus(value: unknown): value is TurnStatus { |
| return typeof value === 'string' && (TURN_STATUSES as readonly string[]).includes(value); |
| } |
| |
| // ============================================================================ |
| // Header (JSONL line 1) |
| // ============================================================================ |
| |
| export const SESSION_TOOL_PROFILES = [ |
| 'headless-coding-v1', |
| 'workhub-coordination-v1', |
| 'workhub-coordination-v2', |
| ] as const; |
| export type SessionToolProfile = (typeof SESSION_TOOL_PROFILES)[number]; |
| |
| export function isSessionToolProfile(value: unknown): value is SessionToolProfile { |
| return typeof value === 'string' && (SESSION_TOOL_PROFILES as readonly string[]).includes(value); |
| } |
| |
| export interface SessionExternalOrigin { |
| readonly adapterId: string; |
| readonly sourceSessionId: string; |
| } |
| |
| export interface SessionHeader { |
| /** Frozen at creation; absent on older tasks means direct tool calling. */ |
| toolMode?: ToolMode; |
| // Identity |
| id: string; |
| /** Absent means an ordinary Session; special roles remain on the same Session substrate. */ |
| readonly role?: SessionRole; |
| workspaceRoot: string; |
| cwd: string; |
| /** Stable project-catalog association. Null means the user explicitly chose no project. */ |
| projectId?: string | null; |
| |
| // Lifecycle timestamps |
| createdAt: number; |
| lastMessageAt?: number; |
| |
| // User metadata |
| name: string; |
| titleIsManual: boolean; |
| isFlagged: boolean; |
| labels: string[]; |
| |
| isArchived: boolean; |
| status: SessionStatus; |
| blockedReason?: SessionBlockedReason; |
| statusUpdatedAt?: number; |
| /** Ordinary branch lineage. Subagent lineage uses subagentParent instead. */ |
| parentSessionId?: string; |
| branchOfTurnId?: string; |
| /** Immutable control-plane relation for a linked child-agent session. */ |
| subagentParent?: SubagentSessionParent; |
| /** Immutable runtime/profile snapshot for child-session execution. */ |
| subagentRuntime?: SubagentSessionRuntime; |
| /** Immutable idempotency and initial-run identity for child creation. */ |
| subagentSpawn?: SubagentSessionSpawn; |
| /** Immutable host-managed filesystem isolation for this child Session. */ |
| subagentWorkspace?: SubagentWorkspaceBinding; |
| /** Immutable Host publication identity for a cross-Session conversation copy. */ |
| conversationCopy?: SessionConversationCopy; |
| /** Immutable identity of the external Session imported into this Session. */ |
| readonly externalOrigin?: SessionExternalOrigin; |
| /** Stable root id for an edit-and-resend version family. */ |
| revisionRootSessionId?: string; |
| /** Immediate previous version in the same conversation slot. */ |
| revisionParentSessionId?: string; |
| /** User turn replaced when this revision was created. */ |
| revisionOfTurnId?: string; |
| /** Stable display order inside the revision family; root is implicitly 1. */ |
| revisionIndex?: number; |
| /** Preparing versions are hidden after restart until their first run starts. */ |
| revisionState?: 'preparing' | 'committed'; |
| |
| // Unread tracking |
| lastReadMessageId?: string; |
| hasUnread: boolean; |
| |
| // Backend / model config |
| backend: PersistedBackendKind; |
| /** Immutable Connection entity identity. Optional only on legacy Session records. */ |
| llmConnectionId?: string; |
| llmConnectionSlug: string; |
| /** True once the Session's first UserMessage is durable. One-way. */ |
| connectionLocked: boolean; |
| /** Sticky session default model id, captured when the session is created. */ |
| model: string; |
| /** Immutable versioned prompt/tool contract for non-product execution surfaces. */ |
| toolProfile?: SessionToolProfile; |
| /** Per-model reasoning-depth variant; `undefined` = model default. Cleared on model switch. */ |
| thinkingLevel?: import('./model-thinking.js').ThinkingLevel; |
| permissionMode: PermissionMode; |
| /** Defaults to `agent` when absent on legacy session records. */ |
| collaborationMode?: CollaborationMode; |
| /** Defaults to `default` when absent on legacy session records. */ |
| orchestrationMode?: OrchestrationMode; |
| |
| /** Zero while an imported transcript is staging; one after materialization. */ |
| transcriptLedgerVersion?: 0 | 1; |
| |
| /** Forward-compatible schema versioning. V0.1 only writes 1. */ |
| schemaVersion: 1; |
| } |
| |
| export type SessionHeaderPatch = Partial<Omit<SessionHeader, 'isArchived' | 'role'>> & { |
| readonly isArchived?: never; |
| readonly role?: never; |
| }; |
| |
| export function isWorkHubCoordinationSession(session: Pick<SessionHeader, 'role'>): boolean { |
| return session.role === WORKHUB_COORDINATION_SESSION_ROLE; |
| } |
| |
| /** Whether durable state claims either half of the reserved Coordination identity/role pair. */ |
| export function isWorkHubCoordinationSessionTarget( |
| session: Pick<SessionHeader, 'id' | 'role'>, |
| ): boolean { |
| return isWorkHubCoordinationSessionId(session.id) || isWorkHubCoordinationSession(session); |
| } |
| |
| /** |
| * The backend a live build may select. |
| * |
| * `'fake'` was retired with the in-process FakeBackend (#3211): nothing in a |
| * shipped build may choose it, so it is not a member here. Values read back |
| * from durable state use {@link PersistedBackendKind} instead. |
| */ |
| export type BackendKind = 'ai-sdk'; |
| |
| /** |
| * The backend value a persisted record may carry. |
| * |
| * Sessions, runs and Automations written by builds that still shipped |
| * FakeBackend hold `'fake'` forever. Decode keeps accepting it so those rows |
| * stay readable — rewriting them to `'ai-sdk'` would only make an unrunnable |
| * task look runnable, since their `llmConnectionSlug` still points at nothing. |
| * Activation refuses them with the product's `fake_backend` reason (see the |
| * refusal registered in `execution-composition.ts`). |
| * |
| * Never write this type: writers take {@link BackendKind}. |
| */ |
| export type PersistedBackendKind = BackendKind | 'fake'; |
| |
| export interface SessionSummary { |
| id: string; |
| cwd?: string; |
| projectId?: string | null; |
| name: string; |
| isFlagged: boolean; |
| isArchived: boolean; |
| labels: string[]; |
| hasUnread: boolean; |
| lastMessageAt?: number; |
| lastMessagePreview?: string; |
| status: SessionStatus; |
| blockedReason?: SessionBlockedReason; |
| statusUpdatedAt?: number; |
| /** |
| * The turns the runtime is running for this session right now. An explicit |
| * empty array means the runtime authoritatively knows there are none; omission |
| * means the summary source does not know the live state. |
| * |
| * Projected from the live runs, never persisted: "a run is in flight" is a |
| * fact about the running process, so it must read false again after a crash. |
| * `status` cannot serve that purpose — it is written to storage, so a crash |
| * between a turn's end and its status write leaves `running` behind forever, |
| * and it carries no turn identity, reading the same before a turn starts and |
| * after it ends. |
| * |
| * A set rather than one turn because a session can carry concurrent runs: a |
| * client asking "is anything OTHER than the turn I sent still running" cannot |
| * answer that from an arbitrary one of them. |
| * |
| * Only populated where the runtime is in a position to know: session LISTS |
| * come from the authority holding the runs and include the field even when it |
| * is empty. A summary returned by a mutation (rename, model change) describes |
| * the header alone and omits it. |
| */ |
| runningTurnIds?: string[]; |
| parentSessionId?: string; |
| branchOfTurnId?: string; |
| subagent?: SessionSubagentProjection; |
| subagentParent?: SubagentSessionParent; |
| subagentRuntime?: SubagentSessionRuntimeSummary; |
| subagentWorkspace?: SubagentWorkspaceBinding; |
| revisionRootSessionId?: string; |
| revisionParentSessionId?: string; |
| revisionOfTurnId?: string; |
| revisionIndex?: number; |
| revisionState?: 'preparing' | 'committed'; |
| backend: PersistedBackendKind; |
| /** Immutable Connection entity identity. Optional only on legacy summaries. */ |
| llmConnectionId?: string; |
| llmConnectionSlug: string; |
| /** |
| * True once the session has user messages — its connection/model is |
| * sticky and compatibility projections never select a replacement target. |
| * Surfaced so onboarding can project existing-session health (#1038). |
| */ |
| connectionLocked: boolean; |
| /** Sticky session default model id for renderer/header display. */ |
| model: string; |
| /** Per-model reasoning-depth variant; `undefined` = model default. Cleared on model switch. */ |
| thinkingLevel?: import('./model-thinking.js').ThinkingLevel; |
| permissionMode: PermissionMode; |
| /** Defaults to `agent` when absent on legacy summaries. */ |
| collaborationMode?: CollaborationMode; |
| /** Defaults to `default` when absent on legacy summaries. */ |
| orchestrationMode?: OrchestrationMode; |
| } |
| |
| /** A complete Session catalog row. Its order key is authoritative and never synthesized by clients. */ |
| export interface SessionCatalogSummary extends SessionSummary { |
| activityAt: number; |
| } |
| |
| export function sessionRevisionFamilyId( |
| session: Pick<SessionSummary, 'id' | 'revisionRootSessionId'>, |
| ): string { |
| return session.revisionRootSessionId ?? session.id; |
| } |
| |
| /** |
| * Host-facing projection of linked subagent Sessions. |
| * |
| * The flat Session list remains the storage/read authority. Hosts use this |
| * projection to nest a linked child beneath its durable parent without |
| * confusing ordinary branch lineage with subagent ownership. Missing-parent |
| * and cyclic relations fail open into roots so an inspectable child can never |
| * disappear from the product surface. |
| */ |
| export interface LinkedSessionTree { |
| roots: SessionSummary[]; |
| childrenByParentId: ReadonlyMap<string, readonly SessionSummary[]>; |
| } |
| |
| export interface LinkedSessionTreeProjectionOptions { |
| /** |
| * Read-model aliases from durable physical parent ids to visible logical |
| * Session ids. Revision projection uses this to keep a child attached when |
| * its spawning parent revision is no longer the selected representative. |
| */ |
| parentSessionIdAliases?: ReadonlyMap<string, string>; |
| } |
| |
| const SUBAGENT_SESSION_PARENT_SHAPE = defineObjectShape<SubagentSessionParent>()( |
| ['kind', 'parentSessionId', 'spawnedBy', 'lifecycle'], |
| ['swarm', 'graph'], |
| ); |
| const SUBAGENT_SESSION_SPAWN_SHAPE = defineObjectShape<SubagentSessionParent['spawnedBy']>()( |
| ['parentRunId', 'parentTurnId', 'toolCallId'], |
| [], |
| ); |
| const SUBAGENT_SESSION_SWARM_SHAPE = defineObjectShape< |
| NonNullable<SubagentSessionParent['swarm']> |
| >()(['swarmId', 'itemId'], []); |
| const SUBAGENT_SESSION_GRAPH_SHAPE = defineObjectShape< |
| NonNullable<SubagentSessionParent['graph']> |
| >()(['graphId', 'workId', 'operatorId'], []); |
| const SUBAGENT_SESSION_RUNTIME_SHAPE = defineObjectShape<SubagentSessionRuntime>()( |
| [ |
| 'schemaVersion', |
| 'definitionVersion', |
| 'agentId', |
| 'agentName', |
| 'profile', |
| 'systemPrompt', |
| 'toolNames', |
| 'categoryPolicy', |
| ], |
| ['presetId'], |
| ['permissionCeiling'], |
| ); |
| const SUBAGENT_SESSION_SPAWN_IDENTITY_SHAPE = defineObjectShape<SubagentSessionSpawn>()( |
| ['schemaVersion', 'requestFingerprint', 'initialTurnId', 'initialRunId'], |
| [], |
| ); |
| const SESSION_CONVERSATION_COPY_SHAPE = defineObjectShape<SessionConversationCopy>()( |
| ['kind', 'sourceSessionId', 'requestFingerprint', 'state'], |
| ['sourceTurnId', 'intent'], |
| ); |
| const SESSION_LINEAGE_ID_MAX_CHARS = 512; |
| const SESSION_LINEAGE_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/; |
| const SUBAGENT_RUNTIME_NAME_MAX_CHARS = 512; |
| const SUBAGENT_RUNTIME_SYSTEM_PROMPT_MAX_CHARS = 100_000; |
| const SUBAGENT_RUNTIME_TOOL_LIMIT = 128; |
| const SUBAGENT_REQUEST_FINGERPRINT_PATTERN = /^[a-f0-9]{64}$/; |
| |
| /** Strict decoder guard for the persisted child-session relation. */ |
| export function isSubagentSessionParent(value: unknown): value is SubagentSessionParent { |
| if ( |
| !isRecord(value) || |
| !hasExactShape(value, SUBAGENT_SESSION_PARENT_SHAPE) || |
| value.kind !== 'subagent' || |
| !isSessionLineageId(value.parentSessionId) || |
| value.lifecycle !== 'foreground' || |
| !isRecord(value.spawnedBy) || |
| !hasExactShape(value.spawnedBy, SUBAGENT_SESSION_SPAWN_SHAPE) || |
| !isSessionLineageId(value.spawnedBy.parentRunId) || |
| !isSessionLineageId(value.spawnedBy.parentTurnId) || |
| !isSessionLineageId(value.spawnedBy.toolCallId) |
| ) { |
| return false; |
| } |
| const swarmValid = |
| value.swarm === undefined || |
| (isRecord(value.swarm) && |
| hasExactShape(value.swarm, SUBAGENT_SESSION_SWARM_SHAPE) && |
| isSessionLineageId(value.swarm.swarmId) && |
| isSessionLineageId(value.swarm.itemId)); |
| const graphValid = |
| value.graph === undefined || |
| (isRecord(value.graph) && |
| hasExactShape(value.graph, SUBAGENT_SESSION_GRAPH_SHAPE) && |
| isSessionLineageId(value.graph.graphId) && |
| isSessionLineageId(value.graph.workId) && |
| isSessionLineageId(value.graph.operatorId)); |
| return swarmValid && graphValid && !(value.swarm && value.graph); |
| } |
| |
| /** Strict decoder guard for the persisted child execution snapshot. */ |
| export function isSubagentSessionRuntime(value: unknown): value is SubagentSessionRuntime { |
| if ( |
| !isRecord(value) || |
| !hasExactShape(value, SUBAGENT_SESSION_RUNTIME_SHAPE) || |
| value.schemaVersion !== SUBAGENT_SESSION_RUNTIME_SCHEMA_VERSION || |
| !Number.isSafeInteger(value.definitionVersion) || |
| (value.definitionVersion as number) < 1 || |
| !isSessionLineageId(value.agentId) || |
| !isSessionLineageId(value.profile) || |
| (value.presetId !== undefined && !isSessionLineageId(value.presetId)) || |
| typeof value.agentName !== 'string' || |
| value.agentName.length === 0 || |
| value.agentName.length > SUBAGENT_RUNTIME_NAME_MAX_CHARS || |
| SESSION_LINEAGE_CONTROL_CHARACTERS.test(value.agentName) || |
| typeof value.systemPrompt !== 'string' || |
| value.systemPrompt.length === 0 || |
| value.systemPrompt.length > SUBAGENT_RUNTIME_SYSTEM_PROMPT_MAX_CHARS || |
| value.systemPrompt.includes('\u0000') || |
| !Array.isArray(value.toolNames) || |
| value.toolNames.length > SUBAGENT_RUNTIME_TOOL_LIMIT || |
| !value.toolNames.every(isSessionLineageId) || |
| new Set(value.toolNames).size !== value.toolNames.length || |
| !isSubagentCategoryPolicy(value.categoryPolicy) |
| ) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** Strict decoder guard for durable child-spawn idempotency metadata. */ |
| export function isSubagentSessionSpawn(value: unknown): value is SubagentSessionSpawn { |
| return ( |
| isRecord(value) && |
| hasExactShape(value, SUBAGENT_SESSION_SPAWN_IDENTITY_SHAPE) && |
| value.schemaVersion === SUBAGENT_SESSION_SPAWN_SCHEMA_VERSION && |
| typeof value.requestFingerprint === 'string' && |
| SUBAGENT_REQUEST_FINGERPRINT_PATTERN.test(value.requestFingerprint) && |
| isSessionLineageId(value.initialTurnId) && |
| isSessionLineageId(value.initialRunId) |
| ); |
| } |
| |
| /** Strict decoder guard for Host-owned conversation-copy publication state. */ |
| export function isSessionConversationCopy(value: unknown): value is SessionConversationCopy { |
| return ( |
| isRecord(value) && |
| hasExactShape(value, SESSION_CONVERSATION_COPY_SHAPE) && |
| (value.kind === 'branch' || value.kind === 'revision') && |
| (value.intent === undefined || |
| (value.kind === 'branch' && value.intent === 'side_conversation')) && |
| isSessionLineageId(value.sourceSessionId) && |
| (value.sourceTurnId === undefined |
| ? value.kind === 'branch' && value.intent === 'side_conversation' |
| : isSessionLineageId(value.sourceTurnId)) && |
| typeof value.requestFingerprint === 'string' && |
| /^sha256:[0-9a-f]{64}$/.test(value.requestFingerprint) && |
| (value.state === 'preparing' || value.state === 'committed') |
| ); |
| } |
| |
| export function subagentSessionRuntimeSummary( |
| value: SubagentSessionRuntime, |
| ): SubagentSessionRuntimeSummary { |
| const { systemPrompt: _systemPrompt, categoryPolicy: _categoryPolicy, ...summary } = value; |
| return summary; |
| } |
| |
| /** Read-model projection; input order is preserved. */ |
| export function childSessionsForParent( |
| sessions: readonly SessionSummary[], |
| parentSessionId: string, |
| ): SessionSummary[] { |
| return sessions.filter((session) => linkedSubagentParentId(session) === parentSessionId); |
| } |
| |
| /** Whether a Session is a linked child in either local or Host projection form. */ |
| export function isLinkedSubagentSession( |
| session: Pick<SessionSummary, 'subagent' | 'subagentParent'>, |
| ): boolean { |
| return linkedSubagentParentId(session) !== undefined; |
| } |
| |
| /** Immediate linked parent session id, if this session is a linked child. */ |
| export function linkedSubagentParentSessionId( |
| session: Pick<SessionSummary, 'subagent' | 'subagentParent'>, |
| ): string | undefined { |
| return linkedSubagentParentId(session); |
| } |
| |
| /** Read-model projection; input order is preserved at every tree level. */ |
| export function projectLinkedSessionTree( |
| sessions: readonly SessionSummary[], |
| options: LinkedSessionTreeProjectionOptions = {}, |
| ): LinkedSessionTree { |
| const sessionsById = new Map(sessions.map((session) => [session.id, session])); |
| const nestedParentByChildId = new Map<string, string>(); |
| const linkedParentId = (session: SessionSummary): string | undefined => { |
| const parentSessionId = linkedSubagentParentId(session); |
| if (!parentSessionId) return undefined; |
| return options.parentSessionIdAliases?.get(parentSessionId) ?? parentSessionId; |
| }; |
| |
| for (const session of sessions) { |
| const parentSessionId = linkedParentId(session); |
| if (!parentSessionId) continue; |
| if (!sessionsById.has(parentSessionId)) continue; |
| if (parentSessionId === session.id) continue; |
| if (linkedParentChainContainsCycle(session.id, sessionsById, linkedParentId)) continue; |
| nestedParentByChildId.set(session.id, parentSessionId); |
| } |
| |
| const roots: SessionSummary[] = []; |
| const mutableChildren = new Map<string, SessionSummary[]>(); |
| for (const session of sessions) { |
| const parentSessionId = nestedParentByChildId.get(session.id); |
| if (!parentSessionId) { |
| roots.push(session); |
| continue; |
| } |
| const children = mutableChildren.get(parentSessionId) ?? []; |
| children.push(session); |
| mutableChildren.set(parentSessionId, children); |
| } |
| |
| return { |
| roots, |
| childrenByParentId: mutableChildren, |
| }; |
| } |
| |
| function linkedSubagentParentId( |
| session: Pick<SessionSummary, 'subagent' | 'subagentParent'>, |
| ): string | undefined { |
| if (isSubagentSessionParent(session.subagentParent)) { |
| return session.subagentParent.parentSessionId; |
| } |
| return session.subagent?.parentSessionId; |
| } |
| |
| function linkedParentChainContainsCycle( |
| startSessionId: string, |
| sessionsById: ReadonlyMap<string, SessionSummary>, |
| linkedParentId: (session: SessionSummary) => string | undefined, |
| ): boolean { |
| const visited = new Set<string>(); |
| let sessionId: string | undefined = startSessionId; |
| while (sessionId) { |
| if (visited.has(sessionId)) return true; |
| visited.add(sessionId); |
| const session = sessionsById.get(sessionId); |
| if (!session) return false; |
| const parentSessionId = linkedParentId(session); |
| if (!parentSessionId || !sessionsById.has(parentSessionId)) return false; |
| sessionId = parentSessionId; |
| } |
| return false; |
| } |
| |
| function isSessionLineageId(value: unknown): value is string { |
| return ( |
| typeof value === 'string' && |
| value.length > 0 && |
| value.length <= SESSION_LINEAGE_ID_MAX_CHARS && |
| !SESSION_LINEAGE_CONTROL_CHARACTERS.test(value) |
| ); |
| } |
| |
| function isSubagentCategoryPolicy( |
| value: unknown, |
| ): value is Partial<Record<ToolCategory, PolicyDecision>> { |
| if (!isRecord(value)) return false; |
| return Object.entries(value).every( |
| ([category, decision]) => |
| isToolCategory(category) && |
| (decision === 'allow' || decision === 'prompt' || decision === 'block'), |
| ); |
| } |
| |
| export type SessionChangedReason = |
| | 'created' |
| | 'migrated' |
| | 'updated' |
| | 'archived' |
| | 'deleted' |
| | 'message-appended' |
| | 'pinned' |
| | 'renamed' |
| | 'mode-change' |
| | 'status-change' |
| | 'turn-status-change' |
| | 'goal-change' |
| | 'rebound'; |
| |
| export interface SessionChangedEvent { |
| reason: SessionChangedReason; |
| sessionId?: string; |
| modelId?: string; |
| /** |
| * The turn this change is ABOUT, when the change has a turn to name. |
| * |
| * Naming the turn is what makes a notification a causal answer to a specific |
| * send rather than a bare invalidation: the session fields alone carry no |
| * turn identity, and `status` reads the same before a turn starts and after |
| * it ends. |
| * |
| * Emitter obligation, and its exact edge: a change about a turn some CLIENT |
| * may be waiting on — one it submitted, so it holds a local claim until it |
| * hears back — must name that turn. Its start, its refusal to start, and its |
| * end all qualify. Changes with no single turn behind them (a rename, a |
| * catalog migration) leave it unset, as do turns no client submitted and none |
| * is waiting on — a linked child agent's own turns, for instance, which are |
| * reported by `SessionSummary.runningTurnIds` instead. A client must never |
| * read an unset change as an answer about a turn it is waiting on. |
| */ |
| turnId?: string; |
| ts: number; |
| } |
| |
| // ============================================================================ |
| // Stored messages (JSONL line 2+, append-only) |
| // ============================================================================ |
| |
| export type StoredMessage = |
| | UserMessage |
| | AssistantMessage |
| | ToolCallMessage |
| | ToolResultMessage |
| | PermissionDecisionMessage |
| | TokenUsageMessage |
| | TurnStateMessage |
| | WorkHubCoordinationMessage |
| | SystemNoteMessage; |
| |
| export interface UserMessage extends MessageContent { |
| /** Derived from the admitted WorkHub action; does not change physical Turn identity. */ |
| coordinationActionId?: string; |
| type: 'user'; |
| id: string; |
| turnId: string; |
| ts: number; |
| /** Canonical RuntimeEvent that materialized this mid-Turn steering projection. */ |
| steeringEventId?: string; |
| /** Non-user trigger source. Lets the chat mark turns the user did not |
| * hand-type. */ |
| origin?: TurnOrigin; |
| } |
| |
| /** Prefer the human-facing view of a user message when one was stored. */ |
| export function userFacingText(message: Pick<UserMessage, 'text' | 'displayText'>): string { |
| return message.displayText ?? message.text; |
| } |
| |
| /** |
| * Closed policy for system notes that are part of the user-visible transcript: |
| * exactly the notes the runtime writes. |
| */ |
| export function isUserVisibleSessionSystemNote(kind: string): boolean { |
| return isRuntimeSystemNoteKind(kind); |
| } |
| |
| export interface AssistantMessage { |
| type: 'assistant'; |
| id: string; |
| turnId: string; |
| ts: number; |
| text: string; |
| /** Provider-owned text metadata such as Responses URL citations. */ |
| providerOptions?: Record<string, unknown>; |
| thinking?: AssistantThinking; |
| /** |
| * First-observed order of visible content inside this assistant step. |
| * RuntimeEvent projection records partial text/thinking and the paired tool |
| * call before dropping partial rows, so live and persisted timelines can use |
| * the same append-only order. Absent on legacy rows, which retain the older |
| * semantic thinking → text → tools fallback. |
| */ |
| contentOrder?: AssistantStepContentKind[]; |
| /** Actual model used for this turn. */ |
| modelId: string; |
| } |
| |
| export interface AssistantThinkingPart { |
| text: string; |
| /** Anthropic signed thinking for replay. */ |
| signature?: string; |
| /** Provider-owned replay metadata that must survive missing-ledger recovery. */ |
| providerOptions?: Record<string, unknown>; |
| } |
| |
| export interface AssistantThinking extends AssistantThinkingPart { |
| /** |
| * Ordered provider reasoning items when one assistant step contains more than |
| * one independently replayable item. The aggregate text remains available on |
| * the parent for existing readers; single-item rows keep the legacy shape. |
| */ |
| parts?: AssistantThinkingPart[]; |
| } |
| |
| export type AssistantStepContentKind = 'thinking' | 'text' | 'tools'; |
| |
| export interface ToolCallMessage { |
| type: 'tool_call'; |
| /** Equals toolUseId — used to match ToolResultMessage.toolUseId. */ |
| id: string; |
| turnId: string; |
| ts: number; |
| toolName: string; |
| /** Stable semantic category for presentation; absent on legacy rows. */ |
| activityKind?: ToolActivityKind; |
| displayName?: string; |
| intent?: string; |
| args: unknown; |
| /** Provider-owned opaque call metadata retained for recovery backfill. */ |
| providerOptions?: Record<string, unknown>; |
| providerExecuted?: boolean; |
| /** |
| * Assistant step this call belongs to (equals the step's AssistantMessage |
| * id, stamped from the same source as ToolStartEvent.stepId). Optional for |
| * legacy rows written before per-step persistence. First consumer is the UI |
| * timeline (materializeTurns), which orders a step's thinking/text ahead of |
| * the tools whose stepId matches that step; the backfill path also reads it |
| * to re-pair tools with their step after a restart. |
| */ |
| stepId?: string; |
| /** Execution surface and replay policy retained for missing-ledger recovery. */ |
| origin?: 'provider' | 'code_mode'; |
| modelVisibility?: 'visible' | 'hidden'; |
| parentToolCallId?: string; |
| parentOperationId?: string; |
| } |
| |
| export interface ToolResultMessage { |
| type: 'tool_result'; |
| /** Own message id (not the tool's). */ |
| id: string; |
| turnId: string; |
| ts: number; |
| /** Matches ToolCallMessage.id. */ |
| toolUseId: string; |
| isError: boolean; |
| content: ToolResultContent; |
| providerExecuted?: boolean; |
| /** Raw provider result retained only for provider-native replay. */ |
| providerOutput?: unknown; |
| durationMs?: number; |
| /** Execution surface and replay policy retained for missing-ledger recovery. */ |
| origin?: 'provider' | 'code_mode'; |
| modelVisibility?: 'visible' | 'hidden'; |
| parentToolCallId?: string; |
| parentOperationId?: string; |
| } |
| |
| export interface PermissionDecisionMessage { |
| type: 'permission_decision'; |
| /** Equals PermissionRequestEvent.requestId for audit correlation. */ |
| id: string; |
| turnId: string; |
| ts: number; |
| toolUseId: string; |
| toolName: string; |
| decision: 'allow' | 'deny'; |
| rememberForTurn?: boolean; |
| reviewer?: import('./permission.js').ApprovalsReviewer; |
| rationale?: string; |
| riskLevel?: import('./permission.js').ApprovalRiskLevel; |
| hint?: string; |
| } |
| |
| export interface TokenUsageMessage extends TokenUsageFields { |
| type: 'token_usage'; |
| id: string; |
| turnId: string; |
| ts: number; |
| } |
| |
| export interface TurnStateMessage { |
| type: 'turn_state'; |
| id: string; |
| turnId: string; |
| ts: number; |
| status: TurnStatus; |
| parentTurnId?: string; |
| retriedFromTurnId?: string; |
| regeneratedFromTurnId?: string; |
| branchOfTurnId?: string; |
| parentSessionId?: string; |
| abortedAt?: number; |
| /** Diagnostic source for user/renderer-triggered aborts, e.g. renderer.stop_button. */ |
| abortSource?: string; |
| errorClass?: string; |
| failureMessage?: string; |
| retry?: ModelRetryDecision; |
| } |
| |
| export const WORKHUB_COORDINATION_RECORD_SCHEMA_VERSION = 1 as const; |
| export const WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION = 2 as const; |
| export const WORKHUB_COORDINATION_STOP_SCHEMA_VERSION = 3 as const; |
| |
| export type WorkHubDelegationDisposition = 'delegate_existing' | 'create_new'; |
| |
| export type WorkHubDelegationWorkspace = |
| | { readonly kind: 'project'; readonly projectId: string } |
| | { readonly kind: 'host_path'; readonly path: string }; |
| |
| /** User-selected creation defaults; never applied to an existing Work. */ |
| export interface WorkHubCreateDefaults { |
| readonly model?: { |
| readonly llmConnectionId: string; |
| readonly llmConnectionSlug: string; |
| readonly model: string; |
| }; |
| readonly permissionMode?: PermissionMode; |
| } |
| |
| export function isWorkHubCreateDefaults(value: unknown): value is WorkHubCreateDefaults { |
| if ( |
| !isRecord(value) || |
| Object.keys(value).some((key) => key !== 'model' && key !== 'permissionMode') |
| ) |
| return false; |
| if (value.permissionMode !== undefined && !isPermissionMode(value.permissionMode)) return false; |
| if (value.model === undefined) return true; |
| const model = value.model; |
| return ( |
| isRecord(model) && |
| Object.keys(model).length === 3 && |
| ['llmConnectionId', 'llmConnectionSlug', 'model'].every( |
| (key) => |
| typeof model[key] === 'string' && model[key].trim().length > 0 && model[key].length <= 512, |
| ) |
| ); |
| } |
| |
| export interface WorkHubDelegationCreateSpec { |
| readonly title: string; |
| readonly workspace: WorkHubDelegationWorkspace; |
| readonly defaults?: WorkHubCreateDefaults; |
| } |
| |
| interface WorkHubCoordinationMessageEnvelope { |
| type: 'workhub_coordination'; |
| id: string; |
| /** The Coordination Turn that owns this action. */ |
| turnId: string; |
| ts: number; |
| schemaVersion: |
| | typeof WORKHUB_COORDINATION_RECORD_SCHEMA_VERSION |
| | typeof WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION; |
| actionId: string; |
| actionFingerprint: `sha256:${string}`; |
| coordinationTurnId: string; |
| targetSessionId: string; |
| disposition: WorkHubDelegationDisposition; |
| /** Original user request retained as the action's authorization evidence. */ |
| userText: string; |
| attachments?: AttachmentRef[]; |
| /** Actual delegated content; omitted when the original request is used verbatim. */ |
| delegationText?: string; |
| /** Present exactly for create_new. */ |
| create?: WorkHubDelegationCreateSpec; |
| } |
| |
| /** |
| * Atomic proof that one Coordination action and one target Message admission |
| * were committed together by the Runtime Host. |
| */ |
| export interface WorkHubDelegationAssignedMessage extends WorkHubCoordinationMessageEnvelope { |
| kind: 'delegation_assigned'; |
| delegationId: string; |
| targetTurnId: string; |
| targetMessageId: string; |
| targetSessionName: string; |
| /** Copied, target-owned attachment locators admitted atomically with this record. */ |
| targetAttachments?: AttachmentRef[]; |
| steered?: true; |
| /** Present only when this assignment atomically supersedes an earlier link. */ |
| replacesActionId?: string; |
| replacesDelegationId?: string; |
| } |
| |
| /** Durable recovery intent written before destructive target cancellation/Stop. */ |
| export interface WorkHubDelegationReplacementRequestedMessage |
| extends WorkHubCoordinationMessageEnvelope { |
| schemaVersion: typeof WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION; |
| kind: 'delegation_replacement_requested'; |
| replacesActionId: string; |
| replacesDelegationId: string; |
| replacedTargetSessionId: string; |
| replacedTargetMessageId: string; |
| targetSessionName: string; |
| } |
| |
| /** Atomic proof that the old link became superseded by the replacement assignment. */ |
| export interface WorkHubDelegationSupersededMessage { |
| type: 'workhub_coordination'; |
| id: string; |
| turnId: string; |
| ts: number; |
| schemaVersion: typeof WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION; |
| kind: 'delegation_superseded'; |
| actionId: string; |
| actionFingerprint: `sha256:${string}`; |
| coordinationTurnId: string; |
| supersededActionId: string; |
| supersededDelegationId: string; |
| replacementDelegationId: string; |
| } |
| |
| /** Durable terminal proof that retirement succeeded but replacement admission did not. */ |
| export interface WorkHubDelegationReplacementAbortedMessage { |
| type: 'workhub_coordination'; |
| id: string; |
| turnId: string; |
| ts: number; |
| schemaVersion: typeof WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION; |
| kind: 'delegation_replacement_aborted'; |
| actionId: string; |
| actionFingerprint: `sha256:${string}`; |
| coordinationTurnId: string; |
| abortedActionId: string; |
| abortedDelegationId: string; |
| targetSessionId: string; |
| reason: 'target_unavailable' | 'target_waiting_for_user'; |
| } |
| |
| export type WorkHubDelegationStopOutcome = |
| | 'cancelled_pending' |
| | 'stop_delivered' |
| | 'already_terminal' |
| | 'not_owned'; |
| |
| /** Durable destructive claim written before attempting to retire one delegation. */ |
| export interface WorkHubDelegationStopRequestedMessage { |
| type: 'workhub_coordination'; |
| id: string; |
| turnId: string; |
| ts: number; |
| schemaVersion: typeof WORKHUB_COORDINATION_STOP_SCHEMA_VERSION; |
| kind: 'delegation_stop_requested'; |
| actionId: string; |
| actionFingerprint: `sha256:${string}`; |
| coordinationTurnId: string; |
| stopsActionId: string; |
| stopsDelegationId: string; |
| targetSessionId: string; |
| targetMessageId: string; |
| targetSessionName: string; |
| userText: string; |
| } |
| |
| /** Durable observed result of a direct-stop attempt. */ |
| export interface WorkHubDelegationStopResolvedMessage { |
| type: 'workhub_coordination'; |
| id: string; |
| turnId: string; |
| ts: number; |
| schemaVersion: typeof WORKHUB_COORDINATION_STOP_SCHEMA_VERSION; |
| kind: 'delegation_stop_resolved'; |
| actionId: string; |
| actionFingerprint: `sha256:${string}`; |
| coordinationTurnId: string; |
| stopsActionId: string; |
| stopsDelegationId: string; |
| targetSessionId: string; |
| outcome: WorkHubDelegationStopOutcome; |
| targetTurnId?: string; |
| } |
| |
| /** |
| * The exact durable operation one WorkHub action identity is allowed to own. |
| * |
| * Per-record identity is keyed by the thing each record is about — an |
| * assignment by its action, a stop or replacement by its delegation — so no |
| * single record can reject an action id that crossed to another delegation or |
| * another disposition. This vocabulary names the one global owner that can. |
| */ |
| export type WorkHubActionOperation = |
| | 'answer_here' |
| | 'clarify' |
| | 'delegate_existing' |
| | 'create_new' |
| | 'replace' |
| | 'stop'; |
| |
| /** Durable global binding from one action identity to one exact operation. */ |
| export interface WorkHubActionClaim { |
| readonly actionId: string; |
| readonly operation: WorkHubActionOperation; |
| readonly actionFingerprint: `sha256:${string}`; |
| /** The durable identity this action owns: a delegation or a Coordination Turn. */ |
| readonly subject: string; |
| } |
| |
| export type WorkHubActionClaimOutcome = 'claimed' | 'same_claim' | 'conflict'; |
| |
| export interface WorkHubCoordinationActionMessage { |
| type: 'workhub_coordination'; |
| kind: 'action_receipt'; |
| schemaVersion: 1; |
| id: string; |
| turnId: string; |
| ts: number; |
| receipt: WorkHubActionReceipt; |
| } |
| |
| export type WorkHubCoordinationMessage = |
| | WorkHubCoordinationActionMessage |
| | WorkHubDelegationAssignedMessage |
| | WorkHubDelegationReplacementRequestedMessage |
| | WorkHubDelegationReplacementAbortedMessage |
| | WorkHubDelegationSupersededMessage |
| | WorkHubDelegationStopRequestedMessage |
| | WorkHubDelegationStopResolvedMessage; |
| |
| function isWorkHubDelegationStopResolution( |
| outcome: unknown, |
| targetTurnId: unknown, |
| ): outcome is WorkHubDelegationStopOutcome { |
| const hasTargetTurnId = typeof targetTurnId === 'string' && targetTurnId.length > 0; |
| if (outcome === 'stop_delivered' || outcome === 'not_owned') return hasTargetTurnId; |
| if (outcome === 'cancelled_pending') return targetTurnId === undefined; |
| return outcome === 'already_terminal' && (targetTurnId === undefined || hasTargetTurnId); |
| } |
| |
| export interface TurnRecord { |
| turnId: string; |
| firstSequence?: number; |
| userPromptPreview?: string; |
| status: TurnStatus; |
| /** |
| * Whether `status` came from a `turn_state` message or was reconstructed by |
| * `inferLegacyTurnStatus` for a session written before them. Only a recorded |
| * status is evidence about this turn; an inferred one is a reading of old |
| * data, and callers reconciling against live state must not treat the two |
| * alike. Absent on hand-built records, which are treated as non-evidence; |
| * `deriveTurnRecords` is the only place that can know. |
| */ |
| statusSource?: 'recorded' | 'inferred'; |
| parentTurnId?: string; |
| retriedFromTurnId?: string; |
| regeneratedFromTurnId?: string; |
| branchOfTurnId?: string; |
| parentSessionId?: string; |
| abortedAt?: number; |
| abortSource?: string; |
| errorClass?: string; |
| failureMessage?: string; |
| retry?: ModelRetryDecision; |
| } |
| |
| /** |
| * The notes the runtime writes: things that happened inside one invocation and |
| * are part of what that invocation did. Their record is its RuntimeEvent ledger. |
| */ |
| export const RUNTIME_SYSTEM_NOTE_KINDS = [ |
| 'context_compacted', |
| 'context_compaction_failed_open', |
| 'context_provider_dropping', |
| 'context_window_suggestion', |
| 'context_window_overrun', |
| 'context_reported_window_exceeded', |
| 'context_overflow_after_compaction', |
| 'step_limit', |
| ] as const; |
| |
| /** |
| * Notes only legacy transcripts carry, still decoded so those rows stay |
| * readable. Nothing writes them: the Session header and the invocation's |
| * opening and terminal facts already own what each of them said. |
| */ |
| export const RETIRED_SYSTEM_NOTE_KINDS = [ |
| 'session_start', |
| 'session_resume', |
| 'mode_change', |
| 'model_change', |
| 'error', |
| 'abort', |
| ] as const; |
| |
| export type RuntimeSystemNoteKind = (typeof RUNTIME_SYSTEM_NOTE_KINDS)[number]; |
| export type SystemNoteKind = RuntimeSystemNoteKind | (typeof RETIRED_SYSTEM_NOTE_KINDS)[number]; |
| |
| export function isRuntimeSystemNoteKind(kind: string): kind is RuntimeSystemNoteKind { |
| return (RUNTIME_SYSTEM_NOTE_KINDS as readonly string[]).includes(kind); |
| } |
| |
| export interface SystemNoteMessage { |
| type: 'system_note'; |
| id: string; |
| /** Retired session-level notes omit turnId. */ |
| turnId?: string; |
| ts: number; |
| kind: SystemNoteKind; |
| /** |
| * Shape depends on `kind`. `context_compaction_failed_open` carries |
| * `{ failOpenReason?: string }` — the reason the fold was refused (e.g. |
| * `coverage_miss`, `source_hash_mismatch`); when a turn is stopped before |
| * settlement, this note is the only durable record of the reason, because |
| * the `token_usage` diagnostic is never written (#4850). |
| */ |
| data?: unknown; |
| } |
| |
| const USER_MESSAGE_SHAPE = defineObjectShape<UserMessage>()( |
| ['type', 'id', 'turnId', 'ts', 'text'], |
| [ |
| 'displayText', |
| 'attachments', |
| 'directoryReferences', |
| 'quotes', |
| 'inlineReferences', |
| 'steeringEventId', |
| 'coordinationActionId', |
| 'origin', |
| ], |
| ); |
| const ASSISTANT_MESSAGE_SHAPE = defineObjectShape<AssistantMessage>()( |
| ['type', 'id', 'turnId', 'ts', 'text', 'modelId'], |
| ['thinking', 'contentOrder', 'providerOptions'], |
| ); |
| const TOOL_CALL_MESSAGE_SHAPE = defineObjectShape<ToolCallMessage>()( |
| ['type', 'id', 'turnId', 'ts', 'toolName', 'args'], |
| [ |
| 'activityKind', |
| 'displayName', |
| 'intent', |
| 'providerOptions', |
| 'providerExecuted', |
| 'stepId', |
| 'origin', |
| 'modelVisibility', |
| 'parentToolCallId', |
| 'parentOperationId', |
| ], |
| ); |
| const TOOL_RESULT_MESSAGE_SHAPE = defineObjectShape<ToolResultMessage>()( |
| ['type', 'id', 'turnId', 'ts', 'toolUseId', 'isError', 'content'], |
| [ |
| 'durationMs', |
| 'providerExecuted', |
| 'providerOutput', |
| 'origin', |
| 'modelVisibility', |
| 'parentToolCallId', |
| 'parentOperationId', |
| ], |
| ); |
| const PERMISSION_DECISION_MESSAGE_SHAPE = defineObjectShape<PermissionDecisionMessage>()( |
| ['type', 'id', 'turnId', 'ts', 'toolUseId', 'toolName', 'decision'], |
| ['rememberForTurn', 'reviewer', 'rationale', 'riskLevel', 'hint'], |
| ); |
| const TOKEN_USAGE_MESSAGE_SHAPE = defineObjectShape<TokenUsageMessage>()( |
| ['type', 'id', 'turnId', 'ts', 'input', 'output'], |
| [ |
| 'cacheHitInput', |
| 'cacheMissInput', |
| 'cacheWriteInput', |
| 'cacheMissInputSource', |
| 'reasoning', |
| 'total', |
| 'rawFinishReason', |
| 'runtimeSteps', |
| 'cacheRead', |
| 'cacheCreation', |
| 'costUsd', |
| 'systemPromptHash', |
| 'contextRemaining', |
| 'prefixHash', |
| 'prefixChangeReason', |
| 'requestShapeHash', |
| 'requestShapeChangeReason', |
| 'promptSegments', |
| 'contextBudget', |
| 'providerRequestTraceId', |
| 'lastRequestAnchor', |
| ], |
| ); |
| const TURN_STATE_MESSAGE_SHAPE = defineObjectShape<TurnStateMessage>()( |
| ['type', 'id', 'turnId', 'ts', 'status'], |
| [ |
| 'parentTurnId', |
| 'retriedFromTurnId', |
| 'regeneratedFromTurnId', |
| 'branchOfTurnId', |
| 'parentSessionId', |
| 'abortedAt', |
| 'abortSource', |
| 'errorClass', |
| 'failureMessage', |
| 'retry', |
| ], |
| ['partialOutputRetained'], |
| ); |
| const WORKHUB_DELEGATION_ASSIGNED_MESSAGE_SHAPE = |
| defineObjectShape<WorkHubDelegationAssignedMessage>()( |
| [ |
| 'type', |
| 'id', |
| 'turnId', |
| 'ts', |
| 'schemaVersion', |
| 'kind', |
| 'actionId', |
| 'actionFingerprint', |
| 'coordinationTurnId', |
| 'targetSessionId', |
| 'disposition', |
| 'userText', |
| 'delegationId', |
| 'targetTurnId', |
| 'targetMessageId', |
| 'targetSessionName', |
| ], |
| [ |
| 'attachments', |
| 'targetAttachments', |
| 'create', |
| 'steered', |
| 'replacesActionId', |
| 'replacesDelegationId', |
| 'delegationText', |
| ], |
| ); |
| const WORKHUB_DELEGATION_REPLACEMENT_REQUESTED_MESSAGE_SHAPE = |
| defineObjectShape<WorkHubDelegationReplacementRequestedMessage>()( |
| [ |
| 'type', |
| 'id', |
| 'turnId', |
| 'ts', |
| 'schemaVersion', |
| 'kind', |
| 'actionId', |
| 'actionFingerprint', |
| 'coordinationTurnId', |
| 'targetSessionId', |
| 'disposition', |
| 'userText', |
| 'replacesActionId', |
| 'replacesDelegationId', |
| 'replacedTargetSessionId', |
| 'replacedTargetMessageId', |
| 'targetSessionName', |
| ], |
| ['attachments', 'create', 'delegationText'], |
| ); |
| const WORKHUB_DELEGATION_SUPERSEDED_MESSAGE_SHAPE = |
| defineObjectShape<WorkHubDelegationSupersededMessage>()( |
| [ |
| 'type', |
| 'id', |
| 'turnId', |
| 'ts', |
| 'schemaVersion', |
| 'kind', |
| 'actionId', |
| 'actionFingerprint', |
| 'coordinationTurnId', |
| 'supersededActionId', |
| 'supersededDelegationId', |
| 'replacementDelegationId', |
| ], |
| [], |
| ); |
| const WORKHUB_DELEGATION_REPLACEMENT_ABORTED_MESSAGE_SHAPE = |
| defineObjectShape<WorkHubDelegationReplacementAbortedMessage>()( |
| [ |
| 'type', |
| 'id', |
| 'turnId', |
| 'ts', |
| 'schemaVersion', |
| 'kind', |
| 'actionId', |
| 'actionFingerprint', |
| 'coordinationTurnId', |
| 'abortedActionId', |
| 'abortedDelegationId', |
| 'targetSessionId', |
| 'reason', |
| ], |
| [], |
| ); |
| const WORKHUB_DELEGATION_STOP_REQUESTED_MESSAGE_SHAPE = |
| defineObjectShape<WorkHubDelegationStopRequestedMessage>()( |
| [ |
| 'type', |
| 'id', |
| 'turnId', |
| 'ts', |
| 'schemaVersion', |
| 'kind', |
| 'actionId', |
| 'actionFingerprint', |
| 'coordinationTurnId', |
| 'stopsActionId', |
| 'stopsDelegationId', |
| 'targetSessionId', |
| 'targetMessageId', |
| 'targetSessionName', |
| 'userText', |
| ], |
| [], |
| ); |
| const WORKHUB_DELEGATION_STOP_RESOLVED_MESSAGE_SHAPE = |
| defineObjectShape<WorkHubDelegationStopResolvedMessage>()( |
| [ |
| 'type', |
| 'id', |
| 'turnId', |
| 'ts', |
| 'schemaVersion', |
| 'kind', |
| 'actionId', |
| 'actionFingerprint', |
| 'coordinationTurnId', |
| 'stopsActionId', |
| 'stopsDelegationId', |
| 'targetSessionId', |
| 'outcome', |
| ], |
| ['targetTurnId'], |
| ); |
| const WORKHUB_DELEGATION_CREATE_SHAPE = defineObjectShape<WorkHubDelegationCreateSpec>()( |
| ['title', 'workspace'], |
| ['defaults'], |
| ); |
| const WORKHUB_DELEGATION_PROJECT_WORKSPACE_SHAPE = defineObjectShape< |
| Extract<WorkHubDelegationWorkspace, { kind: 'project' }> |
| >()(['kind', 'projectId'], []); |
| const WORKHUB_DELEGATION_HOST_PATH_WORKSPACE_SHAPE = defineObjectShape< |
| Extract<WorkHubDelegationWorkspace, { kind: 'host_path' }> |
| >()(['kind', 'path'], []); |
| const SYSTEM_NOTE_MESSAGE_SHAPE = defineObjectShape<SystemNoteMessage>()( |
| ['type', 'id', 'ts', 'kind'], |
| ['turnId', 'data'], |
| ); |
| const ASSISTANT_THINKING_PART_SHAPE = defineObjectShape<AssistantThinkingPart>()( |
| ['text'], |
| ['signature', 'providerOptions'], |
| ); |
| const ASSISTANT_THINKING_SHAPE = defineObjectShape<AssistantThinking>()( |
| ['text'], |
| ['signature', 'providerOptions', 'parts'], |
| ); |
| const SYSTEM_NOTE_KINDS = new Set<string>([ |
| ...RUNTIME_SYSTEM_NOTE_KINDS, |
| ...RETIRED_SYSTEM_NOTE_KINDS, |
| ]); |
| |
| export function decodeCanonicalMessage(value: unknown): StoredMessage { |
| return decodeMessage(value, decodeCanonicalToolResultContent); |
| } |
| |
| export function decodeStoredMessage(persisted: PersistedValue<StoredMessage>): StoredMessage { |
| return decodeMessage(persisted as unknown, (content) => |
| decodePersistedToolResultContent(markPersisted<ToolResultContent>(content)), |
| ); |
| } |
| |
| function decodeMessage( |
| value: unknown, |
| decodeToolResultContent: (content: unknown) => ToolResultContent, |
| ): StoredMessage { |
| const message = decodeStoredMessageContent(value, decodeToolResultContent); |
| if (!isRecord(message)) throw new Error('Invalid stored message schema'); |
| switch (message.type) { |
| case 'user': |
| if ( |
| hasExactShape(message, USER_MESSAGE_SHAPE) && |
| hasMessageEnvelope(message, true) && |
| (message.origin === undefined || decodeTurnOrigin(message.origin) !== undefined) && |
| (message.coordinationActionId === undefined || |
| (typeof message.coordinationActionId === 'string' && |
| message.coordinationActionId.length > 0)) |
| ) { |
| const { |
| displayText, |
| attachments, |
| directoryReferences, |
| quotes, |
| inlineReferences, |
| origin, |
| ...envelope |
| } = message; |
| const decodedOrigin = origin === undefined ? undefined : decodeTurnOrigin(origin); |
| try { |
| return { |
| ...envelope, |
| ...decodeMessageContent({ |
| text: message.text, |
| displayText, |
| attachments, |
| directoryReferences, |
| quotes, |
| inlineReferences, |
| }), |
| ...(decodedOrigin !== undefined ? { origin: decodedOrigin } : {}), |
| } as unknown as UserMessage; |
| } catch { |
| break; |
| } |
| } |
| break; |
| case 'assistant': |
| if ( |
| hasExactShape(message, ASSISTANT_MESSAGE_SHAPE) && |
| hasMessageEnvelope(message, true) && |
| typeof message.text === 'string' && |
| typeof message.modelId === 'string' && |
| (message.providerOptions === undefined || isRecord(message.providerOptions)) && |
| (message.thinking === undefined || isAssistantThinking(message.thinking)) && |
| (message.contentOrder === undefined || |
| (Array.isArray(message.contentOrder) && |
| message.contentOrder.every( |
| (item) => item === 'thinking' || item === 'text' || item === 'tools', |
| ))) |
| ) |
| return message as unknown as AssistantMessage; |
| break; |
| case 'tool_call': |
| if ( |
| hasExactShape(message, TOOL_CALL_MESSAGE_SHAPE) && |
| hasMessageEnvelope(message, true) && |
| typeof message.toolName === 'string' && |
| Object.hasOwn(message, 'args') && |
| (message.activityKind === undefined || |
| (TOOL_ACTIVITY_KINDS as readonly unknown[]).includes(message.activityKind)) && |
| isOptionalString(message.displayName) && |
| isOptionalString(message.intent) && |
| (message.providerOptions === undefined || isRecord(message.providerOptions)) && |
| (message.providerExecuted === undefined || typeof message.providerExecuted === 'boolean') && |
| isOptionalString(message.stepId) && |
| isToolActivityIdentity(message) |
| ) |
| return message as unknown as ToolCallMessage; |
| break; |
| case 'tool_result': |
| if ( |
| hasExactShape(message, TOOL_RESULT_MESSAGE_SHAPE) && |
| hasMessageEnvelope(message, true) && |
| typeof message.toolUseId === 'string' && |
| typeof message.isError === 'boolean' && |
| (message.providerExecuted === undefined || typeof message.providerExecuted === 'boolean') && |
| isOptionalFiniteDuration(message.durationMs) && |
| isToolActivityIdentity(message) |
| ) |
| return message as unknown as ToolResultMessage; |
| break; |
| case 'permission_decision': |
| if ( |
| hasExactShape(message, PERMISSION_DECISION_MESSAGE_SHAPE) && |
| hasMessageEnvelope(message, true) && |
| typeof message.toolUseId === 'string' && |
| typeof message.toolName === 'string' && |
| isPermissionDecisionFields(message, { allowHint: true }) |
| ) |
| return message as unknown as PermissionDecisionMessage; |
| break; |
| case 'token_usage': |
| if ( |
| hasExactShape(message, TOKEN_USAGE_MESSAGE_SHAPE) && |
| hasMessageEnvelope(message, true) && |
| isTokenUsageFields(message) && |
| isOptionalString(message.providerRequestTraceId) |
| ) |
| return message as unknown as TokenUsageMessage; |
| break; |
| case 'turn_state': |
| if ( |
| hasExactShape(message, TURN_STATE_MESSAGE_SHAPE) && |
| hasMessageEnvelope(message, true) && |
| isTurnStatus(message.status) && |
| isOptionalString(message.parentTurnId) && |
| isOptionalString(message.retriedFromTurnId) && |
| isOptionalString(message.regeneratedFromTurnId) && |
| isOptionalString(message.branchOfTurnId) && |
| isOptionalString(message.parentSessionId) && |
| (message.abortedAt === undefined || isFiniteNumber(message.abortedAt)) && |
| isOptionalString(message.abortSource) && |
| isOptionalString(message.errorClass) && |
| (message.failureMessage === undefined || |
| (typeof message.failureMessage === 'string' && |
| new TextEncoder().encode(message.failureMessage).byteLength <= |
| MODEL_FAILURE_MESSAGE_MAX_BYTES)) && |
| (message.retry === undefined || isModelRetryDecision(message.retry)) |
| ) |
| return pickShape(message as unknown as TurnStateMessage, TURN_STATE_MESSAGE_SHAPE); |
| break; |
| case 'workhub_coordination': |
| if (isWorkHubCoordinationMessage(message)) { |
| return message as unknown as WorkHubCoordinationMessage; |
| } |
| break; |
| case 'system_note': |
| if ( |
| hasExactShape(message, SYSTEM_NOTE_MESSAGE_SHAPE) && |
| hasMessageEnvelope(message, false) && |
| isOptionalString(message.turnId) && |
| SYSTEM_NOTE_KINDS.has(message.kind as string) |
| ) |
| return message as unknown as SystemNoteMessage; |
| break; |
| } |
| throw new Error('Invalid stored message schema'); |
| } |
| |
| function isWorkHubCoordinationMessage(message: Record<string, unknown>): boolean { |
| if (message.kind === 'action_receipt') |
| return ( |
| hasMessageEnvelope(message, true) && |
| message.schemaVersion === 1 && |
| Object.keys(message).every((k) => |
| ['type', 'kind', 'schemaVersion', 'id', 'turnId', 'ts', 'receipt'].includes(k), |
| ) && |
| isWorkHubActionReceipt(message.receipt) |
| ); |
| |
| if (message.kind === 'delegation_stop_requested') { |
| return ( |
| hasMessageEnvelope(message, true) && |
| hasExactShape(message, WORKHUB_DELEGATION_STOP_REQUESTED_MESSAGE_SHAPE) && |
| message.schemaVersion === WORKHUB_COORDINATION_STOP_SCHEMA_VERSION && |
| isWorkHubActionIdentity(message) && |
| typeof message.stopsActionId === 'string' && |
| message.stopsActionId.length > 0 && |
| typeof message.stopsDelegationId === 'string' && |
| message.stopsDelegationId.length > 0 && |
| typeof message.targetSessionId === 'string' && |
| message.targetSessionId.length > 0 && |
| typeof message.targetMessageId === 'string' && |
| message.targetMessageId.length > 0 && |
| typeof message.targetSessionName === 'string' && |
| message.targetSessionName.trim().length > 0 && |
| typeof message.userText === 'string' && |
| message.userText.trim().length > 0 |
| ); |
| } |
| if (message.kind === 'delegation_stop_resolved') { |
| return ( |
| hasMessageEnvelope(message, true) && |
| hasExactShape(message, WORKHUB_DELEGATION_STOP_RESOLVED_MESSAGE_SHAPE) && |
| message.schemaVersion === WORKHUB_COORDINATION_STOP_SCHEMA_VERSION && |
| isWorkHubActionIdentity(message) && |
| typeof message.stopsActionId === 'string' && |
| message.stopsActionId.length > 0 && |
| typeof message.stopsDelegationId === 'string' && |
| message.stopsDelegationId.length > 0 && |
| typeof message.targetSessionId === 'string' && |
| message.targetSessionId.length > 0 && |
| isWorkHubDelegationStopResolution(message.outcome, message.targetTurnId) |
| ); |
| } |
| if (message.kind === 'delegation_replacement_aborted') { |
| return ( |
| hasMessageEnvelope(message, true) && |
| hasExactShape(message, WORKHUB_DELEGATION_REPLACEMENT_ABORTED_MESSAGE_SHAPE) && |
| message.schemaVersion === WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION && |
| typeof message.actionId === 'string' && |
| typeof message.actionFingerprint === 'string' && |
| /^sha256:[a-f0-9]{64}$/u.test(message.actionFingerprint) && |
| typeof message.coordinationTurnId === 'string' && |
| message.turnId === message.coordinationTurnId && |
| typeof message.abortedActionId === 'string' && |
| message.abortedActionId.length > 0 && |
| typeof message.abortedDelegationId === 'string' && |
| message.abortedDelegationId.length > 0 && |
| typeof message.targetSessionId === 'string' && |
| message.targetSessionId.length > 0 && |
| (message.reason === 'target_unavailable' || message.reason === 'target_waiting_for_user') |
| ); |
| } |
| if (message.kind === 'delegation_superseded') { |
| return ( |
| hasMessageEnvelope(message, true) && |
| hasExactShape(message, WORKHUB_DELEGATION_SUPERSEDED_MESSAGE_SHAPE) && |
| message.schemaVersion === WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION && |
| typeof message.actionId === 'string' && |
| typeof message.actionFingerprint === 'string' && |
| /^sha256:[a-f0-9]{64}$/u.test(message.actionFingerprint) && |
| typeof message.coordinationTurnId === 'string' && |
| message.turnId === message.coordinationTurnId && |
| typeof message.supersededActionId === 'string' && |
| message.supersededActionId.length > 0 && |
| typeof message.supersededDelegationId === 'string' && |
| message.supersededDelegationId.length > 0 && |
| typeof message.replacementDelegationId === 'string' && |
| message.replacementDelegationId.length > 0 |
| ); |
| } |
| const common = |
| hasMessageEnvelope(message, true) && |
| (message.schemaVersion === WORKHUB_COORDINATION_RECORD_SCHEMA_VERSION || |
| message.schemaVersion === WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION) && |
| typeof message.actionId === 'string' && |
| typeof message.actionFingerprint === 'string' && |
| /^sha256:[a-f0-9]{64}$/u.test(message.actionFingerprint) && |
| typeof message.coordinationTurnId === 'string' && |
| message.turnId === message.coordinationTurnId && |
| typeof message.targetSessionId === 'string' && |
| typeof message.userText === 'string' && |
| message.userText.trim().length > 0 && |
| isWorkHubMessageAttachments(message.attachments) && |
| (message.delegationText === undefined || |
| (typeof message.delegationText === 'string' && message.delegationText.trim().length > 0)) && |
| ((message.disposition === 'delegate_existing' && message.create === undefined) || |
| (message.disposition === 'create_new' && isWorkHubDelegationCreateSpec(message.create))) && |
| (message.disposition === 'delegate_existing' || message.disposition === 'create_new'); |
| if (!common) return false; |
| if (message.kind === 'delegation_replacement_requested') { |
| return ( |
| message.schemaVersion === WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION && |
| hasExactShape(message, WORKHUB_DELEGATION_REPLACEMENT_REQUESTED_MESSAGE_SHAPE) && |
| typeof message.replacesActionId === 'string' && |
| message.replacesActionId.length > 0 && |
| typeof message.replacesDelegationId === 'string' && |
| message.replacesDelegationId.length > 0 && |
| typeof message.replacedTargetSessionId === 'string' && |
| message.replacedTargetSessionId.length > 0 && |
| typeof message.replacedTargetMessageId === 'string' && |
| message.replacedTargetMessageId.length > 0 && |
| typeof message.targetSessionName === 'string' && |
| message.targetSessionName.trim().length > 0 |
| ); |
| } |
| return ( |
| message.kind === 'delegation_assigned' && |
| hasExactShape(message, WORKHUB_DELEGATION_ASSIGNED_MESSAGE_SHAPE) && |
| isWorkHubMessageAttachments(message.targetAttachments) && |
| typeof message.delegationId === 'string' && |
| typeof message.targetTurnId === 'string' && |
| typeof message.targetMessageId === 'string' && |
| typeof message.targetSessionName === 'string' && |
| message.targetSessionName.trim().length > 0 && |
| (message.steered === undefined || message.steered === true) && |
| ((message.schemaVersion === WORKHUB_COORDINATION_RECORD_SCHEMA_VERSION && |
| message.replacesActionId === undefined && |
| message.replacesDelegationId === undefined) || |
| (message.schemaVersion === WORKHUB_COORDINATION_REPLACEMENT_SCHEMA_VERSION && |
| typeof message.replacesActionId === 'string' && |
| message.replacesActionId.length > 0 && |
| typeof message.replacesDelegationId === 'string' && |
| message.replacesDelegationId.length > 0)) |
| ); |
| } |
| |
| function isWorkHubActionIdentity(message: Record<string, unknown>): boolean { |
| return ( |
| typeof message.actionId === 'string' && |
| message.actionId.length > 0 && |
| typeof message.actionFingerprint === 'string' && |
| /^sha256:[a-f0-9]{64}$/u.test(message.actionFingerprint) && |
| typeof message.coordinationTurnId === 'string' && |
| message.coordinationTurnId.length > 0 && |
| message.turnId === message.coordinationTurnId |
| ); |
| } |
| |
| function isWorkHubMessageAttachments(value: unknown): boolean { |
| if (value === undefined) return true; |
| try { |
| decodeMessageContent({ text: '', attachments: value }); |
| return true; |
| } catch { |
| return false; |
| } |
| } |
| |
| function isWorkHubDelegationCreateSpec(value: unknown): value is WorkHubDelegationCreateSpec { |
| if ( |
| !isRecord(value) || |
| !hasExactShape(value, WORKHUB_DELEGATION_CREATE_SHAPE) || |
| (value.defaults !== undefined && !isWorkHubCreateDefaults(value.defaults)) || |
| typeof value.title !== 'string' || |
| value.title.trim().length === 0 || |
| !isRecord(value.workspace) |
| ) { |
| return false; |
| } |
| if (value.workspace.kind === 'project') { |
| return ( |
| hasExactShape(value.workspace, WORKHUB_DELEGATION_PROJECT_WORKSPACE_SHAPE) && |
| typeof value.workspace.projectId === 'string' && |
| value.workspace.projectId.length > 0 |
| ); |
| } |
| return ( |
| value.workspace.kind === 'host_path' && |
| hasExactShape(value.workspace, WORKHUB_DELEGATION_HOST_PATH_WORKSPACE_SHAPE) && |
| typeof value.workspace.path === 'string' && |
| value.workspace.path.length > 0 |
| ); |
| } |
| |
| function decodeStoredMessageContent( |
| value: unknown, |
| decodeToolResultContent: (content: unknown) => ToolResultContent, |
| ): unknown { |
| if (!isRecord(value) || value.type !== 'tool_result') return value; |
| return { |
| ...value, |
| content: decodeToolResultContent(value.content), |
| }; |
| } |
| |
| function hasMessageEnvelope(value: Record<string, unknown>, turnRequired: boolean): boolean { |
| return ( |
| typeof value.id === 'string' && |
| isFiniteNumber(value.ts) && |
| (turnRequired ? typeof value.turnId === 'string' : true) |
| ); |
| } |
| |
| function isAssistantThinkingPart(value: unknown): value is AssistantThinkingPart { |
| return ( |
| isRecord(value) && |
| hasExactShape(value, ASSISTANT_THINKING_PART_SHAPE) && |
| typeof value.text === 'string' && |
| isOptionalString(value.signature) && |
| (value.providerOptions === undefined || isRecord(value.providerOptions)) |
| ); |
| } |
| |
| function isAssistantThinking(value: unknown): value is AssistantThinking { |
| return ( |
| isRecord(value) && |
| hasExactShape(value, ASSISTANT_THINKING_SHAPE) && |
| typeof value.text === 'string' && |
| isOptionalString(value.signature) && |
| (value.providerOptions === undefined || isRecord(value.providerOptions)) && |
| (value.parts === undefined || |
| (Array.isArray(value.parts) && |
| value.parts.length > 0 && |
| value.parts.every(isAssistantThinkingPart))) |
| ); |
| } |
| |
| function isOptionalFiniteDuration(value: unknown): boolean { |
| return value === undefined || isFiniteNumber(value); |
| } |
| |
| function isToolActivityIdentity(value: Record<string, unknown>): boolean { |
| return ( |
| (value.origin === undefined || value.origin === 'provider' || value.origin === 'code_mode') && |
| (value.modelVisibility === undefined || |
| value.modelVisibility === 'visible' || |
| value.modelVisibility === 'hidden') && |
| isOptionalString(value.parentToolCallId) && |
| isOptionalString(value.parentOperationId) |
| ); |
| } |
| |
| export const STEP_LIMIT_NOTICE_TEXT = |
| 'Reached the configured step limit. The task may be incomplete. Send “continue” to resume.'; |
| |
| /** Latest actual model recorded by a completed assistant step. */ |
| export function latestAssistantModelId(messages: readonly StoredMessage[]): string | undefined { |
| for (let index = messages.length - 1; index >= 0; index -= 1) { |
| const message = messages[index]; |
| if (message?.type === 'assistant') return message.modelId; |
| } |
| return undefined; |
| } |
| |
| export function deriveTurnRecords(messages: readonly StoredMessage[]): TurnRecord[] { |
| const order: string[] = []; |
| const buckets = new Map<string, StoredMessage[]>(); |
| for (const message of messages) { |
| const turnId = (message as { turnId?: string }).turnId; |
| if (!turnId) continue; |
| if (!buckets.has(turnId)) { |
| buckets.set(turnId, []); |
| order.push(turnId); |
| } |
| buckets.get(turnId)!.push(message); |
| } |
| |
| return order.map((turnId) => { |
| const bucket = buckets.get(turnId) ?? []; |
| const latestState = bucket |
| .filter((message): message is TurnStateMessage => message.type === 'turn_state') |
| .at(-1); |
| if (latestState) { |
| return { |
| turnId, |
| status: latestState.status, |
| statusSource: 'recorded', |
| ...(latestState.parentTurnId ? { parentTurnId: latestState.parentTurnId } : {}), |
| ...(latestState.retriedFromTurnId |
| ? { retriedFromTurnId: latestState.retriedFromTurnId } |
| : {}), |
| ...(latestState.regeneratedFromTurnId |
| ? { regeneratedFromTurnId: latestState.regeneratedFromTurnId } |
| : {}), |
| ...(latestState.branchOfTurnId ? { branchOfTurnId: latestState.branchOfTurnId } : {}), |
| ...(latestState.parentSessionId ? { parentSessionId: latestState.parentSessionId } : {}), |
| ...(latestState.abortedAt !== undefined ? { abortedAt: latestState.abortedAt } : {}), |
| ...(latestState.abortSource ? { abortSource: latestState.abortSource } : {}), |
| ...(latestState.errorClass ? { errorClass: latestState.errorClass } : {}), |
| ...(latestState.failureMessage ? { failureMessage: latestState.failureMessage } : {}), |
| ...(latestState.retry ? { retry: latestState.retry } : {}), |
| }; |
| } |
| return { |
| turnId, |
| status: inferLegacyTurnStatus(bucket), |
| statusSource: 'inferred', |
| }; |
| }); |
| } |
| |
| function inferLegacyTurnStatus(messages: readonly StoredMessage[]): TurnStatus { |
| if (messages.some((message) => message.type === 'system_note' && message.kind === 'abort')) |
| return 'aborted'; |
| if (messages.some((message) => message.type === 'assistant')) return 'completed'; |
| if (messages.some((message) => message.type === 'tool_result' && message.isError)) |
| return 'failed'; |
| return 'completed'; |
| } |