@agent-compose/sdk 0.8.4 → 0.8.5

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.
Files changed (44) hide show
  1. package/dist/agent/agent-context.d.ts +9 -1
  2. package/dist/agent/agent-loop.d.ts +10 -1
  3. package/dist/client.d.ts +171 -33
  4. package/dist/directives.d.ts +14 -0
  5. package/dist/generated/verb-synopsis.d.ts +34 -0
  6. package/dist/index.d.ts +6 -4
  7. package/dist/index.js +1024 -39
  8. package/dist/runtimes/_cli-agent.d.ts +106 -0
  9. package/dist/runtimes/claude-code.d.ts +31 -1
  10. package/dist/runtimes/openai-desktop.d.ts +50 -0
  11. package/dist/runtimes/openai-desktop.js +1048 -57
  12. package/dist/runtimes/openai-desktop.test.d.ts +20 -0
  13. package/dist/runtimes/tool-pulse.test.d.ts +17 -0
  14. package/dist/sandbox/devbox.d.ts +5 -5
  15. package/dist/sandbox/registry.d.ts +12 -0
  16. package/dist/sandbox/sizes.d.ts +11 -5
  17. package/dist/sandbox.d.ts +1 -1
  18. package/dist/step-invocation/types.d.ts +1 -1
  19. package/dist/types/api-conversations.d.ts +85 -12
  20. package/dist/types/api-factory.d.ts +111 -1
  21. package/dist/types/conversation-stream.d.ts +22 -1
  22. package/dist/types/protocol.d.ts +118 -1
  23. package/dist/types/runtime.d.ts +71 -0
  24. package/package.json +1 -1
  25. package/src/agent/agent-context.ts +43 -9
  26. package/src/agent/agent-loop.ts +11 -5
  27. package/src/agent/desktop-open.ts +13 -1
  28. package/src/client.ts +256 -38
  29. package/src/directives.ts +21 -1
  30. package/src/generated/verb-synopsis.ts +544 -0
  31. package/src/index.ts +17 -3
  32. package/src/runtimes/_cli-agent.ts +313 -22
  33. package/src/runtimes/claude-code.ts +249 -12
  34. package/src/runtimes/openai-desktop.ts +82 -19
  35. package/src/sandbox/devbox.ts +5 -5
  36. package/src/sandbox/providers/e2b.ts +60 -16
  37. package/src/sandbox/registry.ts +19 -1
  38. package/src/sandbox/sizes.ts +11 -5
  39. package/src/sandbox.ts +1 -0
  40. package/src/types/api-conversations.ts +65 -13
  41. package/src/types/api-factory.ts +121 -1
  42. package/src/types/conversation-stream.ts +24 -1
  43. package/src/types/protocol.ts +113 -1
  44. package/src/types/runtime.ts +63 -0
@@ -6,7 +6,7 @@
6
6
  * at creation time.
7
7
  */
8
8
 
9
- import { SandboxNotFoundError, RateLimitError } from "e2b";
9
+ import { SandboxNotFoundError, RateLimitError, NotFoundError } from "e2b";
10
10
  import pRetry from "p-retry";
11
11
  import type { FailedAttemptError } from "p-retry";
12
12
  import type { SandboxProvider } from "../types/sandbox.js";
@@ -45,6 +45,24 @@ function isTransientSandboxError(error: unknown): boolean {
45
45
  .test(message);
46
46
  }
47
47
 
