@zhushanwen/subagent-core 0.2.0 → 0.4.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/README.md +45 -2
- package/agents/analyst.md +60 -0
- package/agents/coder.md +69 -0
- package/agents/debugger.md +66 -0
- package/agents/doc-reviewer.md +49 -0
- package/agents/explorer.md +63 -0
- package/agents/general-purpose.md +32 -0
- package/agents/orchestrator.md +61 -0
- package/agents/planner.md +53 -0
- package/agents/researcher.md +67 -0
- package/agents/reviewer.md +73 -0
- package/dist/chunk-4KN4TTG7.js +240 -0
- package/dist/chunk-APZY4IME.js +27 -0
- package/dist/chunk-X7SZ5HLQ.js +66 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
- package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
- package/dist/execution/engine/engines/zcode/constants.js +43 -1
- package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
- package/dist/execution/engine/engines/zcode/reader.js +4 -234
- package/dist/execution/engine/paths.js +7 -19
- package/dist/index.cjs +14709 -100
- package/dist/index.d.cts +4053 -200
- package/dist/index.d.ts +4053 -200
- package/dist/index.js +14358 -102
- package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
- package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
- package/package.json +6 -3
- package/src/__tests__/review-fix-loop-script.test.ts +90 -2
- package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
- package/src/__tests__/smoke.test.ts +9 -2
- package/src/execution/__tests__/agent-profile.test.ts +232 -0
- package/src/execution/__tests__/agents-assembly.test.ts +219 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
- package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
- package/src/execution/__tests__/delivery-methods.test.ts +2 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
- package/src/execution/__tests__/dialog-queue.test.ts +199 -1
- package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
- package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
- package/src/execution/__tests__/finalize-record.test.ts +106 -0
- package/src/execution/__tests__/gc-timer.test.ts +2 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
- package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
- package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
- package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
- package/src/execution/__tests__/notify-ledger.test.ts +294 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
- package/src/execution/__tests__/record-store.test.ts +83 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
- package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
- package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
- package/src/execution/__tests__/session-pending.test.ts +228 -0
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
- package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
- package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
- package/src/execution/__tests__/spawned-children.test.ts +153 -10
- package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
- package/src/execution/__tests__/timeout-integration.test.ts +7 -7
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
- package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
- package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
- package/src/execution/agent-registry.ts +225 -1
- package/src/execution/agents-assembly.ts +88 -0
- package/src/execution/concurrency-pool.ts +71 -9
- package/src/execution/dialog-queue.ts +101 -3
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
- package/src/execution/engine/__tests__/registry.test.ts +90 -1
- package/src/execution/engine/engine-discovery.ts +12 -16
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
- package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
- package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
- package/src/execution/engine/engines/zcode/connection.ts +585 -0
- package/src/execution/engine/engines/zcode/constants.ts +129 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
- package/src/execution/engine/engines/zcode/preparer.ts +25 -22
- package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
- package/src/execution/engine/host-task-spec.ts +3 -3
- package/src/execution/engine/port.ts +41 -1
- package/src/execution/engine/registry.ts +53 -0
- package/src/execution/finalize-record.ts +63 -5
- package/src/execution/get-state-handshake.ts +85 -10
- package/src/execution/lifecycle-manager.ts +27 -3
- package/src/execution/manifest-store.ts +53 -62
- package/src/execution/notifier.ts +10 -10
- package/src/execution/notify-ledger.ts +117 -16
- package/src/execution/record-entry.ts +8 -2
- package/src/execution/record-store.ts +29 -15
- package/src/execution/session-pending.ts +213 -62
- package/src/execution/session-runner.ts +974 -128
- package/src/execution/sessions-index.ts +10 -55
- package/src/execution/settled-watchdog.ts +99 -0
- package/src/execution/subagent-actions-core.ts +686 -0
- package/src/execution/subagent-service.ts +396 -49
- package/src/execution/ui-request-handler-factory.ts +27 -4
- package/src/execution/worktree-git-ops.ts +397 -0
- package/src/execution/worktree-manager.ts +72 -10
- package/src/execution/worktree-registry.ts +6 -12
- package/src/index.ts +413 -6
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
- package/src/orchestration/__tests__/args-meta.test.ts +358 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
- package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
- package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
- package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
- package/src/orchestration/__tests__/script-generate.test.ts +314 -0
- package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
- package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
- package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
- package/src/orchestration/agent-opts-resolver.ts +7 -7
- package/src/orchestration/args-meta.ts +198 -0
- package/src/orchestration/error-recovery.ts +416 -146
- package/src/orchestration/execute-agent-call.ts +9 -9
- package/src/orchestration/file-run-store.ts +327 -0
- package/src/orchestration/launcher.ts +35 -26
- package/src/orchestration/lifecycle.ts +355 -79
- package/src/orchestration/models/agent-call.ts +7 -7
- package/src/orchestration/models/budget.ts +5 -5
- package/src/orchestration/models/run-runtime.ts +13 -13
- package/src/orchestration/models/trace.ts +8 -8
- package/src/orchestration/models/workflow-run.ts +27 -27
- package/src/orchestration/models/workflow-script.ts +4 -4
- package/src/orchestration/run-snapshot.ts +266 -0
- package/src/orchestration/script-generate.ts +154 -0
- package/src/orchestration/script-lint.ts +33 -33
- package/src/orchestration/worker-handle.ts +10 -10
- package/src/orchestration/worker-host.ts +10 -10
- package/src/orchestration/worker-script-builder.ts +401 -346
- package/src/orchestration/workflow-files.ts +37 -11
- package/src/orchestration/workflow-run-summary.ts +69 -0
- package/src/orchestration/workflow-script-registry-impl.ts +31 -9
- package/src/shared/__tests__/agent-ref.test.ts +209 -2
- package/src/shared/__tests__/atomic-write.test.ts +267 -0
- package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
- package/src/shared/__tests__/injection-render.test.ts +518 -0
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
- package/src/shared/__tests__/resource-discovery.test.ts +3 -2
- package/src/shared/agent-ref.ts +143 -2
- package/src/shared/atomic-write.ts +320 -0
- package/src/shared/bounded-serialize.ts +154 -0
- package/src/shared/injection-render.ts +279 -0
- package/src/shared/meta-parser.ts +41 -1
- package/src/shared/resource-discovery.ts +104 -37
- package/src/shared/resource-meta.ts +14 -0
- package/src/shared/xml-injection.ts +9 -9
- package/workflows/README.md +9 -9
- package/workflows/chain.js +4 -2
- package/workflows/map-reduce.js +5 -3
- package/workflows/parallel.js +5 -3
- package/workflows/review-fix-loop-utils.cjs +14 -3
- package/workflows/review-fix-loop.js +18 -7
- package/workflows/scatter-gather.js +4 -2
- package/dist/chunk-3VOERJPJ.js +0 -22
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
// src/execution/engine/engines/zcode/session-channel.ts
|
|
2
|
+
//
|
|
3
|
+
// ZcodeEngine app-server 会话层(R3)。设计权威源:
|
|
4
|
+
// docs/design/zcode-engine-appserver-resident.md §3.3 D4(会话生命周期:每任务自包含
|
|
5
|
+
// create→run→close,不做热会话复用)/ §2.4 目标数据流 ①-⑦ / §3.4 不变量 1
|
|
6
|
+
// (text_delta 拼接 == read 全文、终态唯一 turn.terminal 权威、收尾帧 usage 完整)
|
|
7
|
+
// 与 2(resolve 严格晚于全部事件回调)/ 附录 A.2(任务生命周期帧序列——协议权威,
|
|
8
|
+
// 本文件帧字段逐字来源)。旧实现参照:zsw 仓 84b63a0^ lib/runner-appserver.js 的
|
|
9
|
+
// _createTurn/_fetchFinalResponse 同等层(终态判定 / read 兜底链为真机已验证形态,
|
|
10
|
+
// TS 重写收窄到 D4 单任务自包含面——-32004 四步恢复序 / resume / 热会话复用整体
|
|
11
|
+
// out of scope,见设计 D4 连带声明)。
|
|
12
|
+
//
|
|
13
|
+
// 分层边界(与 R4 的接线面):
|
|
14
|
+
// - 本层只做「单任务会话通道」:create/subscribe/send/终态/read/close + runTurn
|
|
15
|
+
// 组合入口。AgentEvent 合成(text_delta→事件流、message_end.usage 映射走
|
|
16
|
+
// parser.mapZcodeUsage 同源)与 abort 链(session/stop→杀链)归 R4 编排层;
|
|
17
|
+
// - 连接生命周期(惰性启动/重建/dispose)归 AppServerConnection(R2)与 R4;
|
|
18
|
+
// - 错误透传纪律:RPC error 应答(AppServerRpcError,含 code/message/data)原样
|
|
19
|
+
// 上抛不吞不包装——R5 降级链按 code(-32601/-32602 漂移类)归类(§3.3 错误规格表)。
|
|
20
|
+
//
|
|
21
|
+
// 终态判定(D4 + 不变量 1)双保险:
|
|
22
|
+
// 1. turn.terminal 权威:v4/telemetry/event {kind:"turn.terminal"} 到达即终态
|
|
23
|
+
// (success/error 均 算终态——旧实现实证:不归类会挂到超时);
|
|
24
|
+
// 2. 宽松匹配防洪堤:turn.terminal 缺失/迟到时,收尾帧(session/event
|
|
25
|
+
// payload.response 非空)即终态——防协议小漂移把任务挂死到超时预算。
|
|
26
|
+
// 终态判定后:收尾帧数据(response/usage)仍吸收(它属终态数据不是增量回调,
|
|
27
|
+
// 且与 turn.terminal 常在同一 stdout 批次到达);迟到 delta 不再触发回调
|
|
28
|
+
// (不变量 2 的另一半:resolve 后不再发事件)。
|
|
29
|
+
//
|
|
30
|
+
// 连接崩溃的 turn 收割(R4 已补齐):SessionChannel 在构造时订阅 AppServerConnection
|
|
31
|
+
// 的 onClose 面——进程死亡(崩溃/我方杀链)时立即 fail 全部在途 turn(错误即连接层
|
|
32
|
+
// 的崩溃 reason,含 stderr 尾部),不再依赖 turnTimeoutMs 兜底挂满预算才收割。
|
|
33
|
+
// (onClose 由连接层保证在全部在途 request reject 之后触发。)
|
|
34
|
+
|
|
35
|
+
import { createHash } from "node:crypto";
|
|
36
|
+
|
|
37
|
+
import { getLogger } from "../../../../core/logger.ts";
|
|
38
|
+
|
|
39
|
+
import type { AppServerConnection } from "./connection.ts";
|
|
40
|
+
import {
|
|
41
|
+
ZCODE_APPSERVER_TURN_CLOSE_TIMEOUT_MS,
|
|
42
|
+
ZCODE_APPSERVER_TURN_DEFAULT_TIMEOUT_MS,
|
|
43
|
+
ZCODE_APPSERVER_TURN_READ_TIMEOUT_MS,
|
|
44
|
+
} from "./constants.ts";
|
|
45
|
+
|
|
46
|
+
const logger = getLogger("subagents");
|
|
47
|
+
|
|
48
|
+
/** session/subscribe 的 deliveryKind 必填值(A.2 ②——缺失则终态事件不达、会话假死,旧实测)。 */
|
|
49
|
+
export const SUBSCRIBE_DELIVERY_KIND = "desktop-continuous";
|
|
50
|
+
|
|
51
|
+
/** workspaceKey 取 sha256 hex 的前导字符数(旧实现实测形态 'ws-' + 16 hex 短 hash)。 */
|
|
52
|
+
const WORKSPACE_KEY_HASH_CHARS = 16;
|
|
53
|
+
|
|
54
|
+
/** create 应答异常时的摘要截断长度(够定位形态、不刷屏——与旧实现同值)。 */
|
|
55
|
+
const CREATE_REPLY_LOG_CHARS = 300;
|
|
56
|
+
|
|
57
|
+
/** 推送帧文本进日志的截断长度(诊断够用即可)。 */
|
|
58
|
+
const DELTA_LOG_CHARS = 120;
|
|
59
|
+
|
|
60
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
61
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function errMessage(err: unknown): string {
|
|
65
|
+
return err instanceof Error ? err.message : String(err);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================
|
|
69
|
+
// 协议形态提取(纯函数,conformance/golden 回放层可复用)
|
|
70
|
+
// ============================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* workspacePath → 稳定短 hash(A.2 ①:workspaceKey 算法不限;取旧实现实测形态
|
|
74
|
+
* 'ws-' + sha256 前 16 hex)。
|
|
75
|
+
*/
|
|
76
|
+
export function stableWorkspaceKey(workspacePath: string): string {
|
|
77
|
+
return (
|
|
78
|
+
"ws-" +
|
|
79
|
+
createHash("sha256")
|
|
80
|
+
.update(workspacePath)
|
|
81
|
+
.digest("hex")
|
|
82
|
+
.slice(0, WORKSPACE_KEY_HASH_CHARS)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 从 session/create 应答提取 sessionId。A.2 ①:权威位置 result.session.sessionId
|
|
88
|
+
* (**projection.sessionId 恒 "unknown",勿用**);session.id / 顶层 sessionId 为
|
|
89
|
+
* 历史/未来形态的宽松兜底(旧实现实测提取链)。
|
|
90
|
+
*/
|
|
91
|
+
export function extractCreatedSessionId(created: unknown): string | undefined {
|
|
92
|
+
if (!isRecord(created)) return undefined;
|
|
93
|
+
const session = isRecord(created.session) ? created.session : undefined;
|
|
94
|
+
if (
|
|
95
|
+
session !== undefined &&
|
|
96
|
+
typeof session.sessionId === "string" &&
|
|
97
|
+
session.sessionId !== ""
|
|
98
|
+
) {
|
|
99
|
+
return session.sessionId;
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
session !== undefined &&
|
|
103
|
+
typeof session.id === "string" &&
|
|
104
|
+
session.id !== ""
|
|
105
|
+
) {
|
|
106
|
+
return session.id;
|
|
107
|
+
}
|
|
108
|
+
if (typeof created.sessionId === "string" && created.sessionId !== "") {
|
|
109
|
+
return created.sessionId;
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 推送帧的会话归属(A.2:session/event 帧带 params.sessionId;telemetry 帧未标——
|
|
115
|
+
* 无 sid 时由 lookupTurn 按唯一在途兜底归因,多在途宁丢勿错,旧实现同策略)。 */
|
|
116
|
+
function extractPushSessionId(params: unknown): string | undefined {
|
|
117
|
+
if (!isRecord(params)) return undefined;
|
|
118
|
+
if (typeof params.sessionId === "string" && params.sessionId !== "")
|
|
119
|
+
return params.sessionId;
|
|
120
|
+
if (
|
|
121
|
+
isRecord(params.session) &&
|
|
122
|
+
typeof params.session.id === "string" &&
|
|
123
|
+
params.session.id !== ""
|
|
124
|
+
) {
|
|
125
|
+
return params.session.id;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** read 应答消息条目的 role(实测主形态在 m.info.role,旧形态在 m.role)。 */
|
|
131
|
+
function messageRole(m: Record<string, unknown>): string | undefined {
|
|
132
|
+
if (typeof m.role === "string") return m.role;
|
|
133
|
+
if (isRecord(m.info) && typeof m.info.role === "string") return m.info.role;
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** parts[] 中 type:'text' 部件的 text 拼接(A.2 ⑥ 主形态;无 text 部件 → undefined)。 */
|
|
138
|
+
function partsToText(parts: unknown): string | undefined {
|
|
139
|
+
if (!Array.isArray(parts)) return undefined;
|
|
140
|
+
const texts: string[] = [];
|
|
141
|
+
for (const p of parts) {
|
|
142
|
+
if (isRecord(p) && p.type === "text" && typeof p.text === "string")
|
|
143
|
+
texts.push(p.text);
|
|
144
|
+
}
|
|
145
|
+
return texts.length > 0 ? texts.join("") : undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** content 三形态(字符串 / {text} / 块数组)的旧形态兼容提取。 */
|
|
149
|
+
function contentToText(content: unknown): string | undefined {
|
|
150
|
+
if (typeof content === "string") return content;
|
|
151
|
+
if (isRecord(content) && typeof content.text === "string")
|
|
152
|
+
return content.text;
|
|
153
|
+
if (Array.isArray(content)) {
|
|
154
|
+
const texts = content.map((b) => {
|
|
155
|
+
if (typeof b === "string") return b;
|
|
156
|
+
return isRecord(b) && typeof b.text === "string" ? b.text : "";
|
|
157
|
+
});
|
|
158
|
+
const joined = texts.join("");
|
|
159
|
+
return joined !== "" ? joined : undefined;
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 从 session/read 应答提取最后一条 assistant 全文(A.2 ⑥:messages[].parts[] 中
|
|
166
|
+
* type:'text' 的 text 拼接;role 在 m.info.role 实测主形态,m.role/m.content 旧形态
|
|
167
|
+
* 兼容——旧实现已验证宽容链)。提取不到返回 undefined(调用方走收尾帧→delta 聚合)。
|
|
168
|
+
*/
|
|
169
|
+
export function extractAssistantText(readResult: unknown): string | undefined {
|
|
170
|
+
if (typeof readResult === "string") return readResult;
|
|
171
|
+
if (!isRecord(readResult)) return undefined;
|
|
172
|
+
if (Array.isArray(readResult.messages)) {
|
|
173
|
+
const last = [...readResult.messages]
|
|
174
|
+
.reverse()
|
|
175
|
+
.find(
|
|
176
|
+
(m): m is Record<string, unknown> =>
|
|
177
|
+
isRecord(m) &&
|
|
178
|
+
(messageRole(m) === "assistant" || messageRole(m) === undefined)
|
|
179
|
+
);
|
|
180
|
+
if (last !== undefined) {
|
|
181
|
+
const viaContent = contentToText(last.content);
|
|
182
|
+
if (viaContent !== undefined) return viaContent;
|
|
183
|
+
const viaParts = partsToText(last.parts);
|
|
184
|
+
if (viaParts !== undefined) return viaParts;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const k of ["text", "response", "content", "message"] as const) {
|
|
188
|
+
if (typeof readResult[k] === "string") return readResult[k] as string;
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 从 session/read 应答提取 usage。tokens 在最后一条 assistant 消息 parts 的
|
|
195
|
+
* step-finish 部件 tokens 字段——结构属 A.5 未确认项(未真机复核),宽容解析:
|
|
196
|
+
* tokens 是对象即收、字段缺席不抛;顶层 usage 旧假设形态兜底。
|
|
197
|
+
*/
|
|
198
|
+
export function extractReadUsage(
|
|
199
|
+
readResult: unknown
|
|
200
|
+
): SessionTurnUsage | undefined {
|
|
201
|
+
if (!isRecord(readResult)) return undefined;
|
|
202
|
+
const messages = Array.isArray(readResult.messages)
|
|
203
|
+
? readResult.messages
|
|
204
|
+
: [];
|
|
205
|
+
for (const m of [...messages].reverse()) {
|
|
206
|
+
if (
|
|
207
|
+
!isRecord(m) ||
|
|
208
|
+
messageRole(m) !== "assistant" ||
|
|
209
|
+
!Array.isArray(m.parts)
|
|
210
|
+
)
|
|
211
|
+
continue;
|
|
212
|
+
const finish = [...m.parts]
|
|
213
|
+
.reverse()
|
|
214
|
+
.find(
|
|
215
|
+
(p) => isRecord(p) && p.type === "step-finish" && isRecord(p.tokens)
|
|
216
|
+
);
|
|
217
|
+
if (finish !== undefined && isRecord(finish.tokens)) return finish.tokens;
|
|
218
|
+
}
|
|
219
|
+
return isRecord(readResult.usage) ? readResult.usage : undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================================
|
|
223
|
+
// 类型(R4 引擎 run 的消费面)
|
|
224
|
+
// ============================================================
|
|
225
|
+
|
|
226
|
+
/** session/create 的 per-session 模型(A.2 ①:strict 对象——字符串会被 -32602 拒收)。 */
|
|
227
|
+
export interface SessionModelSpec {
|
|
228
|
+
providerId: string;
|
|
229
|
+
modelId: string;
|
|
230
|
+
variant?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** session/create 参数(A.2 ① 键集逐字;workspaceKey 由 workspacePath 派生,不手传)。 */
|
|
234
|
+
export interface SessionCreateParams {
|
|
235
|
+
workspacePath: string;
|
|
236
|
+
/** 权限模式(如 "yolo")——值语义不限(A.2),由调用方(R4)决定。 */
|
|
237
|
+
mode: string;
|
|
238
|
+
model?: SessionModelSpec;
|
|
239
|
+
thoughtLevel?: string;
|
|
240
|
+
toolAllowlist?: string[];
|
|
241
|
+
toolDenylist?: string[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 一轮 usage(宽容形态):主形态收尾帧 {inputTokens, outputTokens}(A.2 ⑤ 权威);
|
|
246
|
+
* 兜底形态 read step-finish tokens {input,...}(A.5 未确认项)。字段名到 AgentUsage
|
|
247
|
+
* 的映射归 R4(与 parser.mapZcodeUsage 同源),本层不做字段转换。
|
|
248
|
+
*/
|
|
249
|
+
export interface SessionTurnUsage {
|
|
250
|
+
inputTokens?: unknown;
|
|
251
|
+
outputTokens?: unknown;
|
|
252
|
+
[key: string]: unknown;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** runTurn 的流式回调(R4 映射为 AgentEvent {type:"text_delta", delta})。 */
|
|
256
|
+
export interface SessionTurnCallbacks {
|
|
257
|
+
/** payload.delta 实时文本增量(A.2 ④:文本在 session/event,stream.chunk 无文本)。 */
|
|
258
|
+
onTextDelta?: (delta: string) => void;
|
|
259
|
+
/**
|
|
260
|
+
* [R4] create 应答后立即回调(sessionId 已知、早于 subscribe/send/终态)。引擎
|
|
261
|
+
* 编排层的 onHandleReady 回填(§3.4 不变量 3)与 abort 链的 session/stop 目标
|
|
262
|
+
* 都挂在这个时点——runTurn 的 resolve 形态在终态前拿不到 sessionId。
|
|
263
|
+
*/
|
|
264
|
+
onSessionCreated?: (sessionId: string) => void;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface SessionTurnOptions extends SessionTurnCallbacks {
|
|
268
|
+
/** 一轮终态等待预算(缺省 ZCODE_APPSERVER_TURN_DEFAULT_TIMEOUT_MS)。 */
|
|
269
|
+
turnTimeoutMs?: number;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** 终态信号来源(D4:turn.terminal 权威;final-frame = 宽松判定防洪堤)。 */
|
|
273
|
+
export type TerminalSource = "turn.terminal" | "final-frame";
|
|
274
|
+
|
|
275
|
+
export interface SessionTurnResult {
|
|
276
|
+
sessionId: string;
|
|
277
|
+
/** 全文优先级:read 兜底(权威)→ 收尾帧 response → delta 聚合。 */
|
|
278
|
+
response: string;
|
|
279
|
+
/** usage 优先级:收尾帧(A.2 权威)→ read step-finish tokens(宽容)→ 缺席。 */
|
|
280
|
+
usage?: SessionTurnUsage;
|
|
281
|
+
terminal: { status: string; source: TerminalSource };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============================================================
|
|
285
|
+
// 会话通道
|
|
286
|
+
// ============================================================
|
|
287
|
+
|
|
288
|
+
/** 终态信息(turn.terminal 的 status 原样透传——错误语义由 R4 结果层处理)。 */
|
|
289
|
+
interface TerminalInfo {
|
|
290
|
+
status: string;
|
|
291
|
+
source: TerminalSource;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 一个在途 turn 的全部状态(推送泵的归因目标)。settle/fail 由 openTurn 装配
|
|
296
|
+
* (互斥守卫 + timer 清理 + done promise 落定),泵内只调用不感知装配细节。
|
|
297
|
+
*/
|
|
298
|
+
interface ActiveTurn {
|
|
299
|
+
readonly sessionId: string;
|
|
300
|
+
readonly callbacks: SessionTurnCallbacks;
|
|
301
|
+
/** 终态判定是否已落定(settle/fail 任一先行即 true——后续重复终态不再受理)。 */
|
|
302
|
+
settled: boolean;
|
|
303
|
+
deltas: string[];
|
|
304
|
+
finalText: string | undefined;
|
|
305
|
+
finalUsage: SessionTurnUsage | undefined;
|
|
306
|
+
terminal: TerminalInfo | undefined;
|
|
307
|
+
settle: (t: TerminalInfo) => void;
|
|
308
|
+
fail: (err: Error) => void;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 单任务会话通道(D4):在一条共享 AppServerConnection 上实现
|
|
313
|
+
* create→subscribe→send→(事件流)→终态→read 兜底→resolve→close 的自包含生命周期。
|
|
314
|
+
* 多 session 可在同一条连接上并发(sessionId 归因防串线;无 sid 的推送帧仅归因到
|
|
315
|
+
* 唯一在途 turn,多在途宁丢勿错)。
|
|
316
|
+
*/
|
|
317
|
+
export class SessionChannel {
|
|
318
|
+
private readonly conn: AppServerConnection;
|
|
319
|
+
/** sessionId → 在途 turn(终态落定后仍保留到 runTurn 收尾——收尾帧数据吸收窗口)。 */
|
|
320
|
+
private readonly activeTurns = new Map<string, ActiveTurn>();
|
|
321
|
+
private readonly offHandlers: Array<() => void>;
|
|
322
|
+
|
|
323
|
+
constructor(conn: AppServerConnection) {
|
|
324
|
+
this.conn = conn;
|
|
325
|
+
this.offHandlers = [
|
|
326
|
+
conn.onNotification("session/event", (params) =>
|
|
327
|
+
this.handleSessionEvent(params)
|
|
328
|
+
),
|
|
329
|
+
conn.onNotification("v4/telemetry/event", (params) =>
|
|
330
|
+
this.handleTelemetry(params)
|
|
331
|
+
),
|
|
332
|
+
// 连接崩溃收割:进程死亡(崩溃/我方杀链)立即 fail 全部在途 turn——
|
|
333
|
+
// onClose 契约保证触发时连接层在途 request 已全部 reject,此处补齐
|
|
334
|
+
// 「无在途 request 的 turn」的收割(否则挂到 turnTimeoutMs 预算耗尽)
|
|
335
|
+
conn.onClose((reason) => this.failAllTurns(`app-server ${reason}`)),
|
|
336
|
+
];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** 退订连接推送(R4 引擎 dispose 面的配套;不影响连接自身生命周期)。 */
|
|
340
|
+
dispose(): void {
|
|
341
|
+
for (const off of this.offHandlers.splice(0)) off();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** 连接死亡收割:全部在途 turn 立即 reject(reason 即连接层崩溃错误)。 */
|
|
345
|
+
private failAllTurns(reason: string): void {
|
|
346
|
+
const err = new Error(reason);
|
|
347
|
+
for (const turn of [...this.activeTurns.values()]) {
|
|
348
|
+
turn.fail(err);
|
|
349
|
+
this.activeTurns.delete(turn.sessionId);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* session/create(A.2 ① 键集逐字:对面 zod strict 拒未知键,出站必须精确)。
|
|
355
|
+
* persistence:"immediate" 固定携带(D4:SQLite 持久化,上游不可覆盖);空工具
|
|
356
|
+
* 清单不设键(防上游带空数组)。sessionId 从 result.session.sessionId 提取
|
|
357
|
+
* (projection.sessionId 恒 "unknown" 勿用)。
|
|
358
|
+
*/
|
|
359
|
+
async createSession(params: SessionCreateParams): Promise<string> {
|
|
360
|
+
const frame: Record<string, unknown> = {
|
|
361
|
+
workspace: {
|
|
362
|
+
workspacePath: params.workspacePath,
|
|
363
|
+
workspaceKey: stableWorkspaceKey(params.workspacePath),
|
|
364
|
+
},
|
|
365
|
+
mode: params.mode,
|
|
366
|
+
...(params.model !== undefined ? { model: params.model } : {}),
|
|
367
|
+
...(params.thoughtLevel !== undefined
|
|
368
|
+
? { thoughtLevel: params.thoughtLevel }
|
|
369
|
+
: {}),
|
|
370
|
+
...(params.toolAllowlist !== undefined && params.toolAllowlist.length > 0
|
|
371
|
+
? { toolAllowlist: params.toolAllowlist }
|
|
372
|
+
: {}),
|
|
373
|
+
...(params.toolDenylist !== undefined && params.toolDenylist.length > 0
|
|
374
|
+
? { toolDenylist: params.toolDenylist }
|
|
375
|
+
: {}),
|
|
376
|
+
persistence: "immediate",
|
|
377
|
+
};
|
|
378
|
+
const created = await this.conn.request("session/create", frame);
|
|
379
|
+
const sessionId = extractCreatedSessionId(created);
|
|
380
|
+
if (sessionId === undefined) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`session/create 未返回 sessionId: ${JSON.stringify(created).slice(
|
|
383
|
+
0,
|
|
384
|
+
CREATE_REPLY_LOG_CHARS
|
|
385
|
+
)}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return sessionId;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** session/subscribe(A.2 ②:deliveryKind 必填——缺失则终态事件不达、会话假死)。 */
|
|
392
|
+
async subscribe(sessionId: string): Promise<void> {
|
|
393
|
+
await this.conn.request("session/subscribe", {
|
|
394
|
+
sessionId,
|
|
395
|
+
deliveryKind: SUBSCRIBE_DELIVERY_KIND,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** session/send(A.2 ③:字段是 content 不是 text)→ {accepted:true}。 */
|
|
400
|
+
async send(sessionId: string, content: string): Promise<void> {
|
|
401
|
+
const r = await this.conn.request("session/send", { sessionId, content });
|
|
402
|
+
if (isRecord(r) && r.accepted === false) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
"session/send 返回 accepted:false(投递未被接受——会话可能已失效或拒绝投递)"
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* session/close(A.2 ⑦,D4 用后即毁——回收引擎驻留内存,SQLite 持久化保留)。
|
|
411
|
+
* best-effort:失败只 warn 不抛(会话已终态,close 失败只泄漏一个驻留会话,
|
|
412
|
+
* 不应炸掉已成功/已失败的任务结论)。连接已死时跳过——close 是 request 语义,
|
|
413
|
+
* 对死连接发请求会触发惰性重建(凭空 spawn 一代无人使用的进程)。
|
|
414
|
+
*/
|
|
415
|
+
async closeSession(sessionId: string): Promise<void> {
|
|
416
|
+
if (!this.conn.alive) return;
|
|
417
|
+
try {
|
|
418
|
+
await this.conn.request(
|
|
419
|
+
"session/close",
|
|
420
|
+
{ sessionId },
|
|
421
|
+
{ timeoutMs: ZCODE_APPSERVER_TURN_CLOSE_TIMEOUT_MS }
|
|
422
|
+
);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
logger.warn(
|
|
425
|
+
`session/close 失败(会话 ${sessionId},best-effort 忽略): ${errMessage(
|
|
426
|
+
err
|
|
427
|
+
)}`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* 单任务自包含入口(§2.4 ①-⑦):create → subscribe → send → 事件流
|
|
434
|
+
* (onTextDelta)→ 终态 → read 兜底 → resolve {response, usage, sessionId}
|
|
435
|
+
* → close。resolve 严格晚于全部事件回调(不变量 2);close 走 finally——成功/
|
|
436
|
+
* 失败路径都回收会话(D4)。RPC 错误(含 -32601/-32602 漂移类)原样透传(R5 消费)。
|
|
437
|
+
*/
|
|
438
|
+
async runTurn(
|
|
439
|
+
params: SessionCreateParams,
|
|
440
|
+
content: string,
|
|
441
|
+
opts: SessionTurnOptions = {}
|
|
442
|
+
): Promise<SessionTurnResult> {
|
|
443
|
+
if (content === "")
|
|
444
|
+
throw new Error("runTurn: content 必填(session/send 空投递无意义)");
|
|
445
|
+
const sessionId = await this.createSession(params);
|
|
446
|
+
// create 应答后立即回调(§3.4 不变量 3 的 sessionRef 时点;引擎的
|
|
447
|
+
// onHandleReady 回填与 abort 链 stop 目标都从这里取 sessionId)
|
|
448
|
+
opts.onSessionCreated?.(sessionId);
|
|
449
|
+
const opened = this.openTurn(sessionId, opts);
|
|
450
|
+
try {
|
|
451
|
+
await this.subscribe(sessionId);
|
|
452
|
+
try {
|
|
453
|
+
await this.send(sessionId, content);
|
|
454
|
+
} catch (err) {
|
|
455
|
+
// send 未送达(-32010 busy / -32602 漂移 / 连接死):turn 无从等待终态
|
|
456
|
+
opened.turn.fail(err instanceof Error ? err : new Error(String(err)));
|
|
457
|
+
void opened.done.catch(() => {}); // done 已无 await 方,防 dangling rejection
|
|
458
|
+
throw err;
|
|
459
|
+
}
|
|
460
|
+
const terminal = await opened.done;
|
|
461
|
+
// 终态后 read 兜底(D4:read 是全文权威来源——不变量 1 双来源之二;
|
|
462
|
+
// 失败/提取不到走收尾帧→delta 聚合降级链,不抛)
|
|
463
|
+
const readResult = await this.readBestEffort(sessionId);
|
|
464
|
+
const readText = extractAssistantText(readResult);
|
|
465
|
+
const response =
|
|
466
|
+
readText ?? opened.turn.finalText ?? opened.turn.deltas.join("");
|
|
467
|
+
const usage = opened.turn.finalUsage ?? extractReadUsage(readResult);
|
|
468
|
+
return {
|
|
469
|
+
sessionId,
|
|
470
|
+
response,
|
|
471
|
+
...(usage !== undefined ? { usage } : {}),
|
|
472
|
+
terminal,
|
|
473
|
+
};
|
|
474
|
+
} finally {
|
|
475
|
+
clearTimeout(opened.timer);
|
|
476
|
+
this.activeTurns.delete(sessionId);
|
|
477
|
+
await this.closeSession(sessionId);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ============================================================
|
|
482
|
+
// 内部:turn 生命周期与推送泵
|
|
483
|
+
// ============================================================
|
|
484
|
+
|
|
485
|
+
private openTurn(
|
|
486
|
+
sessionId: string,
|
|
487
|
+
opts: SessionTurnOptions
|
|
488
|
+
): { turn: ActiveTurn; done: Promise<TerminalInfo>; timer: NodeJS.Timeout } {
|
|
489
|
+
let resolveDone!: (t: TerminalInfo) => void;
|
|
490
|
+
let rejectDone!: (err: Error) => void;
|
|
491
|
+
const done = new Promise<TerminalInfo>((res, rej) => {
|
|
492
|
+
resolveDone = res;
|
|
493
|
+
rejectDone = rej;
|
|
494
|
+
});
|
|
495
|
+
const turn: ActiveTurn = {
|
|
496
|
+
sessionId,
|
|
497
|
+
callbacks: { onTextDelta: opts.onTextDelta },
|
|
498
|
+
settled: false,
|
|
499
|
+
deltas: [],
|
|
500
|
+
finalText: undefined,
|
|
501
|
+
finalUsage: undefined,
|
|
502
|
+
terminal: undefined,
|
|
503
|
+
// 装配占位:下方 timer 创建后重绑(settle/fail 需要 clearTimeout)
|
|
504
|
+
settle: () => {},
|
|
505
|
+
fail: () => {},
|
|
506
|
+
};
|
|
507
|
+
const timeoutMs =
|
|
508
|
+
opts.turnTimeoutMs ?? ZCODE_APPSERVER_TURN_DEFAULT_TIMEOUT_MS;
|
|
509
|
+
const timer = setTimeout(() => {
|
|
510
|
+
turn.settled = true;
|
|
511
|
+
this.activeTurns.delete(sessionId);
|
|
512
|
+
rejectDone(
|
|
513
|
+
new Error(
|
|
514
|
+
`一轮未在 ${timeoutMs}ms 内观察到终态(turn.terminal 与收尾帧均未到达)。` +
|
|
515
|
+
"恢复指引:跑 app-server 探针冒烟核对协议漂移后重试;或经 abort 链(session/stop)清场后重跑任务。"
|
|
516
|
+
)
|
|
517
|
+
);
|
|
518
|
+
}, timeoutMs);
|
|
519
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
520
|
+
turn.settle = (t: TerminalInfo): void => {
|
|
521
|
+
if (turn.settled) return;
|
|
522
|
+
turn.settled = true;
|
|
523
|
+
turn.terminal = t;
|
|
524
|
+
clearTimeout(timer);
|
|
525
|
+
resolveDone(t);
|
|
526
|
+
};
|
|
527
|
+
turn.fail = (err: Error): void => {
|
|
528
|
+
if (turn.settled) return;
|
|
529
|
+
turn.settled = true;
|
|
530
|
+
clearTimeout(timer);
|
|
531
|
+
rejectDone(err);
|
|
532
|
+
};
|
|
533
|
+
this.activeTurns.set(sessionId, turn);
|
|
534
|
+
return { turn, done, timer };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private lookupTurn(sessionId: string | undefined): ActiveTurn | undefined {
|
|
538
|
+
if (sessionId !== undefined) return this.activeTurns.get(sessionId);
|
|
539
|
+
// 无 sid(A.2 telemetry 帧未标会话归属):仅敢归因到唯一未落定 turn,
|
|
540
|
+
// 多在途宁丢勿错(旧实现同策略——防串线)
|
|
541
|
+
const pending: ActiveTurn[] = [];
|
|
542
|
+
for (const t of this.activeTurns.values()) if (!t.settled) pending.push(t);
|
|
543
|
+
return pending.length === 1 ? pending[0] : undefined;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private handleSessionEvent(params: unknown): void {
|
|
547
|
+
const turn = this.lookupTurn(extractPushSessionId(params));
|
|
548
|
+
if (turn === undefined) return;
|
|
549
|
+
const payload =
|
|
550
|
+
isRecord(params) && isRecord(params.payload) ? params.payload : undefined;
|
|
551
|
+
if (payload === undefined) return;
|
|
552
|
+
if (this.applyFinalFrame(turn, payload)) return;
|
|
553
|
+
this.applyStreamDelta(turn, payload);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* 收尾帧判定与吸收(A.2 ⑤ 两种形态:payload.response / stopReason+content 变体)。
|
|
558
|
+
* 命中任一形态则落 finalText/finalUsage 并以 final-frame 宽松终态收口,返回 true。
|
|
559
|
+
*/
|
|
560
|
+
private applyFinalFrame(
|
|
561
|
+
turn: ActiveTurn,
|
|
562
|
+
payload: Record<string, unknown>
|
|
563
|
+
): boolean {
|
|
564
|
+
if (typeof payload.response === "string" && payload.response !== "") {
|
|
565
|
+
// 收尾帧(A.2 ⑤):全文 + usage。终态落定后到达仍吸收数据(它常与
|
|
566
|
+
// turn.terminal 同批次、且是 usage 权威来源——属终态数据不是增量回调)
|
|
567
|
+
turn.finalText = payload.response;
|
|
568
|
+
if (isRecord(payload.usage)) turn.finalUsage = payload.usage;
|
|
569
|
+
turn.settle({ status: "success", source: "final-frame" });
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
if (
|
|
573
|
+
payload.stopReason === "stop" &&
|
|
574
|
+
typeof payload.content === "string" &&
|
|
575
|
+
payload.content !== ""
|
|
576
|
+
) {
|
|
577
|
+
// 收尾的 content 形态变体(旧实测与 response 同值,携带 usage)
|
|
578
|
+
turn.finalText = payload.content;
|
|
579
|
+
if (isRecord(payload.usage)) turn.finalUsage = payload.usage;
|
|
580
|
+
turn.settle({ status: "success", source: "final-frame" });
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** 增量 delta 分发:非空 delta 入账 + 回调;终态后迟到丢弃(不变量 2)。 */
|
|
587
|
+
private applyStreamDelta(
|
|
588
|
+
turn: ActiveTurn,
|
|
589
|
+
payload: Record<string, unknown>
|
|
590
|
+
): void {
|
|
591
|
+
if (typeof payload.delta !== "string" || payload.delta === "") return;
|
|
592
|
+
if (turn.settled) {
|
|
593
|
+
logger.warn(
|
|
594
|
+
`终态后迟到的 delta 丢弃(会话 ${
|
|
595
|
+
turn.sessionId
|
|
596
|
+
},不变量 2:resolve 后不再发事件): ${payload.delta.slice(
|
|
597
|
+
0,
|
|
598
|
+
DELTA_LOG_CHARS
|
|
599
|
+
)}`
|
|
600
|
+
);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
turn.deltas.push(payload.delta);
|
|
604
|
+
turn.callbacks.onTextDelta?.(payload.delta);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private handleTelemetry(params: unknown): void {
|
|
608
|
+
if (!isRecord(params)) return;
|
|
609
|
+
if (params.kind === "turn.terminal") {
|
|
610
|
+
// 终态权威(A.2 ⑤):status success/error 均算终态(旧实证:不归类挂到超时)
|
|
611
|
+
const turn = this.lookupTurn(extractPushSessionId(params));
|
|
612
|
+
if (turn === undefined) return;
|
|
613
|
+
turn.settle({
|
|
614
|
+
status: typeof params.status === "string" ? params.status : "unknown",
|
|
615
|
+
source: "turn.terminal",
|
|
616
|
+
});
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (params.kind === "stream.chunk") {
|
|
620
|
+
// A.2 ④:stream.chunk 无文本(chunkLength 遥测)。A.5 未确认项:是否偶发
|
|
621
|
+
// 携带文本字段——保留旧实现的形态漂移兜底:带文本则当 delta 收(不变量 1)
|
|
622
|
+
const turn = this.lookupTurn(extractPushSessionId(params));
|
|
623
|
+
if (turn === undefined || turn.settled) return;
|
|
624
|
+
for (const key of ["chunk", "text", "content"] as const) {
|
|
625
|
+
const v = params[key];
|
|
626
|
+
if (typeof v === "string" && v !== "") {
|
|
627
|
+
turn.deltas.push(v);
|
|
628
|
+
turn.callbacks.onTextDelta?.(v);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** 终态后 read(A.2 ⑥)。失败/超时不抛——降级链由 runTurn 收口(read 只兜底不致命)。
|
|
636
|
+
* 连接已死时跳过(与 closeSession 同判——对死连接发请求会凭空重建进程)。 */
|
|
637
|
+
private async readBestEffort(sessionId: string): Promise<unknown> {
|
|
638
|
+
if (!this.conn.alive) return undefined;
|
|
639
|
+
try {
|
|
640
|
+
return await this.conn.request(
|
|
641
|
+
"session/read",
|
|
642
|
+
{ sessionId },
|
|
643
|
+
{
|
|
644
|
+
timeoutMs: ZCODE_APPSERVER_TURN_READ_TIMEOUT_MS,
|
|
645
|
+
}
|
|
646
|
+
);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
logger.warn(
|
|
649
|
+
`session/read 兜底失败(会话 ${sessionId},降级收尾帧/delta 聚合): ${errMessage(
|
|
650
|
+
err
|
|
651
|
+
)}`
|
|
652
|
+
);
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|