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