import type { AgentSessionBackgroundTask, AgentSessionBackgroundTaskState } from './agent-session-background-task-wire' import type { AgentSessionRewindReason, AgentSessionRewindSupport } from './agent-session-rewind' import type { AgentSessionWireRefusal } from './agent-session-wire-refusals' export * from './agent-session-wire-refusals' import type { AgentSessionConversationCommand } from './agent-session-conversation-command' import type { AgentSessionContextUsage } from './agent-session-context-usage' // ─── Structured agent-session wire contract ───────────────────────────────── // The shapes `agentSession.*` accepts and publishes. Phase 2 builds provider // adapters and clients against exactly these types, so everything here must be // plain JSON. The whole surface is gated by agent-session.structured.v1, which // no released baseline advertises; after that capability ships, every new field // must remain optional to old readers (docs/reference/remote-wire-compatibility.md). import type { AgentJournalCursor, AgentJournalRenderItem, AgentJournalResetReason, AgentJournalResolution, AgentJournalSubmission, AgentJournalThreadGoal, AgentJournalTurnOutcome } from './agent-session-journal-types' import { agentSessionScopeKey, type AgentSessionExecutionLocation, type AgentSessionHandoffStage, type AgentSessionRecord } from './agent-session-record' import type { AgentProviderSessionMetadata } from './agent-session-resume' import type { StructuredAgentSessionProjectedStatus } from './structured-agent-session-projection' /** `agentSession.handoffStatus`. Named for the removed terminal handoff; released desktop clients * still read `owner`. Clients parse the reply as unknown, since older hosts sent more fields. */ export type AgentSessionHandoffStatus = { owner: 'native' | 'none' direction: 'to-native' | null phase: 'idle' | 'switching' | 'failed' stage: AgentSessionHandoffStage | null operationId: string | null error?: { message: string; recoverableOwner: 'none' } } export type { AgentSessionBackgroundTask, AgentSessionBackgroundTaskRunState, AgentSessionBackgroundTaskState } from './agent-session-background-task-wire' export { agentSessionBackgroundTasksEqual } from './agent-session-background-task-wire' export type AgentSessionTurnActivity = { turnId: string text: string } export const AGENT_SESSION_ID_MAX_LENGTH = 512 /** Backward paging is the client's normal read; 40 matches the page size the * mobile list renders without a visible fill-in. */ export const AGENT_SESSION_HISTORY_DEFAULT_LIMIT = 40 export const AGENT_SESSION_HISTORY_MAX_LIMIT = 200 export const AGENT_SESSION_HISTORY_DIRECTIONS = ['tail', 'before', 'after'] as const /** `tail` is the newest page, `before` pages backward, `after` catches a live * reader up. Only `after` needs replayable rows; the other two read the * reduced timeline and so survive compaction. */ export type AgentSessionHistoryDirection = (typeof AGENT_SESSION_HISTORY_DIRECTIONS)[number] export type AgentSessionHistoryRequest = { sessionId: string direction: AgentSessionHistoryDirection /** Required for `before` and `after`; ignored for `tail`. */ cursor?: AgentJournalCursor limit?: number } export type AgentSessionHistoryPage = { sessionId: string epoch: string /** Optional for mixed-version readers; write-capable clients use the * checkpoint without forcing a second attach or a redundant snapshot. */ fence?: number direction: AgentSessionHistoryDirection items: AgentJournalRenderItem[] /** Populated by `after` reads so a disconnected client can apply tombstones. */ removedItemIds: string[] /** Submissions overlapping this page, so an unconfirmed bubble renders with * its dispatch state instead of as a plain message. */ submissions: AgentJournalSubmission[] /** Page edges. `nextCursor` is what the client sends back for the same * direction; it equals the request cursor when the page is empty. */ window: { oldest: AgentJournalCursor | null newest: AgentJournalCursor | null nextCursor: AgentJournalCursor } /** Current journal head for switching from a bounded page to live subscribe. */ liveCursor?: AgentJournalCursor hasOlder: boolean hasNewer: boolean /** Present on hosts that expose provider-owned background task lifecycle. */ backgroundTasks?: AgentSessionBackgroundTaskState | null /** Host wall clock (ms epoch) when the page was read, so a client attaching mid-turn * can anchor a live counter on the real start. Absent from older hosts. */ hostNow?: number } export type AgentSessionHistoryResult = | { ok: true; page: AgentSessionHistoryPage; providerSession?: AgentProviderSessionMetadata } /** Every reset carries a byte-bounded tail page so recovery cannot exceed * remote outbound admission or require another call before resubscribing. */ | { ok: false reset: AgentJournalResetReason page: AgentSessionHistoryPage fence?: number providerSession?: AgentProviderSessionMetadata } /** Cursor-qualified incremental publication. Items and submissions carry their * CURRENT reduced state rather than a delta, so applying a batch twice * converges instead of double-appending. */ export type AgentSessionJournalBatch = { cursor: AgentJournalCursor items: AgentJournalRenderItem[] removedItemIds: string[] submissions: AgentJournalSubmission[] } /** Host wall clock (ms epoch) stamped once per published frame; see `AgentSessionHistoryPage`. */ type AgentSessionHostClockField = { hostNow?: number } export type AgentSessionSubscribeEvent = | ({ type: 'snapshot' sessionId: string page: AgentSessionHistoryPage fence: number backgroundTasks?: AgentSessionBackgroundTaskState | null /** Omitted when unchanged; null clears a previous provider catalog. */ commands?: AgentSessionSlashCommand[] | null /** Latest provider-authored turn activity; optional for mixed-version hosts. */ activity?: AgentSessionTurnActivity | null } & AgentSessionHostClockField) | ({ type: 'batch' sessionId: string batch: AgentSessionJournalBatch /** Optional so mixed-version cursors retain the ownership fence. */ fence?: number backgroundTasks?: AgentSessionBackgroundTaskState | null /** Omitted when unchanged; null clears a previous provider catalog. */ commands?: AgentSessionSlashCommand[] | null /** Additive ephemeral state; it never creates or advances journal rows. */ activity?: AgentSessionTurnActivity | null } & AgentSessionHostClockField) | ({ type: 'reset' sessionId: string reset: AgentJournalResetReason page: AgentSessionHistoryPage fence: number backgroundTasks?: AgentSessionBackgroundTaskState | null /** Omitted when unchanged; null clears a previous provider catalog. */ commands?: AgentSessionSlashCommand[] | null activity?: AgentSessionTurnActivity | null } & AgentSessionHostClockField) | { type: 'end' } // ─── Status feed ──────────────────────────────────────────────────────────── /** What a session list needs to know about one session. The host projects it * from the journal so no client has to replay a transcript to learn whether a * turn is running. Additive surface: an older host has no such method. */ export type AgentSessionStatusSummary = { rewindBlockedReason?: AgentSessionRewindReason sessionId: string workspaceId: string agent: AgentSessionRecord['provider'] /** Null until the journal holds a persisted user or assistant message. */ status: StructuredAgentSessionProjectedStatus | null /** Present only while this host has the provider child executing the session. */ hostExecutionOwned?: true /** With `hostExecutionOwned`: whether that child has proven its start. `starting` is a * published session whose provider has not yet answered startup; absent on older hosts. */ hostExecutionPhase?: 'starting' | 'ready' latestPrompt: string /** Provider model in force for the next turn; absent until the host has read the options. */ model?: string /** The tool the running turn is inside, else the last one it used. Absent unless `status` * is 'working'. */ toolName?: string toolInput?: string /** Preview of the newest assistant prose, so a settled row says what the agent said. */ lastAssistantMessage?: string /** The provider's verdict on the newest settled root turn. Present only while `status` is * `idle`: a running or attention-blocked turn has no verdict yet, and a stale one must not * ride along. Absent means UNKNOWN, never success. Optional for mixed-version hosts; the * agent-status row publishes it as `mainAgent.outcome`. */ turnOutcome?: AgentJournalTurnOutcome /** Live provider-owned background tasks, so session lists can render * subagent children without holding a journal reader open. Optional for * mixed-version hosts. */ backgroundTasks?: AgentSessionBackgroundTask[] providerSession?: AgentProviderSessionMetadata updatedAt: number /** When the session's own agent entered `status`, dated by its own lifecycle edges and never by * row activity: `updatedAt` also moves for a subagent's rows. Absent from older hosts, and when * the journal records no such edge; readers then keep dating the state themselves. */ statusStartedAt?: number } /** A summary outlives its provider child: an evicted idle session is still idle, so the host * keeps the last projection and never retracts one. Tabs, not this feed, decide what is listed. */ export type AgentSessionStatusEvent = | { type: 'snapshot'; sessions: AgentSessionStatusSummary[] } | { type: 'status'; session: AgentSessionStatusSummary } | { type: 'end' } // ─── Turn completion feed ─────────────────────────────────────────────────── /** * One root turn reaching a terminal outcome, derived by the EXECUTION HOST at journal commit. * * This is the EDGE, with turn identity; `AgentSessionStatusSummary.turnOutcome` is the STATE. * The summary carries the verdict only while the session is idle, as a fact about the main agent's * last turn that a status reader may act on (attention alerts, the `mainAgent.outcome` row field), * and never a turn id: a reader that needs to know WHICH turn finished, or to react exactly once * per finish, subscribes here. Re-broadcasting the summary on every status change therefore * repeats a state, not a completion. * * `outcome` is A0's provider verdict and is never inferred — a turn the host only observed ending * carries no outcome and produces no event at all, because absent means UNKNOWN, not success. */ export type AgentSessionTurnCompletion = { /** Host-and-workspace scope; a bare provider turn id is not globally unique. */ scope: AgentSessionExecutionLocation sessionId: string /** Root turn identity from the journal turn record; no second identity is minted. */ turnId: string outcome: AgentJournalTurnOutcome /** Execution host's clock at journal commit. */ completedAt: number } /** * LIVE-ONLY: there is no snapshot arm and no replay arm, by decision. A subscriber is told what * completes while it is subscribed and nothing else; completions that land while it is away are * dropped rather than queued, so nothing durable can strand. On reconnect the client baselines. */ export type AgentSessionTurnCompletionEvent = | { type: 'completion'; completion: AgentSessionTurnCompletion } | { type: 'end' } /** Delivery dedupe address. Unread is idempotent and does not need it; mobile fanout does. */ export function agentSessionTurnCompletionKey(completion: AgentSessionTurnCompletion): string { return [agentSessionScopeKey(completion.scope), completion.sessionId, completion.turnId].join( '\u0000' ) } // ─── Mutation envelope ────────────────────────────────────────────────────── /** * The four fields every mutating call carries. Same operation id and same * fingerprint replays the recorded outcome; a different fingerprint under one * operation id is a conflict, never a second effect. */ export type AgentSessionMutationEnvelope = { sessionId: string clientOperationId: string /** Null only on a create for a session that does not exist yet. */ expectedRuntimeFence: number | null /** Client-declared; the host recomputes it and compares. */ payloadFingerprint: string } export type AgentSessionMutationResult = | { ok: true /** True when the recorded outcome was returned instead of a new effect. */ replayed: boolean fence: number cursor: AgentJournalCursor value: TValue } | { ok: false; refusal: AgentSessionWireRefusal } // ─── Per-method payloads ──────────────────────────────────────────────────── export type AgentSessionAttachResult = { sessionId: string fence: number page: AgentSessionHistoryPage /** Submissions the crash boundary settled as `unknown` while attaching. */ unconfirmedClientMessageIds: string[] /** The host-owned id of the tab showing this chat, when it has one. Absent from older hosts. */ tabId?: string } export type AgentSessionSendResult = { clientMessageId: string submission: AgentJournalSubmission } export type AgentSessionCancelResult = { /** The turn the client named, echoed so a late reply can be matched. */ turnId: string cancelled: boolean } export type AgentSessionPromptResult = { itemId: string revision: number resolution: AgentJournalResolution } export type AgentSessionOptionResult = { key: string value: string /** Full effective next-turn values when the provider reconciled related options. */ options?: Record } export type AgentSessionOptionChoice = { value: string label: string description?: string } export type AgentSessionModelOption = { id: string label: string description?: string isDefault: boolean defaultEffort?: string efforts: AgentSessionOptionChoice[] /** Provider catalog fact. Absent means the host could not determine support. */ supportsFastMode?: boolean } export type AgentSessionFastModeState = 'off' | 'cooldown' | 'on' export type AgentSessionFastModeSupport = { supported: boolean /** Provider-authored or host-normalized reason code; presentation may ignore unknown values. */ reason?: string } /** * The host's model catalog for an agent, answered from its own store and * never through a session's queue. `unknown` means this host has no listing * for the key yet — the client keeps its static seed. Additive read-only * surface: an older host simply lacks the method. */ export type AgentSessionModelCatalogResult = | { origin: 'unknown' } | { /** What produced the listing; any age is served, `fetchedAt` carries it. */ origin: 'live-session' | 'probe' models: AgentSessionModelOption[] fastModeSupport?: AgentSessionFastModeSupport fetchedAt: number } /** One entry of the `/` menu the running provider reports for itself. `skill` * marks a name the session loaded as a skill rather than a built-in command; * commands the provider reserves for a terminal UI are already removed. */ export type AgentSessionSlashCommand = { name: string kind: 'command' | 'skill' /** Membership is authoritative, but this provider report did not classify the name. */ kindUnspecified?: true /** Provider-authored row text; absent when the report carried names only. */ description?: string /** Provider-authored argument sketch, e.g. ``. */ argumentHint?: string } /** The provider's own command surface, read per session. Additive read-only * surface: a host that predates it answers `method_not_found`, and the client * keeps rendering its curated catalog. */ export type AgentSessionCommandsResult = { commands?: AgentSessionSlashCommand[] } /** Longest objective a client may send; matches the provider's own limit. */ export const AGENT_SESSION_THREAD_GOAL_OBJECTIVE_MAX_LENGTH = 4000 /** A client's change to the thread goal. `set` replaces the objective and makes * it active, which the provider pursues without a separate turn. */ export type AgentSessionThreadGoalChange = | { kind: 'set'; objective: string } | { kind: 'status'; status: 'active' | 'paused' } | { kind: 'clear' } export type AgentSessionThreadGoalResult = { change: AgentSessionThreadGoalChange['kind'] } /** Provider-reported choices and effective next-turn values. Additive read-only * surface so older hosts can reject it without changing structured v1 writes. */ export type AgentSessionOptionsResult = { rewind?: AgentSessionRewindSupport conversationCommands?: readonly AgentSessionConversationCommand[] /** Present only where this session can change its goal, so a host without * `agentSession.threadGoal` never offers the controls. `current` is the * latest goal the whole journal records, for a client whose loaded page * starts after it. */ threadGoal?: { current: AgentJournalThreadGoal | null } /** Present only where this session writes context facts to its turn rows. * `current` is the newest of each part the whole journal records, for a * client whose loaded page starts after the row that carries it. */ contextUsage?: { current: AgentSessionContextUsage } models: AgentSessionModelOption[] /** Session/account/transport support. Absent means unknown, never unsupported. */ fastModeSupport?: AgentSessionFastModeSupport current: { model: string effort?: string /** Canonical preference for the next turn. Explicit false is meaningful. */ fastMode?: boolean /** Provider-reported effective routing, distinct from the next-turn preference. */ fastModeState?: AgentSessionFastModeState /** * Option ids whose value the provider reported back, not merely accepted. * Optional: a host that predates it sends nothing and the client keeps * treating the value as unconfirmed, which is what it was before. */ confirmed?: readonly string[] } }