@agentproto/runtime 0.3.0 → 0.5.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.
package/dist/index.d.ts CHANGED
@@ -3,35 +3,178 @@ import { B as BuildHeartbeatAgent } from './heartbeat-COGpMrJS.js';
3
3
  export { H as HeartbeatAgent, a as HeartbeatRunner, R as RuntimeEvent, b as RuntimeEvents, p as parseDuration } from './heartbeat-COGpMrJS.js';
4
4
  import { AcpMcpServer } from '@agentproto/acp';
5
5
  import { ChildProcess } from 'node:child_process';
6
+ import { D as DeclaredAdapterOption } from './spawn-defaults-d5gAhNkV.js';
6
7
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
- import { AdapterHandle, AdapterLister } from '@agentproto/adapter-kit';
8
+ import { AdapterHandle, SetupField, AdapterResolver, AdapterLister, AdapterEntry } from '@agentproto/provider-kit';
9
+ import { SandboxProvider } from '@agentproto/sandbox';
8
10
  import { WorkspaceFs } from './workspace-fs.js';
9
11
  export { createWorkspaceFs } from './workspace-fs.js';
10
12
  export { ConversationMeta, ConversationStore, ConversationTurn, fileConversationStore } from './conversations.js';
13
+ import { ProviderPreset } from '@agentproto/provider-presets';
14
+ import { StepCache } from '@agentproto/workflow-runtime';
11
15
  export { PROVIDER_ENV_VARS, ProviderEntry, ProvidersFile, injectProviderKeysIntoEnv, loadProviders, providerEnvVar, providersPath, removeProviderKey, setProviderKey } from './providers-store.js';
12
16
 
17
+ /**
18
+ * Per-session JSONL record of a completed `command_execute` invocation
19
+ * (and cron's `kind: "command"` action jobs, which share the same
20
+ * allowlist + `runCommand` path — see cron-scheduler.ts's "one
21
+ * enforcement path, not two" comment).
22
+ *
23
+ * Each call gets its OWN `kind: "command"` session, minted via
24
+ * `SessionsRegistry.recordCommand` — same as an agent-cli or PTY session,
25
+ * so it shows up in `session_list` / `command_list` for free instead of
26
+ * needing a bespoke query surface. Its result is stored at the exact
27
+ * per-id path an agent-cli session's structured transcript already uses:
28
+ * `~/.agentproto/sessions/<id>/events.jsonl` (see transcript-writer.ts).
29
+ * A command session's file holds exactly one line — there's no seq/turn
30
+ * machinery to reconstruct because the call is already finished by the
31
+ * time this is written.
32
+ *
33
+ * This module only knows how to read/write that one line; minting the
34
+ * session itself (and deciding the file's base directory) is
35
+ * `sessions.ts`'s job via `recordCommand`.
36
+ */
37
+ interface CommandLogEntry {
38
+ ts: string;
39
+ command: string;
40
+ args: string[];
41
+ cwd: string;
42
+ exitCode: number;
43
+ signal: string | null;
44
+ durationMs: number;
45
+ stdout: string;
46
+ stderr: string;
47
+ truncated?: boolean;
48
+ }
49
+
50
+ /**
51
+ * Per-session usage snapshot — the single place that decides a session's
52
+ * `costUsd` and where that number came from.
53
+ *
54
+ * Two cost sources feed a session:
55
+ * - an adapter's own usage reader (`readUsage`, e.g. hermes reads its
56
+ * state.db) or an ACP `usage_update` that carries a `cost` block — these
57
+ * are authoritative, tagged `source: "adapter"`.
58
+ * - raw input/output token counts with NO adapter cost (ACP adapters like
59
+ * claude-code / mastracode that report tokens but not dollars) — here we
60
+ * price them ourselves against agentproto's in-repo LLM pricing catalog,
61
+ * tagged `source: "computed"`.
62
+ *
63
+ * When tokens exist but the model is absent from the catalog we NEVER
64
+ * fabricate a price: `costUsd` is left undefined and the snapshot is tagged
65
+ * `source: "no-pricing"`. A session with neither cost nor tokens is
66
+ * `source: "none"`.
67
+ *
68
+ * This module is pure (the catalog lookup is injected) so the decision tree is
69
+ * unit-testable without building the whole model catalog.
70
+ */
71
+ /** Where a session's `costUsd` came from — see the module doc. */
72
+ type UsageSource = "adapter" | "computed" | "no-pricing" | "none";
73
+ /** Per-token USD prices for a model (per 1M tokens), the subset of the
74
+ * catalog's `LLMPricing` this module needs. */
75
+ interface TokenPricing {
76
+ inputPer1M: number;
77
+ outputPer1M: number;
78
+ }
79
+ /** Pluggable pricing accessor — defaults to the in-repo catalog's
80
+ * `resolvePricing`, overridable in tests. Returns undefined for an
81
+ * unknown model (the caller then tags `no-pricing`). */
82
+ type PricingResolver = (model: string) => TokenPricing | undefined;
83
+ /** Raw signals gathered over a turn, handed to `deriveSessionUsage`. */
84
+ interface UsageComputeInput {
85
+ /** Requested model id — needed to look up per-token prices. */
86
+ model?: string;
87
+ /** Cost the adapter reported directly (readUsage or usage_update.cost).
88
+ * Present → authoritative, wins over token-based computation. */
89
+ adapterCostUsd?: number;
90
+ tokensIn?: number;
91
+ tokensOut?: number;
92
+ contextSize?: number;
93
+ contextUsed?: number;
94
+ }
95
+ /** The resolved usage snapshot — the shape `session_usage` returns and the
96
+ * durable `usage_snapshot` transcript record carries. Cost/token fields are
97
+ * omitted when absent so a missing value never reads as a measured zero. */
98
+ interface SessionUsage {
99
+ model?: string;
100
+ costUsd?: number;
101
+ tokensIn?: number;
102
+ tokensOut?: number;
103
+ contextSize?: number;
104
+ contextUsed?: number;
105
+ source: UsageSource;
106
+ }
107
+ /**
108
+ * Decide a session's usage snapshot + cost source from the raw signals.
109
+ *
110
+ * Priority: an adapter-reported cost wins; else price the tokens against the
111
+ * catalog; else `none`. Never fabricates a price for an unpriced model.
112
+ */
113
+ declare function deriveSessionUsage(input: UsageComputeInput, resolve?: PricingResolver): SessionUsage;
114
+ /** Descriptor-shaped fields `projectSessionUsage` reads. */
115
+ interface UsageDescriptorFields {
116
+ model?: string;
117
+ costUsd?: number;
118
+ tokensIn?: number;
119
+ tokensOut?: number;
120
+ contextSize?: number;
121
+ contextUsed?: number;
122
+ usageSource?: UsageSource;
123
+ }
124
+ /**
125
+ * Project a session descriptor's persisted usage fields into the
126
+ * `session_usage` response shape. Omits absent fields; defaults an
127
+ * unstamped `usageSource` to `"none"`.
128
+ */
129
+ declare function projectSessionUsage(desc: UsageDescriptorFields): SessionUsage;
130
+
13
131
  /**
14
132
  * In-process pub/sub bus for session lifecycle events. Separate from
15
133
  * RuntimeEvents (global daemon bus) — session events are scoped to
16
134
  * individual sessions and fire at turn-level granularity.
17
135
  *
18
- * Consumers: EventRing (poll_events cursor), WebhookNotifier
136
+ * Consumers: EventRing (session_events_poll cursor), WebhookNotifier
19
137
  * (fire-and-forget HTTP), RoutineRunner (state machine fan-in),
20
- * and wait_for_any MCP tool (long-poll multiplexed).
138
+ * and session_monitor MCP tool (long-poll multiplexed).
21
139
  */
22
- type SessionEventType = "session:turn-end" | "session:awaiting-input" | "session:exited" | "session:command-done" | "policy:passed" | "policy:failed" | "policy:commit-ready" | "policy:committed";
140
+ type SessionEventType = "session:turn-end" | "session:awaiting-input" | "session:exited" | "session:command-done" | "policy:passed" | "policy:failed" | "policy:commit-ready" | "policy:committed" | "cron:fired" | "cron:succeeded" | "cron:failed";
141
+ /**
142
+ * Structured detail on why a session is awaiting input, when derivable.
143
+ * `source: "structured"` — a driver-reported ACP-style prompt (e.g. a tool
144
+ * permission request with real options). `source: "heuristic"` — a
145
+ * best-effort guess from the tail of the transcript (trailing "?" plus
146
+ * an optional enumerated option list) for drivers that don't report
147
+ * structured prompts. Absent entirely when neither could be determined —
148
+ * callers still have the plain `awaitingInput` boolean in that case.
149
+ */
150
+ interface SessionAwaitingQuestion {
151
+ text: string;
152
+ options?: string[];
153
+ source: "structured" | "heuristic";
154
+ }
23
155
  interface SessionTurnEndEvent {
24
156
  type: "session:turn-end";
25
157
  sessionId: string;
26
158
  awaitingInput: boolean;
27
159
  label?: string;
28
160
  ts: string;
161
+ question?: SessionAwaitingQuestion;
162
+ /**
163
+ * The adapter's stream `turn-end` reason, when the driver reports one
164
+ * (e.g. `"completed"`, `"cancelled"`, `"max_turns"`, or
165
+ * `"watchdog-timeout"` when the ACP client's turn-idle watchdog fired
166
+ * because the adapter went silent — see
167
+ * `@agentproto/acp/client`'s `AcpClientOptions.turnIdleTimeoutMs`).
168
+ * Absent for drivers that don't report a reason.
169
+ */
170
+ reason?: string;
29
171
  }
30
172
  interface SessionAwaitingInputEvent {
31
173
  type: "session:awaiting-input";
32
174
  sessionId: string;
33
175
  label?: string;
34
176
  ts: string;
177
+ question?: SessionAwaitingQuestion;
35
178
  }
