@silo-code/sdk 0.46.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import type { Disposable } from "./types";
2
- import type { TerminalKind } from "./domain-types";
2
+ import type { TabActivityBinder, TabIconBinder } from "./tab-adornment";
3
3
 
4
4
  // `ctx.agents` — host-computed coding-agent activity and resume-identity
5
5
  // observability. See RFC 0018 (docs/proposals/0018-ctx-agents-surface.md).
@@ -8,8 +8,9 @@ import type { TerminalKind } from "./domain-types";
8
8
  // ever read this surface.
9
9
 
10
10
  /**
11
- * What a terminal's agent is currently doing, as classified by the host from
12
- * OSC/output signals. `"none"` means no agent activity has been observed
11
+ * What an agent is currently doing — classified by the host from OSC/output
12
+ * signals for a terminal-kind session, or reported directly by the protocol
13
+ * for a chat-kind one. `"none"` means no agent activity has been observed
13
14
  * (including plain, non-agent shells). `"idle"` means the agent finished its
14
15
  * last turn and is waiting for the next input — this is purely a fact about
15
16
  * the agent itself, independent of whether anyone is looking at the
@@ -22,17 +23,53 @@ import type { TerminalKind } from "./domain-types";
22
23
  * gone (no daemon to reattach to) after an unclean shutdown; nothing will
23
24
  * arrive to resolve this on its own.
24
25
  *
26
+ * `"blocked"` (chat-kind only) is a turn suspended on
27
+ * {@link AgentSessionHandle.onPermission} — a concrete, protocol-reported
28
+ * question, not the inferred "no output for a while" guess `"idle"` is. It
29
+ * does not repeat the earlier `"waiting"` mistake: unlike a merely-idle
30
+ * agent, it stays `"blocked"` even while its own session is the one on
31
+ * screen, since "the agent needs you to answer something" is worth showing
32
+ * regardless of whether you are already looking — where `needsAttention`
33
+ * exists to be suppressed exactly then.
34
+ *
35
+ * @category Core Types
36
+ * @public
37
+ * @beta
38
+ */
39
+ export type AgentActivity =
40
+ | "none"
41
+ | "working"
42
+ | "blocked"
43
+ | "idle"
44
+ | "error"
45
+ | "dead";
46
+
47
+ /**
48
+ * Which kind of **Agent Session** an {@link AgentInfo} describes (RFC 0038):
49
+ *
50
+ * - `"terminal"` — the agent runs in a PTY and draws its own TUI; Silo
51
+ * *infers* what it is doing from OSC/output signals, and its identity comes
52
+ * from detection (ADR 0028).
53
+ * - `"chat"` — the agent is an Agent Client Protocol child speaking structured
54
+ * JSON-RPC over piped stdio; it *reports* what it is doing and declares its
55
+ * identity at `initialize`, and Silo renders the conversation.
56
+ *
57
+ * `ctx.agents` reports **one shape regardless of kind** — the promise is
58
+ * observation parity, not capability parity. Most consumers never need to
59
+ * branch on this; use {@link AgentsService.reveal} to bring a session into
60
+ * view without knowing which kind it is.
61
+ *
25
62
  * @category Core Types
26
63
  * @public
27
64
  * @beta
28
65
  */
29
- export type AgentActivity = "none" | "working" | "idle" | "error" | "dead";
66
+ export type AgentSessionKind = "terminal" | "chat";
30
67
 
31
68
  /**
32
- * Live agent-activity and resume-identity state for one terminal, computed
33
- * once by the host and shared across every subscriber — never recomputed
34
- * per-extension. Returned by {@link AgentsService.getState} and
35
- * {@link AgentsService.getByTerminalId}; delivered to
69
+ * Live agent-activity and resume-identity state for one **Agent Session**
70
+ * (RFC 0038), computed once by the host and shared across every subscriber —
71
+ * never recomputed per-extension. Returned by {@link AgentsService.getState}
72
+ * and {@link AgentsService.getByTerminalId}; delivered to
36
73
  * {@link AgentsService.subscribe} listeners on every change.
37
74
  *
38
75
  * @category Consumer Services
@@ -40,20 +77,62 @@ export type AgentActivity = "none" | "working" | "idle" | "error" | "dead";
40
77
  * @beta
41
78
  */
42
79
  export interface AgentInfo {
43
- /** The terminal record id this state belongs to. */
44
- readonly terminalId: string;
45
- /** The workspace this terminal belongs to. */
80
+ /**
81
+ * Stable id for this Agent Session — the key {@link AgentsService.reveal},
82
+ * {@link AgentsService.resume}, and {@link AgentsService.acknowledge} take.
83
+ * For a Terminal session this is the terminal record id; for a Chat session
84
+ * it is the session handle's id. Prefer this over
85
+ * {@link AgentInfo.terminalId} for anything that should not care which kind
86
+ * of session it is.
87
+ */
88
+ readonly id: string;
89
+ /**
90
+ * The terminal record id backing this session, when there is one — the same
91
+ * id {@link AgentsService.getByTerminalId} and `ctx.terminals` take.
92
+ * Present for every `kind: "terminal"` session; absent for a `kind: "chat"`
93
+ * session, which has no PTY. A consumer that needs a terminal-tab id should
94
+ * check {@link AgentInfo.kind} first, or use {@link AgentsService.reveal}.
95
+ */
96
+ readonly terminalId?: string;
97
+ /** The workspace this session belongs to. */
46
98
  readonly workspaceId: string;
47
99
  /**
48
- * The terminal's kind at registration time.
100
+ * The session's display label — host-computed, the same string for either
101
+ * kind, and the one a consumer should render. A status row, a navigator row
102
+ * and the session's own dock tab all showing this field is what keeps them
103
+ * from drifting.
104
+ *
105
+ * It is a three-step fallback, and the two kinds are exact parallels:
106
+ *
107
+ * | step | Terminal | Chat |
108
+ * | ------------------ | ----------------------------------- | --------------------------------- |
109
+ * | the agent's words | the OSC window title | `session_info_update.title` |
110
+ * | the user's name | `TerminalRecord.customName` | *(no rename gesture yet)* |
111
+ * | fallback | the terminal's derived name | declared agent name → profile label |
112
+ *
113
+ * The user's name wins where they set one; otherwise the agent's own words
114
+ * win, with agent **status markers stripped** (Claude's spinner, Cursor's
115
+ * trailing ` - Working …`) — the status is already structured state on this
116
+ * same record, so repeating it in the label is noise.
49
117
  *
50
- * @deprecated Always `"shell"` after RFC 0033 — the deprecated `"claude"` /
51
- * `"pi"` kinds are normalized away at load and nothing creates them, so a
52
- * consumer branching on this is reading a constant. Use
53
- * {@link AgentInfo.agentId} for which agent is running, and
54
- * {@link AgentInfo.isAgent} for whether one is.
118
+ * Not every agent volunteers a title, and whether a given one does is its
119
+ * own choice — and its adapter's, version to version. A session that never
120
+ * gets one sits on the fallback, exactly as a terminal tab does for a CLI
121
+ * that writes no OSC title. Silo does not paper over that by inventing a
122
+ * summary from the first prompt.
55
123
  */
56
- readonly kind: TerminalKind;
124
+ readonly title: string;
125
+ /**
126
+ * Which kind of Agent Session this is — `"terminal"` or `"chat"`. See
127
+ * {@link AgentSessionKind}. `ctx.agents` reports the same fields for both;
128
+ * this exists for the rare consumer that genuinely needs a PTY tab id or a
129
+ * transcript panel.
130
+ *
131
+ * (This field was the vestigial `TerminalKind` before RFC 0038 — always
132
+ * `"shell"` and read by nothing — and carries the session discriminator
133
+ * now.)
134
+ */
135
+ readonly kind: AgentSessionKind;
57
136
  /**
58
137
  * Whether this terminal currently hosts an agent — true if it was created
59
138
  * as one, or an agent-specific signal was observed in it (e.g. typing
@@ -102,6 +181,27 @@ export interface AgentInfo {
102
181
  * both live and at `activity === "dead"`.
103
182
  */
104
183
  readonly resumeCommand?: string;
184
+ /**
185
+ * Whether this session can be resumed **through Silo** — i.e. whether
186
+ * {@link AgentsService.resume} will do something for this id.
187
+ *
188
+ * - Terminal session: `true` once an exact session id was resolved (a
189
+ * Settings → Agents hook, or an agent's native session file), which is
190
+ * also when {@link AgentInfo.resumeCommand} becomes exact rather than an
191
+ * honest note. `resume()` is still a no-op for a Terminal session in this
192
+ * release — the user runs `resumeCommand` themselves; the flag is the
193
+ * forward-looking capability signal RFC 0038 replaces the shell-string
194
+ * `resumeCommand` contract with.
195
+ * - Chat session: `true` when the agent advertises `session/load`, so a
196
+ * fresh process can reload the transcript after the old one died.
197
+ *
198
+ * - Chat session: `true` when the agent advertises **either**
199
+ * `sessionCapabilities.resume` or `agentCapabilities.loadSession` (RFC
200
+ * 0042) — the two are distinct capabilities (see {@link ChatResumeState})
201
+ * but either is enough for {@link AgentsService.resume} to bring the
202
+ * conversation back.
203
+ */
204
+ readonly canResume: boolean;
105
205
  /**
106
206
  * Human-readable agent name, e.g. `"Claude Code"` or `"Codex CLI"`. Tells
107
207
  * you *which* agent CLI is running in this terminal, independent of
@@ -119,8 +219,48 @@ export interface AgentInfo {
119
219
  * the same moment and lifecycle as `agentName`.
120
220
  */
121
221
  readonly agentId?: string;
222
+ /**
223
+ * For a `kind: "chat"` session, where it stands in **Chat session
224
+ * resurrection** (RFC 0042) — absent for a `kind: "terminal"` session, which
225
+ * has no such lifecycle. See {@link ChatResumeState}.
226
+ */
227
+ readonly chatResumeState?: ChatResumeState;
122
228
  }
123
229
 
230
+ /**
231
+ * Where a **Chat** Agent Session stands in Chat session resurrection (RFC
232
+ * 0042) — bringing its transcript, and the ability to keep talking, back after
233
+ * its process died with the app.
234
+ *
235
+ * - `"dormant"` — known only from persistence: the conversation exists and its
236
+ * panel is recorded, but nothing has connected to it this run and no agent
237
+ * process is alive. This is what a Chat session in a workspace you have not
238
+ * visited since launching looks like — listing it is the point, and opening
239
+ * it is what starts the reconnect. Its `activity` is always `"idle"`.
240
+ * - `"live"` — an ordinary, uninterrupted connection (a plain `session/new`
241
+ * this run; never went through the resume machinery).
242
+ * - `"resuming"` — a restore is in flight: `session/resume` or `session/load`
243
+ * is being probed and called.
244
+ * - `"resumed"` — reconnected: `session/resume` (no replay, the transcript
245
+ * comes from the **transcript journal**) or `session/load` (replay,
246
+ * reconciled against the journal) succeeded.
247
+ * - `"journal-only"` — the agent could do neither; the transcript is the
248
+ * journal alone and the session cannot be prompted in place — the panel's
249
+ * own "Continue in a new session" action is the way forward.
250
+ * - `"unavailable"` — no session id and no journal; nothing to show.
251
+ *
252
+ * @category Consumer Services
253
+ * @public
254
+ * @beta
255
+ */
256
+ export type ChatResumeState =
257
+ | "dormant"
258
+ | "live"
259
+ | "resuming"
260
+ | "resumed"
261
+ | "journal-only"
262
+ | "unavailable";
263
+
124
264
  /**
125
265
  * A Catalog Agent's brand mark — SVG path data plus the two theme-dependent
126
266
  * hexes. Consumed by {@link AgentIconGlyph}; a single hex cannot have enough
@@ -201,6 +341,22 @@ export interface AgentProfileSummary {
201
341
  * `"agent-takes-none"` after the user has already typed one.
202
342
  */
203
343
  readonly acceptsPrompt: boolean;
344
+ /**
345
+ * Which **interface** this profile starts the agent through — the profile's
346
+ * `launch` arm, expressed in the same {@link AgentSessionKind} vocabulary
347
+ * `AgentInfo.kind` uses (RFC 0038):
348
+ *
349
+ * - `"terminal"` — {@link AgentProfilesService.launch} runs it in a PTY and
350
+ * the agent draws its own TUI.
351
+ * - `"chat"` — {@link AgentSessionsService.connect} speaks the Agent Client
352
+ * Protocol to it and *you* render the conversation.
353
+ *
354
+ * The two are driven through different services, so a picker must filter on
355
+ * this rather than offer every profile to both: `launch()`ing a Chat profile
356
+ * and `connect()`ing a Terminal profile both fail, and the user should never
357
+ * be offered a profile that cannot work in the surface they are looking at.
358
+ */
359
+ readonly interface: AgentSessionKind;
204
360
  }
205
361
 
