@kanyun-ai-infra/agenthub 1.12.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  本文件从 1.12.0 开始记录;更早的版本见 git 历史中的 `chore(agenthub-sdk): release X.Y.Z` 提交。
4
4
 
5
+ ## 1.13.0
6
+
7
+ - 浏览器实时语音支持可选 `onEnded({ callId, reason })`:识别通话终态帧 `call_ended` 或关闭码 `4000`,清理采集与播放并停止自动接手;时长上限映射为 `duration_limit`,未知原因映射为 `unknown`。回调每通至多一次,本地 `stop()` 不触发;终态到达后可以手动开始新通话。
8
+
9
+ - `NO_RESPONSE` 通知新增可选 `contextReset: true`:表示网关已切换到新的模型会话,模型不再持有此前上下文;平台回合历史不变。
10
+ - 随包类型声明移除内部包名与仓库路径,公开方法签名不因这项注释整理而改变。
11
+
12
+ ### 随共享契约内联的变化
13
+
14
+ - Slack 渠道声明新增可选 `approverUserIds`,用于指定可在线程里批准或拒绝工具调用的用户;最多 64 项且不能重复。slack 声明携带有效的 `approverUserIds` 时,SDK 1.12.1 的 `publishRevision` 会在本地校验时报 `unrecognized_keys`;1.13.0 接受该字段。
15
+
16
+ ### 迁移说明
17
+
18
+ - 使用实时语音的接入方须升级到 `@kanyun-ai-infra/agenthub@1.13.0` 才能获得 `onEnded` 和到达时长上限后停止自动接手的行为。旧 SDK 1.11.0 / 1.12.x 会忽略 `call_ended`,收到关闭码 `4000` 后仍自动重连,最终报 `RECONNECT_EXHAUSTED`;仅升级网关不能修复旧客户端。
19
+ - `onEnded` 是可选回调;无需处理结束通知的接入方也会自动停止重连。`adaptiveConnect: false` 不会关闭这项通话终态处理。
20
+
5
21
  ## 1.12.1
6
22
 
7
23
  修复 1.12.0 的已知问题,只改浏览器实时语音入口 `@kanyun-ai-infra/agenthub/realtime` 的一项检查;主入口与类型声明和 1.12.0 相同。
package/README.md CHANGED
@@ -295,6 +295,9 @@ const voice = createRealtimeVoiceClient({
295
295
  },
296
296
  onState: (state) => renderState(state), // listening / thinking / speaking
297
297
  onTurnEnd: ({ turnSeq, turnId }) => finishTurn({ turnSeq, turnId }),
298
+ onEnded: ({ reason }) => showVoiceEnded(
299
+ reason === "duration_limit" ? "通话已结束:达到时长上限" : "通话已结束",
300
+ ),
298
301
  onError: ({ code, retryable }) => showVoiceError({ code, retryable }),
299
302
  });
300
303
 
@@ -314,8 +317,12 @@ stopButton.onclick = () => { void voice.stop(); };
314
317
 
315
318
  `stop()` 立即停止麦克风,发送本轮最终播放回执,再发 `end` 并关闭连接;Worklet 无响应时最多等待 200 ms,使用最后确认的播放统计。页面卸载同步清理。播放回执按 `turnSeq` 归属,`playedMs` 计实际渲染样本,不计排队音频;`firstPlayedAt` 是 Unix 毫秒,尚未播放时为 `null`。`turn_end` 表示业务回合收尾,不是音频 EOF。SDK 记住提前到达的回合终态,在播放排空时发送 final;迟到尾块沿用首次播放时刻并累加 playedMs,再补累计 final。记录保留最近 128 轮,尚在播放队列中的轮不会被淘汰。网关的文本截止不受晚回执影响,晚回执用于录音统计。
316
319
 
320
+ 收到当前通话的 `call_ended` 或专用关闭码 `4000` 时,SDK 停止自动接手、清理计时器、停止麦克风并清空播放,随后调用一次可选的 `onEnded({ callId, reason })`。首期原因是 `duration_limit`,未知原因映射为 `unknown`;关闭码判定不依赖 `wasClean`。只有关闭帧先于 `ready` 到达且没有通话 ID 时,`callId` 缺省;尚未完成的 `start()` 以既有 `REALTIME_STOPPED` 拒绝,不触发 `onError`。本地 `stop()` 不触发 `onEnded`,不等服务端回声。结束后可手动 `start()` 开新通话。`turn_end` 仍只代表一轮结束。
321
+
322
+ SDK 1.13.0 起提供此行为。旧版 1.11.0 / 1.12.0 / 1.12.1 忽略 `call_ended`,收到关闭后仍按原逻辑接手,不能仅靠更新网关修复旧客户端,接入方须升级到 1.13.0。普通 `1000`、`1006` 与 `reconnecting` 保留原恢复行为;若终态 JSON 和关闭帧都未送达,仍可能按网络断线接手。
323
+
317
324
  断线或收到 `reconnecting` 时保留播放队列、暂停采集,换新票并带原 `callId` 重连。每通话最多自动接手 5 次,退避从 250 ms 增至 4 s;取票与 WebSocket upgrade 最多等待 10 s,open 后另等 ready 最多 30 s,音频初始化最多 30 s。WebSocket 握手已验票,SDK 立即处理下行消息;转写、状态回调与播放可能早于 start() 完成。ready 只控制麦克风上行和 start() 完成,interrupted 始终立即清空播放。`no_response` 仅以 `NO_RESPONSE` 可重试通知上报,不主动接手通话。无法恢复时释放资源并报不可重试错误,可由用户重新开始。主动 stop 后再次 start 是新通话。