36
179
  interface SessionExitedEvent {
37
180
  type: "session:exited";
@@ -41,8 +184,8 @@ interface SessionExitedEvent {
41
184
  label?: string;
42
185
  ts: string;
43
186
  }
44
- /** Emitted when execute_command finishes. commandId matches the id
45
- * returned by the execute_command MCP tool. */
187
+ /** Emitted when command_execute finishes. commandId matches the id
188
+ * returned by the command_execute MCP tool. */
46
189
  interface SessionCommandDoneEvent {
47
190
  type: "session:command-done";
48
191
  sessionId: string;
@@ -68,7 +211,7 @@ interface PolicyFailedEvent {
68
211
  /**
69
212
  * Emitted by the supervisor (WP5) when a `then:"commit"` policy's gate passes
70
213
  * but `requireHumanAck` is set: the commit is staged-and-ready but NOT yet
71
- * executed. Carries the exact paths + message that `ack_policy(approve:true)`
214
+ * executed. Carries the exact paths + message that `policy_ack(approve:true)`
72
215
  * will commit. The policy sits in `awaiting-ack` until acked.
73
216
  */
74
217
  interface PolicyCommitReadyEvent {
@@ -81,7 +224,7 @@ interface PolicyCommitReadyEvent {
81
224
  }
82
225
  /** Emitted by the supervisor (WP5) when a `then:"commit"` policy has actually
83
226
  * committed — either directly (requireHumanAck:false) or after an approving
84
- * `ack_policy`. Carries the resulting commit sha. */
227
+ * `policy_ack`. Carries the resulting commit sha. */
85
228
  interface PolicyCommittedEvent {
86
229
  type: "policy:committed";
87
230
  policyId: string;
@@ -91,7 +234,30 @@ interface PolicyCommittedEvent {
91
234
  message: string;
92
235
  ts: string;
93
236
  }
94
- type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionExitedEvent | SessionCommandDoneEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent;
237
+ /** Emitted by CronScheduler when a job fires (before the action runs). */
238
+ interface CronFiredEvent {
239
+ type: "cron:fired";
240
+ jobId: string;
241
+ label?: string;
242
+ ts: string;
243
+ }
244
+ /** Emitted by CronScheduler when a job action completes successfully. */
245
+ interface CronSucceededEvent {
246
+ type: "cron:succeeded";
247
+ jobId: string;
248
+ label?: string;
249
+ summary: string;
250
+ ts: string;
251
+ }
252
+ /** Emitted by CronScheduler when a job action fails. */
253
+ interface CronFailedEvent {
254
+ type: "cron:failed";
255
+ jobId: string;
256
+ label?: string;
257
+ error: string;
258
+ ts: string;
259
+ }
260
+ type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionExitedEvent | SessionCommandDoneEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent;
95
261
  interface SessionEventBus {
96
262
  emit(ev: SessionEvent): void;
97
263
  /** Subscribe to a specific event type. Returns an unsubscribe fn. */
@@ -103,26 +269,40 @@ interface SessionEventBus {
103
269
  }
104
270
 
105
271
  /**
106
- * Sessions registrytracks long-lived child processes spawned by
107
- * the agentproto daemon (terminals, agent CLIs, custom commands).
272
+ * A per-session observer the pluggable seam behind the runtime's transcript
273
+ * tap. `sessions.ts` drives one observer per turn (prompt in, each stream event,
274
+ * the turn-boundary usage snapshot, close). The transcript writer is the first
275
+ * and, today, only member; additional observers (e.g. a Langfuse session tracer)
276
+ * attach without touching the turn loop.
108
277
  *
109
- * The registry lives in-memory for the daemon's lifetime; a recent
110
- * snapshot also persists to `~/.agentproto/sessions.json` so a fresh
111
- * daemon restart can show "you had these running before" instead of
112
- * losing all visibility on a crash.
113
- *
114
- * Each session owns:
115
- * - identity: id, kind, workspace slug, command + args, started time
116
- * - lifecycle: status (starting/running/exited/killed/error)
117
- * - output: last N lines (ring buffer) for instant attach + a
118
- * Node EventEmitter for live streaming consumers
278
+ * The method set is exactly the subset of `TranscriptWriter` the session loop
279
+ * calls, so `TranscriptWriter` IS a `SessionObserver` (see transcript-writer.ts).
280
+ */
281
+
282
+ interface SessionObserver {
283
+ /** Record the outgoing message that opens a new turn. */
284
+ recordPrompt(sessionId: string, message: unknown): void;
285
+ /** Record one structured stream event (text-delta, tool-call, usage_update, ). */
286
+ recordEvent(sessionId: string, evt: AgentStreamEvent): void;
287
+ /** Record the durable turn-boundary / exit usage snapshot. */
288
+ recordUsageSnapshot(sessionId: string, usage: SessionUsage): void;
289
+ /** Flush and close this session's stream. Idempotent, fire-and-forget. */
290
+ close(sessionId: string): Promise<void>;
291
+ /** Close every open session stream (synchronous shutdown path). */
292
+ closeAll(): Promise<void>;
293
+ }
294
+ /**
295
+ * Fan a `SessionObserver` out over many. Each `record*` call is forwarded to
296
+ * every member in order; `close`/`closeAll` await all members.
119
297
  *
120
- * Consumers:
121
- * - HTTP routes (GET /sessions, /sessions/:id, /sessions/:id/stream,
122
- * POST /sessions/:id/kill)
123
- * - CLI `agentproto sessions` (TUI navigation + attach)
124
- * - guilde-web Active tab (session cards + terminal viewer)
298
+ * Every individual member call is isolated in try/catch so one throwing observer
299
+ * can neither break its siblings nor propagate into the turn loop — the
300
+ * transcript writer is effectively infallible today, but a future network-backed
301
+ * observer (a Langfuse sink) must never be able to throw a turn. Failures are
302
+ * swallowed here (no logging dependency at this layer); observers that need
303
+ * visibility into their own failures own that internally.
125
304
  */
305
+ declare function composeSessionObservers(observers: readonly SessionObserver[]): SessionObserver;
126
306
 
127
307
  /**
128
308
  * Minimal shape we need from a driver-agent-cli session — kept as a
@@ -132,6 +312,12 @@ interface SessionEventBus {
132
312
  */
133
313
  interface AgentSessionLike {
134
314
  sessionId: string;
315
+ /** OS-level process id of the spawned child, when the driver owns a
316
+ * real subprocess. Mirrored onto `SessionDescriptor.pid` at
317
+ * `spawnAgent` time so `processAlive` can be computed without
318
+ * adapter-specific forensics. Undefined for drivers that don't
319
+ * expose a process (e.g. a future non-subprocess transport). */
320
+ pid?: number;
135
321
  send(message: unknown): AsyncIterable<AgentStreamEvent>;
136
322
  cancel(): Promise<void>;
137
323
  close(): Promise<void>;
@@ -165,7 +351,15 @@ type PtyFactory = (opts: PtyFactoryOptions) => PtyProcess;
165
351
  interface AgentStreamEvent {
166
352
  kind: string;
167
353
  text?: string;
354
+ /** Correlates a "tool-call" with its later "tool-result" (and an
355
+ * "agent-prompt" with the permission it's asking about) — see
356
+ * @agentproto/acp's `StreamEvent`. */
357
+ toolCallId?: string;
168
358
  toolName?: string;
359
+ /** Tool-call input, e.g. an ACP `tool_call`'s `arguments` — see @agentproto/acp's `StreamEvent`. */
360
+ arguments?: unknown;
361
+ /** Tool-call output, e.g. an ACP `tool_call_update`'s `result` — see @agentproto/acp's `StreamEvent`. */
362
+ result?: unknown;
169
363
  isError?: boolean;
170
364
  reason?: string;
171
365
  error?: {
@@ -173,6 +367,35 @@ interface AgentStreamEvent {
173
367
  code?: number;
174
368
  data?: unknown;
175
369
  };
370
+ /** Structured options offered by an "agent-prompt" event (e.g. an ACP
371
+ * `requestPermission` callback surfaced as a clarifying question rather
372
+ * than auto-answered). Typed `unknown` — like `@agentproto/acp`'s own
373
+ * `StreamEvent["options"]` — so this structural type stays assignable
374
+ * from any driver's runtime session without a hard dependency on its
375
+ * exact option shape (`{optionId,name,kind}` for ACP, or whatever a
376
+ * future driver reports); `normalizeAgentPromptOptions` narrows it
377
+ * defensively at the one place it's consumed. */
378
+ options?: unknown;
379
+ /** "plan" event entries — see @agentproto/acp's `StreamEvent`'s `plan` kind. */
380
+ entries?: Array<{
381
+ content: string;
382
+ priority: string;
383
+ status: string;
384
+ }>;
385
+ /** "usage_update" context-window size (tokens). */
386
+ size?: number;
387
+ /** "usage_update" tokens currently in context. */
388
+ used?: number;
389
+ /** "usage_update" cumulative session cost, when the adapter reports one. */
390
+ cost?: {
391
+ amount: number;
392
+ currency: string;
393
+ };
394
+ /** "usage_update" cumulative input/output token counts, when the adapter
395
+ * reports them (lets the daemon price a session whose adapter gives tokens
396
+ * but no `cost`). */
397
+ tokensIn?: number;
398
+ tokensOut?: number;
176
399
  }
177
400
  type SessionKind = "terminal" | "agent-cli" | "command" | "browser";
178
401
  type SessionStatus = "starting" | "running" | "exited" | "killed" | "error";
@@ -191,6 +414,19 @@ interface SessionDescriptor {
191
414
  /** Last time anything was written to stdout/stderr. Lets the UI
192
415
  * spot stuck sessions ("running for 2h, last output 12min ago"). */
193
416
  lastOutputAt?: string;
417
+ /** Last time ANY adapter-process activity was observed — ACP
418
+ * JSON-RPC traffic, protocol-level events, not just ring-buffer
419
+ * output lines. Stays current during long tool-call chains where
420
+ * `lastOutputAt` goes stale. Updated on incoming session/update
421
+ * notifications AND on outbound RPC calls. ISO 8601. */
422
+ lastActivityAt?: string;
423
+ /** Whether the underlying OS process is still alive. Computed via
424
+ * `process.kill(pid, 0)` at read time (list()/get()) — cheap,
425
+ * zero-overhead, standard POSIX check. Absent when `pid` is null
426
+ * (no process to check). Never persisted — recomputed fresh on
427
+ * every read since it's a live OS query, stale the instant it's
428
+ * written to disk. */
429
+ processAlive?: boolean;
194
430
  /** Free-text label the spawner can attach (e.g. conversation id,
195
431
  * operator name) so the UI can group/filter. */
196
432
  label?: string;
@@ -222,6 +458,16 @@ interface SessionDescriptor {
222
458
  /** Cumulative input / output token counts (same source + cadence as costUsd). */
223
459
  tokensIn?: number;
224
460
  tokensOut?: number;
461
+ /** Context-window size + tokens-in-context from the latest `usage_update`
462
+ * event (ACP adapters that report a context window). Refreshed live as
463
+ * usage_update events arrive, not just at turn-end. */
464
+ contextSize?: number;
465
+ contextUsed?: number;
466
+ /** Where `costUsd` came from — `"adapter"` (adapter's own reader or a
467
+ * usage_update cost block), `"computed"` (tokens × in-repo catalog price),
468
+ * `"no-pricing"` (tokens present but the model isn't in the catalog — cost
469
+ * deliberately left undefined), or `"none"`. Stamped at each turn-end. */
470
+ usageSource?: UsageSource;
225
471
  /** ACP-level session id (the adapter's own handle — claude-code's
226
472
  * conversation id, hermes' chat id, …). Set at spawnAgent time
227
473
  * from `agentSession.sessionId`. Survives across daemon restarts
@@ -249,20 +495,42 @@ interface SessionDescriptor {
249
495
  resumeMetadata?: Record<string, string>;
250
496
  /** Set by the orchestration layer when the agent emits an
251
497
  * "awaiting-input" turn-end. Cleared on the next turn start.
252
- * Used by `wait_for_any` to fast-return without subscribing. */
498
+ * Used by `session_monitor` to fast-return without subscribing. */
253
499
  awaitingInput?: boolean;
500
+ /**
501
+ * Structured detail on WHY the session is awaiting input, when it can be
502
+ * determined — lets an orchestrator distinguish "blocked on a real
503
+ * question" from "just finished a turn" without re-reading raw output.
504
+ * `source: "structured"` comes from a driver-reported ACP-style prompt
505
+ * (`AgentStreamEvent.options`, e.g. a tool permission request);
506
+ * `source: "heuristic"` is a best-effort guess from the tail of the
507
+ * transcript (trailing "?" + an optional enumerated option list) for
508
+ * drivers that don't report structured prompts. Cleared alongside
509
+ * `awaitingInput` on the next turn start. */
510
+ awaitingQuestion?: SessionAwaitingQuestion;
254
511
  /** Count of turns that have fully completed (turn-end emitted) on this
255
- * session. Lets `wait_for_any` fast-return for a session that already
512
+ * session. Lets `session_monitor` fast-return for a session that already
256
513
  * finished its turn before the wait subscribed — a fast turn that ends
257
514
  * in "completed" (not "awaiting-input") leaves no other persisted
258
515
  * signal (status stays "running", busy clears). 0 = never ran a turn,
259
516
  * so a freshly-spawned idle session is NOT mistaken for done. */
260
517
  turnsCompleted?: number;
261
518
  /** True while a turn is actively running (mirror of the internal
262
- * runtime `busy` flag). Lets `wait_for_any` distinguish "idle after a
519
+ * runtime `busy` flag). Lets `session_monitor` distinguish "idle after a
263
520
  * finished turn" from "mid-turn" so it doesn't fast-return a stale
264
521
  * turn-end while the NEXT turn is still generating. */
265
522
  busy?: boolean;
523
+ /** What the in-flight turn is currently blocked on, when classifiable
524
+ * from the pending tool call: a spawned sub-agent (`agent_start`) or a
525
+ * shell/terminal command. Deliberately NO "user" variant — waiting on
526
+ * the user is already covered by `awaitingInput`/`awaitingQuestion`.
527
+ * Set on tool-call, cleared on the MATCHING tool-result (guarded by
528
+ * `pendingToolCallId`), at turn start, and in the turn's finally. */
529
+ blockedOn?: "subagent" | "command";
530
+ /** toolCallId of the tool-call that set `blockedOn`. A tool-result only
531
+ * clears `blockedOn` when its toolCallId matches — so a nested or
532
+ * interleaved tool finishing first can't clear the flag early. */
533
+ pendingToolCallId?: string;
266
534
  /** Session id of the orchestrator that spawned this session, set when
267
535
  * the spawn arrived via the scoped orchestrator sub-gateway (the
268
536
  * token carried the spawner's identity). Absent for direct `/mcp`
@@ -276,6 +544,17 @@ interface SessionDescriptor {
276
544
  * Treated as 0 when absent (legacy rows / direct spawns that predate
277
545
  * WP4). */
278
546
  depth?: number;
547
+ /** Id of the most recently-completed `kind: "command"` session spawned
548
+ * with the same `cwd`, found at spawn time — see `recordCommand` and
549
+ * `findPriorCommandSessionId`. Deliberately a REFERENCE, not copied
550
+ * content: a new session's own ring buffer / structured transcript
551
+ * never gets a prior command_execute's stdout/stderr spliced in, it
552
+ * just gets told where to look (`command_log_tail({sessionId})` or
553
+ * `agent_export`/`command_list` resolve the id into the full record).
554
+ * Set on `spawnAgent`/`spawnPty` when the registry already has a
555
+ * matching command session; absent otherwise (including for legacy
556
+ * rows persisted before this field existed). */
557
+ priorCommandSessionId?: string;
279
558
  /** Adapter id that drives this session (e.g. "camofox", "bureau"). */
280
559
  browserAdapterId?: string;
281
560
  /** Port the browser service listens on. */
@@ -284,6 +563,16 @@ interface SessionDescriptor {
284
563
  browserBaseUrl?: string;
285
564
  /** Execution location — "local" (default) or "cloud". */
286
565
  browserLocation?: "local" | "cloud";
566
+ /** True when this agent-cli session is running inside a sandbox (`agent_start.sandbox`)
567
+ * rather than as a local subprocess — there's no local PID to check, so
568
+ * `processAlive` never applies (it's already absent whenever `pid` is null). */
569
+ remote?: boolean;
570
+ /** Provider-assigned sandbox id (`BootedSandbox.sandboxId`), when `remote` is true. */
571
+ sandboxId?: string;
572
+ /** What session close does to the box (PR3 lifecycle) — `"kill"` (the
573
+ * default, ephemeral) or `"pause"` (keeps `sandboxId` reconnectable via
574
+ * `agent_start.sandbox.reuse`). Only set when `remote` is true. */
575
+ sandboxTeardown?: "kill" | "pause";
287
576
  }
288
577
  interface SessionsRegistry {
289
578
  spawn(input: SpawnSessionInput): SessionDescriptor;
@@ -306,25 +595,66 @@ interface SessionsRegistry {
306
595
  * `attachPty(id, ...)`. Throws when the registry was constructed
307
596
  * without a `spawnPty` factory (node-pty optional dep missing). */
308
597
  spawnPty(input: SpawnPtyInput): SessionDescriptor;
598
+ /** Register a COMPLETED `command_execute` (or cron `kind:"command"`
599
+ * action) invocation as its own `kind: "command"` session. Unlike
600
+ * `spawn`/`spawnAgent`/`spawnPty`, there's no live process to track —
601
+ * the command already ran to completion via `runCommand` before this
602
+ * is called — so the descriptor is minted already "finished"
603
+ * (status exited/error, startedAt backdated by `durationMs`,
604
+ * endedAt now) and its full result is written to that session's own
605
+ * `events.jsonl` (see command-log.ts), the same per-id path an
606
+ * agent-cli session's structured transcript uses. Synchronous: the
607
+ * descriptor (and its id) is available immediately, the JSONL write
608
+ * itself is fire-and-forget internally. */
609
+ recordCommand(input: RecordCommandInput): SessionDescriptor;
610
+ /** Read back a `kind:"command"` session's full logged result (the
611
+ * `CommandLogEntry` `recordCommand` wrote). Resolves to null when the
612
+ * session isn't a command session or has no recorded entry. Routed
613
+ * through the registry (rather than callers importing
614
+ * `readCommandLogEntry` directly) so the read always targets the same
615
+ * base directory `recordCommand` wrote to — that dir is test-overridable
616
+ * (`transcriptDir`) and callers outside sessions.ts have no other way
617
+ * to know it. */
618
+ readCommandLog(sessionId: string): Promise<CommandLogEntry | null>;
309
619
  /** Register an already-running browser service adapter as a tracked
310
620
  * session (kind="browser"). Idempotent by identity — each call
311
621
  * mints a fresh session id. The `stop` callback is invoked by
312
622
  * `kill()` best-effort. */
313
623
  registerBrowser(input: RegisterBrowserInput): SessionDescriptor;
314
624
  /** Send a follow-up turn to a live agent session. Throws when the
315
- * session is missing, not an agent-cli kind, or busy. The events
316
- * stream into the existing ring buffer + line emitter so /stream
317
- * consumers see them as they arrive. */
625
+ * session is missing, not an agent-cli kind, dead (exited/killed/
626
+ * error and unresumable `SessionNotAliveError`), or busy
627
+ * (mid-turn). The events stream into the existing ring buffer +
628
+ * line emitter so /stream consumers see them as they arrive. */
318
629
  sendPrompt(id: string, message: unknown): Promise<void>;
319
- /** Fire-and-forget variant of `sendPrompt`. Validates the same
320
- * preconditions synchronously throws on missing session / wrong
321
- * kind / busy but returns immediately after the turn is started,
322
- * letting the caller stream output via the SSE endpoint instead of
323
- * blocking on the full turn. Async errors during the turn are
324
- * pushed into the ring buffer as `[error]` lines so `/stream`
325
- * subscribers see them. Used by the web UI's chat input where a
326
- * long turn would otherwise freeze the textbox. */
327
- enqueuePrompt(id: string, message: unknown): void;
630
+ /** Fire-and-forget variant of `sendPrompt` for the TURN ITSELF only.
631
+ * Admission (resume attempt + the missing/wrong-kind/dead/busy
632
+ * checks `sendPrompt` throws) is AWAITED before this resolves, so a
633
+ * dead or busy session rejects instead of silently reporting
634
+ * success only resolves once the turn has actually started.
635
+ * Errors during the turn's own execution (network drop, child died
636
+ * mid-turn) are pushed into the ring buffer as `[error]` lines so
637
+ * `/stream` subscribers see them. Used by the web UI's chat input
638
+ * and MCP `agent_prompt`, where a long turn would otherwise freeze
639
+ * the caller.
640
+ *
641
+ * `opts.interrupt` lets a caller redirect a mid-turn session instead
642
+ * of hitting the busy rejection: the in-flight turn is cancelled
643
+ * (`agentSession.cancel()`), admission waits for that turn to
644
+ * actually settle (busy flips false — no fixed sleep), then the new
645
+ * prompt is admitted + fired on the SAME live session. Ignored
646
+ * (identical to the default) when the session is idle. Omitted or
647
+ * `false` reproduces today's mid-turn rejection byte-for-byte. */
648
+ enqueuePrompt(id: string, message: unknown, opts?: {
649
+ interrupt?: boolean;
650
+ }): Promise<void>;
651
+ /** Stamp `lastActivityAt` on a live agent-cli session's descriptor
652
+ * and schedule a debounced persist. Called from the `onActivity`
653
+ * callback threaded down through the driver → ACP client, which
654
+ * fires on ANY adapter-process traffic (not just ring-buffer
655
+ * output) — see `SessionDescriptor.lastActivityAt`. No-op when the
656
+ * id is unknown (session already forgotten). */
657
+ pulseActivity(id: string): void;
328
658
  list(): SessionDescriptor[];
329
659
  get(id: string): SessionDescriptor | undefined;
330
660
  /** Subscribe to a session's output. Returns an unsubscribe fn.
@@ -448,6 +778,18 @@ interface SpawnAgentInput {
448
778
  tokensIn?: number;
449
779
  tokensOut?: number;
450
780
  } | null>;
781
+ /** Opt this session into Langfuse tracing (prompt/completion + tool spans +
782
+ * tokens/cost). Effective opt-in is `trace ?? opts.langfuseTracingDefault ?? false`. */
783
+ trace?: boolean;
784
+ /** True when `agentSession` is a `SandboxAgentSessionProxy` (`agent_start.sandbox`)
785
+ * rather than a local subprocess-backed session — stamped onto the descriptor
786
+ * as `remote` since there's no local PID to report. */
787
+ remote?: boolean;
788
+ /** Provider-assigned sandbox id, when `remote` is true. */
789
+ sandboxId?: string;
790
+ /** What session close does to the box, when `remote` is true — see
791
+ * `SessionDescriptor.sandboxTeardown`. */
792
+ sandboxTeardown?: "kill" | "pause";
451
793
  }
452
794
  interface SpawnSessionInput {
453
795
  kind: SessionKind;
@@ -491,6 +833,24 @@ interface SpawnPtyInput {
491
833
  name?: string;
492
834
  label?: string;
493
835
  }
836
+ interface RecordCommandInput {
837
+ workspaceSlug: string;
838
+ /** Working directory the command actually ran in (post cwd-anchoring).
839
+ * Matched against a fresh session's cwd by `findPriorCommandSessionId`. */
840
+ cwd: string;
841
+ command: string;
842
+ args: string[];
843
+ /** Fields mirror `command-tools.ts`'s `ExecuteResult` — kept inline here
844
+ * (rather than importing that type) so sessions.ts has no dependency
845
+ * on command-tools.ts. */
846
+ exitCode: number;
847
+ signal: string | null;
848
+ durationMs: number;
849
+ stdout: string;
850
+ stderr: string;
851
+ truncated?: boolean;
852
+ label?: string;
853
+ }
494
854
 
495
855
  /**
496
856
  * MCP tools that expose browser service adapters (Camofox, Bureau, …) to
@@ -499,7 +859,7 @@ interface SpawnPtyInput {
499
859
  * for fs/exec/agent-cli.
500
860
  *
501
861
  * Five tools:
502
- * list_adapter_browsers discover available browser adapter ids + metadata
862
+ * browser_adapter_list discover available browser adapter ids + metadata
503
863
  * start_browser ensure a browser adapter is up, register as a session
504
864
  * stop_browser kill a running browser session
505
865
  * list_browsers browse alive/recent browser sessions
@@ -575,7 +935,7 @@ type BrowserAdapterLister = () => {
575
935
  config?: unknown[];
576
936
  }[];
577
937
  /**
578
- * Family-specific descriptor surfaced in the kit-path `list_adapter_browsers`
938
+ * Family-specific descriptor surfaced in the kit-path `browser_adapter_list`
579
939
  * response. Fields match the legacy lister shape so the tool output is
580
940
  * unchanged regardless of which path is active.
581
941
  */
@@ -593,7 +953,7 @@ interface BrowserAdapterInfo {
593
953
  * `TunnelRegistry` (multi-tunnel general surface) can share the same
594
954
  * provider contract without creating a circular dep.
595
955
  *
596
- * The tunnel family is the FIRST consumer of `@agentproto/adapter-kit`:
956
+ * The tunnel family is the FIRST consumer of `@agentproto/provider-kit`:
597
957
  * {@link TunnelProviderHandle} extends the kit's generic `AdapterHandle`
598
958
  * (slug/name/version/description/requiresSetup/check) with the
599
959
  * tunnel-specific `capabilities` + the existing `start`/`stop` lifecycle.
@@ -775,16 +1135,37 @@ declare class TunnelRegistry {
775
1135
  * third-party secrets) merged with descriptor-carried config (named's
776
1136
  * hostname/tunnelId/credentialsFile), descriptor taking precedence.
777
1137
  */
778
- private credsForDescriptor;
1138
+ protected credsForDescriptor(desc: TunnelDescriptor): Promise<TunnelCreds>;
779
1139
  private findEntryByIdOrName;
780
1140
  private schedulePersist;
781
1141
  private persistNow;
782
1142
  private loadFromDisk;
783
1143
  }
784
1144
 
1145
+ /**
1146
+ * Fire-and-forget webhook notifier for session lifecycle events.
1147
+ *
1148
+ * Each session can register its own `notifyUrl` (via `agent_start`).
1149
+ * A global URL can be set via `AGENTPROTO_NOTIFY_URL` env var or
1150
+ * `~/.agentproto/notify.json` (env wins). Both are POSTed when an event
1151
+ * fires — the union of per-session + global URLs, deduplicated.
1152
+ *
1153
+ * Retry policy: one retry after 2 s on network error. No retry on 4xx/5xx.
1154
+ * Timeout: 10 s per attempt. All errors are swallowed — the notifier never
1155
+ * throws into the session's hot path.
1156
+ */
1157
+
1158
+ interface WebhookNotifier {
1159
+ /** Register a per-session URL (called from agent_start). */
1160
+ register(sessionId: string, url: string): void;
1161
+ unregister(sessionId: string): void;
1162
+ /** Handler to wire into SessionEventBus.onAny. Fire-and-forget. */
1163
+ onSessionEvent(ev: SessionEvent): void;
1164
+ }
1165
+
785
1166
  /**
786
1167
  * Cursor-based ring buffer for session events. Bridges the in-process
787
- * SessionEventBus (push) to the poll_events MCP tool (pull).
1168
+ * SessionEventBus (push) to the session_events_poll MCP tool (pull).
788
1169
  *
789
1170
  * Capacity default: 1 000 events. Oldest entry dropped on overflow —
790
1171
  * callers that lag beyond the cap see a gap in their cursor but never
@@ -845,9 +1226,9 @@ interface EventRing {
845
1226
  *
846
1227
  * Trigger: session:turn-end on the watched session(s).
847
1228
  * Gate: optional shell command via the same allowlist + cwd-anchor as
848
- * execute_command. exit 0 = pass.
1229
+ * command_execute. exit 0 = pass.
849
1230
  * Action (then:"emit"): emits policy:passed / policy:failed on the bus
850
- * (readable via poll_events).
1231
+ * (readable via session_events_poll).
851
1232
  *
852
1233
  * WP5 (then:"commit"): on a GREEN gate, prepare a host-side git commit in the
853
1234
  * watched session's cwd — `git add -- <explicit paths>` then `git commit -m
@@ -859,7 +1240,7 @@ interface EventRing {
859
1240
  * "nothing to commit" is treated as SUCCESS (idempotent re-commit after a
860
1241
  * restart). `requireHumanAck` defaults to TRUE: the gate-green transition goes
861
1242
  * to `awaiting-ack` and emits `policy:commit-ready` (paths + message) instead of
862
- * committing; the commit runs only on `ack_policy(approve:true)` →
1243
+ * committing; the commit runs only on `policy_ack(approve:true)` →
863
1244
  * `policy:committed` (+ sha) → done; `approve:false` → cancelled. With
864
1245
  * `requireHumanAck:false` the commit runs directly at the green gate.
865
1246
  * onFail (WP2): when gate fails, re-prompts the session(s) (sendPrompt) up to
@@ -913,7 +1294,10 @@ interface ShellGateSpec {
913
1294
  command: string;
914
1295
  args?: string[];
915
1296
  /** Working directory for the gate command. Defaults to the watched
916
- * session's cwd, anchored to the workspace. */
1297
+ * session's own cwd (trusted as-is it was already an explicit,
1298
+ * vetted choice made at `agent_start` time). When given explicitly,
1299
+ * anchored to that session's cwd (not the daemon's boot workspace)
1300
+ * so a stray/injected value can't escape it. */
917
1301
  cwd?: string;
918
1302
  timeoutMs?: number;
919
1303
  }
@@ -970,7 +1354,7 @@ interface CommitSpec {
970
1354
  /**
971
1355
  * Human-in-the-loop gate before the commit actually runs. Default TRUE:
972
1356
  * the green gate transitions to `awaiting-ack` + emits `policy:commit-ready`
973
- * and waits for `ack_policy`. Set false to commit directly at the green gate.
1357
+ * and waits for `policy_ack`. Set false to commit directly at the green gate.
974
1358
  */
975
1359
  requireHumanAck?: boolean;
976
1360
  }
@@ -1073,6 +1457,15 @@ interface CompletionPolicySupervisor {
1073
1457
  */
1074
1458
  ack(policyId: string, approve: boolean): Promise<PolicyRunState | undefined>;
1075
1459
  list(): PolicyRunState[];
1460
+ /**
1461
+ * Subscribe to policy settlements — fired whenever a policy transitions
1462
+ * into a terminal state (done / blocked / cancelled) OR into awaiting-ack.
1463
+ * Used by the `/policies/:id/wait` long-poll to block until a policy
1464
+ * resolves without reimplementing the state machine's event surface.
1465
+ * Returns an unsubscribe fn. The callback receives the policyId; read
1466
+ * the full state via `getStatus(policyId)`.
1467
+ */
1468
+ onSettle(cb: (policyId: string) => void): () => void;
1076
1469
  /**
1077
1470
  * Sync flush of the policy snapshot to disk. Call from the daemon
1078
1471
  * stop() path — mirrors sessions.shutdown().
@@ -1080,34 +1473,13 @@ interface CompletionPolicySupervisor {
1080
1473
  shutdown(): void;
1081
1474
  }
1082
1475
 
1083
- /**
1084
- * Fire-and-forget webhook notifier for session lifecycle events.
1085
- *
1086
- * Each session can register its own `notifyUrl` (via `start_agent_session`).
1087
- * A global URL can be set via `AGENTPROTO_NOTIFY_URL` env var or
1088
- * `~/.agentproto/notify.json` (env wins). Both are POSTed when an event
1089
- * fires — the union of per-session + global URLs, deduplicated.
1090
- *
1091
- * Retry policy: one retry after 2 s on network error. No retry on 4xx/5xx.
1092
- * Timeout: 10 s per attempt. All errors are swallowed — the notifier never
1093
- * throws into the session's hot path.
1094
- */
1095
-
1096
- interface WebhookNotifier {
1097
- /** Register a per-session URL (called from start_agent_session). */
1098
- register(sessionId: string, url: string): void;
1099
- unregister(sessionId: string): void;
1100
- /** Handler to wire into SessionEventBus.onAny. Fire-and-forget. */
1101
- onSessionEvent(ev: SessionEvent): void;
1102
- }
1103
-
1104
1476
  /**
1105
1477
  * Orchestrator sub-gateway (ADR §4.2 / WP2) — the security primitive
1106
1478
  * that lets a spawned agent become a *scoped* orchestrator without
1107
1479
  * handing it the daemon's full toolset.
1108
1480
  *
1109
- * The plain `/mcp` endpoint registers EVERY tool (execute_command,
1110
- * fs_*, remote_*, import_mcp, terminals, driver CRUD) and bypasses auth
1481
+ * The plain `/mcp` endpoint registers EVERY tool (command_execute,
1482
+ * fs_*, remote_*, mcp_import, terminals, driver CRUD) and bypasses auth
1111
1483
  * on loopback — pointing a child at it would be a privilege handout.
1112
1484
  * This module builds a second, scoped MCP server that registers ONLY a
1113
1485
  * curated orchestration allowlist, gated by an unguessable per-child
@@ -1126,21 +1498,20 @@ interface WebhookNotifier {
1126
1498
  * The curated set of tools a scoped child orchestrator may call.
1127
1499
  *
1128
1500
  * INCLUDED — the orchestration primitives:
1129
- * start_agent_session, prompt_agent_session, get_agent_session_output,
1130
- * wait_for_any, poll_events (spawn + drive + fan-in)
1131
- * list_sessions, list_agent_sessions, kill_agent_session (observe +
1132
- * halt). NB: list/kill are NOT yet scoped to the child's own
1133
- * subtree — that subtree filter is WP4 (parentSessionId/depth). For
1134
- * WP2 they are exposed daemon-wide; documented debt.
1501
+ * agent_start, agent_prompt, agent_output, agent_kill,
1502
+ * session_monitor, session_events_poll (spawn + drive + fan-in)
1503
+ * session_list, session_tree (observe). NB: list is NOT yet scoped
1504
+ * to the child's own subtree that subtree filter is WP4
1505
+ * (parentSessionId/depth). For WP2 it is exposed daemon-wide;
1506
+ * documented debt.
1135
1507
  *
1136
1508
  * EXCLUDED — the danger surface (each a separate trust boundary):
1137
- * execute_command, terminal/PTY tools (raw shell), fs tools
1138
- * (read/write/delete file, create_directory, …), remote_* (publish
1139
- * the daemon to the internet), import_mcp / mcp_imported_* /
1140
- * list_discovered_mcps / remove_imported_mcp (widen the daemon's own
1141
- * MCP surface), and driver CRUD (create_/delete_/update_driver these
1142
- * never register here because the scoped server is built with no
1143
- * doctype specs).
1509
+ * command_execute, terminal/PTY tools (raw shell), file_* /
1510
+ * directory_* tools, remote_* (publish the daemon to the internet),
1511
+ * mcp_import / mcp_discovered_* / mcp_imported_* (widen the daemon's
1512
+ * own MCP surface), and driver CRUD (create_/delete_/update_driver —
1513
+ * these never register here because the scoped server is built with
1514
+ * no doctype specs).
1144
1515
  */
1145
1516
  declare const DEFAULT_ORCHESTRATOR_TOOLS: readonly string[];
1146
1517
  interface OrchestratorScope {
@@ -1169,6 +1540,18 @@ interface OrchestratorScope {
1169
1540
  /** Max concurrently-alive children the owning orchestrator may spawn
1170
1541
  * before further spawns are rejected. */
1171
1542
  maxChildren: number;
1543
+ /**
1544
+ * Name of the resolved role that owns this scope (spawn-role-
1545
+ * profiles / privilege lattice). This is the "parent role" the
1546
+ * `canSpawn` gate checks against when this scope is used to spawn a
1547
+ * further child (`session-spawn.ts`). Defaults to `"supervisor"` at
1548
+ * mint time when the caller doesn't say — every role that can reach
1549
+ * `orchestrator: true` today resolves to supervisor (executor's
1550
+ * `toolPolicy.delegation: "deny"` already excludes it from ever
1551
+ * minting a scope), so this preserves #214 behaviour exactly until a
1552
+ * custom role is actually granted delegation.
1553
+ */
1554
+ role: string;
1172
1555
  }
1173
1556
  /**
1174
1557
  * Narrow a caller-requested tool list to ⊆ a ceiling subset.
@@ -1197,6 +1580,9 @@ interface MintScopeOptions {
1197
1580
  maxDepth?: number;
1198
1581
  /** Per-orchestrator alive-child quota (default DEFAULT_MAX_CHILDREN). */
1199
1582
  maxChildren?: number;
1583
+ /** Name of the resolved role that owns this scope. Default
1584
+ * `"supervisor"` — see `OrchestratorScope.role`. */
1585
+ role?: string;
1200
1586
  }
1201
1587
  interface ScopeTokenRegistry {
1202
1588
  /** Mint a fresh scope-token. See `MintScopeOptions`. */
@@ -1228,16 +1614,21 @@ interface OrchestratorGatewayDeps {
1228
1614
  listAgentAdapters?: AgentAdapterLister;
1229
1615
  /** Orchestrator injector (WP3/WP4). When wired, a child orchestrator
1230
1616
  * driving the scoped server can itself spawn sub-orchestrators
1231
- * (`orchestrator: true` on its `start_agent_session`) — the new
1617
+ * (`orchestrator: true` on its `agent_start`) — the new
1232
1618
  * token inherits depth+1 and is bounded by the caller's tools
1233
1619
  * (non-re-grant). Omitted → recursion injection is unavailable on
1234
1620
  * the scoped surface (a child can still spawn plain sub-agents). */
1235
1621
  orchestratorInjector?: OrchestratorInjector;
1236
1622
  /** Optional webhook notifier — same singleton as the root /mcp server.
1237
- * When provided, per-session notifyUrl values from start_agent_session
1623
+ * When provided, per-session notifyUrl values from agent_start
1238
1624
  * are registered on spawn and unregistered on exit, so child
1239
1625
  * orchestrators spawning through this scoped gateway also fire webhooks. */
1240
1626
  webhookNotifier?: WebhookNotifier;
1627
+ /** Forwarded to `registerSessionTools` — the daemon's own plain `/mcp`
1628
+ * gateway URL, defaulted onto `hermes` `agent_start` spawns issued
1629
+ * through this scoped sub-gateway that pass no `mcpServers`. See
1630
+ * `RegisterAgentToolsOptions.daemonMcpUrl`. */
1631
+ daemonMcpUrl?: string;
1241
1632
  }
1242
1633
  type OrchestratorMcpServerFactory = (scope: OrchestratorScope) => Promise<McpServer>;
1243
1634
  /**
@@ -1310,37 +1701,109 @@ type OrchestratorInjector = (opts?: {
1310
1701
  /** Override the per-orchestrator child quota for the minted scope
1311
1702
  * (clamped to the caller's). Root-only knob. */
1312
1703
  maxChildren?: number;
1704
+ /** Name of the resolved role for the session THIS scope is being
1705
+ * minted for (i.e. the session becoming an orchestrator) — becomes
1706
+ * `OrchestratorScope.role`, the "parent role" a future spawn
1707
+ * through this scope is gated against. Default `"supervisor"`. */
1708
+ role?: string;
1313
1709
  }) => OrchestratorInjection;
1314
1710
  /**
1315
1711
  * Build the orchestrator injector. Closed over the gateway's
1316
1712
  * scope-token registry + session-event bus + HTTP port. Returns a
1317
- * function that the `start_agent_session` handler calls when its
1713
+ * function that the `agent_start` handler calls when its
1318
1714
  * `orchestrator` field is set.
1319
1715
  */
1320
1716
  declare function createOrchestratorInjector(deps: OrchestratorInjectorDeps): OrchestratorInjector;
1321
1717
 
1322
1718
  /**
1323
- * Tiny node:http server that fronts the runtime gateway.
1719
+ * Shared types for pluggable sandbox providers.
1720
+ *
1721
+ * The sandbox family is a `@agentproto/provider-kit` consumer, same shape as
1722
+ * the tunnel family (`remote-providers/types.ts`): {@link SandboxProviderHandle}
1723
+ * extends the kit's generic `AdapterHandle` (slug/name/version/description/
1724
+ * requiresSetup/check) with the AIP-36 capability namespace and the concrete
1725
+ * `@agentproto/sandbox` `SandboxProvider` (`boot()`) the handle wraps.
1726
+ */
1727
+
1728
+ /**
1729
+ * Declared capabilities of a sandbox provider — pure metadata, surfaced in
1730
+ * `list_sandbox_providers`. Never carries secrets. Namespace mirrors AIP-36
1731
+ * SANDBOX.md (`network.egress`, `mounts`, `lifecycle.pause_after_idle`,
1732
+ * `read_only`, `limits.timeout_ms`).
1733
+ */
1734
+ interface SandboxProviderCapabilities {
1735
+ /** Sandbox can be given a network egress allowlist (AIP-36 `network.egress`). */
1736
+ networkEgress: boolean;
1737
+ /** Sandbox supports mounting external filesystems (AIP-36 `mounts`). */
1738
+ mounts: boolean;
1739
+ /** Sandbox can be paused (not just killed) between turns (AIP-36 `lifecycle.pause_after_idle`). */
1740
+ lifecyclePause: boolean;
1741
+ /** Sandbox can be started read-only (AIP-36 `read_only`). */
1742
+ readOnly: boolean;
1743
+ /** Hard cap on `limits.timeout_ms`, when the provider enforces one. */
1744
+ maxTimeoutMs?: number;
1745
+ }
1746
+ /**
1747
+ * A sandbox provider as an adapter-kit handle. Rides on the kit's generic
1748
+ * {@link AdapterHandle} and adds the sandbox-specific `capabilities` plus
1749
+ * the `@agentproto/sandbox` `SandboxProvider` the handle wraps — the thing
1750
+ * `createSandboxAgentSessionHost` actually boots.
1751
+ */
1752
+ interface SandboxProviderHandle extends AdapterHandle {
1753
+ readonly provider: SandboxProvider;
1754
+ readonly capabilities: SandboxProviderCapabilities;
1755
+ /**
1756
+ * Credential fields this provider accepts via `setup_sandbox_provider`
1757
+ * (e.g. e2b's `apiKey`). Omit (or empty) when the provider needs no
1758
+ * credentials (e.g. `local`).
1759
+ */
1760
+ readonly setupFields?: readonly SetupField[];
1761
+ }
1762
+
1763
+ /**
1764
+ * Sandbox family on top of `@agentproto/provider-kit` — mirrors
1765
+ * `tunnel-adapters.ts`. This module is the entire bridge between the
1766
+ * generic kit and `@agentproto/sandbox`'s `SandboxProvider` concept: it
1767
+ * contributes nothing the kit already owns (catalog/status/creds/ledger/
1768
+ * list/MCP-tool plumbing); it only supplies the sandbox-family `TInfo`
1769
+ * (`SandboxAdapterInfo`), the static `SANDBOX_CATALOG`, and the resolver
1770
+ * that maps a catalog slug to a concrete {@link SandboxProviderHandle}
1771
+ * (`./sandbox-providers/registry.js`).
1324
1772
  *
1325
- * Routes:
1326
- * GET /health — { status, workspace, registered, uptime }
1327
- * GET /events — SSE stream of RuntimeEvent
1328
- * POST /mcp — MCP Streamable HTTP transport (POST)
1329
- * GET /mcp — MCP SSE response stream (GET)
1330
- * DELETE /mcp — close MCP session
1331
- * GET /conversations JSON list of conversation summaries
1332
- * GET /conversations/<id> — markdown body of one conversation
1333
- * POST /heartbeat/tick — force-fire one heartbeat tick
1773
+ * Kit primitives used:
1774
+ * - `makeCredsStore` → per-slug 0600 creds under `~/.agentproto/sandbox-creds/`
1775
+ * - `makeSetupLedger` → `~/.agentproto/setup/<slug>.json`
1776
+ * - `makeAdapterResolver` wraps the throwing `load` into null-on-miss
1777
+ * - `makeAdapterLister` → catalog status-classified `AdapterEntry[]`
1778
+ * - `makeListTool` → registers `list_sandbox_providers`
1779
+ * - `makeSetupTool` registers `setup_sandbox_provider` (multi-field
1780
+ * form: e2b's `apiKey`, sensitive)
1334
1781
  *
1335
- * No auth in `mode: "none"` (loopback default). `mode: "bearer"`
1336
- * checks `Authorization: Bearer <token>` against the configured
1337
- * token. Health is always public so external monitors can probe.
1782
+ * This is pure additive plumbing (introspection + setup) it does NOT wire
1783
+ * a `sandbox` field into `agent_start`. That lands in a follow-up PR; the
1784
+ * `resolveSandboxProvider`/`listSandboxProviders` overrides below exist now
1785
+ * so that later wiring can inject the same resolver/lister this module
1786
+ * builds by default, mirroring `resolveAgentAdapter`/`listAgentAdapters`.
1338
1787
  *
1339
- * MCP transport: stateless mode for v1each request is independent.
1340
- * Stateful session pinning can come later when long-running streaming
1341
- * tool calls become a real use case.
1788
+ * Security: `toSandboxInfo` exposes only `capabilities`never a cred
1789
+ * value (Appendix B). The setup tool's fields are marked SENSITIVE and the
1790
+ * result NEVER echoes any field value back.
1342
1791
  */
1343
1792
 
1793
+ /**
1794
+ * Family descriptor (`TInfo`). Pure metadata surfaced in
1795
+ * `list_sandbox_providers`. The kit's `AdapterEntry` already carries
1796
+ * slug/name/description/status/version, so the only sandbox-specific field
1797
+ * is the declared capability set. NEVER carries a cred value.
1798
+ */
1799
+ interface SandboxAdapterInfo {
1800
+ capabilities: SandboxProviderCapabilities;
1801
+ }
1802
+ /** Resolve a sandbox provider slug to a handle, or null when unavailable. */
1803
+ type SandboxProviderResolver = AdapterResolver<SandboxProviderHandle>;
1804
+ /** List every sandbox provider with its live status + capabilities. */
1805
+ type SandboxProviderLister = AdapterLister<SandboxAdapterInfo>;
1806
+
1344
1807
  /**
1345
1808
  * Pluggable adapter resolver — keeps the runtime package free of any
1346
1809
  * @agentproto/cli dep. The host (cli `serve`, playground, embedding
@@ -1361,12 +1824,36 @@ type AgentAdapterResolver = (slug: string) => Promise<{
1361
1824
  startSession(opts: {
1362
1825
  cwd: string;
1363
1826
  resumeSessionId?: string;
1364
- /** Model identifier forwarded from `start_agent_session`. For ACP
1827
+ /**
1828
+ * Manifest-declared mode id forwarded from `agent_start` (AIP-45
1829
+ * `AgentCliHandle.modes` — e.g. claude-code's `plan` /
1830
+ * `accept-edits` / `bypass-permissions`, codex's `read-only`,
1831
+ * mastracode/opencode's `plan`). Applied at spawn time via
1832
+ * `composeSpawn`'s mode patch (`bin_args_append` / `env`) — BEFORE
1833
+ * the child process is exec'd, unlike `model`/`effort` below.
1834
+ * Adapters with no declared `modes` (e.g. hermes) ignore it; an
1835
+ * unknown id for an adapter that DOES declare modes throws
1836
+ * `RuntimeConfigError` (composeSpawn validates against the
1837
+ * manifest, so a typo fails the spawn rather than silently no-op).
1838
+ */
1839
+ mode?: string;
1840
+ /**
1841
+ * Manifest-declared option id → value map forwarded from
1842
+ * `agent_start` (AIP-45 `AgentCliHandle.options` — e.g. hermes'
1843
+ * `skills`). Applied at spawn time via `composeSpawn`'s option
1844
+ * patches (`bin_args_prepend` / `bin_args_template` /
1845
+ * `bin_args_append_when_true` / `env`), validated against each
1846
+ * option's declared `type`/`enum`/`min`/`max`. An id the adapter
1847
+ * doesn't declare throws `RuntimeConfigError` (composeSpawn
1848
+ * validates against the manifest, same as an unknown `mode`).
1849
+ */
1850
+ options?: Record<string, boolean | number | string>;
1851
+ /** Model identifier forwarded from `agent_start`. For ACP
1365
1852
  * adapters this is applied via session/set_config_option after
1366
1853
  * newSession (the ACP wrapper does not forward CLI args to claude).
1367
1854
  * Adapters that don't support model selection ignore it. */
1368
1855
  model?: string;
1369
- /** Effort level forwarded from `start_agent_session`. Effort is
1856
+ /** Effort level forwarded from `agent_start`. Effort is
1370
1857
  * model-dependent — same label ≠ same budget across models; defaults
1371
1858
  * differ by model. Omit to keep the model's own default. Applied
1372
1859
  * via session/set_config_option on ACP adapters; others ignore it. */
@@ -1377,6 +1864,12 @@ type AgentAdapterResolver = (slug: string) => Promise<{
1377
1864
  * a host-chosen scoped toolset (e.g. the daemon's own orchestration
1378
1865
  * gateway). Adapters that don't model MCP mounting ignore it. */
1379
1866
  mcpServers?: AcpMcpServer[];
1867
+ /** Called on any adapter-process activity (ACP JSON-RPC traffic in
1868
+ * either direction) — forwarded to the driver's
1869
+ * `runtime.start({ onActivity })`. The caller (agent_start's MCP
1870
+ * handler, POST /sessions/agent) wires this to pulse
1871
+ * `SessionDescriptor.lastActivityAt` via `registry.pulseActivity(id)`. */
1872
+ onActivity?: () => void;
1380
1873
  }): Promise<AgentSessionLike>;
1381
1874
  /** Display label for the descriptor's `command` field. */
1382
1875
  commandPreview?: string;
@@ -1386,7 +1879,30 @@ type AgentAdapterResolver = (slug: string) => Promise<{
1386
1879
  tokensIn?: number;
1387
1880
  tokensOut?: number;
1388
1881
  } | null>;
1882
+ /** AIP-45 `options[]` this adapter's manifest declares (id + type only —
1883
+ * no spawn internals). Lets `session-spawn.ts` fold a config-level
1884
+ * `defaults.skills` list into `options.skills` using the shape the
1885
+ * manifest actually declared (e.g. hermes' comma-joined string),
1886
+ * instead of guessing. Omitted/empty ⇒ the skills normalization is a
1887
+ * documented no-op for that adapter (e.g. claude-code, which
1888
+ * auto-discovers skills and declares no such option). */
1889
+ declaredOptions?: readonly DeclaredAdapterOption[];
1389
1890
  } | null>;
1891
+ /**
1892
+ * UI-safe projection of an AIP-45 `modes[]` entry as surfaced by
1893
+ * `adapter_list`. Mirrors `@agentproto/cli`'s `AdapterMode` without
1894
+ * importing it (the runtime deliberately carries no cli dep — see the
1895
+ * `AgentAdapterLister` note above). Spawn internals (`bin_args_*`, `env`)
1896
+ * are intentionally omitted; `status` is normalised to `"active"` by the
1897
+ * lister when the manifest omits it, so a declared mode is never
1898
+ * silently statusless.
1899
+ */
1900
+ interface AdapterListMode {
1901
+ id: string;
1902
+ description?: string;
1903
+ status: "active" | "noop" | "planned";
1904
+ status_note?: string;
1905
+ }
1390
1906
  /**
1391
1907
  * Compact adapter metadata for the discovery endpoints. Independent
1392
1908
  * of the resolver function above — hosts that can list installed
@@ -1401,6 +1917,10 @@ interface AdapterListEntry {
1401
1917
  protocol: string;
1402
1918
  streaming: boolean;
1403
1919
  packageName: string;
1920
+ /** Declared operation modes with their honest support status, so a
1921
+ * client can see e.g. hermes' `lean` mode is a measured no-op instead
1922
+ * of being silently accepted. Empty when the adapter declares none. */
1923
+ modes: AdapterListMode[];
1404
1924
  }
1405
1925
  type AgentAdapterLister = () => Promise<AdapterListEntry[]>;
1406
1926
  interface AuthOptions {
@@ -1487,7 +2007,7 @@ interface InboundWatcher {
1487
2007
  }
1488
2008
 
1489
2009
  /**
1490
- * Browser family on top of `@agentproto/adapter-kit` — Phase 3 (lightest
2010
+ * Browser family on top of `@agentproto/provider-kit` — Phase 3 (lightest
1491
2011
  * adoption: no creds, no wizard, no catalog file).
1492
2012
  *
1493
2013
  * Provides the kit-compatible types (`BrowserAdapterHandle`) and the
@@ -1530,6 +2050,138 @@ declare function makeBrowserAdapterLister(opts: {
1530
2050
  resolveBrowserAdapter?: BrowserAdapterResolver;
1531
2051
  }): AdapterLister<BrowserAdapterInfo>;
1532
2052
 
2053
+ /**
2054
+ * Turns a raw tool-call name + args (and its eventual result) into a short,
2055
+ * human-readable line for ring-buffer / CLI / transcript rendering. Without
2056
+ * this, every tool call renders as a bare `[tool] view` with no args and no
2057
+ * outcome — this module is the one place that knows how to summarize both.
2058
+ */
2059
+ /** A one-line human summary of a tool call, e.g. `read src/foo.ts` or `⏰ wake in 30s — checking CI`. */
2060
+ declare function formatToolCall(toolName: string, args: unknown): string;
2061
+ /**
2062
+ * A short outcome line for a completed tool call, or `null` when there's
2063
+ * nothing useful to show (e.g. an empty/void result). `toolName` is
2064
+ * accepted for parity with `formatToolCall` and future bespoke result
2065
+ * formatting, but generic text extraction covers today's cases.
2066
+ */
2067
+ declare function formatToolResult(toolName: string | undefined, result: unknown, isError: boolean): string | null;
2068
+
2069
+ /**
2070
+ * Dependency-injection surface for MCP credential resolution.
2071
+ *
2072
+ * `packages/runtime` intentionally does NOT depend on `@agentproto/auth`;
2073
+ * the daemon bootstrap (e.g. `packages/cli`) can set a broker-backed
2074
+ * resolver here, and `session-spawn.ts` will call it when an `mcpServers`
2075
+ * entry carries `credentialRef`. Errors from the hook are treated as
2076
+ * non-fatal by the caller.
2077
+ */
2078
+ interface McpCredentialDeps {
2079
+ /** Resolve brokered headers for an `mcpServers` entry that names a
2080
+ * `credentialRef`. The returned headers are merged ON TOP of the
2081
+ * entry's static `headers` so brokered auth wins on collision. */
2082
+ resolveMcpCredentialHeaders?: (o: {
2083
+ credentialRef: string;
2084
+ signal?: AbortSignal;
2085
+ }) => Promise<Record<string, string> | undefined>;
2086
+ /** Resolve a single named secret (an env-var-shaped slug — e.g. a sandbox
2087
+ * spec's `env.passthrough` / `env.auth.state.env` entries) to its raw
2088
+ * value. Distinct from `resolveMcpCredentialHeaders` above (which
2089
+ * resolves an MCP `credentialRef` PATH into Authorization-style
2090
+ * headers): this is the `SecretResolver` shape `agent_start`'s sandbox
2091
+ * branch (`session-spawn.ts`) passes to `@agentproto/sandbox`'s
2092
+ * `SandboxSecretsConfig.resolver` — so a booted sandbox's env is filled
2093
+ * from the same host-injected broker as every other secret, never a
2094
+ * bare `process.env` read at the call site. Returns null when the slug
2095
+ * can't be resolved (missing/unconfigured). */
2096
+ resolveSandboxSecret?: (slug: string) => Promise<string | null>;
2097
+ }
2098
+ declare function setMcpCredentialDeps(d: McpCredentialDeps): void;
2099
+ declare function getMcpCredentialDeps(): McpCredentialDeps;
2100
+
2101
+ /**
2102
+ * Provider-preset family on top of `@agentproto/provider-kit` — the *thin,
2103
+ * honest* lister (Option B). Unlike tunnels/sandboxes/eval-reporters, presets
2104
+ * are bundled static data with no binary to install, no package to resolve, and
2105
+ * no setup ledger: their only "setup" is an ambient API-key env var. So this
2106
+ * module does NOT use `makeAdapterResolver` / `makeAdapterLister` / the wizard
2107
+ * (all install-oriented, a forced fit). It reuses only `makeListTool` — the one
2108
+ * kit primitive that's genuinely about "JSON a lister's output as an MCP tool."
2109
+ *
2110
+ * Status is derived honestly from the daemon's own environment (the env agents
2111
+ * spawn into), not the CLI caller's:
2112
+ * "ready" — `process.env[preset.keyEnv]` is set (key available to spawn)
2113
+ * "available" — key absent (preset known, usable once the env var is provided)
2114
+ *
2115
+ * There is no "supported" state: presets ship in-repo, so they're always at
2116
+ * least "available." `version` is the literal "built-in".
2117
+ *
2118
+ * Security (Appendix B): `PresetInfo` carries only `keyEnv` (the env-var NAME),
2119
+ * never a key value. The list tool never returns creds — there are none to
2120
+ * return; keys live in the operator's environment, not a creds store.
2121
+ */
2122
+
2123
+ /**
2124
+ * Manifest-declared AIP-45 preset, the minimum the catalog needs to merge
2125
+ * an adapter-contributed preset into the listing. Mirrors
2126
+ * `AgentCliPresetDeclaration`'s fields without importing
2127
+ * `@agentproto/driver-agent-cli` into the runtime package (same structural-
2128
+ * type pattern as `DeclaredAdapterOption` in spawn-defaults.ts).
2129
+ */
2130
+ interface DeclaredAdapterPreset {
2131
+ id: string;
2132
+ label: string;
2133
+ description?: string;
2134
+ schemaFlavor: ProviderPreset["schemaFlavor"];
2135
+ baseUrl: string;
2136
+ keyEnv: string;
2137
+ scrubEnv?: string[];
2138
+ defaultModel?: string;
2139
+ homepage?: string;
2140
+ }
2141
+ /**
2142
+ * Family descriptor surfaced as `AdapterEntry.info`. User-facing preset facts
2143
+ * only — no cred values (there are none in the registry), no adapter projection
2144
+ * detail (`scrubEnv` stays internal to adapter manifests).
2145
+ */
2146
+ interface PresetInfo {
2147
+ /** API schema flavor — which adapter family can consume this preset. */
2148
+ schemaFlavor: ProviderPreset["schemaFlavor"];
2149
+ /** Base URL the client hits. */
2150
+ baseUrl: string;
2151
+ /** Env-var NAME holding the API key (never the value). */
2152
+ keyEnv: string;
2153
+ /** Conventional default model id, if any. */
2154
+ defaultModel?: string;
2155
+ /** Homepage/docs URL. */
2156
+ homepage?: string;
2157
+ }
2158
+ /**
2159
+ * Normalize a manifest-declared preset into the canonical `ProviderPreset`
2160
+ * shape the catalog lists. `scrubEnv` defaults to empty (an adapter that
2161
+ * scrubs in code may omit it); `description` defaults to empty. Pure.
2162
+ */
2163
+ declare function declaredPresetToProviderPreset(decl: DeclaredAdapterPreset): ProviderPreset;
2164
+ /**
2165
+ * Map the static preset registry → status-classified catalog entries, reading
2166
+ * the daemon's environment for key presence. Pure (no I/O) save the `process`
2167
+ * .env read; safe to call on every list invocation.
2168
+ *
2169
+ * `adapterPresets` — manifest-declared presets contributed by loaded adapters
2170
+ * (Stage 3 AIP-45 `presets` field). Merged AFTER the built-in registry and
2171
+ * deduped by id: an adapter-declared id that collides with a built-in is
2172
+ * ignored (the registry is the source of truth for canonical providers), so
2173
+ * an adapter can't silently override moonshot/openrouter facts. The live
2174
+ * wiring that reads each adapter's `.presets` and passes them here is Stage 4;
2175
+ * today this is the tested seam.
2176
+ */
2177
+ declare function listPresets(env?: Record<string, string | undefined>, adapterPresets?: readonly DeclaredAdapterPreset[]): AdapterEntry<PresetInfo>[];
2178
+
2179
+ /** A journal-file StepCache scoped to one cacheKey. Best-effort: read/parse
2180
+ * failures degrade to "no cache" (a miss), never throw into the run. */
2181
+ declare function createFileStepCache(cacheKey: string, opts?: {
2182
+ dir?: string;
2183
+ }): StepCache;
2184
+
1533
2185
  /**
1534
2186
  * `.agentproto/` config dir — runtime-managed state at the root of
1535
2187
  * every workspace. Mirrors the `.git/` model: user-content stays at
@@ -1648,6 +2300,90 @@ declare function readRuntimeMeta(workspace: string): Promise<{
1648
2300
  */
1649
2301
  declare function sweepStaleRuntimeMetas(workspaces: readonly string[], currentWorkspace: string): Promise<string[]>;
1650
2302
 
2303
+ /**
2304
+ * MCP tools for event-driven orchestration:
2305
+ * - session_events_poll — cheap cursor-based snapshot of session events
2306
+ * - session_monitor — multiplexed long-poll (1 call for N sessions)
2307
+ *
2308
+ * These complement the existing per-session waitForTurnEnd inside
2309
+ * agent_output. The new tools handle multi-session fan-in
2310
+ * and external-client retrigger without burning polling tokens.
2311
+ */
2312
+
2313
+ /**
2314
+ * Event kind a session wait can target. Mirrors the `session_monitor`
2315
+ * `event` parameter. `turn-end` also matches `awaiting-input` (both
2316
+ * signal end-of-turn), matching the MCP tool's semantics.
2317
+ */
2318
+ type SessionWaitEvent = "turn-end" | "awaiting-input" | "exited" | "any";
2319
+ /**
2320
+ * Result of a single-session wait. On a hit, carries the session id, the
2321
+ * matched event (ring-bus form, without the `session:` prefix), the
2322
+ * session's current status, and whether it's awaiting input. On a
2323
+ * timeout, `timedOut: true` and the watched id list.
2324
+ */
2325
+ interface SessionWaitResult {
2326
+ timedOut?: boolean;
2327
+ sessionId?: string;
2328
+ event?: string;
2329
+ source?: "ring" | "state" | "bus";
2330
+ awaitingInput?: boolean;
2331
+ status?: string;
2332
+ turnsCompleted?: number;
2333
+ sessionIds?: string[];
2334
+ since?: number;
2335
+ /** Structured awaiting-input question (harness-parity). Surface on
2336
+ * turn-end / awaiting-input matches so callers (MCP session_monitor +
2337
+ * REST /sessions/:id/wait) can read the question without a separate
2338
+ * transcript fetch. Mirrors desc.awaitingQuestion / ev.question. */
2339
+ question?: SessionAwaitingQuestion;
2340
+ }
2341
+ /**
2342
+ * Block until one of the listed sessions fires a matching lifecycle event
2343
+ * (turn-end / awaiting-input / exited / any), or until `timeoutMs` elapses.
2344
+ *
2345
+ * This is the single-session-capable core that the MCP `session_monitor`
2346
+ * tool and the REST `GET /sessions/:id/wait` route both call. The MCP tool
2347
+ * passes N ids (multiplexed fan-in); the REST route passes exactly one.
2348
+ * The semantics are identical: race-free cursor replay → synchronous
2349
+ * already-in-target-state check → bus long-poll → timeout.
2350
+ *
2351
+ * `since` is an EventRing cursor: when provided, already-emitted matching
2352
+ * events for the watched sessions that occurred after that cursor are
2353
+ * returned immediately (race-free replay) before subscribing to the bus.
2354
+ */
2355
+ declare function monitorSessionWait(opts: {
2356
+ registry: SessionsRegistry;
2357
+ sessionEvents: SessionEventBus;
2358
+ eventRing: EventRing;
2359
+ sessionIds: string[];
2360
+ event?: SessionWaitEvent;
2361
+ timeoutMs?: number;
2362
+ since?: number;
2363
+ }): Promise<SessionWaitResult>;
2364
+ /**
2365
+ * Block until the named policy's status transitions out of the active
2366
+ * `watching` / `queued` / `gating` / `nudging` states — i.e. reaches
2367
+ * `done`, `blocked`, `awaiting-ack`, or `cancelled` — then return the
2368
+ * full PolicyRunState. Returns `{ timedOut: true }` on timeout.
2369
+ *
2370
+ * Hooks into the supervisor's `onSettle` callback (the single reliable
2371
+ * signal — some terminal transitions like `cancel()` / `ack(false)` /
2372
+ * single-session-exit emit no SessionEventBus event). The MCP
2373
+ * `policy_status` tool and the REST `GET /policies/:id/wait` route both
2374
+ * delegate here, so the wait semantics are shared across transports.
2375
+ */
2376
+ declare function monitorPolicyWait(opts: {
2377
+ supervisor: CompletionPolicySupervisor;
2378
+ policyId: string;
2379
+ timeoutMs?: number;
2380
+ }): Promise<{
2381
+ timedOut: true;
2382
+ } | {
2383
+ timedOut: false;
2384
+ state: PolicyRunState;
2385
+ }>;
2386
+
1651
2387
  /**
1652
2388
  * @agentproto/runtime — long-running gateway around an agentproto
1653
2389
  * workspace dir.
@@ -1703,18 +2439,18 @@ interface CreateGatewayOptions {
1703
2439
  * Without this, /sessions still works for raw `argv` spawns. */
1704
2440
  resolveAgentAdapter?: AgentAdapterResolver;
1705
2441
  /** Optional adapter lister — when provided, enables
1706
- * `GET /adapters` HTTP route + `list_adapters` MCP tool so UIs
2442
+ * `GET /adapters` HTTP route + `adapter_list` MCP tool so UIs
1707
2443
  * can discover what's installed on the host. */
1708
2444
  listAgentAdapters?: AgentAdapterLister;
1709
2445
  /** Optional browser adapter resolver — when provided, enables the
1710
2446
  * `start_browser` MCP tool (launches Camofox / Bureau / Chromium). */
1711
2447
  resolveBrowserAdapter?: BrowserAdapterResolver;
1712
2448
  /** Optional browser adapter lister — when provided, enables the
1713
- * `list_adapter_browsers` MCP tool. */
2449
+ * `browser_adapter_list` MCP tool. */
1714
2450
  listBrowserAdapters?: BrowserAdapterLister;
1715
2451
  /** Optional PTY factory (node-pty wrapper, typically from the cli
1716
2452
  * layer's `loadNodePtyFactory()`). When provided, enables
1717
- * `POST /sessions/terminal`, the `start_terminal_session` MCP
2453
+ * `POST /sessions/terminal`, the `terminal_start` MCP
1718
2454
  * tool family, and the `/sessions/:id/pty` WebSocket. Without it,
1719
2455
  * those routes return 501 / the MCP tools aren't registered. */
1720
2456
  spawnPty?: PtyFactory;
@@ -1731,6 +2467,30 @@ interface CreateGatewayOptions {
1731
2467
  /** When true, drop the localhost-wildcard defaults — only the
1732
2468
  * explicit `allowedOrigins` list is honoured. */
1733
2469
  strictOrigins?: boolean;
2470
+ /**
2471
+ * Opt-in deferred/lazy MCP tool loading (harness-parity item 3, see
2472
+ * `deferred-tools.ts`). When set, every root-gateway tool outside
2473
+ * `alwaysOn` registers but starts disabled — excluded from the first
2474
+ * `tools/list` — until the always-on `tool_search` meta-tool pulls it
2475
+ * in by keyword. Omitted (default) → every tool is eagerly enabled,
2476
+ * identical to pre-existing behaviour. Does not affect the scoped
2477
+ * `/mcp/orchestrator` sub-gateway, which already exposes a small
2478
+ * curated allowlist (`DEFAULT_ORCHESTRATOR_TOOLS`).
2479
+ */
2480
+ deferredTools?: {
2481
+ alwaysOn?: readonly string[];
2482
+ };
2483
+ /**
2484
+ * Optional sandbox provider resolver — overrides the sandbox family's
2485
+ * default resolver (built-ins + `@agentproto/sandbox-<slug>` dynamic
2486
+ * import) used by `list_sandbox_providers` / `setup_sandbox_provider`
2487
+ * AND `agent_start.sandbox`. Mirrors `resolveAgentAdapter`'s injection
2488
+ * shape.
2489
+ */
2490
+ resolveSandboxProvider?: SandboxProviderResolver;
2491
+ /** Optional sandbox provider lister — mirrors `listAgentAdapters`.
2492
+ * Overrides the default catalog-driven lister behind `list_sandbox_providers`. */
2493
+ listSandboxProviders?: SandboxProviderLister;
1734
2494
  }
1735
2495
  interface GatewayHandle {
1736
2496
  url: string;
@@ -1774,4 +2534,4 @@ interface GatewayHandle {
1774
2534
  */
1775
2535
  declare function createGateway(opts: CreateGatewayOptions): Promise<GatewayHandle>;
1776
2536
 
1777
- export { type AdapterListEntry, type AgentAdapterLister, type AgentAdapterResolver, type AgentSessionLike, type AgentStreamEvent, type BrowserAdapterHandle, type BrowserAdapterInfo, type BrowserAdapterLister, type BrowserAdapterResolver, BuildHeartbeatAgent, type CreateGatewayOptions, DEFAULT_ORCHESTRATOR_TOOLS, type GatewayHandle, type InboundWatcher, type OrchestratorGatewayDeps, type OrchestratorInjection, type OrchestratorInjector, type OrchestratorInjectorDeps, type OrchestratorMcpServerFactory, type OrchestratorScope, type RegisterBrowserInput, type RegisterSessionInput, type RuntimeMeta, type ScopeTokenRegistry, type SessionDescriptor, type SessionKind, type SessionStatus, type SessionsRegistry, type SpawnAgentInput, type SpawnSessionInput, type TunnelDescriptor, type TunnelProvider, type TunnelStatus, type WatcherDescriptor, type WatcherStartInput, WorkspaceFs, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createScopeTokenRegistry, daemonRegistryDir, makeBrowserAdapterLister, narrowOrchestratorTools, readDaemonRegistry, readRuntimeMeta, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };
2537
+ export { type AdapterListEntry, type AgentAdapterLister, type AgentAdapterResolver, type AgentSessionLike, type AgentStreamEvent, type AttachPolicyInput, type BrowserAdapterHandle, type BrowserAdapterInfo, type BrowserAdapterLister, type BrowserAdapterResolver, BuildHeartbeatAgent, type CommitSpec, type CompletionPolicySupervisor, type CreateGatewayOptions, DEFAULT_ORCHESTRATOR_TOOLS, type DeclaredAdapterPreset, type GateSpec, type GatewayHandle, type InboundWatcher, type JudgeGateSpec, type McpCredentialDeps, type OnFailSpec, type OrchestratorGatewayDeps, type OrchestratorInjection, type OrchestratorInjector, type OrchestratorInjectorDeps, type OrchestratorMcpServerFactory, type OrchestratorScope, type PolicyRunState, type PolicyRunStatus, type PresetInfo, type PricingResolver, type RegisterBrowserInput, type RegisterSessionInput, type RuntimeMeta, type SandboxAdapterInfo, type SandboxProviderCapabilities, type SandboxProviderHandle, type SandboxProviderLister, type SandboxProviderResolver, type ScopeTokenRegistry, type SessionDescriptor, type SessionKind, type SessionObserver, type SessionStatus, type SessionUsage, type SessionWaitEvent, type SessionWaitResult, type SessionsRegistry, type ShellGateSpec, type SpawnAgentInput, type SpawnSessionInput, type TokenPricing, type TunnelDescriptor, type TunnelProvider, type TunnelStatus, type UsageComputeInput, type UsageSource, type WatcherDescriptor, type WatcherStartInput, WorkspaceFs, composeSessionObservers, createFileStepCache, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createScopeTokenRegistry, daemonRegistryDir, declaredPresetToProviderPreset, deriveSessionUsage, formatToolCall, formatToolResult, getMcpCredentialDeps, listPresets, makeBrowserAdapterLister, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, projectSessionUsage, readDaemonRegistry, readRuntimeMeta, setMcpCredentialDeps, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };