@sema-agent/sdk 0.0.101 → 0.0.102

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.
package/openapi.yaml CHANGED
@@ -13,8 +13,29 @@ openapi: 3.1.0
13
13
  # 测试锚定,是目前唯一真正贴合 server 当前实现的文档**。本文件保留作 M0 时代的历史参照(早期
14
14
  # verb 清单/大致响应形状仍可读),但**任何具体字段名/是否顶层/是否为 optional 的判断,一律以
15
15
  # types.ts 为准,本文件不可信**——已实证撞获至少一处两者互斥的真矛盾(`PendingCheckpoint`,
16
- # 见下方该 schema 旁注)。回填 spec + 建反射式字段 diff 门是后续候办项,未完成前不要相信这里
17
- # 的字段级细节。
16
+ # 见下方该 schema 旁注)。
17
+ #
18
+ # ── 状态更新(2026-07-25 回填批完成)────────────────────────────────────────────────────────
19
+ # 上面那句「回填 spec + 建反射式字段 diff 门是后续候办项,未完成前不要相信这里的字段级细节」
20
+ # **已经过时**。现状是:
21
+ # ✅ 反射式字段级 diff 门已建并**零容忍**:`packages/sdk/test/spec-field-drift-gate.test.ts`
22
+ # 用 TS 编译器 API 解析 `types.ts`(跟 extends / 解 Omit / Pick)× 解析本文件(递归展开
23
+ # `allOf`/`$ref`)逐字段比对。存量两族基线**都已清零**:字段级 drift 0、整 schema 缺席 0。
24
+ # ⇒ types.ts 加了字段而这里不加,CI 就红。**字段级细节现在有机器看守了。**
25
+ # ✅ 本批同时补了 3 条此前整条缺失的路径(`/v1/images`、`/v1/images/select`、
26
+ # `/v1/capabilities/scenarios/{name}`)、`AgentEvent` 缺的 6 个臂、`/v1/sessions/{id}` 的
27
+ # 5 个 query 参数,并修掉一处真错(`/v1/tasks/{id}/stream` 原先 $ref 到 `AgentEvent`,实际是
28
+ # `TraceStreamEvent` 词汇表 —— 本文件自己的两处声明互相矛盾)。
29
+ #
30
+ # 🔴 但**权威依然是 types.ts**,理由没变:本文件是手写的第二份声明,门只能保证"字段名集合一致",
31
+ # 保不了语义/可空性/枚举值。已知门**看不见**的三类(判断时请回 types.ts 或 server 源码):
32
+ # ① 枚举值陈旧 —— 本批就抓到 `AssistantTask.status` 少了 `needs_review`(真的一等 triage 态)。
33
+ # ② 联合缺臂 —— `AgentEvent` 曾少 6 臂;门只按"命名 schema ↔ 同名 interface"配对,联合对它不可见。
34
+ # ③ 层差 —— 本文件描述 **wire**(如 gate 的数值字段是显式 `null`),types.ts 描述 **SDK 归一后**
35
+ # (null 已在资源层折成省略键)。两者都对,不要互相"修正"。见 `AssistantGate` 旁注。
36
+ # 另仍有意留在覆盖外(**不是已覆盖**):`/v1/images` 前缀下的 register/:profile/digests 与整个
37
+ # bake 控制面(SDK 侧还用 `Record<string, unknown>`、没有具名类型 ⇒ 应先在 types.ts 具名);
38
+ # `SessionBundle` 目前是本文件里唯一的孤儿 schema(无人 $ref)。
18
39
  #
19
40
  # Consumed by @sema-agent/sdk (→ both doors: the CC/Codex MCP façade and the
20
41
  # portal BFF). Producer = the service.
@@ -124,6 +145,37 @@ paths:
124
145
  schema: { $ref: '#/components/schemas/Capabilities' }
125
146
  '401': { $ref: '#/components/responses/Unauthorized' }
126
147
 
148
+ # 补漏(2026-07-25):本路径此前完全不在本文件里,尽管 SDK 早有 `ScenarioDetail` 类型。形按 server 亲读取证。
149
+ /v1/capabilities/scenarios/{name}:
150
+ get:
151
+ tags: [metrics]
152
+ operationId: scenarioDetail
153
+ x-status: live
154
+ summary: One scenario's detail card.
155
+ description: >
156
+ The detail for a single scenario. The LIST of available names rides `capabilities().scenarios` — this
157
+ route only expands one. Auth = same as `/v1/capabilities` (config surface).
158
+ parameters:
159
+ - name: name
160
+ in: path
161
+ required: true
162
+ schema: { type: string }
163
+ description: 'the scenario name (as listed in `capabilities().scenarios`).'
164
+ responses:
165
+ '200':
166
+ description: The scenario detail card.
167
+ content:
168
+ application/json:
169
+ schema: { $ref: '#/components/schemas/ScenarioDetail' }
170
+ '401': { $ref: '#/components/responses/Unauthorized' }
171
+ '404':
172
+ # 🔴 这条曾是全站唯一的**对象形** error(`{error:{code}}`),SDK 的 toApiError 为它特判过一轮。
173
+ # server 已统一为「字符串 error + 顶层 errorCode」的邻位形(同 cancel/decide),机器判据保留为 errorCode。
174
+ description: 'Unknown scenario — `{ error: "scenario not found", errorCode: "scenario_not_found" }`.'
175
+ content:
176
+ application/json:
177
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
178
+
127
179
  /v1/memory:
128
180
  parameters:
129
181
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -533,14 +585,71 @@ paths:
533
585
  CURRENT model context (older history already folded into a summary message) — can be LARGE, the portal
534
586
  should lazy-expand. Absent entirely on workers without the TiDB pool (capabilities.asyncRuns is the
535
587
  practical same-prereq signal).
588
+
589
+
590
+ 🔴 The 5 query params below are ADDITIVE: a request with NONE of them returns the byte-identical legacy
591
+ payload. `?message=` SHORT-CIRCUITS to a different response schema (`SessionMessageEnvelope`) — see below.
592
+ # 补漏(2026-07-25):此前本操作**一个 query 参数都没声明**,所以整个分页/截断/单条展开面对生成式消费方
593
+ # 等于不存在。以下 5 个按 server 亲读取证(`http/server.ts:5692-5723`)。
594
+ parameters:
595
+ - name: message
596
+ in: query
597
+ required: false
598
+ schema: { type: integer, minimum: 0 }
599
+ description: >
600
+ The single-message EXPAND leg: returns `SessionMessageEnvelope` (ONE full message body) instead of a
601
+ `SessionRecord`, and IGNORES the other params. Index is stable within one `floorEntryId` generation —
602
+ a compaction changes `floorEntryId` and invalidates held indices. Out-of-range → 404 (body carries
603
+ `total`). This is the companion of a `blobCap`-truncated read: expand = refetch that ONE message.
604
+ - name: tail
605
+ in: query
606
+ required: false
607
+ schema: { type: integer, minimum: 0 }
608
+ description: 'return the LAST N messages (+ a `window` `{offset,total}`).'
609
+ - name: before
610
+ in: query
611
+ required: false
612
+ schema: { type: integer, minimum: 0 }
613
+ description: 'return the page ABOVE a prior window offset (pair with `limit`).'
614
+ - name: limit
615
+ in: query
616
+ required: false
617
+ schema: { type: integer, minimum: 0 }
618
+ description: 'page size for the `tail`/`before` window. Ignored unless one of those is present.'
619
+ - name: blobCap
620
+ in: query
621
+ required: false
622
+ schema: { type: integer, minimum: 1 }
623
+ description: >
624
+ Truncate oversized string bodies to a preview and mark that message `truncated: true`. Only values
625
+ > 0 take effect. Expand a truncated message via `?message=<index>` (full size).
536
626
  responses:
537
627
  '200':
538
- description: Session record.
628
+ description: >
629
+ A `SessionRecord` — or a `SessionMessageEnvelope` when `?message=` was supplied.
630
+
631
+ ⚠️ Parameter parsing is LENIENT by construction: a non-numeric or NEGATIVE value is treated as
632
+ ABSENT, not as an error (so `?tail=-1` quietly returns the full record rather than a 400). A client
633
+ must not rely on a 400 to catch its own bad paging arithmetic.
539
634
  content:
540
635
  application/json:
541
- schema: { $ref: '#/components/schemas/SessionRecord' }
636
+ schema:
637
+ oneOf:
638
+ - $ref: '#/components/schemas/SessionRecord'
639
+ - $ref: '#/components/schemas/SessionMessageEnvelope'
542
640
  '401': { $ref: '#/components/responses/Unauthorized' }
543
- '404': { $ref: '#/components/responses/NotFound' }
641
+ '404':
642
+ description: >
643
+ Non-owner / unknown session (no existence oracle) — or, with `?message=`, an out-of-range index, whose
644
+ body additionally carries `total`.
645
+ content:
646
+ application/json:
647
+ schema:
648
+ type: object
649
+ additionalProperties: true
650
+ properties:
651
+ error: { type: string }
652
+ total: { type: integer, description: 'present on the out-of-range `?message=` 404 — the message count available.' }
544
653
  delete:
545
654
  tags: [sessions]
546
655
  operationId: sessionsDelete
@@ -1278,15 +1387,14 @@ paths:
1278
1387
  Owner-gated (NOT D-G); non-owner → 404. SDK → GateNotResumableError on the 409.
1279
1388
  responses:
1280
1389
  '200':
1281
- description: Resumed; `{status}` (e.g. "completed").
1390
+ # 2026-07-25:此前只声明 `{status}`。真体(`http/server.ts:7688-7696`,与 plan_review 共用
1391
+ # `driveResumeIntoRunLog`)带 taskId/sessionId + 失败态的 errorCode/errorMessage/retriable。
1392
+ description: >
1393
+ Resumed. 🔴 A 200 does NOT mean success — `status` can be `failed`, in which case `errorCode`/
1394
+ `errorMessage` say why and `retriable:true` means the park is STILL PENDING (decide again).
1282
1395
  content:
1283
1396
  application/json:
1284
- schema:
1285
- type: object
1286
- required: [status]
1287
- additionalProperties: true
1288
- properties:
1289
- status: { type: string }
1397
+ schema: { $ref: '#/components/schemas/AssistantTaskStatus' }
1290
1398
  '401': { $ref: '#/components/responses/Unauthorized' }
1291
1399
  '404': { $ref: '#/components/responses/NotFound' }
1292
1400
  '409':
@@ -1321,15 +1429,13 @@ paths:
1321
1429
  schema: { $ref: '#/components/schemas/PlanReviewRequest' }
1322
1430
  responses:
1323
1431
  '200':
1324
- description: Resolved; `{status}` (e.g. "completed").
1432
+ # 2026-07-25:同 resume —— 两条共用 `driveResumeIntoRunLog`,响应体逐字相同。
1433
+ description: >
1434
+ Resolved. 🔴 A 200 does NOT mean success — see `status`/`errorCode`/`retriable` (same body as §4b
1435
+ resume; the two share one handler).
1325
1436
  content:
1326
1437
  application/json:
1327
- schema:
1328
- type: object
1329
- required: [status]
1330
- additionalProperties: true
1331
- properties:
1332
- status: { type: string }
1438
+ schema: { $ref: '#/components/schemas/AssistantTaskStatus' }
1333
1439
  '400':
1334
1440
  description: >
1335
1441
  Bad/missing `decision`; `edit` without a non-empty `editedPlan`; `editedPlan` present on
@@ -1364,15 +1470,14 @@ paths:
1364
1470
  `202` no-op; an already-`suspended` task → 409; `RESOURCE_SUSPEND` off → 501. Owner-gated; non-owner → 404.
1365
1471
  responses:
1366
1472
  '202':
1367
- description: Preempting (or 202 no-op on a terminal task); `{status}`.
1473
+ # 2026-07-25:此前只声明 `{status}`。真体是 `{taskId, status, note}`(`http/server.ts` 三个 202 出口)。
1474
+ description: >
1475
+ Preempting — or an honest 202 NO-OP. ⚠️ `status` is NOT always `"preempting"`: the two no-op exits
1476
+ echo the run's own status instead (a non-preempt-eligible leg, or a task that already stopped
1477
+ running), and `note` says which case it was. Read `note`, do not assume the preempt took effect.
1368
1478
  content:
1369
1479
  application/json:
1370
- schema:
1371
- type: object
1372
- required: [status]
1373
- additionalProperties: true
1374
- properties:
1375
- status: { type: string }
1480
+ schema: { $ref: '#/components/schemas/AssistantTaskStatus' }
1376
1481
  '401': { $ref: '#/components/responses/Unauthorized' }
1377
1482
  '404': { $ref: '#/components/responses/NotFound' }
1378
1483
  '409':
@@ -1472,10 +1577,20 @@ paths:
1472
1577
  - $ref: '#/components/parameters/LastEventId'
1473
1578
  responses:
1474
1579
  '200':
1475
- description: SSE stream of AgentEvents (resumable; same durable source as runs.events).
1580
+ # 🔴 修正(2026-07-25):此处原先 $ref 到 `AgentEvent`,是真错 —— 而且本文件自己就矛盾:专门为这条路写的
1581
+ # `TraceStreamEvent` schema 的描述里明写「A DISTINCT vocabulary from AgentEvent」,却没有任何地方引用它
1582
+ # (它是本文件里的一个孤儿 schema)。按 server 真码两侧取证后确定 TraceStreamEvent 才是对的:
1583
+ # `streamTaskTrace` 每帧走 `mapTraceEvent`(`trace/project.ts:530`),发的是 SSE **event 名**
1584
+ # `block-thinking-delta` / `block-content-delta` / `tool-call` / `tool-result` / `turn` /
1585
+ # `prompt-assembled` / `done` / `error`,帧里**没有** `type` 字段;SDK 的 `trace.stream()` 注释逐字同款。
1586
+ # 照旧声明生成的客户端会去 switch 一个不存在的 `type` ⇒ 这条流路对它完全不可用。
1587
+ # (AgentEvent 词汇表在 `GET /v1/runs/:id/events`,那条声明是对的 —— 两条是不同的面,别再混。)
1588
+ description: >
1589
+ Resumable SSE of the workspace LIVE timeline — the BLOCK-grained trace vocabulary
1590
+ (`TraceStreamEvent`), NOT the `AgentEvent` vocabulary of `/v1/runs/:id/events`.
1476
1591
  content:
1477
1592
  text/event-stream:
1478
- schema: { $ref: '#/components/schemas/AgentEvent' }
1593
+ schema: { $ref: '#/components/schemas/TraceStreamEvent' }
1479
1594
  '401': { $ref: '#/components/responses/Unauthorized' }
1480
1595
  '404': { $ref: '#/components/responses/NotFound' }
1481
1596
 
@@ -1793,6 +1908,101 @@ paths:
1793
1908
  '401': { $ref: '#/components/responses/Unauthorized' }
1794
1909
  '404': { $ref: '#/components/responses/NotFound' }
1795
1910
 
1911
+ # ── sandbox 镜像目录(补漏 2026-07-25)──────────────────────────────────────────────────────────────
1912
+ # ⚠️ 诚实记下本次的**范围**:只补了带 SDK 具名类型的这两条读面(`ImageIndexEntry`/`ImageSelectResult`)。
1913
+ # server 的 `/v1/images` 前缀下还有 register / :profile / digests/:digest 以及整个 bake 控制面
1914
+ # (`/v1/images/bakes*`,operator 与 runner 两种主体),SDK 侧目前用 `Record<string, unknown>` 表达、
1915
+ # 没有具名类型 ⇒ 本批不进 spec(补它们等于在这里新发明一份契约,应当先在 types.ts 具名)。**不是已覆盖**。
1916
+ /v1/images:
1917
+ parameters:
1918
+ - $ref: '#/components/parameters/PrincipalHeader'
1919
+ get:
1920
+ tags: [sandbox]
1921
+ operationId: imagesList
1922
+ x-status: live
1923
+ summary: Paginated sandbox-image catalog (principal-scoped visibility).
1924
+ description: >
1925
+ The READ-ONLY data source for a "pick a sandbox template" UI. Visibility is scoped to the request
1926
+ principal by the server. Gated on a configured image index — otherwise the route is not mounted.
1927
+ Paginate with `nextCursor` and treat the cursor as OPAQUE (never parse or synthesize one).
1928
+ parameters:
1929
+ - { name: profile, in: query, required: false, schema: { type: string } }
1930
+ - { name: status, in: query, required: false, schema: { type: string, enum: [building, published, deprecated, failed] } }
1931
+ - { name: capability, in: query, required: false, schema: { type: string }, description: 'filter to images having this capability (e.g. `browser`).' }
1932
+ - { name: latest, in: query, required: false, schema: { type: string, enum: ['true'] }, description: 'string `"true"` ⇒ latest-per-profile only.' }
1933
+ - { name: limit, in: query, required: false, schema: { type: integer } }
1934
+ - { name: cursor, in: query, required: false, schema: { type: string }, description: 'echo `nextCursor` from the previous page verbatim.' }
1935
+ responses:
1936
+ '200':
1937
+ description: 'Catalog page — `{ images, nextCursor? }`.'
1938
+ content:
1939
+ application/json:
1940
+ schema:
1941
+ type: object
1942
+ required: [images]
1943
+ additionalProperties: true
1944
+ properties:
1945
+ images:
1946
+ type: array
1947
+ items: { $ref: '#/components/schemas/ImageIndexEntry' }
1948
+ nextCursor: { type: string, description: 'OPAQUE — pass back as `cursor`. Absent on the last page.' }
1949
+ '401': { $ref: '#/components/responses/Unauthorized' }
1950
+
1951
+ /v1/images/select:
1952
+ parameters:
1953
+ - $ref: '#/components/parameters/PrincipalHeader'
1954
+ post:
1955
+ tags: [sandbox]
1956
+ operationId: imagesSelect
1957
+ x-status: live
1958
+ summary: Resolve a profile to its newest published digest (UI PREVIEW only).
1959
+ description: >
1960
+ Pure resolution, no build. 🔴 The result is a PREVIEW, NOT a binding credential: to actually run on this
1961
+ image, send `sandboxImageProfile` (a PROFILE) on the task request and let the server resolve + re-admit
1962
+ under a trusted principal. A caller that sends/caches/recomputes a digest turns a fail-closed admission
1963
+ into a fail-open one.
1964
+ requestBody:
1965
+ required: true
1966
+ content:
1967
+ application/json:
1968
+ schema:
1969
+ type: object
1970
+ required: [profile]
1971
+ additionalProperties: true
1972
+ properties:
1973
+ profile: { type: string, description: 'non-empty; missing/blank → 400.' }
1974
+ capabilitiesNeeded:
1975
+ type: array
1976
+ items: { type: string }
1977
+ description: 'capability names that MUST be satisfied by the resolved image, else 409.'
1978
+ responses:
1979
+ '200':
1980
+ description: The resolution preview.
1981
+ content:
1982
+ application/json:
1983
+ schema: { $ref: '#/components/schemas/ImageSelectResult' }
1984
+ '400':
1985
+ description: '`profile` missing/blank.'
1986
+ content:
1987
+ application/json:
1988
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1989
+ '401': { $ref: '#/components/responses/Unauthorized' }
1990
+ '404':
1991
+ description: 'No published image for that profile VISIBLE TO THIS PRINCIPAL (invisible and nonexistent are deliberately indistinguishable — no existence oracle).'
1992
+ content:
1993
+ application/json:
1994
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1995
+ '409':
1996
+ description: 'The resolved image does not satisfy `capabilitiesNeeded` — body carries `missing[]`.'
1997
+ content:
1998
+ application/json:
1999
+ schema:
2000
+ type: object
2001
+ additionalProperties: true
2002
+ properties:
2003
+ error: { type: string }
2004
+ missing: { type: array, items: { type: string } }
2005
+
1796
2006
  /metrics/summary:
1797
2007
  get:
1798
2008
  tags: [metrics]
@@ -1939,6 +2149,27 @@ components:
1939
2149
  Primary routing axis (one image serves many; unknown → "default"). Selects model/tools/skills/prompt.
1940
2150
  examples: ['default', 'oa', 'code-review', 'team']
1941
2151
 
2152
+ ScenarioDetail:
2153
+ # 新增(2026-07-25 回填批):`GET /v1/capabilities/scenarios/{name}` 此前**整条路径**都不在本文件里
2154
+ # (只有 `Capabilities.scenarios` 那个名字数组)。形按 server 亲读取证(`http/server.ts:1517-1527`)。
2155
+ type: object
2156
+ description: >
2157
+ One scenario's detail card (`GET /v1/capabilities/scenarios/{name}`). The LIST of names rides
2158
+ `Capabilities.scenarios` instead. `source`/`builtin` = builtin vs center-config overlay; `toolset` names
2159
+ the tool bundle while `tools` are the resolved tool names; `promptSummary` is a human-readable prompt
2160
+ DIGEST (never the full prompt); `enabled` = a center scenario can be declared-but-disabled.
2161
+ required: [name, source, builtin, summary, toolset, tools, promptSummary, enabled]
2162
+ additionalProperties: true
2163
+ properties:
2164
+ name: { type: string }
2165
+ source: { type: string, enum: [builtin, center] }
2166
+ builtin: { type: boolean }
2167
+ summary: { type: string }
2168
+ toolset: { type: string }
2169
+ tools: { type: array, items: { type: string } }
2170
+ promptSummary: { type: string, description: 'a human-readable prompt DIGEST — NOT the full prompt.' }
2171
+ enabled: { type: boolean }
2172
+
1942
2173
  RunStatus:
1943
2174
  type: string
1944
2175
  enum: [running, completed, failed, suspended, needs_review]
@@ -1997,6 +2228,58 @@ components:
1997
2228
  # passes them through rather than dropping, so consumers must not assume this list is exhaustive.
1998
2229
  additionalProperties: true
1999
2230
 
2231
+ TaskAgentDefinition:
2232
+ # 新增(2026-07-25 回填批)—— 取代 `TaskRequest.agents.items` 的 OPEN 对象(理由见那里的旁注)。
2233
+ type: object
2234
+ description: >
2235
+ One per-task subagent definition (`TaskRequest.agents[]` item) — mirrors core `AgentDefinition` verbatim.
2236
+ 🔴 The server WHITELISTS exactly these keys: anything else is a 400 on the strict lane (so an
2237
+ unrecognised key is a client bug, not a passthrough).
2238
+ required: [name]
2239
+ properties:
2240
+ name: { type: string, description: 'the `subagent_type` the model passes to Task. Non-empty, ≤64 chars, UNIQUE per request.' }
2241
+ whenToUse: { type: string, description: 'when the model should delegate to this agent (shown in the Task tool''s agent list). ≤4096 chars.' }
2242
+ whenToUseLean: { type: string, description: 'compact variant of `whenToUse` for lean tool listings. ≤4096 chars.' }
2243
+ allowTools: { type: array, items: { type: string }, description: 'allowlist of tool names. Omit ⇒ inherit ALL tools.' }
2244
+ denyTools: { type: array, items: { type: string }, description: 'denylist (SUBTRACTIVE — applied after `allowTools`).' }
2245
+ skills:
2246
+ type: array
2247
+ description: 'skills preloaded into the agent.'
2248
+ items:
2249
+ type: object
2250
+ required: [name, description, content]
2251
+ additionalProperties: true
2252
+ properties:
2253
+ name: { type: string }
2254
+ description: { type: string }
2255
+ content: { type: string }
2256
+ manifest: { type: object, additionalProperties: true }
2257
+ files:
2258
+ type: array
2259
+ items:
2260
+ type: object
2261
+ required: [path, content]
2262
+ additionalProperties: true
2263
+ properties: { path: { type: string }, content: { type: string } }
2264
+ background: { type: boolean, description: 'run the agent in the background (the parent turn continues).' }
2265
+ isolation: { type: string, enum: [worktree], description: '`worktree` = run the agent in an isolated git worktree.' }
2266
+ model:
2267
+ description: >
2268
+ Shell-resolved REAL model name/id — or a Model object with a string `id`. 🔴 NOT an alias: the server
2269
+ does no translation. Omit ⇒ inherit the parent model.
2270
+ oneOf:
2271
+ - type: string
2272
+ - type: object
2273
+ required: [id]
2274
+ additionalProperties: true
2275
+ properties: { id: { type: string } }
2276
+ thinking: { type: string, enum: [off, minimal, low, medium, high, xhigh, max] }
2277
+ systemPrompt: { type: string, description: 'the agent''s system prompt. ≤65536 chars.' }
2278
+ maxTurns: { type: integer, minimum: 1, description: 'max agent turns before the subagent is stopped.' }
2279
+ memory: { type: object, additionalProperties: true, description: 'memory spec (`{ scope?, scopes?, writeScope?, enabled?, scopeContract? }`).' }
2280
+ observer: { type: string, description: 'observer prompt (agent-observes-agent lane). ≤4096 chars.' }
2281
+ observerMessage: { type: string, description: 'message template the observer sends. ≤4096 chars.' }
2282
+
2000
2283
  TaskRequest:
2001
2284
  type: object
2002
2285
  description: >
@@ -2156,14 +2439,15 @@ components:
2156
2439
  cwd: { type: string, description: Working directory for the execution env. }
2157
2440
  selfOrchestration: { type: boolean, description: Permit S8 self-orchestration (run_workflow) inside this task. }
2158
2441
  enableFork: { type: boolean, description: Permit session forking from inside this task. }
2442
+ # 原注是「形状归 SDK 的 TaskAgentDefinition 所有,这里刻意保持 OPEN 而不重复一遍」。2026-07-25 推翻该选择,
2443
+ # 理由是那条推理只对 TS 消费方成立:TS 消费方本来就 import 得到 `TaskAgentDefinition`,从本文件生成客户端的
2444
+ # 消费方(本文件存在的全部意义)拿到的却是一个「任意键的对象」——而这是**请求体**,写错键会被 server 的严格
2445
+ # 通道 400 掉,恰恰是最需要 schema 的地方。⚠️ 代价诚实记下:这确实成了第二处需要跟着 core `AgentDefinition`
2446
+ # 走的声明。因此反射式 drift 门(`test/spec-field-drift-gate.test.ts`)把 `TaskAgentDefinition` 纳入了逐字段
2447
+ # 比对 —— types.ts 加字段而这里不加,门就会红。这就是"重复"这次可以接受的原因:它被机器看住了。
2159
2448
  agents:
2160
2449
  type: array
2161
- items:
2162
- type: object
2163
- description: >
2164
- One per-task sub-agent definition. Shape owned by `TaskAgentDefinition` in the SDK types; kept OPEN
2165
- here rather than duplicated (same reasoning as `settings`).
2166
- additionalProperties: true
2450
+ items: { $ref: '#/components/schemas/TaskAgentDefinition' }
2167
2451
  description: Per-task sub-agent definitions (roster additions scoped to this task only).
2168
2452
  interactiveTools: { type: boolean, description: Mount interactive (prompting) tools. }
2169
2453
  retainBackgroundProcesses: { type: boolean, description: Keep background processes alive past the turn. }
@@ -2371,6 +2655,93 @@ components:
2371
2655
  error: { type: string }
2372
2656
  retainedFrom: { type: integer, description: Lowest event seq still retained; resume events from here. }
2373
2657
 
2658
+ ImageIndexEntry:
2659
+ # 新增(2026-07-25 回填批):sandbox 镜像目录条目。此前 `/v1/images` 整条路径不在本文件里。
2660
+ # 🔴 为什么 21 个字段**全部** required:server 侧 `plugins/tidb-image-index.ts` 的 `mapRow()` 对每一行
2661
+ # 恒全量赋值 —— nullable 的字段也至少给 `null`,**从不省略键**;`GET /v1/images` 的 handler 把 entries
2662
+ # 原样 sendJson、零裁剪。所以「可空」在这里表达为 `type: [..., 'null']`,而不是从 required 里拿掉。
2663
+ type: object
2664
+ description: >
2665
+ One sandbox-image catalog entry (`GET /v1/images` → `images[]`). Visibility is principal-scoped by the
2666
+ server. READ-ONLY data source for a "pick a sandbox template" UI.
2667
+ required:
2668
+ [id, profile, bands, repo, tag, digest, toolchainVersions, capabilities, podContract, sizeBytes, status,
2669
+ visibility, tenantId, manifestSha, recipeGitSha, generatorVersion, buildDate, supersedes, signed,
2670
+ createdAt, updatedAt]
2671
+ additionalProperties: true
2672
+ properties:
2673
+ id: { type: string, description: 'deterministically derived from (repo, digest) — there is no explicit-id upsert path, so an id collision ⟺ a (repo, digest) collision.' }
2674
+ profile: { type: string }
2675
+ bands: { type: array, items: { type: string } }
2676
+ repo: { type: string }
2677
+ tag: { type: string }
2678
+ digest:
2679
+ type: string
2680
+ description: >
2681
+ The immutable digest a running sandbox is built from. 🔴 DISPLAY ONLY — a caller must NEVER send this
2682
+ back to bind an image (bind by `profile`; the server re-resolves + re-admits under a trusted
2683
+ principal, because trusting a caller-supplied digest would fail OPEN).
2684
+ toolchainVersions: { type: object, additionalProperties: { type: string } }
2685
+ capabilities:
2686
+ type: object
2687
+ additionalProperties: true
2688
+ properties:
2689
+ browser: { type: boolean }
2690
+ db: { type: boolean }
2691
+ nestedBuild: { type: boolean, description: 'can it build container images (back-compat boolean).' }
2692
+ nestedBuildMode:
2693
+ type: string
2694
+ enum: [none, docker-cli-only, rootless-buildkit, privileged-dind]
2695
+ description: 'HOW it nests (the posture). Refines `nestedBuild`; absent ⇒ fall back to the boolean.'
2696
+ podContract:
2697
+ type: object
2698
+ additionalProperties: true
2699
+ properties:
2700
+ devShmMB: { type: integer }
2701
+ minMemMB: { type: integer }
2702
+ minCpu: { type: string }
2703
+ readyTimeoutSec: { type: integer }
2704
+ securityContext: { type: object, additionalProperties: true }
2705
+ sizeBytes: { type: ['integer', 'null'] }
2706
+ status: { type: string, enum: [building, published, deprecated, failed] }
2707
+ visibility: { type: string, enum: [public, tenant] }
2708
+ tenantId: { type: ['string', 'null'] }
2709
+ manifestSha: { type: ['string', 'null'] }
2710
+ recipeGitSha: { type: ['string', 'null'] }
2711
+ generatorVersion: { type: ['string', 'null'] }
2712
+ buildDate: { type: ['string', 'null'], description: 'ISO date-time string (the server projects Date/string uniformly to ISO).' }
2713
+ supersedes: { type: ['string', 'null'], description: 'version-lineage anchor (optionally written by the server''s setStatus; semantics not further specified server-side — pass through).' }
2714
+ signed: { type: boolean }
2715
+ createdAt: { type: string, description: ISO date-time. }
2716
+ updatedAt: { type: string, description: ISO date-time. }
2717
+
2718
+ ImageSelectResult:
2719
+ # 新增(2026-07-25 回填批)。
2720
+ type: object
2721
+ description: >
2722
+ `POST /v1/images/select` resolution result — a PREVIEW of "which digest/capabilities does this profile
2723
+ resolve to" for the UI. 🔴 NOT a binding credential: to actually bind, send `sandboxImageProfile` on the
2724
+ task request and let the server resolve under a trusted principal.
2725
+ additionalProperties: true
2726
+ properties:
2727
+ profile: { type: string }
2728
+ digest: { type: string }
2729
+ repo: { type: string }
2730
+ ref:
2731
+ type: string
2732
+ description: >
2733
+ `${repo}@${digest}` — always present on a 200. This is the value a caller threads into the kata
2734
+ adapter's per-sandbox image (the server handler says so itself), i.e. it is load-bearing, not decorative.
2735
+ podContract: { type: object, additionalProperties: true }
2736
+ capabilities:
2737
+ type: object
2738
+ additionalProperties: true
2739
+ properties:
2740
+ browser: { type: boolean }
2741
+ db: { type: boolean }
2742
+ nestedBuild: { type: boolean }
2743
+ manifestSha: { type: string }
2744
+
2374
2745
  QuotaError:
2375
2746
  type: object
2376
2747
  description: 429 per-principal cumulative-quota body (`quotaExceeded`).
@@ -2867,6 +3238,46 @@ components:
2867
3238
  endedAt: { type: integer, description: 'End epoch ms (absent while running).' }
2868
3239
  createdAt: { type: integer, description: 'Record-creation epoch ms (listByScope sort key).' }
2869
3240
 