206
362
  /**
@@ -353,6 +509,844 @@ export interface AgentProfilesService {
353
509
  launch(options?: LaunchAgentProfileOptions): LaunchAgentProfileResult;
354
510
  }
355
511
 
512
+ /**
513
+ * One block of an outgoing prompt turn for a **Chat session**
514
+ * ({@link AgentSessionHandle.prompt}). Structured content, never a shell
515
+ * string — the entire quoting/escaping risk surface of a Terminal session's
516
+ * opening prompt (RFC 0033 phase 3) does not exist here.
517
+ *
518
+ * - `"text"` — a run of plain text. `$HOME`, backticks, quotes and newlines
519
+ * are all literal.
520
+ * - `"resource_link"` — a pointer to a file (or other URI) the agent may read
521
+ * if it chooses. `uri` is typically a `file://` path inside the workspace.
522
+ * Always accepted — this is a pointer, not embedded content, so it needs no
523
+ * {@link AgentPromptCapabilities} gate.
524
+ * - `"resource"` — the file's own text **inlined** into the prompt, rather
525
+ * than a pointer the agent may or may not follow. Only send this to a
526
+ * session whose {@link AgentSessionHandle.promptCapabilities} has
527
+ * `embeddedContext: true`.
528
+ *
529
+ * Images are deferred — no probe has yet checked what each agent actually
530
+ * accepts for one, and this union grows behind that once one has (RFC 0040
531
+ * open question 3).
532
+ *
533
+ * @category Consumer Services
534
+ * @public
535
+ * @beta
536
+ */
537
+ export type AgentPromptBlock =
538
+ | { readonly type: "text"; readonly text: string }
539
+ | {
540
+ readonly type: "resource_link";
541
+ readonly uri: string;
542
+ /** A short display name for the link; defaults to the last path segment. */
543
+ readonly name?: string;
544
+ }
545
+ | {
546
+ readonly type: "resource";
547
+ /** The resource's own URI, e.g. a `file://` path. */
548
+ readonly uri: string;
549
+ readonly mimeType?: string;
550
+ /** The embedded text content. Binary (blob) resources are not modelled —
551
+ * nothing in Silo has needed to embed one yet. */
552
+ readonly text: string;
553
+ };
554
+
555
+ /**
556
+ * Why a prompt turn ended, straight from the agent's Agent Client Protocol
557
+ * stop reason. `"end_turn"` is the normal completion; `"max_tokens"` /
558
+ * `"max_turn_requests"` are budget cutoffs; `"refusal"` means the agent
559
+ * declined the request; `"cancelled"` follows {@link AgentSessionHandle.cancel}.
560
+ *
561
+ * @category Consumer Services
562
+ * @public
563
+ * @beta
564
+ */
565
+ export type AgentStopReason =
566
+ | "end_turn"
567
+ | "max_tokens"
568
+ | "max_turn_requests"
569
+ | "refusal"
570
+ | "cancelled";
571
+
572
+ /**
573
+ * What {@link AgentSessionHandle.prompt} resolves to. A prompt that cannot be
574
+ * delivered — the connection dropped, the agent errored mid-turn — **rejects**
575
+ * instead, with the agent's own message where there is one.
576
+ *
577
+ * @category Consumer Services
578
+ * @public
579
+ * @beta
580
+ */
581
+ export interface AgentPromptResult {
582
+ readonly stopReason: AgentStopReason;
583
+ }
584
+
585
+ /**
586
+ * One piece of agent-authored content — the Agent Client Protocol content
587
+ * block, which appears both in a streamed message chunk
588
+ * ({@link AgentSessionUpdate.content}) and inside a tool call's output
589
+ * ({@link AgentToolCallContent.content}).
590
+ *
591
+ * **`type` is deliberately a `string`, not a union.** The protocol names
592
+ * `"text"`, `"image"`, `"audio"`, `"resource_link"` and `"resource"`, and a
593
+ * vendor may add its own; the same tolerance rule the rest of this surface
594
+ * follows applies — render the types you know and **name, rather than drop**,
595
+ * one you do not, so a transcript never silently loses a block.
596
+ *
597
+ * Every field but `type` is optional because which ones arrive depends on the
598
+ * type _and_ the agent: only `"text"` blocks were seen from Cursor
599
+ * (2026.09.02) and `claude-agent-acp` (0.75.1) in the 2026-09-08 probe, so
600
+ * treat the rest as modelled-but-unverified and check before you read.
601
+ *
602
+ * @category Consumer Services
603
+ * @public
604
+ * @beta
605
+ */
606
+ export interface AgentContentBlock {
607
+ /** The protocol's block type — `"text"`, `"image"`, `"audio"`,
608
+ * `"resource_link"`, `"resource"`, or a vendor's own. */
609
+ readonly type: string;
610
+ /** The text, for a `"text"` block. */
611
+ readonly text?: string;
612
+ /** MIME type, for an `"image"` / `"audio"` / `"resource_link"` block. */
613
+ readonly mimeType?: string;
614
+ /** Where the content lives, for a `"resource_link"` (or an image given by
615
+ * reference rather than inline). Typically a `file://` URI. */
616
+ readonly uri?: string;
617
+ /** A short display name, for a `"resource_link"`. */
618
+ readonly name?: string;
619
+ }
620
+
621
+ /**
622
+ * One block of a tool call's content — what the agent wants shown _inside_ the
623
+ * tool row. The protocol wraps each in a `{ type }` envelope, and the three it
624
+ * names are `"content"` (an {@link AgentContentBlock}), `"diff"` (a file edit)
625
+ * and `"terminal"` (output streaming into an agent-side terminal).
626
+ *
627
+ * `type` is a `string` for the usual reason: tolerate what you do not know.
628
+ * The remaining fields are the union's arms flattened, so check `type` before
629
+ * reading them.
630
+ *
631
+ * @category Consumer Services
632
+ * @public
633
+ * @beta
634
+ */
635
+ export interface AgentToolCallContent {
636
+ /** `"content"`, `"diff"`, `"terminal"`, or a vendor's own. */
637
+ readonly type: string;
638
+ /** The content block, when `type` is `"content"`. */
639
+ readonly content?: AgentContentBlock;
640
+ /** The file being edited, when `type` is `"diff"`. */
641
+ readonly path?: string;
642
+ /** The file's contents before the edit, when `type` is `"diff"`. Absent for
643
+ * a file the agent is creating — `claude-agent-acp` 0.75.1 sends `null`
644
+ * there, which arrives here as `undefined`. */
645
+ readonly oldText?: string;
646
+ /** The file's contents after the edit, when `type` is `"diff"`. */
647
+ readonly newText?: string;
648
+ /** The agent-side terminal this call is streaming into, when `type` is
649
+ * `"terminal"`. Silo declines `terminal/*` today, so a consumer can name the
650
+ * block but cannot read its output. */
651
+ readonly terminalId?: string;
652
+ }
653
+
654
+ /**
655
+ * A file (and optionally a line) a tool call touches — the protocol's
656
+ * `locations`, which is how an agent says "this call is about _this_ file"
657
+ * independently of its title. A Chat UI can turn these into "open the file"
658
+ * affordances.
659
+ *
660
+ * Both agents probed on 2026-09-08 send it; only `claude-agent-acp` 0.75.1 was
661
+ * seen sending `line`.
662
+ *
663
+ * @category Consumer Services
664
+ * @public
665
+ * @beta
666
+ */
667
+ export interface AgentToolCallLocation {
668
+ /** Absolute path to the file. */
669
+ readonly path: string;
670
+ /** 1-based line the call is focused on, when the agent named one. */
671
+ readonly line?: number;
672
+ }
673
+
674
+ /**
675
+ * One tool call an agent is making, carried by a `tool_call` (the call opening)
676
+ * or a `tool_call_update` (a change to one already open) — see
677
+ * {@link AgentSessionUpdate.toolCall}.
678
+ *
679
+ * **Both kinds map to this same type, so only {@link toolCallId} is
680
+ * guaranteed.** A `tool_call` typically carries `title`, `kind` and `status`;
681
+ * a `tool_call_update` carries **only the fields that changed** (in the
682
+ * 2026-09-08 probe, most updates were `{ toolCallId, status }` alone). So a
683
+ * consumer keys rows by {@link toolCallId} and patches the fields that are
684
+ * present — never overwrite a title with `undefined`.
685
+ *
686
+ * A `tool_call_update` for a call whose opening `tool_call` never arrived is a
687
+ * real shape; render it rather than dropping it.
688
+ *
689
+ * @category Consumer Services
690
+ * @public
691
+ * @beta
692
+ */
693
+ export interface AgentToolCall {
694
+ /** The call's id — stable across its `tool_call` and every
695
+ * `tool_call_update`, and the same id an
696
+ * {@link AgentPermissionRequest.toolCallId} refers to. */
697
+ readonly toolCallId: string;
698
+ /** Human-readable label, e.g. `"Read notes.md"`. Present on the opening
699
+ * `tool_call`, and on an update only when it changed — agents do relabel a
700
+ * call as it progresses. */
701
+ readonly title?: string;
702
+ /** The protocol's coarse category: `"read"`, `"edit"`, `"delete"`,
703
+ * `"move"`, `"search"`, `"execute"`, `"think"`, `"fetch"`,
704
+ * `"switch_mode"`, `"other"`, or a vendor's own. Tolerate unknown values;
705
+ * do not switch exhaustively. */
706
+ readonly kind?: string;
707
+ /** `"pending"`, `"in_progress"`, `"completed"`, `"failed"`, or a vendor's
708
+ * own. Not every agent walks the whole ladder — `claude-agent-acp` 0.75.1
709
+ * went `pending` → `completed` with no `in_progress`. */
710
+ readonly status?: string;
711
+ /** What to show inside the row. Absent on an update that changed something
712
+ * else — treat that as "unchanged", not "now empty". */
713
+ readonly content?: readonly AgentToolCallContent[];
714
+ /** The files this call touches. */
715
+ readonly locations?: readonly AgentToolCallLocation[];
716
+ /** The arguments the agent passed to its own tool, exactly as it sent them.
717
+ * **Vendor-shaped by definition** — the protocol places no schema on it, so
718
+ * it is typed `unknown`: narrow it yourself, and expect a different shape
719
+ * from each agent. */
720
+ readonly rawInput?: unknown;
721
+ /** The tool's own result, same caveat as {@link rawInput}. */
722
+ readonly rawOutput?: unknown;
723
+ }
724
+
725
+ /**
726
+ * One row of the agent's plan — the protocol's `plan` update entry.
727
+ *
728
+ * @category Consumer Services
729
+ * @public
730
+ * @beta
731
+ */
732
+ export interface AgentPlanEntry {
733
+ /** What the step is, in the agent's words. */
734
+ readonly content: string;
735
+ /** `"pending"`, `"in_progress"`, `"completed"`, or a vendor's own. */
736
+ readonly status?: string;
737
+ /** `"high"`, `"medium"`, `"low"`, or a vendor's own, when the agent ranked
738
+ * the step. */
739
+ readonly priority?: string;
740
+ }
741
+
742
+ /**
743
+ * One `session/update` notification from a **Chat session**, lightly
744
+ * normalized. The Agent Client Protocol streams a turn as a sequence of these:
745
+ * assistant text, agent "thinking", tool-call rows, plan updates, and more.
746
+ *
747
+ * **A consumer must tolerate `kind` values it does not recognize** — agents
748
+ * emit different subsets and vendors add their own (recon §3). Switch on the
749
+ * kinds you render and ignore the rest; never treat an unknown kind as an
750
+ * error.
751
+ *
752
+ * Everything a Chat UI must draw to render a transcript is a modelled field:
753
+ * {@link text} / {@link content} for the message kinds, {@link toolCall} for
754
+ * `tool_call` and `tool_call_update`, {@link plan} for `plan`. {@link raw} is
755
+ * for what is deliberately left out — see its own note.
756
+ *
757
+ * @category Consumer Services
758
+ * @public
759
+ * @beta
760
+ */
761
+ export interface AgentSessionUpdate {
762
+ /**
763
+ * The Agent Client Protocol `sessionUpdate` discriminator, e.g.
764
+ * `"agent_message_chunk"`, `"agent_thought_chunk"`, `"tool_call"`,
765
+ * `"tool_call_update"`, `"plan"`, `"available_commands_update"`.
766
+ */
767
+ readonly kind: string;
768
+ /**
769
+ * Text payload for the streaming-text kinds (`agent_message_chunk`,
770
+ * `agent_thought_chunk`, `user_message_chunk`); `undefined` for every other
771
+ * kind — and also for a text kind whose block is not text, in which case
772
+ * read {@link content}.
773
+ */
774
+ readonly text?: string;
775
+ /**
776
+ * The whole content block a streaming-text kind carried, of which
777
+ * {@link text} is the `"text"` shorthand. Read it when you want to render an
778
+ * image or a resource link an agent sent as a message rather than dropping
779
+ * it. `undefined` for every non-message kind.
780
+ */
781
+ readonly content?: AgentContentBlock;
782
+ /**
783
+ * Stable id for the run of chunks this update belongs to. Consecutive
784
+ * same-kind chunks share one — **synthesized by Silo when the agent omits
785
+ * `messageId`**, which real agents do (recon Finding 7), so a consumer can
786
+ * group a streamed sentence into one bubble without minting ids itself.
787
+ */
788
+ readonly messageId?: string;
789
+ /**
790
+ * The tool call, for `kind` `"tool_call"` and `"tool_call_update"`;
791
+ * `undefined` otherwise. Key rows by {@link AgentToolCall.toolCallId} and
792
+ * patch in place — an update carries only what changed.
793
+ */
794
+ readonly toolCall?: AgentToolCall;
795
+ /**
796
+ * The agent's plan **in full**, for `kind` `"plan"`; `undefined` otherwise.
797
+ * The agent reissues the entire list every time, so replace the plan you are
798
+ * showing rather than appending to it. May be empty.
799
+ */
800
+ readonly plan?: readonly AgentPlanEntry[];
801
+ /**
802
+ * The raw Agent Client Protocol `update` object — the **escape hatch**, for
803
+ * the kinds and fields this surface does not model.
804
+ *
805
+ * What is deliberately not modelled, and why: `available_commands_update`
806
+ * (an agent's slash commands — a menu, not a transcript row),
807
+ * `usage_update` (token counts, which no agent reports the same way),
808
+ * `current_mode_update` (already surfaced as
809
+ * {@link AgentSessionConfigOption.currentValue}), `session_info_update`
810
+ * (already surfaced as {@link AgentInfo.title}), and vendor extensions such
811
+ * as `claude-agent-acp`'s `_meta`. None is needed to draw a transcript; all
812
+ * are readable here.
813
+ *
814
+ * **`raw` tracks the protocol, not this SDK's semver.** A field inside it can
815
+ * change shape, or vanish, when an agent or its adapter changes — no SDK
816
+ * major required, and that has already happened three times inside one sprint.
817
+ * Read it defensively, and if you find yourself needing it for something
818
+ * every Chat UI must render, that is a gap in this surface worth reporting.
819
+ * Treat it as read-only.
820
+ */
821
+ readonly raw: Readonly<Record<string, unknown>>;
822
+ }
823
+
824
+ /**
825
+ * One choice offered by an {@link AgentPermissionRequest} — pass its
826
+ * {@link AgentPermissionOption.optionId} to {@link AgentPermissionRequest.respond}.
827
+ *
828
+ * @category Consumer Services
829
+ * @public
830
+ * @beta
831
+ */
832
+ export interface AgentPermissionOption {
833
+ readonly optionId: string;
834
+ /** Label to show on the button, e.g. `"Allow"`, `"Allow for this session"`,
835
+ * `"Reject"`. */
836
+ readonly name: string;
837
+ /** The protocol's coarse category for the option, e.g. `"allow_once"`,
838
+ * `"allow_always"`, `"reject_once"` — for styling a set of buttons
839
+ * consistently. Tolerate unknown values. */
840
+ readonly kind: string;
841
+ }
842
+
843
+ /**
844
+ * The agent is **blocked on the user**: it wants permission to run a tool and
845
+ * the turn will not proceed until {@link AgentPermissionRequest.respond} is
846
+ * called. Delivered to {@link AgentSessionHandle.onPermission}.
847
+ *
848
+ * **This is not a safety boundary.** An agent routes a request through here
849
+ * only if it chooses to; nothing stops it touching the filesystem directly
850
+ * (recon Finding 1). Do not present this as Silo gating the agent's actions.
851
+ *
852
+ * If no `onPermission` listener is registered, Silo answers `cancelled` on the
853
+ * extension's behalf so the agent is never left hanging.
854
+ *
855
+ * @category Consumer Services
856
+ * @public
857
+ * @beta
858
+ */
859
+ export interface AgentPermissionRequest {
860
+ /** The tool call this permission is for — the same id as the matching
861
+ * {@link AgentToolCall.toolCallId} in the update stream. */
862
+ readonly toolCallId: string;
863
+ /** A human-readable description of what the agent wants to do. */
864
+ readonly title: string;
865
+ /** The choices to present. Always at least one; order is the agent's. */
866
+ readonly options: readonly AgentPermissionOption[];
867
+ /**
868
+ * The call the agent is asking to make, when it sent one — the protocol's
869
+ * `toolCall` params, the same shape the update stream carries. It is how a
870
+ * Chat UI can show the diff **before** the user answers rather than only the
871
+ * title; `claude-agent-acp` 0.75.1 sends a full one including `content`.
872
+ */
873
+ readonly toolCall?: AgentToolCall;
874
+ /** The raw Agent Client Protocol `session/request_permission` params. */
875
+ readonly raw: Readonly<Record<string, unknown>>;
876
+ /**
877
+ * Answer the request with one of {@link AgentPermissionRequest.options}.
878
+ * Idempotent — the first call wins, later calls are ignored. Passing an
879
+ * `optionId` that is not in `options` answers `cancelled`.
880
+ */
881
+ respond(optionId: string): void;
882
+ }
883
+
884
+ /**
885
+ * One selectable value inside an {@link AgentSessionConfigOption}.
886
+ *
887
+ * @category Consumer Services
888
+ * @public
889
+ * @beta
890
+ */
891
+ export interface AgentSessionConfigChoice {
892
+ /** The value to pass to {@link AgentSessionHandle.setConfigOption}. */
893
+ readonly value: string;
894
+ /** Label to show, e.g. `"Claude Sonnet"`, `"Plan"`. */
895
+ readonly name: string;
896
+ /** A longer explanation, when the agent gave one. */
897
+ readonly description?: string;
898
+ }
899
+
900
+ /**
901
+ * One session-level control the agent advertised at connect — the Agent Client
902
+ * Protocol `session/new` `configOptions` list (recon 2026-09-08). It is
903
+ * **self-describing and subsumes** the older `modes` / `models` fields: Cursor
904
+ * advertises a `"mode"` entry and a `"model"` entry, Claude a single `"mode"`
905
+ * entry (its permission mode), and a vendor may add its own.
906
+ *
907
+ * The write path is **generic too**:
908
+ * {@link AgentSessionHandle.setConfigOption} goes through the protocol's
909
+ * `session/set_config_option`, so a category Silo has never heard of still
910
+ * works — Claude's `"thought_level"` (Effort) has no typed method anywhere in
911
+ * the protocol and sets fine. Silo keeps `session/set_mode` /
912
+ * `session/set_model` only as a fallback for an agent that does not implement
913
+ * the generic setter.
914
+ *
915
+ * Render one control per entry and **skip an entry whose {@link type} you do
916
+ * not recognise** — the same tolerance rule {@link AgentSessionUpdate} follows
917
+ * for unknown `kind`s. Do not assume every advertised entry is settable: an
918
+ * adapter may list an id its own handler rejects, so treat a failed
919
+ * {@link AgentSessionHandle.setConfigOption} as "stop offering this one".
920
+ *
921
+ * @category Consumer Services
922
+ * @public
923
+ * @beta
924
+ */
925
+ export interface AgentSessionConfigOption {
926
+ /** Stable id — pass it to {@link AgentSessionHandle.setConfigOption}. */
927
+ readonly id: string;
928
+ /** Label for the control, e.g. `"Mode"`, `"Model"`. */
929
+ readonly name: string;
930
+ /** A longer explanation, when the agent gave one. */
931
+ readonly description?: string;
932
+ /**
933
+ * The protocol category that decides how a write is delivered — `"mode"`,
934
+ * `"model"`, or a vendor's own. {@link AgentSessionHandle.setConfigOption}
935
+ * rejects a category with no verified writer.
936
+ */
937
+ readonly category: string;
938
+ /**
939
+ * The control shape. Only `"select"` is modelled today; treat any other
940
+ * value as "do not render".
941
+ */
942
+ readonly type: string;
943
+ /**
944
+ * The currently-selected {@link AgentSessionConfigChoice.value}. Kept
945
+ * current when the agent moves it itself (an ACP `current_mode_update`), so
946
+ * a bound control always reflects reality — subscribe with
947
+ * {@link AgentSessionHandle.onConfigOptionsChanged}.
948
+ */
949
+ readonly currentValue: string;
950
+ /** The choices, in the agent's order. */
951
+ readonly options: readonly AgentSessionConfigChoice[];
952
+ }
953
+
954
+ /**
955
+ * One command the agent advertises (Agent Client Protocol
956
+ * `available_commands_update`, RFC 0040). **Skills arrive in this list too**
957
+ * — the protocol has no separate skills concept, and the two agents probed
958
+ * mark them differently: pi prefixes the name (`skill:code-review`), Claude
959
+ * does not mark them at all outside a `(user)` / `(project)` suffix in
960
+ * {@link description}'s prose. Splitting them back out would mean
961
+ * vendor-sniffing a label neither agent is contractually bound to keep, so
962
+ * this surface deliberately does not try.
963
+ *
964
+ * @category Consumer Services
965
+ * @public
966
+ * @beta
967
+ */
968
+ export interface AgentCommand {
969
+ /** What to send after `/` to invoke it — `prompt([{ type: "text", text:
970
+ * "/" + name }])`. There is no separate invocation method. */
971
+ readonly name: string;
972
+ /** One-line summary for a palette row. */
973
+ readonly description?: string;
974
+ /**
975
+ * Present when the command takes a free-text argument; `hint` is the
976
+ * agent's own placeholder, when it gave one. Absent means the command takes
977
+ * none — normalised at the host boundary, since Claude sends `input: null`
978
+ * where pi omits the key entirely; this type only ever sees one spelling.
979
+ */
980
+ readonly input?: { readonly hint?: string };
981
+ }
982
+
983
+ /**
984
+ * What a Chat session accepts as prompt content, read once from `initialize`
985
+ * and fixed for the session's life (RFC 0040). A UI gates its attachment
986
+ * affordance on this rather than offering one that fails at prompt time.
987
+ *
988
+ * A field the agent's `initialize` response omitted defaults to `false`
989
+ * rather than being left `undefined` — both `codex-acp` and `claude-agent-acp`
990
+ * omit `audio` entirely (recon 2026-09-09), so treating an absent field as
991
+ * "not accepted" is the only reading that doesn't require every consumer to
992
+ * repeat the same `?? false`.
993
+ *
994
+ * @category Consumer Services
995
+ * @public
996
+ * @beta
997
+ */
998
+ export interface AgentPromptCapabilities {
999
+ /** Image content blocks are accepted. Not yet a sendable
1000
+ * {@link AgentPromptBlock} — see its doc comment. */
1001
+ readonly image: boolean;
1002
+ /** Audio content blocks are accepted. */
1003
+ readonly audio: boolean;
1004
+ /**
1005
+ * Embedded `"resource"` {@link AgentPromptBlock}s are accepted. A
1006
+ * `"resource_link"` (a pointer the agent may read itself) is always
1007
+ * allowed and is not gated on this.
1008
+ */
1009
+ readonly embeddedContext: boolean;
1010
+ }
1011
+
1012
+ /**
1013
+ * A live handle to one **Chat session** (RFC 0038) — an Agent Client Protocol
1014
+ * child Silo spawned from a user-authored Chat profile, speaking structured
1015
+ * JSON-RPC over piped stdio. Returned by {@link AgentSessionsService.connect}.
1016
+ *
1017
+ * The same session shows up in {@link AgentsService.getState} as an
1018
+ * {@link AgentInfo} with `kind: "chat"` and this handle's {@link id} — so the
1019
+ * Agents navigator, attention badges and status all work for it exactly as for
1020
+ * a Terminal session, with no extra wiring.
1021
+ *
1022
+ * Drive one turn at a time: call {@link prompt}, await its
1023
+ * {@link AgentPromptResult}, then prompt again.
1024
+ *
1025
+ * @category Consumer Services
1026
+ * @public
1027
+ * @beta
1028
+ */
1029
+ export interface AgentSessionHandle {
1030
+ /**
1031
+ * This session's {@link AgentInfo.id} — the key {@link AgentsService.reveal},
1032
+ * {@link AgentsService.resume} and {@link AgentsService.acknowledge} take.
1033
+ * Stable across a {@link AgentsService.resume}.
1034
+ */
1035
+ readonly id: string;
1036
+ /**
1037
+ * The agent's own session id (`session/new`'s `sessionId`, or the fresh id
1038
+ * `session/load` adopted) — what to pass back as
1039
+ * {@link AgentSessionRestore.sessionId} on a future {@link
1040
+ * AgentSessionsService.connect} to restore this conversation (RFC 0042).
1041
+ * Distinct from {@link AgentSessionHandle.id}, which is namespaced for
1042
+ * `ctx.agents` and does not change even when this does (an adopted id after
1043
+ * `session/load`).
1044
+ */
1045
+ readonly sessionId: string;
1046
+ /** The user's asserted catalog agent id for the profile, if any — the same
1047
+ * value that selects the profile's `+`-menu icon. */
1048
+ readonly agentId?: string;
1049
+ /** Display name for the agent: what it declared at connect (`initialize`),
1050
+ * falling back to the profile label when it declared none (recon Finding 4). */
1051
+ readonly agentName: string;
1052
+ /**
1053
+ * Whether the agent advertises `session/resume` or `session/load`, i.e.
1054
+ * whether {@link AgentsService.resume} can bring this conversation back
1055
+ * after the process dies. Mirrors {@link AgentInfo.canResume}.
1056
+ */
1057
+ readonly canResume: boolean;
1058
+ /**
1059
+ * How this handle came to be connected (RFC 0042) — see
1060
+ * {@link AgentSessionRestore}:
1061
+ *
1062
+ * - `"new"` — an ordinary `session/new` (no {@link
1063
+ * AgentSessionConnectOptions.resume} was given, or the persisted session
1064
+ * had gone stale and `connect()` fell through to a fresh one).
1065
+ * - `"resumed"` — a persisted session reconnected, via `session/resume` or
1066
+ * `session/load`.
1067
+ * - `"journal-only"` — the agent could do neither. There is no live
1068
+ * session: {@link prompt} rejects. Reconnect with
1069
+ * `resume: { sessionId, startFresh: true }` to continue in a new one,
1070
+ * preserving {@link journal} for continuity.
1071
+ */
1072
+ readonly resumeOutcome: "new" | "resumed" | "journal-only";
1073
+ /**
1074
+ * Prior turns to paint **before** subscribing to {@link onUpdate} — the
1075
+ * **transcript journal** (RFC 0042), read from disk. Empty when
1076
+ * {@link resumeOutcome} is `"new"` and nothing preceded this connection;
1077
+ * populated for `"resumed"` (a `session/resume` reconnect, which itself
1078
+ * replays nothing — this is the only record) and `"journal-only"`. Feed
1079
+ * these through the same reducer as {@link onUpdate} before subscribing to
1080
+ * it, e.g. `journal.reduce(applyUpdate, emptyTranscript)`.
1081
+ */
1082
+ readonly journal: readonly AgentSessionUpdate[];
1083
+ /**
1084
+ * Send a prompt turn and resolve when it ends. Content is structured blocks,
1085
+ * never a shell string. Rejects if the turn cannot be completed (connection
1086
+ * lost, agent error) — with the agent's message where it gave one. Also
1087
+ * rejects immediately when {@link resumeOutcome} is `"journal-only"` — there
1088
+ * is no live agent to prompt.
1089
+ */
1090
+ prompt(blocks: readonly AgentPromptBlock[]): Promise<AgentPromptResult>;
1091
+ /**
1092
+ * Ask the agent to stop the current turn (Agent Client Protocol
1093
+ * `session/cancel`). The in-flight {@link prompt} promise then resolves with
1094
+ * `stopReason: "cancelled"` rather than rejecting.
1095
+ */
1096
+ cancel(): void;
1097
+ /**
1098
+ * Subscribe to the turn's {@link AgentSessionUpdate} stream — streaming text,
1099
+ * tool calls, plans. **Not confined to between {@link prompt} and its
1100
+ * resolution**: some agents send one at connect, before any prompt at all
1101
+ * (pi's own startup banner arrives as an `agent_message_chunk` — recon
1102
+ * 2026-09-09), and a replay of prior turns can follow
1103
+ * {@link AgentsService.resume}. Subscribe before assuming nothing will
1104
+ * arrive yet. Returns a {@link Disposable}.
1105
+ */
1106
+ onUpdate(listener: (update: AgentSessionUpdate) => void): Disposable;
1107
+ /**
1108
+ * Subscribe to {@link AgentPermissionRequest}s. Returns a {@link Disposable}.
1109
+ * With at least one listener registered, answering is your responsibility;
1110
+ * with none, Silo answers `cancelled`.
1111
+ */
1112
+ onPermission(listener: (request: AgentPermissionRequest) => void): Disposable;
1113
+ /**
1114
+ * The session-level controls the agent advertised at connect — mode, model,
1115
+ * and whatever else it offers, each a self-describing
1116
+ * {@link AgentSessionConfigOption}. Empty when it advertised none.
1117
+ *
1118
+ * This is a **live snapshot**: {@link setConfigOption} and a mode the agent
1119
+ * changes itself both update it in place. Subscribe with
1120
+ * {@link onConfigOptionsChanged} and re-read.
1121
+ */
1122
+ readonly configOptions: readonly AgentSessionConfigOption[];
1123
+ /**
1124
+ * Change one advertised control. `id` names an entry in
1125
+ * {@link configOptions}; `value` is one of that entry's
1126
+ * {@link AgentSessionConfigChoice.value}s.
1127
+ *
1128
+ * Works for **any** category the agent advertises, including ones Silo has
1129
+ * never heard of — the host writes through the protocol's generic
1130
+ * `session/set_config_option`, falling back to the typed
1131
+ * `session/set_mode` / `session/set_model` only for an agent that does not
1132
+ * implement it.
1133
+ *
1134
+ * Rejects, with nothing written, on an unknown `id` or a `value` outside
1135
+ * that entry's options — and with the agent's own message when the agent
1136
+ * refuses (an adapter may advertise an entry its own handler does not know).
1137
+ * A rejection is a signal to stop offering that control.
1138
+ *
1139
+ * Resolves once the agent has acknowledged the change. {@link configOptions}
1140
+ * is then replaced from the agent's own updated list — setting one option can
1141
+ * move another — and {@link onConfigOptionsChanged} fires just before it
1142
+ * resolves.
1143
+ */
1144
+ setConfigOption(id: string, value: string): Promise<void>;
1145
+ /**
1146
+ * Fires whenever {@link configOptions} changes — a {@link setConfigOption}
1147
+ * landing, or the agent moving a value on its own (an ACP
1148
+ * `current_mode_update`). Re-read {@link configOptions} from the handle.
1149
+ * Returns a {@link Disposable}.
1150
+ */
1151
+ onConfigOptionsChanged(listener: () => void): Disposable;
1152
+ /**
1153
+ * The commands the agent advertises, live — a `/` palette's data source
1154
+ * (RFC 0040). **Empty until the agent's first
1155
+ * `available_commands_update`**: both agents probed send one at connect,
1156
+ * before any prompt, but nothing in the protocol requires it, and pi took
1157
+ * ~12.5s to get there (behind its startup banner) — `connect()` does not
1158
+ * wait for it. Subscribe with {@link onCommandsChanged} and re-read.
1159
+ *
1160
+ * There is no `runCommand()`. Invoke one the same way you'd send anything
1161
+ * else: `prompt([{ type: "text", text: "/" + command.name }])`.
1162
+ */
1163
+ readonly commands: readonly AgentCommand[];
1164
+ /** Fires whenever {@link commands} is replaced. Returns a
1165
+ * {@link Disposable}. */
1166
+ onCommandsChanged(listener: () => void): Disposable;
1167
+ /**
1168
+ * What this session accepts as prompt content, read once from `initialize`
1169
+ * and fixed for its life (RFC 0040) — unlike {@link commands}, this never
1170
+ * changes, so there is no change event for it.
1171
+ */
1172
+ readonly promptCapabilities: AgentPromptCapabilities;
1173
+ /**
1174
+ * Tear the session down: kill the agent process and drop it from
1175
+ * {@link AgentsService.getState}. Idempotent. The process is a piped child of
1176
+ * Silo — it does **not** survive this, and closing the workspace or quitting
1177
+ * the app reaps it the same way.
1178
+ */
1179
+ dispose(): void;
1180
+ }
1181
+
1182
+ /**
1183
+ * Options for {@link AgentSessionsService.connect}.
1184
+ *
1185
+ * @category Consumer Services
1186
+ * @public
1187
+ * @beta
1188
+ */
1189
+ export interface AgentSessionConnectOptions {
1190
+ /** Working directory for the agent. Defaults to the workspace folder. */
1191
+ cwd?: string;
1192
+ /** Which workspace the session belongs to (for
1193
+ * {@link AgentInfo.workspaceId} and `reveal`). Defaults to the active one. */
1194
+ workspaceId?: string;
1195
+ /**
1196
+ * A last-known display title, shown immediately with {@link resume} — before
1197
+ * the connection (`initialize` alone can take several seconds) or a
1198
+ * `session/resume` / `session/load` round trip resolves. Without it, a
1199
+ * restoring session's dock tab, workspace row, and Agents navigator entry
1200
+ * fall back to the profile's plain label for however long reconnecting
1201
+ * takes, even when the agent volunteered a real title before the app
1202
+ * closed. It **outlives the handshake**: it is still the session's title
1203
+ * once the connection is up, until the agent volunteers a real one of its
1204
+ * own (a `session_info_update`). The agent's product name from `initialize`
1205
+ * (`agentInfo.title`, e.g. "Claude Agent") does *not* supersede it — that is
1206
+ * the fallback for a session that has never had a title, not a replacement
1207
+ * for one this conversation already earned. Persist whatever
1208
+ * {@link AgentInfo.title} last was and pass it back here on the next
1209
+ * restore. Ignored without {@link resume} — a fresh session has no prior
1210
+ * title to show early.
1211
+ */
1212
+ title?: string;
1213
+ /**
1214
+ * How to bring **your** UI for this session into view. Silo calls this from
1215
+ * {@link AgentsService.reveal} — after activating the session's workspace —
1216
+ * so a kind-agnostic caller (the Agents navigator, a notification, a
1217
+ * command) can focus a Chat session's transcript without knowing that a
1218
+ * transcript is what it is.
1219
+ *
1220
+ * Implement it with whatever "come to the front" means for your surface: a
1221
+ * dock panel calls `api.setActive()` on its own `DockPanelApi`, a side
1222
+ * panel reveals itself through `ctx.layout`. Called on the main thread,
1223
+ * possibly more than once; keep it cheap and idempotent, and expect it after
1224
+ * an `await` — capture the handle you need in a ref rather than closing over
1225
+ * render-scoped state.
1226
+ *
1227
+ * Omit it and `reveal(id)` still activates the workspace, which is all Silo
1228
+ * can honestly do for a session whose UI it does not own.
1229
+ */
1230
+ reveal?: () => void;
1231
+ /**
1232
+ * Reconnect a previously-persisted session instead of starting a fresh one
1233
+ * — Chat session resurrection (RFC 0042). Pass the {@link
1234
+ * AgentSessionHandle.sessionId} a prior `connect()` on this same profile
1235
+ * returned (what a recorded panel keeps in its `DockPanelState`).
1236
+ *
1237
+ * `connect()` never rejects because the target has gone stale: it probes
1238
+ * `session/resume` and `session/load` separately (`resume` wins when both
1239
+ * are advertised), and falls through to a fresh `session/new` if neither
1240
+ * works and there is no journal to fall back to. See {@link
1241
+ * AgentSessionHandle.resumeOutcome} for which path was taken.
1242
+ */
1243
+ resume?: AgentSessionRestore;
1244
+ }
1245
+
1246
+ /**
1247
+ * A restore target for {@link AgentSessionConnectOptions.resume} — Chat
1248
+ * session resurrection (RFC 0042).
1249
+ *
1250
+ * @category Consumer Services
1251
+ * @public
1252
+ * @beta
1253
+ */
1254
+ export interface AgentSessionRestore {
1255
+ /**
1256
+ * The session id to reconnect — an earlier connection's {@link
1257
+ * AgentSessionHandle.sessionId}.
1258
+ */
1259
+ sessionId: string;
1260
+ /**
1261
+ * Skip probing `session/resume` / `session/load` and start a fresh
1262
+ * `session/new` directly, carrying the **transcript journal** kept under
1263
+ * {@link sessionId} into the new session's — "Continue in a new session"
1264
+ * from a `"journal-only"` {@link AgentSessionHandle.resumeOutcome}. The new
1265
+ * handle's {@link AgentSessionHandle.sessionId} is whatever `session/new`
1266
+ * mints, **not** this `sessionId` — persist that returned id for the next
1267
+ * restore, exactly as after a plain `connect()`. Keeping the original,
1268
+ * already-known-unresumable id instead would mean every future restore
1269
+ * keeps retrying an id the agent will never resume, forever, even while the
1270
+ * new conversation itself works fine turn after turn (found live,
1271
+ * 2026-09-09).
1272
+ */
1273
+ startFresh?: boolean;
1274
+ }
1275
+
1276
+ /**
1277
+ * Connect to a **Chat session** and drive it — exposed as
1278
+ * `ctx.agents.sessions` (RFC 0038 phase 2). The counterpart to
1279
+ * {@link AgentProfilesService} for the `chat` launch arm: where `profiles`
1280
+ * *starts a terminal and walks away*, this *holds a live connection* an
1281
+ * extension can prompt, watch and cancel.
1282
+ *
1283
+ * **Sourced from user-authored profiles only.** There is deliberately no
1284
+ * "connect to this command" — an extension cannot point Silo at an arbitrary
1285
+ * binary to spawn with pipes. The user defines a Chat profile on
1286
+ * Settings → Agents; an extension names it.
1287
+ *
1288
+ * Needs the **`"agents"` {@link Permission}**, declared in the extension's
1289
+ * `silo.permissions` and granted at install: {@link connect} throws without it.
1290
+ *
1291
+ * @example
1292
+ * ```ts
1293
+ * const session = await ctx.agents.sessions.connect("claude-chat");
1294
+ * const off = session.onUpdate((u) => {
1295
+ * if (u.kind === "agent_message_chunk") appendToTranscript(u.messageId, u.text);
1296
+ * });
1297
+ * const result = await session.prompt([{ type: "text", text: "Explain this repo." }]);
1298
+ * ctx.log.info(`turn ended: ${result.stopReason}`);
1299
+ * ctx.subscriptions.push(off, { dispose: () => session.dispose() });
1300
+ * ```
1301
+ *
1302
+ * @category Consumer Services
1303
+ * @public
1304
+ * @beta
1305
+ */
1306
+ export interface AgentSessionsService {
1307
+ /**
1308
+ * Spawn the agent for the named Chat profile, run the Agent Client Protocol
1309
+ * `initialize` + `session/new` handshake, and resolve with a live
1310
+ * {@link AgentSessionHandle}.
1311
+ *
1312
+ * Rejects when: the extension lacks the `"agents"` permission; no profile has
1313
+ * that id; the profile is a Terminal profile, not a Chat one; there is no
1314
+ * target workspace; the agent needs authentication (its `session/new`
1315
+ * failed); or the agent reported a startup error (the rejection carries its
1316
+ * message).
1317
+ *
1318
+ * With {@link AgentSessionConnectOptions.resume}, `connect()` still never
1319
+ * rejects merely because the target has gone stale — it falls all the way
1320
+ * through to a fresh `session/new` (RFC 0042); see {@link
1321
+ * AgentSessionHandle.resumeOutcome}.
1322
+ *
1323
+ * @param profileId — an {@link AgentProfileSummary.id} whose profile uses the
1324
+ * `chat` launch arm.
1325
+ */
1326
+ connect(
1327
+ profileId: string,
1328
+ options?: AgentSessionConnectOptions,
1329
+ ): Promise<AgentSessionHandle>;
1330
+ /**
1331
+ * Read a session's **transcript journal** (RFC 0042) without connecting —
1332
+ * for painting a restored panel *before* `connect({ resume })` resolves,
1333
+ * which itself also pays for the `session/resume` / `session/load` network
1334
+ * round trip. This is the "paint from journal" half of the restore flow;
1335
+ * `connect()` is the "reconnect the agent" half, and the two run
1336
+ * independently on purpose.
1337
+ *
1338
+ * Resolves `[]` for a session that never wrote a journal entry, or an
1339
+ * unknown id — never rejects on a merely-missing journal. Needs the
1340
+ * `"agents"` {@link Permission}, same as {@link connect}.
1341
+ *
1342
+ * @param sessionId — an {@link AgentSessionHandle.sessionId}.
1343
+ */
1344
+ readJournal(
1345
+ sessionId: string,
1346
+ options?: { workspaceId?: string },
1347
+ ): Promise<readonly AgentSessionUpdate[]>;
1348
+ }
1349
+
356
1350
  /**
357
1351
  * Host-computed coding-agent observability — exposed as
358
1352
  * {@link ExtensionContext.agents}. Detection (what OSC/output signals mean
@@ -383,7 +1377,10 @@ export interface AgentsService {
383
1377
  * instead.
384
1378
  */
