@agent-compose/sdk 0.8.0 → 0.8.2

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 +1 -1
  2. package/dist/agent/agent-loop.d.ts +8 -0
  3. package/dist/agent/run-agent.d.ts +4 -0
  4. package/dist/client.d.ts +77 -15
  5. package/dist/display.d.ts +16 -0
  6. package/dist/index.d.ts +6 -6
  7. package/dist/index.js +522 -123
  8. package/dist/runtimes/_cli-agent.d.ts +34 -7
  9. package/dist/runtimes/claude-code.d.ts +10 -8
  10. package/dist/runtimes/codex.buildcommand.test.d.ts +9 -0
  11. package/dist/runtimes/codex.d.ts +4 -1
  12. package/dist/runtimes/openai-desktop.js +507 -122
  13. package/dist/sandbox/sizes.d.ts +120 -30
  14. package/dist/sandbox.d.ts +1 -1
  15. package/dist/types/api-conversations.d.ts +198 -0
  16. package/dist/types/api-factory.d.ts +84 -7
  17. package/dist/types/api-runs.d.ts +48 -2
  18. package/dist/types/protocol.d.ts +8 -0
  19. package/dist/types/workflow-metadata.d.ts +14 -5
  20. package/dist/utils/bundler.d.ts +56 -0
  21. package/dist/workflow-steps/workflow.d.ts +7 -0
  22. package/dist/workflows/invoke-child.d.ts +18 -0
  23. package/dist/workflows/invoke-child.test.d.ts +9 -0
  24. package/package.json +2 -2
  25. package/src/agent/agent-context.ts +28 -17
  26. package/src/agent/agent-loop.ts +9 -0
  27. package/src/agent/run-agent.ts +5 -0
  28. package/src/client.ts +201 -30
  29. package/src/display.ts +61 -15
  30. package/src/index.ts +22 -9
  31. package/src/runtimes/_cli-agent.ts +302 -63
  32. package/src/runtimes/claude-code.ts +25 -15
  33. package/src/runtimes/codex.ts +19 -5
  34. package/src/sandbox/providers/e2b.ts +8 -4
  35. package/src/sandbox/sizes.ts +127 -44
  36. package/src/sandbox.ts +8 -0
  37. package/src/types/api-conversations.ts +180 -0
  38. package/src/types/api-factory.ts +89 -7
  39. package/src/types/api-runs.ts +50 -2
  40. package/src/types/protocol.ts +8 -0
  41. package/src/types/workflow-metadata.ts +15 -5
  42. package/src/utils/bundler.ts +213 -3
  43. package/src/workflow-steps/workflow.ts +7 -0
  44. package/src/workflows/invoke-child.ts +47 -11
package/src/client.ts CHANGED
@@ -18,7 +18,8 @@ import type { RunEvent } from "./types/events.js";
18
18
  import type { ConversationStreamEvent } from "./types/conversation-stream.js";
19
19
  import { normalizeConversationStreamEvent } from "./types/conversation-stream.js";