48
+ /** Did a CREATE fail because its create SOURCE (template alias / snapshot id)
49
+ * does not resolve on the provider — vs any other create fault? The server's
50
+ * recovery-boot fall-through keys on this: a ring snapshot licensed on an
51
+ * INDETERMINATE existence probe that turns out to be gone must fall through
52
+ * the ring toward the base image, while every other create fault (quota,
53
+ * auth, 5xx, network) must propagate — falling through on those would trade
54
+ * a still-recoverable machine state for a base boot. E2B maps the create-
55
+ * time 404 ("template not found") to its typed `NotFoundError`; the message
56
+ * match is the fallback for untyped transports (Vercel). Lives in the SDK
57
+ * because the server holds its OWN `e2b` module instance — an `instanceof`
58
+ * against the class from the wrong copy is always false. */
59
+ export function isSandboxSourceNotFoundError(error: unknown): boolean {
60
+ if (error instanceof NotFoundError) return true;
61
+ const message = error instanceof Error ? error.message : String(error ?? "");
62
+ return /(?:template|snapshot)[^\n]{0,80}not[\s_-]?found|not[\s_-]?found[^\n]{0,80}(?:template|snapshot)/i
63
+ .test(message);
64
+ }
65
+
48
66
  /** The ONE place the sandbox retry policy lives. Provisioning, reconnecting, and
49
67
  * snapshotting all race with the sandbox being paused/reclaimed; this runs the call
50
68
  * through p-retry's generic backoff, retrying ONLY the transient race (and failing
@@ -148,15 +148,21 @@ export function sandboxSizeLabel(size: SandboxSize): string {
148
148
  /** Stable E2B template ALIAS for the platform base at a given size
149
149
  * (`agent-compose-base-<size>`). Aliases — not snapshot ids — so the refs are
150
150
  * multi-account-clean: the same string resolves in any E2B account that built
151
- * the templates. Built by `infra/e2b-template/build.ts`; the E2B provider
152
- * boots this when a run on E2B declares no explicit `bootFrom`/template. */
151
+ * the templates. Built by `infra/e2b-template/build.ts`. An EXPLICIT
152
+ * `bootFrom`/template target only — nothing boots it by default: template-less
153
+ * E2B creates resolve `e2bAgentEnvTemplate` (the session-identical image)
154
+ * instead, so the run and session lanes can never diverge on baked tooling. */
153
155
  export function e2bBaseTemplate(size: SandboxSize): string {
154
156
  return `agent-compose-base-${size}`;
155
157
  }
156
158
 
157
- /** Stable E2B template ALIAS for the agent runtime (base + claude binary) at a
158
- * given size (`agent-env-<size>`). The agent default templates boot from this
159
- * via `bootFrom: { snapshotId: e2bAgentEnvTemplate(size) }`. Multi-account-clean
159
+ /** Stable E2B template ALIAS for the agent runtime (base + claude binary +
160
+ * dev toolbelt + session desktop) at a given size (`agent-env-<size>`).
161
+ * THE default E2B boot image, both lanes: every cloud SESSION boots it
162
+ * (server/src/sandbox/persistent.ts), and every template-less E2B workflow
163
+ * RUN boots it too (the e2b provider's size-matched default). The agent
164
+ * default templates boot from this via
165
+ * `bootFrom: { snapshotId: e2bAgentEnvTemplate(size) }`. Multi-account-clean
160
166
  * for the same reason as `e2bBaseTemplate`. */
