@ai-workbench/remote-agent 0.8.13 → 0.8.15
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 +10 -8
- package/dist/acp.d.ts +12 -7
- package/dist/acp.js +65 -41
- package/dist/broker-runtime.js +46 -20
- package/dist/broker.d.ts +4 -2
- package/dist/broker.js +18 -9
- package/dist/claude-local.d.ts +2 -1
- package/dist/claude-local.js +62 -35
- package/dist/close-diagnostics.d.ts +56 -0
- package/dist/close-diagnostics.js +104 -0
- package/dist/conversation-details.js +67 -9
- package/dist/employee-endpoint.d.ts +3 -1
- package/dist/employee-endpoint.js +8 -6
- package/dist/employee-exec.d.ts +7 -4
- package/dist/employee-exec.js +39 -29
- package/dist/employee-mcp.d.ts +6 -3
- package/dist/employee-mcp.js +75 -39
- package/dist/employee-presentation.d.ts +6 -0
- package/dist/employee-presentation.js +16 -0
- package/dist/employee-provider.js +59 -36
- package/dist/employee-shell.d.ts +2 -0
- package/dist/employee-shell.js +17 -5
- package/dist/employee-ssh.d.ts +2 -0
- package/dist/employee-ssh.js +6 -4
- package/dist/employee-tools.d.ts +3 -1
- package/dist/employee-tools.js +6 -5
- package/dist/index.d.ts +18 -4
- package/dist/index.js +183 -55
- package/dist/model-catalog.d.ts +9 -1
- package/dist/model-catalog.js +63 -5
- package/dist/model-configuration.d.ts +14 -1
- package/dist/model-configuration.js +122 -89
- package/dist/model-presentation.d.ts +13 -0
- package/dist/model-presentation.js +18 -0
- package/dist/presentation.js +2 -1
- package/dist/provider.d.ts +8 -2
- package/dist/recovery.d.ts +4 -0
- package/dist/rpc-namespace.d.ts +2 -2
- package/dist/rpc-namespace.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
**`@ai-workbench/remote-agent@0.8.
|
|
1
|
+
**`@ai-workbench/remote-agent@0.8.15` 发布形态包。模型菜单逐项解析与同次展示目录保持一致;失效选择明确提示重新选择,发送准备和每轮发送仍独立查可信目录。模型型号、推理等级和来源只进技术详情;本机执行说明每个会话首次一句,完整限制可在技术详情查阅。编排已回证0.8.14 Claude在leo本地文件夹首轮成功;0.8.15仍待真机复验,Codex历史EMPLOYEE_EXEC_CLOSED根因仍未查明。X2出固定包及SHA,不publish。通过官方插件 → 添加插件安装,由编排发布;X2不publish。**
|
|
2
2
|
|
|
3
3
|
# 远程ACP插件
|
|
4
4
|
|
|
5
5
|
一个插件提供 **Claude(远程主机)** 和 **Codex(远程主机)**,通过员工主机上的官方CLI运行。Codex保留单次模式;Claude固定会话模式。只用官方模型列表、插件管理和配置入口,不提供自制面板、问答或登录按钮。登录和原生工作区绑定唯一归连接插件L。
|
|
6
6
|
|
|
7
|
-
当前版本号为0.8.
|
|
7
|
+
当前版本号为0.8.15;包名采用`@ai-workbench/remote-agent`以涵盖两个品牌;本轮按编排放行准备发布形态包,供 leo 真机自验;X 不执行实际发布,由编排统一发布。开发fixture逐字节保留L0.7.19固定包,公开类型与SSH导出从该包消费。运行时仍要求L的`getContractInfo()`同时返回API0.7.0、implemented、native-local-v1;旧L包、draft.3或只有版本号的不完整实现会明确报缺项。依赖范围为`~0.7.19`,L由官方插件管理单独安装,不打进X。组件验证不代表真实开通、模型或leo E通过;HanaMesh另验。
|
|
8
8
|
|
|
9
9
|
已包含L0.7.18消费增量:来源确认、输入接纳、登录、本机接纳、输入准备、模型目录及运行时解析的早期失败,用正式`preflightDiagnostics:1` / `preflight-check`进入L「连接前技术详情(含登录与工作区确认)」。阶段、真实来源/状态和错误代码可展开;未知来源为null,旧login-check的完整安全原因保留。记录不选择来源、不接纳输入、不自动重发;无原生会话的菜单查询仍在模型设置报告。当前工程和设置组件验证不等于leo GUI验收。
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
## 员工路径
|
|
14
14
|
|
|
15
|
-
从官方插件管理安装,照常使用默认工作区/会话/模型入口。L0.7.17沿唯一“远程主机连接”设置节记录文件来源;主机来源沿用L既有解析;本地来源接入N0.4.4/S0.4.11员工端点。旧映射可在本轮官方一次确认中选择改用本机文件夹;拒绝、取消或输入/来源变化时不切换、不执行。也可在L设置逐工作区确认沿用原主机关联,新主机位置仍按L现有合同停止为WORKSPACE_LOCATION_PROVIDER_REQUIRED。本地六类工具经Claude公开toolAliases路由到员工电脑:cwd是员工选定目录,Git使用本机仓库,不回退主机同名目录。本地真实员工输入先取得官方当前轮次CAS预期,再调用L0.7.17 acceptLocalWorkspaceUserTurn;核回包后才打开D19和本轮主机目录/执行。每轮localAcceptanceId冻结进原详情sink,迟到结果不借新轮ID;LOCAL_USER_TURN_CHANGED立即停止本轮,不重接纳或退legacy。已用固定真包完成本地D19
|
|
15
|
+
从官方插件管理安装,照常使用默认工作区/会话/模型入口。L0.7.17沿唯一“远程主机连接”设置节记录文件来源;主机来源沿用L既有解析;本地来源接入N0.4.4/S0.4.11员工端点。旧映射可在本轮官方一次确认中选择改用本机文件夹;拒绝、取消或输入/来源变化时不切换、不执行。也可在L设置逐工作区确认沿用原主机关联,新主机位置仍按L现有合同停止为WORKSPACE_LOCATION_PROVIDER_REQUIRED。本地六类工具经Claude公开toolAliases路由到员工电脑:cwd是员工选定目录,Git使用本机仓库,不回退主机同名目录。本地真实员工输入先取得官方当前轮次CAS预期,再调用L0.7.17 acceptLocalWorkspaceUserTurn;核回包后才打开D19和本轮主机目录/执行。每轮localAcceptanceId冻结进原详情sink,迟到结果不借新轮ID;LOCAL_USER_TURN_CHANGED立即停止本轮,不重接纳或退legacy。已用固定真包完成本地D19组件联测;编排已回证0.8.14 Claude/Opus5/Default在leo本地目录首轮写文件成功;该回证不覆盖本0.8.15或Codex,产品验收以各版本leo实际结果为准。本地文件不自动上传,X不猜目录、不选首项。首次说明使用官方普通回复;先经官方会话提交、持久化并读回核对,才向L确认。持久记录不等于员工已看见,GUI另验;日志丢失后的未知claim由L保留,不宣称端到端恰好一次。
|
|
16
16
|
|
|
17
17
|
未登录时两个模型均回复:**需要登录,请到设置 → 远程主机连接 点登录**。无L或缺正式能力时直接说明缺项,不让员工填写主机、用户、路径或密钥。
|
|
18
18
|
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
|
|
25
25
|
文本、思考、工具步骤/正文/补丁、计划、通知映射为官方普通流;远端工具不变成本机工具调用。图片输入由官方附件服务读取已核验字节,经ACP传给主机,不传员工文件路径。非文本输出无法得到官方持久附件引用时,明确说明收到的内容类型与所缺能力,不假造图片引用。远端结构化提问、未映射扩展会明确提示并取消,不静默丢弃。
|
|
26
26
|
|
|
27
|
-
首次系统/开发者上下文按协商的ACP系统能力传入。**首版不支持:已有Claude会话中立即生效的系统/开发者上下文变更。** 固定适配器说明恢复后的变更要到下次上下文整理才生效;X
|
|
27
|
+
首次系统/开发者上下文按协商的ACP系统能力传入。**首版不支持:已有Claude会话中立即生效的系统/开发者上下文变更。** 固定适配器说明恢复后的变更要到下次上下文整理才生效;X按已有公开 resume/load 传送新要求,技术详情注明延迟生效,继续本轮;不宣称本轮已立即采用,不自动整理或重试。实际 RPC 拒绝或缺少公开更新能力仍会明确停止。未声明系统能力的agent会明确报缺项。
|
|
28
28
|
|
|
29
29
|
`usage_update`的used/size是上下文占用,不能当作本轮输入/输出计费量;远端真实prompt响应usage及Claude quota令牌计数是会话累计,按保存在当前实例/恢复标识中的可信基线减差后进入DSH本轮用量;重复值不重复结算。恢复缺少基线、计数缺失、回退或来源改变时明确不计入该轮,重建可信基线,不能把历史用量算成本轮。
|
|
30
30
|
|
|
31
|
-
会话模式跨轮保留同一ACP客户端和主机进程;关闭实际agent/session或插件时精确清理。重开恢复优先协商resume,无能力时明确使用load,不静默新建。恢复标识绑定品牌、员工目标、DSH会话和L工作区;复制/分叉不能沿用父远端会话。主机来源沿用D24同实例自动恢复/安全重建。本地来源重开后原MCP socket失效,先由既有S同实例条件流程结束旧broker再重建,不代答旧审批;未获真实清理确认时保留未知,不自动重放。无TTL
|
|
31
|
+
会话模式跨轮保留同一ACP客户端和主机进程;关闭实际agent/session或插件时精确清理。重开恢复优先协商resume,无能力时明确使用load,不静默新建。恢复标识绑定品牌、员工目标、DSH会话和L工作区;复制/分叉不能沿用父远端会话。主机来源沿用D24同实例自动恢复/安全重建。本地来源重开后原MCP socket失效,先由既有S同实例条件流程结束旧broker再重建,不代答旧审批;未获真实清理确认时保留未知,不自动重放。无TTL或隐式淘汰上限。缺私有恢复 pointer 只撤回恢复资格;只有原 slot 不存在已核实,或同 UUID 条件结束与 list 消失均确认,才新建并仅使用官方本次接纳的输入 ID。纯发送前失败经确认清理后不永久封住新轮;真实输入送达未知、dirty 或清理未知仍阻断重放。
|
|
32
32
|
|
|
33
33
|
Codex单次模式首版仅文本,不转接中途审批;图片或系统/开发者上下文需使用会话模式。文件传输、远端VFS与自动同步不在本轮。其他模型不锁定、不拦截、不记录。
|
|
34
34
|
|
|
@@ -74,9 +74,9 @@ D22阶段耗时进入技术详情;D23文件审批用中文动作,原工具
|
|
|
74
74
|
|
|
75
75
|
D21(0.8.5):两个品牌的官方模型菜单从 L 的 `modelCatalogTarget:1` / `getModelCatalogTarget` 取得当前可信员工目标,再只读调用 S 的 `aw-model-catalog --provider codex|claude`。目录按 host-user 作用域展示完整模型与逐模型推理等级,保留远端标识原值;失败显示“无法获取模型列表:原因”,不登录、不创建项目、不返回替代模型。L 必须宣告该正式能力,S 必须安装保护入口;这两项未就绪时不能据组件测试声称菜单可用。
|
|
76
76
|
|
|
77
|
-
每轮先核对目录代次和所选模型,再经 ACP 逐项配置模型及推理等级,收到完整配置确认后才发送输入。Claude
|
|
77
|
+
每轮先核对目录代次和所选模型,再经 ACP 逐项配置模型及推理等级,收到完整配置确认后才发送输入。Claude 别名每轮须有精确的模型配置成功回执,并由同一员工主机、同一连接代际的目录唯一映射到具体型号。具体型号注明来自主机目录,会话未单独回报;旧恢复记录不作本轮证据。目录缺项、映射不唯一、回执不符或身份/代际变化时不发送输入。所选值、远端确认值及型号来源仅在恢复标识与技术详情保留,插件不向真实回复追加技术参数,确认配置不等于验证推理效果。连接/身份/目录变化清除旧确认;远端清理待定或结果未知时阻止派发,不自动清理重试或另开替代会话。Codex 单次模式当前不能确认此选择,会在发送前明确拒绝。新增设置 0;本轮工程证据、真实只读链路与产品未验项见 `docs/evidence/2026-10-22-X2-D21/`。
|
|
78
78
|
|
|
79
|
-
0.8.5统一包原依赖L `~0.7.13`;当前0.8.
|
|
79
|
+
0.8.5统一包原依赖L `~0.7.13`;当前0.8.15对齐L `~0.7.19`。D8模型目录双事件长期监听保留;同epoch的recovery提示/失败消息变化不触发执行状态失效,真实实例/epoch/phase/可信目标/登录变化仍失效。完整D9两品牌正文/状态/就绪门呈现按编排00:50允许的范围记下一版,本版不宣称已消费完成;L的connectionMessages.administratorAfterMs仍只由L管理,X新增设置0。
|
|
80
80
|
|
|
81
81
|
D18(0.8.6):Codex 需要主机自有 adapter `2.2.2-aw.1` 的明确新建会话上下文政策,官方 CLI 仍为未改的0.160.1。系统/开发者要求仅在新建时生效;恢复时与初建摘要相同则正常恢复且省略新建专用字段,不声称恢复接口会更新指令。要求不同或清空时,在同一真实回复说明“会话要求已变化,已新建 Codex 会话继续处理,原会话仍保留。”,并新建远端承接公开对话,保留旧标识和查询记录;旧/非法初建摘要明确拒绝,不能猜测。
|
|
82
82
|
|
|
@@ -88,7 +88,7 @@ D18(0.8.6):Codex 需要主机自有 adapter `2.2.2-aw.1` 的明确新建
|
|
|
88
88
|
|
|
89
89
|
Read/Write/Edit/Glob/Grep由员工DSH同UID后端执行;Bash经L严格SSH到S同代桥再经N员工SSH端点执行,命令正文只走stdin。每次调用核官方会话唯一工作区、L实例/epoch/绑定/目录身份、N真实prepare及S实时target。取消、SSH255或退出证明缺失记录UNKNOWN,不重放。只关闭自己的绑定执行器,不撤其他工作区或设备服务。cwd不是权限根,绝对路径和上级访问遵循真实OS权限;不增加目录白名单。
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
本机执行在每个DSH会话首次用一句“文件和命令在你的电脑上执行”说明;既有0.8.14说明也计为已显示。完整限制每轮记录在技术详情,遇到不支持的操作再明确提示:子代理和harness内部直接工具调用尚未证明可在员工端执行,可能仍在主机;已识别的原生未改道工具/Agent审批拒绝。PDF等二进制读取及后台命令明确不支持,不静默主机代跑。六类组件/MCP/配置转交证明不等于真实Claude原生alias执行或真实员工链路通过。实际Git依赖员工环境中已有Git,缺失会返回实际命令失败,不在主机替代,也不自动安装。
|
|
92
92
|
|
|
93
93
|
本地目前仅支持macOS(arm64/x64);Grep固定携带微软@vscode/ripgrep-universal1.18.0的两架构原字节并核SHA,无员工PATH搜索或安装下载。主机需S0.4.11,N可选peer~0.4.4(缺失明确报能力缺项),L当前~0.7.18,本地接纳与D19工程路径已接,真实员工端门仍开放。无DSH/CLI修改、额外设置节或npm发布。
|
|
94
94
|
|
|
@@ -122,3 +122,5 @@ D20(0.8.8开发记录):本地来源的Codex原生文件与命令通过固
|
|
|
122
122
|
0.8.12目录诊断修复:入口为“设置 → 模型 → 模型目录诊断”,官方既有 settings.models.footer 公开扩展位直接显示;“内置插件 → 远程助手参数”也可只读查看,即使参数命名空间暂未提供也保留入口;参数编辑使用当前插件公开Loader entry id对应的官方ConfigForms。该页只读本机记录,刷新不查询主机、不登录、不发送待发送原话。模型页原错误行仍通过公开提供方目录发布,诊断数据不再调用directory.replace或发布适配器拓扑变化;changed只负责真实连接/目标代次变化,菜单提示指向上述实际入口。
|
|
123
123
|
|
|
124
124
|
每次目录SSH保独立事件ID、品牌、UTC起止、耗时、PID、L实例/epoch/phase、严格L argv/目标/known_hosts/ProxyCommand、退出码/信号、完整脱敏stderr与原字节数/SHA256、stdout字节数/SHA及主机typed错误;不记录凭据或整份模型目录。日志在官方profile的 `ai-workbench-remote-agent/catalog-diagnostics/<eventId>-started.json` 与 `-closed.json`,700目录/600文件、fsync;无自动过期,最新两品牌在界面可展开,旧调用保留不可变历史。容量沿现有`acpMaxMessageBytes`,超限停止并标记stderrComplete=false,UTF-8替换另显式标记,未返回部分模型;保存失败明确显示仅内存,不冒称耐久。旧0811丢失的stderr无法还原。同品牌、instanceId、epoch只合并仍在进行的查询;完成后不缓存,身份/代际/可信目标改变即作废。调用方各持独立lease/取消与错误身份,一个取消不会取消其他等待者;全员取消才撤销该次查询,取消中的旧进程结束前同作用域新调用等待清理。登录检查的显示变化不会取消无需登录的host-user目录。此次没有增加超时、重试、SSH连接复用或产品设置;不等于255、Claude主机错误及N层根因已修复,也未进行leo GUI验收。
|
|
125
|
+
|
|
126
|
+
0.8.14 不把空菜单、非穷举 picker、可选 UI category/label 或同值配置通知当作额外发送门槛;每轮仍要求可靠模型入口与 fresh exact setter ACK。本轮完成后的配置变化只记录 warning,保留真实回复与 finish,并明确未获单独的实际执行型号回报。同一身份、代际与可信目标的 needCode/诊断变化只记 info;真实身份、代际、目标或能力失效仍撤回。每次关闭/取消保留 caller、字段前后值和首次观测原因(不等于根因)。新增配置、阈值、超时、重试为 0。
|
package/dist/acp.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CloseDiagnostics, type CloseCause } from './close-diagnostics.ts';
|
|
1
2
|
import * as stable from '@agentclientprotocol/sdk';
|
|
2
3
|
import * as acp from '@agentclientprotocol/sdk/unstable';
|
|
3
4
|
import { NeedError, type Need } from './remote.ts';
|
|
@@ -9,6 +10,7 @@ import { type ModelConfigurationSnapshot } from './model-configuration.ts';
|
|
|
9
10
|
import type { EmployeeRouting } from './employee-relay.ts';
|
|
10
11
|
export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
|
|
11
12
|
export interface AcpHooks {
|
|
13
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
12
14
|
timing?: StageTiming;
|
|
13
15
|
onUpdate?: (notification: acp.SessionNotification) => void;
|
|
14
16
|
onResponse?: (response: acp.PromptResponse) => void;
|
|
@@ -36,6 +38,7 @@ export type AcpStartMethod = 'session/new' | 'session/resume' | 'session/load';
|
|
|
36
38
|
export declare class AcpSession {
|
|
37
39
|
private connection;
|
|
38
40
|
private hooks;
|
|
41
|
+
private closeDiagnostics;
|
|
39
42
|
private sessionId?;
|
|
40
43
|
private turnSignal?;
|
|
41
44
|
private turnToken?;
|
|
@@ -44,6 +47,7 @@ export declare class AcpSession {
|
|
|
44
47
|
private negotiated;
|
|
45
48
|
private method?;
|
|
46
49
|
private ready;
|
|
50
|
+
private closeRequested;
|
|
47
51
|
private updating;
|
|
48
52
|
private cwd?;
|
|
49
53
|
private systemPrompt;
|
|
@@ -55,15 +59,13 @@ export declare class AcpSession {
|
|
|
55
59
|
private configRevision;
|
|
56
60
|
private selected?;
|
|
57
61
|
private selecting;
|
|
58
|
-
private resolvedEvidence?;
|
|
59
62
|
private pendingConfigurations;
|
|
63
|
+
private dispatchedPrompts;
|
|
64
|
+
get promptDispatchCount(): number;
|
|
60
65
|
get modelConfiguration(): ModelConfigurationSnapshot | undefined;
|
|
61
|
-
get resolvedModelEvidence(): Readonly<{
|
|
62
|
-
modelValue: string;
|
|
63
|
-
resolvedModel: string;
|
|
64
|
-
}> | undefined;
|
|
65
66
|
get models(): readonly import("./model-configuration.ts").ModelCatalogValue[];
|
|
66
67
|
private receiveConfiguration;
|
|
68
|
+
private pendingConfiguration;
|
|
67
69
|
private resetConfiguration;
|
|
68
70
|
private lifecycleConfiguration;
|
|
69
71
|
selectModel(model: string, effort?: string, signal?: AbortSignal): Promise<ModelConfigurationSnapshot>;
|
|
@@ -77,6 +79,9 @@ export declare class AcpSession {
|
|
|
77
79
|
private need;
|
|
78
80
|
private checkReporting;
|
|
79
81
|
constructor(transport: acp.Stream | acp.AgentApp | stable.AgentApp, hooks: AcpHooks);
|
|
82
|
+
/** A rejected request on an already-closed SDK never reaches this write.
|
|
83
|
+
* Once a transport write starts, failure remains delivery-unknown. */
|
|
84
|
+
private observeDispatch;
|
|
80
85
|
private observe;
|
|
81
86
|
get id(): string | undefined;
|
|
82
87
|
get capabilities(): Readonly<acp.AgentCapabilities>;
|
|
@@ -92,8 +97,8 @@ export declare class AcpSession {
|
|
|
92
97
|
start(cwd: string, resumeId?: string, signal?: AbortSignal, context?: AcpStartContext, prompt?: acp.ContentBlock[]): Promise<string>;
|
|
93
98
|
/** Codex context is immutable after creation; keep the old remote session intact. */
|
|
94
99
|
newContextSession(context: AcpStartContext, signal?: AbortSignal): Promise<string>;
|
|
95
|
-
updateContext(context: AcpStartContext, signal?: AbortSignal): Promise<AcpStartMethod | undefined>;
|
|
100
|
+
updateContext(context: AcpStartContext, signal?: AbortSignal, force?: boolean): Promise<AcpStartMethod | undefined>;
|
|
96
101
|
prompt(input: string | acp.ContentBlock[], signal?: AbortSignal): Promise<acp.StopReason>;
|
|
97
|
-
close(): void;
|
|
102
|
+
close(cause?: CloseCause, error?: Error): void;
|
|
98
103
|
}
|
|
99
104
|
export declare function acpNeed(error: unknown): NeedError;
|
package/dist/acp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CloseDiagnostics } from "./close-diagnostics.js";
|
|
1
2
|
import * as acp from '@agentclientprotocol/sdk/unstable';
|
|
2
3
|
import { NeedError } from "./remote.js";
|
|
3
4
|
import { employeeFailure } from "./presentation.js";
|
|
@@ -10,6 +11,7 @@ import { ModelConfiguration, ModelConfigurationError } from "./model-configurati
|
|
|
10
11
|
export class AcpSession {
|
|
11
12
|
connection;
|
|
12
13
|
hooks;
|
|
14
|
+
closeDiagnostics;
|
|
13
15
|
sessionId;
|
|
14
16
|
turnSignal;
|
|
15
17
|
turnToken;
|
|
@@ -18,6 +20,7 @@ export class AcpSession {
|
|
|
18
20
|
negotiated = {};
|
|
19
21
|
method;
|
|
20
22
|
ready = false;
|
|
23
|
+
closeRequested = false;
|
|
21
24
|
updating = false;
|
|
22
25
|
cwd;
|
|
23
26
|
systemPrompt = '';
|
|
@@ -29,33 +32,29 @@ export class AcpSession {
|
|
|
29
32
|
configRevision = 0;
|
|
30
33
|
selected;
|
|
31
34
|
selecting = false;
|
|
32
|
-
resolvedEvidence;
|
|
33
35
|
pendingConfigurations = new Map();
|
|
36
|
+
dispatchedPrompts = 0;
|
|
37
|
+
get promptDispatchCount() { return this.dispatchedPrompts; }
|
|
34
38
|
get modelConfiguration() { return this.configuration?.snapshot; }
|
|
35
|
-
get resolvedModelEvidence() { return this.resolvedEvidence; }
|
|
36
39
|
get models() { if (!this.configuration)
|
|
37
40
|
throw new ModelConfigurationError('远端没有提供完整配置列表'); return this.configuration.models; }
|
|
38
|
-
receiveConfiguration(options) {
|
|
41
|
+
receiveConfiguration(options, routing) {
|
|
39
42
|
this.configRevision++;
|
|
40
43
|
if (this.configuration)
|
|
41
44
|
this.configuration.update(options);
|
|
42
45
|
else
|
|
43
46
|
this.configuration = new ModelConfiguration(options, async (configId, value) => {
|
|
44
47
|
const result = await this.connection.agent.request(acp.methods.agent.session.setConfigOption, { sessionId: this.sessionId, configId, value }, { cancellationSignal: this.turnSignal });
|
|
45
|
-
if (this.resolvedEvidence && (this.configuration?.snapshot.model !== this.resolvedEvidence.modelValue || (configId === this.configuration?.snapshot.modelConfigId && value !== this.resolvedEvidence.modelValue)))
|
|
46
|
-
this.resolvedEvidence = undefined;
|
|
47
48
|
this.hooks.broker?.acknowledge(acp.methods.agent.session.setConfigOption);
|
|
48
49
|
return result;
|
|
49
|
-
});
|
|
50
|
-
if (this.resolvedEvidence && this.configuration.snapshot.model !== this.resolvedEvidence.modelValue)
|
|
51
|
-
this.resolvedEvidence = undefined;
|
|
50
|
+
}, reason => this.detail({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_INFORMATION', summary: ({ PICKER_INCOMPLETE: '远端菜单没有穷举当前或所选值;以本轮精确配置回执为准。', CONFIG_UI_METADATA_INVALID: '远端配置的显示资料不完整;没有改变本轮选择。', UNRELATED_CONFIG_IGNORED: '远端有未使用的界面配置;未把它当模型配置入口。', OPTIONAL_CATEGORY_OMITTED: '远端省略了显示类别;继续使用本会话已核实的配置入口。', SAME_SELECTION_UPDATE: '收到同值或无关配置更新,本轮真实配置回执仍有效。' })[reason], payload: { reason } }), routing);
|
|
52
51
|
}
|
|
53
|
-
|
|
52
|
+
pendingConfiguration(sessionId, options) { const pending = this.pendingConfigurations.get(sessionId) ?? []; pending.push(options); this.pendingConfigurations.set(sessionId, pending); }
|
|
53
|
+
resetConfiguration(clearRoles = true) {
|
|
54
54
|
this.selected = undefined;
|
|
55
|
-
this.resolvedEvidence = undefined;
|
|
56
55
|
if (this.configuration) {
|
|
57
56
|
try {
|
|
58
|
-
this.
|
|
57
|
+
this.configuration.invalidate(clearRoles);
|
|
59
58
|
}
|
|
60
59
|
catch { /* Invalidated until this lifecycle operation supplies new evidence. */ }
|
|
61
60
|
}
|
|
@@ -64,14 +63,22 @@ export class AcpSession {
|
|
|
64
63
|
lifecycleConfiguration(result, revision) {
|
|
65
64
|
const pending = this.pendingConfigurations.get(this.sessionId);
|
|
66
65
|
this.pendingConfigurations.clear();
|
|
67
|
-
if (pending)
|
|
68
|
-
this
|
|
66
|
+
if (pending) {
|
|
67
|
+
// A lifecycle response can identify selectors for this same session even
|
|
68
|
+
// when newer notifications omit UX categories. It never supplies an ACK.
|
|
69
|
+
if (result.configOptions)
|
|
70
|
+
try {
|
|
71
|
+
this.receiveConfiguration(result.configOptions);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
this.detail({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_INFORMATION', summary: '会话建立回执的配置资料由本会话较新通知重新核对,仍需本轮真实配置回执。', payload: { reason: 'LIFECYCLE_CONFIGURATION_SUPERSEDED' } });
|
|
75
|
+
}
|
|
76
|
+
for (const options of pending)
|
|
77
|
+
this.receiveConfiguration(options);
|
|
78
|
+
}
|
|
69
79
|
// A notification observed during the operation is newer than its in-flight response.
|
|
70
80
|
else if (this.configRevision === revision && result.configOptions)
|
|
71
81
|
this.receiveConfiguration(result.configOptions);
|
|
72
|
-
const model = this.configuration?.snapshot.model;
|
|
73
|
-
if (!pending && this.configRevision === revision + 1 && model && typeof result.models?.currentModelId === 'string' && result.models.currentModelId)
|
|
74
|
-
this.resolvedEvidence = { modelValue: model, resolvedModel: result.models.currentModelId };
|
|
75
82
|
}
|
|
76
83
|
async selectModel(model, effort, signal) {
|
|
77
84
|
if (!this.ready || !this.sessionId)
|
|
@@ -104,7 +111,14 @@ export class AcpSession {
|
|
|
104
111
|
reports = [];
|
|
105
112
|
reportingFailure;
|
|
106
113
|
timing = new StageTiming();
|
|
107
|
-
stage(phase, operation) {
|
|
114
|
+
async stage(phase, operation) { this.closeDiagnostics.stage = phase; try {
|
|
115
|
+
return await this.closeDiagnostics.run(phase, () => (this.hooks.timing ?? this.timing).run(phase, operation));
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (['acp-session-new', 'acp-session-resume', 'acp-session-load', 'acp-initialize'].includes(phase))
|
|
119
|
+
this.closeDiagnostics.record({ layer: 'acp', initiator: phase === 'acp-session-new' ? 'acp.session-new.failure' : 'acp.lifecycle.failure', reason: acpNeed(error).code, operation: phase, action: 'failure' });
|
|
120
|
+
throw error;
|
|
121
|
+
} }
|
|
108
122
|
get diagnostics() { return this.reports.map(entry => sanitizeObservation(entry)); }
|
|
109
123
|
detail(observation, hooks = this.hooks) {
|
|
110
124
|
const safe = sanitizeObservation(observation);
|
|
@@ -113,8 +127,10 @@ export class AcpSession {
|
|
|
113
127
|
hooks.onDetail?.(safe);
|
|
114
128
|
}
|
|
115
129
|
catch (cause) {
|
|
130
|
+
const first = !this.reportingFailure;
|
|
116
131
|
this.reportingFailure ??= { cause };
|
|
117
|
-
|
|
132
|
+
if (first)
|
|
133
|
+
this.close({ layer: 'acp', initiator: 'acp.reporting-failure', reason: 'CONVERSATION_DETAILS_FAILED', action: 'close' });
|
|
118
134
|
}
|
|
119
135
|
}
|
|
120
136
|
need(need, hooks = this.hooks) {
|
|
@@ -125,12 +141,13 @@ export class AcpSession {
|
|
|
125
141
|
throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能记录;本轮未继续发送', '可用的技术详情记录渠道'); }
|
|
126
142
|
constructor(transport, hooks) {
|
|
127
143
|
this.hooks = hooks;
|
|
144
|
+
this.closeDiagnostics = hooks.closeDiagnostics ?? new CloseDiagnostics(event => this.detail(event));
|
|
128
145
|
this.timing.bind(detail => this.detail(detail));
|
|
129
146
|
const app = acp.client({ name: '@ai-workbench/remote-agent' })
|
|
130
147
|
.onNotification(acp.methods.client.session.update, ({ params: notification }) => {
|
|
131
148
|
if (this.creatingReplacement) {
|
|
132
149
|
if (notification.sessionId !== this.sessionId && notification.update.sessionUpdate === 'config_option_update')
|
|
133
|
-
this.
|
|
150
|
+
this.pendingConfiguration(notification.sessionId, notification.update.configOptions);
|
|
134
151
|
this.detail({ kind: 'session', severity: 'info', code: 'ACP_IDLE_UPDATE', summary: '新会话建立期间的轨迹,未作为正文处理', method: 'session/update', payload: notification });
|
|
135
152
|
return;
|
|
136
153
|
}
|
|
@@ -140,9 +157,8 @@ export class AcpSession {
|
|
|
140
157
|
}
|
|
141
158
|
const meta = record(notification._meta);
|
|
142
159
|
if (notification.update.sessionUpdate === 'config_option_update') {
|
|
143
|
-
this.resolvedEvidence = undefined;
|
|
144
160
|
if (!this.sessionId)
|
|
145
|
-
this.
|
|
161
|
+
this.pendingConfiguration(notification.sessionId, notification.update.configOptions);
|
|
146
162
|
else
|
|
147
163
|
try {
|
|
148
164
|
this.receiveConfiguration(notification.update.configOptions);
|
|
@@ -258,21 +274,27 @@ export class AcpSession {
|
|
|
258
274
|
this.detail({ kind: 'extension', severity: 'warning', code: 'ACP_UNKNOWN_NOTIFICATION', summary: '收到未知远端扩展通知,已保留技术记录', method, payload: params });
|
|
259
275
|
});
|
|
260
276
|
if ('readable' in transport && 'writable' in transport)
|
|
261
|
-
this.connection = app.connect(isolateRpcIds(this.observe(transport)));
|
|
277
|
+
this.connection = app.connect(isolateRpcIds(this.observe(transport), message => this.observeDispatch(message)));
|
|
262
278
|
else {
|
|
263
279
|
// Stable and unstable apps use distinct private symbols. Bridge only their
|
|
264
280
|
// public stream interfaces so either SDK entry point remains supported.
|
|
265
281
|
const outward = new TransformStream();
|
|
266
282
|
const inward = new TransformStream();
|
|
267
283
|
const peer = transport.connect({ readable: outward.readable, writable: inward.writable });
|
|
268
|
-
this.connection = app.connect(isolateRpcIds(this.observe({ readable: inward.readable, writable: outward.writable })));
|
|
269
|
-
|
|
270
|
-
void
|
|
284
|
+
this.connection = app.connect(isolateRpcIds(this.observe({ readable: inward.readable, writable: outward.writable }), message => this.observeDispatch(message)));
|
|
285
|
+
const observeConnection = this.closeDiagnostics.capture();
|
|
286
|
+
void this.connection.closed.then(() => { observeConnection({ layer: 'acp', initiator: 'acp.connection-closed', reason: 'ACP_CONNECTION_CLOSED', action: 'close' }); peer.close(); });
|
|
287
|
+
void peer.closed.then(() => { observeConnection({ layer: 'acp', initiator: 'acp.peer-closed', reason: 'ACP_PEER_CLOSED', action: 'close' }); this.connection.close(); });
|
|
271
288
|
}
|
|
272
289
|
}
|
|
290
|
+
/** A rejected request on an already-closed SDK never reaches this write.
|
|
291
|
+
* Once a transport write starts, failure remains delivery-unknown. */
|
|
292
|
+
observeDispatch(message) { if (record(message)?.method === 'session/prompt' && Object.hasOwn(message, 'id'))
|
|
293
|
+
this.dispatchedPrompts++; }
|
|
273
294
|
observe(stream) {
|
|
274
295
|
// Observe already-parsed public Stream messages: no NDJSON parsing, second
|
|
275
296
|
// byte buffer, message-size policy, or transport fallback is introduced.
|
|
297
|
+
const observe = this.closeDiagnostics.capture();
|
|
276
298
|
const readable = stream.readable.pipeThrough(new TransformStream({ transform: (message, controller) => {
|
|
277
299
|
const raw = record(message);
|
|
278
300
|
const params = record(raw?.params);
|
|
@@ -289,13 +311,13 @@ export class AcpSession {
|
|
|
289
311
|
return;
|
|
290
312
|
}
|
|
291
313
|
controller.enqueue(message);
|
|
292
|
-
} }));
|
|
314
|
+
}, flush: () => observe({ layer: 'acp', initiator: 'acp.read-eof', reason: 'ACP_STREAM_EOF', action: 'eof' }) }));
|
|
293
315
|
return { readable, writable: stream.writable };
|
|
294
316
|
}
|
|
295
317
|
get id() { return this.sessionId; }
|
|
296
318
|
get capabilities() { return this.negotiated; }
|
|
297
319
|
get startMethod() { return this.method; }
|
|
298
|
-
get checkpoint() { return this.ready && !this.busy && !this.updating && !this.selecting && this.sessionId ? { sessionId: this.sessionId, capabilities: recoveryCapabilities(this.negotiated), ...(this.configuration ? { modelConfiguration: { configOptions: JSON.parse(JSON.stringify(this.configuration.snapshot.configOptions)),
|
|
320
|
+
get checkpoint() { return this.ready && !this.busy && !this.updating && !this.selecting && this.sessionId ? { sessionId: this.sessionId, capabilities: recoveryCapabilities(this.negotiated), ...(this.configuration ? { modelConfiguration: { configOptions: JSON.parse(JSON.stringify(this.configuration.snapshot.configOptions)), selectorRoles: { model: this.configuration.snapshot.modelConfigId, effort: this.configuration.snapshot.effortConfigId } } } : {}) } : undefined; }
|
|
299
321
|
restore(cwd, saved, context, verifiedSameContext = false) {
|
|
300
322
|
if (context.employeeRouting)
|
|
301
323
|
throw new NeedError('EMPLOYEE_CHECKPOINT_STALE', '本机工具通道需要重新建立,旧审批没有代答。', '按原主机实例安全结束后新建本机会话');
|
|
@@ -311,13 +333,12 @@ export class AcpSession {
|
|
|
311
333
|
// Old journals may lack new-only metadata; never manufacture it for them.
|
|
312
334
|
if (this.codexNewSessionOnly && !verifiedSameContext)
|
|
313
335
|
this.validateNewContext(this.systemPrompt, context.systemPrompt !== undefined);
|
|
336
|
+
// Legacy currentModelId/resolvedEvidence is not a per-turn setter ACK and
|
|
337
|
+
// never proves the concrete model. Only restore selectable config options.
|
|
314
338
|
if (saved.modelConfiguration) {
|
|
315
|
-
this.receiveConfiguration(saved.modelConfiguration.configOptions);
|
|
316
|
-
|
|
317
|
-
for (const update of saved.configUpdates ?? []) {
|
|
318
|
-
this.resolvedEvidence = undefined;
|
|
339
|
+
this.receiveConfiguration(saved.modelConfiguration.configOptions, saved.modelConfiguration.selectorRoles);
|
|
340
|
+
for (const update of saved.configUpdates ?? [])
|
|
319
341
|
this.receiveConfiguration(update);
|
|
320
|
-
}
|
|
321
342
|
}
|
|
322
343
|
this.hooks.broker?.finishRecovery();
|
|
323
344
|
this.detail({ kind: 'need', severity: 'info', code: 'ACP_CLIENT_REATTACHED', summary: '已接回原模型会话', payload: { sessionId: saved.sessionId } });
|
|
@@ -364,12 +385,12 @@ export class AcpSession {
|
|
|
364
385
|
throw new NeedError('SESSION_ALREADY_STARTED', '此连接已用于建立会话,不能转用工作区或身份', '新的独立远端连接');
|
|
365
386
|
this.startAttempted = true;
|
|
366
387
|
this.employeeMeta = context?.employeeExecution;
|
|
367
|
-
const abort = () => this.
|
|
388
|
+
const abort = () => this.close({ layer: 'acp', initiator: 'acp.start-signal', reason: 'ABORTED', action: 'abort' }, new NeedError('ABORTED', '会话建立已取消', '重新连接主机'));
|
|
368
389
|
signal?.throwIfAborted();
|
|
369
390
|
signal?.addEventListener('abort', abort, { once: true });
|
|
370
391
|
try {
|
|
371
392
|
const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
|
|
372
|
-
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.
|
|
393
|
+
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.15' },
|
|
373
394
|
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1 } } } : {}) },
|
|
374
395
|
}, { cancellationSignal: signal }));
|
|
375
396
|
this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
|
|
@@ -453,7 +474,7 @@ export class AcpSession {
|
|
|
453
474
|
this.ready = false;
|
|
454
475
|
this.creatingReplacement = true;
|
|
455
476
|
this.pendingConfigurations.clear();
|
|
456
|
-
const abort = () => this.
|
|
477
|
+
const abort = () => this.close({ layer: 'acp', initiator: 'acp.new-context-signal', reason: 'ABORTED', action: 'abort' }, new NeedError('ABORTED', '新会话建立已取消,远端状态需确认', '管理员核对远端状态'));
|
|
457
478
|
signal?.addEventListener('abort', abort, { once: true });
|
|
458
479
|
try {
|
|
459
480
|
const revision = this.resetConfiguration();
|
|
@@ -481,32 +502,34 @@ export class AcpSession {
|
|
|
481
502
|
signal?.removeEventListener('abort', abort);
|
|
482
503
|
}
|
|
483
504
|
}
|
|
484
|
-
async updateContext(context, signal) {
|
|
505
|
+
async updateContext(context, signal, force = false) {
|
|
485
506
|
this.checkReporting();
|
|
486
507
|
if (!this.ready || !this.sessionId || !this.cwd)
|
|
487
508
|
throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
|
|
488
509
|
if (this.busy || this.updating || this.selecting)
|
|
489
510
|
throw new NeedError('TURN_BUSY', '此远端会话正在处理上一轮或更新配置', '上一轮结束');
|
|
490
511
|
const text = context.systemPrompt ?? '';
|
|
491
|
-
if (text === this.systemPrompt)
|
|
512
|
+
if (!force && text === this.systemPrompt)
|
|
492
513
|
return undefined;
|
|
493
514
|
this.validateContext(context);
|
|
515
|
+
if (force && (!this.negotiated.sessionCapabilities?.systemPrompt || !this.negotiated.sessionCapabilities?.resume && !this.negotiated.loadSession))
|
|
516
|
+
throw new NeedError('SYSTEM_PROMPT_UPDATE_UNSUPPORTED', '远端没有可传送更新要求的公开恢复能力', 'session/resume 或 session/load 的 systemPrompt 能力');
|
|
494
517
|
const resume = !!this.negotiated.sessionCapabilities?.resume;
|
|
495
518
|
signal?.throwIfAborted();
|
|
496
519
|
this.updating = true;
|
|
497
520
|
this.ready = false;
|
|
498
|
-
const abort = () => this.
|
|
521
|
+
const abort = () => this.close({ layer: 'acp', initiator: 'acp.update-context-signal', reason: 'ABORTED', action: 'abort' }, new NeedError('ABORTED', '上下文更新已取消,原会话状态需确认', '重新连接原会话'));
|
|
499
522
|
signal?.addEventListener('abort', abort, { once: true });
|
|
500
523
|
try {
|
|
501
524
|
const method = resume ? acp.methods.agent.session.resume : acp.methods.agent.session.load;
|
|
502
525
|
const params = { sessionId: this.sessionId, cwd: this.cwd, mcpServers: [], systemPrompt: { mode: 'append', content: [{ type: 'text', text }] } };
|
|
503
|
-
const configRevision = this.resetConfiguration();
|
|
526
|
+
const configRevision = this.resetConfiguration(false);
|
|
504
527
|
const result = await this.stage(resume ? 'acp-session-resume' : 'acp-session-load', () => this.connection.agent.request(method, params, { cancellationSignal: signal }));
|
|
505
528
|
this.lifecycleConfiguration(result, configRevision);
|
|
506
529
|
this.hooks.broker?.acknowledge(method);
|
|
507
530
|
this.systemPrompt = text;
|
|
508
531
|
this.ready = true;
|
|
509
|
-
this.
|
|
532
|
+
this.detail({ kind: 'session', severity: 'warning', code: 'SYSTEM_PROMPT_UPDATE_DEFERRED', summary: '系统与开发者要求已通过原会话恢复接口传送;新要求在下次上下文整理后生效,本轮不能保证已立即采用。', payload: { reason: 'OFFICIAL_CONTEXT_UPDATE_DEFERRED' } });
|
|
510
533
|
this.checkReporting();
|
|
511
534
|
return method;
|
|
512
535
|
}
|
|
@@ -542,6 +565,7 @@ export class AcpSession {
|
|
|
542
565
|
let rejectAbort;
|
|
543
566
|
const interrupted = new Promise((_resolve, reject) => { rejectAbort = reject; });
|
|
544
567
|
const cancel = () => {
|
|
568
|
+
this.closeDiagnostics.record({ layer: 'acp', initiator: 'acp.prompt-cancel', reason: 'ABORTED', operation: 'session/prompt', action: 'abort' });
|
|
545
569
|
rejectAbort?.(new NeedError('ABORTED', '已请求取消本轮,远端状态需确认', '确认主机取消状态'));
|
|
546
570
|
void this.connection.agent.notify(acp.methods.agent.session.cancel, { sessionId: this.sessionId }).catch(() => {
|
|
547
571
|
this.need({ code: 'CANCEL_UNCONFIRMED', message: '连接中断,无法确认主机已取消本轮', requires: '恢复连接以确认远端状态' });
|
|
@@ -573,7 +597,7 @@ export class AcpSession {
|
|
|
573
597
|
this.turnToken = undefined;
|
|
574
598
|
}
|
|
575
599
|
}
|
|
576
|
-
close() { this.
|
|
600
|
+
close(cause = { layer: 'acp', initiator: 'acp.close', reason: 'ACP_CLOSE_REQUESTED', action: 'close' }, error) { this.closeDiagnostics.record({ ...cause, changes: cause.changes ?? [{ field: 'acp.close-requested', before: this.closeRequested, after: true }] }); this.closeRequested = true; this.connection?.close(error); }
|
|
577
601
|
}
|
|
578
602
|
export function acpNeed(error) {
|
|
579
603
|
if (error instanceof NeedError)
|