3241
+ WorkflowActivityBeat:
3242
+ # 新增(2026-07-25 回填批)—— workflow 监视面 "Activity · last N of M tool calls" 的一拍。
3243
+ type: object
3244
+ description: >
3245
+ One tool-call beat on a workflow agent's BOUNDED activity tail (source = core `ToolActivity`, projected
3246
+ verbatim by the service detail handler).
3247
+
3248
+ 🔴 `arg` is a SHORT primary-arg SUMMARY (command→name, path→basename, url→origin+path), secret-scrubbed
3249
+ and truncated (~80 code points) by core. It exists for the monitor's `Read(path)` / `Bash(grep …)`
3250
+ display — it is NOT the full args. RENDER it; NEVER re-feed it to a model.
3251
+ additionalProperties: true
3252
+ properties:
3253
+ phase: { type: string, enum: [start, end] }
3254
+ toolCallId: { type: string }
3255
+ toolName: { type: string, description: 'the monitor renders `${toolName}(${arg})`.' }
3256
+ arg: { type: string, description: 'SHORT, secret-scrubbed, ~80-code-point summary (set on `phase:"start"`). UNTRUSTED display text.' }
3257
+ isError: { type: boolean, description: 'set on `phase:"end"` — whether the tool call errored.' }
3258
+
3259
+ WorkflowAgentRow:
3260
+ # 新增(2026-07-25 回填批)—— 取代 `WorkflowRun.agents` 的 `items: {}`(零形状)。
3261
+ type: object
3262
+ description: >
3263
+ One agent-run row in a workflow's detail. The shape is PERMISSIVE — read the known render fields below,
3264
+ tolerate the rest as the projection evolves.
3265
+ additionalProperties: true
3266
+ properties:
3267
+ label: { type: string, description: 'display label.' }
3268
+ status: { type: string, description: 'lifecycle status (the input vocabulary the display derivation reads).' }
3269
+ phase: { type: string, description: 'the phase title this agent ran under (groups it into a phase bucket).' }
3270
+ model: { type: string, description: 'per-agent model DISPLAY label (e.g. "Opus 4.8 (1M context)"), not an id.' }
3271
+ tokens: { type: integer }
3272
+ turns: { type: integer }
3273
+ toolCalls: { type: integer, description: 'total tool calls — the "M" in "last N of M tool calls".' }
3274
+ activity:
3275
+ type: array
3276
+ description: 'the "Activity" tail — the LAST-N tool-call beats (bounded).'
3277
+ items: { $ref: '#/components/schemas/WorkflowActivityBeat' }
3278
+ prompt: { type: string, description: 'what the worker was ASKED (core-redacted + bounded). UNTRUSTED display text.' }
3279
+ output: { type: string, description: 'the worker''s final OUTPUT (core-redacted + bounded). UNTRUSTED display text.' }
3280
+
2870
3281
  WorkflowRun:
2871
3282
  type: object
2872
3283
  description: >
@@ -2881,7 +3292,13 @@ components:
2881
3292
  description: { type: string, description: "The workflow script's declared `meta.description` (one-liner)." }
2882
3293
  status: { $ref: '#/components/schemas/WorkflowRunStatus' }
2883
3294
  phases: { type: array, description: 'Phase records (permissive).', items: {} }
2884
- agents: { type: array, description: 'Agent-run records (permissive).', items: {} }
3295
+ # `items: {}`(= 任意值,零形状)改为具名 $ref(2026-07-25 回填批):这是 workflow 监视面最吃重的数组,
3296
+ # 空 schema 等于对非 TS 消费方完全没描述过它。`WorkflowAgentRow` 仍是 PERMISSIVE(additionalProperties)——
3297
+ # 只钉住监视面真正 key 的那些字段,其余随投影演进。
3298
+ agents:
3299
+ type: array
3300
+ description: 'Agent-run records (permissive — read the known render fields, tolerate the rest).'
3301
+ items: { $ref: '#/components/schemas/WorkflowAgentRow' }
2885
3302
  stats:
2886
3303
  type: object
2887
3304
  description: 'Token stats — own at stats.tokens, nested at stats.nested.tokens (R-5 own/nested split).'
@@ -3198,27 +3615,79 @@ components:
3198
3615
  type: array
3199
3616
  items: { $ref: '#/components/schemas/InboxRow' }
3200
3617
 
