@agent-compose/sdk 0.8.2 → 0.8.4

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 (43) hide show
  1. package/dist/agent/__tests__/perf-sampler.test.d.ts +10 -0
  2. package/dist/agent/agent-context.d.ts +1 -1
  3. package/dist/agent/agent-loop.d.ts +9 -1
  4. package/dist/agent/desktop-open.d.ts +184 -0
  5. package/dist/agent/perf-sampler.d.ts +99 -0
  6. package/dist/agent/services-manifest.d.ts +88 -0
  7. package/dist/agent/services-restore.d.ts +58 -0
  8. package/dist/client.d.ts +164 -8
  9. package/dist/display.d.ts +17 -0
  10. package/dist/index.d.ts +14 -5
  11. package/dist/index.js +1393 -53
  12. package/dist/runtimes/_cli-agent.d.ts +359 -2
  13. package/dist/runtimes/claude-code.d.ts +12 -0
  14. package/dist/runtimes/codex.d.ts +8 -0
  15. package/dist/runtimes/openai-desktop.js +1329 -53
  16. package/dist/runtimes/session-env.test.d.ts +14 -0
  17. package/dist/sandbox.d.ts +1 -1
  18. package/dist/types/api-conversations.d.ts +309 -1
  19. package/dist/types/api-factory.d.ts +115 -10
  20. package/dist/types/api-runs.d.ts +21 -0
  21. package/dist/types/protocol.d.ts +44 -1
  22. package/dist/types/runtime.d.ts +120 -0
  23. package/package.json +1 -1
  24. package/src/agent/agent-context.ts +100 -11
  25. package/src/agent/agent-loop.ts +15 -3
  26. package/src/agent/desktop-open.ts +418 -0
  27. package/src/agent/perf-sampler.ts +202 -0
  28. package/src/agent/services-manifest.ts +356 -0
  29. package/src/agent/services-restore.ts +195 -0
  30. package/src/client.ts +328 -12
  31. package/src/display.ts +44 -1
  32. package/src/index.ts +65 -2
  33. package/src/runtimes/_cli-agent.ts +911 -35
  34. package/src/runtimes/claude-code.ts +198 -14
  35. package/src/runtimes/codex.ts +58 -1
  36. package/src/sandbox/providers/e2b.ts +29 -1
  37. package/src/sandbox/providers/local.ts +16 -4
  38. package/src/sandbox.ts +1 -1
  39. package/src/types/api-conversations.ts +307 -3
  40. package/src/types/api-factory.ts +118 -10
  41. package/src/types/api-runs.ts +23 -0
  42. package/src/types/protocol.ts +44 -1
  43. package/src/types/runtime.ts +122 -0
package/src/client.ts CHANGED
@@ -23,7 +23,7 @@ import type {
23
23
  RunStatus, ResumePauseOptions, ResumePauseResponse, AnswerSteerOptions,
24
24
  RequestAgentPauseOptions, RequestAgentPauseResponse,
25
25
  SendAgentMessageOptions, SendAgentMessageResponse,
26
- RunDetail, RunListEntry, ListRunsOptions, TimelineEvent, RunFundingResponse, EventRow, ReportEventInput,
26
+ RunDetail, RunListEntry, ListRunsOptions, TimelineEvent, RunFundingResponse, RunStepUsageResponse, EventRow, ReportEventInput,
27
27
  ListEventsOptions, ListEventsResult, RunArtifactRow, RunLogLine, ListRunLogsOptions,
28
28
  CancelRunResponse, ListSnapshotsOptions, SnapshotListResponse, SnapshotListEntry, RunSnapshotEntry,
29
29
  } from "./types/api-runs.js";
@@ -33,9 +33,13 @@ import type {
33
33
  CreateCloudSessionInput, CloudSessionCreated,
34
34
  SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked, BackgroundWorkHeld,
35
35
  SessionChangeSet, SessionMergeGated, SessionMergeReport, SessionDiscardReport,
36
+ SessionChangePreflight, SessionRebaseReport, SessionAutoRebaseState,
37
+ ReviewSuggestionInput, ReviewNotesPublished, ReviewGitCredential,
36
38
  SendConversationMessageInput, SendConversationMessageResult,
37
39
  ChannelSessionRow, ChannelSessionsResponse, SessionChannelMessagePosted,
40
+ SessionDirectMessageSent, BranchClaimGranted, BranchClaimsReleased, BranchClaimState,
38
41
  ConversationPresenceSnapshot, StreamConversationOptions, AgentListRow,
42
+ SessionPerfHistoryPage,
39
43
  } from "./types/api-conversations.js";
