@agent-compose/sdk 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +66 -39
  2. package/dist/agent/__tests__/runtime-json-schema.test.d.ts +10 -0
  3. package/dist/agent/agent-context.d.ts +21 -1
  4. package/dist/agent/agent-loop.d.ts +24 -1
  5. package/dist/client.d.ts +338 -534
  6. package/dist/directives.d.ts +112 -0
  7. package/dist/display.d.ts +242 -0
  8. package/dist/errors.d.ts +24 -1
  9. package/dist/index.d.ts +34 -13
  10. package/dist/index.js +2984 -861
  11. package/dist/pause/wrappers.d.ts +31 -9
  12. package/dist/processors/ask-human.d.ts +30 -0
  13. package/dist/processors/ask-human.test.d.ts +1 -0
  14. package/dist/processors/index.d.ts +1 -0
  15. package/dist/runtimes/_acp-client.d.ts +46 -1
  16. package/dist/runtimes/_cli-agent.d.ts +58 -4
  17. package/dist/runtimes/_jsonl-guard.d.ts +103 -0
  18. package/dist/runtimes/amp.d.ts +2 -2
  19. package/dist/runtimes/claude-code.d.ts +59 -0
  20. package/dist/runtimes/claude-code.test.d.ts +14 -0
  21. package/dist/runtimes/claude.d.ts +16 -0
  22. package/dist/runtimes/claude.test.d.ts +8 -0
  23. package/dist/runtimes/codex.d.ts +9 -3
  24. package/dist/runtimes/cursor.d.ts +9 -0
  25. package/dist/runtimes/droid.d.ts +9 -0
  26. package/dist/runtimes/jsonl-guard.test.d.ts +19 -0
  27. package/dist/runtimes/openai-desktop.js +2922 -861
  28. package/dist/runtimes/opencode.d.ts +25 -0
  29. package/dist/runtimes/vercel.js +22 -1
  30. package/dist/sandbox/devbox.d.ts +42 -0
  31. package/dist/sandbox/exec-stream.d.ts +14 -0
  32. package/dist/sandbox/network-policy.d.ts +100 -0
  33. package/dist/sandbox/provider-def.d.ts +79 -0
  34. package/dist/sandbox/providers/desktop.d.ts +10 -0
  35. package/dist/sandbox/providers/e2b.d.ts +17 -0
  36. package/dist/sandbox/providers/local.d.ts +11 -0
  37. package/dist/sandbox/providers/vercel.d.ts +18 -0
  38. package/dist/sandbox/registry.d.ts +45 -0
  39. package/dist/sandbox/sizes.d.ts +68 -0
  40. package/dist/sandbox.d.ts +24 -299
  41. package/dist/step-invocation/__tests__/foreground-recovery.test.d.ts +1 -0
  42. package/dist/step-invocation/invoker.d.ts +24 -1
  43. package/dist/step-invocation/protocol.d.ts +13 -0
  44. package/dist/types/api-compliance.d.ts +71 -0
  45. package/dist/types/api-conversations.d.ts +492 -0
  46. package/dist/types/api-factory.d.ts +309 -0
  47. package/dist/types/api-projects.d.ts +131 -0
  48. package/dist/types/api-runs.d.ts +377 -0
  49. package/dist/types/api-scopes.d.ts +102 -0
  50. package/dist/types/conversation-stream.d.ts +191 -0
  51. package/dist/types/execution-context.d.ts +12 -2
  52. package/dist/types/protocol.d.ts +30 -1
  53. package/dist/types/sandbox-environment.d.ts +8 -5
  54. package/dist/types/sandbox.d.ts +79 -0
  55. package/dist/types/workflow-metadata.d.ts +33 -8
  56. package/dist/types/workflow-plan.d.ts +10 -0
  57. package/dist/types/workflow.d.ts +18 -193
  58. package/dist/utils/bundler.d.ts +12 -1
  59. package/dist/utils/errors.d.ts +9 -1
  60. package/dist/workflow-steps/index.d.ts +1 -1
  61. package/dist/workflow-steps/observability.d.ts +8 -1
  62. package/dist/workflow-steps/runner.d.ts +3 -3
  63. package/dist/workflow-steps/step.d.ts +15 -1
  64. package/dist/workflow-steps/types.d.ts +19 -5
  65. package/dist/workflow-steps/workflow.d.ts +22 -1
  66. package/dist/workflows/engine.d.ts +3 -2
  67. package/dist/workflows/invoke-child.d.ts +2 -2
  68. package/package.json +1 -1
  69. package/src/agent/agent-context.ts +206 -16
  70. package/src/agent/agent-loop.ts +40 -4
  71. package/src/agent/run-agent.ts +9 -1
  72. package/src/client.ts +909 -621
  73. package/src/directives.ts +184 -0
  74. package/src/display.ts +788 -0
  75. package/src/errors.ts +39 -0
  76. package/src/index.ts +117 -10
  77. package/src/pause/wrappers.ts +44 -9
  78. package/src/processors/ask-human.ts +136 -0
  79. package/src/processors/index.ts +5 -0
  80. package/src/runtimes/_acp-client.ts +72 -3
  81. package/src/runtimes/_cli-agent.ts +171 -38
  82. package/src/runtimes/_jsonl-guard.ts +219 -0
  83. package/src/runtimes/claude-code.ts +246 -0
  84. package/src/runtimes/claude.ts +32 -2
  85. package/src/runtimes/codex.ts +55 -3
  86. package/src/runtimes/cursor.ts +59 -0
  87. package/src/runtimes/droid.ts +63 -0
  88. package/src/runtimes/openai-desktop.ts +59 -14
  89. package/src/runtimes/opencode.ts +61 -0
  90. package/src/sandbox/devbox.ts +48 -0
  91. package/src/sandbox/exec-stream.ts +48 -0
  92. package/src/sandbox/network-policy.ts +181 -0
  93. package/src/sandbox/provider-def.ts +94 -0
  94. package/src/sandbox/providers/desktop.ts +57 -0
  95. package/src/sandbox/providers/e2b.ts +354 -0
  96. package/src/sandbox/providers/local.ts +106 -0
  97. package/src/sandbox/providers/vercel.ts +331 -0
  98. package/src/sandbox/registry.ts +198 -0
  99. package/src/sandbox/sizes.ts +95 -0
  100. package/src/sandbox.ts +59 -1263
  101. package/src/step-invocation/invoker.ts +319 -34
  102. package/src/step-invocation/protocol.ts +19 -0
  103. package/src/types/api-compliance.ts +79 -0
  104. package/src/types/api-conversations.ts +522 -0
  105. package/src/types/api-factory.ts +336 -0
  106. package/src/types/api-projects.ts +140 -0
  107. package/src/types/api-runs.ts +412 -0
  108. package/src/types/api-scopes.ts +102 -0
  109. package/src/types/conversation-stream.ts +231 -0
  110. package/src/types/execution-context.ts +10 -2
  111. package/src/types/protocol.ts +33 -0
  112. package/src/types/sandbox-environment.ts +28 -9
  113. package/src/types/sandbox.ts +78 -0
  114. package/src/types/workflow-metadata.ts +35 -8
  115. package/src/types/workflow-plan.ts +11 -0
  116. package/src/types/workflow.ts +25 -280
  117. package/src/utils/bundler.ts +32 -5
  118. package/src/utils/errors.ts +34 -2
  119. package/src/workflow-steps/index.ts +1 -0
  120. package/src/workflow-steps/observability.ts +19 -8
  121. package/src/workflow-steps/runner.ts +4 -4
  122. package/src/workflow-steps/step.ts +49 -1
  123. package/src/workflow-steps/types.ts +20 -5
  124. package/src/workflow-steps/workflow.ts +22 -1
  125. package/src/workflows/engine.ts +3 -2
  126. package/src/workflows/invoke-child.ts +2 -2
