mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 08:02:32 +00:00
* refactor(native-chat): a subagent's rows live with that subagent, not in the conversation
A subagent's rows were drawn in its parent's conversation, each captioned with
the subagent's name. They now belong to the subagent: the transcript projection
keeps the session's own rows as the conversation and each subagent's rows apart,
keyed by the agent id its roster entry already carries, folded on their own.
Desktop: a subagent's rows open in a section under the roster row that names it,
from that agent's roster entry, and are windowed like any other rows. A subagent
no loaded roster names opens where its first row happened, inside the section of
the agent that spawned it or in the conversation. Its edits still count in the
turn they were made, and revealing one opens the sections around it.
Mobile shows the conversation, with each spawn's roster line. Worker reads and
structured terminal reads serve the worker's own rows.
Removes what the move makes redundant: the per-row caption and its copy, the
producer check in the tool fold and the turn answer, the per-agent frontier
interleaved in the conversation, worker-text subagent tags, and the agent id on
worker-read messages.
* refactor(native-chat): a diff target names the sections its row sits in
Revealing a subagent's edit opens the sections around it from the target the
rollup already holds, instead of looking the row up at click time. The section
head keeps to the agent's name and dot; its state in words stays on the roster
entry. The worker page test stubs the host through its module rather than a cast.
* fix(native-chat): a working subagent's section is open; a worker page windows its own rows
A subagent's section is open while its agent works and closes once it settles,
the way the turn's own live run does; a section the reader opened or closed by
hand keeps that choice. A subagent another subagent spawned opens inside that
one's section, so a working grandchild shows inside its working parent. Openness
is derived from the roster's state and the reader's choices; nothing stores an
automatic open.
A worker page is now the newest page of the worker's own rows. The host windows
the read over them before the limit, so a subagent's burst can no longer crowd
the worker's rows off the page, and "older" still means older worker rows. The
scope is an in-process argument of the host's history read; no wire request
carries it.
* fix(native-chat): a subagent section head names the turn it sits in, for the outline rail
* fix(native-chat): a subagent section's rows sit in the turn the section is shown in, for the outline rail
A background subagent's rows written during a later turn carried that later
turn onto their slots, so scrolling through its section lit the later turn's
rail tick and then snapped back. The rollup still counts each edit in the turn
it was made; only the slot, which the rail reads, takes the shown turn.
* fix(mobile): Load earlier reads past pages that hold only a subagent's rows
Mobile draws only the session's own rows, so an older page made entirely of a
subagent's rows landed as nothing: the reader tapped Load earlier, saw the
spinner, and got the same transcript back. One load now reads on (up to 8 pages)
until a page holds a row of the session's own, then applies the pages in order.
* test(mobile): stub the RPC client the way the other structured-session hook tests do
* perf(native-chat): order subagent rows for the changed-files rollup once per change to them
The rollup flattened and re-sorted every subagent row on each update, including
every token the parent streamed. The ordering now keys on the projection's
subagent rows, which keep their identity while only the conversation changes.
* refactor(native-chat): order subagent rows in the sections hook, keeping the list under its line limit
* fix(mobile): a transcript whose newest page is only a subagent's rows reads back on its own
Opened while a subagent is busy, the newest page can hold nothing but that
subagent's rows. Mobile draws none of them, so the reader saw an empty chat with
a Load earlier button, and an empty list cannot be scrolled to page. The hook now
reads back once from each such head, and the read runs on to the session's own rows.
* fix(native-chat): count the live window in the session's own rows, so a subagent's burst keeps its roster
The live window kept the newest 1,024 rows of every agent. A subagent writing
more than that trimmed its own spawn's roster row and the prompt, and its
section fell back to a closed, unnamed header. The window now keeps the newest
1,024 of the session's own rows and everything after, with an 8,192-row cap on
every agent's rows as the memory backstop. A transcript with no subagent rows
trims exactly as before.
* fix(agent-session): window history pages by the session's own rows, with a subagent's rows riding along
A history page held the newest 200 rows of every agent, so a subagent's burst
could fill a page on its own: the phone opened on an empty chat and "Load
earlier" landed nothing. A page now starts at the oldest of the newest `limit`
rows of the session's own and serves every row from there, so the subagent's
rows come with the conversation they happened in. The page stays contiguous,
the cursor still names its first row, and the byte bound still applies. A
transcript with no subagent rows gets the same pages as before.
Clients already take a page larger than its limit: both reducers raise their
retained window to the page's size. The mobile read-on and read-back stay for
older hosts.
* test(agent-session): a page reaches back to the start rather than leaving a subagent-only page
* fix(native-chat): an own-row trim takes a trimmed roster's subagent rows with it
The live window trimmed to just after the own row it dropped, so a subagent
whose roster row went kept its rows at the top as an unnamed section until
the parent wrote again. Trim to the oldest own row kept instead; it still
fires only once an own row passes the limit, so a paged-in run of subagent
rows at the head stays until then. With no subagent rows nothing changes.
* perf(native-chat): cap the live window at 4,096 rows, bounding each delta's re-derivation
Every live batch re-derives the transcript over every retained row. On the
largest real window (7,374 rows) that cost 7-8 ms a delta on desktop against
0.6 ms at the old 1,024-row window, and held about 26 MB of row content.
4,096 halves both. The most rows any local journal puts between a roster and
its subagent's last row, with the parent inside its own-row limit, is 3,005,
so no observed subagent loses its roster to the lower cap.
* fix(native-chat): a subagent section opens only while its roster is the running scope's live frontier
A section used to open whenever its roster said the subagent was working, anywhere
in the transcript and whether or not the session was running, so a background
subagent's section stayed open and grew mid-transcript while the parent moved on.
It now opens by default only while the session runs and the roster row naming the
subagent is the newest thing the parent produced, user rows aside. Newer parent
output closes it even while the subagent still works; the roster row keeps
showing that live state. A subagent still working is a running scope of its own
for the sections it spawned; a settled one closes its scope. Derived every
render, no latch; the reader's own open or close still wins.
* fix(native-chat): name a subagent's section from a client roster the window never trims
A section took its name and state from a roster row in the loaded window. Once a
burst trimmed that row, or the row sat on an older page, the section fell back to
an unnamed, closed "Subagent" header.
The shared reducer now keeps a roster keyed by agent id, folded from every roster
row and revision the client receives: pages, older pages and live batches,
including revisions of roster rows outside the window, which live batches already
carry. The first roster naming an agent wins and its revisions update it; a
removed roster row drops its entries; it is rebuilt on every page that replaces
the window and bounded to 512 agents. Sections take their name, state and
live-frontier place from it; placement stays under the loaded roster row, else
at the section's first loaded row. Only a subagent no roster ever named stays
unnamed.
* feat(agent-session): a history page names the subagents whose roster row is older than it
A page is a contiguous run of the journal whose older-page cursor is its first
item, so it cannot pull an older roster row in without skipping the rows between.
When a page held a subagent's rows but not the roster row naming it (about 11% of
the moments a reader could open a session on local journals), that subagent drew
as an unnamed "Subagent" header.
History and hydration pages now carry an optional `subagentRoster`: the first
roster entry naming each subagent whose rows are on the page and whose roster row
is not, with the row's id, sequence and revision; bounded to 64 entries and
16 KB. Items and cursor are unchanged. The client seeds its roster from it.
Rule 1 in docs/reference/remote-wire-compatibility.md: an optional field on an
existing frame, no capability gate. An older client ignores it (the released
reducer reads a page with it exactly as one without); against an older host the
field is absent and the section falls back to an unnamed header.
* Revert "fix(native-chat): an own-row trim takes a trimmed roster's subagent rows with it"
This reverts commit 22078656b2.
Its only purpose was to stop a subagent whose roster row an own-row trim had
dropped from showing at the head of the window as an unnamed section. The client
roster now names that section whatever the window holds, so the cut is back at
just after the own row the limit passes. The retention test that pinned the
unnamed-section case now asserts the section at the head keeps its name.
* chore(native-chat): state the retention limits' own reasons, now that no name depends on the window
Own-row retention keeps the conversation a reader sees from being crowded out by
rows drawn as a one-row section on desktop and not at all on mobile; the
every-agent cap bounds memory and each live delta's re-derivation. Neither is
about keeping a roster row loaded any more.
* fix(native-chat): hold the roster fold's draft map where type narrowing can see closure writes
* fix(native-chat): a roster row's newer revision replaces it in the client roster too
A revision that stops naming an agent (the host drops an entry it learns is not a
subagent, or re-keys a provisional one) left the client roster holding the old
entry, often still "working", with nothing to re-derive it. The section then read
as working forever and could auto-open, while a fresh read of the same journal
left it unnamed. The fold now drops an entry when a newer revision of the row that
named it no longer does, before any roster takes it over.
* fix(native-chat): a parent's spawn and wait calls keep the subagent they name open
A subagent section auto-opened only while its roster row was the running session's
newest row, so any later row closed it: a Codex wait on the agent, or the parent's
text before its next spawn call. Now a row that is part of delegating to a subagent
keeps that subagent open:
- a Codex collab call (spawn, wait, resume, message, close) opens each agent its
receiver thread ids name; one naming none is ordinary output;
- a Claude spawn call names no agent, so it counts toward the roster announcing it;
- a roster row at the frontier opens its most recently added agent, not all of them.
A roster or call naming only agents one subagent spawned is that subagent's output,
so a grandchild's roster, which the host journals as the session's row, no longer
closes the spawner's section.
* fix(native-chat): a parent's call right after the roster closes its subagent's section
A parent's tool calls after a roster row fold into the tool run drawn above
the roster, so the roster stayed the newest drawn row and its section stayed
open while the parent was already reading or running commands. The fold now
records the newest journal position among the rows it merged, and the live
frontier orders rows by that newest part. The layout is unchanged. A spawn
call folded there still counts as part of the roster announcing it.
* fix(native-chat): a Codex call naming several subagents delegates to the first
A Codex collab call that names several agents opened every one of their
sections. It now counts as delegating to the first agent it names, so one
section opens, the same as a call naming one agent.
* fix(native-chat): closing a roster's list closes the sections under it
Collapsing a roster row's list of subagents left their open sections drawn,
so the section's own head became the only way to close them. And the list's
open state lived in the row, so a row the window unmounted came back
collapsed.
The transcript now holds each roster list's open state beside the section
choices. A closed list hides every section it anchors; each section keeps
its own open or closed choice for when the list reopens. With no choice from
the reader, a list is open while a section under it is open. Closing a
section from its entry keeps the list open, and revealing a subagent's edit
opens the list it sits under.
* perf(native-chat): a reveal finds the roster lists it opens with one set lookup per entry
* fix(native-chat): a subagent's roster entry heads its own rows
An open section drew the agent's name twice: its entry in the roster's list,
then a separate section head above its rows. The entry is now the head. The
roster row draws its entries through the first open one, that agent's rows
follow, then the entries after it, each run in its own windowed slot. A
section no loaded roster row holds (an older page, a grandchild, an unnamed
agent) keeps its own head.
A roster list is open while the live frontier or a reader's choice is on
one of its agents, unless the reader closed the list, so closing an agent
from its entry no longer needs to pin the list open.
The section emitter moves to its own module, and the trailing-run
predicates it shares with the slot builder to theirs, to keep the slot
builder under its line limit.
* fix(native-chat): the entries after an open subagent's rows set in its roster's type
The roster row's list inherits the system row's small muted type; the entries that
follow an open section sit outside that row, so they now carry the same type.
* docs(native-chat): a current host can also serve a page of only a subagent's rows
A page is bounded by bytes after it is windowed by the session's own rows, so a
burst that fills the bound yields a page, or an opening page, with none of the
session's own rows. Mobile's read-on and read-back therefore serve current hosts
too, not only older ones; the comments said otherwise. The retention comment
still described a closed section as a row of its own; it now sits behind its
roster entry.
* fix(native-chat): a section's prose keeps its copy/timestamp controls inside the section
An assistant row's hover controls (copy, scroll-to-top, timestamp) hang 20px
below the row into the gap before the next one (`-mb-5`). Inside a subagent's
section that put them below the section's left border, and on the section's
last row they touched the parent's next row with no gap.
Inside a section the controls now stay in flow, so the border covers them and
the next row sits the normal gap below. The row-height estimate reserves the
same 20px for a section's prose so windowing does not jump on measure.
* test(agent-session): state each appended row's turn scope, as the journal now requires
* refactor(native-chat): the client's journal retention policy lives in its own module
502 lines
22 KiB
TypeScript
502 lines
22 KiB
TypeScript
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'
|
|
import type {
|
|
AgentSessionQueuedMessage,
|
|
AgentSessionQueuePause
|
|
} from './agent-session-queued-message-wire'
|
|
|
|
export * from './agent-session-wire-refusals'
|
|
export * from './agent-session-queued-message-wire'
|
|
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 { NativeChatSubagentEntry } from './native-chat-types'
|
|
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
|
|
}
|
|
|
|
/** A subagent named by a roster row the page does not carry, while its own rows are on it. */
|
|
export type AgentSessionSubagentRosterEntry = {
|
|
/** The roster row naming it: the first that does. */
|
|
itemId: string
|
|
sequence: number
|
|
sequenceIndex?: number
|
|
revision: number
|
|
entry: NativeChatSubagentEntry
|
|
}
|
|
|
|
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
|
|
/** The host's queued drafts. Absent = no claim (older host); `[]`/null = empty.
|
|
* Live subscription state stays authoritative over a stale history answer. */
|
|
queuedMessages?: AgentSessionQueuedMessage[] | null
|
|
/** The queue's pause, published with the list: present whenever `queuedMessages` is, null
|
|
* when the queue sends on its own. */
|
|
queuePause?: AgentSessionQueuePause | 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
|
|
/** Names the subagents with rows on the page whose roster row is older than it; bounded.
|
|
* Absent from older hosts, and when every such roster row is on the page. */
|
|
subagentRoster?: AgentSessionSubagentRosterEntry[]
|
|
}
|
|
|
|
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
|
|
/** Whole-list draft publication; omitted when unchanged since the last frame sent. */
|
|
queuedMessages?: AgentSessionQueuedMessage[] | null
|
|
/** Rides with `queuedMessages`; null when the queue sends on its own. */
|
|
queuePause?: AgentSessionQueuePause | 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
|
|
/** Whole-list draft publication. On a multi-page catch-up it rides only the
|
|
* final page, so a consumed card never vanishes before its bubble arrives. */
|
|
queuedMessages?: AgentSessionQueuedMessage[] | null
|
|
/** Rides with `queuedMessages`; null when the queue sends on its own. */
|
|
queuePause?: AgentSessionQueuePause | 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
|
|
/** Whole-list draft publication; a reset re-hydrates it with the page. */
|
|
queuedMessages?: AgentSessionQueuedMessage[] | null
|
|
/** Rides with `queuedMessages`; null when the queue sends on its own. */
|
|
queuePause?: AgentSessionQueuePause | 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'
|
|
/** The current provider child, distinct from the conversation and from replacement children.
|
|
* Absent on older hosts and whenever this host has no live child. */
|
|
hostExecutionChild?: { generation: string | null; fence: number }
|
|
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 ───────────────────────────────────────────────────
|
|
|
|
/**
|
|
* The session's latest request reaching a terminal outcome — a root turn, or a send the agent or
|
|
* its start refused — 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
|
|
/** The request's identity: the root turn's id, or for a send refused before any turn, that
|
|
* send's journal item key. Neither is minted here. */
|
|
turnId: string
|
|
outcome: AgentJournalTurnOutcome
|
|
/** Execution host's clock at journal commit. */
|
|
completedAt: number
|
|
/** The request settled while a prompt waits on the user. Absent otherwise, and from older hosts. */
|
|
awaitingUser?: true
|
|
}
|
|
|
|
/**
|
|
* 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<TValue> =
|
|
| {
|
|
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 a crash boundary left `unknown` that provider history could not decide. */
|
|
unconfirmedClientMessageIds: string[]
|
|
/** The host-owned id of the tab showing this chat, when it has one. Absent from older hosts. */
|
|
tabId?: string
|
|
}
|
|
|
|
/** The host queued the send as a draft instead of submitting it. Only clients
|
|
* that sent `delivery: 'queue-if-active'` — gated on
|
|
* `agent-session.queued-messages.v1` — ever receive this arm; `state` other
|
|
* than `waiting` appears only on replays of an already-settled draft. */
|
|
export type AgentSessionQueuedSendReceipt = {
|
|
messageId: string
|
|
position: number
|
|
state: 'waiting' | 'dispatched' | 'returned' | 'withdrawn'
|
|
}
|
|
|
|
export type AgentSessionSendResult =
|
|
| {
|
|
clientMessageId: string
|
|
submission: AgentJournalSubmission
|
|
}
|
|
| { clientMessageId: string; queued: AgentSessionQueuedSendReceipt }
|
|
|
|
/** The submission arm's payload; undefined for a queued answer. For callers that
|
|
* never send `delivery` the queued arm cannot arrive, and `undefined` reads as
|
|
* delivery-unknown rather than as an error. */
|
|
export function agentSessionSendSubmission(
|
|
result: AgentSessionSendResult | undefined
|
|
): AgentJournalSubmission | undefined {
|
|
return result !== undefined && 'submission' in result ? result.submission : undefined
|
|
}
|
|
|
|
export type AgentSessionCancelResult = {
|
|
/** The turn the client named, echoed so a late reply can be matched; absent when it named none. */
|
|
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<string, string>
|
|
}
|
|
|
|
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. `<issue-url>`. */
|
|
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[]
|
|
}
|
|
}
|