385
1379
  getState(options?: { allWorkspaces?: boolean }): AgentInfo[];
386
- /** Look up {@link AgentInfo} for a specific terminal tab by its record id. */
1380
+ /** Look up {@link AgentInfo} for a specific terminal tab by its record id.
1381
+ * Only ever resolves a `kind: "terminal"` session — use
1382
+ * {@link AgentsService.getState} and match on {@link AgentInfo.id} to find a
1383
+ * Chat session. */
387
1384
  getByTerminalId(terminalId: string): AgentInfo | undefined;
388
1385
  /**
389
1386
  * Subscribe to changes in the active workspace's agent state. Pass
@@ -416,8 +1413,134 @@ export interface AgentsService {
416
1413
  * }),
417
1414
  * );
418
1415
  * ```
1416
+ *
1417
+ * @param id — an {@link AgentInfo.id}. A terminal record id is one (every
1418
+ * Terminal session's `id` equals its `terminalId`), so existing callers
1419
+ * passing a terminal id keep working; a Chat session's id works too.
1420
+ */
1421
+ acknowledge(id: string): void;
1422
+ /**
1423
+ * The Agent Session the user is currently **looking at** — the one whose
1424
+ * surface is the active tab — or `null` when the active tab is not an agent
1425
+ * at all (an editor, a settings page, nothing).
1426
+ *
1427
+ * Kind-agnostic by construction: a terminal tab reports its Terminal
1428
+ * session, and a dock panel that declared
1429
+ * `DockPanelApi.setAgentSession` reports its Chat session. That is
1430
+ * what lets one consumer implement "clear the badge for the session I'm
1431
+ * watching" or "hide the row for the session I'm already looking at" without
1432
+ * knowing which kind it got.
1433
+ *
1434
+ * @example
1435
+ * ```ts
1436
+ * // Hide the status row for whatever the user is already watching.
1437
+ * const watching = ctx.agents.getActive();
1438
+ * const rows = ctx.agents.getState().filter((a) => a.id !== watching);
1439
+ * ```
1440
+ */
1441
+ getActive(): string | null;
1442
+ /**
1443
+ * Subscribe to changes in {@link AgentsService.getActive} — fired with the
1444
+ * new value (or `null`) whenever the active surface moves. Returns a
1445
+ * {@link Disposable} that cancels the subscription.
1446
+ */
1447
+ subscribeActive(listener: (id: string | null) => void): Disposable;
1448
+ /**
1449
+ * Bind a provider of **activity badges** for Agent Session tabs — the
1450
+ * host-owned `Activity` chrome (spinner / ready / warn / error) on the
1451
+ * trailing edge of a tab.
1452
+ *
1453
+ * The binder's `provide` is handed an {@link AgentInfo.id}, and the host
1454
+ * routes the result to whichever tab is showing that session: a terminal tab
1455
+ * for a Terminal session, or the dock panel that declared
1456
+ * `DockPanelApi.setAgentSession` for a Chat session. One binder,
1457
+ * both kinds — which is the point: the `ctx.terminals` equivalent takes a
1458
+ * *terminal* id, so an extension literally could not badge a Chat session.
1459
+ *
1460
+ * `provide` is called synchronously for every visible tab during render.
1461
+ * Keep it a lookup: no allocation, no async, no work proportional to the
1462
+ * number of sessions.
1463
+ *
1464
+ * @example
1465
+ * ```ts
1466
+ * ctx.subscriptions.push(
1467
+ * ctx.agents.bindActivity({
1468
+ * id: "my-ext.agent-badge",
1469
+ * provide(agentSessionId) {
1470
+ * const info = ctx.agents
1471
+ * .getState({ allWorkspaces: true })
1472
+ * .find((a) => a.id === agentSessionId);
1473
+ * if (info?.activity !== "working") return null;
1474
+ * return { activity: "working", tooltip: "Agent working" };
1475
+ * },
1476
+ * }),
1477
+ * );
1478
+ * ```
1479
+ */
1480
+ bindActivity(binder: TabActivityBinder): Disposable;
1481
+ /**
1482
+ * Bind a provider of **leading icons** for Agent Session tabs — a brand mark
1483
+ * so a tab running an agent is identifiable at a glance. Same routing and
1484
+ * same synchronous-`provide` contract as
1485
+ * {@link AgentsService.bindActivity}.
1486
+ *
1487
+ * Return `null` for "no icon". Take care that a component which renders
1488
+ * nothing produces `null` here rather than a truthy element descriptor, or
1489
+ * the host reserves tab space for an icon that never appears.
1490
+ */
1491
+ bindIcon(binder: TabIconBinder): Disposable;
1492
+ /**
1493
+ * Re-query every bound {@link AgentsService.bindActivity} /
1494
+ * {@link AgentsService.bindIcon} provider. Call it when something *outside*
1495
+ * the agent snapshot changed what a provider would return — a setting, the
1496
+ * active theme — since the host cannot know about those.
1497
+ */
1498
+ invalidateAdornments(): void;
1499
+ /**
1500
+ * End an Agent Session, either kind: close its terminal tab, or close the
1501
+ * dock panel that declared `DockPanelApi.setAgentSession` for it
1502
+ * (which reaps the agent process as the panel unmounts).
1503
+ *
1504
+ * A no-op for an unknown id, or for a Chat session with no panel mounted —
1505
+ * Silo will not kill a connection whose UI it cannot account for.
1506
+ *
1507
+ * @param id — an {@link AgentInfo.id}.
1508
+ */
1509
+ close(id: string): void;
1510
+ /**
1511
+ * Bring an Agent Session into view: focus its terminal tab if it is a
1512
+ * Terminal session, or its transcript panel if it is a Chat session —
1513
+ * activating the owning workspace first. The caller does not need to know
1514
+ * which kind it is, which is the whole point of the verb (RFC 0038): a
1515
+ * consumer holding an {@link AgentInfo.id} should never have to branch to
1516
+ * `ctx.terminals.focus` vs. some chat-panel API.
1517
+ *
1518
+ * A no-op for an unknown id, or when the session's backing surface is gone
1519
+ * (a closed terminal, a disposed panel).
1520
+ *
1521
+ * @param id — an {@link AgentInfo.id}.
419
1522
  */