package/src/errors.ts CHANGED
@@ -1,10 +1,49 @@
1
+ /** Stable machine-readable denial codes on membership/capability-gated
2
+ * mutations (ADR-0045). Reads never carry a code — a visibility denial is
3
+ * a plain 404, indistinguishable from a missing id. */
4
+ export type AuthzErrorCode =
5
+ | "role_read_only"
6
+ | "owner_required"
7
+ | "capability_required"
8
+ | "membership_required"
9
+ | "last_owner"
10
+ | "approver_only";
11
+
1
12
  /** Thrown by AgentComposeClient when the server returns a non-2xx response. */
2
13
  export class AgentComposeError extends Error {
14
+ /** Parsed `Retry-After` response header, in milliseconds, when the server
15
+ * sent one (rate-limit 429s do). Pollers use it to honor the server's own
16
+ * estimate of when the limit window clears instead of guessing. */
17
+ readonly retryAfterMs?: number;
18
+ /** Stable error code on gated-mutation 403s (ADR-0045) — one of
19
+ * `AuthzErrorCode` when the denial is a membership/capability gate;
20
+ * undefined otherwise. */
21
+ readonly code?: string;
22
+ /** The missing capability on a `capability_required` denial
23
+ * (e.g. "write", "invoke", "see_runs"). */
24
+ readonly capability?: string;
25
+
3
26
  constructor(
4
27
  public readonly status: number,
5
28
  message: string,
29
+ opts?: { retryAfterMs?: number; code?: string; capability?: string },
6
30
  ) {
7
31
  super(message);
8
32
  this.name = "AgentComposeError";
33
+ if (opts?.retryAfterMs !== undefined) this.retryAfterMs = opts.retryAfterMs;
34
+ if (opts?.code !== undefined) this.code = opts.code;
35
+ if (opts?.capability !== undefined) this.capability = opts.capability;
9
36
  }
10
37
  }
38
+
39
+ /** Parse a `Retry-After` header into milliseconds. Accepts both RFC 9110
40
+ * forms — delta-seconds (`"120"`) and HTTP-date — and returns `undefined`
41
+ * for anything unparseable so callers fall back to their own backoff. */
42
+ export function parseRetryAfterMs(header: string | null | undefined): number | undefined {
43
+ if (!header) return undefined;
44
+ const seconds = Number(header);
45
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
46
+ const date = Date.parse(header);
47
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
48
+ return undefined;
49
+ }
package/src/index.ts CHANGED
@@ -16,7 +16,6 @@
16
16
  // Factory functions
17
17
  export { defineRuntime } from "./types/runtime.js";
18
18
  export { defineWorkflow } from "./types/workflow.js";
19
- export type { WorkflowDefinition } from "./types/workflow.js";
20
19
  export { defineSandboxEnvironment } from "./types/sandbox-environment.js";
21
20
  export type { SandboxEnvironmentDefinition } from "./types/sandbox-environment.js";
22
21
 