318
- 浏览器子入口同时导出只读 `REALTIME_RECOVERY_REASONS` 和 `RealtimeRecoveryReason` 类型。实时 `NO_RESPONSE` 可重试通知可附带 `reason`:`input_not_received`(本句未获得回答)、`response_incomplete`(回答未完成)、`connection_interrupted`(空闲连接中断)。调用方应显示对应提示并允许重试;旧网关不带 reason 时沿用通用提示,未知 reason 被忽略。reason 不改变 SDK 的 stop/自动接手策略,也不表示丢失输入已被重放。已发布 1.11.0 忽略该可选字段,但仍正常通知 NO_RESPONSE。
325
+ 浏览器子入口同时导出只读 `REALTIME_RECOVERY_REASONS` 和 `RealtimeRecoveryReason` 类型。实时 `NO_RESPONSE` 可重试通知可附带 `reason`:`input_not_received`(本句未获得回答)、`response_incomplete`(回答未完成)、`connection_interrupted`(空闲连接中断)。调用方应显示对应提示并允许重试;旧网关不带 reason 时沿用通用提示,未知 reason 被忽略。reason 不改变 SDK 的 stop/自动接手策略,也不表示丢失输入已被重放。已发布 1.11.0 忽略该可选字段,但仍正常通知 NO_RESPONSE。网关在 GoAway 截止时换新模型会话、无法保留此前对话上下文时,同一通知另带 `contextReset: true`:通话继续,平台回合历史不变,调用方可提示用户模型已不记得之前的对话;已发布 1.11.0 / 1.12.0 忽略该字段。
319
326
 
320
327
  首次建连(第一次收到 ready 之前)的 WebSocket 若在 open 之前关闭,SDK 依次等待 500 ms、1 s、2 s,每次重新取票再连,最多重试 3 次,每次以可重试通知 `CONNECT_RETRYING` 上报;第 3 次重试仍被拒时报 `CONNECTION_FAILED`。浏览器不暴露握手的 HTTP 状态码,网关排空或满额时的 503 与 401 等其他拒绝无法区分,因此都按此重试;票是一次性的,网关可能已经兑票,所以每次重试都重新取票。以下情况不重试:`getTicket()` 抛错、票格式错误、open 之后的协议不符或网关的不可重试错误、open 之后 ready 之前断开、10 s 内未完成取票与 upgrade。首次建连重试与通话接手的 5 次额度分开计数。`CONNECT_RETRYING` 与 `TICKET_GATEWAY_URL_*` 都是 `retryable: true` 的通知,与 `RECONNECTING` 一样不表示通话失败。
321
328
 
package/dist/index.cjs CHANGED
@@ -4402,6 +4402,7 @@ var SlackChannelInboundSchema = ChannelInboundSchema.extend({
4402
4402
  allowedBotIds: SlackAllowedBotIdsSchema.optional()
4403
4403
  });
4404
4404
  var SLACK_CHANNEL_ADMIN_USER_IDS_MAX_LENGTH = 64;