3201
- AssistantTaskRow:
3618
+ AssistantGate:
3619
+ # 从 `AssistantTaskRow.gate` 的内联形提取为命名 schema(2026-07-25 回填批):内联的 schema 生成不出可复用
3620
+ # 的具名类型,非 TS 消费方只能拿到匿名对象;且反射式 drift 门只按「命名 schema ↔ 同名 exported interface」
3621
+ # 配对,内联形对它完全不可见 ⇒ 这里的字段漂移过去无人看守。
3622
+ type: object
3623
+ description: >
3624
+ ASSISTANT-WIRE-CONTRACT §3 — the active gate inlined on an `AssistantTask` row.
3625
+
3626
+ 🔴 LAYER NOTE (this schema and the SDK's `AssistantGate` type describe DIFFERENT layers, deliberately):
3627
+ this file documents the WIRE, where `severity`/`spentMicroUsd`/`deadline` are sent as EXPLICIT `null`
3628
+ when absent (server `http/server.ts` sends `severity: s.severity ?? null` — a present key holding null,
3629
+ NOT an omitted key). The SDK's TS type declares them `?: number` because the SDK FOLDS null → omitted
3630
+ key at its resource boundary (`resources/assistant.ts` `RawAssistantGate` + `normalizeGate()`), so a
3631
+ TS consumer never observes the null. Both are correct for their layer — do NOT "fix" either to match
3632
+ the other. A non-TS client generated from this file MUST tolerate the nulls itself.
3633
+ required: [kind]
3634
+ additionalProperties: true
3635
+ properties:
3636
+ kind:
3637
+ type: string
3638
+ description: >
3639
+ Same open set as `CheckpointSummary.gateKind` — the FLATTENED projection of core's
3640
+ `CheckpointGate`. (Nested-as-`gate` ⇒ the discriminator is named `kind`; flattened onto a row ⇒
3641
+ `gateKind`. Same value domain, different position — a naming CONVENTION, not an inconsistency.)
3642
+ enum: [human, irreversible_ask, resource_limit, needs_review, plan_review, task_done]
3643
+ x-open-enum: true # a future core kind must render generically, never crash — do not close this union
3644
+ severity:
3645
+ type: ['integer', 'null']
3646
+ enum: [1, 2, 3, 4, 5, null]
3647
+ description: >
3648
+ Risk tier. `null` (or, post-SDK-normalization, absent) = this gate kind carries no RiskDescriptor
3649
+ (resource_limit / needs_review / plan_review) — treat as "unknown", never as 0.
3650
+ spentMicroUsd:
3651
+ type: ['number', 'null']
3652
+ description: >
3653
+ Cumulative spend ALREADY INCURRED on the suspend chain (micro-USD). ⚠️ NOT the same concept as
3654
+ `costMicroUsd` ("what this thing cost") — never treat the two as aliases.
3655
+ deadline:
3656
+ type: ['integer', 'null']
3657
+ description: 'awaiting-human SLA deadline — epoch ms (the checkpoint family''s time convention; run-ledger rows use ISO strings instead).'
3658
+
3659
+ AssistantTask:
3660
+ # 重命名自 `AssistantTaskRow`(2026-07-25):SDK 的 exported interface 叫 `AssistantTask`,而权威已在
3661
+ # types.ts(见本文件顶注),spec 侧沿用另一个名字会让反射式 drift 门永远配不上对 ⇒ 以 TS 名为准对齐。
3202
3662
  type: object
3203
3663
  description: >
3204
3664
  ASSISTANT-WIRE-CONTRACT §3 — a scheduler-overview row = run-store `listRuns(owner)` joined with the core
3205
3665
  scheduler seam. ⚠️ `createdAt`/`updatedAt` are ISO date-time strings (unlike inbox/§1 epoch-ms).
3206
- required: [taskId, sessionId, status, needsAttention, createdAt, updatedAt]
3666
+ required: [taskId, sessionId, status, needsAttention, gate, createdAt, updatedAt]
3207
3667
  additionalProperties: true
3208
3668
  properties:
3209
3669
  taskId: { type: string }
3210
3670
  sessionId: { type: string }
3211
- status: { type: string, enum: [running, suspended] }
3212
- needsAttention: { type: boolean, description: '= suspended on a HITL gate.' }
3671
+ status:
3672
+ type: string
3673
+ # 🔴 `needs_review` 是 2026-07-25 补的:server 的行过滤器逐字是
3674
+ # `.filter(r => r.status === "running" || r.status === "suspended" || r.status === "needs_review")`
3675
+ # (`http/server.ts:4677`,已亲读取证)——本枚举此前只有前两个,是真陈旧,照它生成的客户端会把一个
3676
+ # 合法的一等 triage 态判成非法值。字段级 drift 门只比字段名、比不到枚举值,所以它没抓到这条。
3677
+ enum: [running, suspended, needs_review]
3678
+ description: >
3679
+ `running` — live. `suspended` — durably parked on a HITL gate (resolve via §4b resume / §4a decide).
3680
+ `needs_review` — a FIRST-CLASS triage state: a plan_review / dry-run review park (NOT `suspended`);
3681
+ carries `needsAttention:true` + `gate.kind` = `plan_review`/`needs_review` (severity absent) and
3682
+ sorts FIRST. 🔴 Resolve it via §4c `POST /v1/assistant/tasks/:id/plan_review`, NOT §4b resume.
3683
+ ⚠️ Distinct axis from `RunStatus.needs_review` (that one is a dry-run TERMINAL run status; this is a
3684
+ park state on the overview row) — same name, different axis.
3685
+ needsAttention: { type: boolean, description: '= parked on a HITL gate (`suspended` OR `needs_review`); server derives it as `!!gate`.' }
3213
3686
  gate:
3214
- type: ['object', 'null']
3215
- description: 'the active gate, or null. severity/spentMicroUsd/deadline may themselves be null on the wire.'
3216
- additionalProperties: true
3217
- properties:
3218
- kind: { type: string, description: 'same open set as gateKind.' }
3219
- severity: { type: ['integer', 'null'], enum: [1, 2, 3, 4, 5, null] }
3220
- spentMicroUsd: { type: ['number', 'null'] }
3221
- deadline: { type: ['integer', 'null'], description: epoch ms. }
3687
+ description: 'the active gate, or null when the row is not parked.'
3688
+ oneOf:
3689
+ - $ref: '#/components/schemas/AssistantGate'
3690
+ - type: 'null'
3222
3691
  createdAt: { type: string, description: ISO date-time. }
3223
3692
  updatedAt: { type: string, description: ISO date-time. }
3224
3693
 
@@ -3232,7 +3701,39 @@ components:
3232
3701
  properties:
3233
3702
  tasks:
3234
3703
  type: array
3235
- items: { $ref: '#/components/schemas/AssistantTaskRow' }
3704
+ items: { $ref: '#/components/schemas/AssistantTask' }
3705
+
3706
+ AssistantTaskStatus:
3707
+ # 新增(2026-07-25 回填批):§4b resume / §4c plan_review / §4d preempt 三条决策端点此前在本文件里都只声明
3708
+ # 了 `{status}` 一个字段——与 SDK 三批核查前的旧类型同样陈旧。下面的形按 server 源码逐字取证:
3709
+ # • resume / plan_review 200(共用 `driveResumeIntoRunLog`,`http/server.ts:7688-7696`):
3710
+ # `{ taskId, sessionId, status, errorCode?, errorMessage?, retriable? }`(后三个条件 spread ⇒ 缺则省键)
3711
+ # • preempt 202(`http/server.ts:4726/4734/4737` 三个出口):`{ taskId, status, note }`
3712
+ # ⚠️ `status` 在 202 上**并非恒为 `"preempting"`**:两个 no-op 出口分别回 `run.status` 与
3713
+ # `now?.status ?? "failed"` —— 所以这里不能把它写成 const。
3714
+ type: object
3715
+ description: >
3716
+ The response of the §4b/§4c/§4d decision endpoints (resume / plan_review / preempt). Union of the two
3717
+ real body shapes — `sessionId` appears only on resume/plan_review, `note` only on preempt; everything
3718
+ else is shared (absent keys are OMITTED, never null).
3719
+ required: [taskId, status]
3720
+ additionalProperties: true
3721
+ properties:
3722
+ taskId: { type: string, description: 'always present (all three response bodies carry it).' }
3723
+ status:
3724
+ type: string
3725
+ description: >
3726
+ OPEN string — do NOT narrow. Observed values include `completed` / `failed` / `suspended` /
3727
+ `needs_review` / `preempting`, plus whatever run status a no-op preempt echoes back.
3728
+ sessionId: { type: string, description: 'resume/plan_review ONLY — the session the decided checkpoint belongs to. Absent from the preempt body.' }
3729
+ errorCode: { type: string, description: 'present on a failed resume/plan_review (e.g. `checkpoint.reopen_failed`).' }
3730
+ errorMessage: { type: string, description: 'human-readable failure detail (server-side secret-redacted).' }
3731
+ retriable:
3732
+ type: boolean
3733
+ description: >
3734
+ `true` = the park is STILL PENDING (the server re-parked it), so re-fetching and deciding again is
3735
+ meaningful. Emitted only when the store CONFIRMED the reopen — never inferred.
3736
+ note: { type: string, description: 'preempt ONLY — human-readable explanation (incl. the honest "preempt is a no-op" cases). Absent from the resume/plan_review body.' }
3236
3737
 
3237
3738
  PlanReviewRequest:
3238
3739
  type: object
@@ -3309,6 +3810,23 @@ components:
3309
3810
  offset: { type: integer, description: Index of the first returned message within the full context. }
3310
3811
  total: { type: integer, description: Total messages available (so a client can page). }
3311
3812
 
3813
+ SessionMessageEnvelope:
3814
+ # 新增(2026-07-25 回填批):`GET /v1/sessions/{id}?message=<index>` 的单条消息展开支路。本文件此前连
3815
+ # `/v1/sessions/{id}` 的 query 参数都一个没声明,所以这条支路对生成式消费方等于不存在。
3816
+ type: object
3817
+ description: >
3818
+ The single-message expand leg — `GET /v1/sessions/{sessionId}?message=<index>` returns THIS instead of a
3819
+ `SessionRecord`. An out-of-range index → 404 (the error body carries `total`).
3820
+ required: [sessionId, index, message]
3821
+ additionalProperties: true
3822
+ properties:
3823
+ sessionId: { type: string }
3824
+ floorEntryId:
3825
+ type: ['string', 'null']
3826
+ description: 'the compaction floor this index is relative to, when one applies.'
3827
+ index: { type: integer, description: 'the requested message index (echoed).' }
3828
+ message: { description: 'OPEN — the message payload as stored; shape belongs to the transcript format, not to this envelope.' }
3829
+
3312
3830
  PromptEpoch:
3313
3831
  type: object
3314
3832
  description: >
@@ -3578,6 +4096,16 @@ components:
3578
4096
  - $ref: '#/components/schemas/Event_task_progress'
3579
4097
  - $ref: '#/components/schemas/Event_workspace_changed'
3580
4098
  - $ref: '#/components/schemas/Event_suspended'
4099
+ # 🔴 以下 6 臂是 2026-07-25 补的:`events.ts` 的 `AgentEvent` 联合有 21 个 `type` 字面量,本文件此前只声明
4100
+ # 15 个 —— 照本文件生成客户端的消费方会把 6 种合法帧当成非法值(事件面是最吃重的 wire 面,这个缺口比
4101
+ # 字段级 drift 更伤)。字段级 drift 门只按「命名 schema ↔ 同名 exported interface」配对,联合缺臂对它
4102
+ # 不可见,故这条只能靠人工核对 —— 补齐时是拿 `events.ts` 的 type 字面量集与本联合做机器 diff 得出的。
4103
+ - $ref: '#/components/schemas/Event_file_link'
4104
+ - $ref: '#/components/schemas/Event_prompt_assembled'
4105
+ - $ref: '#/components/schemas/Event_model_usage'
4106
+ - $ref: '#/components/schemas/Event_task_notification'
4107
+ - $ref: '#/components/schemas/Event_diagnostics'
4108
+ - $ref: '#/components/schemas/Event_steering_injected'
3581
4109
  - $ref: '#/components/schemas/Event_done'
3582
4110
  - $ref: '#/components/schemas/Event_failed'
3583
4111
  discriminator:
@@ -3596,6 +4124,12 @@ components:
3596
4124
  task_progress: '#/components/schemas/Event_task_progress'
3597
4125
  workspace_changed: '#/components/schemas/Event_workspace_changed'
3598
4126
  suspended: '#/components/schemas/Event_suspended'
4127
+ file_link: '#/components/schemas/Event_file_link'
4128
+ prompt_assembled: '#/components/schemas/Event_prompt_assembled'
4129
+ model_usage: '#/components/schemas/Event_model_usage'
4130
+ task_notification: '#/components/schemas/Event_task_notification'
4131
+ diagnostics: '#/components/schemas/Event_diagnostics'
4132
+ steering_injected: '#/components/schemas/Event_steering_injected'
3599
4133
  done: '#/components/schemas/Event_done'
3600
4134
  failed: '#/components/schemas/Event_failed'
3601
4135
 
@@ -3747,6 +4281,60 @@ components:
3747
4281
  properties:
3748
4282
  type: { const: workspace_changed }
3749
4283
  cwd: { type: string }
4284
+ CheckpointGate:
4285
+ # 从 `Event_suspended.gate` 的内联 oneOf 提取为命名 schema(2026-07-25 回填批)。两个理由:
4286
+ # ① 内联形生成不出可复用具名类型;② 反射式 drift 门只按「命名 schema ↔ 同名 exported interface」配对,
4287
+ # 内联形对它不可见 —— 这块 wire 契约过去没有任何机器看守。
4288
+ # 🔴 为什么同时写顶层 `properties` 和 `oneOf`:门解析有效属性集时只展开 `$ref`/`allOf`,**不认 `oneOf`**
4289
+ # (`test/spec-field-drift-gate.test.ts` 的 `effectiveSpecProps`)。若只留 oneOf,门会认为本 schema
4290
+ # 「没有属性」从而当作整个 schema 缺席 ⇒ 回填等于没做。两者是合取关系(JSON Schema 语义):顶层
4291
+ # properties 给出字段全集(=门的比对面),oneOf 继续施加逐 kind 的形状校验,互不削弱。
4292
+ # nullability 现在挂在**引用点**(`gate: oneOf[$ref, null]`),不再是本 schema 的一个 union 臂 ——
4293
+ # 命名类型自身是对象形,更贴 TS 的 `gate?: CheckpointGate | null`。
4294
+ type: object
4295
+ description: >
4296
+ CheckpointGate — OPEN DISCRIMINATED SET (design/80 D-0), mirrors core src/core/checkpoint-store.ts.
4297
+ 5 known kinds; consumers MUST branch on `kind` and render an UNKNOWN kind generically (core can mint a
4298
+ kind this contract predates — a core→wire contract test fails CI, not prod). Tool args / question text
4299
+ are NOT here — join the task's trace tool-call block via approvals' toolCallId.
4300
+ required: [kind]
4301
+ properties:
4302
+ kind:
4303
+ type: string
4304
+ description: >
4305
+ `human` — plain HITL approval (budget-auto-approvable). `irreversible_ask` — design/37
4306
+ irreversibility / design/70 egress tighten; NOT budgetable (load-bearing safety gate).
4307
+ `resource_limit` — design/74 cost/token/time slice boundary; resolve `{decision:"continue"}`.
4308
+ `needs_review` — design/76 dry-run/shadow → durable `needs_review` TERMINAL (post-prediction
4309
+ review, disjoint from the approval family). `task_done` — design/38 Path A background sub-task
4310
+ handle (door B never sees it).
4311
+ enum: [human, irreversible_ask, resource_limit, needs_review, task_done]
4312
+ x-open-enum: true # do not close this union — an unknown future kind is VALID on the wire
4313
+ reason: { type: string, description: 'present on human / irreversible_ask / resource_limit / needs_review (not task_done).' }
4314
+ toolName: { type: string, description: 'present on human / irreversible_ask.' }
4315
+ oneOf:
4316
+ - type: object # plain human approval (budget-auto-approvable)
4317
+ required: [kind, reason, toolName]
4318
+ properties: { kind: { const: human } }
4319
+ - type: object # design/37 irreversibility / design/70 egress safety-tighten — NOT budgetable (load-bearing safety gate)
4320
+ required: [kind, reason, toolName]
4321
+ properties: { kind: { const: irreversible_ask } }
4322
+ - type: object # design/74 resource (cost/token/time) slice boundary — resolve { decision: "continue" }
4323
+ required: [kind, reason]
4324
+ properties: { kind: { const: resource_limit } }
4325
+ - type: object # design/76 dry-run/shadow → durable needs_review TERMINAL (POST-prediction review, disjoint from approval family)
4326
+ required: [kind, reason]
4327
+ properties: { kind: { const: needs_review } }
4328
+ - type: object # design/38 Path A background sub-task handle (door B never sees it)
4329
+ required: [kind]
4330
+ properties: { kind: { const: task_done } }
4331
+ - type: object # OPEN SET — unknown future kind (e.g. design/80 D-B plan_review); render generically, never crash
4332
+ required: [kind]
4333
+ # 🔴 the pinned wire contract: exclude the 5 KNOWN kinds so a known kind matches ONLY its const member above
4334
+ # (not also this fallback) → keeps `oneOf` unambiguous AND preserves each kind's shape validation.
4335
+ # This fallback matches ONLY a future kind this contract predates. Add new known kinds to the enum.
4336
+ properties: { kind: { type: string, not: { enum: [human, irreversible_ask, resource_limit, needs_review, task_done] } } }
4337
+
3750
4338
  Event_suspended:
3751
4339
  type: object
3752
4340
  description: HITL — the run paused on an F4 gate or AskUserQuestion. Resolve via approvals.decide, then resume.
@@ -3754,34 +4342,225 @@ components:
3754
4342
  properties:
3755
4343
  type: { const: suspended }
3756
4344
  gate:
3757
- description: >
3758
- CheckpointGate — OPEN DISCRIMINATED SET (design/80 D-0), mirrors core src/core/checkpoint-store.ts.
3759
- 5 known kinds below; consumers MUST branch on `kind` and render an UNKNOWN kind generically (core
3760
- can mint a kind this contract predates — a core→wire contract test fails CI, not prod). Tool
3761
- args/question text are NOT here — join the task's trace tool-call block via approvals' toolCallId.
4345
+ description: 'the gate the run paused on, or null.'
3762
4346
  oneOf:
3763
- - type: object # plain human approval (budget-auto-approvable)
3764
- required: [kind, reason, toolName]
3765
- properties: { kind: { const: human }, reason: { type: string }, toolName: { type: string } }
3766
- - type: object # design/37 irreversibility / design/70 egress safety-tighten — NOT budgetable (load-bearing safety gate)
3767
- required: [kind, reason, toolName]
3768
- properties: { kind: { const: irreversible_ask }, reason: { type: string }, toolName: { type: string } }
3769
- - type: object # design/74 resource (cost/token/time) slice boundary — resolve { decision: "continue" }
3770
- required: [kind, reason]
3771
- properties: { kind: { const: resource_limit }, reason: { type: string } }
3772
- - type: object # design/76 dry-run/shadow → durable needs_review TERMINAL (POST-prediction review, disjoint from approval family)
3773
- required: [kind, reason]
3774
- properties: { kind: { const: needs_review }, reason: { type: string } }
3775
- - type: object # design/38 Path A background sub-task handle (door B never sees it)
3776
- required: [kind]
3777
- properties: { kind: { const: task_done } }
3778
- - type: object # OPEN SET — unknown future kind (e.g. design/80 D-B plan_review); render generically, never crash
3779
- required: [kind]
3780
- # 🔴 the pinned wire contract: exclude the 5 KNOWN kinds so a known kind matches ONLY its const member above
3781
- # (not also this fallback) → keeps `oneOf` unambiguous AND preserves each kind's shape validation.
3782
- # This fallback matches ONLY a future kind this contract predates. Add new known kinds to the enum.
3783
- properties: { kind: { type: string, not: { enum: [human, irreversible_ask, resource_limit, needs_review, task_done] } } }
4347
+ - $ref: '#/components/schemas/CheckpointGate'
3784
4348
  - type: 'null'
4349
+ ModelUsageDelta:
4350
+ # 新增(2026-07-25 回填批)。SDK 的 facade 审计把这个形状命名了一次(此前是在两个使用点各写一遍的匿名内联形:
4351
+ # `AgentEvent` 的 `model_usage` 臂 与 `done.result.stats.modelUsage` 的值类型,同源同形)。
4352
+ type: object
4353
+ description: >
4354
+ Per-model token/cost delta, keyed by model name in a `Record<string, ModelUsageDelta>`. Fields mirror
4355
+ `turn_end.usage`; ALL optional (open contract — tolerates future core fields).
4356
+ additionalProperties: true
4357
+ properties:
4358
+ inputTokens: { type: integer }
4359
+ outputTokens: { type: integer }
4360
+ cacheReadTokens: { type: integer }
4361
+ cacheWriteTokens: { type: integer }
4362
+ costMicroUsd:
4363
+ type: number
4364
+ description: >
4365
+ What THIS model's slice cost (micro-USD). ⚠️ Distinct concept from `spentMicroUsd`
4366
+ ("cumulative already-incurred spend") — never treat the two as aliases.
4367
+
4368
+ PromptManifest:
4369
+ # 新增(2026-07-25 回填批):server ≥1.219 的 prompt-assembly manifest。此前本文件完全没有它(全文 0 次提及),
4370
+ # 尽管它同时是 `AgentEvent.prompt_assembled` 臂和 trace 流 `prompt-assembled` 帧的载荷。
4371
+ type: object
4372
+ description: >
4373
+ The prompt-assembly manifest (core's `prompt.assembled` trace contract verbatim). Carries ONLY
4374
+ ids/digests/counts — NEVER prompt bodies (privacy boundary). 🔴 Hashes are PROCESS-SALTED: they are NOT
4375
+ comparable across worker restarts, so a UI must never diff them cross-process. `constitution`/`blocks`/
4376
+ `totalChars` are the v1 face; `sections`/`tools` are the v2 ADDITIVE face (absent on a v1-only engine).
4377
+ ⚠️ One task can carry MULTIPLE manifests — a cascade/verify run re-prepares under one taskId and every
4378
+ rung's manifest is persisted in emission order.
4379
+ required: [constitution, blocks, totalChars]
4380
+ additionalProperties: true
4381
+ properties:
4382
+ constitution:
4383
+ type: string
4384
+ description: 'who owned the constitution layer — `core` is the steady state; anything else is worth eyes.'
4385
+ enum: [core, replaced, provider-assembled, legacy]
4386
+ x-open-enum: true
4387
+ blocks:
4388
+ type: array
4389
+ items:
4390
+ type: object
4391
+ required: [id, chars, hash]
4392
+ additionalProperties: true
4393
+ properties:
4394
+ id: { type: string }
4395
+ chars: { type: integer }
4396
+ hash: { type: string, description: 'process-salted digest — never compare across restarts.' }
4397
+ sections:
4398
+ type: array
4399
+ description: 'v2 additive face — absent on a v1-only engine.'
4400
+ items:
4401
+ type: object
4402
+ required: [id, slot, carrier, cadence, cacheClass, chars, hash]
4403
+ additionalProperties: true
4404
+ properties:
4405
+ id: { type: string }
4406
+ slot: { type: string }
4407
+ carrier: { type: string }
4408
+ cadence: { type: string }
4409
+ cacheClass: { type: string }
4410
+ chars: { type: integer }
4411
+ hash: { type: string }
4412
+ tools:
4413
+ type: array
4414
+ description: 'v2 additive face — the tool contract summary.'
4415
+ items:
4416
+ type: object
4417
+ required: [wireName, aliases, contractId, implementationRevision, cardId, shapeDigest, wireSchemaDigest]
4418
+ additionalProperties: true
4419
+ properties:
4420
+ wireName: { type: string }
4421
+ aliases: { type: array, items: { type: string } }
4422
+ contractId: { type: string }
4423
+ implementationRevision: { type: string }
4424
+ cardId: { type: string }
4425
+ shapeDigest: { type: string }
4426
+ wireSchemaDigest: { type: string }
4427
+ totalChars: { type: integer }
4428
+
4429
+ Event_file_link:
4430
+ type: object
4431
+ description: >
4432
+ The engine SENT THE USER A FILE via a presigned link (probe `capabilities().sendUserFile` before
4433
+ rendering affordances). 🔴 `url` EXPIRES after `ttlSec` seconds — render a fetch-soon affordance, never
4434
+ persist the URL as durable.
4435
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
4436
+ required: [type, url, filename, size, ttlSec, status]
4437
+ properties:
4438
+ type: { const: file_link }
4439
+ url: { type: string, description: 'presigned download URL — EXPIRING, do not persist.' }
4440
+ filename: { type: string }
4441
+ size: { type: integer }
4442
+ ttlSec: { type: integer }
4443
+ status: { type: string, enum: [normal, proactive], description: '`normal` = a turn deliverable; `proactive` = a mid-run push.' }
4444
+ caption: { type: string }
4445
+ display: { type: string, enum: [render, attach], description: 'absent = the consumer''s default.' }
4446
+
4447
+ Event_prompt_assembled:
4448
+ type: object
4449
+ description: >
4450
+ DURABLE only (server ≥1.219) — the prepare's prompt-assembly manifest, FLATTENED onto the frame (the
4451
+ manifest's own keys sit at the top level next to `type`, they are NOT nested under a `manifest` key).
4452
+ Lands AHEAD of the first event of the turn it prepared. ⚠️ MULTIPLE per task on cascade/verify runs.
4453
+ allOf:
4454
+ - $ref: '#/components/schemas/PromptManifest'
4455
+ required: [type]
4456
+ properties:
4457
+ type: { const: prompt_assembled }
4458
+
4459
+ Event_model_usage:
4460
+ type: object
4461
+ description: >
4462
+ Per-model usage delta (live-verified on canary). `usage` is keyed BY MODEL NAME — same source as
4463
+ `done.result.stats.modelUsage` (lets a multi-model / role-routed run attribute spend per model).
4464
+ required: [type, usage]
4465
+ properties:
4466
+ type: { const: model_usage }
4467
+ usage:
4468
+ type: object
4469
+ additionalProperties: { $ref: '#/components/schemas/ModelUsageDelta' }
4470
+
4471
+ Event_task_notification:
4472
+ type: object
4473
+ description: >
4474
+ A BACKGROUND task (bg agent / bash / monitor / workflow / external) settled or emitted a batch while THIS
4475
+ run was live. The engine ALREADY injected the `<task-notification>` into the model in-process — this frame
4476
+ is the UI/display projection only. `partial:true` = a KILLED child's salvaged remnant (render as
4477
+ incomplete). `seq` = stop-cycle/batch counter (dedup key together with task_id + status).
4478
+ 🔴 summary/result/lines/diagnostics are UNTRUSTED redacted free text — render, never re-feed to a model.
4479
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
4480
+ required: [type, task_id, task_type, status, summary]
4481
+ additionalProperties: true
4482
+ properties:
4483
+ type: { const: task_notification }
4484
+ task_id: { type: string }
4485
+ task_type: { type: string }
4486
+ status: { type: string }
4487
+ summary: { type: string, description: 'UNTRUSTED display text.' }
4488
+ sessionId: { type: string }
4489
+ toolUseId: { type: string }
4490
+ seq: { type: integer }
4491
+ source: { type: string }
4492
+ lines: { type: array, items: { type: string } }
4493
+ stoppedBy: { type: string }
4494
+ exitCode: { type: integer }
4495
+ partial: { type: boolean, description: '`true` = a KILLED child''s salvaged remnant — render as incomplete.' }
4496
+ diagnostics: { type: string }
4497
+ recentSteps:
4498
+ type: array
4499
+ items:
4500
+ type: object
4501
+ required: [tool, target, outcome]
4502
+ additionalProperties: true
4503
+ properties: { tool: { type: string }, target: { type: string }, outcome: { type: string } }
4504
+ editedFiles:
4505
+ type: array
4506
+ items:
4507
+ type: object
4508
+ required: [path, edits]
4509
+ additionalProperties: true
4510
+ properties: { path: { type: string }, edits: { type: integer } }
4511
+ resumable: { type: boolean }
4512
+ result: { type: string, description: 'UNTRUSTED display text.' }
4513
+ output_file: { type: string }
4514
+ usage: { description: 'open shape — usage rollup of the background task.' }
4515
+
4516
+ Event_diagnostics:
4517
+ type: object
4518
+ description: >
4519
+ NEW LSP diagnostics drained at a turn boundary (server-side redacted). The model already got the context
4520
+ message in-process; this frame is the RENDER source.
4521
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
4522
+ required: [type, files]
4523
+ additionalProperties: true
4524
+ properties:
4525
+ type: { const: diagnostics }
4526
+ files:
4527
+ type: array
4528
+ items:
4529
+ type: object
4530
+ required: [uri, diagnostics]
4531
+ additionalProperties: true
4532
+ properties:
4533
+ uri: { type: string }
4534
+ diagnostics:
4535
+ type: array
4536
+ items:
4537
+ type: object
4538
+ required: [message]
4539
+ additionalProperties: true
4540
+ properties:
4541
+ message: { type: string }
4542
+ severity: { type: integer }
4543
+ range: { description: 'open shape (LSP Range).' }
4544
+ code: { type: ['string', 'integer'] }
4545
+ source: { type: string }
4546
+ isNew: { type: boolean }
4547
+
4548
+ Event_steering_injected:
4549
+ type: object
4550
+ description: >
4551
+ A mid-run system-reminder core injected into the model's context (todo / file-change / date-roll /
4552
+ skills-delta / …). The model already got the injected text in-process; this frame is the RENDER source
4553
+ (`source` picks the icon/label, `preview` is the truncated display text — server-side redacted).
4554
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
4555
+ required: [type, source, preview]
4556
+ additionalProperties: true
4557
+ properties:
4558
+ type: { const: steering_injected }
4559
+ source:
4560
+ type: string
4561
+ description: 'OPEN string — deliberately NOT the closed core enum, so a future core source value cannot runtime-crash a strict switch.'
4562
+ preview: { type: string, description: 'truncated, redacted display text — UNTRUSTED.' }
4563
+
3785
4564
  Event_done:
3786
4565
  type: object
3787
4566
  description: Carries the full TaskResult (output string at .result, stats at .stats) — service Drift 4.