40
44
  import type {
41
45
  ConversationMemberRole, ConversationMember, ArtifactScope,
@@ -49,9 +53,10 @@ import type {
49
53
  import type {
50
54
  RegisterResult, RegisterWorkflowInput, TemplateRow, TemplateDetail, ListTemplatesOptions,
51
55
  SearchFactoryFilesOptions, FactoryFileSearchResult, FactoryFileWriteResult,
52
- FactoryRow, CreateFactoryInput, UpdateFactoryInput,
56
+ FactoryFileListRow, ListFactoryFilesOptions, FactoryFileListPage,
57
+ FactoryRow, CreateFactoryInput, UpdateFactoryInput, FactoryPerfSummary,
53
58
  ScheduleRow, CreateScheduleInput,
54
- SecretOptions, SetSecretResult, SecretListEntry,
59
+ SecretOptions, SetSecretResult, SecretListEntry, SessionSecretEntry, SessionSecretInput, SessionSecretRequestCreated, SessionSecretRequestStatus,
55
60
  CreateApiKeyInput, ApiKey, ApiKeyCreated, ListApiKeysOptions, ApiKeyPage, UsageResponse,
56
61
  DriveRepoLink, CreateDriveRepoLinkInput,
57
62
  DriveMountSession, CreateDriveMountSessionInput,
@@ -73,6 +78,7 @@ export type {
73
78
  SendAgentMessageOptions, SendAgentMessageResponse,
74
79
  RunDetail, RunListEntry, ListRunsOptions, TimelineEvent,
75
80
  FundingLane, RunFundingStamp, RunFundingUsageRow, RunFundingResponse,
81
+ RunStepUsage, RunStepUsageResponse,
76
82
  EventSubjectType, EventRow, RunArtifactRow, ReportEventInput, ListEventsOptions, ListEventsResult,
77
83
  RunLogLine, ListRunLogsOptions, CancelRunResponse,
78
84
  ListSnapshotsOptions, SnapshotListEntry, SnapshotListResponse, RunSnapshotEntry,
@@ -84,11 +90,15 @@ export type {
84
90
  CreateCloudSessionInput, CloudSessionCreated, ConversationThread,
85
91
  SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked, BackgroundWorkHeld,
86
92
  SessionFileChange, SessionChangeSet, SessionChangeStats, SessionDiffReviewBound, ReviewSuggestion,
93
+ ReviewSuggestionInput, ReviewNotesPublished, ReviewGitCredential, SessionReviewDocument,
87
94
  ReviewSuggestionDecision, ReviewSuggestionDecisions,
88
95
  SessionMergeReportDetail, SessionMergeReport, SessionMergeGated, SessionDiscardReport,
96
+ SessionPreflightFileRow, SessionChangePreflight, SessionRebaseReport, SessionAutoRebaseState,
89
97
  ConversationPageContext, SendConversationMessageInput, ConversationTurnState,
90
98
  SendConversationMessageResult, UnnotifiedMention, ConversationPresenceSnapshot, AgentListRow, StreamConversationOptions,
91
99
  ChannelSessionStatus, ChannelSessionRow, ChannelSessionsResponse, SessionChannelMessagePosted,
100
+ SessionDirectMessageSent, BranchClaimInfo, BranchClaimGranted, BranchClaimsReleased, BranchClaimState,
101
+ SessionPerfBucket, SessionPerfHistoryPage,
92
102
  } from "./types/api-conversations.js";
93
103
  export type {
94
104
  ConversationMemberRole, ConversationMember,
@@ -105,10 +115,11 @@ export type {
105
115
  RegisterResult, RegisteredRuntime, RuntimeSourceInput, TemplateSourceRef, RegisterWorkflowInput,
106
116
  TemplateRow, TemplateDetail, ListTemplatesOptions,
107
117
  FactoryFileSearchRow, FactoryFolderSearchRow, SearchFactoryFilesOptions, FactoryFileSearchResult, FactoryFileWriteResult,
118
+ FactoryFileListRow, ListFactoryFilesOptions, FactoryFileListPage,
108
119
  PublicFileLinkState,
109
- FactoryRow, CreateFactoryInput, UpdateFactoryInput,
120
+ FactoryRow, CreateFactoryInput, UpdateFactoryInput, FactoryPerfSummary,
110
121
  ScheduleRow, CreateScheduleInput,
111
- SecretOptions, SetSecretResult, SecretListEntry,
122
+ SecretOptions, SetSecretResult, SecretListEntry, SessionSecretEntry, SessionSecretInput, SessionSecretRequestCreated, SessionSecretRequestStatus,
112
123
  CreateApiKeyInput, ApiKey, ApiKeyCreated, ListApiKeysOptions, ApiKeyPage,
113
124
  UsageRollupRow, UsageResponse,
114
125
  DriveRepoLink, CreateDriveRepoLinkInput,
@@ -165,6 +176,17 @@ interface FactoryFileWriteWire {
165
176
  created: boolean;
166
177
  }
167
178
 
179
+ /** `GET /factories/:slug/files` — the rows are camelCase on the wire
180
+ * already; only the envelope is snake_case. `at` appears exactly when the
181
+ * request carried `?at=`. */
182
+ interface FactoryFileListWire {
183
+ object: "list";
184
+ data: FactoryFileListRow[];
185
+ has_more: boolean;
186
+ next_cursor: string | null;
187
+ at?: { head: string; committed_at: string | null };
188
+ }
189
+
168
190
  export interface AgentComposeClientOptions {
169
191
  /** Your team's API key — minted from the dashboard or `agentc keys create`.
170
192
  * Required. Resolved from `process.env.AGENT_COMPOSE_API_KEY` when omitted. */
@@ -653,6 +675,16 @@ export class AgentComposeClient {
653
675
  );
654
676
  }
655
677
 
678
+ /** Attested platform tokens attributed to each of a run's steps, derived
679
+ * server-side from the spend ledger by step time window. Empty `steps` is
680
+ * an honest answer (legacy run, or funded outside the platform lane —
681
+ * that money is never metered). */
682
+ async getRunStepUsage(runId: string): Promise<RunStepUsageResponse> {
683
+ return this.fetch<RunStepUsageResponse>(
684
+ `/api/v1/workflows/${encodeURIComponent(runId)}/step-usage`,
685
+ );
686
+ }
687
+
656
688
  /** Report a durable event against a run. Events are late-binding facts
657
689
  * like quality.accepted, defect.regression, or intervention.override. */
658
690
  async reportEvent(runId: string, input: ReportEventInput): Promise<EventRow> {
@@ -745,7 +777,7 @@ export class AgentComposeClient {
745
777
  * U+FFFD and the original bytes are unrecoverable). */
746
778
  async getFactoryFileBytes(
747
779
  path: string,
748
- opts?: { factorySlug?: string; revision?: number; branch?: string },
780
+ opts?: { factorySlug?: string; revision?: number; branch?: string; at?: string },
749
781
  ): Promise<Uint8Array> {
750
782
  const factorySlug = opts?.factorySlug
751
783
  ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined)
@@ -756,6 +788,10 @@ export class AgentComposeClient {
756
788
  // session's own `session-<uuid>` branch, where its unmerged work lives.
757
789
  // Mutually exclusive with `revision` (the server rejects the combination).
758
790
  if (opts?.branch !== undefined) q.set("branch", opts.branch);
791
+ // Browse-at-head (read-only): the file AS OF a retained 64-hex commit of
792
+ // the MAIN drive. A garbage-collected/unknown head answers 410
793
+ // `at_unavailable`.
794
+ if (opts?.at !== undefined) q.set("at", opts.at);
759
795
  const buf = await this.fetch<ArrayBuffer, "arrayBuffer">(
760
796
  `/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?${q}`,
761
797
  { responseType: "arrayBuffer" },
@@ -768,7 +804,7 @@ export class AgentComposeClient {
768
804
  * text corrupts the bytes irreversibly. */
769
805
  async getFactoryFile(
770
806
  path: string,
771
- opts?: { factorySlug?: string; revision?: number; branch?: string },
807
+ opts?: { factorySlug?: string; revision?: number; branch?: string; at?: string },
772
808
  ): Promise<string> {
773
809
  return new TextDecoder().decode(await this.getFactoryFileBytes(path, opts));
774
810
  }
@@ -841,6 +877,31 @@ export class AgentComposeClient {
841
877
  return this.fetch<SessionsPage>(`/api/v1/sessions?${q}`);
842
878
  }
843
879
 
880
+ /** A session's sandbox perf HISTORY — 15-minute rollup buckets
881
+ * (CPU/mem/disk percentiles, load, saturation seconds), newest first.
882
+ * Member-gated like the session itself. Cursor-paginated. */
883
+ getSessionPerfHistory(
884
+ conversationId: string, opts?: { limit?: number; cursor?: string },
885
+ ): Promise<SessionPerfHistoryPage> {
886
+ const q = new URLSearchParams();
887
+ if (opts?.limit !== undefined) q.set("limit", String(opts.limit));
888
+ if (opts?.cursor) q.set("cursor", opts.cursor);
889
+ return this.fetch<SessionPerfHistoryPage>(
890
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/perf-history${q.toString() ? `?${q}` : ""}`);
891
+ }
892
+
893
+ /** Team-level sandbox perf aggregates over a window (default trailing
894
+ * 24h, max 30 days). Aggregate-only — no per-session identifiers. */
895
+ getFactoryPerfSummary(
896
+ factorySlug: string, opts?: { from?: string; to?: string },
897
+ ): Promise<FactoryPerfSummary> {
898
+ const q = new URLSearchParams();
899
+ if (opts?.from) q.set("from", opts.from);
900
+ if (opts?.to) q.set("to", opts.to);
901
+ return this.fetch<FactoryPerfSummary>(
902
+ `/api/v1/factories/${encodeURIComponent(factorySlug)}/perf-summary${q.toString() ? `?${q}` : ""}`);
903
+ }
904
+
844
905
  /** One conversation with its latest page of messages. `limit` bounds the
845
906
  * page (newest N) — metadata-only consumers (e.g. resolving the
846
907
  * session's drive branch) pass 1 instead of pulling the full hydrate. */
@@ -945,6 +1006,51 @@ export class AgentComposeClient {
945
1006
  );
946
1007
  }
947
1008
 
1009
+ /** Message ANOTHER session's conversation AS the calling session
1010
+ * (`agentc session message @alias`). Session toolbelt credential ONLY —
1011
+ * the sender resolves from the key. `target` is an @alias (team-scoped)
1012
+ * or a conversation uuid. Unlike a channel post this WAKES the target's
1013
+ * turn machinery; the response's `turn` verdict says whether it started,
1014
+ * queued behind a live turn, or was parked by the agent-to-agent
1015
+ * exchange damper. Unknown alias → 404 with `candidates`. */
1016
+ sendSessionMessage(input: { target: string; text: string }): Promise<SessionDirectMessageSent> {
1017
+ return this.fetch<SessionDirectMessageSent>("/api/v1/session-messages", {
1018
+ method: "POST", body: input,
1019
+ });
1020
+ }
1021
+
1022
+ /** Claim ANOTHER session's drive branch for the calling session
1023
+ * (`agentc branch claim @alias`) — transfers the branch's one-writer
1024
+ * authority: the target's own writes refuse honestly until release, and
1025
+ * the response carries the mount token the CLI uses to FUSE-mount the
1026
+ * branch in-guest. Session toolbelt credential only; same-owner sessions
1027
+ * only (cross-user claims refuse with the gap stated); refuses while the
1028
+ * target is mid-turn (409 `mid_turn`) or already claimed
1029
+ * (409 `already_claimed`). */
1030
+ claimSessionBranch(input: { target: string; reason?: string }): Promise<BranchClaimGranted> {
1031
+ return this.fetch<BranchClaimGranted>("/api/v1/session-branch-claims", {
1032
+ method: "POST", body: { target: input.target, ...(input.reason ? { reason: input.reason } : {}) },
1033
+ });
1034
+ }
1035
+
1036
+ /** Release branch claims HELD by the calling session (`agentc branch
1037
+ * release [@alias]`). `target` narrows to one claim; omitted releases
1038
+ * them all. Idempotent — an empty `released` list means nothing was
1039
+ * held. */
1040
+ releaseSessionBranchClaims(input: { target?: string } = {}): Promise<BranchClaimsReleased> {
1041
+ return this.fetch<BranchClaimsReleased>("/api/v1/session-branch-claims/release", {
1042
+ method: "POST", body: { ...(input.target ? { target: input.target } : {}) },
1043
+ });
1044
+ }
1045
+
1046
+ /** One session's branch-claim state: the live claim ON its branch (null =
1047
+ * writable there) and the claims it HOLDS (`agentc branch status`, the
1048
+ * dashboard's Changes-panel badge). Any viewer with access. */
1049
+ getSessionBranchClaimState(conversationId: string): Promise<BranchClaimState> {
1050
+ return this.fetch<BranchClaimState>(
1051
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/branch-claim`);
1052
+ }
1053
+
948
1054
  // ── Cloud-session developer surface (ADR-0052) ────────────────────────────
949
1055
 
950
1056
  /** Open a dev preview on a cloud session: expose a port a dev server is
@@ -1029,9 +1135,82 @@ export class AgentComposeClient {
1029
1135
  * writes `main` directly. The list reflects the branch's last flush and
1030
1136
  * is advisory; a merge re-verifies against fresh state. 404 = not found
1031
1137
  * or not a member (uniform). */
1032
- getSessionChanges(conversationId: string): Promise<SessionChangeSet> {
1138
+ getSessionChanges(
1139
+ conversationId: string,
1140
+ opts: { after?: string; limit?: number } = {},
1141
+ ): Promise<SessionChangeSet> {
1142
+ // Graph-plane pagination (additive): `after` = the previous page's
1143
+ // `nextAfter` cursor; `limit` 1..1000. Older servers ignore both.
1144
+ const q = new URLSearchParams();
1145
+ if (opts.after !== undefined) q.set("after", opts.after);
1146
+ if (opts.limit !== undefined) q.set("limit", String(opts.limit));
1147
+ const tail = q.toString();
1033
1148
  return this.fetch<SessionChangeSet>(
1034
- `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes`,
1149
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes${tail ? `?${tail}` : ""}`,
1150
+ );
1151
+ }
1152
+
1153
+ /** One proposed file's RAW BYTES from a session's change set
1154
+ * (`GET /conversations/:id/changes/content`) — the review surface's
1155
+ * per-file read, also the in-sandbox `agentc review file` door.
1156
+ * `side: "base"` serves the PRE-change side (graph plane; the legacy
1157
+ * plane ignores it and always serves the proposed side). 404 = missing
1158
+ * path or no access (uniform, no existence oracle). */
1159
+ async getSessionChangeContent(
1160
+ conversationId: string, path: string, opts: { side?: "base" } = {},
1161
+ ): Promise<Uint8Array> {
1162
+ const q = new URLSearchParams({ path });
1163
+ if (opts.side !== undefined) q.set("side", opts.side);
1164
+ const buf = await this.fetch<ArrayBuffer, "arrayBuffer">(
1165
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/content?${q}`,
1166
+ { responseType: "arrayBuffer" },
1167
+ );
1168
+ return new Uint8Array(buf);
1169
+ }
1170
+
1171
+ /** Publish a diff review's WHOLE document — prose notes plus structured
1172
+ * suggestions, FULL REPLACE on every call (`POST
1173
+ * /conversations/:id/review-notes`; `agentc review publish`). Callable
1174
+ * ONLY with the bound review session's own toolbelt credential — any
1175
+ * other key (another session, a human cookie, a plain API key) refuses
1176
+ * 403. `conversationId` is the REVIEWED session's conversation (in a
1177
+ * review sandbox: `AGENT_COMPOSE_REVIEW_OF_CONVERSATION_ID`).
1178
+ *
1179
+ * Failures: 403 `session_credential_required` | `not_review_session` |
1180
+ * `review_superseded` (a newer review replaced this one); 400
1181
+ * `suggestion_path_not_in_change_set` (naming the offenders); 409
1182
+ * `review_disabled`. */
1183
+ publishReviewNotes(
1184
+ conversationId: string,
1185
+ input: { notes: string; suggestions?: ReviewSuggestionInput[] },
1186
+ ): Promise<ReviewNotesPublished> {
1187
+ return this.fetch<ReviewNotesPublished>(
1188
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/review-notes`,
1189
+ {
1190
+ method: "POST",
1191
+ body: {
1192
+ notes: input.notes,
1193
+ ...(input.suggestions !== undefined ? { suggestions: input.suggestions } : {}),
1194
+ },
1195
+ },
1196
+ );
1197
+ }
1198
+
1199
+ /** Mint ONE fresh short-lived credential for the review git remote
1200
+ * (`POST /conversations/:id/review-git-credential`) — the server half
1201
+ * of the review sandbox's git credential helper (`agentc review
1202
+ * git-credential`), which calls this per git operation so the remote's
1203
+ * credential can never expire mid-review. Callable only with a review
1204
+ * session's own toolbelt credential; `conversationId` is the REVIEWED
1205
+ * session's conversation (`AGENT_COMPOSE_REVIEW_OF_CONVERSATION_ID`).
1206
+ *
1207
+ * Failures: 403 `session_credential_required` | `not_review_session`;
1208
+ * 409 `review_disabled` | `git_remote_unavailable` (legacy plane — read
1209
+ * the proposal through `getSessionChanges` instead). */
1210
+ getReviewGitCredential(conversationId: string): Promise<ReviewGitCredential> {
1211
+ return this.fetch<ReviewGitCredential>(
1212
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/review-git-credential`,
1213
+ { method: "POST" },
1035
1214
  );
1036
1215
  }
1037
1216
 
@@ -1076,6 +1255,59 @@ export class AgentComposeClient {
1076
1255
  );
1077
1256
  }
1078
1257
 
1258
+ /** The conflict PREFLIGHT: which of the session's changed files would
1259
+ * conflict with main if merged right now — the real merge's own per-file
1260
+ * classification, report-only (no writes, no locks). Read-tier.
1261
+ * Discriminate on `state`: `pending` means a cold compute is still
1262
+ * running — poll again to collect it; `unavailable` = no cheap anchors
1263
+ * (legacy plane / unbranched) — degrade honestly. */
1264
+ getSessionChangePreflight(conversationId: string): Promise<SessionChangePreflight> {
1265
+ return this.fetch<SessionChangePreflight>(
1266
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/preflight`,
1267
+ );
1268
+ }
1269
+
1270
+ /** Rebase the session's branch ON main — fold main's current state
1271
+ * into the session's workspace (the reverse merge; main is never
1272
+ * touched). Conflicting files KEEP the session's version: each is
1273
+ * recorded as a conflict row and the session is told in-conversation to
1274
+ * reconcile. Write-tier — a session's own toolbelt key may rebase its
1275
+ * OWN branch (unlike merge/discard, which need a human). Pass
1276
+ * `opts.branch` to fail 409 `branch_changed` if it rotated since review.
1277
+ *
1278
+ * Failures: 409 `plane_unsupported` (legacy drive plane) |
1279
+ * `review_session_scratch` | `branch_moved` (the branch advanced
1280
+ * mid-rebase — retry) | `turn_active` | `decision_in_progress` |
1281
+ * `freshen_failed` / `merge_retry` (mount mid-handoff — retry); 400
1282
+ * `no_branch`; 502 `rebase_failed` (nothing changed — retry-safe). */
1283
+ rebaseSessionChanges(
1284
+ conversationId: string,
1285
+ opts: { branch?: string } = {},
1286
+ ): Promise<SessionRebaseReport> {
1287
+ return this.fetch<SessionRebaseReport>(
1288
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/rebase`,
1289
+ { method: "POST", body: opts.branch ? { branch: opts.branch } : {} },
1290
+ );
1291
+ }
1292
+
1293
+ /** Arm/disarm the OPT-IN main-advance auto-rebase reflex for a session
1294
+ * (default OFF): when armed and the drive's main moves, the platform
1295
+ * preflights and — only on a provably clean fold (zero conflicts) —
1296
+ * rebases the session's branch from main headlessly, posting a quiet
1297
+ * receipt. Trade-off (why default OFF): the agent's view of files it
1298
+ * did not touch can change mid-task. Human, write-tier act.
1299
+ *
1300
+ * Failures: 409 `review_session_scratch` | `plane_unsupported`; 403
1301
+ * (needs a user-bound human caller with write access). */
1302
+ setSessionAutoRebase(
1303
+ conversationId: string, enabled: boolean,
1304
+ ): Promise<SessionAutoRebaseState> {
1305
+ return this.fetch<SessionAutoRebaseState>(
1306
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/autorebase`,
1307
+ { method: "POST", body: { enabled } },
1308
+ );
1309
+ }
1310
+
1079
1311
  // ── Conversation membership (ADR-0045) ────────────────────────────────────
1080
1312
  // Channel rosters carry roles (`owner > write > read`). Inviting is a
1081
1313
  // `write` action; removing and re-roling are `owner` (or team admin)
@@ -1167,10 +1399,15 @@ export class AgentComposeClient {
1167
1399
  * scoped, not surface-scoped: any member can cancel, whichever surface
1168
1400
  * started the turn. The canceled turn closes with a terminal error part
1169
1401
  * (it is never re-run); a message sent after it stays queued and is
1170
- * answered next. `canceled: false` = nothing was in flight. Requires
1402
+ * answered next. `canceled: false` = nothing was in flight. `owed`
1403
+ * reports the queued-backlog kick: 'dispatched' = a turn answering the
1404
+ * queue was started by this call; 'held' = a live release loop owns the
1405
+ * backlog; 'none' = nothing queued (absent on older servers). Requires
1171
1406
  * the `invoke` scope. */
1172
- cancelConversationTurn(conversationId: string): Promise<{ canceled: boolean }> {
1173
- return this.fetch<{ canceled: boolean }>(
1407
+ cancelConversationTurn(
1408
+ conversationId: string,
1409
+ ): Promise<{ canceled: boolean; owed?: "dispatched" | "held" | "none" }> {
1410
+ return this.fetch<{ canceled: boolean; owed?: "dispatched" | "held" | "none" }>(
1174
1411
  `/api/v1/conversations/${encodeURIComponent(conversationId)}/cancel-turn`,
1175
1412
  { method: "POST", body: {} },
1176
1413
  );
@@ -1233,6 +1470,33 @@ export class AgentComposeClient {
1233
1470
  );
1234
1471
  }
1235
1472
 
1473
+ /** List a factory drive's files — a flat recursive listing, cursor-
1474
+ * paginated by path. The live index by default; pass `at` (a retained
1475
+ * 64-hex commit) to browse the MAIN drive as of that head — read-only
1476
+ * time travel, with `branch` + `at` pinning a graph branch instead. A
1477
+ * garbage-collected/unknown head throws 410 `at_unavailable`. */
1478
+ async listFactoryFiles(opts?: ListFactoryFilesOptions): Promise<FactoryFileListPage> {
1479
+ const factorySlug = opts?.factorySlug
1480
+ ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined)
1481
+ ?? DEFAULT_FACTORY;
1482
+ const q = new URLSearchParams();
1483
+ if (opts?.prefix) q.set("prefix", opts.prefix);
1484
+ if (opts?.cursor) q.set("cursor", opts.cursor);
1485
+ if (opts?.limit != null) q.set("limit", String(opts.limit));
1486
+ if (opts?.branch !== undefined) q.set("branch", opts.branch);
1487
+ if (opts?.at !== undefined) q.set("at", opts.at);
1488
+ const qs = q.toString();
1489
+ const wire = await this.fetch<FactoryFileListWire>(
1490
+ `/api/v1/factories/${encodeURIComponent(factorySlug)}/files${qs ? `?${qs}` : ""}`,
1491
+ );
1492
+ return {
1493
+ data: wire.data,
1494
+ hasMore: wire.has_more,
1495
+ nextCursor: wire.next_cursor,
1496
+ at: wire.at ? { head: wire.at.head, committedAt: wire.at.committed_at } : null,
1497
+ };
1498
+ }
1499
+
1236
1500
  /** List the human members of your team — the people you (or an agent) can
1237
1501
  * @-flag with `createMentions`. Each row's `userId` is what
1238
1502
  * `mentionedUserIds` expects. */
@@ -1684,6 +1948,58 @@ export class AgentComposeClient {
1684
1948
  return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets/${encodeURIComponent(key)}`, { method: "DELETE" });
1685
1949
  }
1686
1950
 
1951
+ // ── Session secrets ─────────────────────────────────────────────────────────
1952
+ // Per-session env for a cloud session's sandbox, keyed by the session's
1953
+ // CONVERSATION id. Values are write-only (set/rotate/delete — never read
1954
+ // back); each secret has an OWNER (the user who set it) and only the owner
1955
+ // or a team admin may replace/remove it. `source: "factory"` entries attach
1956
+ // a factory secret BY NAME instead of carrying a value. Changes apply from
1957
+ // the session's next turn.
1958
+
1959
+ /** List a session's secrets (names + metadata only — values are never returned). */
1960
+ async listSessionSecrets(conversationId: string): Promise<SessionSecretEntry[]> {
1961
+ const body = await this.fetch<{ secrets: SessionSecretEntry[] }>(
1962
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/secrets`);
1963
+ return body.secrets;
1964
+ }
1965
+
1966
+ /** Set session secrets (bulk-friendly): value entries and/or `source:
1967
+ * "factory"` attachments-by-name. Returns the keys that were set. */
1968
+ async setSessionSecrets(conversationId: string, secrets: SessionSecretInput[]): Promise<string[]> {
1969
+ const body = await this.fetch<{ set: string[] }>(
1970
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/secrets`,
1971
+ { method: "POST", body: { secrets } });
1972
+ return body.set;
1973
+ }
1974
+
1975
+ /** Delete one session secret (owner or team admin). */
1976
+ deleteSessionSecret(conversationId: string, key: string): Promise<void> {
1977
+ return this.fetch(
1978
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/secrets/${encodeURIComponent(key)}`,
1979
+ { method: "DELETE" });
1980
+ }
1981
+
1982
+ /** Mint a VAULT LINK for this session — a short-lived, single-use page
1983
+ * where a session writer types the named credentials straight into the
1984
+ * session's secret store (they never transit the chat). Posts a card
1985
+ * into the session conversation; callable with the session's own
1986
+ * toolbelt credential (the in-sandbox agent's door) or by a session
1987
+ * writer. */
1988
+ requestSessionSecrets(
1989
+ conversationId: string, keys: string[], opts?: { reason?: string },
1990
+ ): Promise<SessionSecretRequestCreated> {
1991
+ return this.fetch(
1992
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/secret-requests`,
1993
+ { method: "POST", body: { keys, ...(opts?.reason ? { reason: opts.reason } : {}) } });
1994
+ }
1995
+
1996
+ /** Poll a vault link's status (`pending` → `fulfilled`; `expired` when the
1997
+ * clock ran out). The in-sandbox `agentc secrets request --wait` loop. */
1998
+ getSessionSecretRequest(conversationId: string, requestId: string): Promise<SessionSecretRequestStatus> {
1999
+ return this.fetch(
2000
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/secret-requests/${encodeURIComponent(requestId)}`);
2001
+ }
2002
+
1687
2003
  // ── API keys ───────────────────────────────────────────────────────────────
1688
2004
  // Both endpoints require an admin-scoped key as the bearer token.
1689
2005
 
package/src/display.ts CHANGED
@@ -70,7 +70,11 @@ export type DisplayMarker =
70
70
  | { kind: "image"; path: string; factorySlug?: string }
71
71
  | { kind: "table"; columns: DisplayTableColumn[]; rows: Array<Record<string, DisplayTableCell>>; truncated: boolean }
72
72
  | { kind: "chart"; chartKind: DisplayChartKind; series: DisplayChartSeries[]; xLabel?: string; yLabel?: string }
73
- | { kind: "ask"; askId: string; prompt: string; options?: DisplayAskOption[]; freeText: boolean };
73
+ | { kind: "ask"; askId: string; prompt: string; options?: DisplayAskOption[]; freeText: boolean }
74
+ /** A desktop snapshot (`agentc display desktop`): the CLI captured the
75
+ * session VM's screen to `path` on the session's drive branch. The card
76
+ * renders the snapshot + an "Open desktop" door to the live surface. */
77
+ | { kind: "desktop"; path: string; factorySlug?: string; capturedAt?: string; note?: string };
74
78
 
75
79
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
76
80
 
@@ -273,6 +277,9 @@ export function clampAskOptions(raw: unknown): DisplayAskOption[] {
273
277
  return options;
274
278
  }
275
279
 
280
+ /** Desktop-snapshot note bound — one caption line on the card. */
281
+ export const DESKTOP_NOTE_MAX_CHARS = 200;
282
+
276
283
  /** One-line JSON marker — exactly what `agentc display` prints. */
277
284
  export function serializeDisplayMarker(marker: DisplayMarker): string {
278
285
  return JSON.stringify({ [DISPLAY_MARKER_KEY]: DISPLAY_MARKER_VERSION, ...marker });
@@ -350,6 +357,21 @@ export function parseDisplayMarker(line: string): DisplayMarker | null {
350
357
  ...(yLabel !== undefined ? { yLabel } : {}),
351
358
  };
352
359
  }
360
+ case "desktop": {
361
+ const path = str("path");
362
+ if (!path || !isPlausibleDrivePath(path)) return null;
363
+ // capturedAt must parse as a real instant — a garbage stamp is
364
+ // dropped rather than rendered as "Invalid Date" on the card.
365
+ const capturedAt = str("capturedAt");
366
+ const capturedOk = capturedAt !== undefined && Number.isFinite(Date.parse(capturedAt));
367
+ const note = str("note");
368
+ return {
369
+ kind: "desktop", path,
370
+ ...(str("factorySlug") ? { factorySlug: str("factorySlug") } : {}),
371
+ ...(capturedOk ? { capturedAt } : {}),
372
+ ...(note !== undefined ? { note: note.slice(0, DESKTOP_NOTE_MAX_CHARS) } : {}),
373
+ };
374
+ }
353
375
  case "ask": {
354
376
  const askId = str("askId");
355
377
  const rawPrompt = str("prompt");
@@ -384,6 +406,9 @@ export type AgentcInvocation =
384
406
  | { verb: "display-document"; path: string; factorySlug?: string }
385
407
  | { verb: "display-plan" }
386
408
  | { verb: "display-image"; path: string; factorySlug?: string }
409
+ /** `agentc display desktop` — the CLI captures the screen itself, so the
410
+ * call has no positional path; the marker in the result names the file. */
411
+ | { verb: "display-desktop" }
387
412
  /** Directive verb — detection only arms the tool_call promotion; the
388
413
  * result keeps its raw output so the directive marker reaches the cloud
389
414
  * executor. Same for table `source:"file"`, diff, and ask-with-approver. */
@@ -432,6 +457,7 @@ const VALUE_FLAGS = new Set([
432
457
  "--factory", "--input", "--entries", "--url", "--api-key", "--limit",
433
458
  "--thread", "--content-type", "--file", "--out", "--revision", "--ended-at",
434
459
  "--data", "--kind", "--from", "--to", "--prompt", "--options", "--approver",
460
+ "--note",
435
461
  ]);
436
462
 
437
463
  /** Detect a known agentc invocation inside a shell command string. Returns
@@ -514,6 +540,8 @@ export function detectAgentcInvocation(command: string): AgentcInvocation | null
514
540
  const path = drivePath(args[2]);
515
541
  return path ? { verb: "display-image", path, ...withSlug } : null;
516
542
  }
543
+ case "desktop":
544
+ return { verb: "display-desktop" };
517
545
  case "preview": {
518
546
  const path = drivePath(args[2]);
519
547
  return path ? { verb: "display-preview", path } : null;
@@ -700,6 +728,8 @@ export function createDisplayPromoter(ctx: DisplayPromoterContext = {}): Display
700
728
  return call("display_plan", {});
701
729
  case "display-image":
702
730
  return call("display_image", { path: invocation.path, ...factoryRef(invocation.factorySlug) });
731
+ case "display-desktop":
732
+ return call("display_desktop", {});
703
733
  case "display-preview":
704
734
  return call("display_preview", { path: invocation.path, ...factoryRef(undefined) });
705
735
  case "display-table":
@@ -789,6 +819,19 @@ export function createDisplayPromoter(ctx: DisplayPromoterContext = {}): Display
789
819
  ...factoryRef(m?.factorySlug ?? inv.factorySlug),
790
820
  })];
791
821
  }
822
+ case "display-desktop": {
823
+ // No marker → the capture failed before it could name a file —
824
+ // keep the raw output (the CLI's own error is the feedback);
825
+ // a desktop card without a snapshot would lie.
826
+ const marker = findDisplayMarker(text);
827
+ if (marker?.kind !== "desktop") return [result(msg.output)];
828
+ return [result({
829
+ path: marker.path,
830
+ ...factoryRef(marker.factorySlug),
831
+ capturedAt: marker.capturedAt ?? null,
832
+ note: marker.note ?? null,
833
+ })];
834
+ }
792
835
  case "display-table": {
793
836
  // File-sourced tables are a DIRECTIVE: keep the raw output so the
794
837
  // marker inside it reaches the cloud executor (the real card pair).