@@ -31,8 +30,6 @@ export type {
31
30
 
32
31
  // Workflow types and runtime utilities
33
32
  export type {
34
- WorkflowFn,
35
- WorkflowCtx,
36
33
  WorkflowRun,
37
34
  AgentBudget,
38
35
  WorkflowHooks,
@@ -46,7 +43,7 @@ export type { ConnectorRequestRules } from "./types/workflow-metadata.js";
46
43
 
47
44
  // Snapshot entry type re-exported for consumers (dashboard, CLI).
48
45
  export type { RunSnapshotEntry } from "./client.js";
49
- export type { WorkflowPlan, WorkflowStepPlan } from "./types/workflow-plan.js";
46
+ export type { WorkflowPlan, WorkflowStepPlan, WorkflowStepDeliverable } from "./types/workflow-plan.js";
50
47
  export type { BaseExecutionContext, InvokeChild } from "./types/execution-context.js";
51
48
 
52
49
  // Request context — per-run typed bag (tenant identity + freeform).
@@ -77,6 +74,8 @@ export {
77
74
  requireScope,
78
75
  redactPattern,
79
76
  createGatePauseProcessor,
77
+ createAskHumanProcessor,
78
+ ASK_USER_QUESTION_TOOL,
80
79
  } from "./processors/index.js";
81
80
  export type {
82
81
  Processor,
@@ -106,6 +105,8 @@ export type {
106
105
  export type {
107
106
  SandboxProvider,
108
107
  DesktopSandboxProvider,
108
+ SandboxPtyOpts,
109
+ SandboxPtyHandle,
109
110
  } from "./types/sandbox.js";
110
111
 
111
112
  // HTTP client
@@ -120,6 +121,8 @@ export type {
120
121
  TeamMember, Mention, CreateMentionsInput,
121
122
  EventSubjectType, EventRow, ReportEventInput, ListEventsOptions, ListEventsResult,
122
123
  RunLogLine, ListRunLogsOptions,
124
+ RunListEntry, ListRunsOptions, RunDetail,
125
+ FundingLane, RunFundingStamp, RunFundingUsageRow, RunFundingResponse,
123
126
  RegisteredRuntime, RunState, RunStatus, FactoryRow, SnapshotListEntry, SnapshotListResponse,
124
127
  ApiKey, ApiKeyCreated,
125
128
  UsageRollupRow, UsageResponse,
@@ -128,13 +131,73 @@ export type {
128
131
  SendAgentMessageOptions, SendAgentMessageResponse,
129
132
  AnswerSteerOptions,
130
133
  ResumePauseOptions, ResumePauseResponse, ResumePauseSuccess, ResumePausePending, ResumePauseActor,
134
+ ConversationMessagePart, ConversationRow, ConversationMessageRow,
135
+ ConversationsPage, ConversationDetail, ConversationThread,
136
+ ConversationPageContext, SendConversationMessageInput, SendConversationMessageResult,
137
+ ConversationTurnState, ConversationPresenceSnapshot, StreamConversationOptions, AgentListRow,
138
+ CreateCloudSessionInput, CloudSessionCreated,
139
+ // Cloud-session developer surface (ADR-0052)
140
+ SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked,
141
+ // Session branch proposals (ADR-0053)
142
+ SessionFileChange, SessionChangeSet,
143
+ SessionMergeReportDetail, SessionMergeReport, SessionDiscardReport,
144
+ // Channel-attached sessions (ADR-0057)
145
+ ChannelSessionStatus, ChannelSessionRow, ChannelSessionsResponse, SessionChannelMessagePosted,
146
+ FactoryFileSearchRow, FactoryFolderSearchRow, SearchFactoryFilesOptions, FactoryFileSearchResult, PublicFileLinkState,
147
+ // Membership-scoped authorization (ADR-0045)
148
+ ConversationMemberRole, ConversationMember,
149
+ DocumentCapability, TemplateCapability, ScopeGrant, ArtifactScope,
150
+ RunContext, SetScopeGrantsInput, SetTemplateScopeInput, TemplateDetail,
151
+ // Projects (ADR-0045 projects extension)
152
+ Project, ProjectsPage, ProjectRole, ProjectMember,
153
+ ProjectObject, ProjectObjectsPage, ProjectSkippedFile, ProjectPreviewFile,
154
+ ProjectAddPreview, AddProjectObjectInput, AddProjectObjectResult,
155
+ RefreshProjectObjectResult,
156
+ // Break-glass compliance sessions (ADR-0051)
157
+ ComplianceScopeKind, ComplianceStatus, ComplianceSession, ComplianceAccess,
158
+ RequestComplianceSessionInput, ListComplianceSessionsOptions, ComplianceSessionsPage,
159
+ ListComplianceAccessesOptions, ComplianceAccessesPage,
131
160
  } from "./client.js";
132
161
 
133
162
  // SSE parser — exposed so tests and downstream callers can reuse it.
134
163
  export { parseSseStream } from "./sse.js";
135
164
 
165
+ // Cloud-session UI directives — the marker contract between the in-sandbox
166
+ // `agentc` CLI and the server's cloud executor.
167
+ export {
168
+ renderDirectiveMarker, parseDirectiveMarkers,
169
+ DIRECTIVE_MARKER_PREFIX, DIRECTIVE_MARKER_SUFFIX, MAX_DIRECTIVES_PER_OUTPUT,
170
+ REVISION_SELECTOR_RE,
171
+ ASK_DIRECTIVE_PROMPT_MAX, ASK_DIRECTIVE_MAX_OPTIONS,
172
+ ASK_DIRECTIVE_OPTION_ID_MAX, ASK_DIRECTIVE_OPTION_LABEL_MAX,
173
+ ASK_APPROVER_MIN, ASK_APPROVER_MAX,
174
+ } from "./directives.js";
175
+ export type { CloudDirective } from "./directives.js";
176
+
177
+ // Conversation stream — typed wire events + the frame normalizer, shared by
178
+ // `streamConversation` and clients that run their own SSE loop (the CLI's
179
+ // `agentc session` terminal driver reuses its bridge parser + normalizes
180
+ // through this, so every surface holds the same durable-vs-live invariant).
181
+ export { normalizeConversationStreamEvent } from "./types/conversation-stream.js";
182
+ export type {
183
+ ConversationStreamEvent, ConversationStreamAuthor,
184
+ ConversationPartEvent, ConversationPartPartialEvent, ConversationMessageDoneEvent,
185
+ ConversationErrorEvent, ConversationReactionEvent, ConversationMessageDeletedEvent,
186
+ ConversationReplayContinueEvent, ConversationTurnStateEvent, ConversationPresenceEvent,
187
+ ConversationSessionStatusEvent,
188
+ ConversationPresenceClient, ConversationAgentPresence,
189
+ } from "./types/conversation-stream.js";
190
+
191
+ // ACP client peer — the JSON-RPC client half of ADR-0020. Re-exported for the
192
+ // CLI's local-agent bridge daemon (`agentc bridge`), which is an ACP client
193
+ // over a locally spawned agent process and reuses the exact same peer +
194
+ // session/update normaliser as the sandbox CLI-agent runtimes.
195
+ export { AcpClientPeer, normalizeSessionUpdate, ACP_PROTOCOL_VERSION } from "./runtimes/_acp-client.js";
196
+ export type { AcpAvailableCommand, AcpClientPeerDeps, AcpSessionCaps } from "./runtimes/_acp-client.js";
197
+
136
198
  // Errors and utilities
137
199
  export { AgentComposeError } from "./errors.js";
200
+ export type { AuthzErrorCode } from "./errors.js";
138
201
  export { formatError } from "./utils/errors.js";
139
202
 
140
203
  // Bundling utilities
@@ -168,12 +231,33 @@ export type { GatewayModelId } from "ai";
168
231
  // CLI-agent runtimes — drive an external agentic CLI (codex / amp) inside the
169
232
  // sandbox and stream-parse its JSONL. No heavy npm deps (the CLI lives in the
170
233
  // sandbox image), so these are root-exported like claudeRuntime.
171
- export { createCodexRuntime } from "./runtimes/codex.js";
234
+ export { createCodexRuntime, codexSpec } from "./runtimes/codex.js";
172
235
  export type { CodexRuntimeConfig } from "./runtimes/codex.js";
173
236
  export { default as codexRuntime } from "./runtimes/codex.js";
237
+ // Reasoning-effort level a CLI runtime turn may carry (claude-code / codex).
238
+ export type { CliReasoningEffort } from "./runtimes/_cli-agent.js";
174
239
  export { createAmpRuntime } from "./runtimes/amp.js";
175
240
  export type { AmpRuntimeConfig } from "./runtimes/amp.js";
176
241
  export { default as ampRuntime } from "./runtimes/amp.js";
242
+ export { createOpencodeRuntime, opencodeSpec } from "./runtimes/opencode.js";
243
+ export type { OpencodeRuntimeConfig } from "./runtimes/opencode.js";
244
+ export { default as opencodeRuntime } from "./runtimes/opencode.js";
245
+
246
+ export { createCursorRuntime, cursorSpec } from "./runtimes/cursor.js";
247
+ export type { CursorRuntimeConfig } from "./runtimes/cursor.js";
248
+ export { default as cursorRuntime } from "./runtimes/cursor.js";
249
+
250
+ export { createDroidRuntime, droidSpec } from "./runtimes/droid.js";
251
+ export type { DroidRuntimeConfig } from "./runtimes/droid.js";
252
+ export { default as droidRuntime } from "./runtimes/droid.js";
253
+
254
+ // Claude Code as a CLI-agent runtime (in-sandbox `claude -p` / the Zed ACP
255
+ // adapter) — the cloud-hostable counterpart of `claudeRuntime` (claude.ts,
256
+ // which drives the Agent SDK in the calling process and so can never run a
257
+ // server-driven cloud-session turn).
258
+ export { createClaudeCodeRuntime, claudeCodeSpec, CLAUDE_CODE_ACP_ADAPTER, CLAUDE_CODE_THINKING_TOKENS } from "./runtimes/claude-code.js";
259
+ export type { ClaudeCodeRuntimeConfig } from "./runtimes/claude-code.js";
260
+ export { default as claudeCodeRuntime } from "./runtimes/claude-code.js";
177
261
 
178
262
  // Built-in coding tools for Vercel AI SDK runtime.
179
263
  export { bashTool, codingTools, editTool, readTool, writeTool } from "./tools/index.js";
@@ -189,7 +273,10 @@ export { createSandbox, reconnectSandbox, killAllSandboxes, killSandboxById,
189
273
  parseSseExecStream, AGENT_COMPOSE_TAG,
190
274
  SANDBOX_VCPUS, DEFAULT_SANDBOX_SIZE, E2B_TEMPLATE_SIZES,
191
275
  isE2bSupportedSize, e2bMachineSpec, e2bBaseTemplate, e2bAgentEnvTemplate,
192
- isPlatformE2bTemplateAlias } from "./sandbox.js";
276
+ isPlatformE2bTemplateAlias,
277
+ // ADR-0038 "Computer" — the per-member persistent desktop machine template.
278
+ E2B_DEVBOX_TEMPLATE, E2B_DEVBOX_SPEC, E2B_DEVBOX_RECIPE_VERSION,
279
+ e2bDevboxTemplateRef } from "./sandbox.js";
193
280
  export { SandboxUnavailableError, SANDBOX_UNAVAILABLE_PREFIX } from "./sandbox-errors.js";
194
281
  export type {
195
282
  SandboxCreateOpts, SandboxNetworkPolicy, SandboxNetworkHeaderTransform,
@@ -197,7 +284,7 @@ export type {
197
284
  SandboxQuotaResult, OwnedSandboxResult, OwnedSandbox, SandboxSize,
198
285
  ParseSseExecStreamOptions, SandboxCommandRunOptions, SandboxCommandResult,
199
286
  } from "./sandbox.js";
200
- export type { SandboxResources } from "./types/workflow-metadata.js";
287
+ export type { SandboxResources, DriveMergePolicy } from "./types/workflow-metadata.js";
201
288
 
202
289
  // Workflow engine
203
290
  export { runWorkflow, WorkflowError, EngineError, classifyError, parseNameVersion } from "./workflows/engine.js";
@@ -218,6 +305,7 @@ export {
218
305
  export type {
219
306
  Step,
220
307
  StepContext,
308
+ StepDeliverable,
221
309
  StepRunResult,
222
310
  Workflow,
223
311
  DefineStepOpts,
@@ -261,7 +349,7 @@ export {
261
349
  } from "./pause/errors.js";
262
350
  export type { PauseErrorCode } from "./pause/errors.js";
263
351
  export type { PauseRequest } from "./pause/pause-core.js";
264
- export type { WaitForEventRequest } from "./pause/wrappers.js";
352
+ export type { WaitForEventRequest, DecisionRequest } from "./pause/wrappers.js";
265
353
  // ADR-0028 — the in-sandbox pause client. `agentc pause` and the gate-pause
266
354
  // processor both pause their OWN run through the server pause API and block
267
355
  // until a human answers (create-then-poll; the VM suspends in place while
@@ -285,7 +373,26 @@ export { agentLoop, parseAgentStatus, DEFAULT_CLAUDE_MODEL } from "./agent/agent
285
373
  export type { AgentLifecycleEvent, AgentLoopOpts, AgentLoopResult } from "./agent/agent-loop.js";
286
374
  export { agent } from "./agent/run-agent.js";
287
375
  export type { AgentOpts } from "./agent/run-agent.js";
288
- export { AGENT_COMPOSE_MANUAL, buildAgentContextDoc, writeAgentContext } from "./agent/agent-context.js";
289
- export type { AgentConnectorInfo } from "./agent/agent-context.js";
376
+ export { AGENT_COMPOSE_MANUAL, buildAgentContextDoc, writeAgentContext, buildAddedSessionBrief } from "./agent/agent-context.js";
377
+ export type { AgentConnectorInfo, AddedSessionBriefParams } from "./agent/agent-context.js";
290
378
  export { AgentMessageSchema, parseAgentResponse } from "./agent/protocol.js";
291
379
  export { importSourceModule, TMP_DIR, LATEST_VERSION } from "./utils/source-loader.js";
380
+
381
+ // The display palette (ADR-0040 §7) — the typed marker contract between
382
+ // `agentc display` and the session executors, plus the promoter that maps
383
+ // native-harness tool traffic onto the platform's rich-card part shapes.
384
+ export {
385
+ DISPLAY_MARKER_KEY, DISPLAY_MARKER_VERSION, PLAN_MAX_ENTRIES, PLAN_ENTRY_MAX_CHARS,
386
+ TABLE_MAX_COLUMNS, TABLE_MAX_ROWS, TABLE_CELL_MAX_CHARS,
387
+ CHART_MAX_SERIES, CHART_MAX_POINTS_PER_SERIES, CHART_LABEL_MAX_CHARS,
388
+ ASK_PROMPT_MAX_CHARS, ASK_MAX_OPTIONS, ASK_OPTION_ID_MAX_CHARS, ASK_OPTION_LABEL_MAX_CHARS,
389
+ serializeDisplayMarker, parseDisplayMarker, findDisplayMarker, clampPlanEntries,
390
+ clampTableData, clampChartSeries, clampChartAxisLabel, clampAskOptions,
391
+ detectAgentcInvocation, shellWords, createDisplayPromoter,
392
+ } from "./display.js";
393
+ export type {
394
+ DisplayMarker, DisplayPlanEntry, DisplayPlanStatus,
395
+ DisplayTableCell, DisplayTableColumn, DisplayTableData,
396
+ DisplayChartKind, DisplayChartPoint, DisplayChartSeries, DisplayAskOption,
397
+ AgentcInvocation, PromotedPart, DisplayPromoter, DisplayPromoterContext,
398
+ } from "./display.js";
@@ -1,21 +1,24 @@
1
1
  /**
2
- * The two opinionated pause wrappers (ADR-0006 §"SDK surface"), each a thin
3
- * closure over `ctx.pause`:
2
+ * The three opinionated pause wrappers (ADR-0006 §"SDK surface", ADR-0042),
3
+ * each a thin closure over `ctx.pause`:
4
4
  *
5
5
  * - `sleep` — a lightweight timed pause; resolves on its own TTL,
6
6
  * skips the snapshot, returns void.
7
7
  * - `waitForEvent` — pause until an event resumes by correlation key.
8
- *
9
- * A typed human/agent decision is NOT a wrapper — it's a plain `ctx.pause`
10
- * with a `schema` (and `payload.options` for the dashboard's answer UI). The
11
- * pause primitive carries the reason (the ask) and the resume value (the
12
- * resolution); there is no separate `requestDecision`.
8
+ * - `requestDecision` — a typed human/agent decision (wire `kind:
9
+ * "decision"`): the prompt/options/approvers ride
10
+ * `payload` for the dashboard's answer UI, and the
11
+ * resume value is the frozen `{ decision: string }`
12
+ * convention. `approvers` travel as RAW member refs
13
+ * (user ids or emails) — the SERVER resolves them
14
+ * against the team roster at pause-insert time and
15
+ * stamps the gate-bearing snapshots (ADR-0042 §2).
13
16
  *
14
17
  * Built from a `PauseFn` so the wrapper logic lives in one place and the step
15
18
  * runner just spreads them onto the context next to `pause`.
16
19
  */
17
20
 
18
- import type { z } from "zod";
21
+ import { z } from "zod";
19
22
  import type { StepPauseRequest } from "../step-invocation/types.js";
20
23
  import type { PauseRequest } from "./pause-core.js";
21
24
 
@@ -23,7 +26,7 @@ import type { PauseRequest } from "./pause-core.js";
23
26
  export type PauseFn = <T = unknown>(req: PauseRequest<T>) => Promise<T>;
24
27
 
25
28
  /** Internal: pause with an explicit wire `kind`. The wrappers stamp
26
- * `sleep`/`event` through this; the public `ctx.pause` is always
29
+ * `decision`/`sleep`/`event` through this; the public `ctx.pause` is always
27
30
  * `custom` and never exposes it. */
28
31
  export type KindedPauseFn = <T = unknown>(req: PauseRequest<T>, kind: StepPauseRequest["kind"]) => Promise<T>;
29
32
 
@@ -35,9 +38,26 @@ export interface WaitForEventRequest<T> {
35
38
  ttlMs?: number;
36
39
  }
37
40
 
41
+ export interface DecisionRequest {
42
+ prompt: string;
43
+ /** Choices the decision UI offers as buttons; free text is always open. */
44
+ options?: Array<string | { label: string; value: string }>;
45
+ /** Team member refs (user ids or emails, ≤ 10) — with any named, ONLY a
46
+ * named approver may resolve the decision, and each gets an Activity
47
+ * ping. Resolution is server-side at pause creation. */
48
+ approvers?: string[];
49
+ /** DecisionDraft artifact rendered alongside the prompt. */
50
+ draft?: Record<string, unknown>;
51
+ ttlMs?: number;
52
+ correlationKey?: string;
53
+ }
54
+
55
+ const DecisionResultSchema = z.object({ decision: z.string() });
56
+
38
57
  export interface PauseWrappers {
39
58
  sleep(durationMs: number): Promise<void>;
40
59
  waitForEvent<T = unknown>(req: WaitForEventRequest<T>): Promise<T>;
60
+ requestDecision(req: DecisionRequest): Promise<{ decision: string }>;
41
61
  }
42
62
 
43
63
  export function buildPauseWrappers(pause: KindedPauseFn): PauseWrappers {
@@ -61,5 +81,20 @@ export function buildPauseWrappers(pause: KindedPauseFn): PauseWrappers {
61
81
  ...(req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {}),
62
82
  }, "event");
63
83
  },
84
+
85
+ requestDecision(req: DecisionRequest): Promise<{ decision: string }> {
86
+ return pause<{ decision: string }>({
87
+ reason: req.prompt,
88
+ payload: {
89
+ prompt: req.prompt,
90
+ ...(req.options !== undefined ? { options: req.options } : {}),
91
+ ...(req.approvers !== undefined ? { approvers: req.approvers } : {}),
92
+ ...(req.draft !== undefined ? { draft: req.draft } : {}),
93
+ },
94
+ schema: DecisionResultSchema,
95
+ ...(req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {}),
96
+ ...(req.correlationKey !== undefined ? { correlationKey: req.correlationKey } : {}),
97
+ }, "decision");
98
+ },
64
99
  };
65
100
  }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Ask-human processor (ADR-0028).
3
+ *
4
+ * Makes "ask a human" a FIRST-CLASS agent affordance: when the agent calls the
5
+ * built-in `AskUserQuestion` tool, this short-circuits it into a SERVER pause
6
+ * (`requestPauseAndAwait`) — the run freezes (compute stops), the question +
7
+ * options land on the human's pause feed, and the human's answer comes back as
8
+ * the tool result. No `agentc pause` CLI for the model to remember, and no
9
+ * dependency on prompt discipline: the moment the agent asks, the run pauses.
10
+ *
11
+ * Loud by construction — the danger this fixes is a pause that SILENTLY doesn't
12
+ * happen (a stale in-sandbox CLI, a non-E2B substrate, an auth error) letting
13
+ * the agent proceed as if it had an answer:
14
+ * - pause cannot be created (server reject) → `Verdict.abort` ENDS the agent
15
+ * loop with a WorkflowError. The run fails loud; it never guesses an answer.
16
+ * - pause expires / is cancelled → the tool result says NO answer came and to
17
+ * not assume one.
18
+ *
19
+ * Lives in the shared `gateToolCall` chain, so one implementation covers every
20
+ * runtime (the Claude Agent SDK `PreToolUse` hook and the ACP permission path).
21
+ * No run credential in the env (local / non-sandbox) → no-op: `AskUserQuestion`
22
+ * passes through untouched so a dev invocation isn't hard-failed.
23
+ */
24
+
25
+ import type { Processor, ProcessorContext, ToolCall } from "./processor.js";
26
+ import { Verdict } from "./processor.js";
27
+ import { requestPauseAndAwait } from "../agent/pause-client.js";
28
+ import type { GatePauseConnection } from "./gate-pause.js";
29
+
30
+ /** The Claude built-in tool an agent uses to ask the user a question. */
31
+ export const ASK_USER_QUESTION_TOOL = "AskUserQuestion";
32
+
33
+ /** One question in an `AskUserQuestion` call (only the fields we read). */
34
+ interface AskQuestion { question?: unknown; header?: unknown; options?: unknown }
35
+
36
+ /** Pull the human-facing question + its option labels out of an
37
+ * `AskUserQuestion` tool input. We pause on the FIRST question (the common
38
+ * case); any others are folded into the reason so nothing is lost. */
39
+ function parseAsk(input: Record<string, unknown>): { reason: string; options: Array<{ id: string; label: string }> } {
40
+ const questions = Array.isArray(input.questions) ? (input.questions as AskQuestion[]) : [];
41
+ const first = questions[0] ?? {};
42
+ const head = typeof first.question === "string" && first.question.trim()
43
+ ? first.question.trim()
44
+ : "The agent needs your input to continue.";
45
+ const extra = questions.length > 1
46
+ ? ` (+${questions.length - 1} more question${questions.length > 2 ? "s" : ""})`
47
+ : "";
48
+ const options = Array.isArray(first.options)
49
+ ? (first.options as Array<{ label?: unknown }>)
50
+ .map((o) => String(o?.label ?? "").trim())
51
+ .filter(Boolean)
52
+ .map((label) => ({ id: label, label }))
53
+ : [];
54
+ return { reason: head + extra, options };
55
+ }
56
+
57
+ /** Unwrap the dashboard's `{ decision }` resume payload to the raw answer text. */
58
+ function answerText(decision: unknown): string {
59
+ const raw = decision !== null && typeof decision === "object" && "decision" in decision
60
+ ? (decision as { decision: unknown }).decision
61
+ : decision;
62
+ return typeof raw === "string" ? raw.trim() : raw == null ? "" : JSON.stringify(raw);
63
+ }
64
+
65
+ /** Recognise the agent shelling out to `agentc pause` and extract the same
66
+ * {reason, options} we'd get from AskUserQuestion. We INTERCEPT it here — in the
67
+ * tool gate, BEFORE the command runs — so the pause takes the DESIGNED path (the
68
+ * answer returns as the tool result and the agent loop continues) instead of the
69
+ * command actually blocking inside the sandbox shell, which froze the runner
70
+ * mid-tool-exec and never resumed the loop. */
71
+ function parseAgentcPause(command: string): { reason: string; options: Array<{ id: string; label: string }> } | null {
72
+ if (!/(^|\s|&&|;|\|)\s*agentc\s+pause(\s|$)/.test(command)) return null;
73
+ const r = command.match(/--reason(?:=|\s+)(?:"([^"]*)"|'([^']*)'|(\S+))/);
74
+ const reason = (r?.[1] ?? r?.[2] ?? r?.[3] ?? "The agent needs your input to continue.").trim();
75
+ const options = [...command.matchAll(/--option(?:=|\s+)(?:"([^"]*)"|'([^']*)'|(\S+))/g)]
76
+ .map((m) => (m[1] ?? m[2] ?? m[3] ?? "").trim())
77
+ .filter(Boolean)
78
+ .map((label) => ({ id: label, label }));
79
+ return { reason, options };
80
+ }
81
+
82
+ /** Pull the ask (reason + options) from either the `AskUserQuestion` tool OR a
83
+ * `Bash` call running `agentc pause`. Null for anything else. */
84
+ function extractAsk(call: ToolCall): { reason: string; options: Array<{ id: string; label: string }> } | null {
85
+ if (call.toolName === ASK_USER_QUESTION_TOOL) return parseAsk(call.toolInput);
86
+ if (call.toolName === "Bash") {
87
+ const cmd = (call.toolInput as { command?: unknown })?.command;
88
+ return typeof cmd === "string" ? parseAgentcPause(cmd) : null;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ export function createAskHumanProcessor(opts: { connection?: GatePauseConnection } = {}): Processor {
94
+ return {
95
+ name: "ask-human",
96
+ async processToolCall(call: ToolCall, ctx: ProcessorContext) {
97
+ // Fires for AskUserQuestion OR a `Bash` call running `agentc pause` — both
98
+ // ask a human and must take the SAME processor path so the answer returns
99
+ // as the tool result and the agent loop continues.
100
+ const ask = extractAsk(call);
101
+ if (!ask) return Verdict.continue(call);
102
+
103
+ const conn = opts.connection ?? {
104
+ baseUrl: process.env.AGENT_COMPOSE_URL ?? "",
105
+ token: process.env.AGENT_COMPOSE_RUN_TOKEN ?? "",
106
+ runId: process.env.RUN_ID ?? "",
107
+ };
108
+ // No run credential (local / non-sandbox) — can't pause; let the tool
109
+ // through rather than hard-failing a dev invocation.
110
+ if (!conn.baseUrl || !conn.token || !conn.runId) return Verdict.continue(call);
111
+
112
+ let decision;
113
+ try {
114
+ decision = await requestPauseAndAwait({
115
+ baseUrl: conn.baseUrl, token: conn.token, runId: conn.runId,
116
+ reason: ask.reason,
117
+ ...(ask.options.length ? { options: ask.options } : {}),
118
+ action: { tool: call.toolName, input: call.toolInput },
119
+ signal: ctx.abortSignal,
120
+ });
121
+ } catch (err) {
122
+ // The pause could NOT be honored (server reject, non-E2B substrate, auth).
123
+ // Abort the loop — never let the agent proceed as if it had an answer.
124
+ const msg = err instanceof Error ? err.message : String(err);
125
+ return Verdict.abort(`Could not ask the human — the run could not be paused (${msg}). Stopping rather than guessing an answer.`);
126
+ }
127
+
128
+ if (decision.status === "resolved") {
129
+ const answer = answerText(decision.decision);
130
+ return Verdict.deny(`The human answered: ${answer || "(no text returned)"}. Continue using this answer.`);
131
+ }
132
+ // Expired / cancelled — no answer. Do NOT let the agent assume one.
133
+ return Verdict.deny(`No answer came back (${decision.status}). Do NOT assume an answer — ask again, or stop and report exactly what you need from a human.`);
134
+ },
135
+ };
136
+ }
@@ -20,3 +20,8 @@ export {
20
20
  // through the shared gateToolCall chain.
21
21
  export { createGatePauseProcessor } from "./gate-pause.js";
22
22
  export type { GatePausePolicy, GatePauseApproval, GatePauseConnection } from "./gate-pause.js";
23
+
24
+ // ADR-0028 — first-class "ask a human": maps the agent's `AskUserQuestion` tool
25
+ // to a server pause and returns the human's answer as the tool result. Added by
26
+ // default to every agent (no-op unless the tool is granted + called).
27
+ export { createAskHumanProcessor, ASK_USER_QUESTION_TOOL } from "./ask-human.js";
@@ -60,6 +60,28 @@ const OPTION_KIND_FOR_VERDICT = {
60
60
  deny: ["reject_once", "reject_always"],
61
61
  } as const;
62
62
 
63
+ /** Session-lifecycle capabilities the agent advertised at `initialize`,
64
+ * surfaced (not discarded) so callers can pick a resume strategy
65
+ * (ADR-0037 §3). The in-sandbox CLI-agent runtimes ignore both — they
66
+ * resume via the CLI's own on-disk rollout (ADR-0020 Q4); the bridge
67
+ * daemon attempts `session/resume` only when `resume` is true. */
68
+ export interface AcpSessionCaps {
69
+ /** Agent supports `session/load` — full-history replay as
70
+ * `agent_message_chunk`s. Deliberately NOT used for resume: replaying
71
+ * history would re-emit the transcript as new parts and duplicate the
72
+ * conversation (ADR-0037 alternative D). */
73
+ loadSession: boolean;
74
+ /** Agent supports `session/resume` — context restore with NO replay. */
75
+ resume: boolean;
76
+ }
77
+
78
+ /** One entry of the agent's slash-command palette, as announced by the ACP
79
+ * `available_commands_update` session notification. */
80
+ export interface AcpAvailableCommand {
81
+ name: string;
82
+ description: string;
83
+ }
84
+
63
85
  /** Collaborators `AcpClientPeer` needs from `CliAgentRunner`, kept as a small
64
86
  * injected surface so the peer stays transport-agnostic and unit-testable. */
65
87
  export interface AcpClientPeerDeps {
@@ -80,6 +102,14 @@ export interface AcpClientPeerDeps {
80
102
  /** Turn-scoped abort signal from `sendMessage`. On abort the peer sends
81
103
  * `session/cancel` for the live session. */
82
104
  signal?: AbortSignal;
105
+ /** The agent announced (or changed) its slash-command palette — an ACP
106
+ * `available_commands_update` session NOTIFICATION, which can arrive at
107
+ * any time (typically right after session/new, outside any prompt), so
108
+ * it rides a callback rather than the per-turn message stream. Each call
109
+ * carries the ENTIRE palette (replace semantics). Optional: the
110
+ * in-sandbox runtimes ignore it; the bridge daemon reports it to the
111
+ * server for composer autocomplete. */
112
+ onAvailableCommands?: (commands: AcpAvailableCommand[]) => void;
83
113
  }
84
114
 
85
115
  // ── The single session/update → AgentMessage normaliser ───────────────────────
@@ -270,6 +300,17 @@ export class AcpClientPeer {
270
300
  // answers method-not-found.
271
301
  const handler: Client = {
272
302
  sessionUpdate: (params: acp.SessionNotification): void => {
303
+ // The slash-command palette is session state, not turn content —
304
+ // it can land OUTSIDE any prompt (right after session/new), when
305
+ // no turn queue is being drained, so it goes to its own callback
306
+ // instead of the AgentMessage stream (which drops it).
307
+ if (params.update.sessionUpdate === "available_commands_update") {
308
+ this.deps.onAvailableCommands?.(params.update.availableCommands.map((c) => ({
309
+ name: c.name,
310
+ description: c.description,
311
+ })));
312
+ return;
313
+ }
273
314
  // Push mapped messages onto the queue. Tolerate updates that race in
274
315
  // after cancel (the agent may flush a few before unwinding) — the
275
316
  // normaliser handles them the same way.
@@ -290,9 +331,12 @@ export class AcpClientPeer {
290
331
  * Negotiate the wire protocol at `initialize`. Pins `protocolVersion` to
291
332
  * integer 1 and advertises NO fs / terminal capabilities. Returns the
292
333
  * version the agent negotiated — `CliAgentRunner` compares it to
293
- * `ACP_PROTOCOL_VERSION` to decide ACP-path vs. legacy-JSONL fallback.
334
+ * `ACP_PROTOCOL_VERSION` to decide ACP-path vs. legacy-JSONL fallback —
335
+ * plus the agent's advertised session-lifecycle capabilities (`caps`),
336
+ * which the bridge daemon caches as `agent_sessions.resume_supported`
337
+ * (ADR-0037 §3). Absent capability blocks mean "not supported".
294
338
  */
295
- async initialize(): Promise<{ protocolVersion: number }> {
339
+ async initialize(): Promise<{ protocolVersion: number; caps: AcpSessionCaps }> {
296
340
  const res = await this.conn.initialize({
297
341
  protocolVersion: ACP_PROTOCOL_VERSION,
298
342
  clientCapabilities: {
@@ -300,7 +344,15 @@ export class AcpClientPeer {
300
344
  terminal: false,
301
345
  },
302
346
  });
303
- return { protocolVersion: res.protocolVersion };
347
+ return {
348
+ protocolVersion: res.protocolVersion,
349
+ caps: {
350
+ loadSession: res.agentCapabilities?.loadSession ?? false,
351
+ // Per the ACP schema, `resume` is a capability OBJECT (`{}` =
352
+ // supported), not a boolean — presence is the signal.
353
+ resume: res.agentCapabilities?.sessionCapabilities?.resume != null,
354
+ },
355
+ };
304
356
  }
305
357
 
306
358
  /**
@@ -316,6 +368,23 @@ export class AcpClientPeer {
316
368
  return res.sessionId;
317
369
  }
318
370
 
371
+ /**
372
+ * Resume an existing session by id — ACP `session/resume` (ADR-0037 §3).
373
+ * ADDITIVE and opt-in: the in-sandbox CLI-agent runtimes never call this
374
+ * (their resume path is the CLI's own persisted rollout, see
375
+ * `startSession`); the bridge daemon calls it only when `initialize`
376
+ * advertised `caps.resume`. Unlike `session/load` there is NO history
377
+ * replay — the agent restores its internal context and returns nothing,
378
+ * so nothing is re-emitted through the caller's part sink. Throws on an
379
+ * unknown/expired session id (JSON-RPC error from the agent) — the caller
380
+ * marks the durable row stale and falls back to `startSession` + full
381
+ * re-render. `this.sessionId` is only adopted on success.
382
+ */
383
+ async resumeSession(cwd: string, acpSessionId: string): Promise<void> {
384
+ await this.conn.resumeSession({ sessionId: acpSessionId, cwd, mcpServers: [] });
385
+ this.sessionId = acpSessionId;
386
+ }
387
+
319
388
  /**
320
389
  * Drive one `session/prompt` turn, yielding `AgentMessage`s as the agent
321
390
  * streams `session/update` notifications. Resolves the generator when the