4405
+ var SLACK_CHANNEL_APPROVER_USER_IDS_MAX_LENGTH = 64;
4405
4406
  var SlackChannelSectionSchema = import_zod4.z.object({
4406
4407
  appId: ChannelBotIdSchema,
4407
4408
  teamId: SlackIdSchema,
@@ -4411,7 +4412,10 @@ var SlackChannelSectionSchema = import_zod4.z.object({
4411
4412
  adminUserIds: import_zod4.z.array(SlackIdSchema).max(SLACK_CHANNEL_ADMIN_USER_IDS_MAX_LENGTH).refine((ids) => new Set(ids).size === ids.length, {
4412
4413
  message: "adminUserIds must not contain duplicates"
4413
4414
  }).optional(),
4414
- allowGuests: import_zod4.z.boolean().optional()
4415
+ allowGuests: import_zod4.z.boolean().optional(),
4416
+ approverUserIds: import_zod4.z.array(SlackIdSchema).max(SLACK_CHANNEL_APPROVER_USER_IDS_MAX_LENGTH).refine((ids) => new Set(ids).size === ids.length, {
4417
+ message: "approverUserIds must not contain duplicates"
4418
+ }).optional()
4415
4419
  }).strict().refine((section) => SLACK_ID_PATTERN.test(section.appId), {
4416
4420
  path: ["appId"],
4417
4421
  message: `appId ${SLACK_ID_MESSAGE}`
@@ -9238,7 +9242,9 @@ var SlackChannelBindingSettingsSchema = import_zod61.z.object({
9238
9242
  teamId: SlackIdSchema,
9239
9243
  enterpriseId: SlackIdSchema.optional(),
9240
9244
  adminUserIds: import_zod61.z.array(SlackIdSchema).max(SLACK_CHANNEL_ADMIN_USER_IDS_MAX_LENGTH),
9241
- allowGuests: import_zod61.z.boolean()
9245
+ allowGuests: import_zod61.z.boolean(),
9246
+ /** #4993: who may approve a tool call from the thread. Empty = nobody in Slack; the portal only. */
9247
+ approverUserIds: import_zod61.z.array(SlackIdSchema).max(SLACK_CHANNEL_APPROVER_USER_IDS_MAX_LENGTH)
9242
9248
  }).strict();
9243
9249
  var ChannelBindingSchema = import_zod61.z.object({
9244
9250
  projectId: import_zod61.z.string().uuid(),
@@ -11092,8 +11098,9 @@ var FileAgentClaudeCodeDefinitionSchema = import_zod77.z.object({
11092
11098
  voice: WorkloadManifestVoiceSchema.optional(),
11093
11099
  /**
11094
11100
  * promote 门禁档位(#4870 G1;方案 #4866 与 owner 裁决):`advisory` | `required`。
11095
- * 两档的含义是**设计含义、当前不生效**(本键今天没有消费方)——`advisory` 档下
11096
- * 评测结论只记录不挡发布,随按档位执行(G2)落地才生效;`required` 即今天的硬门禁。
11101
+ * `required` 下评测判红挡发布(可带理由 override);`advisory` 下评测结论照常
11102
+ * 记录,但不挡发布(按档位执行,G2 已落地);两档的执行语义见
11103
+ * `workload-stage-service.ts` 的 `assertPipelinePromoteGate`。
11097
11104
  * 闭集与类型是 `PROMOTE_GATE_MODES` / `PromoteGateMode`(本包 manifest 文件)。
11098
11105
  *
11099
11106
  * 与 `format` 正交,两臂都可声明(同 `voice`、`memory`)——live 也走 promote。
@@ -14906,9 +14913,9 @@ var ProjectsResource = class {
14906
14913
  /**
14907
14914
  * `GET /api/projects/:id/workbench` — lifecycle-console workbench
14908
14915
  * aggregation (workload count / session stats / stage drift / recent
14909
- * changes). Same DTO and same `@pilot/control-plane` aggregation the
14910
- * AgentHub console reads at `GET /api/v2/projects/:id/workbench`.
14911
- * Validated against the contracts `WorkbenchDTOSchema` (single wire truth).
14916
+ * changes). Same DTO and same server-side aggregation the AgentHub console
14917
+ * reads. Validated against the wire `WorkbenchDTOSchema` (single wire
14918
+ * truth).
14912
14919
  */
14913
14920
  async getWorkbench(projectId, options = {}) {
14914
14921
  const path = `/api/projects/${encodeURIComponent(projectId)}/workbench`;
@@ -14927,10 +14934,9 @@ var ProjectsResource = class {
14927
14934
  }
14928
14935
  /**
14929
14936
  * `GET /api/projects/:id/release-board` — one row per workload with its
14930
- * test/production stage slot summaries. Same DTO and same
14931
- * `@pilot/control-plane` aggregation the AgentHub console reads at
14932
- * `GET /api/v2/projects/:id/release-board`. Validated against the
14933
- * contracts `ReleaseBoardDTOSchema` (single wire truth).
14937
+ * test/production stage slot summaries. Same DTO and same server-side
14938
+ * aggregation the AgentHub console reads. Validated against the wire
14939
+ * `ReleaseBoardDTOSchema` (single wire truth).
14934
14940
  */
14935
14941
  async getReleaseBoard(projectId, options = {}) {
14936
14942
  const path = `/api/projects/${encodeURIComponent(projectId)}/release-board`;
@@ -15239,9 +15245,9 @@ var SessionsResource = class {
15239
15245
  * aggregation (session detail + failure + events + sandbox, each block
15240
15246
  * degrading independently on its own read error — see
15241
15247
  * `SessionDiagnosisDTOSchema`'s `blockErrors`). Same DTO and same
15242
- * `@pilot/control-plane` aggregation the AgentHub admin console reads at
15243
- * `GET /api/v2/sessions/:id/diagnosis`. Validated against the contracts
15244
- * `SessionDiagnosisDTOSchema` (single wire truth), like `workloads.get`.
15248
+ * server-side aggregation the AgentHub admin console reads. Validated
15249
+ * against the wire `SessionDiagnosisDTOSchema` (single wire truth), like
15250
+ * `workloads.get`.
15245
15251
  */
15246
15252
  async getDiagnosis(sessionId, options = {}) {
15247
15253
  const path = `/api/sessions/${encodeURIComponent(sessionId)}/diagnosis`;
@@ -15272,8 +15278,7 @@ var SessionsResource = class {
15272
15278
  * to a managed agent through the SAME friendly wrapper the portal chat
15273
15279
  * uses: the server decides first-turn-vs-followup, builds idempotency keys
15274
15280
  * and deadlines, resolves MCP/skills/memory config, and dispatches through
15275
- * the Worker Protocol. The SDK carries ZERO dispatch logic — see
15276
- * `specs/features/agenthub-sdk.md` § "Session invocation".
15281
+ * the Worker Protocol. The SDK carries ZERO dispatch logic.
15277
15282
  *
15278
15283
  * Requires scope `runs:create`. The session must be `ready`
15279
15284
  * (`sessions.wait`) and have no turn in flight — a prior running turn
@@ -15351,8 +15356,7 @@ var SessionsResource = class {
15351
15356
  * `VOICE_AUDIO_UNREADABLE` (400), `VOICE_TRANSCRIPT_EMPTY` (422),
15352
15357
  * `VOICE_TRANSCRIPTION_FAILED` / `VOICE_MODEL_NOT_AUTHORIZED` (502),
15353
15358
  * `VOICE_RATE_LIMITED` (429), `VOICE_MODEL_SERVICE_UNSUPPORTED` (400).
15354
- * The codes and their statuses live in `@pilot/contracts`'s
15355
- * `VOICE_ERROR_CODES`.
15359
+ * The codes and their statuses are the server's voice error-code table.
15356
15360
  *
15357
15361
  * **`VOICE_TURNS_DISABLED` is NOT one of them on this path**, despite being
15358
15362
  * in that table: it answers a NON-empty-text turn while `voice_turns_enabled`
@@ -15670,9 +15674,7 @@ var SessionsResource = class {
15670
15674
  /**
15671
15675
  * `GET /api/sessions/:id/events/stream` — live SSE chunk stream, the same
15672
15676
  * rendering feed the portal chat consumes (`pilot_coder.chunk` payloads,
15673
- * ~50ms server-side latency; wire contract in
15674
- * `specs/features/pilot-v2-session-event-stream.md`). Requires scope
15675
- * `runs:stream`.
15677
+ * ~50ms server-side latency). Requires scope `runs:stream`.
15676
15678
  *
15677
15679
  * Yields `{ seq, chunk }` per frame; heartbeat comments are skipped; the
15678
15680
  * iterator ENDS when the server emits `[DONE]` (session failed/terminal).
@@ -16425,8 +16427,7 @@ var WorkloadsResource = class {
16425
16427
  * `POST /api/workloads/{slug}/revisions` — publish an immutable Revision.
16426
16428
  *
16427
16429
  * ⚠️ **`setDefault: true` behavior depends on whether the workload has any
16428
- * stage slot yet (2026-07-13 behavior; spec `pilot-v2-agent-stage-environment.md`
16429
- * §5.1 STG-M5~M8):**
16430
+ * stage slot yet (2026-07-13 behavior; STG-M5~M8):**
16430
16431
  * - **First-ever publish** (zero existing slots) — writes the **`test`**
16431
16432
  * stage slot only, with `environmentId: null` (does NOT inherit
16432
16433
  * whatever environment the revision's runtime snapshot happens to
@@ -16446,8 +16447,7 @@ var WorkloadsResource = class {
16446
16447
  * follow up with `agents.stages.set(workloadId, "production", { revisionId,
16447
16448
  * environmentId })` or `agents.stages.promote(workloadId, { environmentId })`
16448
16449
  * — see the README "发布闭环" walkthrough for the full sequence. This is a
16449
- * real, verified behavior (not a guess): `applySetDefault` in
16450
- * `packages/control-plane/src/workload/workload-manifest-apply.ts` decides
16450
+ * real, verified behavior (not a guess): the server decides
16451
16451
  * first-vs-non-first by whether any `workload_stages` row exists yet, and
16452
16452
  * only ever passes `environmentId: undefined` into the `test` slot upsert
16453
16453
  * on the first-ever path — `undefined` on a not-yet-existing slot inserts