@sema-agent/sdk 0.0.46

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 (160) hide show
  1. package/LICENSE +103 -0
  2. package/README.md +53 -0
  3. package/dist/client.d.ts +55 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +113 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/control-client.d.ts +36 -0
  8. package/dist/control-client.d.ts.map +1 -0
  9. package/dist/control-client.js +72 -0
  10. package/dist/control-client.js.map +1 -0
  11. package/dist/control-types.d.ts +282 -0
  12. package/dist/control-types.d.ts.map +1 -0
  13. package/dist/control-types.js +2 -0
  14. package/dist/control-types.js.map +1 -0
  15. package/dist/errors.d.ts +99 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +190 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/events.d.ts +87 -0
  20. package/dist/events.d.ts.map +1 -0
  21. package/dist/events.js +4 -0
  22. package/dist/events.js.map +1 -0
  23. package/dist/idempotency.d.ts +3 -0
  24. package/dist/idempotency.d.ts.map +1 -0
  25. package/dist/idempotency.js +9 -0
  26. package/dist/idempotency.js.map +1 -0
  27. package/dist/index.d.ts +23 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +11 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/resources/approvals.d.ts +40 -0
  32. package/dist/resources/approvals.d.ts.map +1 -0
  33. package/dist/resources/approvals.js +83 -0
  34. package/dist/resources/approvals.js.map +1 -0
  35. package/dist/resources/assistant.d.ts +23 -0
  36. package/dist/resources/assistant.d.ts.map +1 -0
  37. package/dist/resources/assistant.js +59 -0
  38. package/dist/resources/assistant.js.map +1 -0
  39. package/dist/resources/control/auth-providers.d.ts +18 -0
  40. package/dist/resources/control/auth-providers.d.ts.map +1 -0
  41. package/dist/resources/control/auth-providers.js +16 -0
  42. package/dist/resources/control/auth-providers.js.map +1 -0
  43. package/dist/resources/control/config.d.ts +31 -0
  44. package/dist/resources/control/config.d.ts.map +1 -0
  45. package/dist/resources/control/config.js +28 -0
  46. package/dist/resources/control/config.js.map +1 -0
  47. package/dist/resources/control/fleet.d.ts +31 -0
  48. package/dist/resources/control/fleet.d.ts.map +1 -0
  49. package/dist/resources/control/fleet.js +31 -0
  50. package/dist/resources/control/fleet.js.map +1 -0
  51. package/dist/resources/control/images.d.ts +19 -0
  52. package/dist/resources/control/images.d.ts.map +1 -0
  53. package/dist/resources/control/images.js +28 -0
  54. package/dist/resources/control/images.js.map +1 -0
  55. package/dist/resources/control/lifecycle.d.ts +15 -0
  56. package/dist/resources/control/lifecycle.d.ts.map +1 -0
  57. package/dist/resources/control/lifecycle.js +15 -0
  58. package/dist/resources/control/lifecycle.js.map +1 -0
  59. package/dist/resources/control/publish.d.ts +20 -0
  60. package/dist/resources/control/publish.d.ts.map +1 -0
  61. package/dist/resources/control/publish.js +20 -0
  62. package/dist/resources/control/publish.js.map +1 -0
  63. package/dist/resources/control/secrets.d.ts +27 -0
  64. package/dist/resources/control/secrets.d.ts.map +1 -0
  65. package/dist/resources/control/secrets.js +26 -0
  66. package/dist/resources/control/secrets.js.map +1 -0
  67. package/dist/resources/control/users.d.ts +27 -0
  68. package/dist/resources/control/users.d.ts.map +1 -0
  69. package/dist/resources/control/users.js +23 -0
  70. package/dist/resources/control/users.js.map +1 -0
  71. package/dist/resources/control/versioning.d.ts +21 -0
  72. package/dist/resources/control/versioning.d.ts.map +1 -0
  73. package/dist/resources/control/versioning.js +23 -0
  74. package/dist/resources/control/versioning.js.map +1 -0
  75. package/dist/resources/control/workers.d.ts +18 -0
  76. package/dist/resources/control/workers.d.ts.map +1 -0
  77. package/dist/resources/control/workers.js +17 -0
  78. package/dist/resources/control/workers.js.map +1 -0
  79. package/dist/resources/elicitations.d.ts +28 -0
  80. package/dist/resources/elicitations.d.ts.map +1 -0
  81. package/dist/resources/elicitations.js +15 -0
  82. package/dist/resources/elicitations.js.map +1 -0
  83. package/dist/resources/fleet.d.ts +65 -0
  84. package/dist/resources/fleet.d.ts.map +1 -0
  85. package/dist/resources/fleet.js +55 -0
  86. package/dist/resources/fleet.js.map +1 -0
  87. package/dist/resources/images.d.ts +24 -0
  88. package/dist/resources/images.d.ts.map +1 -0
  89. package/dist/resources/images.js +28 -0
  90. package/dist/resources/images.js.map +1 -0
  91. package/dist/resources/leader.d.ts +19 -0
  92. package/dist/resources/leader.d.ts.map +1 -0
  93. package/dist/resources/leader.js +18 -0
  94. package/dist/resources/leader.js.map +1 -0
  95. package/dist/resources/memory.d.ts +29 -0
  96. package/dist/resources/memory.d.ts.map +1 -0
  97. package/dist/resources/memory.js +36 -0
  98. package/dist/resources/memory.js.map +1 -0
  99. package/dist/resources/models.d.ts +13 -0
  100. package/dist/resources/models.d.ts.map +1 -0
  101. package/dist/resources/models.js +10 -0
  102. package/dist/resources/models.js.map +1 -0
  103. package/dist/resources/policy.d.ts +10 -0
  104. package/dist/resources/policy.d.ts.map +1 -0
  105. package/dist/resources/policy.js +10 -0
  106. package/dist/resources/policy.js.map +1 -0
  107. package/dist/resources/questions.d.ts +38 -0
  108. package/dist/resources/questions.d.ts.map +1 -0
  109. package/dist/resources/questions.js +15 -0
  110. package/dist/resources/questions.js.map +1 -0
  111. package/dist/resources/runs.d.ts +43 -0
  112. package/dist/resources/runs.d.ts.map +1 -0
  113. package/dist/resources/runs.js +72 -0
  114. package/dist/resources/runs.js.map +1 -0
  115. package/dist/resources/session-sync.d.ts +38 -0
  116. package/dist/resources/session-sync.d.ts.map +1 -0
  117. package/dist/resources/session-sync.js +97 -0
  118. package/dist/resources/session-sync.js.map +1 -0
  119. package/dist/resources/sessions.d.ts +46 -0
  120. package/dist/resources/sessions.d.ts.map +1 -0
  121. package/dist/resources/sessions.js +70 -0
  122. package/dist/resources/sessions.js.map +1 -0
  123. package/dist/resources/tasks.d.ts +19 -0
  124. package/dist/resources/tasks.d.ts.map +1 -0
  125. package/dist/resources/tasks.js +53 -0
  126. package/dist/resources/tasks.js.map +1 -0
  127. package/dist/resources/trace.d.ts +43 -0
  128. package/dist/resources/trace.d.ts.map +1 -0
  129. package/dist/resources/trace.js +65 -0
  130. package/dist/resources/trace.js.map +1 -0
  131. package/dist/resources/usage.d.ts +10 -0
  132. package/dist/resources/usage.d.ts.map +1 -0
  133. package/dist/resources/usage.js +10 -0
  134. package/dist/resources/usage.js.map +1 -0
  135. package/dist/resources/workflows.d.ts +20 -0
  136. package/dist/resources/workflows.d.ts.map +1 -0
  137. package/dist/resources/workflows.js +43 -0
  138. package/dist/resources/workflows.js.map +1 -0
  139. package/dist/settings.d.ts +44 -0
  140. package/dist/settings.d.ts.map +1 -0
  141. package/dist/settings.js +2 -0
  142. package/dist/settings.js.map +1 -0
  143. package/dist/sse.d.ts +26 -0
  144. package/dist/sse.d.ts.map +1 -0
  145. package/dist/sse.js +244 -0
  146. package/dist/sse.js.map +1 -0
  147. package/dist/sync.d.ts +34 -0
  148. package/dist/sync.d.ts.map +1 -0
  149. package/dist/sync.js +143 -0
  150. package/dist/sync.js.map +1 -0
  151. package/dist/transport.d.ts +37 -0
  152. package/dist/transport.d.ts.map +1 -0
  153. package/dist/transport.js +134 -0
  154. package/dist/transport.js.map +1 -0
  155. package/dist/types.d.ts +574 -0
  156. package/dist/types.d.ts.map +1 -0
  157. package/dist/types.js +2 -0
  158. package/dist/types.js.map +1 -0
  159. package/openapi.yaml +3434 -0
  160. package/package.json +42 -0
package/openapi.yaml ADDED
@@ -0,0 +1,3434 @@
1
+ openapi: 3.1.0
2
+
3
+ # ─────────────────────────────────────────────────────────────────────────────
4
+ # M0 wire contract β€” the SINGLE SOURCE OF TRUTH for the usage plane.
5
+ # Consumed by @sema-agent/sdk (β†’ both doors: the CC/Codex MCP faΓ§ade and the
6
+ # portal BFF). Producer = the service. BOTH sides anchor contract tests to THIS
7
+ # file; drift fails in CI, not in prod (CORE-PRINCIPLES Β§5).
8
+ #
9
+ # Authoritative producer input: ../docs/SERVICE-CORE-CONTEXT.md Β§2/Β§4.
10
+ # packages/sdk/src/types.ts + events.ts + errors.ts MIRROR these schemas (kept in sync by hand;
11
+ # the contract test guards the drift). Where types.ts intentionally leaves an M2 surface as
12
+ # `unknown`, this spec may be slightly ahead β€” those are marked `x-status: draft`.
13
+ #
14
+ # x-status legend:
15
+ # live β€” implemented & verified server-side; in the contract-test assertion set.
16
+ # draft β€” verb exists but the response shape is not fully pinned; co-design pending (do not over-assert).
17
+ # planned β€” NOT yet implemented server-side; documented for SDK/producer alignment; EXCLUDED from
18
+ # contract-test assertions until it ships.
19
+ # gated β€” implemented but off by a server deploy flag (e.g. leader); not on the default product path.
20
+ # ─────────────────────────────────────────────────────────────────────────────
21
+
22
+ info:
23
+ title: AI Agent Fleet β€” Usage-Plane Wire Contract
24
+ version: 0.1.0
25
+ description: >
26
+ M0 contract for the agent-service worker as consumed by the usage plane.
27
+ Conventions (enforced by the SDK + this contract):
28
+ β€’ Every request carries `Authorization: Bearer <SERVICE_AUTH_TOKEN>` (SERVER-SIDE ONLY β€” the SDK's
29
+ browser guard refuses to hold this in a browser; the portal's browser talks to its own BFF) and
30
+ `x-agent-principal: <principal>` (REQUIRED; opaque to the service; principal-first, CORE-PRINCIPLES Β§3).
31
+ β€’ Submit-class POSTs (`/v1/tasks`, `/v1/runs`) carry an `Idempotency-Key` (the SDK auto-generates one;
32
+ in-process dedupe window = 15 min; cross-replica hard-dedupe = the session-active 409, see Β§below).
33
+ β€’ New contract fields are OPTIONAL with no default (CORE-PRINCIPLES Β§6).
34
+ β€’ Async results are delivered by POLL (`GET /v1/runs/:id`) or SSE (`/v1/runs/:id/events`).
35
+ There is NO generic webhook/callback and NO signing (SERVICE-CORE-CONTEXT Β§2.4).
36
+ x-sources:
37
+ - ../docs/SERVICE-CORE-CONTEXT.md # Β§2 wire answers, Β§4 verb surface (authoritative producer input)
38
+ - ../docs/CORE-PRINCIPLES.md # Β§2 secret discipline, Β§3 principal-first, Β§5 contract-as-anchor, Β§6 optional-no-default
39
+ x-contract-test:
40
+ producer: the service
41
+ consumer: '@sema-agent/sdk (packages/sdk)'
42
+ note: 'Both sides assert against this file. `x-status` planned|draft operations are excluded until live/pinned.'
43
+
44
+ servers:
45
+ - url: '{baseUrl}'
46
+ description: >
47
+ Worker base URL β€” injected from deploy env, NEVER hardcoded (secret/config discipline, CORE-PRINCIPLES Β§2:
48
+ config stores env-NAME, not values). e.g. a Traefik route prefix in front of a worker pod.
49
+ variables:
50
+ baseUrl:
51
+ default: http://worker.invalid
52
+ description: Placeholder. Real value comes from the integrator's env (e.g. SERVICE_BASE_URL).
53
+
54
+ security:
55
+ - bearerAuth: []
56
+
57
+ tags:
58
+ - name: tasks
59
+ description: Synchronous tasks (run-to-completion + live token stream).
60
+ - name: runs
61
+ description: Asynchronous, durable, resumable runs (the door-B / HITL path).
62
+ - name: sessions
63
+ description: Conversation continuity (the "对话摆") audit reads.
64
+ - name: approvals
65
+ description: HITL β€” approve/deny a policy gate or answer an AskUserQuestion (SERVICE-CORE-CONTEXT Β§3.4).
66
+ - name: assistant
67
+ description: >
68
+ Assistant-scheduler + HITL decision surface (ASSISTANT-WIRE-CONTRACT.md, service main 6cd0164 /
69
+ core 1.110.0, LIVE on worker): severity-sorted inbox, N-task overview, and the 3 DISTINCT decision
70
+ wires (resume / plan_review / preempt). Owner-gated via the principal header (NOT D-G crypto-bound β€”
71
+ only /decide is, Β§6); a non-owner non-operator gets 404 (no existence oracle), never 403.
72
+ - name: trace
73
+ description: Workspace data source β€” list/turns/stream + jobId grouping (the anti-fragmentation substrate).
74
+ - name: usage
75
+ description: Read-only per-principal cost/quota snapshot (the CLI `/cost` panel's real data).
76
+ - name: policy
77
+ description: Read-only effective-governance snapshot (the CLI `/permissions` panel's real data; inspect-only).
78
+ - name: workflows
79
+ description: >
80
+ S8 self-orchestration run observability (design/97/98) β€” owner-scoped list / detail / live SSE.
81
+ Gated server-side (SELF_ORCHESTRATION_ENABLED, default OFF β†’ 501); replica-local (list/stream see this
82
+ replica's runs). NOT resumable on the stream (in-process subscription; a drop is a full restart).
83
+ - name: fleet
84
+ description: >
85
+ MF-Fleet (shell-host data contract) β€” the LIVE multi-row fleet view SSE (active task + workflow rows,
86
+ transitioning over time). Owner-scoped by the credential principal (scope wire-stripped) unless fleet-wide;
87
+ in-process bus, NOT resumable (a drop is a full restart). No fleet wiring β†’ 501.
88
+ - name: leader
89
+ description: v2 leader pipeline (gated; E2B + git push). NOT the default door-B brain.
90
+ - name: metrics
91
+ description: Worker health/cost (ops; uses metricsToken, not a principal).
92
+
93
+ paths:
94
+
95
+ /v1/capabilities:
96
+ get:
97
+ tags: [metrics]
98
+ operationId: capabilities
99
+ x-status: live # service 7aa0f3d. Booleans share deps with the route gates.
100
+ summary: Worker capability discovery (what surfaces are enabled).
101
+ description: >
102
+ One call replaces probe-by-501. Booleans are computed from the SAME deps as the route gates β€”
103
+ "capabilities says yes but the route 501s" is structurally impossible (producer-tested). Auth = same
104
+ as config surfaces (Bearer authToken; NO principal header). Open set: ignore unknown keys.
105
+ `pricingConfigured` disambiguates stats.costMicroUsd=0 (true zero vs MODEL_COST_* unset) β€” it is
106
+ worker-level, deliberately NOT on per-result stats (the pinned wire contract β‘‘).
107
+ responses:
108
+ '200':
109
+ description: Capability map.
110
+ content:
111
+ application/json:
112
+ schema: { $ref: '#/components/schemas/Capabilities' }
113
+ '401': { $ref: '#/components/responses/Unauthorized' }
114
+
115
+ /v1/memory:
116
+ parameters:
117
+ - $ref: '#/components/parameters/PrincipalHeader'
118
+ get:
119
+ tags: [sessions]
120
+ operationId: memoryGet
121
+ x-status: live # service 170c384. Owner-scoped by construction.
122
+ summary: Read your own <user_memory> (transparency surface).
123
+ description: >
124
+ Scope derives from the request principal (same rule as task execution) β€” a caller CANNOT address
125
+ anyone else's memory. No principal header β†’ 401. Discover via capabilities().memory.
126
+ responses:
127
+ '200':
128
+ description: The memory record (content null = empty).
129
+ content:
130
+ application/json:
131
+ schema: { $ref: '#/components/schemas/MemoryRecord' }
132
+ '401': { $ref: '#/components/responses/Unauthorized' }
133
+ delete:
134
+ tags: [sessions]
135
+ operationId: memoryClear
136
+ x-status: live
137
+ summary: Clear your WHOLE memory scope (per-entry deletion is a future additive).
138
+ responses:
139
+ '200':
140
+ description: Cleared.
141
+ content:
142
+ application/json:
143
+ schema:
144
+ type: object
145
+ required: [ok, scope]
146
+ properties:
147
+ ok: { type: boolean }
148
+ scope: { type: string }
149
+ '401': { $ref: '#/components/responses/Unauthorized' }
150
+
151
+ /v1/tasks:
152
+ parameters:
153
+ - $ref: '#/components/parameters/PrincipalHeader'
154
+ post:
155
+ tags: [tasks]
156
+ operationId: tasksSubmit
157
+ x-status: live
158
+ summary: Run a short task to completion (synchronous).
159
+ description: >
160
+ Synchronous run-to-completion. Auto `Idempotency-Key` (β†’ retry-safe). Supports `verify`/`cascade`.
161
+ For anything that may take minutes or hit a HITL gate, use `/v1/runs` instead (this call blocks).
162
+ parameters:
163
+ - $ref: '#/components/parameters/IdempotencyKey'
164
+ requestBody:
165
+ required: true
166
+ content:
167
+ application/json:
168
+ schema: { $ref: '#/components/schemas/TaskRequest' }
169
+ responses:
170
+ '200':
171
+ description: Task result.
172
+ content:
173
+ application/json:
174
+ schema: { $ref: '#/components/schemas/TaskResult' }
175
+ '400': { $ref: '#/components/responses/BadRequest' }
176
+ '401': { $ref: '#/components/responses/Unauthorized' }
177
+ '409': { $ref: '#/components/responses/Conflict' }
178
+ '429': { $ref: '#/components/responses/RateLimited' }
179
+ '503': { $ref: '#/components/responses/Unauthorized' }
180
+ get:
181
+ tags: [trace]
182
+ operationId: traceList
183
+ x-status: live
184
+ summary: List runs, optionally filtered by jobId (the work-view grouping).
185
+ description: >
186
+ Lists runs (keyset pagination). `?jobId=<id>` returns all runs of one logical job β€” the substrate the
187
+ workspace uses to group a job's scattered runs across BOTH doors (jobId LIVE, service 63b8696).
188
+ Summary items carry `jobId` when present. Full per-task shape beyond the summary is draft (see traceTurns).
189
+ parameters:
190
+ - in: query
191
+ name: jobId
192
+ required: false
193
+ schema: { type: string, minLength: 1, maxLength: 64 }
194
+ description: Filter to one logical job's runs (idx_job, keyset).
195
+ - in: query
196
+ name: status
197
+ required: false
198
+ schema: { $ref: '#/components/schemas/RunStatus' }
199
+ description: Filter by run status.
200
+ - in: query
201
+ name: source
202
+ required: false
203
+ schema: { type: string }
204
+ description: Filter by credential-derived system (oa | cc-mcp | portal …). the pinned wire contract.
205
+ - in: query
206
+ name: owner
207
+ required: false
208
+ schema: { type: string }
209
+ description: >
210
+ Filter to one principal's runs. AUTHZ IS THE BFF'S RESPONSIBILITY (trace surface trusts the
211
+ system token): ordinary users MUST be forced to owner=<their principal>; operator role may omit.
212
+ the pinned wire contract.
213
+ - in: query
214
+ name: limit
215
+ required: false
216
+ schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
217
+ - in: query
218
+ name: cursor
219
+ required: false
220
+ schema: { type: string }
221
+ description: Keyset pagination cursor (opaque).
222
+ responses:
223
+ '200':
224
+ description: Run summaries.
225
+ content:
226
+ application/json:
227
+ schema: { $ref: '#/components/schemas/TaskList' }
228
+ '401': { $ref: '#/components/responses/Unauthorized' }
229
+ '429': { $ref: '#/components/responses/RateLimited' }
230
+
231
+ /v1/tasks/stream:
232
+ parameters:
233
+ - $ref: '#/components/parameters/PrincipalHeader'
234
+ post:
235
+ tags: [tasks]
236
+ operationId: tasksStream
237
+ x-status: live
238
+ summary: Live token stream (synchronous, per-token).
239
+ description: >
240
+ Same instance, transient, per-token SSE. `verify`/`cascade` are REJECTED here with 400 (multi-attempt,
241
+ not a stream). NOT a resumable surface β€” a client disconnect ABORTS the run (kills token burn). HITL can
242
+ still checkpoint mid-stream, but RESUME is only via a durable `/v1/runs` + `/v1/approvals` (the pinned wire contract/[5]
243
+ Q3). Use this only for fast, single-connection, no-HITL interactions; the portal standardizes on `/v1/runs`.
244
+ requestBody:
245
+ required: true
246
+ content:
247
+ application/json:
248
+ schema: { $ref: '#/components/schemas/TaskRequest' }
249
+ responses:
250
+ '200':
251
+ description: SSE stream of AgentEvents (per-token text/reasoning carry the `delta` flag).
252
+ content:
253
+ text/event-stream:
254
+ schema: { $ref: '#/components/schemas/AgentEvent' }
255
+ '400':
256
+ description: Bad request β€” e.g. `verify`/`cascade` not supported on the stream, or invalid `jobId`.
257
+ content:
258
+ application/json:
259
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
260
+ '401': { $ref: '#/components/responses/Unauthorized' }
261
+ '429': { $ref: '#/components/responses/RateLimited' }
262
+
263
+ /v1/runs:
264
+ parameters:
265
+ - $ref: '#/components/parameters/PrincipalHeader'
266
+ post:
267
+ tags: [runs]
268
+ operationId: runsCreate
269
+ x-status: live
270
+ summary: Start an async run (202).
271
+ description: >
272
+ Async; runs in the background. Requires a TiDB run store (absent β†’ 501). Auto `Idempotency-Key`.
273
+ Poll `GET /v1/runs/:id` or stream `GET /v1/runs/:id/events`. A client disconnect does NOT stop the run.
274
+ parameters:
275
+ - $ref: '#/components/parameters/IdempotencyKey'
276
+ requestBody:
277
+ required: true
278
+ content:
279
+ application/json:
280
+ schema: { $ref: '#/components/schemas/TaskRequest' }
281
+ responses:
282
+ '202':
283
+ description: Run accepted (the capability token is NEVER on the wire β€” server-internal).
284
+ content:
285
+ application/json:
286
+ schema: { $ref: '#/components/schemas/RunReceipt' }
287
+ '400': { $ref: '#/components/responses/BadRequest' }
288
+ '401': { $ref: '#/components/responses/Unauthorized' }
289
+ '409': { $ref: '#/components/responses/Conflict' }
290
+ '429': { $ref: '#/components/responses/RateLimited' }
291
+ '501': { $ref: '#/components/responses/NotImplemented' }
292
+ '503': { $ref: '#/components/responses/Unauthorized' }
293
+
294
+ /v1/runs/{taskId}:
295
+ parameters:
296
+ - $ref: '#/components/parameters/PrincipalHeader'
297
+ - $ref: '#/components/parameters/TaskIdPath'
298
+ get:
299
+ tags: [runs]
300
+ operationId: runsGet
301
+ x-status: live
302
+ summary: Read run state (poll).
303
+ description: Owner-scoped read. Response includes `jobId` when the run has one.
304
+ responses:
305
+ '200':
306
+ description: Run state.
307
+ content:
308
+ application/json:
309
+ schema: { $ref: '#/components/schemas/RunRecord' }
310
+ '401': { $ref: '#/components/responses/Unauthorized' }
311
+ '404': { $ref: '#/components/responses/NotFound' }
312
+
313
+ /v1/runs/{taskId}/events:
314
+ parameters:
315
+ - $ref: '#/components/parameters/PrincipalHeader'
316
+ - $ref: '#/components/parameters/TaskIdPath'
317
+ get:
318
+ tags: [runs]
319
+ operationId: runsEvents
320
+ x-status: live
321
+ summary: Resumable SSE event stream (durable log).
322
+ description: >
323
+ Replays durable events from after `Last-Event-ID` (each event's `id:` = the TiDB `task_event.seq`,
324
+ monotonic; events are NOT rolling-deleted, so the normal path never evicts), then tails to a terminal
325
+ state. Durable `text`/`reasoning` are turn-AGGREGATED (no per-token deltas β€” those are only on
326
+ `/v1/tasks/stream`). Heartbeat = 30s (`:` comment lines); the SDK declares a drop at β‰₯~45s and re-enters
327
+ the resume loop without consuming a retry. On a rare window eviction the server returns `416` with
328
+ `{ error, retainedFrom }` (NO machine code field β€” match the STATUS, not a body code); the SDK then
329
+ full-syncs via `GET /v1/tasks/:id/turns` and resumes events from `retainedFrom` (no event lost;
330
+ consumer is event-id idempotent).
331
+ parameters:
332
+ - $ref: '#/components/parameters/LastEventId'
333
+ responses:
334
+ '200':
335
+ description: SSE stream of AgentEvents (turn-aggregated; resumable).
336
+ content:
337
+ text/event-stream:
338
+ schema: { $ref: '#/components/schemas/AgentEvent' }
339
+ '401': { $ref: '#/components/responses/Unauthorized' }
340
+ '404': { $ref: '#/components/responses/NotFound' }
341
+ '416':
342
+ description: >
343
+ Resume point evicted past retention. Match the STATUS 416 (there is NO `code` field); read
344
+ `retainedFrom`, full-sync via `GET /v1/tasks/:id/turns`, then resume from `retainedFrom`.
345
+ content:
346
+ application/json:
347
+ schema: { $ref: '#/components/schemas/ResumeEvicted' }
348
+
349
+ /v1/runs/{taskId}/cancel:
350
+ parameters:
351
+ - $ref: '#/components/parameters/PrincipalHeader'
352
+ - $ref: '#/components/parameters/TaskIdPath'
353
+ post:
354
+ tags: [runs]
355
+ operationId: runsCancel
356
+ x-status: live # LIVE (service ced3d88). SDK verb runs.cancel(id) = M1.5.
357
+ summary: Cancel a durable async run (LIVE).
358
+ description: >
359
+ Cancels a durable async run (one created by `POST /v1/runs`). Auth = same as `runs.get`: owner-checked
360
+ (non-owner β†’ 404, no existence leak; missing principal β†’ 401). All responses are synchronous ACKs; actual
361
+ termination is async (same-replica immediate; cross-replica β‰ˆ30s via the heartbeat tick). A cancelled run
362
+ settles to `status` failed + `errorCode` "cancelled" (NO new status β€” distinguish via errorCode; the UI
363
+ shows "cancelled", not an error). A SUSPENDED run has no running stream to abort β†’ 409; cancel it by
364
+ DENYING its approval instead. LIVE (service ced3d88).
365
+ responses:
366
+ '202':
367
+ description: >
368
+ Ack. A running run returns status "cancelling"; an already-terminal run is an idempotent no-op
369
+ (returns the terminal status + a note).
370
+ content:
371
+ application/json:
372
+ schema: { $ref: '#/components/schemas/CancelAck' }
373
+ '401': { $ref: '#/components/responses/Unauthorized' }
374
+ '404': { $ref: '#/components/responses/NotFound' }
375
+ '409':
376
+ description: Run is suspended (HITL) β€” cancel by denying its approval (`/v1/approvals/:sessionId/decide`), not here.
377
+ content:
378
+ application/json:
379
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
380
+ '501': { $ref: '#/components/responses/NotImplemented' }
381
+
382
+ /v1/runs/{taskId}/steer:
383
+ parameters:
384
+ - $ref: '#/components/parameters/PrincipalHeader'
385
+ - $ref: '#/components/parameters/TaskIdPath'
386
+ post:
387
+ tags: [runs]
388
+ operationId: runsSteer
389
+ x-status: draft # design/80 D-A; core landing the slice (pendingSteer freeze/close-tag). Skeleton per the pinned wire contract.
390
+ summary: Steer a RUNNING durable run β€” inject mid-flight direction (supervision).
391
+ description: >
392
+ design/80 D-A. The core of *supervision* (vs mere approve/deny): inject direction into a running run.
393
+ AT-MOST-ONCE β€” steer is NOT idempotent (no retry). `trusted` is NOT a body field β€” the server derives it
394
+ from the authenticated operator (the BFF's operator role); a network steer defaults to untrusted. A
395
+ durable-`suspended` run β†’ 409 `errorCode` "steering.not_running" (steer is not queued; resolve its
396
+ approval instead). The pendingSteer freeze / close-tag guard is server-side; the client only sends text.
397
+ requestBody:
398
+ required: true
399
+ content:
400
+ application/json:
401
+ schema:
402
+ type: object
403
+ required: [text]
404
+ properties:
405
+ text: { type: string, description: 'The steering direction (untrusted DATA; the server fences it).' }
406
+ mode:
407
+ type: string
408
+ enum: [all, one-at-a-time]
409
+ description: >
410
+ Engine QueueMode (default one-at-a-time) β€” how many queued steers drain per TURN BOUNDARY:
411
+ "all" = inject ALL queued steers together at the next turn; "one-at-a-time" = inject ONE per
412
+ turn (FIFO), spread across turns. FORWARD-DRAFT: core's steer is harness-level today, not
413
+ per-call; the per-call mapping is pending D-A core.
414
+ responses:
415
+ '200': { description: Steer accepted and applied. }
416
+ '202': { description: Steer accepted (queued to the next turn-boundary drain). }
417
+ '401': { $ref: '#/components/responses/Unauthorized' }
418
+ '404': { $ref: '#/components/responses/NotFound' }
419
+ '409':
420
+ description: 'errorCode "steering.not_running" β€” the run is suspended/terminal, not running (steer is not queued).'
421
+ content:
422
+ application/json:
423
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
424
+ '422':
425
+ description: 'errorCode "steering.invalid_content" β€” the steer text carried a control-plane escape (e.g. </system-reminder>); core rejected it.'
426
+ content:
427
+ application/json:
428
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
429
+
430
+ /v1/runs/{taskId}/subagents/{target}/steer:
431
+ parameters:
432
+ - $ref: '#/components/parameters/PrincipalHeader'
433
+ - $ref: '#/components/parameters/TaskIdPath'
434
+ - name: target
435
+ in: path
436
+ required: true
437
+ schema: { type: string }
438
+ description: >
439
+ The delegating tool call's id (`parentToolCallId`, stamped on every forwarded subagent content
440
+ event when `forwardSubagentEvents` is on) or the subagent's agentName (an ambiguous name β†’ 409).
441
+ post:
442
+ tags: [runs]
443
+ operationId: runsSteerSubagent
444
+ x-status: live # core 1.219 C2 (onSubagentSpawn steer handles) / server 1.89 SubagentSteerRegistry; wire-verified 2026-07-04 (subagent-wire C1C2).
445
+ summary: Steer a RUNNING SUBAGENT of this run (drains at the subagent's next tool round).
446
+ description: >
447
+ core 1.219 C2 / server 1.89. The CC "enter the agent's view and type to it" semantic (AGe
448
+ pendingMessages): the message is queued and drains at the subagent's NEXT TOOL ROUND. BODY SHAPE
449
+ differs from the main-run steer β€” `{ content }`, NOT `{ text }`. AT-MOST-ONCE (no retry). A
450
+ settled/unknown subagent β†’ 409 `errorCode` "steering.not_running".
451
+ requestBody:
452
+ required: true
453
+ content:
454
+ application/json:
455
+ schema:
456
+ type: object
457
+ required: [content]
458
+ properties:
459
+ content: { type: string, description: 'The message for the subagent (untrusted DATA; the server fences it β€” same framing as the workflow-agent steer).' }
460
+ responses:
461
+ '200':
462
+ description: >
463
+ Queued/applied. Body `{ taskId, target, status, delivery, note }` β€” `note` carries the
464
+ CC-verbatim "Message queued for delivery to <name> at its next tool round." receipt copy.
465
+ '401': { $ref: '#/components/responses/Unauthorized' }
466
+ '404': { $ref: '#/components/responses/NotFound' }
467
+ '409':
468
+ description: 'errorCode "steering.not_running" β€” the run/subagent is settled or unknown (also: ambiguous agentName).'
469
+ content:
470
+ application/json:
471
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
472
+
473
+ /v1/sessions:
474
+ parameters:
475
+ - $ref: '#/components/parameters/PrincipalHeader'
476
+ get:
477
+ tags: [sessions]
478
+ operationId: sessionsList
479
+ x-status: live # E16, service 6cbf61e (durable session store).
480
+ summary: List the caller's sessions (keyset page).
481
+ description: >
482
+ E16 (shell-host sessionPicker) β€” owner-scoped keyset page of the caller's sessions; each row aggregates a
483
+ session's run ledger (last activity + most-recent objective preview + status). `owner` is honored only for
484
+ a fleet-wide credential (a principal caller is force-scoped to itself). 501 without a durable session store
485
+ (SESSION_BACKEND=tidb) β€” gate the picker off `capabilities()` instead of trial-by-501.
486
+ parameters:
487
+ - in: query
488
+ name: limit
489
+ schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
490
+ - in: query
491
+ name: cursor
492
+ schema: { type: string }
493
+ description: Opaque keyset cursor from a previous page's `nextCursor` (echo verbatim). Absent = first page.
494
+ - in: query
495
+ name: owner
496
+ schema: { type: string }
497
+ description: Fleet-wide credential only β€” scope the page to a tenant. Ignored for a principal caller.
498
+ responses:
499
+ '200':
500
+ description: Keyset page of session summaries.
501
+ content:
502
+ application/json:
503
+ schema: { $ref: '#/components/schemas/SessionListPage' }
504
+ '401': { $ref: '#/components/responses/Unauthorized' }
505
+ '501': { $ref: '#/components/responses/NotImplemented' }
506
+
507
+ /v1/sessions/{sessionId}:
508
+ parameters:
509
+ - $ref: '#/components/parameters/PrincipalHeader'
510
+ - in: path
511
+ name: sessionId
512
+ required: true
513
+ schema: { type: string }
514
+ get:
515
+ tags: [sessions]
516
+ operationId: sessionsGet
517
+ x-status: live # true shape from source.
518
+ summary: Session audit (current context + owner + compaction state).
519
+ description: >
520
+ Owner-checked read of a durable session (same semantics as runs: non-owner β†’ 404). `messages` is the
521
+ CURRENT model context (older history already folded into a summary message) β€” can be LARGE, the portal
522
+ should lazy-expand. Absent entirely on workers without the TiDB pool (capabilities.asyncRuns is the
523
+ practical same-prereq signal).
524
+ responses:
525
+ '200':
526
+ description: Session record.
527
+ content:
528
+ application/json:
529
+ schema: { $ref: '#/components/schemas/SessionRecord' }
530
+ '401': { $ref: '#/components/responses/Unauthorized' }
531
+ '404': { $ref: '#/components/responses/NotFound' }
532
+ delete:
533
+ tags: [sessions]
534
+ operationId: sessionsDelete
535
+ x-status: live # E21, service durable session store.
536
+ summary: Purge a session's history + run ledger (idempotent).
537
+ description: >
538
+ E21 (shell-host) β€” purge the session's conversation history + service-owned run-ledger/checkpoint/tool-result
539
+ rows. Owner-scoped (non-owner β†’ 404). Idempotent (`{deleted:false}` when nothing existed). πŸ”΄ 409 while a run
540
+ is LIVE on the session (`activeTaskId` in the body) β€” cancel it first so the session lock isn't orphaned.
541
+ 501 without a durable session store.
542
+ responses:
543
+ '200':
544
+ description: Delete result (idempotent).
545
+ content:
546
+ application/json:
547
+ schema:
548
+ type: object
549
+ required: [deleted]
550
+ properties:
551
+ deleted: { type: boolean }
552
+ '401': { $ref: '#/components/responses/Unauthorized' }
553
+ '404': { $ref: '#/components/responses/NotFound' }
554
+ '409': { $ref: '#/components/responses/Conflict' }
555
+ '501': { $ref: '#/components/responses/NotImplemented' }
556
+
557
+ /v1/sessions/{sessionId}/fork:
558
+ parameters:
559
+ - $ref: '#/components/parameters/PrincipalHeader'
560
+ - in: path
561
+ name: sessionId
562
+ required: true
563
+ schema: { type: string }
564
+ post:
565
+ tags: [sessions]
566
+ operationId: sessionsFork
567
+ x-status: live # E17, service durable session store.
568
+ summary: Branch a NEW session off this one's whole history.
569
+ description: >
570
+ E17 (shell-host) β€” branch a fresh session off the source's WHOLE history (a "branch this conversation" /
571
+ regenerate-from-here). Owner-scoped (non-owner β†’ 404; the fork is owned by the caller). 400 on a non-uuidv7
572
+ id. 501 without a durable session store.
573
+ responses:
574
+ '200':
575
+ description: The new branched session id.
576
+ content:
577
+ application/json:
578
+ schema:
579
+ type: object
580
+ required: [sessionId]
581
+ properties:
582
+ sessionId: { type: string }
583
+ '400': { $ref: '#/components/responses/BadRequest' }
584
+ '401': { $ref: '#/components/responses/Unauthorized' }
585
+ '404': { $ref: '#/components/responses/NotFound' }
586
+ '501': { $ref: '#/components/responses/NotImplemented' }
587
+
588
+ /v1/sessions/{sessionId}/policy:
589
+ parameters:
590
+ - $ref: '#/components/parameters/PrincipalHeader'
591
+ - in: path
592
+ name: sessionId
593
+ required: true
594
+ schema: { type: string }
595
+ get:
596
+ tags: [sessions]
597
+ operationId: sessionsGetPolicy
598
+ x-status: live # E6, service + core session-policy store.
599
+ summary: Read the per-session tool-permission rules (permissionMode).
600
+ description: >
601
+ E6 (shell-host permissionMode) β€” the operator-tightened per-session tool-permission rules core reads at
602
+ prepare-time (SUBTRACT-only). Owner-scoped (non-owner β†’ 404); ownerless session β†’ 409. Read-only snapshot
603
+ (enforcement is 100% core-side). 501 without a session-policy store.
604
+ responses:
605
+ '200':
606
+ description: The stored rules (incl. the OCC `rev`).
607
+ content:
608
+ application/json:
609
+ schema:
610
+ type: object
611
+ required: [rules]
612
+ properties:
613
+ rules: { $ref: '#/components/schemas/StoredSessionRules' }
614
+ '401': { $ref: '#/components/responses/Unauthorized' }
615
+ '404': { $ref: '#/components/responses/NotFound' }
616
+ '409': { $ref: '#/components/responses/Conflict' }
617
+ '501': { $ref: '#/components/responses/NotImplemented' }
618
+ put:
619
+ tags: [sessions]
620
+ operationId: sessionsPutPolicy
621
+ x-status: live
622
+ summary: Write the per-session tool-permission rules (tighten-only).
623
+ description: >
624
+ E6 β€” write the per-session rules. πŸ”΄ TIGHTEN-ONLY for a normal principal: a loosen (remove a deny / widen
625
+ an allowlist or allowDirs) β†’ 403 `loosen_forbidden` (operator-only). `expectedRev` = optimistic-concurrency
626
+ CAS; a stale rev β†’ 409 `conflict`. Each rule field must be `string[]` β†’ malformed body 400. ownerless
627
+ session β†’ 409; non-owner β†’ 404. 501 without a session-policy store.
628
+ requestBody:
629
+ required: true
630
+ content:
631
+ application/json:
632
+ schema:
633
+ type: object
634
+ required: [rules]
635
+ properties:
636
+ rules: { $ref: '#/components/schemas/SessionPermissionRules' }
637
+ expectedRev: { type: integer, minimum: 0, description: CAS β€” reject if the stored rev differs. }
638
+ responses:
639
+ '200':
640
+ description: The stored rules with the new `rev`.
641
+ content:
642
+ application/json:
643
+ schema:
644
+ type: object
645
+ required: [rules]
646
+ properties:
647
+ rules: { $ref: '#/components/schemas/StoredSessionRules' }
648
+ '400': { $ref: '#/components/responses/BadRequest' }
649
+ '401': { $ref: '#/components/responses/Unauthorized' }
650
+ '403':
651
+ description: 403 `loosen_forbidden` β€” a non-operator write tried to relax the rules (tighten-only).
652
+ content:
653
+ application/json:
654
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
655
+ '404': { $ref: '#/components/responses/NotFound' }
656
+ '409': { $ref: '#/components/responses/Conflict' }
657
+ '501': { $ref: '#/components/responses/NotImplemented' }
658
+
659
+ /v1/sessions/{sessionId}/mcp:
660
+ parameters:
661
+ - $ref: '#/components/parameters/PrincipalHeader'
662
+ - in: path
663
+ name: sessionId
664
+ required: true
665
+ schema: { type: string }
666
+ get:
667
+ tags: [sessions]
668
+ operationId: sessionsMcp
669
+ x-status: live # E9, service + core 1.124.
670
+ summary: Per-server MCP status (materialization-time projection).
671
+ description: >
672
+ E9 (shell-host mcpStatus) β€” per-server MCP status for the `/mcp` panel. πŸ”΄ MATERIALIZATION-TIME, not live
673
+ session health: core never persists MCP connections, so the service fresh-connects→lists→disposes the
674
+ worker's configured MCP servers on demand and projects their status (`asOf` = that moment; render "as of
675
+ <asOf>"). `degraded:true` (servers empty) β‡’ materialize timed out/failed (NOT "no MCP"). No MCP configured
676
+ β‡’ `servers:[]` without `degraded`.
677
+ responses:
678
+ '200':
679
+ description: The MCP status panel.
680
+ content:
681
+ application/json:
682
+ schema: { $ref: '#/components/schemas/McpStatusPanel' }
683
+ '401': { $ref: '#/components/responses/Unauthorized' }
684
+
685
+ /v1/sessions/{sessionId}/memory:
686
+ parameters:
687
+ - $ref: '#/components/parameters/PrincipalHeader'
688
+ - in: path
689
+ name: sessionId
690
+ required: true
691
+ schema: { type: string }
692
+ post:
693
+ tags: [sessions]
694
+ operationId: memoryAppend
695
+ x-status: live # MF-30, service @sema-agent/server 1.3.0 (server.ts:2880).
696
+ summary: Append a memory note (MF-30 β€” the /memory write verb).
697
+ description: >
698
+ πŸ”΄ Memory is PER-PRINCIPAL, not per-session β€” {sessionId} is only the shell's addressing context (the
699
+ /memory command runs inside some session); a note appended for a principal is visible from ALL that
700
+ principal's sessions. Note stored VERBATIM (≀8 KiB, user-authored = intentional, NOT redacted).
701
+ Empty/oversized β†’ 400. Always available when capabilities().memoryWrite.
702
+ requestBody:
703
+ required: true
704
+ content:
705
+ application/json:
706
+ schema:
707
+ type: object
708
+ required: [content]
709
+ properties: { content: { type: string } }
710
+ responses:
711
+ '200':
712
+ description: Append ack β€” unified { ok, id, scope } (service; handler always returns ok).
713
+ content:
714
+ application/json:
715
+ schema: { $ref: '#/components/schemas/MemoryWriteAck' }
716
+ '400': { description: "Empty / oversized / missing content." }
717
+ '401': { $ref: '#/components/responses/Unauthorized' }
718
+
719
+ /v1/sessions/{sessionId}/memory/{noteId}:
720
+ parameters:
721
+ - $ref: '#/components/parameters/PrincipalHeader'
722
+ - in: path
723
+ name: sessionId
724
+ required: true
725
+ schema: { type: string }
726
+ - in: path
727
+ name: noteId
728
+ required: true
729
+ schema: { type: string }
730
+ patch:
731
+ tags: [sessions]
732
+ operationId: memoryEdit
733
+ x-status: live # MF-30 (server.ts:2881).
734
+ summary: Edit one memory note by id (id unchanged).
735
+ description: >
736
+ πŸ”΄ 501 when the backend lacks id-addressable update (honest degrade, never a fake 200). Per-PRINCIPAL.
737
+ requestBody:
738
+ required: true
739
+ content:
740
+ application/json:
741
+ schema:
742
+ type: object
743
+ required: [content]
744
+ properties: { content: { type: string } }
745
+ responses:
746
+ '200':
747
+ description: Edited note ack.
748
+ content:
749
+ application/json:
750
+ schema: { $ref: '#/components/schemas/MemoryWriteAck' }
751
+ '401': { $ref: '#/components/responses/Unauthorized' }
752
+ '501': { description: "Backend lacks id-addressable update." }
753
+ delete:
754
+ tags: [sessions]
755
+ operationId: memoryForget
756
+ x-status: live # MF-30 (server.ts:2882).
757
+ summary: Forget one memory note by id.
758
+ description: >
759
+ πŸ”΄ 501 when the backend lacks id-addressable delete (honest degrade). Per-PRINCIPAL.
760
+ responses:
761
+ '200':
762
+ description: Forgotten note ack.
763
+ content:
764
+ application/json:
765
+ schema: { $ref: '#/components/schemas/MemoryWriteAck' }
766
+ '401': { $ref: '#/components/responses/Unauthorized' }
767
+ '501': { description: "Backend lacks id-addressable delete." }
768
+
769
+ # ── 2c session-sync (P1d) β€” the cloud as a SYNC PEER ──────────────────────────────────────────────────────────
770
+ # The local shell's local backend ⇄ this cloud service. PULL a session's whole state out, or PUSH the local
771
+ # peer's in. Gate the whole surface off `capabilities.sessionSync` (= durable backend + entry export + a
772
+ # file-snapshot store). Owner-gated like fork/delete (Β§9): a PULL 404s a non-owner; a PUSH is own-or-FRESH.
773
+ /v1/sessions/{sessionId}/sync/manifest:
774
+ parameters:
775
+ - $ref: '#/components/parameters/PrincipalHeader'
776
+ - in: path
777
+ name: sessionId
778
+ required: true
779
+ schema: { type: string }
780
+ get:
781
+ tags: [sessions]
782
+ operationId: sessionSyncManifest
783
+ x-status: live # 2c P1d, service @sema-agent/server@1.2.0 (LIVE pending worker 1.2.0 deploy).
784
+ summary: PULL the session manifest (thin cross-backend snapshot).
785
+ description: >
786
+ 2c session-sync PULL β€” the THIN snapshot: entry IDS (oldest-first, NOT payloads β€” those stream via
787
+ `/sync/entries`) + per-snapshot relPath→blobHash + policy + anchors + leaf. The local peer feeds `entryIds`
788
+ to the Β§7 classifier to decide fast-forward/fork BEFORE pulling the entry stream + only the blob hashes it
789
+ lacks. Owner-scoped (non-owner β†’ 404, no oracle); 400 on a non-uuidv7 id; 501 without a durable session store.
790
+ responses:
791
+ '200':
792
+ description: The manifest, wrapped as `{ manifest }`.
793
+ content:
794
+ application/json:
795
+ schema:
796
+ type: object
797
+ required: [manifest]
798
+ properties:
799
+ manifest: { $ref: '#/components/schemas/SessionManifest' }
800
+ '400': { $ref: '#/components/responses/BadRequest' }
801
+ '401': { $ref: '#/components/responses/Unauthorized' }
802
+ '404': { $ref: '#/components/responses/NotFound' }
803
+ '501': { $ref: '#/components/responses/NotImplemented' }
804
+
805
+ /v1/sessions/{sessionId}/sync/entries:
806
+ parameters:
807
+ - $ref: '#/components/parameters/PrincipalHeader'
808
+ - in: path
809
+ name: sessionId
810
+ required: true
811
+ schema: { type: string }
812
+ - in: query
813
+ name: afterSeq
814
+ required: false
815
+ schema: { type: integer, minimum: 0 }
816
+ description: >
817
+ Resume cursor = the backend-native durable `seq` (yield entries whose seq > afterSeq). SAME-BACKEND only.
818
+ A non-integer / negative value β†’ 400.
819
+ get:
820
+ tags: [sessions]
821
+ operationId: sessionSyncEntries
822
+ x-status: live
823
+ summary: PULL the entry payloads as an NDJSON stream.
824
+ description: >
825
+ 2c session-sync PULL β€” the entry PAYLOADS as `application/x-ndjson`: a `{"__sync":"begin",sessionId}` header
826
+ line, then ONE entry JSON per line, then a `{"__sync":"end",count}` TRAILER line. πŸ”΄ The status is committed
827
+ at 200 BEFORE the first line, so a mid-stream failure ends the body WITHOUT the trailer β€” the trailer's
828
+ ABSENCE (or a count mismatch) is the SOLE truncation signal (NO 5xx mid-stream). The SDK reader throws
829
+ `SyncTruncatedError` on a missing trailer / count mismatch. Owner-scoped (404); 400 on a bad afterSeq /
830
+ non-uuidv7 id; 501 without a durable entry-streaming store.
831
+ responses:
832
+ '200':
833
+ description: The NDJSON entry stream (begin / entryΓ— N / end-trailer).
834
+ content:
835
+ application/x-ndjson:
836
+ schema: { type: string }
837
+ '400': { $ref: '#/components/responses/BadRequest' }
838
+ '401': { $ref: '#/components/responses/Unauthorized' }
839
+ '404': { $ref: '#/components/responses/NotFound' }
840
+ '501': { $ref: '#/components/responses/NotImplemented' }
841
+
842
+ /v1/sessions/{sessionId}/sync/snapshots/{key}/blobs/{hash}:
843
+ parameters:
844
+ - $ref: '#/components/parameters/PrincipalHeader'
845
+ - in: path
846
+ name: sessionId
847
+ required: true
848
+ schema: { type: string }
849
+ - in: path
850
+ name: key
851
+ required: true
852
+ schema: { type: string }
853
+ description: The snapshot key (= a `SessionTreeEntry.id`).
854
+ - in: path
855
+ name: hash
856
+ required: true
857
+ schema: { type: string, pattern: '^[0-9a-f]{64}$' }
858
+ description: The content-address (SHA-256, 64 lowercase-hex).
859
+ get:
860
+ tags: [sessions]
861
+ operationId: sessionSyncGetBlob
862
+ x-status: live
863
+ summary: PULL one content-addressed blob's raw bytes (scope-checked).
864
+ description: >
865
+ 2c session-sync PULL β€” one content-addressed blob's RAW bytes (`application/octet-stream`). πŸ”΄ SCOPE BOUNDARY:
866
+ a content-addressed blob is shared across sessions, so the cloud serves a hash ONLY if THIS session's `:key`
867
+ manifest references it (a foreign / unreferenced hash β†’ 404 `blob not found`, never a blind read). 400 on a
868
+ non-64-hex hash or an undecodable key/hash. Owner-scoped (404).
869
+ responses:
870
+ '200':
871
+ description: The blob bytes.
872
+ content:
873
+ application/octet-stream:
874
+ schema: { type: string, format: binary }
875
+ '400': { $ref: '#/components/responses/BadRequest' }
876
+ '401': { $ref: '#/components/responses/Unauthorized' }
877
+ '404': { $ref: '#/components/responses/NotFound' }
878
+ '501': { $ref: '#/components/responses/NotImplemented' }
879
+
880
+ /v1/sessions/{sessionId}/sync/blobs/{hash}:
881
+ parameters:
882
+ - $ref: '#/components/parameters/PrincipalHeader'
883
+ - in: path
884
+ name: sessionId
885
+ required: true
886
+ schema: { type: string }
887
+ - in: path
888
+ name: hash
889
+ required: true
890
+ schema: { type: string, pattern: '^[0-9a-f]{64}$' }
891
+ put:
892
+ tags: [sessions]
893
+ operationId: sessionSyncPutBlob
894
+ x-status: live
895
+ summary: PUSH one content-addressed blob (own-or-fresh, integrity-checked).
896
+ description: >
897
+ 2c session-sync PUSH β€” store one content-addressed blob's RAW bytes ahead of import (`application/octet-stream`
898
+ body). The cloud verifies `sha256(body) === :hash` (the content-address integrity guard) BEFORE storing β€” a
899
+ mismatch β†’ 400 `blob hash mismatch`. own-or-FRESH gate. Per-blob cap 64 MiB β†’ 413 mid-stream beyond it. A
900
+ backend write failure (e.g. a MinIO non-2xx) β†’ 502 `blob store write failed`. 204 on success (no body). The
901
+ PUSH path is the BARE `blobs/:hash` (the PULL is scoped under `snapshots/:key`).
902
+ requestBody:
903
+ required: true
904
+ content:
905
+ application/octet-stream:
906
+ schema: { type: string, format: binary }
907
+ responses:
908
+ '204': { description: Stored (no body). }
909
+ '400': { $ref: '#/components/responses/BadRequest' }
910
+ '401': { $ref: '#/components/responses/Unauthorized' }
911
+ '404': { $ref: '#/components/responses/NotFound' }
912
+ '413': { $ref: '#/components/responses/PayloadTooLarge' }
913
+ '501': { $ref: '#/components/responses/NotImplemented' }
914
+ '502':
915
+ description: 502 `blob store write failed` β€” the backend (MinIO/…) rejected the write. Re-PUT (idempotent).
916
+ content:
917
+ application/json:
918
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
919
+
920
+ /v1/sessions/{sessionId}/sync/plan:
921
+ parameters:
922
+ - $ref: '#/components/parameters/PrincipalHeader'
923
+ - in: path
924
+ name: sessionId
925
+ required: true
926
+ schema: { type: string }
927
+ post:
928
+ tags: [sessions]
929
+ operationId: sessionSyncPlan
930
+ x-status: live
931
+ summary: DRY-RUN β€” classify what a PUSH would do (no write).
932
+ description: >
933
+ 2c session-sync PLAN β€” classify the Β§7 relation between the LOCAL peer's `entryIds` (oldest-first, the SOURCE)
934
+ and the cloud's current log (the DST), WITHOUT writing. Returns the `SyncRelation` (`fresh` for a fresh cloud
935
+ session). own-or-FRESH gate; 400 on a non-string-array body / non-uuidv7 id. Unlike import, plan NEVER 409s β€”
936
+ a fork/stale is REPORTED in the relation, not raised.
937
+ requestBody:
938
+ required: true
939
+ content:
940
+ application/json:
941
+ schema:
942
+ type: object
943
+ required: [entryIds]
944
+ properties:
945
+ entryIds: { type: array, items: { type: string } }
946
+ responses:
947
+ '200':
948
+ description: The classified relation.
949
+ content:
950
+ application/json:
951
+ schema: { $ref: '#/components/schemas/SyncRelation' }
952
+ '400': { $ref: '#/components/responses/BadRequest' }
953
+ '401': { $ref: '#/components/responses/Unauthorized' }
954
+ '404': { $ref: '#/components/responses/NotFound' }
955
+ '501': { $ref: '#/components/responses/NotImplemented' }
956
+
957
+ /v1/sessions/{sessionId}/sync/import:
958
+ parameters:
959
+ - $ref: '#/components/parameters/PrincipalHeader'
960
+ - in: path
961
+ name: sessionId
962
+ required: true
963
+ schema: { type: string }
964
+ post:
965
+ tags: [sessions]
966
+ operationId: sessionSyncImport
967
+ x-status: live
968
+ summary: PUSH Phase A β€” classify + stage (mint a stagingId).
969
+ description: >
970
+ 2c session-sync PUSH Phase A β€” a SMALL metadata body (`entryIds, snapshots, policy, anchors, resolution?`); the
971
+ entries stream in Phase B. The cloud classifies (Β§7), pre-checks blob presence (a missing referenced hash β†’
972
+ 422 `missing_blob` β€” PUT it first), guards the import lease + any active run (409), then mints a stagingId.
973
+ `identical` β†’ `{ relation:"identical" }` with NO stagingId (skip Phase B); `fresh`/`fast_forward` β†’
974
+ `{ stagingId, relation }`. πŸ”΄ a `fork`/`stale` without `resolution:"overwrite-dst"` β†’ 409
975
+ `conflict` carrying the `relation` (the SDK surfaces SyncConflictError).
976
+ requestBody:
977
+ required: true
978
+ content:
979
+ application/json:
980
+ schema:
981
+ type: object
982
+ required: [entryIds, snapshots, policy, anchors]
983
+ properties:
984
+ entryIds: { type: array, items: { type: string } }
985
+ snapshots: { type: array, items: { $ref: '#/components/schemas/SyncSnapshot' } }
986
+ policy: { type: array, items: { $ref: '#/components/schemas/SessionRulesRecord' } }
987
+ anchors: { type: array, items: { $ref: '#/components/schemas/SyncAnchor' } }
988
+ resolution: { type: string, enum: [overwrite-dst], description: Override a fork/stale loss-of-history conflict. }
989
+ responses:
990
+ '200':
991
+ description: Staged (or `identical` no-op).
992
+ content:
993
+ application/json:
994
+ schema: { $ref: '#/components/schemas/ImportStaged' }
995
+ '400': { $ref: '#/components/responses/BadRequest' }
996
+ '401': { $ref: '#/components/responses/Unauthorized' }
997
+ '404': { $ref: '#/components/responses/NotFound' }
998
+ '409':
999
+ description: >
1000
+ 409 `conflict` β€” a `fork`/`stale` PUSH without `resolution:"overwrite-dst"` (carries `relation`; the SDK
1001
+ surfaces SyncConflictError), OR an active run (`session_active`, `activeTaskId`) / an in-flight import
1002
+ (`import_in_flight`) on the session (ConflictError).
1003
+ content:
1004
+ application/json:
1005
+ schema: { $ref: '#/components/schemas/SyncConflict' }
1006
+ '422': { $ref: '#/components/responses/UnprocessableEntity' }
1007
+ '501': { $ref: '#/components/responses/NotImplemented' }
1008
+
1009
+ /v1/sessions/{sessionId}/sync/import/{stagingId}/entries:
1010
+ parameters:
1011
+ - $ref: '#/components/parameters/PrincipalHeader'
1012
+ - in: path
1013
+ name: sessionId
1014
+ required: true
1015
+ schema: { type: string }
1016
+ - in: path
1017
+ name: stagingId
1018
+ required: true
1019
+ schema: { type: string }
1020
+ description: The token minted by Phase A.
1021
+ post:
1022
+ tags: [sessions]
1023
+ operationId: sessionSyncImportEntries
1024
+ x-status: live
1025
+ summary: PUSH Phase B β€” stream the entry log (NDJSON) β†’ commit.
1026
+ description: >
1027
+ 2c session-sync PUSH Phase B β€” stream the entry log as an NDJSON REQUEST body into the staged shadow id; the
1028
+ cloud validates per-line (the streaming invariant gate) then `finish()` (exactly-one-root + leaf-resolvable) β†’
1029
+ commits (the atomic swap) β†’ replays snapshots/policy/anchors. Returns `{ relation }` (the AUTHORITATIVE in-txn
1030
+ re-classify). Re-POSTing the same stagingId is idempotent. Typed errors: a per-entry / structural violation β†’
1031
+ 422 `invalid_entries`; a line too large β†’ 413; a commit-time TOCTOU flip to fork/stale β†’ 409 `conflict`
1032
+ (SyncConflictError); a refused policy loosen β†’ 403 `loosen_forbidden`; a wrong/foreign stagingId β†’ 404.
1033
+ requestBody:
1034
+ required: true
1035
+ content:
1036
+ application/x-ndjson:
1037
+ schema: { type: string, description: One entry JSON per line. }
1038
+ responses:
1039
+ '200':
1040
+ description: Committed.
1041
+ content:
1042
+ application/json:
1043
+ schema: { $ref: '#/components/schemas/ImportCommitted' }
1044
+ '400': { $ref: '#/components/responses/BadRequest' }
1045
+ '401': { $ref: '#/components/responses/Unauthorized' }
1046
+ '403':
1047
+ description: 403 `loosen_forbidden` β€” a post-commit policy replay tried to relax the rules (tighten-only).
1048
+ content:
1049
+ application/json:
1050
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1051
+ '404': { $ref: '#/components/responses/NotFound' }
1052
+ '409':
1053
+ description: 409 `conflict` β€” a commit-time TOCTOU re-classify flipped to fork/stale (carries `relation`).
1054
+ content:
1055
+ application/json:
1056
+ schema: { $ref: '#/components/schemas/SyncConflict' }
1057
+ '413': { $ref: '#/components/responses/PayloadTooLarge' }
1058
+ '422': { $ref: '#/components/responses/UnprocessableEntity' }
1059
+ '501': { $ref: '#/components/responses/NotImplemented' }
1060
+
1061
+ /v1/approvals:
1062
+ parameters:
1063
+ - $ref: '#/components/parameters/PrincipalHeader'
1064
+ get:
1065
+ tags: [approvals]
1066
+ operationId: approvalsList
1067
+ x-status: live # true shape from source. NEVER returns a capability token.
1068
+ summary: List pending HITL checkpoints (operator-scoped).
1069
+ description: >
1070
+ Pending approvals/questions for runs that went `suspended` (an F4 policy gate or an AskUserQuestion;
1071
+ AskUserQuestion appears as the question gate's toolName). Envelope key = `pending` (NOT a bare array),
1072
+ ordered by created_at asc. Operator view sees all tenants; `scope` is enforced in the decide CAS.
1073
+ NEVER returns the capability token (secret discipline).
1074
+ responses:
1075
+ '200':
1076
+ description: Pending checkpoints.
1077
+ content:
1078
+ application/json:
1079
+ schema: { $ref: '#/components/schemas/PendingList' }
1080
+ '401': { $ref: '#/components/responses/Unauthorized' }
1081
+
1082
+ /v1/approvals/{sessionId}/decide:
1083
+ parameters:
1084
+ - $ref: '#/components/parameters/PrincipalHeader'
1085
+ - in: path
1086
+ name: sessionId
1087
+ required: true
1088
+ schema: { type: string }
1089
+ post:
1090
+ tags: [approvals]
1091
+ operationId: approvalsDecide
1092
+ x-status: live
1093
+ summary: Resolve a suspend β€” approve/deny or answer.
1094
+ description: >
1095
+ Resolves a `suspended` run: an F4 approve/deny, or an answer to an AskUserQuestion. The resumed run
1096
+ continues its durable stream (`/v1/runs/:id/events`). design/80 D-1: the decision SHOULD carry the
1097
+ action binding (checkpointToken/boundCallId/boundInputHash read off the PendingCheckpoint) β€” the server
1098
+ resolves THAT exact checkpoint, fail-closed on mismatch. (Path param stays `sessionId` until service
1099
+ finalizes a checkpointToken-keyed route; the binding travels in the body regardless.)
1100
+ requestBody:
1101
+ required: true
1102
+ content:
1103
+ application/json:
1104
+ schema: { $ref: '#/components/schemas/ApprovalDecision' }
1105
+ responses:
1106
+ '200':
1107
+ description: Decision accepted; the run resumes.
1108
+ content:
1109
+ application/json:
1110
+ schema: { type: object, additionalProperties: true }
1111
+ '401': { $ref: '#/components/responses/Unauthorized' }
1112
+ '404': { $ref: '#/components/responses/NotFound' }
1113
+ '409':
1114
+ description: >
1115
+ Either a session-CAS conflict (`ErrorResponse.activeTaskId`), OR a design/80 D-1
1116
+ `approval_binding_mismatch` (`errorCode`, `field`: boundCallId|boundInputHash) β€” your binding does
1117
+ NOT match the current pending action (TOCTOU/swap). The SDK splits these by `errorCode` into
1118
+ ConflictError vs ApprovalBindingMismatchError. binding_mismatch = SAFETY signal: refetch + re-present
1119
+ to the human, NEVER auto-retry.
1120
+ content:
1121
+ application/json:
1122
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1123
+ '410':
1124
+ description: >
1125
+ design/80 D-1 `approval_stale` (`errorCode`, `terminal`: resolved|expired_abort|expired_sla) β€” the
1126
+ checkpoint is already terminal (someone resolved it / abandonment-TTL GC / SLA resolve-deny). Client
1127
+ action: refetch the inbox; this pending is gone. SDK β†’ ApprovalStaleError.
1128
+ content:
1129
+ application/json:
1130
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1131
+
1132
+ /v1/approvals/{sessionId}/exemptions:
1133
+ parameters:
1134
+ - $ref: '#/components/parameters/PrincipalHeader'
1135
+ - in: path
1136
+ name: sessionId
1137
+ required: true
1138
+ schema: { type: string }
1139
+ get:
1140
+ tags: [approvals]
1141
+ operationId: approvalsListExemptions
1142
+ x-status: live # service exemptions ruling β€” server.ts exemptions surface; 501 without a store backend.
1143
+ summary: List the session's approval exemptions ("don't ask again").
1144
+ description: >
1145
+ The session's granted per-tool approval exemptions () β€” an APPROVAL MEMORY, not a policy loosen
1146
+ (deny/neverAuto always outrank a grant). Envelope key = `exemptions`. OWNER-GATED with a 404 (no
1147
+ existence oracle): a non-owner and an unknown session are indistinguishable. Explicit operators may
1148
+ inspect any session. `toolName` is in CANONICAL tool-name space. GET is list-only (a per-tool GET
1149
+ `/exemptions/:toolName` is 404).
1150
+ responses:
1151
+ '200':
1152
+ description: The session's exemptions.
1153
+ content:
1154
+ application/json:
1155
+ schema: { $ref: '#/components/schemas/ExemptionList' }
1156
+ '401': { $ref: '#/components/responses/Unauthorized' }
1157
+ '404': { $ref: '#/components/responses/NotFound' }
1158
+ '501':
1159
+ description: Worker has no exemption store backend (DB_BACKEND / LOCAL lane required).
1160
+ content:
1161
+ application/json:
1162
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1163
+
1164
+ /v1/approvals/{sessionId}/exemptions/{toolName}:
1165
+ parameters:
1166
+ - $ref: '#/components/parameters/PrincipalHeader'
1167
+ - in: path
1168
+ name: sessionId
1169
+ required: true
1170
+ schema: { type: string }
1171
+ - in: path
1172
+ name: toolName
1173
+ required: true
1174
+ schema: { type: string }
1175
+ description: Canonical tool name (echo `toolName` off the list row; the service canonicalizes).
1176
+ delete:
1177
+ tags: [approvals]
1178
+ operationId: approvalsRevokeExemption
1179
+ x-status: live # service exemptions ruling; 501 without a store backend.
1180
+ summary: Revoke one approval exemption (the tool asks again).
1181
+ description: >
1182
+ Revokes a single (sessionId, toolName) exemption. 200 `{revoked: true}` on success; 404 when
1183
+ there is nothing to revoke OR the caller is not the owner / the session is unknown (deliberately
1184
+ indistinguishable β€” no existence oracle). The SDK surfaces this as a boolean
1185
+ (`approvals.revokeExemption` β†’ false on 404), matching the store's idempotent revoke.
1186
+ responses:
1187
+ '200':
1188
+ description: Revoked.
1189
+ content:
1190
+ application/json:
1191
+ schema:
1192
+ type: object
1193
+ required: [revoked]
1194
+ properties:
1195
+ revoked: { const: true }
1196
+ '401': { $ref: '#/components/responses/Unauthorized' }
1197
+ '404': { $ref: '#/components/responses/NotFound' }
1198
+ '501':
1199
+ description: Worker has no exemption store backend.
1200
+ content:
1201
+ application/json:
1202
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1203
+
1204
+ /v1/assistant/inbox:
1205
+ parameters:
1206
+ - $ref: '#/components/parameters/PrincipalHeader'
1207
+ - in: query
1208
+ name: owner
1209
+ required: false
1210
+ schema: { type: string }
1211
+ description: >
1212
+ Operator-only β€” inspect ONE owner's inbox. An ordinary user OMITS it β†’ own scope. Non-owner
1213
+ non-operator β†’ 404 (owner gate, no existence oracle).
1214
+ get:
1215
+ tags: [assistant]
1216
+ operationId: inboxList
1217
+ x-status: live # ASSISTANT-WIRE-CONTRACT Β§2 (server.ts ~1388), LIVE on worker.
1218
+ summary: The single severity-sorted HITL inbox (per-user).
1219
+ description: >
1220
+ ASSISTANT-WIRE-CONTRACT Β§2 β€” a PER-USER view of pending checkpoints. Each row = a CheckpointSummary
1221
+ (Β§1) + an `objective` (enriched from task ctx; `null` if unavailable). ALREADY severity-sorted by
1222
+ core's `listPending` β€” the consumer MUST NOT re-sort, and must tolerate `undefined` severity
1223
+ (present only on human/irreversible_ask gates).
1224
+ responses:
1225
+ '200':
1226
+ description: Severity-sorted inbox rows.
1227
+ content:
1228
+ application/json:
1229
+ schema: { $ref: '#/components/schemas/InboxList' }
1230
+ '401': { $ref: '#/components/responses/Unauthorized' }
1231
+
1232
+ /v1/assistant/tasks:
1233
+ parameters:
1234
+ - $ref: '#/components/parameters/PrincipalHeader'
1235
+ get:
1236
+ tags: [assistant]
1237
+ operationId: assistantTasks
1238
+ x-status: live # ASSISTANT-WIRE-CONTRACT Β§3 (server.ts ~1410), LIVE on worker.
1239
+ summary: The N-task scheduler overview (per-user cross-task).
1240
+ description: >
1241
+ ASSISTANT-WIRE-CONTRACT Β§3 β€” a PER-USER cross-task view = run-store `listRuns(owner)` joined with the
1242
+ core scheduler seam `listByScope`. Triage-ordered server-side (needs-attention first, then severity
1243
+ DESC, then spend DESC) β€” DO NOT re-sort. Requires the TiDB run store, else `{ "tasks": [] }` (no 501).
1244
+ ⚠️ `createdAt`/`updatedAt` are ISO date-time strings here (unlike inbox/§1 epoch-ms).
1245
+ responses:
1246
+ '200':
1247
+ description: Triage-ordered task rows (empty `{tasks:[]}` when no TiDB run store).
1248
+ content:
1249
+ application/json:
1250
+ schema: { $ref: '#/components/schemas/AssistantTaskList' }
1251
+ '401': { $ref: '#/components/responses/Unauthorized' }
1252
+
1253
+ /v1/assistant/tasks/{taskId}/resume:
1254
+ parameters:
1255
+ - $ref: '#/components/parameters/PrincipalHeader'
1256
+ - $ref: '#/components/parameters/TaskIdPath'
1257
+ post:
1258
+ tags: [assistant]
1259
+ operationId: assistantResume
1260
+ x-status: live # ASSISTANT-WIRE-CONTRACT Β§4b (server.ts ~1490), LIVE on worker.
1261
+ summary: Resume a resource_limit-suspended task (CONTINUE).
1262
+ description: >
1263
+ ASSISTANT-WIRE-CONTRACT Β§4b β€” resumes a task durably SUSPENDED on a `resource_limit` gate (scheduler
1264
+ preempt OR a budget/turns/walltime slice). NO request body. πŸ”΄ GATE-KIND GUARD: a `human`/`policy_ask`
1265
+ gate β†’ 409 `gate_not_resumable` (a "continue" must NEVER bypass a human approval β€” use `/decide`).
1266
+ Owner-gated (NOT D-G); non-owner β†’ 404. SDK β†’ GateNotResumableError on the 409.
1267
+ responses:
1268
+ '200':
1269
+ description: Resumed; `{status}` (e.g. "completed").
1270
+ content:
1271
+ application/json:
1272
+ schema:
1273
+ type: object
1274
+ required: [status]
1275
+ additionalProperties: true
1276
+ properties:
1277
+ status: { type: string }
1278
+ '401': { $ref: '#/components/responses/Unauthorized' }
1279
+ '404': { $ref: '#/components/responses/NotFound' }
1280
+ '409':
1281
+ description: >
1282
+ `errorCode:"gate_not_resumable"` β€” the gate is `human`/`policy_ask`, NOT `resource_limit`. A
1283
+ "continue" must NEVER bypass a human approval β€” resolve via `/v1/approvals/:sessionId/decide`.
1284
+ SEMANTIC (wrong-gate), NOT a session-CAS conflict. SDK β†’ GateNotResumableError.
1285
+ content:
1286
+ application/json:
1287
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1288
+ '501': { $ref: '#/components/responses/NotImplemented' }
1289
+
1290
+ /v1/assistant/tasks/{taskId}/plan_review:
1291
+ parameters:
1292
+ - $ref: '#/components/parameters/PrincipalHeader'
1293
+ - $ref: '#/components/parameters/TaskIdPath'
1294
+ post:
1295
+ tags: [assistant]
1296
+ operationId: assistantPlanReview
1297
+ x-status: live # ASSISTANT-WIRE-CONTRACT Β§4c (server.ts ~1505), LIVE on worker.
1298
+ summary: Resolve a plan_review gate (3-state approve/edit/reject).
1299
+ description: >
1300
+ ASSISTANT-WIRE-CONTRACT Β§4c (design/80 D-B) β€” resolves a PRE-ACTION `plan_review` gate: the human
1301
+ reviews the PLAN (no bound tool action). A SIBLING of `/resume` (owner-gated via the run store), a
1302
+ DISTINCT wire from `/decide` and from the `needs_review` wire. `approve` β†’ resume with the original
1303
+ plan; `edit` β†’ resume with `editedPlan`; `reject` β†’ resume to handle the rejection. approve/edit/reject
1304
+ all run the model β†’ BILLABLE.
1305
+ requestBody:
1306
+ required: true
1307
+ content:
1308
+ application/json:
1309
+ schema: { $ref: '#/components/schemas/PlanReviewRequest' }
1310
+ responses:
1311
+ '200':
1312
+ description: Resolved; `{status}` (e.g. "completed").
1313
+ content:
1314
+ application/json:
1315
+ schema:
1316
+ type: object
1317
+ required: [status]
1318
+ additionalProperties: true
1319
+ properties:
1320
+ status: { type: string }
1321
+ '400':
1322
+ description: >
1323
+ Bad/missing `decision`; `edit` without a non-empty `editedPlan`; `editedPlan` present on
1324
+ approve/reject (FORBIDDEN); non-string `reason`.
1325
+ content:
1326
+ application/json:
1327
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1328
+ '401': { $ref: '#/components/responses/Unauthorized' }
1329
+ '404': { $ref: '#/components/responses/NotFound' }
1330
+ '409':
1331
+ description: >
1332
+ `errorCode:"gate_not_plan_review"` β€” a non-`plan_review` gate. A plan decision must NEVER resolve a
1333
+ tool approval / resource_limit suspend (the checkpoint is never consumed). SEMANTIC (wrong-gate),
1334
+ NOT a session-CAS conflict. SDK β†’ GateNotPlanReviewError.
1335
+ content:
1336
+ application/json:
1337
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1338
+ '501': { $ref: '#/components/responses/NotImplemented' }
1339
+
1340
+ /v1/assistant/tasks/{taskId}/preempt:
1341
+ parameters:
1342
+ - $ref: '#/components/parameters/PrincipalHeader'
1343
+ - $ref: '#/components/parameters/TaskIdPath'
1344
+ post:
1345
+ tags: [assistant]
1346
+ operationId: assistantPreempt
1347
+ x-status: live # ASSISTANT-WIRE-CONTRACT Β§4d (server.ts ~1434), LIVE on worker.
1348
+ summary: Gracefully preempt (YIELD) a task.
1349
+ description: >
1350
+ ASSISTANT-WIRE-CONTRACT Β§4d β€” durable cross-replica "yield this task" (the OPPOSITE of
1351
+ `/v1/runs/:id/cancel`'s kill). NO body. NOT billable. `202 {status:"preempting"|…}`; a terminal task β†’
1352
+ `202` no-op; an already-`suspended` task β†’ 409; `RESOURCE_SUSPEND` off β†’ 501. Owner-gated; non-owner β†’ 404.
1353
+ responses:
1354
+ '202':
1355
+ description: Preempting (or 202 no-op on a terminal task); `{status}`.
1356
+ content:
1357
+ application/json:
1358
+ schema:
1359
+ type: object
1360
+ required: [status]
1361
+ additionalProperties: true
1362
+ properties:
1363
+ status: { type: string }
1364
+ '401': { $ref: '#/components/responses/Unauthorized' }
1365
+ '404': { $ref: '#/components/responses/NotFound' }
1366
+ '409':
1367
+ description: The task is already `suspended` β€” there is nothing running to yield.
1368
+ content:
1369
+ application/json:
1370
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1371
+ '501': { $ref: '#/components/responses/NotImplemented' }
1372
+
1373
+ /v1/tasks/{taskId}/turns:
1374
+ parameters:
1375
+ - $ref: '#/components/parameters/PrincipalHeader'
1376
+ - $ref: '#/components/parameters/TaskIdPath'
1377
+ get:
1378
+ tags: [trace]
1379
+ operationId: traceTurns
1380
+ x-status: live # shape confirmed the pinned wire contract.
1381
+ summary: Full per-task turn trace (the workspace action timeline).
1382
+ description: >
1383
+ The full turn-by-turn trace that the workspace renders as the action timeline (tool cards, etc.).
1384
+ Requires a `SESSION_BACKEND=tidb` worker. Turn shape is draft (co-design with the artifacts shape, Q7).
1385
+ responses:
1386
+ '200':
1387
+ description: Turn trace β€” envelope `{ turns, nextCursor?, retainedFrom }` (key `turns`; item shape draft).
1388
+ content:
1389
+ application/json:
1390
+ schema: { $ref: '#/components/schemas/TurnList' }
1391
+ '401': { $ref: '#/components/responses/Unauthorized' }
1392
+ '404': { $ref: '#/components/responses/NotFound' }
1393
+
1394
+ /v1/tasks/source-summary:
1395
+ parameters:
1396
+ - $ref: '#/components/parameters/PrincipalHeader'
1397
+ get:
1398
+ tags: [trace]
1399
+ operationId: traceSourceSummary
1400
+ x-status: live # the pinned wire contract β€” per-door usage rollup (prove-before-scale data spine).
1401
+ summary: Per-source run counts (door-level usage).
1402
+ parameters:
1403
+ - in: query
1404
+ name: sinceSec
1405
+ required: false
1406
+ schema: { type: integer }
1407
+ responses:
1408
+ '200':
1409
+ description: Rollup rows.
1410
+ content:
1411
+ application/json:
1412
+ schema:
1413
+ type: object
1414
+ required: [rows]
1415
+ properties:
1416
+ rows:
1417
+ type: array
1418
+ items:
1419
+ type: object
1420
+ required: [source, status, count]
1421
+ properties:
1422
+ source: { type: string }
1423
+ status: { type: string }
1424
+ count: { type: integer }
1425
+ '401': { $ref: '#/components/responses/Unauthorized' }
1426
+
1427
+ /v1/tasks/{taskId}/artifacts:
1428
+ parameters:
1429
+ - $ref: '#/components/parameters/PrincipalHeader'
1430
+ - $ref: '#/components/parameters/TaskIdPath'
1431
+ get:
1432
+ tags: [trace]
1433
+ operationId: traceArtifacts
1434
+ x-status: live # producer shipped (service 2779155 co-signed).
1435
+ summary: Workspace artifacts for a run (projection of the persisted trace).
1436
+ description: >
1437
+ Deterministic projection from persisted tool events β€” works for ALL historical runs, never drifts from
1438
+ real execution. Job-level aggregation is client-side: `GET /v1/tasks?jobId=X` then fetch per-run
1439
+ artifacts (each carries taskId+jobId). Failed calls / unpaired starts emit nothing.
1440
+ responses:
1441
+ '200':
1442
+ description: Artifacts (possibly empty).
1443
+ content:
1444
+ application/json:
1445
+ schema: { $ref: '#/components/schemas/ArtifactList' }
1446
+ '401': { $ref: '#/components/responses/Unauthorized' }
1447
+ '404': { $ref: '#/components/responses/NotFound' }
1448
+ '501': { $ref: '#/components/responses/NotImplemented' }
1449
+
1450
+ /v1/tasks/{taskId}/stream:
1451
+ parameters:
1452
+ - $ref: '#/components/parameters/PrincipalHeader'
1453
+ - $ref: '#/components/parameters/TaskIdPath'
1454
+ get:
1455
+ tags: [trace]
1456
+ operationId: traceStream
1457
+ x-status: draft # trace relay (same source/semantics as runs.events); kept for the workspace live view.
1458
+ summary: Trace relay SSE (same source/semantics as runs.events).
1459
+ parameters:
1460
+ - $ref: '#/components/parameters/LastEventId'
1461
+ responses:
1462
+ '200':
1463
+ description: SSE stream of AgentEvents (resumable; same durable source as runs.events).
1464
+ content:
1465
+ text/event-stream:
1466
+ schema: { $ref: '#/components/schemas/AgentEvent' }
1467
+ '401': { $ref: '#/components/responses/Unauthorized' }
1468
+ '404': { $ref: '#/components/responses/NotFound' }
1469
+
1470
+ /v1/usage:
1471
+ parameters:
1472
+ - $ref: '#/components/parameters/PrincipalHeader'
1473
+ get:
1474
+ tags: [usage]
1475
+ operationId: usageGet
1476
+ x-status: live # service GET /v1/usage (shape audited from source). SDK usage.get().
1477
+ summary: Per-principal cost/quota snapshot (the CLI `/cost` panel's real data).
1478
+ description: >
1479
+ READ-ONLY. Worker computes this from the REQUEST principal (crypto-verified on a direct door β€” never the
1480
+ spoofable header); the client never sends a scope. When no cost-quota window is configured β†’
1481
+ `{ enabled:false, windowSec, maxTaskCostUsd?, maxTaskTokens? }` (only the single-task ceilings); the caller
1482
+ renders "no quota data" and NEVER blocks on it (enforcement is 100% server-side; this only displays).
1483
+ responses:
1484
+ '200':
1485
+ description: Usage/quota snapshot (enabled=false when no window is configured).
1486
+ content:
1487
+ application/json:
1488
+ schema: { $ref: '#/components/schemas/UsageInfo' }
1489
+ '401': { $ref: '#/components/responses/Unauthorized' }
1490
+
1491
+ /v1/policy:
1492
+ parameters:
1493
+ - $ref: '#/components/parameters/PrincipalHeader'
1494
+ get:
1495
+ tags: [policy]
1496
+ operationId: policyGet
1497
+ x-status: live # service GET /v1/policy (shape audited from source). SDK policy.get().
1498
+ summary: Effective-governance snapshot (the CLI `/permissions` panel's real data; inspect-only).
1499
+ description: >
1500
+ READ-ONLY snapshot of the worker's effective autonomy band + command policy + approval conditions + limits
1501
+ (worker-GLOBAL config, same for every principal β€” a principal is OPTIONAL, only enforced under
1502
+ REQUIRE_PRINCIPAL). The CLI renders "what can run automatically, what needs approval, what the ceilings
1503
+ are" from this. πŸ”΄ The client NEVER re-decides/admits commands from this β€” command admission + approval
1504
+ enforcement is 100% server-side (in resolveSpec); this is pure inspect. The CLI's "permission modes" are
1505
+ TIGHTEN-ONLY client cosmetics and cannot loosen server gating.
1506
+ responses:
1507
+ '200':
1508
+ description: Effective policy snapshot.
1509
+ content:
1510
+ application/json:
1511
+ schema: { $ref: '#/components/schemas/PolicyInfo' }
1512
+ '401': { $ref: '#/components/responses/Unauthorized' }
1513
+
1514
+ /v1/models:
1515
+ parameters:
1516
+ - $ref: '#/components/parameters/PrincipalHeader'
1517
+ get:
1518
+ tags: [models]
1519
+ operationId: modelsList
1520
+ x-status: live # service GET /v1/models (server.ts:859-882). SDK models.list().
1521
+ summary: Non-secret model catalog (the `@model` picker / autocomplete data source).
1522
+ description: >
1523
+ READ-ONLY catalog of `@mention`-pickable models. The user selects a model PER-TASK by typing `@<name>` in
1524
+ the objective body (the service allowlist-parses it; the client never re-decides). Envelope `{ models, default }`
1525
+ (`default` = the deployment default model id, display-only β€” the internal `default` alias is hidden).
1526
+ πŸ”΄ NON-SECRET: only name/id/provider/reasoning/vision + the E4/E7 capability fields β€” NEVER baseUrl/apiKey/
1527
+ headers (the service strips them). Fail-safe: on error, render an empty picker (never block).
1528
+ responses:
1529
+ '200':
1530
+ description: Model catalog (possibly empty).
1531
+ content:
1532
+ application/json:
1533
+ schema:
1534
+ type: object
1535
+ required: [models, default]
1536
+ properties:
1537
+ models:
1538
+ type: array
1539
+ items: { $ref: '#/components/schemas/ModelInfo' }
1540
+ default: { type: string, description: "deployment default model id (display-only)" }
1541
+ '401': { $ref: '#/components/responses/Unauthorized' }
1542
+
1543
+ /v1/elicitations/{id}/respond:
1544
+ parameters:
1545
+ - $ref: '#/components/parameters/PrincipalHeader'
1546
+ - name: id
1547
+ in: path
1548
+ required: true
1549
+ schema: { type: string }
1550
+ description: the elicitationId from the live `elicitation` SSE frame.
1551
+ post:
1552
+ tags: [elicitations]
1553
+ operationId: elicitationRespond
1554
+ x-status: gated # service POST /v1/elicitations/:id/respond β€” present only under MCP_ELICITATION_ENABLED (else 501).
1555
+ summary: Answer an inbound-MCP elicitation (design/99 Β§E23, LIVE-only HITL).
1556
+ description: >
1557
+ Resolve a parked inbound-MCP elicitation with the user's form answer; the value flows back to the MCP server
1558
+ on the still-open task connection. πŸ”΄ LIVE-ONLY + SAME-REPLICA (the parked promise is in-memory): a respond
1559
+ on another replica / after answer / after the TTL finds nothing β†’ 404. πŸ”΄ OWNER-GATED with a 404 (NO existence
1560
+ oracle): a non-owner AND an unknown id both get 404 β€” never an existence leak. A malformed body β†’ 400
1561
+ (existence-independent). Not a submit / not idempotent β†’ no client retry.
1562
+ requestBody:
1563
+ required: true
1564
+ content:
1565
+ application/json:
1566
+ schema: { $ref: '#/components/schemas/ElicitResponse' }
1567
+ responses:
1568
+ '200':
1569
+ description: Answer applied (the promise resolved).
1570
+ content:
1571
+ application/json:
1572
+ schema: { $ref: '#/components/schemas/ElicitRespondAck' }
1573
+ '400': { description: malformed body (action not accept/decline/cancel, or non-scalar content). }
1574
+ '401': { $ref: '#/components/responses/Unauthorized' }
1575
+ '404': { description: no pending elicitation for this id (non-owner, answered, expired, or wrong replica) β€” no existence oracle. }
1576
+ '501': { description: inbound MCP elicitation is not enabled on this worker (MCP_ELICITATION_ENABLED). }
1577
+
1578
+ /v1/questions/{id}/respond:
1579
+ parameters:
1580
+ - $ref: '#/components/parameters/PrincipalHeader'
1581
+ - name: id
1582
+ in: path
1583
+ required: true
1584
+ schema: { type: string }
1585
+ description: the questionId from the live `question` SSE frame.
1586
+ post:
1587
+ tags: [questions]
1588
+ operationId: questionRespond
1589
+ x-status: gated # service POST /v1/questions/:id/respond β€” present only under ASK_QUESTION_ENABLED (else 501).
1590
+ summary: Answer the agent's own AskUserQuestion (Β§4β‘£ tool-panorama, LIVE-only HITL).
1591
+ description: >
1592
+ Resolve a parked LIVE AskUserQuestion with the end user's selection; the value flows back to the model on the
1593
+ still-open task connection (the model's AskUserQuestion tool call returns it). The SIBLING of elicitationRespond
1594
+ (that is an inbound-MCP-server ask; this is the AGENT's OWN structured multiple-choice question to the user).
1595
+ πŸ”΄ LIVE-ONLY + SAME-REPLICA (the parked promise is in-memory): a respond on another replica / after answer /
1596
+ after the TTL (5 min) finds nothing β†’ 404. πŸ”΄ OWNER-GATED with a 404 (NO existence oracle): a non-owner AND an
1597
+ unknown id both get 404 β€” never an existence leak. A malformed body β†’ 400 (existence-independent). Not a submit /
1598
+ not idempotent (it resolves an in-flight promise exactly once) β†’ no client retry.
1599
+ requestBody:
1600
+ required: true
1601
+ content:
1602
+ application/json:
1603
+ schema: { $ref: '#/components/schemas/QuestionAnswer' }
1604
+ responses:
1605
+ '200':
1606
+ description: Answer applied (the parked question promise resolved).
1607
+ content:
1608
+ application/json:
1609
+ schema: { $ref: '#/components/schemas/QuestionRespondAck' }
1610
+ '400': { description: 'malformed body (answers not an array of {header, selected:string[], note?}).' }
1611
+ '401': { $ref: '#/components/responses/Unauthorized' }
1612
+ '404': { description: no pending question for this id (non-owner, answered, expired, or wrong replica) β€” no existence oracle. }
1613
+ '501': { description: AskUserQuestion is not enabled on this worker (ASK_QUESTION_ENABLED). }
1614
+
1615
+ /v1/workflows:
1616
+ parameters:
1617
+ - $ref: '#/components/parameters/PrincipalHeader'
1618
+ get:
1619
+ tags: [workflows]
1620
+ operationId: workflowsList
1621
+ x-status: gated # design/97/98; SELF_ORCHESTRATION_ENABLED (default OFF β†’ 501). Owner-scoped (scope = creating principal).
1622
+ summary: List S8 self-orchestration runs (owner-scoped summaries).
1623
+ description: >
1624
+ OWNER-SCOPED β€” returns ONLY the calling principal's own runs (scope = the run's creating principal,
1625
+ `default` when none). Envelope key = `workflows` (NOT a bare array). REPLICA-LOCAL: this lists the runs the
1626
+ run store on THIS replica knows (durable cross-replica history is a service follow-on). Gated off
1627
+ (SELF_ORCHESTRATION_ENABLED) β†’ 501; the caller fail-safes to empty.
1628
+ parameters:
1629
+ - in: query
1630
+ name: status
1631
+ required: false
1632
+ schema: { $ref: '#/components/schemas/WorkflowRunStatus' }
1633
+ description: Filter by lifecycle status.
1634
+ - in: query
1635
+ name: limit
1636
+ required: false
1637
+ schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
1638
+ description: Max rows (server clamps to 1..100; default 50).
1639
+ responses:
1640
+ '200':
1641
+ description: Owner's workflow-run summaries.
1642
+ content:
1643
+ application/json:
1644
+ schema:
1645
+ type: object
1646
+ required: [workflows]
1647
+ properties:
1648
+ workflows:
1649
+ type: array
1650
+ items: { $ref: '#/components/schemas/WorkflowRunSummary' }
1651
+ '401': { $ref: '#/components/responses/Unauthorized' }
1652
+ '501': { $ref: '#/components/responses/NotImplemented' }
1653
+
1654
+ /v1/workflows/{workflowRunId}:
1655
+ parameters:
1656
+ - $ref: '#/components/parameters/PrincipalHeader'
1657
+ - $ref: '#/components/parameters/WorkflowRunIdPath'
1658
+ get:
1659
+ tags: [workflows]
1660
+ operationId: workflowsGet
1661
+ x-status: gated # design/97/98; SELF_ORCHESTRATION_ENABLED (default OFF β†’ 501). Per-id owner gate (non-owner β†’ 404).
1662
+ summary: Full S8 self-orchestration run (phases/agents/stats).
1663
+ description: >
1664
+ Full run detail. OWNER-GATED via the persisted run's scope β€” a non-owner or unknown id β†’ 404 (NO existence
1665
+ oracle, parity with runs). Gated off β†’ 501.
1666
+ responses:
1667
+ '200':
1668
+ description: Full workflow run.
1669
+ content:
1670
+ application/json:
1671
+ schema: { $ref: '#/components/schemas/WorkflowRun' }
1672
+ '401': { $ref: '#/components/responses/Unauthorized' }
1673
+ '404': { $ref: '#/components/responses/NotFound' }
1674
+ '501': { $ref: '#/components/responses/NotImplemented' }
1675
+
1676
+ /v1/workflows/{workflowRunId}/stream:
1677
+ parameters:
1678
+ - $ref: '#/components/parameters/PrincipalHeader'
1679
+ - $ref: '#/components/parameters/WorkflowRunIdPath'
1680
+ get:
1681
+ tags: [workflows]
1682
+ operationId: workflowsStream
1683
+ x-status: gated # design/97/98; SELF_ORCHESTRATION_ENABLED (default OFF β†’ 501). In-process subscribeWorkflow; NOT resumable.
1684
+ summary: Live S8 run progress SSE (NON-resumable).
1685
+ description: >
1686
+ LIVE progress. FIRST frame is `event: meta` `{version, runId}`; each subsequent frame carries a core
1687
+ `WorkflowEvent` (its `type` becomes the SSE event name). πŸ”΄ NOT RESUMABLE β€” the source is REPLICA-LOCAL +
1688
+ in-process (no durable seq, no `Last-Event-ID`); a drop is a FULL restart (re-`GET /v1/workflows/:id` for
1689
+ the snapshot), not a resume. Owner-gated (non-owner β†’ 404). Gated off β†’ 501 (before any frame).
1690
+ responses:
1691
+ '200':
1692
+ description: SSE stream of workflow events (`meta` first, then core WorkflowEvents; non-resumable).
1693
+ content:
1694
+ text/event-stream:
1695
+ schema: { $ref: '#/components/schemas/WorkflowStreamEvent' }
1696
+ '401': { $ref: '#/components/responses/Unauthorized' }
1697
+ '404': { $ref: '#/components/responses/NotFound' }
1698
+ '501': { $ref: '#/components/responses/NotImplemented' }
1699
+
1700
+ /v1/fleet/stream:
1701
+ parameters:
1702
+ - $ref: '#/components/parameters/PrincipalHeader'
1703
+ get:
1704
+ tags: [fleet]
1705
+ operationId: fleetStream
1706
+ x-status: live # service 1bf7a5c β€” streamFleet (http/server.ts:4874) + fleet/fleet-bus.ts. Owner-gated; in-process bus; NOT resumable.
1707
+ summary: Live multi-row fleet view SSE (NON-resumable).
1708
+ description: >
1709
+ MF-Fleet (shell-host data contract). The LIVE fleet as an SSE PUSH stream: a SNAPSHOT on connect, then a
1710
+ per-row delta for every transition. Frame order: `event: meta` `{version, scoped}` β†’ `event: snapshot`
1711
+ `{tasks, workflows, ts}` β†’ then `event: task`/`event: workflow` UPSERTS (each `data: {row, ts}` is the
1712
+ POST-MERGE FULL row β€” replace by `row.id`, NOT a sparse patch) + `event: task_remove`/`event:
1713
+ workflow_remove` `{id, ts}` departures. Heartbeat `: hb` comment frames keep the connection alive.
1714
+ πŸ”΄ OWNER-SCOPED by the credential-derived principal (NOT a spoofable header) UNLESS fleet-wide (an operator
1715
+ / deployment trace token sees every tenant's rows). The row `scope` is WIRE-STRIPPED after the owner-gate β€”
1716
+ it never appears on a row; the `meta.scoped` flag tells the consumer which view this is (true = owner-scoped).
1717
+ πŸ”΄ NOT RESUMABLE β€” replica-local + in-process (no durable seq / Last-Event-ID); a drop is a FULL restart
1718
+ (re-connect for a fresh snapshot), not a resume. No fleet wiring (`deps.fleetBus` absent) β†’ 501 before any frame.
1719
+ responses:
1720
+ '200':
1721
+ description: SSE stream of fleet frames (`meta` first, then `snapshot`, then per-row upserts/removes; non-resumable).
1722
+ content:
1723
+ text/event-stream:
1724
+ schema: { $ref: '#/components/schemas/FleetFrame' }
1725
+ '401': { $ref: '#/components/responses/Unauthorized' }
1726
+ '501': { $ref: '#/components/responses/NotImplemented' }
1727
+
1728
+ /v1/leader:
1729
+ parameters:
1730
+ - $ref: '#/components/parameters/PrincipalHeader'
1731
+ post:
1732
+ tags: [leader]
1733
+ operationId: leaderCreate
1734
+ x-status: gated # v2 pipeline; LEADER_ENABLED (default OFF) + REMOTE_EXEC=e2b. NOT the default door-B path.
1735
+ summary: Start a v2 leader run (gated; E2B + git push).
1736
+ description: >
1737
+ πŸ”΄ The v2 leader pipeline (E2B workers + git push), NOT door B's brain. Gated server-side:
1738
+ `LEADER_ENABLED` (default OFF) + `REMOTE_EXEC=e2b`. Mode is a SERVER deploy flag, not a client choice:
1739
+ `LEADER_FANOUT_ENABLED=true` (default) lets the router fan out to N isolated workers (= the value-HOLD
1740
+ correctness bet); `=false` pins a single worker. Either way it runs the E2B+git pipeline. Door B's brain
1741
+ is the strong single agent via `/v1/tasks`+`/v1/runs`+`scenario`. When disabled β†’ 501.
1742
+ requestBody:
1743
+ required: true
1744
+ content:
1745
+ application/json:
1746
+ schema:
1747
+ type: object
1748
+ required: [objective]
1749
+ additionalProperties: true
1750
+ properties:
1751
+ objective: { type: string }
1752
+ sessionId: { type: string }
1753
+ jobId: { type: string, minLength: 1, maxLength: 64 }
1754
+ responses:
1755
+ '202':
1756
+ description: Leader run accepted.
1757
+ content:
1758
+ application/json:
1759
+ schema: { $ref: '#/components/schemas/LeaderReceipt' }
1760
+ '401': { $ref: '#/components/responses/Unauthorized' }
1761
+ '501': { $ref: '#/components/responses/NotImplemented' }
1762
+
1763
+ /v1/leader/{leaderRunId}:
1764
+ parameters:
1765
+ - $ref: '#/components/parameters/PrincipalHeader'
1766
+ - in: path
1767
+ name: leaderRunId
1768
+ required: true
1769
+ schema: { type: string }
1770
+ get:
1771
+ tags: [leader]
1772
+ operationId: leaderGet
1773
+ x-status: gated
1774
+ summary: Read leader run state.
1775
+ responses:
1776
+ '200':
1777
+ description: Leader run state.
1778
+ content:
1779
+ application/json:
1780
+ schema: { $ref: '#/components/schemas/LeaderRecord' }
1781
+ '401': { $ref: '#/components/responses/Unauthorized' }
1782
+ '404': { $ref: '#/components/responses/NotFound' }
1783
+
1784
+ /metrics/summary:
1785
+ get:
1786
+ tags: [metrics]
1787
+ operationId: metricsSummary
1788
+ x-status: draft # ops surface; uses a metricsToken (NOT a principal). Shape loose.
1789
+ summary: Worker health/cost summary (ops).
1790
+ description: Uses `metricsToken` (operator), not `x-agent-principal`. Shape is loose/ops-defined.
1791
+ security:
1792
+ - metricsToken: []
1793
+ responses:
1794
+ '200':
1795
+ description: Metrics summary (shape draft).
1796
+ content:
1797
+ application/json:
1798
+ schema: { type: object, additionalProperties: true }
1799
+ '401': { $ref: '#/components/responses/Unauthorized' }
1800
+
1801
+ # ─────────────────────────────────────────────────────────────────────────────
1802
+
1803
+ components:
1804
+
1805
+ securitySchemes:
1806
+ bearerAuth:
1807
+ type: http
1808
+ scheme: bearer
1809
+ description: >
1810
+ Worker `SERVICE_AUTH_TOKEN`. SERVER-SIDE ONLY (the SDK browser guard refuses to construct a
1811
+ token-bearing client in a browser; the portal's browser calls its own BFF). Missing β†’ 503; bad β†’ 401.
1812
+ metricsToken:
1813
+ type: http
1814
+ scheme: bearer
1815
+ description: Operator metrics token (ops only; distinct from the service auth token and from any principal).
1816
+
1817
+ parameters:
1818
+ PrincipalHeader:
1819
+ in: header
1820
+ name: x-agent-principal
1821
+ required: true
1822
+ schema: { $ref: '#/components/schemas/Principal' }
1823
+ description: >
1824
+ Authenticated end-user identity (REQUIRED; principal-first, CORE-PRINCIPLES Β§3). Opaque to the service
1825
+ (memory scope + session owner key; format NOT validated server-side). The SDK normalizes; anonymous must
1826
+ be explicit (`anon:<id>`). On the multi-tenant doors this is set PER REQUEST.
1827
+ IdempotencyKey:
1828
+ in: header
1829
+ name: idempotency-key
1830
+ required: false
1831
+ schema: { type: string }
1832
+ description: >
1833
+ Submit-class dedupe key (the SDK auto-generates a UUID β†’ retries are safe by default). In-process window
1834
+ = 15 min (per-instance). A submit is retried by the SDK ONLY when a key exists.
1835
+ LastEventId:
1836
+ in: header
1837
+ name: Last-Event-ID
1838
+ required: false
1839
+ schema: { type: string }
1840
+ description: Resume an SSE stream from after this event id (= the durable `task_event.seq`).
1841
+ TaskIdPath:
1842
+ in: path
1843
+ name: taskId
1844
+ required: true
1845
+ schema: { type: string }
1846
+ WorkflowRunIdPath:
1847
+ in: path
1848
+ name: workflowRunId
1849
+ required: true
1850
+ schema: { type: string }
1851
+
1852
+ responses:
1853
+ BadRequest:
1854
+ description: >
1855
+ Bad request β€” malformed input only (e.g. missing `objective`, or `jobId` present but empty or >64 chars,
1856
+ NOT silently truncated). NOTE: budget rejections are NOT 4xx β€” they are result-level
1857
+ (HTTP 200 + `TaskResult.errorCode`), see the TaskResult/RateLimited notes (pinned wire contract).
1858
+ content:
1859
+ application/json:
1860
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1861
+ Unauthorized:
1862
+ description: Fail-closed auth β€” 401 (bad token) / 503 (missing token). Mapped to AuthError.
1863
+ content:
1864
+ application/json:
1865
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1866
+ NotFound:
1867
+ description: >
1868
+ No such resource for this principal. Owner-scoped reads/cancel return 404 (not 403) for a non-owned or
1869
+ unknown id β€” existence is not leaked across principals.
1870
+ content:
1871
+ application/json:
1872
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1873
+ Conflict:
1874
+ description: >
1875
+ 409 β€” session already has an active run, OR an idempotency/session-CAS conflict. Body carries
1876
+ `activeTaskId` (the task holding the session lock). ⚠️ The SDK surfaces this as a TYPED ConflictError and
1877
+ does NOT silently resolve it to that run: `activeTaskId` may be the caller's own first attempt OR an
1878
+ unrelated concurrent run on the same session β€” 409 alone cannot tell (the pinned wire contract Q6). Only triggers when
1879
+ a `sessionId` is present.
1880
+ content:
1881
+ application/json:
1882
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1883
+ RateLimited:
1884
+ description: >
1885
+ 429 β€” per-principal CUMULATIVE quota exceeded (`quotaExceeded`). `Retry-After` (seconds) honored by the
1886
+ SDK on retry. This is the ONLY budget/limit signal at the HTTP layer; per-task budget gates are
1887
+ result-level (200 + errorCode), NOT here (pinned wire contract).
1888
+ headers:
1889
+ Retry-After:
1890
+ schema: { type: integer }
1891
+ description: Seconds to wait before retrying.
1892
+ content:
1893
+ application/json:
1894
+ schema: { $ref: '#/components/schemas/QuotaError' }
1895
+ NotImplemented:
1896
+ description: 501 β€” capability not available (e.g. no TiDB run store; leader gated off; `wake` unsupported).
1897
+ content:
1898
+ application/json:
1899
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1900
+ PayloadTooLarge:
1901
+ description: >
1902
+ 413 β€” the request body exceeded a server cap (2c session-sync: a blob PUT past the 64 MiB per-blob cap, or a
1903
+ Phase-B NDJSON line past the per-line cap). Rejected mid-stream (no unbounded buffer). Not retryable verbatim.
1904
+ content:
1905
+ application/json:
1906
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1907
+ UnprocessableEntity:
1908
+ description: >
1909
+ 422 β€” a semantically-invalid submission the server understood but cannot apply (2c session-sync: a referenced
1910
+ blob is missing at import Phase A `missing_blob`, or the staged entry log failed the tree invariants
1911
+ `invalid_entries`). Fix the offending data + retry; deterministic, not a transient.
1912
+ content:
1913
+ application/json:
1914
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1915
+
1916
+ schemas:
1917
+
1918
+ Principal:
1919
+ type: string
1920
+ description: >
1921
+ Opaque end-user identity. Convention (NOT enforced server-side): `user:` / `org:` / `ai:` / `anon:`.
1922
+ examples: ['user:42', 'org:acme', 'anon:7f3c']
1923
+
1924
+ Scenario:
1925
+ type: string
1926
+ description: >
1927
+ Primary routing axis (one image serves many; unknown β†’ "default"). Selects model/tools/skills/prompt.
1928
+ examples: ['default', 'oa', 'code-review', 'team']
1929
+
1930
+ RunStatus:
1931
+ type: string
1932
+ enum: [running, completed, failed, suspended, needs_review]
1933
+ description: >
1934
+ `suspended` = HITL (an F4 policy gate or AskUserQuestion is waiting on a human) β€” NOT an error.
1935
+ `needs_review` (design/80 D-0) = durable TERMINAL of a dry-run/shadow review (POST-prediction),
1936
+ distinct from the mid-run `suspended` pause. A user-cancelled run is `failed` with `errorCode`
1937
+ "cancelled" (cancel is LIVE; `cancelling` is a cancel-ack field, NOT a member of this enum).
1938
+
1939
+ TaskStats:
1940
+ type: object
1941
+ properties:
1942
+ turns: { type: integer }
1943
+ tokens: { type: integer }
1944
+ promptTokens: { type: integer }
1945
+ cachedTokens: { type: integer }
1946
+ outputTokens: { type: integer }
1947
+ costUsd: { type: number }
1948
+ cacheHitRate: { type: number }
1949
+ required: [turns, tokens]
1950
+
1951
+ TaskRequest:
1952
+ type: object
1953
+ description: >
1954
+ Common request body. `[additionalProperties]` mirrors the service passthrough β€” scenario-specific fields
1955
+ (code-review's `repo`/`council`/`debate`/`rounds`, etc.) ride here.
1956
+ required: [objective]
1957
+ additionalProperties: true
1958
+ properties:
1959
+ objective:
1960
+ type: string
1961
+ description: The task. Put per-request context here (keep `systemPrompt` stable/cacheable).
1962
+ scenario: { $ref: '#/components/schemas/Scenario' }
1963
+ sessionId:
1964
+ type: string
1965
+ description: Continue a conversation (same sessionId across turns). Omit on first turn β†’ service mints one.
1966
+ jobId:
1967
+ type: string
1968
+ minLength: 1
1969
+ maxLength: 64
1970
+ description: >
1971
+ Work-view correlation key (the anti-fragmentation substrate, DIRECTION Β§2). LIVE server-side
1972
+ (service 63b8696): persisted to `task_run.job_id`, groupable via `GET /v1/tasks?jobId=`.
1973
+ Set one jobId across a job's sub-runs so they group into one Task across BOTH doors. >64 chars or empty
1974
+ β†’ 400 (not silently truncated). The SDK auto-generates one if absent.
1975
+ systemPrompt:
1976
+ type: string
1977
+ maxLength: 16384
1978
+ description: >
1979
+ Business/UX system prompt the CLIENT owns β€” STABLE per integrator version (cacheable prefix).
1980
+ Server cap 16384 chars β†’ 400.
1981
+ skills:
1982
+ type: array
1983
+ maxItems: 10
1984
+ description: >
1985
+ Per-request user skills (LIVE, service 170c384) β€” progressive disclosure, same
1986
+ mechanism as scenario skills. Merge: scenario WINS on name collision (user's dropped, warn-logged).
1987
+ Caps server-enforced; violations 400 with the offending rule named.
1988
+ items: { $ref: '#/components/schemas/SkillSpec' }
1989
+ reasoningEffort:
1990
+ type: string
1991
+ enum: [off, minimal, low, medium, high, xhigh, max]
1992
+ description: >
1993
+ Per-turn reasoning strength (CC /effort parity). Mapped to TaskSpec.thinking; an UNKNOWN value
1994
+ is a 400 (fail-loud). Omit β‡’ engine default (the model/role default). The shell stamps the
1995
+ user's /effort pick here per turn.
1996
+ model:
1997
+ type: string
1998
+ description: >
1999
+ Per-turn MODEL (CC /model picker parity). Alias ('sonnet'/'opus') or full id. The service
2000
+ resolveTaskModel CATALOG-GATES it (body.model wins over @mention wins over the deployment default;
2001
+ out-of-roster falls back). Maps to core TaskSpec.model. Omit β‡’ deployment/role default.
2002
+ mcpServers:
2003
+ type: array
2004
+ maxItems: 32
2005
+ description: >
2006
+ Per-request MCP servers (CC .mcp.json parity). The trusted BFF/shell projects the local .mcp.json
2007
+ here so the engine mounts those servers' tools. HONORED only on a SINGLE-USER deployment
2008
+ (mcpInjectionHonored = requirePrincipal!==true); multi-tenant ignores it (fail-closed). Merge:
2009
+ deployment/center baseline wins on name collision (caller can ADD, never SHADOW).
2010
+ items: { $ref: '#/components/schemas/McpServerSpec' }
2011
+ resumeAt:
2012
+ type: string
2013
+ description: >
2014
+ CC Rewind parity (E18). Fork the conversation FROM this user-message SessionTreeEntry.id (non-destructive
2015
+ branch via setLeafId). With rewindFiles=true β‡’ "both" (also restore the working tree). Alone β‡’
2016
+ "conversation". HONORED only where the deployment wired a resume-anchor store + getLeafId (capabilities.resumeAt);
2017
+ unknown anchor β‡’ 4xx, capability-off β‡’ 501. Cannot combine with a durable resume.
2018
+ rewindFiles:
2019
+ type: boolean
2020
+ description: >
2021
+ CC Rewind parity (E18). On a NORMAL turn β‡’ CAPTURE a working-tree snapshot at turn end (per-turn, by leaf;
2022
+ required so later rewinds have something to restore). On a resumeAt turn β‡’ RESTORE the tree to resumeAt's
2023
+ snapshot ("both"). Needs a fileSnapshotStore wired (capabilities.rewindFiles).
2024
+ rewindFilesTo:
2025
+ type: string
2026
+ description: >
2027
+ CC Rewind "code"-only (core 1.166.0 TaskSpec.rewindFilesTo). Restore the working tree to this user-message
2028
+ entryId's snapshot WITHOUT forking the conversation (no setLeafId). Use instead of resumeAt+rewindFiles when
2029
+ the user wants only files reverted. Target = a user-message SessionTreeEntry.id.
2030
+ permissionMode:
2031
+ type: string
2032
+ enum: [default, plan, acceptEdits, bypassPermissions]
2033
+ description: >
2034
+ CC permission-mode INTENT. The frontend (TUI shell / web portal) carries the RAW mode; the SERVICE
2035
+ INTERPRETS it (axis-aware, tighten-only): `plan` β‡’ mount present_plan (core enablePlanMode) + read-only
2036
+ hands (handsReadOnly) = CC EnterPlanMode read-only research; `acceptEdits`/`bypassPermissions` are
2037
+ loosening, not honorable remotely (coerced to default engine-side, enforced client-side). Carrying the
2038
+ intent (not pre-interpreted core fields) = one interpretation across frontends + service-owned governance.
2039
+ images:
2040
+ type: array
2041
+ items:
2042
+ oneOf:
2043
+ - type: object
2044
+ required: [data, mimeType]
2045
+ properties:
2046
+ data: { type: string }
2047
+ mimeType: { type: string }
2048
+ - type: object
2049
+ required: [url]
2050
+ properties:
2051
+ url: { type: string }
2052
+ maxCostUsd:
2053
+ type: number
2054
+ description: Caller-requested cost ceiling; server caps to the operator max (may ask less, never more).
2055
+ maxTokens: { type: integer, description: Caller-requested token ceiling; server-capped. }
2056
+ verify:
2057
+ type: boolean
2058
+ description: Adversarial verify gate (impl β†’ independent read-only verify β†’ fix loop). Not on `/v1/tasks/stream`.
2059
+ verifyRounds: { type: integer }
2060
+ cascade:
2061
+ type: boolean
2062
+ description: Quality-gate cascade (cheap→strong). Mutually exclusive with `verify`. Not on the stream.
2063
+ suggestNextPrompts:
2064
+ description: >
2065
+ E12 (shell-host; service spec-fields.ts:9 / runs.ts:392, SHIPPED) β€” opt-in post-completion "what to ask
2066
+ next" suggestions. After a COMPLETED run, core runs an LLM pass and the service appends a `suggestions`
2067
+ event to the durable run-events tail. `true` = core defaults; the object form tunes count / generating
2068
+ role. πŸ”΄ Mutually exclusive with `verify`/`cascade` (those return a result, not a streamed run β†’ 400).
2069
+ Only meaningful on `/v1/runs` (the sync `/v1/tasks` has no events tail). UNTRUSTED model text (redacted)
2070
+ β€” UI-only, never re-fed to a model.
2071
+ oneOf:
2072
+ - type: boolean
2073
+ - type: object
2074
+ properties:
2075
+ count: { type: integer }
2076
+ role: { type: string, description: A model role name. }
2077
+
2078
+ TaskResult:
2079
+ type: object
2080
+ description: Synchronous task result (`POST /v1/tasks`).
2081
+ required: [taskId, sessionId, status, stats]
2082
+ properties:
2083
+ taskId: { type: string }
2084
+ sessionId: { type: string }
2085
+ status: { $ref: '#/components/schemas/RunStatus' }
2086
+ result: { type: string }
2087
+ errorCode:
2088
+ type: string
2089
+ description: >
2090
+ Result-level terminal code (NOT an HTTP error). Includes `budget.precall` / `budget.exceeded`
2091
+ (cost gate, non-retryable) β€” branch on this, not on a 4xx (pinned wire contract).
2092
+ errorMessage: { type: string }
2093
+ stats: { $ref: '#/components/schemas/TaskStats' }
2094
+ verification:
2095
+ type: object
2096
+ description: Present when run behind the verify gate.
2097
+ properties:
2098
+ verdict: { type: string, enum: [PASS, FAIL, PARTIAL, unverified] }
2099
+ rounds: { type: integer }
2100
+ findings: { type: array, items: { type: string } }
2101
+
2102
+ RunReceipt:
2103
+ type: object
2104
+ description: Async run receipt (`POST /v1/runs` β†’ 202). The capability token is NEVER on the wire.
2105
+ required: [taskId, sessionId, status]
2106
+ properties:
2107
+ taskId: { type: string }
2108
+ sessionId: { type: string }
2109
+ status: { $ref: '#/components/schemas/RunStatus' }
2110
+
2111
+ RunRecord:
2112
+ type: object
2113
+ description: >
2114
+ Run state (`GET /v1/runs/:id`) β€” live shape per the pinned wire contract Drift 2: `result` is the NESTED TaskResult
2115
+ object (output text in result.result, stats in result.stats; NO top-level stats), and the error text
2116
+ field is `error` (not errorMessage; errorCode is floated up from result.errorCode).
2117
+ required: [taskId, sessionId, status]
2118
+ properties:
2119
+ taskId: { type: string }
2120
+ sessionId: { type: string }
2121
+ status: { $ref: '#/components/schemas/RunStatus' }
2122
+ result: { $ref: '#/components/schemas/TaskResult' }
2123
+ errorCode:
2124
+ type: string
2125
+ description: Result-level terminal code (e.g. `budget.exceeded`, `cancelled`); NOT an HTTP error.
2126
+ error: { type: string }
2127
+ jobId:
2128
+ type: string
2129
+ description: Present when the run has a job correlation (omitted when null). LIVE.
2130
+ source:
2131
+ type: string
2132
+ description: >
2133
+ System attribution DERIVED FROM the authenticating credential (unforgeable; body injection ignored).
2134
+ e.g. oa | cc-mcp | portal. LIVE (service cf1be73).
2135
+
2136
+ CancelAck:
2137
+ type: object
2138
+ description: >
2139
+ Synchronous ack for `POST /v1/runs/:id/cancel`. `status` is "cancelling" (accepted) or a terminal status
2140
+ (idempotent no-op). NOTE: "cancelling" is NOT a RunStatus enum member β€” it only appears here.
2141
+ required: [taskId, status]
2142
+ properties:
2143
+ taskId: { type: string }
2144
+ status: { type: string }
2145
+ note: { type: string }
2146
+
2147
+ ResumeEvicted:
2148
+ type: object
2149
+ description: >
2150
+ 416 body for an evicted SSE resume point (`GET /v1/runs/:id/events`). There is NO machine `code` field β€”
2151
+ match the 416 STATUS and read `retainedFrom`, then full-sync via `/v1/tasks/:id/turns` (pinned SSE contract).
2152
+ required: [error, retainedFrom]
2153
+ properties:
2154
+ error: { type: string }
2155
+ retainedFrom: { type: integer, description: Lowest event seq still retained; resume events from here. }
2156
+
2157
+ QuotaError:
2158
+ type: object
2159
+ description: 429 per-principal cumulative-quota body (`quotaExceeded`).
2160
+ additionalProperties: true
2161
+ properties:
2162
+ error: { type: string }
2163
+ usedMicroUsd: { type: integer }
2164
+ limitMicroUsd: { type: integer }
2165
+ retryAfterSec: { type: integer }
2166
+
2167
+ TraceTurn:
2168
+ type: object
2169
+ description: >
2170
+ Trace turn (true shape, service-pinned). role currently only "assistant" (user/system/tool reserved β€”
2171
+ the user/objective opening turn is NOT projected today). blocks is an OPEN set; tool-result.output
2172
+ is currently ALWAYS absent (session-storage join not wired β€” known gap).
2173
+ required: [seq, ts, role, blocks]
2174
+ additionalProperties: true
2175
+ properties:
2176
+ seq: { type: integer }
2177
+ ts: { type: string }
2178
+ role: { type: string }
2179
+ blocks:
2180
+ type: array
2181
+ items:
2182
+ type: object
2183
+ required: [type]
2184
+ additionalProperties: true
2185
+ properties:
2186
+ type: { type: string, description: 'thinking | content | tool-call | tool-result | (open)' }
2187
+ text: { type: string }
2188
+ id: { type: string, description: tool-call β€” matches approvals toolCallId. }
2189
+ name: { type: string }
2190
+ input: { description: tool-call β€” redacted args. }
2191
+ callId: { type: string }
2192
+ isError: { type: boolean }
2193
+ tokens:
2194
+ type: object
2195
+ properties:
2196
+ in: { type: integer }
2197
+ out: { type: integer }
2198
+ cacheRead: { type: integer }
2199
+ cacheWrite: { type: integer }
2200
+
2201
+ TurnList:
2202
+ type: object
2203
+ description: >
2204
+ Turn trace envelope (`GET /v1/tasks/:id/turns`). Key is `turns` (resource-named). `retainedFrom` marks
2205
+ the lowest retained seq. Turn ITEM shape is draft (co-design with artifacts, Q7).
2206
+ required: [turns]
2207
+ properties:
2208
+ turns:
2209
+ type: array
2210
+ items: { $ref: '#/components/schemas/TraceTurn' }
2211
+ nextCursor: { type: string }
2212
+ retainedFrom: { type: integer }
2213
+
2214
+ TaskList:
2215
+ type: object
2216
+ description: Paginated run summaries (`GET /v1/tasks`). Envelope key is `tasks` (resource-named, NOT `items`).
2217
+ required: [tasks]
2218
+ properties:
2219
+ tasks:
2220
+ type: array
2221
+ items: { $ref: '#/components/schemas/TaskSummary' }
2222
+ nextCursor: { type: string, description: Keyset cursor for the next page (absent at end). }
2223
+
2224
+ TaskSummary:
2225
+ type: object
2226
+ description: >
2227
+ Run list item (the live `runSummary` cheap summary β€” the pinned wire contract Drift 1). NOTE the wire names:
2228
+ `id` (not taskId), `startedAt` (not createdAt), `costMicroUsd` (micro-USD Γ—1e6, not costUsd); no sessionId.
2229
+ required: [id, status, startedAt, lastStep]
2230
+ additionalProperties: true
2231
+ properties:
2232
+ id: { type: string }
2233
+ status: { $ref: '#/components/schemas/RunStatus' }
2234
+ scenario: { type: string }
2235
+ jobId: { type: string }
2236
+ source: { type: string, description: 'Credential-derived system attribution (service-pinned).' }
2237
+ objectivePreview: { type: string, maxLength: 120, description: 'Objective, secret-redacted then truncated (service-pinned; older rows absent).' }
2238
+ owner: { type: string, description: 'Run owner principal (service-pinned; operator view).' }
2239
+ startedAt: { type: string, format: date-time }
2240
+ endedAt: { type: string, format: date-time }
2241
+ turns: { type: integer }
2242
+ tokens: { type: integer }
2243
+ costMicroUsd: { type: integer }
2244
+ lastStep:
2245
+ type: object
2246
+ required: [type]
2247
+ properties:
2248
+ type: { type: string, enum: [thinking, tool-call, responding, idle] }
2249
+ label: { type: string }
2250
+
2251
+ Artifact:
2252
+ type: object
2253
+ description: >
2254
+ Workspace artifact β€” a deterministic PROJECTION of persisted tool_start/tool_end pairs (no new write
2255
+ path; args already redacted at persist time). Co-signed the pinned wire contract. kind is an OPEN set β€” render
2256
+ unknown kinds as a generic row, never crash. v1 emits "file" (successful write_file/edit_file, deduped
2257
+ by path) and "git-push" (successful bash containing git push); "diff" is reserved, not emitted.
2258
+ required: [id, taskId, kind, ref, createdAt]
2259
+ additionalProperties: true
2260
+ properties:
2261
+ id: { type: string, description: '"<taskId>:<n>" β€” stable within the task (UI key).' }
2262
+ taskId: { type: string }
2263
+ jobId: { type: string, description: Present when the run has a job (job-level aggregation key). }
2264
+ kind: { type: string, description: 'Open set; v1: "file" | "git-push" ("diff" reserved).' }
2265
+ ref: { type: string, description: 'file: workspace-relative path; git-push: refspec tail or command.' }
2266
+ summary: { type: string }
2267
+ createdAt: { type: string, format: date-time, description: ts of the last successful contributing call. }
2268
+ additions:
2269
+ type: integer
2270
+ description: file kind only β€” hunk-level line stats from logged args (NOT a git diff). the pinned wire contract.
2271
+ deletions: { type: integer }
2272
+
2273
+ ArtifactList:
2274
+ type: object
2275
+ required: [artifacts]
2276
+ properties:
2277
+ artifacts:
2278
+ type: array
2279
+ items: { $ref: '#/components/schemas/Artifact' }
2280
+
2281
+ LeaderReceipt:
2282
+ type: object
2283
+ required: [leaderRunId, status]
2284
+ properties:
2285
+ leaderRunId: { type: string }
2286
+ status: { type: string, enum: [running, completed, failed] }
2287
+
2288
+ LeaderRecord:
2289
+ type: object
2290
+ required: [id, status]
2291
+ properties:
2292
+ id: { type: string }
2293
+ status: { type: string, enum: [running, completed, failed] }
2294
+ result: {}
2295
+ error: { type: string }
2296
+
2297
+ ApprovalDecision:
2298
+ type: object
2299
+ description: HITL decision (`POST /v1/approvals/:sessionId/decide`).
2300
+ required: [decision]
2301
+ properties:
2302
+ decision: { type: string, enum: [approve, deny] }
2303
+ answer:
2304
+ description: >
2305
+ AskUserQuestion answer β€” TRUE WIRE SHAPE (worker-validated, 400 otherwise; found in prod
2306
+ integration, service-pinned): {answers:[{header, selected:string[], note?}]} where header matches
2307
+ the question's header. Omit for a plain F4 approve/deny.
2308
+ type: object
2309
+ properties:
2310
+ answers:
2311
+ type: array
2312
+ items:
2313
+ type: object
2314
+ required: [header, selected]
2315
+ properties:
2316
+ header: { type: string }
2317
+ selected: { type: array, items: { type: string } }
2318
+ note: { type: string }
2319
+ reason: { type: string }
2320
+ # design/80 D-1 β€” action binding (TOCTOU guard). Read the three off the PendingCheckpoint the human saw
2321
+ # and echo VERBATIM; server resolves THAT exact checkpoint, fail-closed on mismatch (approval_binding_mismatch).
2322
+ # Omit only against a pre-D-1 worker. boundInputHash is OPAQUE β€” never hashed/recomputed client-side.
2323
+ checkpointToken: { type: string, description: 'Single-use token of the exact checkpoint the human saw.' }
2324
+ boundCallId: { type: string, description: '= PendingCheckpoint.boundCallId (the bound pendingAction.toolCallId).' }
2325
+ boundInputHash: { type: string, description: 'Echo PendingCheckpoint.boundInputHash byte-for-byte; server-minted opaque, NEVER recompute.' }
2326
+ updatedInput:
2327
+ description: >
2328
+ Approve-with-edit (design/37 last-wins): operator's rewritten tool args, applied AFTER the binding
2329
+ check. `boundInputHash` STILL binds the ORIGINAL input the human saw β€” never hash updatedInput.
2330
+
2331
+ Capabilities:
2332
+ type: object
2333
+ description: >
2334
+ Worker capability map (`GET /v1/capabilities`). Open set β€” ignore unknown keys.
2335
+ Booleans share deps with the route gates (can't say yes and then 501).
2336
+ required: [version]
2337
+ additionalProperties: true
2338
+ properties:
2339
+ asyncRuns: { type: boolean }
2340
+ artifacts: { type: boolean }
2341
+ approvals: { type: boolean }
2342
+ leader: { type: boolean }
2343
+ version: { type: string }
2344
+ scenarios:
2345
+ type: array
2346
+ items: { type: string }
2347
+ pricingConfigured:
2348
+ type: boolean
2349
+ description: false β‡’ stats.costMicroUsd=0 may mean "no MODEL_COST_* configured", not "free".
2350
+ memory:
2351
+ type: boolean
2352
+ description: Memory transparency endpoints (GET/DELETE /v1/memory) available. the pinned wire contract.
2353
+ memoryWrite:
2354
+ type: boolean
2355
+ description: MF-30 β€” memory WRITE (append/edit/forget) available (deps.memory.append; server.ts:828).
2356
+ sessionSync:
2357
+ type: boolean
2358
+ description: >
2359
+ 2c session-sync (P1d) β€” the `/v1/sessions/{sessionId}/sync/*` peer routes resolve (durable backend +
2360
+ entry-export seam + a file-snapshot store wired; server.ts:850). Gate the whole sync surface off this.
2361
+
2362
+ SkillSpec:
2363
+ type: object
2364
+ description: A per-request skill (passed as an object; core-native TaskSpec.skills shape).
2365
+ required: [name, description, content]
2366
+ properties:
2367
+ name: { type: string, maxLength: 64 }
2368
+ description: { type: string, maxLength: 1024 }
2369
+ content: { type: string, maxLength: 32768 }
2370
+
2371
+ McpServerSpec:
2372
+ type: object
2373
+ description: >
2374
+ A per-request MCP server (CC .mcp.json parity) β€” core-native McpServerSpec shape. stdio = a local
2375
+ command the worker spawns (single-user only); http = a remote streamable-HTTP server.
2376
+ required: [name, transport]
2377
+ properties:
2378
+ name: { type: string }
2379
+ transport:
2380
+ oneOf:
2381
+ - type: object
2382
+ required: [kind, command]
2383
+ properties:
2384
+ kind: { type: string, enum: [stdio] }
2385
+ command: { type: string }
2386
+ args: { type: array, items: { type: string } }
2387
+ env: { type: object, additionalProperties: { type: string } }
2388
+ - type: object
2389
+ required: [kind, url]
2390
+ properties:
2391
+ kind: { type: string, enum: [http] }
2392
+ url: { type: string }
2393
+ headers: { type: object, additionalProperties: { type: string } }
2394
+ principalHeader: { type: string }
2395
+ allowTools: { type: array, items: { type: string } }
2396
+ elicitation: { type: boolean }
2397
+ toolAxes:
2398
+ type: object
2399
+ additionalProperties:
2400
+ type: object
2401
+ properties:
2402
+ effect: { type: string, enum: [read, write] }
2403
+ egress: { type: boolean }
2404
+ irreversibility: { type: string, enum: [always, never] }
2405
+
2406
+ MemoryRecord:
2407
+ type: object
2408
+ description: The caller's own <user_memory> (owner-scoped by construction). content null = empty.
2409
+ required: [scope, content]
2410
+ properties:
2411
+ scope: { type: string }
2412
+ content: { type: ['string', 'null'] }
2413
+
2414
+ MemoryWriteAck:
2415
+ type: object
2416
+ description: MF-30 write ack β€” unified { ok, id, scope } for append/edit/forget (service; the handler
2417
+ always returns ok β€” the old src:2880 summary comment that dropped it was a doc-bug). id = the note (unchanged on edit).
2418
+ required: [ok, id, scope]
2419
+ properties:
2420
+ ok: { type: boolean }
2421
+ id: { type: string }
2422
+ scope: { type: string }
2423
+
2424
+ ModelInfo:
2425
+ type: object
2426
+ description: >
2427
+ A non-secret `@model` catalog entry (GET /v1/models; service server.ts:860-880). `name` = the `@handle` the
2428
+ user types in the objective body to pick this model per-task. πŸ”΄ NON-SECRET β€” name/capabilities only, never
2429
+ baseUrl/apiKey/headers (the service strips them). Open set (additive future fields).
2430
+ required: [name]
2431
+ additionalProperties: true
2432
+ properties:
2433
+ name: { type: string, description: 'the @mention handle (user-pickable model name).' }
2434
+ id: { type: string, description: 'underlying model id (display, e.g. deepseek-v4-pro).' }
2435
+ provider: { type: string }
2436
+ reasoning: { type: boolean, description: 'a reasoning-tier model (picker can badge it).' }
2437
+ vision: { type: boolean, description: 'accepts image input.' }
2438
+ contextWindow: { type: integer, description: 'E4 (SHIPPED) β€” context-window token DENOMINATOR for the %-used gauge; omitted when unknown.' }
2439
+ maxOutputTokens: { type: integer, description: 'E4 (SHIPPED) β€” single-call max output tokens; omitted when unknown.' }
2440
+ supportedEffortLevels:
2441
+ type: array
2442
+ items: { type: string }
2443
+ description: 'E7 (SHIPPED) β€” the /effort picker default set (minimal/low/medium/high); present ONLY for a reasoning model.'
2444
+
2445
+ ElicitResponse:
2446
+ type: object
2447
+ description: >
2448
+ The shell's answer to an inbound-MCP elicitation (= core McpElicitResponse). On `accept`, `content` is a flat
2449
+ record of scalars / string[] β€” an OPAQUE passthrough core NEVER validates against requestedSchema; the service
2450
+ validates only this OUTER shape. A non-accept carries no content.
2451
+ required: [action]
2452
+ properties:
2453
+ action: { type: string, enum: [accept, decline, cancel] }
2454
+ content:
2455
+ type: object
2456
+ description: 'accept only β€” submitted form values (flat scalars).'
2457
+ additionalProperties:
2458
+ oneOf:
2459
+ - { type: string }
2460
+ - { type: number }
2461
+ - { type: boolean }
2462
+ - { type: array, items: { type: string } }
2463
+
2464
+ ElicitRespondAck:
2465
+ type: object
2466
+ description: The 200 ack from a successful elicitation respond (service elicitation.ts:267).
2467
+ required: [elicitationId, delivery, action]
2468
+ properties:
2469
+ elicitationId: { type: string }
2470
+ delivery: { const: applied }
2471
+ action: { type: string, enum: [accept, decline, cancel] }
2472
+
2473
+ QuestionAnswer:
2474
+ type: object
2475
+ description: >
2476
+ The shell's answer to a live `question` frame (= core QuestionAnswer, ask-question.ts:69). The service validates
2477
+ only this OUTER shape (`answers[]` of `{header, selected:string[], note?}`); core owns the semantic fence
2478
+ (`selected βŠ† the offered options`, `note` wrapped in an untrusted-DATA fence).
2479
+ required: [answers]
2480
+ properties:
2481
+ answers:
2482
+ type: array
2483
+ items: { $ref: '#/components/schemas/QuestionAnswerItem' }
2484
+
2485
+ QuestionAnswerItem:
2486
+ type: object
2487
+ description: >
2488
+ One question's answer, keyed by the question's `header`. `selected` echoes the EXACT offered option label(s)
2489
+ (core rejects any not in the offered options); single-select β†’ a one-element array, multiSelect β†’ 0..n labels.
2490
+ required: [header, selected]
2491
+ properties:
2492
+ header: { type: string }
2493
+ selected: { type: array, items: { type: string } }
2494
+ note: { type: string, description: 'optional free-text / off-list ("Other") input; core untrusted-fences it.' }
2495
+
2496
+ QuestionRespondAck:
2497
+ type: object
2498
+ description: The 200 ack from a successful question respond (service question.ts:226).
2499
+ required: [questionId, delivery]
2500
+ properties:
2501
+ questionId: { type: string }
2502
+ delivery: { const: applied }
2503
+
2504
+ UsageInfo:
2505
+ type: object
2506
+ description: >
2507
+ Per-principal cost/quota snapshot (GET /v1/usage; shape audited from source). Computed from
2508
+ the REQUEST principal. When no quota window is configured β†’ enabled=false + ONLY windowSec/maxTask*
2509
+ (the amount/limit fields are ABSENT) β€” render must tolerate the gap. Open set (additive future fields).
2510
+ required: [enabled]
2511
+ additionalProperties: true
2512
+ properties:
2513
+ enabled: { type: boolean, description: 'A cost-quota WINDOW is configured (false β‡’ single-task ceilings only).' }
2514
+ windowSec: { type: integer, description: 'Rolling quota window seconds (e.g. 86400 = daily).' }
2515
+ usedMicroUsd: { type: integer, description: 'Spent in the current window (micro-USD = 1e-6 USD).' }
2516
+ usedUsd: { type: number, description: 'Spent in the current window (USD; convenience projection).' }
2517
+ limitMicroUsd: { type: integer, description: 'Window quota ceiling (micro-USD).' }
2518
+ limitUsd: { type: number, description: 'Window quota ceiling (USD).' }
2519
+ remainingMicroUsd: { type: integer, description: 'Window remaining (limit - used, floored at 0).' }
2520
+ overLimit: { type: boolean, description: 'Window quota exceeded (new tasks blocked until retryAfterSec).' }
2521
+ retryAfterSec: { type: integer, description: 'Suggested wait when overLimit.' }
2522
+ maxTaskCostUsd: { type: number, description: 'Single-task cost hard ceiling (USD; independent of the window, always present).' }
2523
+ maxTaskTokens: { type: integer, description: 'Single-task token hard ceiling (independent of the window).' }
2524
+
2525
+ PolicyInfo:
2526
+ type: object
2527
+ description: >
2528
+ Effective-governance snapshot (GET /v1/policy; shape audited from source). Worker-GLOBAL
2529
+ config (same for every principal). πŸ”΄ INSPECT-ONLY β€” the client never re-decides/admits from this (command
2530
+ admission + approval enforcement is 100% server-side). Open set; nested shapes permissive (evolution-safe).
2531
+ additionalProperties: true
2532
+ properties:
2533
+ autonomy: { type: ['string', 'null'], description: 'Effective autonomy band (open string; null = unset/default).' }
2534
+ commandPolicy:
2535
+ type: array
2536
+ description: 'Command β†’ decision table (internal shape permissive; read command/decision).'
2537
+ items:
2538
+ type: object
2539
+ additionalProperties: true
2540
+ properties:
2541
+ command: { type: string }
2542
+ decision: { type: string }
2543
+ approvalRequire:
2544
+ type: array
2545
+ description: 'Conditions that force human approval (open structure; passed through, not parsed).'
2546
+ items: {}
2547
+ limits:
2548
+ type: object
2549
+ description: 'Effective ceilings (single-task + principal-level + quota window). Tolerate missing fields.'
2550
+ additionalProperties: true
2551
+ properties:
2552
+ maxTaskCostUsd: { type: number }
2553
+ maxTaskTokens: { type: integer }
2554
+ maxPrincipalCostUsd: { type: number }
2555
+ costQuotaWindowSec: { type: integer }
2556
+
2557
+ WorkflowRunStatus:
2558
+ type: string
2559
+ description: >
2560
+ S8 self-orchestration run lifecycle (core WorkflowRunStatus, workflow-run-store.ts). Open set (oneOf NOT
2561
+ exhaustive β€” render an unknown future state generically).
2562
+ examples: [running, completed, failed]
2563
+
2564
+ WorkflowRunSummary:
2565
+ type: object
2566
+ description: >
2567
+ GET /v1/workflows list row = core summarizeWorkflowRun projection (owner-scoped; scope = the creating
2568
+ principal). All times are epoch MS. Open set.
2569
+ required: [id, scope, status, phaseCount, agentCount, tokens, startedAt, createdAt]
2570
+ additionalProperties: true
2571
+ properties:
2572
+ id: { type: string, description: 'Run id (= WorkflowRun.id).' }
2573
+ scope: { type: string, description: 'Tenant/group scope (= the creating principal).' }
2574
+ status: { $ref: '#/components/schemas/WorkflowRunStatus' }
2575
+ phaseCount: { type: integer, description: 'Phases recorded.' }
2576
+ agentCount: { type: integer, description: 'Agent-runs recorded.' }
2577
+ tokens: { type: integer, description: 'Total tokens = own + nested (folded in the summary; a triage sort key).' }
2578
+ startedAt: { type: integer, description: 'Start epoch ms.' }
2579
+ endedAt: { type: integer, description: 'End epoch ms (absent while running).' }
2580
+ createdAt: { type: integer, description: 'Record-creation epoch ms (listByScope sort key).' }
2581
+
2582
+ WorkflowRun:
2583
+ type: object
2584
+ description: >
2585
+ GET /v1/workflows/:id full run (core WorkflowRun). Non-owner β†’ 404 (no existence oracle).
2586
+ πŸ”΄ phases/agents/stats nested shapes may evolve β†’ permissive (read length/known fields, tolerate the rest).
2587
+ required: [id, scope, status, phases, agents, stats, startedAt, createdAt]
2588
+ additionalProperties: true
2589
+ properties:
2590
+ id: { type: string }
2591
+ scope: { type: string }
2592
+ status: { $ref: '#/components/schemas/WorkflowRunStatus' }
2593
+ phases: { type: array, description: 'Phase records (permissive).', items: {} }
2594
+ agents: { type: array, description: 'Agent-run records (permissive).', items: {} }
2595
+ stats:
2596
+ type: object
2597
+ description: 'Token stats β€” own at stats.tokens, nested at stats.nested.tokens (R-5 own/nested split).'
2598
+ additionalProperties: true
2599
+ properties:
2600
+ tokens: { type: integer }
2601
+ nested:
2602
+ type: object
2603
+ additionalProperties: true
2604
+ properties:
2605
+ tokens: { type: integer }
2606
+ startedAt: { type: integer }
2607
+ endedAt: { type: integer }
2608
+ createdAt: { type: integer }
2609
+
2610
+ WorkflowStreamEvent:
2611
+ type: object
2612
+ description: >
2613
+ One frame of GET /v1/workflows/:id/stream (streamWorkflowRun). FIRST frame is `event:meta`
2614
+ `{version, runId}`; subsequent frames carry a core WorkflowEvent (its `type` is the SSE event name). NOT
2615
+ resumable (replica-local, in-process; no id/Last-Event-ID). `data` is permissive (core-internal, evolves).
2616
+ NOTE: on the wire this is SSE framing (event name + JSON data), not a JSON body β€” schema describes one
2617
+ decoded frame.
2618
+ required: [event, data]
2619
+ additionalProperties: true
2620
+ properties:
2621
+ event: { type: string, description: 'SSE event name = "meta" for the opener, else the core WorkflowEvent type (open set).' }
2622
+ data: { type: object, additionalProperties: true, description: 'The frame JSON data: payload (core WorkflowEvent shape; permissive).' }
2623
+
2624
+ # ── MF-Fleet (shell-host data contract) β€” GET /v1/fleet/stream frame vocabulary (service fleet/fleet-bus.ts) ──
2625
+ FleetTaskStatus:
2626
+ type: string
2627
+ description: >
2628
+ The fleet vocabulary the shell renders (service FleetTaskStatus, fleet-bus.ts:27-36). The service maps its
2629
+ run/workflow status onto this NEUTRAL set: `awaiting approval` = a needs-review/plan-approval park,
2630
+ `waiting` = a durable suspend. CLOSED on the wire; render OPEN (branch known, fall back generically).
2631
+ enum: [queued, running, waiting, stopping, 'awaiting approval', idle, completed, failed, killed]
2632
+ FleetTaskRow:
2633
+ type: object
2634
+ description: >
2635
+ One active task row (service FleetTaskRow fleet-bus.ts:40-54) β€” MINUS the wire-STRIPPED `scope` (a
2636
+ tenant-isolation field the service removes AFTER the owner-gate; never rendered, never on this wire). A
2637
+ `task` frame carries the POST-MERGE FULL row β†’ replace by `id`, NOT a sparse patch. A subagent child row
2638
+ sets `parentId` (E2 parentToolCallId) + an id of `"<runId> <taskId>"`.
2639
+ required: [id, name, status]
2640
+ additionalProperties: true
2641
+ properties:
2642
+ id: { type: string, description: 'Stable identity (durable runId/taskId; subagent child = "<runId> <taskId>"). Update/remove by this.' }
2643
+ name: { type: string }
2644
+ description: { type: string }
2645
+ parentId: { type: string, description: 'Subagent nesting (E2 parentToolCallId) β€” top-level when absent.' }
2646
+ status: { $ref: '#/components/schemas/FleetTaskStatus' }
2647
+ elapsedMs: { type: integer }
2648
+ tokens: { type: integer }
2649
+ tokenDir: { type: string, enum: [up, down], description: 'Token-meter direction for the row animation.' }
2650
+ queuedCount: { type: integer }
2651
+ awaitingPlanApproval: { type: boolean }
2652
+ FleetWorkflowRow:
2653
+ type: object
2654
+ description: >
2655
+ One workflow row (service FleetWorkflowRow fleet-bus.ts:57-69) β€” MINUS the wire-stripped `scope`. `status`
2656
+ is the NEUTRAL workflow run status (running|completed|failed), open on read.
2657
+ required: [id, name, status]
2658
+ additionalProperties: true
2659
+ properties:
2660
+ id: { type: string }
2661
+ name: { type: string }
2662
+ description: { type: string }
2663
+ status: { type: string, description: 'Neutral workflow run status (running|completed|failed; open).' }
2664
+ doneCount: { type: integer }
2665
+ totalCount: { type: integer }
2666
+ failedCount: { type: integer }
2667
+ elapsedMs: { type: integer }
2668
+ tokens: { type: integer }
2669
+ FleetFrame:
2670
+ description: >
2671
+ One decoded frame off GET /v1/fleet/stream (service streamFleet emit set, http/server.ts:4877-4908). On the
2672
+ wire this is SSE framing (the `type` here = the SSE `event` name; the object = the decoded `data:` JSON).
2673
+ Emit order: `meta` (FIRST) β†’ `snapshot` (connect-time full set) β†’ per-row `task`/`workflow` upserts +
2674
+ `task_remove`/`workflow_remove` departures. The SDK PRODUCES these frames; the consumer maintains the
2675
+ active-set state (update-in-place by `row.id`, remove by `id`). Heartbeat `: hb` comments are not frames.
2676
+ OPEN set β€” branch on `type`, ignore an unknown future frame.
2677
+ oneOf:
2678
+ - $ref: '#/components/schemas/FleetFrame_meta'
2679
+ - $ref: '#/components/schemas/FleetFrame_snapshot'
2680
+ - $ref: '#/components/schemas/FleetFrame_task'
2681
+ - $ref: '#/components/schemas/FleetFrame_task_remove'
2682
+ - $ref: '#/components/schemas/FleetFrame_workflow'
2683
+ - $ref: '#/components/schemas/FleetFrame_workflow_remove'
2684
+ discriminator:
2685
+ propertyName: type
2686
+ mapping:
2687
+ meta: '#/components/schemas/FleetFrame_meta'
2688
+ snapshot: '#/components/schemas/FleetFrame_snapshot'
2689
+ task: '#/components/schemas/FleetFrame_task'
2690
+ task_remove: '#/components/schemas/FleetFrame_task_remove'
2691
+ workflow: '#/components/schemas/FleetFrame_workflow'
2692
+ workflow_remove: '#/components/schemas/FleetFrame_workflow_remove'
2693
+ FleetFrame_meta:
2694
+ type: object
2695
+ description: 'FIRST frame β€” `{version, scoped}`. `scoped:true` β‡’ owner-scoped view; `false` β‡’ fleet-wide (operator/trace token).'
2696
+ required: [type, version, scoped]
2697
+ properties:
2698
+ type: { const: meta }
2699
+ version: { type: integer }
2700
+ scoped: { type: boolean }
2701
+ FleetFrame_snapshot:
2702
+ type: object
2703
+ description: 'Connect-time FULL state (the visible, scope-stripped rows). Seed the active-set Map from this.'
2704
+ required: [type, tasks, workflows, ts]
2705
+ properties:
2706
+ type: { const: snapshot }
2707
+ tasks: { type: array, items: { $ref: '#/components/schemas/FleetTaskRow' } }
2708
+ workflows: { type: array, items: { $ref: '#/components/schemas/FleetWorkflowRow' } }
2709
+ ts: { type: integer }
2710
+ FleetFrame_task:
2711
+ type: object
2712
+ description: 'A task-row UPSERT β€” `row` is the POST-MERGE FULL row β†’ replace the Map entry by `row.id`.'
2713
+ required: [type, row, ts]
2714
+ properties:
2715
+ type: { const: task }
2716
+ row: { $ref: '#/components/schemas/FleetTaskRow' }
2717
+ ts: { type: integer }
2718
+ FleetFrame_task_remove:
2719
+ type: object
2720
+ description: 'A task row left the active set (terminal + swept) β†’ delete `id` from the Map.'
2721
+ required: [type, id, ts]
2722
+ properties:
2723
+ type: { const: task_remove }
2724
+ id: { type: string }
2725
+ ts: { type: integer }
2726
+ FleetFrame_workflow:
2727
+ type: object
2728
+ description: 'A workflow-row UPSERT β€” `row` is the POST-MERGE FULL row β†’ replace by `row.id`.'
2729
+ required: [type, row, ts]
2730
+ properties:
2731
+ type: { const: workflow }
2732
+ row: { $ref: '#/components/schemas/FleetWorkflowRow' }
2733
+ ts: { type: integer }
2734
+ FleetFrame_workflow_remove:
2735
+ type: object
2736
+ description: 'A workflow row left the active set β†’ delete `id`.'
2737
+ required: [type, id, ts]
2738
+ properties:
2739
+ type: { const: workflow_remove }
2740
+ id: { type: string }
2741
+ ts: { type: integer }
2742
+
2743
+ TraceStreamEvent:
2744
+ type: object
2745
+ description: >
2746
+ One frame of GET /v1/tasks/:id/stream β€” the workspace LIVE timeline (streamTaskTrace + mapTraceEvent). A
2747
+ DISTINCT vocabulary from AgentEvent (the durable log is BLOCK-grained, each frame is a delta to append).
2748
+ Resumable (each `id:` = the durable task_event.seq; on 416 the server returns `{retainedFrom}` and the SDK
2749
+ resumes from there after a turns full-sync). Known events: meta / block-thinking-delta / block-content-delta
2750
+ / tool-call / tool-result / turn / done / error (+ heartbeat keep-alives the SDK swallows). Open set.
2751
+ NOTE: SSE framing on the wire (event name + JSON data + optional id), not a JSON body.
2752
+ required: [event, data]
2753
+ additionalProperties: true
2754
+ properties:
2755
+ event: { type: string, description: 'SSE event name (open set; dispatch on this).' }
2756
+ id: { type: string, description: 'The durable seq (id: line) when present β€” feed back as Last-Event-ID to resume. meta/heartbeat carry none.' }
2757
+ data: { type: object, additionalProperties: true, description: 'The frame JSON data: payload (shape varies by event; permissive).' }
2758
+
2759
+ PendingList:
2760
+ type: object
2761
+ description: Envelope for pending HITL checkpoints (key `pending`, NOT a bare array β€” the pinned wire contract).
2762
+ required: [pending]
2763
+ properties:
2764
+ pending:
2765
+ type: array
2766
+ items: { $ref: '#/components/schemas/PendingCheckpoint' }
2767
+
2768
+ ExemptionList:
2769
+ type: object
2770
+ description: Envelope for a session's approval exemptions (; key `exemptions`, NOT a bare array).
2771
+ required: [exemptions]
2772
+ properties:
2773
+ exemptions:
2774
+ type: array
2775
+ items: { $ref: '#/components/schemas/ApprovalExemption' }
2776
+
2777
+ ApprovalExemption:
2778
+ type: object
2779
+ description: >
2780
+ One granted "don't ask again" exemption (service ApprovalExemptionRow). An APPROVAL
2781
+ MEMORY only β€” deny/neverAuto always outrank it; it never loosens the session-policy layer.
2782
+ required: [toolName, grantedBy, createdAt]
2783
+ properties:
2784
+ toolName: { type: string, description: Canonical tool name the exemption covers. }
2785
+ grantedBy:
2786
+ type: ['string', 'null']
2787
+ description: Principal that granted it; null = ownerless/anonymous grant.
2788
+ createdAt: { type: number, description: Epoch MS of the FIRST grant (re-grant refreshes grantedBy only). }
2789
+
2790
+ PendingCheckpoint:
2791
+ type: object
2792
+ description: >
2793
+ A pending HITL checkpoint (true shape). NEVER includes a capability token.
2794
+ ⚠️ `createdAt`/`deadline` are EPOCH MILLISECONDS (numbers), not ISO strings.
2795
+ πŸ”΄ ASSISTANT-WIRE-CONTRACT Β§1/Β§7 (service main 6cd0164 / core 1.110.0): this MIRRORS the core
2796
+ `CheckpointSummary` β€” `token`/`gateKind`/`severity?`/`spentMicroUsd?` are Β§1 fields (the inbox endpoint
2797
+ projects the full set; legacy `/v1/approvals` may omit them, so they are optional here). Project ONLY
2798
+ the `severity` SCALAR β€” NEVER a `riskDescriptor` object.
2799
+ required: [sessionId, scope]
2800
+ additionalProperties: true
2801
+ properties:
2802
+ sessionId: { type: string, description: 'The decide handle: POST /v1/approvals/:sessionId/decide.' }
2803
+ token: { type: string, description: 'ASSISTANT Β§1 β€” resume capability token (opaque; echo, never parse).' }
2804
+ gateKind:
2805
+ type: string
2806
+ enum: [human, irreversible_ask, resource_limit, needs_review, plan_review, task_done]
2807
+ description: 'ASSISTANT Β§1 β€” which kind of pause (6-arm open set).'
2808
+ severity:
2809
+ type: integer
2810
+ enum: [1, 2, 3, 4, 5]
2811
+ description: >
2812
+ ASSISTANT Β§1 β€” deterministic risk tier = the inbox SORT KEY. PRESENT ONLY for human/irreversible_ask
2813
+ gates; ABSENT for resource_limit/needs_review/plan_review β€” the UI MUST tolerate undefined.
2814
+ spentMicroUsd: { type: number, description: 'ASSISTANT Β§1 β€” cumulative spend on the suspend chain.' }
2815
+ taskId: { type: ['string', 'null'], description: 'Suspended run holding the session claim (service-pinned) β€” join key to trace tool-call blocks + context link.' }
2816
+ scope: { type: string, description: 'Principal; "_" = submitted without one.' }
2817
+ toolName: { type: ['string', 'null'], description: Tool awaiting approval (AskUserQuestion = the question gate). }
2818
+ toolCallId: { type: ['string', 'null'] }
2819
+ input:
2820
+ description: >
2821
+ The pending tool call's args (post-hook), REDACTED + size-bounded β€” for a tool gate the write
2822
+ payload, for an AskUserQuestion the question itself. Lets the approval card render WITHOUT an N+1
2823
+ `trace.turns` fetch per item. Oversized payloads collapse to `{ truncated, bytes }`.
2824
+ `null` for checkpoints suspended before this field existed (consumer falls back to the trace join).
2825
+ # design/80 D-1 β€” the three binding values the human implicitly approves; the consumer reads them here
2826
+ # and echoes them VERBATIM into decide (TOCTOU guard). Absent on pre-D-1 checkpoints β†’ omit from decide.
2827
+ checkpointToken: { type: string, description: 'Single-use token for the exact pending checkpoint the human saw.' }
2828
+ boundCallId: { type: string, description: '= toolCallId of the action the human saw (the bound call).' }
2829
+ boundInputHash: { type: string, description: 'Server-minted OPAQUE sha256 over the pending action args; echo verbatim, NEVER recompute. Binds the input the human SAW, not any updatedInput.' }
2830
+ createdAt: { type: integer, description: epoch ms. }
2831
+ deadline: { type: ['integer', 'null'], description: epoch ms; expired checkpoints are reaped. }
2832
+
2833
+ # ── assistant-scheduler wire (ASSISTANT-WIRE-CONTRACT.md, service main 6cd0164 / core 1.110.0, LIVE) ──
2834
+ CheckpointSummary:
2835
+ type: object
2836
+ description: >
2837
+ ASSISTANT-WIRE-CONTRACT Β§1 β€” core's lightweight projection of every PENDING checkpoint
2838
+ (`summarizeCheckpoint`, identical across all store backends). The shared inbox/overview row shape.
2839
+ Null-able fields are OMITTED when absent, not null. πŸ”΄ projects ONLY the `severity` SCALAR β€” NO
2840
+ `riskDescriptor` object.
2841
+ required: [token, sessionId, scope, gateKind]
2842
+ additionalProperties: true
2843
+ properties:
2844
+ token: { type: string, description: 'resume capability token (opaque; echo, never parse).' }
2845
+ sessionId: { type: string, description: 'the suspended task/session id.' }
2846
+ scope: { type: string, description: 'the multi-tenant owner scope.' }
2847
+ gateKind:
2848
+ type: string
2849
+ enum: [human, irreversible_ask, resource_limit, needs_review, plan_review, task_done]
2850
+ description: 'which kind of pause (6-arm open set).'
2851
+ severity:
2852
+ type: integer
2853
+ enum: [1, 2, 3, 4, 5]
2854
+ description: >
2855
+ deterministic risk tier = the inbox SORT KEY. PRESENT ONLY for human/irreversible_ask gates
2856
+ (they carry a RiskDescriptor); ABSENT for resource_limit/needs_review/plan_review β€” UI tolerates undefined.
2857
+ spentMicroUsd: { type: number, description: 'cumulative spend on the suspend chain (when a resource ledger is attached).' }
2858
+ deadline: { type: integer, description: 'awaiting-human SLA deadline (epoch ms).' }
2859
+
2860
+ InboxRow:
2861
+ description: >
2862
+ ASSISTANT-WIRE-CONTRACT Β§2 β€” an inbox row = a CheckpointSummary + an `objective` (enriched from the
2863
+ task ctx; `null` if unavailable).
2864
+ allOf:
2865
+ - $ref: '#/components/schemas/CheckpointSummary'
2866
+ - type: object
2867
+ properties:
2868
+ objective: { type: ['string', 'null'], description: 'task objective, null if unavailable.' }
2869
+
2870
+ InboxList:
2871
+ type: object
2872
+ description: 'ASSISTANT-WIRE-CONTRACT Β§2 β€” `GET /v1/assistant/inbox` envelope. Already severity-sorted by core.'
2873
+ required: [inbox]
2874
+ additionalProperties: true
2875
+ properties:
2876
+ inbox:
2877
+ type: array
2878
+ items: { $ref: '#/components/schemas/InboxRow' }
2879
+
2880
+ AssistantTaskRow:
2881
+ type: object
2882
+ description: >
2883
+ ASSISTANT-WIRE-CONTRACT Β§3 β€” a scheduler-overview row = run-store `listRuns(owner)` joined with the core
2884
+ scheduler seam. ⚠️ `createdAt`/`updatedAt` are ISO date-time strings (unlike inbox/§1 epoch-ms).
2885
+ required: [taskId, sessionId, status, needsAttention, createdAt, updatedAt]
2886
+ additionalProperties: true
2887
+ properties:
2888
+ taskId: { type: string }
2889
+ sessionId: { type: string }
2890
+ status: { type: string, enum: [running, suspended] }
2891
+ needsAttention: { type: boolean, description: '= suspended on a HITL gate.' }
2892
+ gate:
2893
+ type: ['object', 'null']
2894
+ description: 'the active gate, or null. severity/spentMicroUsd/deadline may themselves be null on the wire.'
2895
+ additionalProperties: true
2896
+ properties:
2897
+ kind: { type: string, description: 'same open set as gateKind.' }
2898
+ severity: { type: ['integer', 'null'], enum: [1, 2, 3, 4, 5, null] }
2899
+ spentMicroUsd: { type: ['number', 'null'] }
2900
+ deadline: { type: ['integer', 'null'], description: epoch ms. }
2901
+ createdAt: { type: string, description: ISO date-time. }
2902
+ updatedAt: { type: string, description: ISO date-time. }
2903
+
2904
+ AssistantTaskList:
2905
+ type: object
2906
+ description: >
2907
+ ASSISTANT-WIRE-CONTRACT Β§3 β€” `GET /v1/assistant/tasks` envelope. Triage-ordered by core (needs-attention
2908
+ first, then severity DESC, then spend DESC). Empty `{tasks:[]}` when no TiDB run store.
2909
+ required: [tasks]
2910
+ additionalProperties: true
2911
+ properties:
2912
+ tasks:
2913
+ type: array
2914
+ items: { $ref: '#/components/schemas/AssistantTaskRow' }
2915
+
2916
+ PlanReviewRequest:
2917
+ type: object
2918
+ description: >
2919
+ ASSISTANT-WIRE-CONTRACT Β§4c β€” `POST /v1/assistant/tasks/:id/plan_review` body (3-state). `editedPlan` is
2920
+ REQUIRED iff `decision==="edit"` (FORBIDDEN otherwise β†’ 400).
2921
+ required: [decision]
2922
+ additionalProperties: false
2923
+ properties:
2924
+ decision: { type: string, enum: [approve, edit, reject] }
2925
+ editedPlan: { type: string, description: 'the operator''s revised plan β€” required iff decision==="edit".' }
2926
+ reason: { type: string }
2927
+
2928
+ SessionRecord:
2929
+ type: object
2930
+ description: >
2931
+ Session audit (`GET /v1/sessions/:id`) β€” true shape. `messages` = current model context
2932
+ (older history folded into a summary message); can be large.
2933
+ required: [sessionId, createdAt, messages]
2934
+ additionalProperties: true
2935
+ properties:
2936
+ sessionId: { type: string }
2937
+ owner: { type: ['string', 'null'], description: Principal. }
2938
+ createdAt: { type: string, description: ISO timestamp (NOTE β€” unlike approvals' epoch-ms). }
2939
+ floorEntryId: { type: ['string', 'null'], description: Compaction window floor; null = never compacted. }
2940
+ thinkingLevel: { type: string }
2941
+ model:
2942
+ type: ['object', 'null']
2943
+ properties:
2944
+ provider: { type: string }
2945
+ modelId: { type: string }
2946
+ messages:
2947
+ type: array
2948
+ items: {}
2949
+
2950
+ SessionSummary:
2951
+ type: object
2952
+ description: >
2953
+ E16 (shell-host sessionPicker; service SessionSummary tidb-run-store.ts:42) β€” a `GET /v1/sessions` list row
2954
+ aggregating a session's run ledger (last activity + most-recent objective preview + status). owner-scoped.
2955
+ All timestamps ISO. `objectivePreview` is service-redacted + truncated (genuinely `null` when no run).
2956
+ required: [sessionId, owner, lastActivityAt, firstActivityAt, runCount, objectivePreview, lastStatus]
2957
+ additionalProperties: true
2958
+ properties:
2959
+ sessionId: { type: string }
2960
+ owner: { type: ['string', 'null'] }
2961
+ lastActivityAt: { type: string }
2962
+ firstActivityAt: { type: string }
2963
+ runCount: { type: integer }
2964
+ objectivePreview: { type: ['string', 'null'] }
2965
+ lastStatus: { type: string }
2966
+
2967
+ SessionListPage:
2968
+ type: object
2969
+ description: Keyset page envelope for `GET /v1/sessions`. `nextCursor` absent = last page.
2970
+ required: [sessions]
2971
+ properties:
2972
+ sessions:
2973
+ type: array
2974
+ items: { $ref: '#/components/schemas/SessionSummary' }
2975
+ nextCursor: { type: string, description: "Opaque keyset cursor (echo verbatim as ?cursor=)." }
2976
+
2977
+ SessionPermissionRules:
2978
+ type: object
2979
+ description: >
2980
+ E6 (shell-host permissionMode; core SessionPermissionRules session-policy-store.ts:23) β€” operator-tightened
2981
+ per-session tool-permission rules (core reads them at prepare-time, SUBTRACT-only). 5 optional `string[]`
2982
+ fields. tighten-only invariant is core-enforced (a loosen needs an operator).
2983
+ properties:
2984
+ toolAllow: { type: array, items: { type: string }, description: Narrowing allowlist (only these tools allowed). }
2985
+ toolDeny: { type: array, items: { type: string }, description: Always-denied tool names (deny wins). }
2986
+ allowDirs: { type: array, items: { type: string }, description: Write tools confined to these dirs; bash denied while set. }
2987
+ commandAllow: { type: array, items: { type: string }, description: "Only these bash command names (argv[0]) allowed." }
2988
+ commandDeny: { type: array, items: { type: string }, description: "Always-denied bash command names (argv[0])." }
2989
+
2990
+ StoredSessionRules:
2991
+ description: '{@link SessionPermissionRules} + the monotonic `rev` the store stamps (the OCC key).'
2992
+ allOf:
2993
+ - $ref: '#/components/schemas/SessionPermissionRules'
2994
+ - type: object
2995
+ required: [rev]
2996
+ properties:
2997
+ rev: { type: integer }
2998
+
2999
+ McpServerStatus:
3000
+ type: object
3001
+ description: >
3002
+ E9 (shell-host mcpStatus; core McpServerStatus core/mcp.ts:76) β€” one MCP server's status at materialization
3003
+ time. `status` is connected | failed (core never emits disabled). `error` (failed only) is service-redacted
3004
+ + length-bounded.
3005
+ required: [name, status]
3006
+ additionalProperties: true
3007
+ properties:
3008
+ name: { type: string }
3009
+ status: { type: string, description: "connected | failed (open set)" }
3010
+ serverInfo:
3011
+ type: object
3012
+ properties:
3013
+ name: { type: string }
3014
+ version: { type: string }
3015
+ toolNames: { type: array, items: { type: string } }
3016
+ error: { type: string }
3017
+
3018
+ McpStatusPanel:
3019
+ type: object
3020
+ description: >
3021
+ `GET /v1/sessions/:id/mcp` envelope. `asOf` = THIS materialization moment (ISO). `degraded:true` (servers
3022
+ empty) β‡’ materialize timed out/failed (NOT "no MCP"). No MCP configured β‡’ `servers:[]` without `degraded`.
3023
+ required: [asOf, servers]
3024
+ additionalProperties: true
3025
+ properties:
3026
+ asOf: { type: string }
3027
+ servers:
3028
+ type: array
3029
+ items: { $ref: '#/components/schemas/McpServerStatus' }
3030
+ degraded: { type: boolean }
3031
+
3032
+ # ── 2c session-sync (P1d) β€” the cloud-as-a-SYNC-PEER schemas ──────────────────────────────────────────────
3033
+ SyncEntry:
3034
+ type: object
3035
+ description: >
3036
+ ONE session-tree entry as it crosses the sync wire β€” OPAQUE to the SDK (verbatim, cross-backend-stable ids).
3037
+ The SDK reads only `.id` (for the Β§7 id-set classify) and pipes the rest through untouched; the cloud
3038
+ re-validates the whole tree (unique ids / parent-before-child / one-root / leaf-resolvable).
3039
+ required: [id]
3040
+ additionalProperties: true
3041
+ properties:
3042
+ id: { type: string, description: Stable entry id, unique within a session (the Β§7 divergence key). }
3043
+ parentId: { type: [string, 'null'] }
3044
+ type: { type: string, description: Entry discriminator (message / leaf / compaction / …) β€” opaque to the SDK. }
3045
+
3046
+ SyncSnapshot:
3047
+ type: object
3048
+ description: >
3049
+ One E19 file snapshot keyed by a `SessionTreeEntry.id`. `manifest` = `[relPath, blobHash]` tuples; the blob
3050
+ BYTES are NOT inlined (they ride the content-addressed blob routes).
3051
+ required: [key, manifest]
3052
+ properties:
3053
+ key: { type: string }
3054
+ manifest:
3055
+ type: array
3056
+ items:
3057
+ type: array
3058
+ minItems: 2
3059
+ maxItems: 2
3060
+ items: { type: string }
3061
+ description: A `[relPath, blobHash]` tuple.
3062
+
3063
+ SyncAnchor:
3064
+ type: object
3065
+ description: One E18 resume-at anchor. `owner` is RE-KEYED to the importing principal on a PUSH (Β§9).
3066
+ required: [eventId, entryId, owner]
3067
+ properties:
3068
+ eventId: { type: string }
3069
+ entryId: { type: string }
3070
+ owner: { type: [string, 'null'] }
3071
+
3072
+ SessionRulesRecord:
3073
+ type: object
3074
+ description: A (principal, rules) policy record β€” one row across ALL principals (E6 `listBySession`). Replayed verbatim on import (the cloud applies the tighten-only gate).
3075
+ required: [rules]
3076
+ properties:
3077
+ principal: { type: string, description: Absent = the session-default rules. }
3078
+ rules: { $ref: '#/components/schemas/StoredSessionRules' }
3079
+
3080
+ SessionManifest:
3081
+ type: object
3082
+ description: >
3083
+ 2c session-sync `GET …/sync/manifest` payload (wrapped server-side as `{ manifest }`). The THIN cross-backend
3084
+ snapshot: entry IDS (oldest-first, NOT payloads) + per-snapshot relPath→blobHash + policy + anchors + leaf.
3085
+ required: [sessionId, entryIds, entryCount, leafId, snapshots, policy, anchors]
3086
+ properties:
3087
+ sessionId: { type: string }
3088
+ entryIds: { type: array, items: { type: string }, description: Oldest-first, verbatim cross-backend. }
3089
+ entryCount: { type: integer, description: '= entryIds.length (the `/sync/entries` trailer count).' }
3090
+ leafId: { type: [string, 'null'] }
3091
+ snapshots: { type: array, items: { $ref: '#/components/schemas/SyncSnapshot' } }
3092
+ policy: { type: array, items: { $ref: '#/components/schemas/SessionRulesRecord' } }
3093
+ anchors: { type: array, items: { $ref: '#/components/schemas/SyncAnchor' } }
3094
+
3095
+ SessionBundle:
3096
+ type: object
3097
+ description: >
3098
+ The portable state of ONE session, ready to replay into another backend. Blobs are NOT inlined (each
3099
+ snapshot carries only its `manifest`; the bytes stream through the content-addressed blob routes). The SDK
3100
+ assembles this from a manifest + the streamed entries for a PUSH.
3101
+ required: [sessionId, entries, snapshots, policy, anchors]
3102
+ properties:
3103
+ sessionId: { type: string }
3104
+ entries: { type: array, items: { $ref: '#/components/schemas/SyncEntry' } }
3105
+ snapshots: { type: array, items: { $ref: '#/components/schemas/SyncSnapshot' } }
3106
+ policy: { type: array, items: { $ref: '#/components/schemas/SessionRulesRecord' } }
3107
+ anchors: { type: array, items: { $ref: '#/components/schemas/SyncAnchor' } }
3108
+
3109
+ SyncRelation:
3110
+ description: >
3111
+ Β§7 β€” how a SOURCE log relates to a DESTINATION log, decided over the entry-ID SETS. A discriminated union on
3112
+ `relation`. `fresh`/`identical` carry nothing else; `fast_forward` the appended tail; `stale` the dst entries
3113
+ the src lacks; `fork` the common ancestor + each side's exclusive ids.
3114
+ oneOf:
3115
+ - type: object
3116
+ required: [relation]
3117
+ properties: { relation: { const: fresh } }
3118
+ - type: object
3119
+ required: [relation]
3120
+ properties: { relation: { const: identical } }
3121
+ - type: object
3122
+ required: [relation, newEntryIds]
3123
+ properties:
3124
+ relation: { const: fast_forward }
3125
+ newEntryIds: { type: array, items: { type: string } }
3126
+ - type: object
3127
+ required: [relation, dstAheadBy]
3128
+ properties:
3129
+ relation: { const: stale }
3130
+ dstAheadBy: { type: array, items: { type: string } }
3131
+ - type: object
3132
+ required: [relation, commonAncestor, srcExclusive, dstExclusive]
3133
+ properties:
3134
+ relation: { const: fork }
3135
+ commonAncestor: { type: [string, 'null'] }
3136
+ srcExclusive: { type: array, items: { type: string } }
3137
+ dstExclusive: { type: array, items: { type: string } }
3138
+ discriminator:
3139
+ propertyName: relation
3140
+ mapping:
3141
+ fresh: '#/components/schemas/SyncRelation'
3142
+ identical: '#/components/schemas/SyncRelation'
3143
+ fast_forward: '#/components/schemas/SyncRelation'
3144
+ stale: '#/components/schemas/SyncRelation'
3145
+ fork: '#/components/schemas/SyncRelation'
3146
+
3147
+ ImportStaged:
3148
+ type: object
3149
+ description: >
3150
+ `POST …/sync/import` Phase-A result. `identical` β†’ no `stagingId` (skip Phase B); `fresh`/`fast_forward` β†’
3151
+ `{ stagingId, relation }`. `relation` is the BARE classifier tag (not the full object).
3152
+ required: [relation]
3153
+ properties:
3154
+ stagingId: { type: string, description: Present iff Phase B is needed (absent on `identical`). }
3155
+ relation: { type: string, enum: [fresh, identical, fast_forward], description: The Β§7 tag classified at Phase A. }
3156
+
3157
+ ImportCommitted:
3158
+ type: object
3159
+ description: '`POST …/sync/import/:stagingId/entries` (Phase B) commit result β€” the AUTHORITATIVE in-txn re-classify tag.'
3160
+ required: [relation]
3161
+ properties:
3162
+ relation: { type: string, enum: [fresh, identical, fast_forward], description: The committed Β§7 tag. }
3163
+
3164
+ SyncConflict:
3165
+ type: object
3166
+ description: >
3167
+ 409 body for a 2c session-sync PUSH that would lose destination history. `errorCode:"conflict"` + the Β§7
3168
+ `relation` (`fork`/`stale`) carrying the exclusive entry sets β†’ the SDK surfaces a typed SyncConflictError.
3169
+ The SAME envelope also carries an active-run / in-flight-import conflict WITHOUT a `relation` (ConflictError).
3170
+ additionalProperties: true
3171
+ properties:
3172
+ error: { type: string }
3173
+ errorCode: { type: string, enum: [conflict, session_active, import_in_flight] }
3174
+ relation: { $ref: '#/components/schemas/SyncRelation' }
3175
+ activeTaskId: { type: string, description: Present on `session_active` β€” the task holding the session lock. }
3176
+
3177
+ ErrorResponse:
3178
+ type: object
3179
+ description: >
3180
+ Typed-error source. The SDK maps (status, errorCode) β†’ a semantic error class so callers branch on the
3181
+ error, not a string. Messages are secret-scrubbed before surfacing.
3182
+ additionalProperties: true
3183
+ properties:
3184
+ errorCode:
3185
+ type: string
3186
+ description: >
3187
+ HTTP-error typed code. Known: `conflict` (409 session active / CAS), `quotaExceeded` (429).
3188
+ NOTE: budget (`budget.precall`/`budget.exceeded`) and `cancelled` are RESULT-level codes on
3189
+ TaskResult/RunRecord, NOT HTTP errors (pinned wire contract).
3190
+ error: { type: string }
3191
+ errorMessage: { type: string }
3192
+ activeTaskId:
3193
+ type: string
3194
+ description: On a 409 β€” the task currently holding the session lock (see the Conflict response note).
3195
+
3196
+ # ── Streaming event taxonomy (the UX 命脉). Each SSE `data:` line is one JSON-serialized AgentEvent. ──
3197
+ # TWO vocabularies (the pinned wire contract Drift 3):
3198
+ # live (POST /v1/tasks/stream): text_delta / reasoning_delta (delta = string fragment, core-native)
3199
+ # + tool_start/tool_end/turn_end/compacted/done
3200
+ # durable (GET /v1/runs/:id/events): text / reasoning (turn-AGGREGATED, no deltas)
3201
+ # + tool_start/tool_end/turn_end/compacted/suspended/done/failed
3202
+ AgentEvent:
3203
+ oneOf:
3204
+ - $ref: '#/components/schemas/Event_reasoning'
3205
+ - $ref: '#/components/schemas/Event_text'
3206
+ - $ref: '#/components/schemas/Event_reasoning_delta'
3207
+ - $ref: '#/components/schemas/Event_text_delta'
3208
+ - $ref: '#/components/schemas/Event_tool_start'
3209
+ - $ref: '#/components/schemas/Event_tool_end'
3210
+ - $ref: '#/components/schemas/Event_turn_end'
3211
+ - $ref: '#/components/schemas/Event_compacted'
3212
+ - $ref: '#/components/schemas/Event_suggestions'
3213
+ - $ref: '#/components/schemas/Event_status'
3214
+ - $ref: '#/components/schemas/Event_task_progress'
3215
+ - $ref: '#/components/schemas/Event_workspace_changed'
3216
+ - $ref: '#/components/schemas/Event_suspended'
3217
+ - $ref: '#/components/schemas/Event_done'
3218
+ - $ref: '#/components/schemas/Event_failed'
3219
+ discriminator:
3220
+ propertyName: type
3221
+ mapping:
3222
+ reasoning: '#/components/schemas/Event_reasoning'
3223
+ text: '#/components/schemas/Event_text'
3224
+ reasoning_delta: '#/components/schemas/Event_reasoning_delta'
3225
+ text_delta: '#/components/schemas/Event_text_delta'
3226
+ tool_start: '#/components/schemas/Event_tool_start'
3227
+ tool_end: '#/components/schemas/Event_tool_end'
3228
+ turn_end: '#/components/schemas/Event_turn_end'
3229
+ compacted: '#/components/schemas/Event_compacted'
3230
+ suggestions: '#/components/schemas/Event_suggestions'
3231
+ status: '#/components/schemas/Event_status'
3232
+ task_progress: '#/components/schemas/Event_task_progress'
3233
+ workspace_changed: '#/components/schemas/Event_workspace_changed'
3234
+ suspended: '#/components/schemas/Event_suspended'
3235
+ done: '#/components/schemas/Event_done'
3236
+ failed: '#/components/schemas/Event_failed'
3237
+
3238
+ # design/99 Β§E2 (SHIPPED, core TaskEventIdentity) β€” additive per-event identity on every CONTENT event (NOT on
3239
+ # the lifecycle arms turn_end/compacted/suspended/done/failed). eventId = uuidv7 dedup/resume handle (the durable
3240
+ # `text` re-stamps the turn's first text_delta eventId, runs.ts:250); parentToolCallId = sub-agent attribution.
3241
+ EventIdentity:
3242
+ type: object
3243
+ properties:
3244
+ eventId: { type: string }
3245
+ parentToolCallId: { type: string }
3246
+ Event_reasoning:
3247
+ type: object
3248
+ description: durable stream only β€” turn-aggregated thinking.
3249
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
3250
+ required: [type, text]
3251
+ properties:
3252
+ type: { const: reasoning }
3253
+ text: { type: string }
3254
+ Event_text:
3255
+ type: object
3256
+ description: durable stream only β€” turn-aggregated answer text (carries the E18 resume-handle eventId).
3257
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
3258
+ required: [type, text]
3259
+ properties:
3260
+ type: { const: text }
3261
+ text: { type: string }
3262
+ Event_reasoning_delta:
3263
+ type: object
3264
+ description: live stream only β€” per-token thinking fragment (`delta` is the string fragment).
3265
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
3266
+ required: [type, delta]
3267
+ properties:
3268
+ type: { const: reasoning_delta }
3269
+ delta: { type: string }
3270
+ Event_text_delta:
3271
+ type: object
3272
+ description: live stream only β€” per-token answer fragment.
3273
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
3274
+ required: [type, delta]
3275
+ properties:
3276
+ type: { const: text_delta }
3277
+ delta: { type: string }
3278
+ Event_tool_start:
3279
+ type: object
3280
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
3281
+ required: [type, toolCallId, toolName]
3282
+ properties:
3283
+ type: { const: tool_start }
3284
+ toolCallId: { type: string }
3285
+ toolName: { type: string }
3286
+ args: {}
3287
+ Event_tool_end:
3288
+ type: object
3289
+ description: >
3290
+ design/99 Β§E1 (SHIPPED) β€” `output` is the model-facing action-card BODY: NON-UNIFORM (a string, or a
3291
+ (TextContent|ImageContent)[] block array) and degrades to a single TRUNCATED string over the core size cap
3292
+ (`truncated: true`). Absent β‡’ the tool produced no body. πŸ”΄ UNTRUSTED RAW + observability-only: the service
3293
+ has redactDeep'd + size-bounded it; a consumer MAY further bound, MUST never re-feed it to a model.
3294
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
3295
+ required: [type, toolCallId, toolName, isError]
3296
+ properties:
3297
+ type: { const: tool_end }
3298
+ toolCallId: { type: string }
3299
+ toolName: { type: string }
3300
+ isError: { type: boolean }
3301
+ output: {} # NON-UNIFORM: string | (TextContent|ImageContent)[] | (truncated) string. Absent β‡’ no body.
3302
+ truncated: { type: boolean } # true β‡’ output was size-bounded to a truncated string.
3303
+ Event_turn_end:
3304
+ type: object
3305
+ required: [type]
3306
+ properties:
3307
+ type: { const: turn_end }
3308
+ usage:
3309
+ type: object
3310
+ description: Full live shape β€” the portal cost bar's data source.
3311
+ properties:
3312
+ inputTokens: { type: integer }
3313
+ outputTokens: { type: integer }
3314
+ cacheReadTokens: { type: integer }
3315
+ cacheWriteTokens: { type: integer }
3316
+ costMicroUsd: { type: integer }
3317
+ Event_compacted:
3318
+ type: object
3319
+ required: [type]
3320
+ properties:
3321
+ type: { const: compacted }
3322
+ tokensBefore: { type: integer }
3323
+ Event_suggestions:
3324
+ type: object
3325
+ description: >
3326
+ E12 (shell-host; service runs.ts:392 + server.ts:3633, opt-in via `TaskRequest.suggestNextPrompts`) β€”
3327
+ post-completion "what to ask next" prompt suggestions. Appended to the DURABLE run-events tail AFTER a
3328
+ `done` with `status:"completed"` (core's fire-and-forget LLM pass). πŸ”΄ UNTRUSTED model text, service-
3329
+ redacted, UI-ONLY β€” render as clickable next-prompt chips; NEVER re-feed to a model. Empty/absent β‡’ none.
3330
+ required: [type, suggestions]
3331
+ properties:
3332
+ type: { const: suggestions }
3333
+ suggestions: { type: array, items: { type: string } }
3334
+ Event_status:
3335
+ type: object
3336
+ description: >
3337
+ design/99 Β§E3/Β§E10 (SHIPPED, LIVE-only) β€” a brain-layer liveness status the brain would otherwise absorb
3338
+ silently (429 rate-limit / 5xx-or-network retry / reconnect / open circuit breaker). EPHEMERAL β€” never
3339
+ persisted/replayed on resume; provider-neutral (NO HTTP status / stop_reason). `phase` is core's closed
3340
+ BrainStatusPhase but render open (a deployment may add a phase on its own channel).
3341
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
3342
+ required: [type, phase]
3343
+ properties:
3344
+ type: { const: status }
3345
+ phase: { type: string, description: "rate_limited | retrying | reconnecting | circuit_open (open set)" }
3346
+ detail: { type: string, description: "neutral human hint (NO provider/HTTP detail)" }
3347
+ retryInSec: { type: number }
3348
+ Event_task_progress:
3349
+ type: object
3350
+ description: >
3351
+ design/99 MF-10 (core 1.147+, `task_progress` arm) β€” a SUBAGENT PROGRESS TICK: a delegated sub-run's usage
3352
+ ACCRUING at each turn boundary WHILE it runs (tokens/tool-uses climbing), so a parent UI animates the Task
3353
+ card rollup. Emitted ONLY for a subagent (a task carrying a `parentToolCallId`, via EventIdentity); a
3354
+ top-level run's own usage is the consumer's own (turn_end). `status` is the literal "running" (the terminal
3355
+ usage is the `done` result). LIVE/EPHEMERAL β€” NOT persisted, NOT replayed on resume.
3356
+ πŸ”΄ WIRE NOTE: core mints this arm, but as of service 1bf7a5c NO service projection forwards it onto a CLIENT
3357
+ wire (/v1/tasks/stream or /v1/runs/:id/events) β€” the service consumes it INTERNALLY (fleetRunPublisher) and
3358
+ re-expresses it as a fleet CHILD row on /v1/fleet/stream. So today MF-10 is observable via the fleet stream
3359
+ subagent child rows; this arm is the core-arm MIRROR and surfaces on the task/run streams ONLY IF a future
3360
+ service projection passes it through. Additive / tolerate-absent.
3361
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
3362
+ required: [type, taskId, usage, status]
3363
+ properties:
3364
+ type: { const: task_progress }
3365
+ taskId: { type: string, description: 'The subagent task id (the delegated sub-run).' }
3366
+ usage:
3367
+ type: object
3368
+ description: 'Cumulative usage as of this turn boundary (closed 3-field set, byte-aligned to core).'
3369
+ required: [totalTokens, toolUses, durationMs]
3370
+ properties:
3371
+ totalTokens: { type: integer }
3372
+ toolUses: { type: integer }
3373
+ durationMs: { type: integer }
3374
+ status: { const: running, description: 'Always "running" mid-run (the terminal usage is the done result).' }
3375
+ Event_workspace_changed:
3376
+ type: object
3377
+ description: >
3378
+ design/99 Β§E13 (SHIPPED, LIVE-only) β€” a `bash` `cd` moved the task's tracked working directory; carries the
3379
+ new RAW logical cwd so a shell UI updates its prompt/path. Only fires with a real shell mounted; the initial
3380
+ cwd is NOT announced (only subsequent changes). EPHEMERAL β€” not persisted; resets across suspend/resume.
3381
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
3382
+ required: [type, cwd]
3383
+ properties:
3384
+ type: { const: workspace_changed }
3385
+ cwd: { type: string }
3386
+ Event_suspended:
3387
+ type: object
3388
+ description: HITL β€” the run paused on an F4 gate or AskUserQuestion. Resolve via approvals.decide, then resume.
3389
+ required: [type]
3390
+ properties:
3391
+ type: { const: suspended }
3392
+ gate:
3393
+ description: >
3394
+ CheckpointGate β€” OPEN DISCRIMINATED SET (design/80 D-0), mirrors core src/core/checkpoint-store.ts.
3395
+ 5 known kinds below; consumers MUST branch on `kind` and render an UNKNOWN kind generically (core
3396
+ can mint a kind this contract predates — a core→wire contract test fails CI, not prod). Tool
3397
+ args/question text are NOT here β€” join the task's trace tool-call block via approvals' toolCallId.
3398
+ oneOf:
3399
+ - type: object # plain human approval (budget-auto-approvable)
3400
+ required: [kind, reason, toolName]
3401
+ properties: { kind: { const: human }, reason: { type: string }, toolName: { type: string } }
3402
+ - type: object # design/37 irreversibility / design/70 egress safety-tighten β€” NOT budgetable (load-bearing safety gate)
3403
+ required: [kind, reason, toolName]
3404
+ properties: { kind: { const: irreversible_ask }, reason: { type: string }, toolName: { type: string } }
3405
+ - type: object # design/74 resource (cost/token/time) slice boundary β€” resolve { decision: "continue" }
3406
+ required: [kind, reason]
3407
+ properties: { kind: { const: resource_limit }, reason: { type: string } }
3408
+ - type: object # design/76 dry-run/shadow β†’ durable needs_review TERMINAL (POST-prediction review, disjoint from approval family)
3409
+ required: [kind, reason]
3410
+ properties: { kind: { const: needs_review }, reason: { type: string } }
3411
+ - type: object # design/38 Path A background sub-task handle (door B never sees it)
3412
+ required: [kind]
3413
+ properties: { kind: { const: task_done } }
3414
+ - type: object # OPEN SET β€” unknown future kind (e.g. design/80 D-B plan_review); render generically, never crash
3415
+ required: [kind]
3416
+ # πŸ”΄ the pinned wire contract: exclude the 5 KNOWN kinds so a known kind matches ONLY its const member above
3417
+ # (not also this fallback) β†’ keeps `oneOf` unambiguous AND preserves each kind's shape validation.
3418
+ # This fallback matches ONLY a future kind this contract predates. Add new known kinds to the enum.
3419
+ properties: { kind: { type: string, not: { enum: [human, irreversible_ask, resource_limit, needs_review, task_done] } } }
3420
+ - type: 'null'
3421
+ Event_done:
3422
+ type: object
3423
+ description: Carries the full TaskResult (output string at .result, stats at .stats) β€” service Drift 4.
3424
+ required: [type, result]
3425
+ properties:
3426
+ type: { const: done }
3427
+ result: { $ref: '#/components/schemas/TaskResult' }
3428
+ Event_failed:
3429
+ type: object
3430
+ required: [type]
3431
+ properties:
3432
+ type: { const: failed }
3433
+ errorCode: { type: string }
3434
+ errorMessage: { type: string }