20
20
  import type {
21
- InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, StreamRunLogsOptions,
21
+ InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, FundingChoice, StreamRunLogsOptions,
22
+ InlineWorkflowPayload, InvokeInlineOptions, InvokeInlineAndWaitOptions,
22
23
  RunStatus, ResumePauseOptions, ResumePauseResponse, AnswerSteerOptions,
23
24
  RequestAgentPauseOptions, RequestAgentPauseResponse,
24
25
  SendAgentMessageOptions, SendAgentMessageResponse,
@@ -30,8 +31,8 @@ import type {
30
31
  TeamMember, Mention, CreateMentionsInput,
31
32
  ConversationsPage, SessionsPage, ConversationDetail, ConversationThread,
32
33
  CreateCloudSessionInput, CloudSessionCreated,
33
- SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked,
34
- SessionChangeSet, SessionMergeReport, SessionDiscardReport,
34
+ SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked, BackgroundWorkHeld,
35
+ SessionChangeSet, SessionMergeGated, SessionMergeReport, SessionDiscardReport,
35
36
  SendConversationMessageInput, SendConversationMessageResult,
36
37
  ChannelSessionRow, ChannelSessionsResponse, SessionChannelMessagePosted,
37
38
  ConversationPresenceSnapshot, StreamConversationOptions, AgentListRow,
@@ -51,8 +52,9 @@ import type {
51
52
  FactoryRow, CreateFactoryInput, UpdateFactoryInput,
52
53
  ScheduleRow, CreateScheduleInput,
53
54
  SecretOptions, SetSecretResult, SecretListEntry,
54
- CreateApiKeyInput, ApiKey, ApiKeyCreated, UsageResponse,
55
+ CreateApiKeyInput, ApiKey, ApiKeyCreated, ListApiKeysOptions, ApiKeyPage, UsageResponse,
55
56
  DriveRepoLink, CreateDriveRepoLinkInput,
57
+ DriveMountSession, CreateDriveMountSessionInput,
56
58
  } from "./types/api-factory.js";
57
59
  import type {
58
60
  ComplianceSession, RequestComplianceSessionInput, ListComplianceSessionsOptions,
@@ -64,7 +66,8 @@ import type {
64
66
  // here so `client.js` stays the single import surface for the client and its
65
67
  // shapes (index.ts re-exports from here).
66
68
  export type {
67
- RunState, InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, StreamRunLogsOptions,
69
+ RunState, InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, FundingChoice, StreamRunLogsOptions,
70
+ InlineWorkflowPayload, InvokeInlineOptions, InvokeInlineAndWaitOptions,
68
71
  RunStatus, ResumePauseActor, ResumePauseSuccess, ResumePausePending, ResumePauseResponse,
69
72
  ResumePauseOptions, RequestAgentPauseOptions, AnswerSteerOptions, RequestAgentPauseResponse,
70
73
  SendAgentMessageOptions, SendAgentMessageResponse,
@@ -79,11 +82,12 @@ export type {
79
82
  ConversationMessagePart, ConversationRow, ConversationMessageRow,
80
83
  ConversationsPage, SessionsPage, ConversationDetail,
81
84
  CreateCloudSessionInput, CloudSessionCreated, ConversationThread,
82
- SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked,
83
- SessionFileChange, SessionChangeSet,
84
- SessionMergeReportDetail, SessionMergeReport, SessionDiscardReport,
85
+ SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked, BackgroundWorkHeld,
86
+ SessionFileChange, SessionChangeSet, SessionChangeStats, SessionDiffReviewBound, ReviewSuggestion,
87
+ ReviewSuggestionDecision, ReviewSuggestionDecisions,
88
+ SessionMergeReportDetail, SessionMergeReport, SessionMergeGated, SessionDiscardReport,
85
89
  ConversationPageContext, SendConversationMessageInput, ConversationTurnState,
86
- SendConversationMessageResult, ConversationPresenceSnapshot, AgentListRow, StreamConversationOptions,
90
+ SendConversationMessageResult, UnnotifiedMention, ConversationPresenceSnapshot, AgentListRow, StreamConversationOptions,
87
91
  ChannelSessionStatus, ChannelSessionRow, ChannelSessionsResponse, SessionChannelMessagePosted,
88
92
  } from "./types/api-conversations.js";
89
93
  export type {
@@ -105,9 +109,10 @@ export type {
105
109
  FactoryRow, CreateFactoryInput, UpdateFactoryInput,
106
110
  ScheduleRow, CreateScheduleInput,
107
111
  SecretOptions, SetSecretResult, SecretListEntry,
108
- CreateApiKeyInput, ApiKey, ApiKeyCreated,
112
+ CreateApiKeyInput, ApiKey, ApiKeyCreated, ListApiKeysOptions, ApiKeyPage,
109
113
  UsageRollupRow, UsageResponse,
110
114
  DriveRepoLink, CreateDriveRepoLinkInput,
115
+ DriveMountSession, CreateDriveMountSessionInput,
111
116
  } from "./types/api-factory.js";
112
117
  export type {
113
118
  ComplianceScopeKind, ComplianceStatus, ComplianceSession, ComplianceAccess,
@@ -296,6 +301,45 @@ export class AgentComposeClient {
296
301
  ...(opts?.size !== undefined ? { size: opts.size } : {}),
297
302
  ...(parentRunId ? { parentRunId } : {}),
298
303
  ...(opts?.agentId ? { agentId: opts.agentId } : {}),
304
+ ...(opts?.funding !== undefined ? { funding: opts.funding } : {}),
305
+ ...(opts?.fundingSecret !== undefined ? { fundingSecret: opts.fundingSecret } : {}),
306
+ },
307
+ });
308
+ }
309
+
310
+ /** Invoke an INLINE workflow — the exact payload `bundleWorkflow` produced
311
+ * plus a `name` — WITHOUT registering it. The server validates it through
312
+ * the same core as registration (manifest required + bound to the source
313
+ * bytes) but writes no registry row: the run snapshots the source it
314
+ * executes, and registration stays the door for named/versioned/scheduled
315
+ * workflows. Same parent-child auto-detection and `Idempotency-Key`
316
+ * semantics as `invoke()`. Requires the `invoke` scope. */
317
+ invokeInline(
318
+ workflow: InlineWorkflowPayload,
319
+ input?: Record<string, unknown>,
320
+ opts?: InvokeInlineOptions,
321
+ ): Promise<InvokeResult> {
322
+ const parentRunId = opts?.parentRunId === undefined
323
+ ? detectAmbientParentRunId()
324
+ : opts.parentRunId;
325
+ const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
326
+ const headers: Record<string, string> = {};
327
+ if (opts?.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
328
+ return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/invoke`, {
329
+ method: "POST",
330
+ ...(opts?.idempotencyKey ? { headers } : {}),
331
+ body: {
332
+ workflow,
333
+ input,
334
+ ...(opts?.title !== undefined ? { title: opts.title } : {}),
335
+ ...(opts?.snapshots !== undefined ? { snapshots: opts.snapshots } : {}),
336
+ ...(opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {}),
337
+ ...(opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {}),
338
+ ...(opts?.size !== undefined ? { size: opts.size } : {}),
339
+ ...(parentRunId ? { parentRunId } : {}),
340
+ ...(opts?.agentId ? { agentId: opts.agentId } : {}),
341
+ ...(opts?.funding !== undefined ? { funding: opts.funding } : {}),
342
+ ...(opts?.fundingSecret !== undefined ? { fundingSecret: opts.fundingSecret } : {}),
299
343
  },
300
344
  });
301
345
  }
@@ -312,12 +356,33 @@ export class AgentComposeClient {
312
356
  input?: Record<string, unknown>,
313
357
  opts?: InvokeAndWaitOptions,
314
358
  ): Promise<RunStatus<TOutput>> {
315
- const timeoutMs = opts?.timeoutMs ?? 30 * 60 * 1000;
316
- const pollMs = opts?.pollIntervalMs ?? 1000;
317
359
  // InvokeAndWaitOptions extends InvokeWorkflowOptions, so we can forward
318
360
  // `opts` directly — invoke() picks only the fields it sends, so the
319
361
  // extra `timeoutMs` / `pollIntervalMs` never leak into the request body.
320
362
  const { id: runId } = await this.invoke(name, input, opts);
363
+ return this.waitForRun<TOutput>(runId, opts);
364
+ }
365
+
366
+ /** `invokeInline` + block until the run settles — the "call blocks →
367
+ * result returns on the same turn" contract for inline sub-workflows. */
368
+ async invokeInlineAndWait<TOutput = unknown>(
369
+ workflow: InlineWorkflowPayload,
370
+ input?: Record<string, unknown>,
371
+ opts?: InvokeInlineAndWaitOptions,
372
+ ): Promise<RunStatus<TOutput>> {
373
+ const { id: runId } = await this.invokeInline(workflow, input, opts);
374
+ return this.waitForRun<TOutput>(runId, opts);
375
+ }
376
+
377
+ /** Poll one run until it settles (success / failed / abandoned) and return
378
+ * its final status. Shared tail of `invokeAndWait` / `invokeInlineAndWait`;
379
+ * also useful to re-attach to a run you dispatched fire-and-forget. */
380
+ async waitForRun<TOutput = unknown>(
381
+ runId: string,
382
+ opts?: { timeoutMs?: number; pollIntervalMs?: number },
383
+ ): Promise<RunStatus<TOutput>> {
384
+ const timeoutMs = opts?.timeoutMs ?? 30 * 60 * 1000;
385
+ const pollMs = opts?.pollIntervalMs ?? 1000;
321
386
  const deadline = Date.now() + timeoutMs;
322
387
  while (Date.now() < deadline) {
323
388
  const status = await this.getStatus<TOutput>(runId);
@@ -329,7 +394,7 @@ export class AgentComposeClient {
329
394
  // Use AgentComposeError (not plain Error) so catch-blocks handling SDK
330
395
  // transport failures also handle timeouts uniformly. HTTP 504 is the
331
396
  // closest idiomatic status for "upstream didn't answer in time."
332
- throw new AgentComposeError(504, `invokeAndWait: run ${runId} did not settle within ${timeoutMs}ms`);
397
+ throw new AgentComposeError(504, `waitForRun: run ${runId} did not settle within ${timeoutMs}ms`);
333
398
  }
334
399
 
335
400
  /** List captured snapshots in a factory. */
@@ -622,6 +687,21 @@ export class AgentComposeClient {
622
687
  }));
623
688
  }
624
689
 
690
+ /** One run artifact's bytes, resolved server-side through the DRIVE INDEX
691
+ * (never the run's gone sandbox or branch) — a listed artifact with an
692
+ * indexed path is always readable here, including after the run's branch
693
+ * is merged/retired. The path is the listing's `path`, sent as a single
694
+ * query parameter so slashes / spaces / unicode in agent-derived
695
+ * filenames survive verbatim. */
696
+ async getRunArtifactBytes(runId: string, path: string): Promise<Uint8Array> {
697
+ const q = new URLSearchParams({ path });
698
+ const buf = await this.fetch<ArrayBuffer, "arrayBuffer">(
699
+ `/api/v1/workflows/${encodeURIComponent(runId)}/artifacts/content?${q}`,
700
+ { responseType: "arrayBuffer" },
701
+ );
702
+ return new Uint8Array(buf);
703
+ }
704
+
625
705
  // ── Factory files ──────────────────────────────────────────────────────────
626
706
  // The factory drive: documents surfaced in the dashboard's Files tab.
627
707
  // Writes from inside a sandbox automatically carry the run-callback token
@@ -665,13 +745,17 @@ export class AgentComposeClient {
665
745
  * U+FFFD and the original bytes are unrecoverable). */
666
746
  async getFactoryFileBytes(
667
747
  path: string,
668
- opts?: { factorySlug?: string; revision?: number },
748
+ opts?: { factorySlug?: string; revision?: number; branch?: string },
669
749
  ): Promise<Uint8Array> {
670
750
  const factorySlug = opts?.factorySlug
671
751
  ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined)
672
752
  ?? DEFAULT_FACTORY;
673
753
  const q = new URLSearchParams({ path });
674
754
  if (opts?.revision !== undefined) q.set("revision", String(opts.revision));
755
+ // Branch view (read-only): the file AS OF a drive branch — e.g. a cloud
756
+ // session's own `session-<uuid>` branch, where its unmerged work lives.
757
+ // Mutually exclusive with `revision` (the server rejects the combination).
758
+ if (opts?.branch !== undefined) q.set("branch", opts.branch);
675
759
  const buf = await this.fetch<ArrayBuffer, "arrayBuffer">(
676
760
  `/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?${q}`,
677
761
  { responseType: "arrayBuffer" },
@@ -684,11 +768,57 @@ export class AgentComposeClient {
684
768
  * text corrupts the bytes irreversibly. */
685
769
  async getFactoryFile(
686
770
  path: string,
687
- opts?: { factorySlug?: string; revision?: number },
771
+ opts?: { factorySlug?: string; revision?: number; branch?: string },
688
772
  ): Promise<string> {
689
773
  return new TextDecoder().decode(await this.getFactoryFileBytes(path, opts));
690
774
  }
691
775
 
776
+ // ── User drive mounts ──────────────────────────────────────────────────────
777
+ // `agentc files mount` on a human's own machine: the server forks a user
778
+ // branch, backs it with a mount session (the review surface), and mints a
779
+ // user-principal gateway token. Human-held credentials only (sign-in
780
+ // cookie or a device-flow bridge key) — sandbox session keys are refused.
781
+
782
+ /** Create a local drive mount (or re-mint an existing one's token by
783
+ * passing its `conversationId`). */
784
+ async createDriveMountSession(
785
+ opts?: CreateDriveMountSessionInput & { factorySlug?: string },
786
+ ): Promise<DriveMountSession> {
787
+ const factorySlug = opts?.factorySlug
788
+ ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined)
789
+ ?? DEFAULT_FACTORY;
790
+ return this.fetch<DriveMountSession>(
791
+ `/api/v1/factories/${encodeURIComponent(factorySlug)}/files/mount-sessions`,
792
+ {
793
+ method: "POST",
794
+ body: {
795
+ ...(opts?.conversationId ? { conversationId: opts.conversationId } : {}),
796
+ ...(opts?.host ? { host: opts.host } : {}),
797
+ },
798
+ },
799
+ );
800
+ }
801
+
802
+ /** Release a local mount's gateway branch mount (clean unmount). The
803
+ * branch and its review card survive — this only drops the gateway's
804
+ * in-memory mount so the exclusive branch is not pinned. */
805
+ async releaseDriveMountSession(
806
+ conversationId: string,
807
+ opts?: { factorySlug?: string },
808
+ ): Promise<{ released: boolean }> {
809
+ const factorySlug = opts?.factorySlug
810
+ ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined)
811
+ ?? DEFAULT_FACTORY;
812
+ return this.fetch<{ released: boolean }>(
813
+ `/api/v1/factories/${encodeURIComponent(factorySlug)}/files/mount-sessions/${encodeURIComponent(conversationId)}/release`,
814
+ { method: "POST", body: {} },
815
+ );
816
+ }
817
+
818
+ // A mount's change set + merge/discard ride the EXISTING session-branch
819
+ // proposal surface below (`getSessionChanges` / `mergeSessionChanges` /
820
+ // `discardSessionChanges`) — the mount's `conversationId` is a session.
821
+
692
822
  // ── Conversations ──────────────────────────────────────────────────────────
693
823
  // The chat surfaces (channels / solo chats / sessions) the caller can
694
824
  // access. Key callers see what their sender scope allows; the server is the
@@ -711,9 +841,12 @@ export class AgentComposeClient {
711
841
  return this.fetch<SessionsPage>(`/api/v1/sessions?${q}`);
712
842
  }
713
843
 
714
- /** One conversation with its latest page of messages. */
715
- getConversation(id: string): Promise<ConversationDetail> {
716
- return this.fetch<ConversationDetail>(`/api/v1/conversations/${encodeURIComponent(id)}`);
844
+ /** One conversation with its latest page of messages. `limit` bounds the
845
+ * page (newest N) — metadata-only consumers (e.g. resolving the
846
+ * session's drive branch) pass 1 instead of pulling the full hydrate. */
847
+ getConversation(id: string, opts?: { limit?: number }): Promise<ConversationDetail> {
848
+ const q = opts?.limit !== undefined ? `?limit=${opts.limit}` : "";
849
+ return this.fetch<ConversationDetail>(`/api/v1/conversations/${encodeURIComponent(id)}${q}`);
717
850
  }
718
851
 
719
852
  /** Provision a CLOUD-native session (ADR-0037 Phase 3b / ADR-0055 §9): a
@@ -742,6 +875,9 @@ export class AgentComposeClient {
742
875
  ...(input.sandboxSize ? { sandboxSize: input.sandboxSize } : {}),
743
876
  ...(input.networkPolicy !== undefined ? { networkPolicy: input.networkPolicy } : {}),
744
877
  ...(input.connectorProfileId !== undefined ? { connectorProfileId: input.connectorProfileId } : {}),
878
+ ...(input.setupCommand !== undefined ? { setupCommand: input.setupCommand } : {}),
879
+ ...(input.handoffOrigin !== undefined ? { handoffOrigin: input.handoffOrigin } : {}),
880
+ ...(input.seedFiles !== undefined ? { seedFiles: input.seedFiles } : {}),
745
881
  },
746
882
  });
747
883
  }
@@ -849,6 +985,28 @@ export class AgentComposeClient {
849
985
  return body.closed;
850
986
  }
851
987
 
988
+ /** Hold (or extend — the stamp is monotone) a cloud session's
989
+ * background-work busy lease: while it is live, the between-turns
990
+ * park/suspend leaves the session's VM running so work the turn left
991
+ * behind keeps executing. The lease lapses on its own — re-hold to
992
+ * extend past `minutes` (server-capped). Write-tier. */
993
+ holdBackgroundWork(conversationId: string, input?: { minutes?: number }): Promise<BackgroundWorkHeld> {
994
+ return this.fetch<BackgroundWorkHeld>(
995
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/background-work`,
996
+ { method: "POST", body: { ...(input?.minutes !== undefined ? { minutes: input.minutes } : {}) } },
997
+ );
998
+ }
999
+
1000
+ /** Release the session's background-work lease (the work finished).
1001
+ * Idempotent — `released` is false when no lease was held. */
1002
+ async releaseBackgroundWork(conversationId: string): Promise<boolean> {
1003
+ const body = await this.fetch<{ released: boolean }>(
1004
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/background-work`,
1005
+ { method: "DELETE" },
1006
+ );
1007
+ return body.released;
1008
+ }
1009
+
852
1010
  /** Fork a cloud session from HEAD: snapshot the VM + branch the drive + seed
853
1011
  * a new conversation from the transcript so far, booting from both. Returns
854
1012
  * the child conversation id to switch to. Write-tier; "Branch from here." */
@@ -882,6 +1040,11 @@ export class AgentComposeClient {
882
1040
  * `main`. Write-tier + human caller. Pass `opts.branch` (the branch you
883
1041
  * reviewed) to fail 409 `branch_changed` if it moved since.
884
1042
  *
1043
+ * On a merge-GATED session (merge-gate spec) a non-approver's call does
1044
+ * NOT merge: it answers 202 `SessionMergeGated` — the ask froze into (or
1045
+ * converged on) a kind='merge' approval routed to the session's
1046
+ * approvers. Discriminate on `object`.
1047
+ *
885
1048
  * Failures: 403 `role_read_only` (read-only member) or a plain 403 for
886
1049
  * session toolbelt keys (agents cannot self-approve); 409 `turn_active`
887
1050
  * (a turn is running — retry when idle) | `branch_changed`; 400
@@ -891,8 +1054,8 @@ export class AgentComposeClient {
891
1054
  mergeSessionChanges(
892
1055
  conversationId: string,
893
1056
  opts: { branch?: string } = {},
894
- ): Promise<SessionMergeReport> {
895
- return this.fetch<SessionMergeReport>(
1057
+ ): Promise<SessionMergeReport | SessionMergeGated> {
1058
+ return this.fetch<SessionMergeReport | SessionMergeGated>(
896
1059
  `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/merge`,
897
1060
  { method: "POST", body: opts.branch ? { branch: opts.branch } : {} },
898
1061
  );
@@ -1473,17 +1636,18 @@ export class AgentComposeClient {
1473
1636
  return body.links;
1474
1637
  }
1475
1638
 
1476
- /** Link a drive directory to a GitHub repo + tracked branch (`manage`
1477
- * scope). Requires the drive to be graph-authoritative — 409 names the
1478
- * promotion prerequisite otherwise. */
1639
+ /** Link a drive directory to a GitHub repo's tracked BRANCHES (`manage`
1640
+ * scope) — one link row per branch, created in one action. Requires the
1641
+ * drive to be graph-authoritative — 409 names the promotion
1642
+ * prerequisite otherwise. */
1479
1643
  async createRepoLink(
1480
1644
  input: CreateDriveRepoLinkInput, opts?: { factorySlug?: string },
1481
- ): Promise<DriveRepoLink> {
1645
+ ): Promise<DriveRepoLink[]> {
1482
1646
  const slug = opts?.factorySlug ?? DEFAULT_FACTORY;
1483
- const body = await this.fetch<{ link: DriveRepoLink }>(
1647
+ const body = await this.fetch<{ links: DriveRepoLink[] }>(
1484
1648
  `/api/v1/factories/${encodeURIComponent(slug)}/repo-links`,
1485
1649
  { method: "POST", body: input });
1486
- return body.link;
1650
+ return body.links;
1487
1651
  }
1488
1652
 
1489
1653
  /** Unlink (§6.3: the prefix's files and history stay on the drive). */
@@ -1533,10 +1697,17 @@ export class AgentComposeClient {
1533
1697
  }
1534
1698
 
1535
1699
  /** List API keys on the caller's team (metadata only — plaintext keys are
1536
- * never returned). */
1537
- async listApiKeys(): Promise<ApiKey[]> {
1538
- const body = await this.fetch<{ object: "list"; data: ApiKey[]; has_more: boolean }>("/api-keys");
1539
- return body.data;
1700
+ * never returned). Bounded + keyset-paginated: pass `cursor` from the
1701
+ * previous page's `nextCursor` to walk forward. */
1702
+ async listApiKeys(opts?: ListApiKeysOptions): Promise<ApiKeyPage> {
1703
+ const qs = new URLSearchParams();
1704
+ if (opts?.limit !== undefined) qs.set("limit", String(opts.limit));
1705
+ if (opts?.status !== undefined) qs.set("status", opts.status);
1706
+ if (opts?.cursor !== undefined) qs.set("cursor", opts.cursor);
1707
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
1708
+ const body = await this.fetch<{ object: "list"; data: ApiKey[]; has_more: boolean; next_cursor: string | null }>(
1709
+ `/api-keys${suffix}`);
1710
+ return { data: body.data, hasMore: body.has_more, nextCursor: body.next_cursor ?? null };
1540
1711
  }
1541
1712
 
1542
1713
  // ── Usage ──────────────────────────────────────────────────────────────────
package/src/display.ts CHANGED
@@ -101,8 +101,32 @@ export const TABLE_MAX_COLUMNS = 12;
101
101
  export const TABLE_MAX_ROWS = 100;
102
102
  export const TABLE_CELL_MAX_CHARS = 200;
103
103
 
104
- /** Drive-path bound for image markers (mirrors the directive path cap). */
105
- const IMAGE_PATH_MAX_CHARS = 512;
104
+ /** Drive-path bound for promoted/marker paths (mirrors the directive cap). */
105
+ export const DRIVE_PATH_MAX_CHARS = 512;
106
+
107
+ /**
108
+ * Whether a string is a PLAUSIBLE factory-drive path — the gate every
109
+ * promoted document/image/preview/diff path and document/image marker
110
+ * passes before it can become a card. A malformed agent command can hand
111
+ * the promoter shell fragments instead of a path (live failure: a partial
112
+ * `agentc display document` call promoted the redirect word `2>&1` — and a
113
+ * bare `/` — into "Showed document" cards whose viewer link was dead), so
114
+ * anything that reads as a shell operator, flag, or empty reference is not
115
+ * a path: reject whitespace-only, over-long, flag-shaped (leading `-`),
116
+ * shell operators / substitution (`|`, `<`, `>`, `;`, backticks, `$(`,
117
+ * which covers `2>&1`), control characters, backslashes, bare/duplicate
118
+ * slashes, and `.`/`..` segments (the server rejects those anyway).
119
+ */
120
+ export function isPlausibleDrivePath(raw: string): boolean {
121
+ if (raw.trim().length === 0 || raw.length > DRIVE_PATH_MAX_CHARS) return false;
122
+ if (raw.startsWith("-")) return false;
123
+ // eslint-disable-next-line no-control-regex
124
+ if (/[\u0000-\u001f\u007f\\]/.test(raw)) return false;
125
+ if (/[|<>;`]|\$\(/.test(raw)) return false;
126
+ const trimmed = raw.replace(/^\/+|\/+$/g, "");
127
+ if (trimmed.length === 0) return false; // bare "/" (or only slashes)
128
+ return trimmed.split("/").every((seg) => seg !== "" && seg !== "." && seg !== "..");
129
+ }
106
130
 
107
131
  function clampCellString(s: string): string {
108
132
  return s.length > TABLE_CELL_MAX_CHARS ? `${s.slice(0, TABLE_CELL_MAX_CHARS - 1)}…` : s;
@@ -288,7 +312,7 @@ export function parseDisplayMarker(line: string): DisplayMarker | null {
288
312
  }
289
313
  case "document": {
290
314
  const path = str("path");
291
- if (!path) return null;
315
+ if (!path || !isPlausibleDrivePath(path)) return null;
292
316
  return {
293
317
  kind: "document", path,
294
318
  ...(str("factorySlug") ? { factorySlug: str("factorySlug") } : {}),
@@ -301,7 +325,7 @@ export function parseDisplayMarker(line: string): DisplayMarker | null {
301
325
  }
302
326
  case "image": {
303
327
  const path = str("path");
304
- if (!path || path.length > IMAGE_PATH_MAX_CHARS) return null;
328
+ if (!path || !isPlausibleDrivePath(path)) return null;
305
329
  return {
306
330
  kind: "image", path,
307
331
  ...(str("factorySlug") ? { factorySlug: str("factorySlug") } : {}),
@@ -446,6 +470,11 @@ export function detectAgentcInvocation(command: string): AgentcInvocation | null
446
470
  for (let i = start + 1; i < words.length; i += 1) {
447
471
  const w = words[i];
448
472
  if (SHELL_STOPPERS.has(w)) break;
473
+ // A merged-stream redirect (`2>&1`) is tolerated by the control-word
474
+ // gate above, but it is shell syntax, never an argument — collecting
475
+ // it turned a truncated `agentc display document 2>&1` into a "Showed
476
+ // document" card whose path was the redirect itself (live failure).
477
+ if (/^\d*>&\d+$/.test(w)) continue;
449
478
  if (w.startsWith("--")) {
450
479
  if (VALUE_FLAGS.has(w) && i + 1 < words.length && !SHELL_STOPPERS.has(words[i + 1])) {
451
480
  flags.set(w, words[i + 1]);
@@ -458,6 +487,12 @@ export function detectAgentcInvocation(command: string): AgentcInvocation | null
458
487
  const factorySlug = flags.get("--factory");
459
488
  const withSlug = factorySlug ? { factorySlug } : {};
460
489
  const uuid = (v: string | undefined): string | null => (v && UUID_RE.test(v) ? v : null);
490
+ // Path-taking verbs promote only PLAUSIBLE drive paths — a shell
491
+ // fragment or bare `/` from a malformed command is unparseable-argument
492
+ // territory (the pair persists raw, the CLI's own error is the agent's
493
+ // feedback), never a document card.
494
+ const drivePath = (v: string | undefined): string | null =>
495
+ (v && isPlausibleDrivePath(v) ? v : null);
461
496
 
462
497
  if (args[0] === "display") {
463
498
  switch (args[1]) {
@@ -469,19 +504,28 @@ export function detectAgentcInvocation(command: string): AgentcInvocation | null
469
504
  const runId = uuid(args[2]);
470
505
  return runId ? { verb: "display-changes", runId, ...withSlug } : null;
471
506
  }
472
- case "document":
473
- return args[2] ? { verb: "display-document", path: args[2], ...withSlug } : null;
507
+ case "document": {
508
+ const path = drivePath(args[2]);
509
+ return path ? { verb: "display-document", path, ...withSlug } : null;
510
+ }
474
511
  case "plan":
475
512
  return { verb: "display-plan" };
476
- case "image":
477
- return args[2] ? { verb: "display-image", path: args[2], ...withSlug } : null;
478
- case "preview":
479
- return args[2] ? { verb: "display-preview", path: args[2] } : null;
513
+ case "image": {
514
+ const path = drivePath(args[2]);
515
+ return path ? { verb: "display-image", path, ...withSlug } : null;
516
+ }
517
+ case "preview": {
518
+ const path = drivePath(args[2]);
519
+ return path ? { verb: "display-preview", path } : null;
520
+ }
480
521
  case "table": {
481
522
  const file = flags.get("--file");
482
523
  const hasData = flags.has("--data");
483
524
  // Exactly one source; both or neither is an unpromotable combination.
484
- if (file && !hasData) return { verb: "display-table", source: "file", path: file };
525
+ if (file && !hasData) {
526
+ const path = drivePath(file);
527
+ return path ? { verb: "display-table", source: "file", path } : null;
528
+ }
485
529
  if (hasData && !file) return { verb: "display-table", source: "inline" };
486
530
  return null;
487
531
  }
@@ -494,9 +538,10 @@ export function detectAgentcInvocation(command: string): AgentcInvocation | null
494
538
  case "diff": {
495
539
  const from = flags.get("--from");
496
540
  const to = flags.get("--to");
497
- if (!args[2] || !from || !to) return null;
541
+ const path = drivePath(args[2]);
542
+ if (!path || !from || !to) return null;
498
543
  if (!REVISION_SELECTOR_RE.test(from) || !REVISION_SELECTOR_RE.test(to)) return null;
499
- return { verb: "display-diff", path: args[2], from, to };
544
+ return { verb: "display-diff", path, from, to };
500
545
  }
501
546
  case "ask": {
502
547
  const prompt = flags.get("--prompt");
@@ -524,8 +569,9 @@ export function detectAgentcInvocation(command: string): AgentcInvocation | null
524
569
  const runId = uuid(args[2]);
525
570
  return runId ? { verb: "run-get", runId } : null;
526
571
  }
527
- if (args[0] === "files" && args[1] === "read" && args[2]) {
528
- return { verb: "files-read", path: args[2], ...withSlug };
572
+ if (args[0] === "files" && args[1] === "read") {
573
+ const path = drivePath(args[2]);
574
+ return path ? { verb: "files-read", path, ...withSlug } : null;
529
575
  }
530
576
  return null;
531
577
  }
package/src/index.ts CHANGED
@@ -113,7 +113,8 @@ export type {
113
113
  export { AgentComposeClient } from "./client.js";
114
114
  export type {
115
115
  RegisterResult, RegisterWorkflowInput, RuntimeSourceInput, TemplateSourceRef,
116
- InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult,
116
+ InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, FundingChoice,
117
+ InlineWorkflowPayload, InvokeInlineOptions, InvokeInlineAndWaitOptions,
117
118
  ListSnapshotsOptions, TemplateRow, ListTemplatesOptions,
118
119
  CreateFactoryInput, UpdateFactoryInput,
119
120
  SecretOptions, SetSecretResult, SecretListEntry,
@@ -124,7 +125,7 @@ export type {
124
125
  RunListEntry, ListRunsOptions, RunDetail,
125
126
  FundingLane, RunFundingStamp, RunFundingUsageRow, RunFundingResponse,
126
127
  RegisteredRuntime, RunState, RunStatus, FactoryRow, SnapshotListEntry, SnapshotListResponse,
127
- ApiKey, ApiKeyCreated,
128
+ ApiKey, ApiKeyCreated, ListApiKeysOptions, ApiKeyPage,
128
129
  UsageRollupRow, UsageResponse,
129
130
  CancelRunResponse,
130
131
  RequestAgentPauseOptions, RequestAgentPauseResponse,
@@ -134,13 +135,17 @@ export type {
134
135
  ConversationMessagePart, ConversationRow, ConversationMessageRow,
135
136
  ConversationsPage, ConversationDetail, ConversationThread,
136
137
  ConversationPageContext, SendConversationMessageInput, SendConversationMessageResult,
138
+ UnnotifiedMention,
137
139
  ConversationTurnState, ConversationPresenceSnapshot, StreamConversationOptions, AgentListRow,
138
140
  CreateCloudSessionInput, CloudSessionCreated,
139
141
  // Cloud-session developer surface (ADR-0052)
140
- SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked,
142
+ SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked, BackgroundWorkHeld,
141
143
  // Session branch proposals (ADR-0053)
142
- SessionFileChange, SessionChangeSet,
143
- SessionMergeReportDetail, SessionMergeReport, SessionDiscardReport,
144
+ SessionFileChange, SessionChangeSet, SessionChangeStats, SessionDiffReviewBound, ReviewSuggestion,
145
+ ReviewSuggestionDecision, ReviewSuggestionDecisions,
146
+ SessionMergeReportDetail, SessionMergeReport, SessionMergeGated, SessionDiscardReport,
147
+ // User drive mounts (`agentc files mount` on a human's own machine)
148
+ DriveMountSession, CreateDriveMountSessionInput,
144
149
  // Channel-attached sessions (ADR-0057)
145
150
  ChannelSessionStatus, ChannelSessionRow, ChannelSessionsResponse, SessionChannelMessagePosted,
146
151
  FactoryFileSearchRow, FactoryFolderSearchRow, SearchFactoryFilesOptions, FactoryFileSearchResult, PublicFileLinkState,
@@ -206,6 +211,10 @@ export {
206
211
  BUNDLER_VERSION,
207
212
  WorkflowSourceValidationError,
208
213
  assertDefaultExportIsDefineWorkflow,
214
+ SDK_PACKAGE,
215
+ SDK_SPECIFIER_ALIASES,
216
+ resolveSdkAlias,
217
+ explainBundleFailure,
209
218
  } from "./utils/bundler.js";
210
219
  export type { BundledWorkflow, WorkflowManifest } from "./utils/bundler.js";
211
220
 
@@ -232,7 +241,7 @@ export type { GatewayModelId } from "ai";
232
241
  // sandbox and stream-parse its JSONL. No heavy npm deps (the CLI lives in the
233
242
  // sandbox image), so these are root-exported like claudeRuntime.
234
243
  export { createCodexRuntime, codexSpec } from "./runtimes/codex.js";
235
- export type { CodexRuntimeConfig } from "./runtimes/codex.js";
244
+ export type { CodexRuntimeConfig, CodexReasoningEffort } from "./runtimes/codex.js";
236
245
  export { default as codexRuntime } from "./runtimes/codex.js";
237
246
  // Reasoning-effort level a CLI runtime turn may carry (claude-code / codex).
238
247
  export type { CliReasoningEffort } from "./runtimes/_cli-agent.js";
@@ -255,7 +264,7 @@ export { default as droidRuntime } from "./runtimes/droid.js";
255
264
  // adapter) — the cloud-hostable counterpart of `claudeRuntime` (claude.ts,
256
265
  // which drives the Agent SDK in the calling process and so can never run a
257
266
  // server-driven cloud-session turn).
258
- export { createClaudeCodeRuntime, claudeCodeSpec, CLAUDE_CODE_ACP_ADAPTER, CLAUDE_CODE_THINKING_TOKENS } from "./runtimes/claude-code.js";
267
+ export { createClaudeCodeRuntime, claudeCodeSpec, CLAUDE_CODE_ACP_ADAPTER, CLAUDE_CODE_EFFORT_LEVELS } from "./runtimes/claude-code.js";
259
268
  export type { ClaudeCodeRuntimeConfig } from "./runtimes/claude-code.js";
260
269
  export { default as claudeCodeRuntime } from "./runtimes/claude-code.js";
261
270
 
@@ -271,8 +280,11 @@ export { createSandbox, reconnectSandbox, killAllSandboxes, killSandboxById,
271
280
  getSandboxQuotas, listOwnedSandboxes, deleteSandboxSnapshot, snapshotResolves,
272
281
  makeSandboxProvider, makeDesktopSandboxProvider,
273
282
  parseSseExecStream, AGENT_COMPOSE_TAG,
274
- SANDBOX_VCPUS, DEFAULT_SANDBOX_SIZE, E2B_TEMPLATE_SIZES,
275
- isE2bSupportedSize, e2bMachineSpec, e2bBaseTemplate, e2bAgentEnvTemplate,
283
+ SANDBOX_SIZES, SANDBOX_MACHINES, SANDBOX_VCPUS, DEFAULT_SANDBOX_SIZE, SESSION_DEFAULT_SANDBOX_SIZE,
284
+ E2B_TEMPLATE_SIZES, E2B_MAX_VCPUS, E2B_MAX_MEMORY_MB,
285
+ VERCEL_MEMORY_MB_PER_VCPU,
286
+ isE2bSupportedSize, isVercelSupportedSize, sandboxSizeLabel,
287
+ e2bMachineSpec, e2bBaseTemplate, e2bAgentEnvTemplate,
276
288
  isPlatformE2bTemplateAlias,
277
289
  // ADR-0038 "Computer" — the per-member persistent desktop machine template.
278
290
  E2B_DEVBOX_TEMPLATE, E2B_DEVBOX_SPEC, E2B_DEVBOX_RECIPE_VERSION,
@@ -386,6 +398,7 @@ export {
386
398
  TABLE_MAX_COLUMNS, TABLE_MAX_ROWS, TABLE_CELL_MAX_CHARS,
387
399
  CHART_MAX_SERIES, CHART_MAX_POINTS_PER_SERIES, CHART_LABEL_MAX_CHARS,
388
400
  ASK_PROMPT_MAX_CHARS, ASK_MAX_OPTIONS, ASK_OPTION_ID_MAX_CHARS, ASK_OPTION_LABEL_MAX_CHARS,
401
+ DRIVE_PATH_MAX_CHARS, isPlausibleDrivePath,
389
402
  serializeDisplayMarker, parseDisplayMarker, findDisplayMarker, clampPlanEntries,
390
403
  clampTableData, clampChartSeries, clampChartAxisLabel, clampAskOptions,
391
404
  detectAgentcInvocation, shellWords, createDisplayPromoter,