420
- acknowledge(terminalId: string): void;
1523
+ reveal(id: string): void;
1524
+ /**
1525
+ * Resume a session **through Silo**, when {@link AgentInfo.canResume} is
1526
+ * `true`.
1527
+ *
1528
+ * - Chat session: spawn a fresh agent process and reconnect the persisted
1529
+ * id over `session/resume` or `session/load` — whichever the agent
1530
+ * advertises, `resume` preferred (RFC 0042) — so the transcript and
1531
+ * context come back after the old process died. Then
1532
+ * {@link AgentsService.reveal} it.
1533
+ * - Terminal session: currently a no-op — a dead PTY cannot be re-run in
1534
+ * place, and the resume path stays "the user runs
1535
+ * {@link AgentInfo.resumeCommand}". Present on the surface so a
1536
+ * kind-agnostic consumer can call it unconditionally once Chat sessions
1537
+ * ship (RFC 0038 phase 4).
1538
+ *
1539
+ * A no-op for an unknown id or one whose `canResume` is `false`.
1540
+ *
1541
+ * @param id — an {@link AgentInfo.id}.
1542
+ */
1543
+ resume(id: string): void;
421
1544
  /**
422
1545
  * Every coding agent Silo knows about, as read-only
423
1546
  * {@link CatalogAgentSummary} records. Detection stays sealed (ADR 0028) —
@@ -440,4 +1563,15 @@ export interface AgentsService {
440
1563
  * @beta
441
1564
  */
442
1565
  readonly profiles: AgentProfilesService;
1566
+ /**
1567
+ * Connect to and drive a **Chat session** — a user-authored `chat` profile
1568
+ * spawned as an Agent Client Protocol child (RFC 0038 phase 2). See
1569
+ * {@link AgentSessionsService}. Needs the `"agents"` {@link Permission}:
1570
+ * every `connect()` throws without it.
1571
+ *
1572
+ * @category Consumer Services
1573
+ * @public
1574
+ * @beta
1575
+ */
1576
+ readonly sessions: AgentSessionsService;
443
1577
  }