161
167
  export function e2bAgentEnvTemplate(size: SandboxSize): string {
162
168
  return `agent-env-${size}`;
package/src/sandbox.ts CHANGED
@@ -65,6 +65,7 @@ export {
65
65
  reconnectSandbox,
66
66
  deleteSandboxSnapshot,
67
67
  snapshotResolves,
68
+ isSandboxSourceNotFoundError,
68
69
  getSandboxQuotas,
69
70
  listOwnedSandboxes,
70
71
  killSandboxById,
@@ -430,6 +430,49 @@ export interface BackgroundWorkHeld {
430
430
  leaseUntil: string;
431
431
  }
432
432
 
433
+ /** One background CHILD declared beside the busy lease (task #63): a
434
+ * detached process, by pid, with its durable journal/log file — what lets
435
+ * the platform VERIFY the work (`/proc/<pid>` + journal mtime) and
436
+ * reattach it after a park instead of losing it. */
437
+ export interface BackgroundWorkChildDecl {
438
+ pid: number;
439
+ /** Absolute guest-side path to the child's own durable output/journal
440
+ * file — its mtime is the progress evidence. */
441
+ journalPath?: string;
442
+ label?: string;
443
+ }
444
+
445
+ /** Outcome of requesting one machine size up
446
+ * (`POST /conversations/:id/machine/request-upsize` — task #110, the
447
+ * auto-resize policy's agent door). The platform arbitrates: within the
448
+ * team's daily cap the upsize is auto-granted (and lands immediately when
449
+ * no turn/background work holds the machine); past it a human approval
450
+ * card is posted. Wire shape mirrors
451
+ * server/src/sandbox/session-auto-resize.ts `UpsizeRequestOutcome`. */
452
+ export interface MachineUpsizeOutcome {
453
+ /** "executed" (landed now), "granted" (lands when the work settles),
454
+ * "pending_approval" (a card awaits the owner). Refusals arrive as HTTP
455
+ * errors carrying `code`. */
456
+ outcome: "executed" | "granted" | "pending_approval";
457
+ /** The size the grant/card names (e.g. "4vcpu-8gb"). */
458
+ size: string;
459
+ approvalId: string;
460
+ /** One human-readable line the CLI can print verbatim. */
461
+ message: string;
462
+ }
463
+
464
+ /** The session's background-work status
465
+ * (`GET /conversations/:id/background-work`). */
466
+ export interface BackgroundWorkStatus {
467
+ /** ISO deadline of the live lease, or null (no lease held). */
468
+ leaseUntil: string | null;
469
+ /** The declared children (each with its server-stamped `declaredAt`). */
470
+ children: Array<BackgroundWorkChildDecl & { declaredAt: string }>;
471
+ /** Non-null when a mid-work park froze declared children and the session
472
+ * still owes its conversation a status report (ISO park instant). */
473
+ frozenAt: string | null;
474
+ }
475
+
433
476
  // ── Session branch proposals (ADR-0053) ─────────────────────────────────────
434
477
  // Every cloud session works on its own factory-drive branch; the whole branch
435
478
  // is the unit of review, like a PR. Wire shapes mirror
@@ -511,11 +554,9 @@ export interface SessionChangeSet {
511
554
  * (hard cap 2; a manual re-review resets it). */
512
555
  reviewAutoFollowup?: boolean;
513
556
  reviewAutoRounds?: number;
514
- /** The OPT-IN main-advance auto-rebase reflex (additive — absent on
515
- * older servers; default OFF): armed ⇒ when main moves and a preflight
516
- * proves the fold clean, the platform rebases this session's branch
517
- * from main headlessly. Toggled via `setSessionAutoRebase`. */
518
- autoRebaseFromMain?: boolean;
557
+ /** The session's selectable cap on automatic follow-up rounds (additive
558
+ * — absent on older servers; default 2, range 1..5). */
559
+ reviewAutoRoundsMax?: number;
519
560
  /** STRUCTURED review suggestions beside the notes (additive — absent on
520
561
  * older servers): individually actionable {id, path, title, rationale,
521
562
  * patch} entries the review session wrote back, status-stamped
@@ -793,14 +834,6 @@ export interface SessionRebaseReport {
793
834
  foldSkipped?: boolean;
794
835
  }
795
836
 
796
- /** Result of `POST /conversations/:id/changes/autorebase` — the opt-in
797
- * main-advance auto-rebase reflex's new state. */
798
- export interface SessionAutoRebaseState {
799
- object: "session_autorebase";
800
- conversationId: string;
801
- autoRebaseFromMain: boolean;
802
- }
803
-
804
837
  /** The sender's page stamp (HUD bar sends) — persisted server-side, never
805
838
  * echoed back on the wire. Mirrors the server's `PageContext` schema. */
806
839
  export interface ConversationPageContext {
@@ -1004,3 +1037,22 @@ export interface StreamConversationOptions {
1004
1037
  lastEventId?: number;
1005
1038
  signal?: AbortSignal;
1006
1039
  }
1040
+
1041
+ /** Shared chat creation includes its own Ivy unless explicitly disabled. */
1042
+ export interface CreateChatInput {
1043
+ title?: string;
1044
+ visibility: "shared";
1045
+ access?: "public" | "private";
1046
+ memberIds?: string[];
1047
+ includeIvy?: boolean;
1048
+ }
1049
+ export interface ChannelIvyState {
1050
+ projects: Array<{ id: string; name: string }>;
1051
+ agent: AgentListRow | null;
1052
+ enabled: boolean; agentId: string | null; canManage: boolean; nextReviewAt: string | null;
1053
+ work: Array<{ id: string; title: string; status: string; evidence: string; nextAction: string; waitingOn: string | null; reviewAt: string | null }>;
1054
+ }
1055
+ export interface ProjectIvyConnection {
1056
+ id: string; teamId: string; projectId: string; provider: "github"; installationId: string;
1057
+ accountLogin: string; repositories: string[]; allowWrites: boolean; configuredBy: string | null; updatedAt: string;
1058
+ }
@@ -107,9 +107,15 @@ export interface RegisterWorkflowInput {
107
107
  export interface TemplateRow {
108
108
  name: string;
109
109
  version: string;
110
- factorySlug: string;
110
+ /** Owning factory's slug. `null` on platform-published rows — a published
111
+ * default belongs to no single factory and is invokable via any slug. */
112
+ factorySlug: string | null;
111
113
  /** Share scope (ADR-0045) — `null` = unscoped/grandfathered team tier. */
112
114
  scope: ArtifactScope | null;
115
+ /** `factory` = registered in a factory of the caller's team; `published` =
116
+ * platform default (team-wide read+invoke in EVERY factory, never write;
117
+ * fork it to edit under a new name). Optional: older servers omit it. */
118
+ origin?: "factory" | "published";
113
119
  }
114
120
 
115
121
  /** Template detail (`GET /factories/:slug/templates/:name`) — the typed
@@ -132,6 +138,17 @@ export interface ListTemplatesOptions {
132
138
  factorySlug?: string;
133
139
  }
134
140
 
141
+ /** One team connector grant, as `GET /api/v1/connectors` returns it (wire
142
+ * shape verbatim — snake_case). The typed subset the SDK pins; additional
143
+ * fields flow through untyped. */
144
+ export interface ConnectorGrantSummary {
145
+ id: string;
146
+ provider: string;
147
+ external_account_label: string | null;
148
+ status: "active" | "revoked" | "reauth_required";
149
+ connected_by_user_id: string;
150
+ }
151
+
135
152
  // ── Factory-file search (wire shape mirrors routes/factory-files.ts) ────────
136
153
 
137
154
  /** Public "anyone with the link" state — the additive `public` field on file
@@ -381,6 +398,27 @@ export interface SessionSecretRequestCreated {
381
398
  requestId: string;
382
399
  url: string;
383
400
  expiresAt: string;
401
+ /** True = a standing auto-approve set already filled the request at the
402
+ * mint — nothing is pending; source the session env now. */
403
+ autoApproved?: boolean;
404
+ }
405
+
406
+ /** What KIND of credential a vault request asks for (advisory): the vault
407
+ * page leads with the user's MATCHING standing entries. */
408
+ export type VaultRequestKind =
409
+ | "login" | "password" | "api_key" | "payment_card" | "env_file" | "note" | "other";
410
+
411
+ /** One STANDING vault entry usable for a session (the catalog read,
412
+ * 2026-08-31) — labels, kinds, and field NAMES only; values are write-only
413
+ * and never on this wire. */
414
+ export interface VaultCatalogEntry {
415
+ label: string;
416
+ kind: string;
417
+ fieldKeys: string[];
418
+ via: "owner" | "member" | "project";
419
+ ownerName: string | null;
420
+ projectName: string | null;
421
+ lastUsedAt: string | null;
384
422
  }
385
423
 
386
424
  /** A vault link's polled status. */
@@ -389,6 +427,26 @@ export interface SessionSecretRequestStatus {
389
427
  status: "pending" | "fulfilled" | "cancelled" | "expired";
390
428
  keys: string[];
391
429
  expiresAt: string;
430
+ /** Cancellation attribution (task #111) — present on cancelled rows:
431
+ * who ended the ask ('human' = denied; 'agent' = withdrawn) and why. */
432
+ cancelledVia?: "human" | "agent";
433
+ cancelReason?: string | null;
434
+ /** The denying human's display name (best-effort; 'human' via only). */
435
+ deniedByName?: string | null;
436
+ /** Newest USER message in the session SINCE the mint (pending rows only)
437
+ * — the chat-interrupt probe: the human may be answering in chat while
438
+ * the agent blocks on --wait. */
439
+ userMessageAt?: string;
440
+ }
441
+
442
+ /** One OPEN vault request, as the list endpoint returns it — receipts
443
+ * only (keys + reason + clocks), never values. */
444
+ export interface SessionSecretRequestSummary {
445
+ requestId: string;
446
+ keys: string[];
447
+ reason: string | null;
448
+ expiresAt: string;
449
+ createdAt: string;
392
450
  }
393
451
 
394
452
  export interface CreateApiKeyInput {
@@ -515,6 +573,23 @@ export interface DriveRepoLink {
515
573
  * auto-unlink; capped at 100 branches). Explicit `trackedBranches`:
516
574
  * index 0 is the PRIMARY and keeps the plain placement; every additional
517
575
  * branch lands at the sibling `dirPrefix@<sanitized-branch>` placement. */
576
+ /** POST /factories/:slug/repo-links/native — a repo whose origin IS the
577
+ * platform git plane (no GitHub side). `name` is ONE segment of GitHub's
578
+ * repo alphabet; `dirPrefix` defaults to `repos/<name>`. */
579
+ export interface CreateNativeRepoInput {
580
+ name: string;
581
+ dirPrefix?: string;
582
+ }
583
+
584
+ export interface CreateNativeRepoResult {
585
+ link: DriveRepoLink;
586
+ /** Stock-git clone/push URL on the platform origin (credentials are
587
+ * minted per operation by `agentc repos git-credential --link`). */
588
+ repoUrl: string;
589
+ /** The CLI one-liner: `agentc repos clone <id> --via origin`. */
590
+ cloneCommand: string;
591
+ }
592
+
518
593
  export interface CreateDriveRepoLinkInput {
519
594
  dirPrefix: string;
520
595
  repoFullName: string;
@@ -524,3 +599,48 @@ export interface CreateDriveRepoLinkInput {
524
599
  /** Two-way sync ("push-out") — defaults to TRUE for new links. */
525
600
  pushOut?: boolean;
526
601
  }
602
+
603
+ /** A retained merge conflict. Resolution edits a live session branch;
604
+ * publication still goes through its normal merge and approval gates. */
605
+ /** A project-scoped secret's METADATA (scoped-secrets spec §4.1) — names
606
+ * only; values are write-only. Reaches SESSIONS linked into the project whose
607
+ * owner is a live member; workflow runs never receive project secrets. */
608
+ export interface ProjectSecretMeta {
609
+ id: string;
610
+ projectId: string;
611
+ secretKey: string;
612
+ createdBy: string | null;
613
+ delivery: "injected" | "brokered";
614
+ brokerHost: string | null;
615
+ brokerHeaderName: string | null;
616
+ brokerHeaderScheme: string | null;
617
+ createdAt: string | null;
618
+ updatedAt: string | null;
619
+ }
620
+
621
+ export interface ProjectSecretList {
622
+ secrets: ProjectSecretMeta[];
623
+ tier: "project";
624
+ audience: "sessions";
625
+ }
626
+
627
+ export interface FactoryFileConflict {
628
+ id: string;
629
+ path: string;
630
+ conversationId: string | null;
631
+ runId: string | null;
632
+ runLabel: string | null;
633
+ reason: "same_region" | "binary" | "delete_vs_edit";
634
+ oursHash: string | null;
635
+ theirsHash: string | null;
636
+ baseHash: string | null;
637
+ status: string;
638
+ createdAt: string;
639
+ resolvesTo: "branch" | "main";
640
+ }
641
+ export interface FactoryFileConflictList {
642
+ object: "list";
643
+ data: FactoryFileConflict[];
644
+ has_more: boolean;
645
+ next_cursor: string | null;
646
+ }
@@ -130,6 +130,15 @@ export interface ConversationTurnStateEvent {
130
130
  pendingCount: number;
131
131
  at: number;
132
132
  partial: true;
133
+ /** When the open turn started (ms) — carried by the connect-time
134
+ * snapshot frame only; absent on live transition frames. */
135
+ startedAt?: number | null;
136
+ /** The session holds a live background-work lease (snapshot frames
137
+ * only). */
138
+ leaseHeld?: boolean;
139
+ /** The open turn is blocked on a HUMAN ask — an open vault/secret
140
+ * request or approval (snapshot frames only). */
141
+ blockedOnAsk?: "secrets" | "approval" | null;
133
142
  }
134
143
 
135
144
  /** One attached surface (a dashboard tab, an `agentc session` TUI process)
@@ -185,6 +194,19 @@ export interface ConversationSessionStatusEvent {
185
194
  at: number;
186
195
  }
187
196
 
197
+ /** Live-only tool-run pulse (2026-08-29): the running turn's guest session
198
+ * PROVED progress (CPU/output counters advanced between heartbeat pulse
199
+ * samples) while the stream was otherwise silent — a long foreground tool
200
+ * (`bun install`) is working. Emitted at the executor's evidence-tick
201
+ * cadence, only on proof; carries no output content. Same contract as
202
+ * `turn_state`: never persisted, never advances `Last-Event-ID`. */
203
+ export interface ConversationToolPulseEvent {
204
+ event: "tool_pulse";
205
+ conversationId: string;
206
+ advancing: true;
207
+ at: number;
208
+ }
209
+
188
210
  export type ConversationStreamEvent =
189
211
  | ConversationPartEvent
190
212
  | ConversationPartPartialEvent
@@ -195,7 +217,8 @@ export type ConversationStreamEvent =
195
217
  | ConversationReplayContinueEvent
196
218
  | ConversationTurnStateEvent
197
219
  | ConversationPresenceEvent
198
- | ConversationSessionStatusEvent;
220
+ | ConversationSessionStatusEvent
221
+ | ConversationToolPulseEvent;
199
222
 
200
223
  /**
201
224
  * Fold one parsed SSE frame into a `ConversationStreamEvent`.
@@ -147,6 +147,61 @@ export interface AgentMessageTaskNotification extends AgentMessageBase {
147
147
  usage?: { tokens?: number; toolUses?: number; durationMs?: number };
148
148
  }
149
149
 
150
+ /** One entry of a harness workflow's live progress feed — the
151
+ * `workflow_progress` array claude-code's `system`/`task_progress` events
152
+ * carry for its in-harness Workflow tool (the dynamic-workflow
153
+ * orchestrator). `phase` entries are the script's declared phases (seeded
154
+ * up front, 1-based `index`); `agent` entries are the spawned workflow
155
+ * agents, updated in place as they queue → run → settle. Preview text the
156
+ * harness includes (prompt/result previews) is internal plumbing and is
157
+ * deliberately NOT forwarded — same rule as task notifications. */
158
+ export type WorkflowProgressEntry =
159
+ | { kind: "phase"; index: number; title: string }
160
+ | {
161
+ kind: "agent"; index: number; label: string;
162
+ /** Lifecycle: `start` (spawned) / `progress` (heartbeat) are live;
163
+ * `done` / `error` are settled. Verbatim from the harness. */
164
+ state: "start" | "progress" | "done" | "error";
165
+ phaseIndex?: number; phaseTitle?: string;
166
+ model?: string; agentId?: string;
167
+ /** Self-reported usage so far (cumulative for this agent). */
168
+ tokens?: number; toolCalls?: number; durationMs?: number;
169
+ /** Epoch ms the agent actually started (for live elapsed). */
170
+ startedAt?: number;
171
+ /** The failure message when `state: "error"`, clamped. */
172
+ error?: string;
173
+ /** Replayed from a resume cache — settled instantly, no fresh spend. */
174
+ cached?: true;
175
+ /** Skipped by the user (workflow dialog) — an error state that is
176
+ * not a failure. */
177
+ skipped?: true;
178
+ };
179
+
180
+ /** LIVE progress of a harness BACKGROUND task (claude-code
181
+ * `system`/`task_progress`). For the in-harness Workflow tool the message
182
+ * carries the CUMULATIVE `workflow_progress` entry array (the harness
183
+ * re-sends the whole picture: state changes immediately, heartbeats
184
+ * throttled ~10s), so a consumer treats the latest message as
185
+ * authoritative per entry `(kind, index)`. A plain background AGENT task
186
+ * (Task tool, run_in_background) heartbeats on the same event with NO
187
+ * workflow entries — forwarded with `workflow: []` as liveness evidence
188
+ * so the platform can declare the running task as session background
189
+ * work; its completion evidence rides `task_notification`. `toolUseId`
190
+ * names the spawning call — the correlation key to its card. Additive
191
+ * kind: existing producers never emit it. */
192
+ export interface AgentMessageTaskProgress extends AgentMessageBase {
193
+ type: "task_progress";
194
+ /** The harness's background task id. */
195
+ taskId: string;
196
+ /** The SPAWNING Workflow/Task call's tool_use id, when carried. */
197
+ toolUseId?: string;
198
+ /** The task's cumulative usage totals so far. */
199
+ usage?: { tokens?: number; toolUses?: number; durationMs?: number };
200
+ /** The cumulative workflow progress entries — EMPTY for a plain
201
+ * background Agent-task heartbeat (only Workflow tasks carry entries). */
202
+ workflow: WorkflowProgressEntry[];
203
+ }
204
+
150
205
  /** HARNESS-authored notice text — content the CLI composed itself rather
151
206
  * than the model speaking: slash-command stdout, model/skills advisories,
152
207
  * queued-input notes. claude-code marks these structurally (assistant
@@ -160,6 +215,60 @@ export interface AgentMessageHarnessNotice extends AgentMessageBase {
160
215
  text: string;
161
216
  }
162
217
 
218
+ /** Context-compaction lifecycle — the harness summarizing its own
219
+ * conversation to reclaim context. Mapped 1:1 from claude-code's wire
220
+ * (verified live on 2.1.212, the baked sandbox pin, and 2.1.241 — both
221
+ * emit the identical shapes, `/compact` and auto alike):
222
+ *
223
+ * `system`/`status` `{status:"compacting"}` → phase "start"
224
+ * `system`/`status` `{status:null, compact_result, → phase "settled"
225
+ * compact_error?}`
226
+ * `system`/`compact_boundary` `{compact_metadata: → phase "boundary"
227
+ * {trigger, pre_tokens, post_tokens,
228
+ * cumulative_dropped_tokens, duration_ms, …}}`
229
+ *
230
+ * Order on the wire: start → settled → (fresh init) → boundary → the
231
+ * continuation summary as a SYNTHETIC user message (never forwarded — it
232
+ * quotes conversation content verbatim). "boundary" only follows a
233
+ * successful settle and only on the turn the compaction ran (verified: it
234
+ * does NOT replay on later resumes). A compaction can span MINUTES of
235
+ * otherwise-silent stream — the whole point of forwarding it is that
236
+ * downstream can show the silence as work (the 2026-08-23 dead-air
237
+ * incident: 94% auto-compact read as a dead session). Additive kind:
238
+ * existing producers never emit it. */
239
+ export interface AgentMessageCompaction extends AgentMessageBase {
240
+ type: "compaction";
241
+ phase: "start" | "settled" | "boundary";
242
+ /** settled: how it ended. Absent on start/boundary (a boundary IS a
243
+ * success by construction — the harness only emits it after one). */
244
+ result?: "success" | "failed";
245
+ /** settled+failed: the harness's own reason, clamped. */
246
+ error?: string;
247
+ /** boundary: what initiated the compaction. */
248
+ trigger?: "auto" | "manual";
249
+ /** boundary: context tokens before / after, dropped total, wall time. */
250
+ preTokens?: number;
251
+ postTokens?: number;
252
+ droppedTokens?: number;
253
+ durationMs?: number;
254
+ }
255
+
256
+ /** A user-role message landing INSIDE a subagent's thread — the delivered
257
+ * form of a steer (the parent's `SendMessage` to a RUNNING child, queued
258
+ * "for delivery at its next tool round") or any other message the harness
259
+ * folds into a child's conversation mid-flight. Emitted ONLY with sidechain
260
+ * attribution: `parentToolUseId` (the spawning Agent/Task call's tool_use
261
+ * id) is REQUIRED — an unattributed user event is the parent's own prompt
262
+ * echo, which stays unmapped as before. Lets renderers show the steer as a
263
+ * user-role message inside the child's mini-session instead of leaving it
264
+ * an opaque SendMessage tool call on the parent only (task #97, owner
265
+ * directive 2026-08-27). Additive kind: existing producers never emit it. */
266
+ export interface AgentMessageSubagentUserMessage extends AgentMessageBase {
267
+ type: "subagent_user_message";
268
+ text: string;
269
+ parentToolUseId: string;
270
+ }
271
+
163
272
  export type AgentMessage =
164
273
  | AgentMessageInit
165
274
  | AgentMessageText
@@ -173,7 +282,10 @@ export type AgentMessage =
173
282
  | AgentMessageUsageDelta
174
283
  | AgentMessagePlan
175
284
  | AgentMessageTaskNotification
176
- | AgentMessageHarnessNotice;
285
+ | AgentMessageTaskProgress
286
+ | AgentMessageHarnessNotice
287
+ | AgentMessageCompaction
288
+ | AgentMessageSubagentUserMessage;
177
289
 
178
290
  /** Status block the agent emits to signal iteration completion or blockers. */
179
291
  export interface AgentStatus {
@@ -94,6 +94,16 @@ export interface RuntimeOptions {
94
94
  * Absent ⇒ nothing extra is sourced. Must be a plain absolute path — no
95
95
  * quotes, no `..`; the runtime validates and drops anything else. */
96
96
  credEnvFile?: string;
97
+ /** Durable launch report (boot-time turn adoption): called by the durable
98
+ * detached transport the moment its in-guest runner exists — with the
99
+ * guest prompt path (every durable file derives from it), the exit
100
+ * sentinel string, and the detached wrapper's pid. The caller stamps
101
+ * these on the turn row so a SUCCESSOR process (a deploy roll's new
102
+ * server) can re-attach to the runner's durable `.out` without this
103
+ * process's memory. Fired once per detached launch (a retry with a fresh
104
+ * runner fires again with the new paths); never on transports without
105
+ * durable files. Must not throw — the transport calls it inline. */
106
+ onDetachedLaunch?: (info: { promptPath: string; sentinel: string; pid: number }) => void;
97
107
  }
98
108
 
99
109
  /** Three-valued liveness verdict for a runtime's CURRENT turn, read from
@@ -182,6 +192,21 @@ export interface ModelExecutionContract {
182
192
  * fallback probe it already had; null is never a verdict.
183
193
  */
184
194
  probeTurnLiveness?(): Promise<RunnerLivenessVerdict | null>;
195
+ /**
196
+ * TELEMETRY, NEVER A VERDICT (tool-run pulse, 2026-08-29): the newest
197
+ * tool-run pulse the durable liveness probe carried — the guest
198
+ * heartbeat's sample of the runner's own session (aggregate CPU jiffies,
199
+ * written bytes, live process count) plus the guest clock it was read
200
+ * against. The executor's evidence ticker peeks it AFTER its liveness
201
+ * check and compares successive samples: counters ADVANCING is proof a
202
+ * long silent foreground tool is working, fanned to clients as a live
203
+ * `tool_pulse` frame. Never probes on its own; null before any probe or
204
+ * on a transport without the pulse file. No liveness decision may ever
205
+ * read it.
206
+ */
207
+ peekTurnPulse?(): {
208
+ atMs: number; cpuJiffies: number; ioBytes: number; procs: number; guestNowMs: number;
209
+ } | null;
185
210
  /**
186
211
  * DOORBELL, NEVER A VERDICT (exit-event push, v0.10.43): wake the current
187
212
  * turn's durable watchdog NOW so it runs its normal verification pass —
@@ -232,6 +257,44 @@ export interface ModelExecutionContract {
232
257
  * Calls are serialized per turn; never throws.
233
258
  */
234
259
  injectUserMessage?(text: string): Promise<"delivered" | "pending" | "closed" | "unsupported">;
260
+ /**
261
+ * Request an in-band STEP INTERRUPT of the currently running turn — the
262
+ * ESC equivalent. Where `injectUserMessage` queues content for the turn
263
+ * loop's next boundary, this rides the same durable inbox but carries a
264
+ * control line the CLI handles immediately, mid-step included: the
265
+ * running tool call aborts, the run ends within ~100ms, and the guest
266
+ * session stays resumable with the whole turn context (verified live
267
+ * against claude 2.1.236). Verdicts mirror `injectUserMessage`; only
268
+ * "delivered" means the CLI got the control line — callers escalate
269
+ * anything else (and "unsupported": no stream-input turn, or a runtime
270
+ * with no in-band interrupt, e.g. codex) to kill semantics, which stay
271
+ * honest because thread stores are durable and the successor turn
272
+ * resumes them. Never throws.
273
+ */
274
+ interruptTurn?(): Promise<"delivered" | "pending" | "closed" | "unsupported">;
275
+ /**
276
+ * Guest pid of the CURRENT turn's detached runner wrapper (the setsid
277
+ * process-group leader recorded at launch), or null when no detached
278
+ * durable-transport runner is live (boot phase, ACP path, single-exec
279
+ * transports). Advisory identity, NEVER a liveness verdict: the platform
280
+ * reads it to DECLARE harness-reported background work (an in-harness
281
+ * Workflow task) against the process tree that hosts it, so the park
282
+ * machinery can verify the tree from `/proc/<pid>` later. Runtimes
283
+ * without a detached guest simply omit the method.
284
+ */
285
+ currentRunnerPid?(): number | null;
286
+ /**
287
+ * Durable byte offset of the CURRENT turn's `.out` file just past the
288
+ * last line whose messages have ALL been yielded to the consumer — the
289
+ * safe harvest watermark for boot-time turn adoption. Null when no
290
+ * durable-transport turn is live, or before the first line completes.
291
+ * The contract is deliberately one line BEHIND the parse cursor: a
292
+ * caller that persists parts after each yielded message may stamp this
293
+ * offset at any time and a successor re-parses AT MOST the line whose
294
+ * parts were mid-persist (the same crash window the workflow tailer's
295
+ * flush-before-advance ordering accepts). Advisory, never a verdict.
296
+ */
297
+ currentTurnDurableOffset?(): number | null;
235
298
  sendMessage(opts: {
236
299
  prompt: string;
237
300
  sessionId?: string;