@ai-workbench/remote-agent 0.8.10 → 0.8.11
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 +20 -4
- package/dist/acp.js +1 -1
- package/dist/employee-mcp.js +1 -1
- package/dist/index.js +94 -20
- package/dist/model-catalog.d.ts +18 -2
- package/dist/model-catalog.js +101 -36
- package/dist/model-defaults.d.ts +29 -0
- package/dist/model-defaults.js +62 -0
- package/dist/model-input-retention.d.ts +16 -0
- package/dist/model-input-retention.js +129 -0
- package/dist/presentation.js +6 -0
- package/dist/provider.d.ts +4 -1
- package/dist/provider.js +44 -21
- package/dist/workspace-source-confirmation.d.ts +4 -0
- package/dist/workspace-source-confirmation.js +12 -0
- package/dist/workspace-source.d.ts +25 -0
- package/dist/workspace-source.js +136 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
**`@ai-workbench/remote-agent@0.8.
|
|
1
|
+
**`@ai-workbench/remote-agent@0.8.11` 发布形态包;由编排发布后通过官方插件 → 添加插件安装。修复 Claude 模型目录及旧默认模型,供 leo 真机复验。Codex 线因主机适配器 aw.2 实装失败(UNSAFE_MODE_MISMATCH),待 S 修复后另验,当前未验证可用。由编排统一发布,X 未执行 publish。**
|
|
2
2
|
|
|
3
3
|
# 远程ACP插件
|
|
4
4
|
|
|
5
5
|
一个插件提供 **Claude(远程主机)** 和 **Codex(远程主机)**,通过员工主机上的官方CLI运行。Codex保留单次模式;Claude固定会话模式。只用官方模型列表、插件管理和配置入口,不提供自制面板、问答或登录按钮。登录和原生工作区绑定唯一归连接插件L。
|
|
6
6
|
|
|
7
|
-
当前为0.8.
|
|
7
|
+
当前为0.8.11,包名采用`@ai-workbench/remote-agent`以涵盖两个品牌;本轮按编排放行准备发布形态包,供 leo 真机自验;X 不执行实际发布,由编排统一发布。开发fixture逐字节保留L0.7.17固定包,公开类型与SSH导出从该包消费。运行时仍要求L的`getContractInfo()`同时返回API0.7.0、implemented、native-local-v1;旧L包、draft.3或只有版本号的不完整实现会明确报缺项。依赖范围为`~0.7.17`,L由官方插件管理单独安装,不打进X。组件验证不代表真实开通、模型或leo E通过;HanaMesh另验。
|
|
8
8
|
|
|
9
9
|
## 员工路径
|
|
10
10
|
|
|
11
|
-
从官方插件管理安装,照常使用默认工作区/会话/模型入口。L0.7.17沿唯一“远程主机连接”设置节记录文件来源;主机来源沿用L既有解析;本地来源接入N0.4.4/S0.4.11
|
|
11
|
+
从官方插件管理安装,照常使用默认工作区/会话/模型入口。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组件联测;真实N/S员工端仍NOT_RUN,产品验收以 leo 实际执行结果为准。本地文件不自动上传,X不猜目录、不选首项。首次说明使用官方普通回复;先经官方会话提交、持久化并读回核对,才向L确认。持久记录不等于员工已看见,GUI另验;日志丢失后的未知claim由L保留,不宣称端到端恰好一次。
|
|
12
12
|
|
|
13
13
|
未登录时两个模型均回复:**需要登录,请到设置 → 远程主机连接 点登录**。无L或缺正式能力时直接说明缺项,不让员工填写主机、用户、路径或密钥。
|
|
14
14
|
|
|
@@ -72,7 +72,7 @@ D21(0.8.5):两个品牌的官方模型菜单从 L 的 `modelCatalogTarget:
|
|
|
72
72
|
|
|
73
73
|
每轮先核对目录代次和所选模型,再经 ACP 逐项配置模型及推理等级,收到完整配置确认后才发送输入。Claude 别名必须有当前会话的具体型号证据;没有就明确拒绝。真实回复追加所选值和远端确认值,并在恢复标识与技术详情保留,确认配置不等于验证推理效果。连接/身份/目录变化清除旧确认;远端清理待定或结果未知时阻止派发,不自动清理重试或另开替代会话。Codex 单次模式当前不能确认此选择,会在发送前明确拒绝。新增设置 0;本轮工程证据、真实只读链路与产品未验项见 `docs/evidence/2026-10-22-X2-D21/`。
|
|
74
74
|
|
|
75
|
-
0.8.5统一包原依赖L `~0.7.13`;当前0.8.
|
|
75
|
+
0.8.5统一包原依赖L `~0.7.13`;当前0.8.11对齐L `~0.7.17`。D8模型目录双事件长期监听保留;同epoch的recovery提示/失败消息变化不触发执行状态失效,真实实例/epoch/phase/可信目标/登录变化仍失效。完整D9两品牌正文/状态/就绪门呈现按编排00:50允许的范围记下一版,本版不宣称已消费完成;L的connectionMessages.administratorAfterMs仍只由L管理,X新增设置0。
|
|
76
76
|
|
|
77
77
|
D18(0.8.6):Codex 需要主机自有 adapter `2.2.2-aw.1` 的明确新建会话上下文政策,官方 CLI 仍为未改的0.160.1。系统/开发者要求仅在新建时生效;恢复时与初建摘要相同则正常恢复且省略新建专用字段,不声称恢复接口会更新指令。要求不同或清空时,在同一真实回复说明“会话要求已变化,已新建 Codex 会话继续处理,原会话仍保留。”,并新建远端承接公开对话,保留旧标识和查询记录;旧/非法初建摘要明确拒绝,不能猜测。
|
|
78
78
|
|
|
@@ -97,3 +97,19 @@ D20(0.8.8开发记录):本地来源的Codex原生文件与命令通过固
|
|
|
97
97
|
当前明确不支持本地单次模式、断开后的冷恢复/load/fork、快捷命令、主机文件报告及附加MCP/hooks/plugins。主机同路径的启用项目配置、会话层项目发现/信任覆盖也会明确拒绝;配置检查不静默清空或改写原配置。普通原生shell/patch/fs使用所选员工环境,未覆盖的辅助入口保留具体原因到员工回复与技术详情。位置、身份或代次改变只撤回对应执行器,在途结果保持UNKNOWN,不自动重放。新增产品设置0,执行通道和桥队列沿已有`acpMaxMessageBytes`控制容量。当前工程进度、首红和开放门见`docs/evidence/2026-10-23-X2-D20-088/STATUS.md`。
|
|
98
98
|
|
|
99
99
|
0.8.10仅增加公开插件参数页,不改变上述七项执行语义。mode/ACP/frame/control/grace的修改不改变运行中当轮;保留会话下一轮配置签名不一致会停止,须新建会话。ACP容量还在下一次独立目录查询读取新值。sandbox与approval仅单次通道适用,当前单次仍明确拒绝执行。GUI与真实链路门仍NOT_RUN;本次由编排在 leo 验证 Claude 线、设置界面和 D24 重开,Codex 线另验。完整逐项盘点见 docs/evidence/2026-10-23-X-settings-0810/INVENTORY.md。
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## 0.8.11 模型目录与选择
|
|
103
|
+
|
|
104
|
+
不提供推理档位的模型省略官方 reasoning 元数据;单项资料不合法时只剔除该项,在官方“设置 → 模型”页按模型名称显示中文原因和诊断代码。整组查询失败在菜单显示中文,可到同一设置页核对技术原因。没有会话的目录查询不写入其他会话的技术详情。
|
|
105
|
+
|
|
106
|
+
只修正属于本插件的旧版或失效全局默认:Claude 只取目录 value 为 default 的唯一别名,resolvedModel 原值包括 null 都保留;Codex 只取 source.isDefault 为 true 的唯一项。有效的员工明确选择保持不变。没有唯一默认时不选首项,发送前提示“请先选择模型”。官方默认设置只有字符串 model、没有 nullable 清除接口;此时写入空字符串,原生界面可能显示提供方加斜线,而无实际模型选中。通过官方 configEditor.entries/edit 找到唯一默认模型设置,在配置锁内重新核对员工选择后才更新;不使用缺少条件保护的 saveSelection。保存失败或选择已变化时在官方模型设置页明示。全局默认也会影响没有独立选择的旧空白会话,已有持久模型选择的老会话不改。
|
|
107
|
+
|
|
108
|
+
老会话存着 remote 时,请在该会话的模型菜单重新选择当前目录中的型号。插件不会替员工改写会话选择。
|
|
109
|
+
|
|
110
|
+
官方 loop 在 prepareCall 成功后才把原话追加到对话正文。插件不能更改此核心顺序;由实际 X 准备入口标记、同一错误对象及同一信号可核对的零发送准备失败,会在官方 agent 空闲后,把原话(原 ID、附件及顺序)恢复到官方持久待发送队列,核验落盘后在模型设置诊断中说明;员工可在原生输入区待发送队列查看。恢复本身不会唤醒、重试或再发模型请求;更换模型后由员工手动发送,新的外部输入仍可能触发官方队列消费。取消、卸载、未知错误、已进入模型派发、无法证明来自 X 的官方外围元数据/effort 校验错误,以及早于公开 pre-step 的附件准入/assembly 失败不盲目重排。没有承诺把这些失败直接补成正文气泡;这一能力位于官方 loop 的核心追加顺序,插件的最佳可用结果是持久待发送队列。恢复/持久化不能确认时显示明确诊断。
|
|
111
|
+
|
|
112
|
+
工程验证使用 emp-a 实际完整 Claude 13 项及 Codex 7 项只读目录,交官方 rc.2 校验;不代表本版 leo 菜单、输入显示或真实模型执行已经验收。新增设置、上限、阈值为 0;七项参数沿用 0.8.10。
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
0.8.11 同时包含 main f8cddc5 的旧来源一次确认:先确认唯一原生工作区、原始输入与连接代次,再按 L0.7.17 公开 CAS 选择本机来源,接纳后继续原本这一轮。不会自动保留主机作为替代,也不会重发旧任务。确认前后失败的私有耐久记录随本版提供;L 可见的前置诊断入口尚待 L0.7.18 后续交付,本版不宣称已显示该详情。
|
package/dist/acp.js
CHANGED
|
@@ -369,7 +369,7 @@ export class AcpSession {
|
|
|
369
369
|
signal?.addEventListener('abort', abort, { once: true });
|
|
370
370
|
try {
|
|
371
371
|
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.
|
|
372
|
+
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.11' },
|
|
373
373
|
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1 } } } : {}) },
|
|
374
374
|
}, { cancellationSignal: signal }));
|
|
375
375
|
this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
|
package/dist/employee-mcp.js
CHANGED
|
@@ -121,7 +121,7 @@ export async function openEmployeeMcp(options) {
|
|
|
121
121
|
socket.destroy();
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
|
-
const mcp = new Server({ name: 'ai-workbench-employee', version: '0.8.
|
|
124
|
+
const mcp = new Server({ name: 'ai-workbench-employee', version: '0.8.11' }, { capabilities: { tools: {} } }), transport = new EmployeeSocketTransport(socket, options.maxBytes);
|
|
125
125
|
clients.add(mcp);
|
|
126
126
|
transports.add(transport);
|
|
127
127
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: EMPLOYEE_MCP_TOOLS }));
|
package/dist/index.js
CHANGED
|
@@ -24,11 +24,14 @@ import { verifyRecoveryCheckpoint, currentInputIds } from "./recovery-checkpoint
|
|
|
24
24
|
import { ModelCatalog, modelInfo } from "./model-catalog.js";
|
|
25
25
|
import { ModelConfigurationError } from "./model-configuration.js";
|
|
26
26
|
import { officialEmployeeWorkspace } from "./employee-workspace.js";
|
|
27
|
+
import { nativeWorkspaceSourceConfirmation } from "./workspace-source-confirmation.js";
|
|
27
28
|
import { resolveEmployeeNative } from "./employee-native.js";
|
|
28
29
|
import { createLocalUserTurnObserver } from "./local-user-turn.js";
|
|
29
30
|
import { createClaudeLocalProvider } from "./claude-local.js";
|
|
30
31
|
import { readLocalUserTurn } from "./local-user-turn.js";
|
|
31
32
|
import { readEmployeeWorkspace } from "./employee-workspace.js";
|
|
33
|
+
import { createModelDefaults, createDefaultSelectionWriter } from "./model-defaults.js";
|
|
34
|
+
import { installModelInputRetention, markModelPreparationFailure } from "./model-input-retention.js";
|
|
32
35
|
export const name = 'ai-workbench-remote-agent';
|
|
33
36
|
export const settingsNamespace = 'ai-workbench-codex';
|
|
34
37
|
// L can be installed/enabled after X; keeping X mounted lets it explain the prerequisite.
|
|
@@ -100,14 +103,21 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
100
103
|
finally {
|
|
101
104
|
catalog.release();
|
|
102
105
|
} }
|
|
103
|
-
async resolveModel(provider, id, signal) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
async resolveModel(provider, id, signal) { try {
|
|
107
|
+
const catalog = await this.catalog(signal);
|
|
108
|
+
try {
|
|
109
|
+
const model = catalog.models.find(model => model.id === id);
|
|
110
|
+
if (!model)
|
|
111
|
+
throw new LlmError(id === 'remote' ? '无法获取模型列表:这个会话保存的是旧版模型选择,请打开本会话的模型菜单,重新选择模型后再发送。' : '所选模型已不在当前主机目录中,请打开本会话的模型菜单重新选择模型。', 'MODEL_CATALOG_UNAVAILABLE');
|
|
112
|
+
return modelInfo(provider, model);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
catalog.release();
|
|
116
|
+
}
|
|
108
117
|
}
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
catch (error) {
|
|
119
|
+
markModelPreparationFailure(provider, error, signal);
|
|
120
|
+
throw error;
|
|
111
121
|
} }
|
|
112
122
|
async invalidateModelState() { for (const turn of this.turns.values())
|
|
113
123
|
turn.abort(); await Promise.all([...this.sessions.keys()].map(id => this.disposeSession(id))); }
|
|
@@ -206,6 +216,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
206
216
|
const signal = AbortSignal.any([this.lifetime.signal, turn.signal, ...(original.signal ? [original.signal] : [])]);
|
|
207
217
|
const options = { ...original, signal };
|
|
208
218
|
const timing = new StageTiming();
|
|
219
|
+
let request;
|
|
220
|
+
let earlyStage = 'input-prepare';
|
|
209
221
|
let runtime;
|
|
210
222
|
let session;
|
|
211
223
|
let live;
|
|
@@ -236,7 +248,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
236
248
|
const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
|
|
237
249
|
if (live && live.configuration !== configuration)
|
|
238
250
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
|
|
239
|
-
|
|
251
|
+
request = { timing, acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, recoveryCheckpointId: previous?.recoveryCheckpointId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, retainProcess: !!key && mode === 'session', signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) };
|
|
240
252
|
const after = options.messages.findLastIndex(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
|
|
241
253
|
request.userInputIds = Object.freeze(options.messages.slice(after + 1).flatMap(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string' ? [message.id] : []));
|
|
242
254
|
request.userMessageIds = request.userInputIds;
|
|
@@ -270,6 +282,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
270
282
|
if (mode === 'one-shot')
|
|
271
283
|
throw new NeedError('MODEL_CONFIGURATION_UNSUPPORTED', '单次模式不支持确认当前模型与推理等级,请改用会话模式', '支持模型配置确认的会话模式');
|
|
272
284
|
request.localAdmissionCheck?.();
|
|
285
|
+
earlyStage = 'model-catalog';
|
|
273
286
|
catalog = await this.catalog(signal);
|
|
274
287
|
selected = catalog.models.find(model => model.id === options.model);
|
|
275
288
|
if (!selected)
|
|
@@ -279,6 +292,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
279
292
|
let modelSignal = AbortSignal.any([signal, catalog.signal]);
|
|
280
293
|
options.signal = modelSignal;
|
|
281
294
|
request.signal = modelSignal;
|
|
295
|
+
earlyStage = 'runtime-resolve';
|
|
282
296
|
runtime = await this.resolveRuntime(request);
|
|
283
297
|
signal.throwIfAborted();
|
|
284
298
|
if (runtime.execution?.signal) {
|
|
@@ -315,6 +329,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
315
329
|
if (!details)
|
|
316
330
|
throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
|
|
317
331
|
timing.bind(detail => details.record(detail));
|
|
332
|
+
request.recordEarlyFailure = undefined;
|
|
318
333
|
const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (runtime.restartNotice ? undefined : live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
|
|
319
334
|
activeMapper = mapper;
|
|
320
335
|
let id;
|
|
@@ -505,11 +520,29 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
505
520
|
if (live && (remoteAttempted || runtime === live.runtime))
|
|
506
521
|
live.failure = { cause: error };
|
|
507
522
|
let reportingFailure;
|
|
523
|
+
if (request?.recordEarlyFailure) {
|
|
524
|
+
const record = request.recordEarlyFailure;
|
|
525
|
+
request.recordEarlyFailure = undefined;
|
|
526
|
+
const code = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : signal.aborted ? 'ABORTED' : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
|
|
527
|
+
try {
|
|
528
|
+
await record(new NeedError(code, '来源确认后本轮在执行前失败。', '白名单阶段与错误代码'), earlyStage);
|
|
529
|
+
}
|
|
530
|
+
catch (failure) {
|
|
531
|
+
reportingFailure = { cause: failure };
|
|
532
|
+
}
|
|
533
|
+
}
|
|
508
534
|
try {
|
|
509
535
|
await runtime?.details?.flush();
|
|
510
536
|
}
|
|
511
537
|
catch (failure) {
|
|
512
|
-
reportingFailure
|
|
538
|
+
if (reportingFailure === undefined)
|
|
539
|
+
reportingFailure = { cause: failure };
|
|
540
|
+
else {
|
|
541
|
+
const first = reportingFailure.cause;
|
|
542
|
+
const originalCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
|
|
543
|
+
const flushCode = failure instanceof NeedError || failure instanceof ModelConfigurationError ? failure.code : failure instanceof LlmError ? failure.failure.code : 'CONVERSATION_DETAILS_FAILED';
|
|
544
|
+
reportingFailure = { cause: Object.assign(new NeedError('CONVERSATION_DETAILS_FAILED', '本轮诊断保存及详情收尾失败,已停止。', '原错误代码 ' + originalCode + ';' + (first instanceof NeedError ? first.requires : '早期诊断保存失败') + ';详情收尾代码 ' + flushCode), { cause: new AggregateError([error, first, failure], 'ADMISSION_DIAGNOSTIC_AND_FLUSH_FAILED') }) };
|
|
545
|
+
}
|
|
513
546
|
}
|
|
514
547
|
if (activeMapper)
|
|
515
548
|
yield* activeMapper.flush();
|
|
@@ -582,8 +615,26 @@ export function apply(ctx, live) {
|
|
|
582
615
|
}
|
|
583
616
|
return manager;
|
|
584
617
|
};
|
|
585
|
-
let changed = () => { };
|
|
586
|
-
const
|
|
618
|
+
let changed = () => { }, publishDiagnostics = () => { };
|
|
619
|
+
const diagnostics = new Map();
|
|
620
|
+
const diagnostic = (scope, provider, message) => {
|
|
621
|
+
let entry = diagnostics.get(provider);
|
|
622
|
+
if (!entry) {
|
|
623
|
+
entry = new Map();
|
|
624
|
+
diagnostics.set(provider, entry);
|
|
625
|
+
}
|
|
626
|
+
if (entry.get(scope) === message)
|
|
627
|
+
return;
|
|
628
|
+
if (message === undefined)
|
|
629
|
+
entry.delete(scope);
|
|
630
|
+
else
|
|
631
|
+
entry.set(scope, message);
|
|
632
|
+
publishDiagnostics();
|
|
633
|
+
};
|
|
634
|
+
let defaults;
|
|
635
|
+
const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes, undefined, () => changed(), (brand, message) => {
|
|
636
|
+
diagnostic('catalog', AGENTS.find(agent => agent.loginProvider === brand).id, message);
|
|
637
|
+
}, (agent, models, assertCurrent) => defaults?.observe(agent, models, assertCurrent));
|
|
587
638
|
const adapter = new RemoteAgentsAdapter(config, agent => {
|
|
588
639
|
const details = async (request, api, connection, target) => {
|
|
589
640
|
return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection, localAcceptanceId: request.localAcceptanceId ?? request.localAcceptance?.acceptanceId });
|
|
@@ -618,15 +669,19 @@ export function apply(ctx, live) {
|
|
|
618
669
|
return runtime;
|
|
619
670
|
};
|
|
620
671
|
const local = agent.loginProvider === 'claude' ? createClaudeLocalProvider({ getConnect: get, getNetwork: () => ctx.get('aiWorkbenchNetwork'), readWorkspace: id => readEmployeeWorkspace(ctx, id), readUserTurn: request => readLocalUserTurn(ctx, { sessionId: request.sessionId, route: agent.id, userMessageIds: request.userMessageIds ?? [] }), reportAdmissionFailure: (request, code) => managerForProfile().recordLocalFailure({ sessionId: request.sessionId, provider: 'claude', code }), recordUnscoped: (request, observation) => managerForProfile().recordLocalFailure({ sessionId: request.sessionId, provider: 'claude', code: observation.code, observation }), configuration: config, details, open }) : undefined;
|
|
621
|
-
|
|
672
|
+
const workspace = async (request) => {
|
|
673
|
+
const services = ctx;
|
|
674
|
+
const registry = services.get('workspaceRegistry');
|
|
675
|
+
const sessions = services.get('sessions');
|
|
676
|
+
return officialEmployeeWorkspace(request.sessionId, registry, sessions?.get(request.sessionId)?.header);
|
|
677
|
+
};
|
|
678
|
+
return connectProvider(get, { agent, workspaceSource: {
|
|
679
|
+
workspace, currentUserTurn: request => localTurns.current(request), confirm: nativeWorkspaceSourceConfirmation(ctx),
|
|
680
|
+
record: (request, observation) => managerForProfile().recordLocalFailure({ sessionId: request.sessionId, provider: agent.loginProvider, code: observation.code, observation }),
|
|
681
|
+
}, ...(agent.loginProvider === 'codex' ? { employee: {
|
|
622
682
|
currentUserTurn: request => localTurns.current(request),
|
|
623
683
|
getNetwork: () => ctx.get('aiWorkbenchNetwork'),
|
|
624
|
-
workspace:
|
|
625
|
-
const services = ctx;
|
|
626
|
-
const registry = services.get('workspaceRegistry');
|
|
627
|
-
const sessions = services.get('sessions');
|
|
628
|
-
return officialEmployeeWorkspace(request.sessionId, registry, sessions?.get(request.sessionId)?.header);
|
|
629
|
-
}, native: () => resolveEmployeeNative(),
|
|
684
|
+
workspace, native: () => resolveEmployeeNative(),
|
|
630
685
|
} } : {}), notice: createWorkspaceNoticeConsumer(ctx, agent), details, open, local });
|
|
631
686
|
}, dshApproval(ctx), agent => agent.name[language], readImage, (sessionId, code, diagnostic) => {
|
|
632
687
|
const events = sessionId ? ctx.get('agents')?.get(SessionId(sessionId))?.session.snapshotEvents() : undefined;
|
|
@@ -648,11 +703,30 @@ export function apply(ctx, live) {
|
|
|
648
703
|
return managerForProfile().recordLoginFailure({ sessionId, code, diagnostic, context: { turn, step } });
|
|
649
704
|
}, modelDirectory);
|
|
650
705
|
const ids = AGENTS.map(agent => agent.id);
|
|
651
|
-
const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: settingsNamespace, settingsPath: [] }));
|
|
706
|
+
const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: settingsNamespace, settingsPath: [], ...(diagnostics.get(agent.id)?.size ? { error: [...diagnostics.get(agent.id).values()].join('\n') } : {}) }));
|
|
652
707
|
const directory = ctx.llm.registerConfigurableProviders(entries());
|
|
653
708
|
const registration = ctx.llm.registerAdapter(ids, adapter);
|
|
654
|
-
|
|
709
|
+
installModelInputRetention(ctx, { isOurProvider: provider => ids.includes(provider), diagnostic: (provider, message) => diagnostic('input', provider, message) });
|
|
655
710
|
const serviceValue = (name) => ctx.get(name);
|
|
711
|
+
publishDiagnostics = () => directory.replace(entries());
|
|
712
|
+
const refreshDefault = () => {
|
|
713
|
+
const service = serviceValue('agentDefaultModel');
|
|
714
|
+
const selected = service?.currentSelection();
|
|
715
|
+
const agent = AGENTS.find(agent => agent.id === selected?.provider);
|
|
716
|
+
if (!defaults || !agent)
|
|
717
|
+
return;
|
|
718
|
+
// Read through the same trusted, fresh catalog. Failures are already recorded there.
|
|
719
|
+
void modelDirectory.read(agent).then(lease => lease.release(), () => { });
|
|
720
|
+
};
|
|
721
|
+
ctx.inject(['agentDefaultModel'], child => {
|
|
722
|
+
const service = child.get('agentDefaultModel');
|
|
723
|
+
const current = createModelDefaults(service, (provider, message) => diagnostic('default', provider, message), createDefaultSelectionWriter(service, () => serviceValue('configEditor')));
|
|
724
|
+
defaults = current;
|
|
725
|
+
refreshDefault();
|
|
726
|
+
child.effect(() => () => { current.dispose(); if (defaults === current)
|
|
727
|
+
defaults = undefined; });
|
|
728
|
+
});
|
|
729
|
+
changed = () => { registration.replace(ids); refreshDefault(); void adapter.invalidateModelState().catch(() => { ctx.logger('remote-agent').warn('原会话清理结果未确认,已阻断继续发送;请联系管理员核对。'); }); };
|
|
656
730
|
let employeeConnect = serviceValue('aiWorkbenchConnect'), employeeNetwork = serviceValue('aiWorkbenchNetwork');
|
|
657
731
|
ctx.on('internal/service', service => {
|
|
658
732
|
if (service === 'aiWorkbenchConnect')
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -28,7 +28,19 @@ export interface ModelDirectory {
|
|
|
28
28
|
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
|
29
29
|
}
|
|
30
30
|
export type CatalogRunner = (target: ConnectTarget, command: string, signal: AbortSignal, maxBytes: number) => Promise<string>;
|
|
31
|
-
export
|
|
31
|
+
export interface CatalogIssue {
|
|
32
|
+
readonly modelName: string;
|
|
33
|
+
readonly reason: string;
|
|
34
|
+
readonly code: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ParsedCatalog {
|
|
37
|
+
readonly models: readonly HostModel[];
|
|
38
|
+
readonly issues: readonly CatalogIssue[];
|
|
39
|
+
}
|
|
40
|
+
export declare function catalogDiagnostic(issues: readonly CatalogIssue[]): string | undefined;
|
|
41
|
+
/** Defaults are provider declarations, never catalog order or display names. */
|
|
42
|
+
export declare function declaredDefault(agent: AgentDefinition, models: readonly HostModel[]): HostModel | undefined;
|
|
43
|
+
export declare function parseHostCatalog(input: string, provider: string, username: string): ParsedCatalog;
|
|
32
44
|
/** Only the public L sshArgs function chooses SSH trust and transport arguments. */
|
|
33
45
|
export declare const runCatalog: CatalogRunner;
|
|
34
46
|
/** No cache: every query binds to L's current object, host-user and generation. */
|
|
@@ -37,6 +49,8 @@ export declare class ModelCatalog implements ModelDirectory {
|
|
|
37
49
|
private maxBytes;
|
|
38
50
|
private run;
|
|
39
51
|
private changed;
|
|
52
|
+
private diagnostic;
|
|
53
|
+
private loaded;
|
|
40
54
|
private api?;
|
|
41
55
|
private unsubscribe?;
|
|
42
56
|
private signature?;
|
|
@@ -44,7 +58,9 @@ export declare class ModelCatalog implements ModelDirectory {
|
|
|
44
58
|
private disposed;
|
|
45
59
|
private initialized;
|
|
46
60
|
private active;
|
|
47
|
-
|
|
61
|
+
private serial;
|
|
62
|
+
private latest;
|
|
63
|
+
constructor(get: () => ConnectApi | undefined, maxBytes: () => number | undefined, run?: CatalogRunner, changed?: () => void, diagnostic?: (provider: 'claude' | 'codex', message: string | undefined) => void, loaded?: (agent: AgentDefinition, models: readonly HostModel[], assertCurrent: () => void) => void | Promise<void>);
|
|
48
64
|
refresh(): void;
|
|
49
65
|
private trusted;
|
|
50
66
|
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
package/dist/model-catalog.js
CHANGED
|
@@ -13,12 +13,25 @@ import { ModelConfigurationError } from "./model-configuration.js";
|
|
|
13
13
|
import { sanitizeObservation } from "./conversation-details.js";
|
|
14
14
|
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
15
15
|
const text = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
16
|
-
|
|
16
|
+
class CatalogError extends LlmError {
|
|
17
|
+
diagnostic;
|
|
18
|
+
constructor(reason, diagnostic) { super('无法获取模型列表:' + reason, 'MODEL_CATALOG_UNAVAILABLE'); this.diagnostic = diagnostic + '(诊断代码:MODEL_CATALOG_UNAVAILABLE)'; }
|
|
19
|
+
}
|
|
20
|
+
const safe = (value) => sanitizeObservation({ kind: 'session', severity: 'warning', code: 'MODEL_CATALOG_DIAGNOSTIC', summary: value, payload: {} }).summary;
|
|
21
|
+
function fail(reason, diagnostic = reason) { throw new CatalogError(reason, diagnostic); }
|
|
17
22
|
function freeze(value) { if (value && typeof value === 'object') {
|
|
18
23
|
for (const item of Object.values(value))
|
|
19
24
|
freeze(item);
|
|
20
25
|
Object.freeze(value);
|
|
21
26
|
} return value; }
|
|
27
|
+
export function catalogDiagnostic(issues) {
|
|
28
|
+
return issues.length ? '以下模型未加入可选列表:\n' + issues.map(issue => `模型「${issue.modelName}」:${issue.reason}(诊断代码:${issue.code})`).join('\n') : undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Defaults are provider declarations, never catalog order or display names. */
|
|
31
|
+
export function declaredDefault(agent, models) {
|
|
32
|
+
const selected = models.filter(model => agent.loginProvider === 'claude' ? model.value === 'default' : object(model.source) && model.source.isDefault === true);
|
|
33
|
+
return selected.length === 1 ? selected[0] : undefined;
|
|
34
|
+
}
|
|
22
35
|
export function parseHostCatalog(input, provider, username) {
|
|
23
36
|
let wire;
|
|
24
37
|
try {
|
|
@@ -32,36 +45,60 @@ export function parseHostCatalog(input, provider, username) {
|
|
|
32
45
|
if (wire.ok === false) {
|
|
33
46
|
const error = object(wire.error) ? wire.error : undefined;
|
|
34
47
|
const code = typeof error?.code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(error.code) ? error.code : 'PROTOCOL_INVALID';
|
|
35
|
-
const reason = text(error?.message) ?
|
|
36
|
-
fail('
|
|
48
|
+
const reason = text(error?.message) ? safe(error.message) : '主机未提供失败说明';
|
|
49
|
+
fail('主机目录查询失败,请到设置 → 模型查看诊断。', `主机目录查询失败(诊断代码:${code}):${reason}`);
|
|
37
50
|
}
|
|
38
51
|
if (wire.ok !== true || wire.permissionProbe !== false || !object(wire.subject) || wire.subject.username !== username || !Number.isSafeInteger(wire.subject.uid) || !text(wire.observedAt) || !object(wire.source) || !object(wire.settings) || !Array.isArray(wire.models))
|
|
39
52
|
fail('主机目录缺少完整元数据');
|
|
40
|
-
const ids = new
|
|
41
|
-
for (const model of wire.models)
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
const ids = new Map(), values = new Map();
|
|
54
|
+
for (const model of wire.models)
|
|
55
|
+
if (object(model)) {
|
|
56
|
+
if (text(model.id))
|
|
57
|
+
ids.set(model.id, (ids.get(model.id) ?? 0) + 1);
|
|
58
|
+
if (text(model.value))
|
|
59
|
+
values.set(model.value, (values.get(model.value) ?? 0) + 1);
|
|
60
|
+
}
|
|
61
|
+
const models = [], issues = [];
|
|
62
|
+
for (const [index, model] of wire.models.entries()) {
|
|
63
|
+
const modelName = object(model) && text(model.name) ? safe(model.name) : object(model) && text(model.id) ? safe(model.id) : `第 ${index + 1} 项(缺少模型名称)`;
|
|
64
|
+
const reject = (reason, code = 'MODEL_CATALOG_ITEM_INVALID') => issues.push({ modelName, reason, code });
|
|
65
|
+
if (!object(model) || !text(model.id) || !text(model.value) || !text(model.name) || (model.resolvedModel !== null && !text(model.resolvedModel)) || !object(model.source) || !object(model.reasoning)) {
|
|
66
|
+
reject('模型标识、名称或能力资料不完整');
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if ((ids.get(model.id) ?? 0) > 1 || (values.get(model.value) ?? 0) > 1) {
|
|
70
|
+
reject('模型标识或发送值重复,无法确定对应关系', 'MODEL_CATALOG_DUPLICATE');
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (provider === 'codex' && model.resolvedModel !== model.value) {
|
|
74
|
+
reject('具体型号与发送模型值不一致');
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
50
77
|
const r = model.reasoning;
|
|
51
|
-
if (!Array.isArray(r.options) || typeof r.defaultKnown !== 'boolean' || (r.defaultValue !== null && !text(r.defaultValue)) || (!r.defaultKnown && r.defaultValue !== null))
|
|
52
|
-
|
|
78
|
+
if (!Array.isArray(r.options) || typeof r.defaultKnown !== 'boolean' || (r.defaultValue !== null && !text(r.defaultValue)) || (!r.defaultKnown && r.defaultValue !== null)) {
|
|
79
|
+
reject('推理档位或默认档位资料无效', 'MODEL_REASONING_INVALID');
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
53
82
|
const efforts = new Set();
|
|
83
|
+
let invalid = false;
|
|
54
84
|
for (const effort of r.options) {
|
|
55
|
-
if (!object(effort) || !text(effort.value) || !text(effort.name) || efforts.has(effort.value))
|
|
56
|
-
|
|
85
|
+
if (!object(effort) || !text(effort.value) || !text(effort.name) || efforts.has(effort.value)) {
|
|
86
|
+
invalid = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
57
89
|
efforts.add(effort.value);
|
|
58
90
|
}
|
|
59
|
-
if (
|
|
60
|
-
|
|
91
|
+
if (invalid) {
|
|
92
|
+
reject('推理档位缺少名称、值无效或重复', 'MODEL_REASONING_INVALID');
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (r.defaultKnown && (r.defaultValue === null || !efforts.has(r.defaultValue))) {
|
|
96
|
+
reject('默认推理档位不在该模型提供的选项中', 'MODEL_REASONING_DEFAULT_INVALID');
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
models.push(model);
|
|
61
100
|
}
|
|
62
|
-
|
|
63
|
-
fail('主机返回的完整模型目录为空');
|
|
64
|
-
return freeze(structuredClone(wire.models));
|
|
101
|
+
return freeze(structuredClone({ models, issues }));
|
|
65
102
|
}
|
|
66
103
|
/** Only the public L sshArgs function chooses SSH trust and transport arguments. */
|
|
67
104
|
export const runCatalog = async (target, command, signal, maxBytes) => {
|
|
@@ -141,6 +178,8 @@ export class ModelCatalog {
|
|
|
141
178
|
maxBytes;
|
|
142
179
|
run;
|
|
143
180
|
changed;
|
|
181
|
+
diagnostic;
|
|
182
|
+
loaded;
|
|
144
183
|
api;
|
|
145
184
|
unsubscribe;
|
|
146
185
|
signature;
|
|
@@ -148,11 +187,15 @@ export class ModelCatalog {
|
|
|
148
187
|
disposed = false;
|
|
149
188
|
initialized = false;
|
|
150
189
|
active = new Set();
|
|
151
|
-
|
|
190
|
+
serial = 0;
|
|
191
|
+
latest = new Map();
|
|
192
|
+
constructor(get, maxBytes, run = runCatalog, changed = () => { }, diagnostic = () => { }, loaded = () => { }) {
|
|
152
193
|
this.get = get;
|
|
153
194
|
this.maxBytes = maxBytes;
|
|
154
195
|
this.run = run;
|
|
155
196
|
this.changed = changed;
|
|
197
|
+
this.diagnostic = diagnostic;
|
|
198
|
+
this.loaded = loaded;
|
|
156
199
|
this.refresh();
|
|
157
200
|
}
|
|
158
201
|
refresh() {
|
|
@@ -183,9 +226,12 @@ export class ModelCatalog {
|
|
|
183
226
|
this.signature = signature;
|
|
184
227
|
if (changed) {
|
|
185
228
|
this.revision++;
|
|
229
|
+
this.latest.clear();
|
|
186
230
|
for (const controller of this.active)
|
|
187
231
|
controller.abort();
|
|
188
232
|
this.active.clear();
|
|
233
|
+
this.diagnostic('claude', undefined);
|
|
234
|
+
this.diagnostic('codex', undefined);
|
|
189
235
|
this.changed();
|
|
190
236
|
}
|
|
191
237
|
}
|
|
@@ -198,7 +244,7 @@ export class ModelCatalog {
|
|
|
198
244
|
fail('当前没有已验证的远程主机连接');
|
|
199
245
|
const result = api.getModelCatalogTarget(agent.loginProvider);
|
|
200
246
|
if (!result.ok)
|
|
201
|
-
fail(result.error.message);
|
|
247
|
+
fail('远程主机连接暂未提供可信的模型目录目标,请到设置 → 模型查看诊断。', `模型目录目标不可用(诊断代码:${safe(result.error.code)}):${safe(result.error.message)}`);
|
|
202
248
|
const target = result.value;
|
|
203
249
|
if (target.version !== 1 || target.purpose !== 'model-catalog' || target.scope !== 'host-user' || target.provider !== agent.loginProvider || target.instanceId !== state.instanceId || target.epoch !== state.epoch)
|
|
204
250
|
fail('模型目录目标与当前连接或品牌不匹配');
|
|
@@ -212,43 +258,62 @@ export class ModelCatalog {
|
|
|
212
258
|
}
|
|
213
259
|
async read(agent, signal) {
|
|
214
260
|
let controller;
|
|
261
|
+
let api;
|
|
262
|
+
let revision = this.revision;
|
|
263
|
+
const serial = ++this.serial;
|
|
264
|
+
this.latest.set(agent.loginProvider, serial);
|
|
265
|
+
const current = () => !this.disposed && revision === this.revision && api === this.get() && this.latest.get(agent.loginProvider) === serial;
|
|
266
|
+
let reported = false;
|
|
267
|
+
const report = (message) => { if (current()) {
|
|
268
|
+
reported = true;
|
|
269
|
+
this.diagnostic(agent.loginProvider, message);
|
|
270
|
+
} };
|
|
215
271
|
try {
|
|
216
272
|
if (this.disposed)
|
|
217
273
|
fail('远程模型插件已关闭');
|
|
218
274
|
this.refresh();
|
|
219
|
-
|
|
275
|
+
api = this.get();
|
|
276
|
+
revision = this.revision;
|
|
220
277
|
if (!api)
|
|
221
278
|
fail('远程主机连接组件没有装好');
|
|
222
|
-
|
|
279
|
+
// refresh may clear reads from the previous connection generation.
|
|
280
|
+
this.latest.set(agent.loginProvider, serial);
|
|
281
|
+
const trustedApi = api, target = freeze(this.trusted(agent, trustedApi));
|
|
223
282
|
const max = this.maxBytes();
|
|
224
283
|
if (!Number.isSafeInteger(max) || max < 1)
|
|
225
284
|
fail('acpMaxMessageBytes 设置无效');
|
|
226
285
|
controller = new AbortController();
|
|
227
286
|
this.active.add(controller);
|
|
228
287
|
const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
229
|
-
const assertCurrent = () => { if (combined.aborted || this.disposed || this.get() !==
|
|
230
|
-
fail('连接已改变,原模型目录已失效'); const current = this.trusted(agent,
|
|
288
|
+
const assertCurrent = () => { if (combined.aborted || this.disposed || this.get() !== trustedApi || revision !== this.revision)
|
|
289
|
+
fail('连接已改变,原模型目录已失效'); const current = this.trusted(agent, trustedApi); if (current.instanceId !== target.instanceId || current.epoch !== target.epoch || current.provider !== target.provider || !sameTarget(current.target, target.target))
|
|
231
290
|
fail('可信目标或连接代次已改变,原模型目录已失效'); };
|
|
232
291
|
assertCurrent();
|
|
233
292
|
const output = await this.run(target.target, '/usr/local/aw-host/bin/aw-model-catalog --provider ' + agent.loginProvider, combined, max);
|
|
234
293
|
assertCurrent();
|
|
235
|
-
const
|
|
294
|
+
const parsed = parseHostCatalog(output, agent.loginProvider, target.target.username), diagnostic = catalogDiagnostic(parsed.issues);
|
|
295
|
+
if (!parsed.models.length)
|
|
296
|
+
fail('当前没有可选模型,请到设置 → 模型查看诊断。', diagnostic ?? '主机返回的完整模型目录为空(诊断代码:MODEL_CATALOG_EMPTY)');
|
|
297
|
+
report(diagnostic);
|
|
298
|
+
if (current())
|
|
299
|
+
await this.loaded(agent, parsed.models, assertCurrent);
|
|
300
|
+
assertCurrent();
|
|
236
301
|
const own = controller;
|
|
237
|
-
return { target, models, signal: combined, assertCurrent, release: () => this.active.delete(own) };
|
|
302
|
+
return { target, models: parsed.models, signal: combined, assertCurrent, release: () => this.active.delete(own) };
|
|
238
303
|
}
|
|
239
304
|
catch (error) {
|
|
240
305
|
if (controller)
|
|
241
306
|
this.active.delete(controller);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
fail('目录查询失败,根因未查明');
|
|
307
|
+
const failure = error instanceof CatalogError ? error : new CatalogError(error instanceof ModelConfigurationError ? error.message.replace(/^无法获取模型列表:/, '') : '目录查询失败,请到设置 → 模型查看诊断。', error instanceof Error ? `${error instanceof LlmError ? error.code : 'MODEL_CATALOG_QUERY_FAILED'}:${safe(error.message)}` : 'MODEL_CATALOG_QUERY_FAILED:目录查询失败,根因未查明');
|
|
308
|
+
if (!reported)
|
|
309
|
+
report(failure.diagnostic);
|
|
310
|
+
throw failure;
|
|
247
311
|
}
|
|
248
312
|
}
|
|
249
313
|
dispose() { this.disposed = true; this.unsubscribe?.(); for (const controller of this.active)
|
|
250
314
|
controller.abort(); this.active.clear(); }
|
|
251
315
|
}
|
|
252
316
|
export function modelInfo(provider, model) {
|
|
253
|
-
|
|
317
|
+
// Official rc.2 represents no reasoning levels by absence, and rejects an empty efforts array.
|
|
318
|
+
return { provider, id: model.id, name: model.name, inputModalities: ['text', 'image'], ...(model.reasoning.options.length ? { reasoning: { efforts: model.reasoning.options.map(option => ({ id: ReasoningEffortId(option.value), name: option.name })), ...(model.reasoning.defaultKnown && model.reasoning.defaultValue !== null ? { defaultEffort: ReasoningEffortId(model.reasoning.defaultValue) } : {}) } } : {}), catalog: model };
|
|
254
319
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentDefinition } from './agents.ts';
|
|
2
|
+
import { type HostModel } from './model-catalog.ts';
|
|
3
|
+
export interface DefaultSelection {
|
|
4
|
+
provider: string;
|
|
5
|
+
model: string;
|
|
6
|
+
reasoningEffort?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Public agentDefaultModel service; writes below use the atomic config editor. */
|
|
9
|
+
export interface DefaultModelService {
|
|
10
|
+
currentSelection(): DefaultSelection;
|
|
11
|
+
}
|
|
12
|
+
interface DefaultEntry {
|
|
13
|
+
options: {
|
|
14
|
+
name?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Public rc.2 configEditor.entries/edit subset; no ownerContext/private config access. */
|
|
18
|
+
export interface DefaultConfigEditor {
|
|
19
|
+
entries(): readonly DefaultEntry[];
|
|
20
|
+
edit(entry: DefaultEntry, change: (current: Record<string, unknown>, inherited: Record<string, unknown>) => Record<string, unknown>): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export type DefaultSelectionWriter = (expected: DefaultSelection, next: DefaultSelection, assertCurrent: () => void) => Promise<void>;
|
|
23
|
+
/** The editor invokes the guard synchronously under its profile lock after reconciliation. */
|
|
24
|
+
export declare function createDefaultSelectionWriter(service: DefaultModelService, getEditor: () => DefaultConfigEditor | undefined): DefaultSelectionWriter;
|
|
25
|
+
export declare function createModelDefaults(service: DefaultModelService, report: (provider: string, message: string | undefined) => void, write?: DefaultSelectionWriter): {
|
|
26
|
+
observe(agent: AgentDefinition, models: readonly HostModel[], assertCurrent?: () => void): Promise<void>;
|
|
27
|
+
dispose(): void;
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { declaredDefault } from "./model-catalog.js";
|
|
2
|
+
const same = (a, b) => a.provider === b.provider && a.model === b.model && a.reasoningEffort === b.reasoningEffort;
|
|
3
|
+
/** The editor invokes the guard synchronously under its profile lock after reconciliation. */
|
|
4
|
+
export function createDefaultSelectionWriter(service, getEditor) {
|
|
5
|
+
return async (expected, next, assertCurrent) => {
|
|
6
|
+
const editor = getEditor();
|
|
7
|
+
const entries = editor?.entries().filter(entry => entry.options.name === '@deepseek-ai/dsh-agent-default-model');
|
|
8
|
+
if (!editor || entries?.length !== 1)
|
|
9
|
+
throw Error('缺少唯一可原子编辑的官方默认模型设置');
|
|
10
|
+
await editor.edit(entries[0], current => {
|
|
11
|
+
assertCurrent();
|
|
12
|
+
if (!same(expected, service.currentSelection()))
|
|
13
|
+
throw Error('员工已改变模型选择,本次默认更新已撤回');
|
|
14
|
+
const updated = { ...current, provider: next.provider, model: next.model };
|
|
15
|
+
Reflect.deleteProperty(updated, 'reasoningEffort');
|
|
16
|
+
if (next.reasoningEffort !== undefined)
|
|
17
|
+
Object.assign(updated, { reasoningEffort: next.reasoningEffort });
|
|
18
|
+
return updated;
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createModelDefaults(service, report, write) {
|
|
23
|
+
let disposed = false, queue = Promise.resolve();
|
|
24
|
+
return {
|
|
25
|
+
observe(agent, models, assertCurrent = () => { }) {
|
|
26
|
+
const snapshot = { ...service.currentSelection() };
|
|
27
|
+
const job = queue.then(async () => {
|
|
28
|
+
if (disposed || snapshot.provider !== agent.id || !same(snapshot, service.currentSelection()))
|
|
29
|
+
return;
|
|
30
|
+
if (snapshot.model !== 'remote' && models.some(model => model.id === snapshot.model)) {
|
|
31
|
+
report(agent.id, undefined);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const model = declaredDefault(agent, models);
|
|
35
|
+
const notice = model ? undefined : '提供方没有声明唯一的默认模型,请先选择模型。(诊断代码:MODEL_DEFAULT_UNAVAILABLE)';
|
|
36
|
+
const next = { provider: agent.id, model: model?.id ?? '', ...(model?.reasoning.defaultKnown && model.reasoning.defaultValue !== null ? { reasoningEffort: model.reasoning.defaultValue } : {}) };
|
|
37
|
+
try {
|
|
38
|
+
const guard = () => { assertCurrent(); if (disposed)
|
|
39
|
+
throw Error('默认设置消费已撤回'); };
|
|
40
|
+
guard();
|
|
41
|
+
if (!same(snapshot, next)) {
|
|
42
|
+
if (!write)
|
|
43
|
+
throw Error('缺少原子配置能力');
|
|
44
|
+
await write(snapshot, next, guard);
|
|
45
|
+
}
|
|
46
|
+
if (disposed)
|
|
47
|
+
return;
|
|
48
|
+
if (!same(next, service.currentSelection()))
|
|
49
|
+
throw Error('未确认官方默认模型设置已保存');
|
|
50
|
+
report(agent.id, notice);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
if (!disposed)
|
|
54
|
+
report(agent.id, (notice ? notice + '\n' : '') + '未能确认默认模型设置已更新,员工新选择不会被迟到的自动更新覆盖;请在模型菜单选择当前目录中的模型。(诊断代码:MODEL_DEFAULT_SAVE_FAILED)');
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
queue = job;
|
|
58
|
+
return job;
|
|
59
|
+
},
|
|
60
|
+
dispose() { disposed = true; },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
export interface ModelInputRetentionOptions {
|
|
3
|
+
isOurProvider: (provider: string) => boolean;
|
|
4
|
+
diagnostic: (provider: string, message: string | undefined) => void;
|
|
5
|
+
}
|
|
6
|
+
/** Mark only at the actual X adapter's preparation catch, then rethrow the same
|
|
7
|
+
* error. One error object can prove only its first preparation, for that signal.
|
|
8
|
+
* Public request config is an intermediate proposal and is not route evidence. */
|
|
9
|
+
export declare function markModelPreparationFailure(provider: string, error: unknown, signal?: AbortSignal): void;
|
|
10
|
+
/**
|
|
11
|
+
* Retain a claimed human batch only after a known, undispatched metadata prepare
|
|
12
|
+
* failure. Restoration uses the public durable inbox without sending or waking
|
|
13
|
+
* the agent. This cannot cover errors before pre-step/request, or unknown core
|
|
14
|
+
* admission failures; it never manufactures transcript messages or model routes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function installModelInputRetention(ctx: Context, options: ModelInputRetentionOptions): void;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { Session } from '@deepseek-ai/dsh-session';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
const preparationFailures = new WeakMap();
|
|
5
|
+
/** Mark only at the actual X adapter's preparation catch, then rethrow the same
|
|
6
|
+
* error. One error object can prove only its first preparation, for that signal.
|
|
7
|
+
* Public request config is an intermediate proposal and is not route evidence. */
|
|
8
|
+
export function markModelPreparationFailure(provider, error, signal) {
|
|
9
|
+
if (!signal || signal.aborted || error === null || typeof error !== 'object' || preparationFailures.has(error))
|
|
10
|
+
return;
|
|
11
|
+
preparationFailures.set(error, { provider, signal, consumed: false });
|
|
12
|
+
}
|
|
13
|
+
// Even a source-proven unknown early failure is deliberately not replayed.
|
|
14
|
+
const preparationCodes = new Set(['MODEL_REQUIRED', 'MODEL_CATALOG_UNAVAILABLE', 'INVALID_MODEL_INFO', 'INVALID_MODEL_CONTEXT', 'INVALID_MODEL_MAX_TOKENS', 'INVALID_MODEL_REASONING', 'UNSUPPORTED_REASONING_EFFORT']);
|
|
15
|
+
function codeOf(error) {
|
|
16
|
+
const code = error !== null && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
|
17
|
+
return typeof code === 'string' && /^[A-Z][A-Z0-9_]*$/u.test(code) ? code : undefined;
|
|
18
|
+
}
|
|
19
|
+
function committed(events, messages) {
|
|
20
|
+
return events.some(event => event.type === 'user/message' && messages.some(message => message.id === event.data.id));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Retain a claimed human batch only after a known, undispatched metadata prepare
|
|
24
|
+
* failure. Restoration uses the public durable inbox without sending or waking
|
|
25
|
+
* the agent. This cannot cover errors before pre-step/request, or unknown core
|
|
26
|
+
* admission failures; it never manufactures transcript messages or model routes.
|
|
27
|
+
*/
|
|
28
|
+
export function installModelInputRetention(ctx, options) {
|
|
29
|
+
const attempts = new Map();
|
|
30
|
+
let disposed = false;
|
|
31
|
+
const live = (a) => !disposed && attempts.get(a.agent) === a && !a.signal.aborted
|
|
32
|
+
&& ctx.get('agents')?.get(a.agent.id) === a.agent && ctx.get('sessions')?.get(a.session.id) === a.session && a.agent.session === a.session;
|
|
33
|
+
const safe = (a) => {
|
|
34
|
+
const events = a.session.snapshotEvents();
|
|
35
|
+
return !a.dispatched && !committed(events, a.messages) && !events.slice(a.offset).some(event => event.type === 'request/header' || event.type === 'assistant/message' || event.type === 'assistant/attempt');
|
|
36
|
+
};
|
|
37
|
+
const report = (a, message) => { if (!disposed && a.provider)
|
|
38
|
+
options.diagnostic(a.provider, message); };
|
|
39
|
+
const restore = async (a, originalCode) => {
|
|
40
|
+
try {
|
|
41
|
+
await a.agent.whenIdle();
|
|
42
|
+
if (!live(a) || !safe(a))
|
|
43
|
+
return;
|
|
44
|
+
// Claim true idle synchronously. A competing maintenance claim is reported,
|
|
45
|
+
// not retried. Input waking during this task remains the driver's own work.
|
|
46
|
+
await a.agent.runMaintenance(async (signal) => {
|
|
47
|
+
if (!live(a) || signal.aborted || !safe(a))
|
|
48
|
+
return;
|
|
49
|
+
const queue = [...a.agent.inbox.nextTurn, ...a.agent.inbox.nextStep];
|
|
50
|
+
if (a.messages.some(message => queue.some(item => item.id === message.id)))
|
|
51
|
+
return;
|
|
52
|
+
a.agent.inbox.splice('next-turn', 0, 0, a.messages);
|
|
53
|
+
const store = ctx.get('sessions'), persistence = ctx.get('sessionPersistence');
|
|
54
|
+
if (!store || typeof persistence?.open !== 'function' || !await store.flush(a.session))
|
|
55
|
+
throw Object.assign(new Error('缺少官方持久化能力'), { code: 'INPUT_RETENTION_PERSISTENCE_REQUIRED' });
|
|
56
|
+
if (!live(a) || signal.aborted)
|
|
57
|
+
return;
|
|
58
|
+
const handle = await persistence.open(a.session.id, 'read');
|
|
59
|
+
try {
|
|
60
|
+
if (handle.id !== a.session.id || handle.header?.id !== a.session.id || handle.access !== 'read')
|
|
61
|
+
throw Object.assign(new Error('持久会话身份不符'), { code: 'INPUT_RETENTION_VERIFY_FAILED' });
|
|
62
|
+
const { events } = await handle.read();
|
|
63
|
+
// Replay through the official Session validator and registered inbox fold,
|
|
64
|
+
// including exact fork metadata. Do not normalize invalid durable splices.
|
|
65
|
+
const replay = Session.create(a.session.id, events, handle.header, handle.inheritedEventCount);
|
|
66
|
+
const inbox = ctx.get('sessionProjections')?.snapshot(replay, ['inbox']).values.inbox;
|
|
67
|
+
if (!inbox)
|
|
68
|
+
throw Object.assign(new Error('缺少官方待发队列投影'), { code: 'INPUT_RETENTION_PROJECTION_REQUIRED' });
|
|
69
|
+
const restored = [...inbox['next-turn'], ...inbox['next-step']];
|
|
70
|
+
if (committed(events, a.messages) || a.messages.some(message => restored.filter(item => item !== null && typeof item === 'object' && !Array.isArray(item) && item.id === message.id).length !== 1 || !restored.some(item => isDeepStrictEqual(item, message))))
|
|
71
|
+
throw Object.assign(new Error('持久待发队列核对失败'), { code: 'INPUT_RETENTION_VERIFY_FAILED' });
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
await handle.close();
|
|
75
|
+
}
|
|
76
|
+
if (live(a) && !signal.aborted)
|
|
77
|
+
report(a, `本次模型准备失败,员工原话已保留在待发送队列;请先选择模型或修正设置后手动发送。没有自动重试。(诊断代码:${originalCode})`);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
report(a, `本次模型准备失败;员工原话的恢复或持久化未能确认,请联系管理员核对,不要重复发送。没有自动重试。(原诊断代码:${originalCode};恢复诊断代码:${codeOf(error) ?? 'INPUT_RETENTION_FAILED'})`);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
if (attempts.get(a.agent) === a)
|
|
85
|
+
attempts.delete(a.agent);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
ctx.on('agent/pre-step', async (payload, next) => {
|
|
89
|
+
const decision = await next();
|
|
90
|
+
if (disposed)
|
|
91
|
+
return decision;
|
|
92
|
+
const messages = payload.messages.filter(message => message.source.kind === 'user');
|
|
93
|
+
if (decision.kind !== 'enter' || messages.length === 0 || messages.some(message => !decision.messages.some(accepted => accepted === message || isDeepStrictEqual(accepted, message)))) {
|
|
94
|
+
attempts.delete(payload.agent);
|
|
95
|
+
return decision;
|
|
96
|
+
}
|
|
97
|
+
attempts.set(payload.agent, { agent: payload.agent, session: payload.agent.session, turn: payload.turn, step: payload.step, signal: payload.signal, messages, offset: payload.agent.session.snapshotEvents().length, scheduled: false, dispatched: false });
|
|
98
|
+
return decision;
|
|
99
|
+
});
|
|
100
|
+
ctx.on('agent/request', async (payload, next) => {
|
|
101
|
+
const config = await next(), a = attempts.get(payload.agent);
|
|
102
|
+
if (a && a.turn === payload.turn && a.step === payload.step && a.signal === payload.signal && options.isOurProvider(config.provider)) {
|
|
103
|
+
if (!config.model?.trim()) {
|
|
104
|
+
const error = new LlmError('请先选择模型', 'MODEL_REQUIRED');
|
|
105
|
+
markModelPreparationFailure(config.provider, error, payload.signal);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return config;
|
|
110
|
+
});
|
|
111
|
+
ctx.on('agent/assistant-stream', ({ agent, frame }) => { const a = attempts.get(agent); if (a && frame.type === 'start' && frame.turn === a.turn && frame.step === a.step)
|
|
112
|
+
a.dispatched = true; });
|
|
113
|
+
ctx.on('agent/error', ({ agent, turn, step, error }) => {
|
|
114
|
+
const a = attempts.get(agent), code = codeOf(error);
|
|
115
|
+
const proof = error !== null && typeof error === 'object' ? preparationFailures.get(error) : undefined;
|
|
116
|
+
if (!a || a.turn !== turn || a.step !== step || !proof || proof.consumed || proof.signal !== a.signal || !options.isOurProvider(proof.provider))
|
|
117
|
+
return;
|
|
118
|
+
// Consume before further checks: canceled/unsafe attempts must not leave a
|
|
119
|
+
// reusable proof that could authorize a later failure of the same object.
|
|
120
|
+
proof.consumed = true;
|
|
121
|
+
if (a.scheduled || !code || !preparationCodes.has(code) || !live(a) || !safe(a))
|
|
122
|
+
return;
|
|
123
|
+
a.provider = proof.provider;
|
|
124
|
+
a.scheduled = true;
|
|
125
|
+
void restore(a, code);
|
|
126
|
+
});
|
|
127
|
+
ctx.on('agent/disposed', ({ agent }) => { attempts.delete(agent); });
|
|
128
|
+
ctx.effect(() => () => { disposed = true; attempts.clear(); });
|
|
129
|
+
}
|
package/dist/presentation.js
CHANGED
|
@@ -5,6 +5,12 @@ export const TEST_HOST_NOTICE = '远程主机正在使用测试配置,遇到
|
|
|
5
5
|
/** Employee failures use business language; diagnostic codes remain separately available. */
|
|
6
6
|
export function employeeFailure(code, brand = 'codex') {
|
|
7
7
|
const messages = {
|
|
8
|
+
WORKSPACE_SOURCE_CONFIRMATION_DECLINED: '你没有确认改用本机文件夹,本轮未执行;你的原话仍保留。',
|
|
9
|
+
WORKSPACE_SOURCE_CONFIRMATION_UNAVAILABLE: '我需要打开工作区来源确认,目前无法显示确认动作;本轮未执行,你的原话仍保留。',
|
|
10
|
+
WORKSPACE_SOURCE_SELECTION_UNCONFIRMED: '无法核对工作区来源的确认结果,本轮已停止;你的原话仍保留。',
|
|
11
|
+
WORKSPACE_SOURCE_CONFIRMATION_FAILED: '工作区来源确认未完成,本轮已停止;你的原话仍保留。',
|
|
12
|
+
WORKSPACE_LOCATION_REQUIRED: '我需要你确认这个工作区使用哪一个文件夹,本轮未执行;你的原话仍保留。',
|
|
13
|
+
WORKSPACE_LOCATION_CONFLICT: '工作区来源选择已更新,本次确认不能覆盖新的选择;本轮未执行,你的原话仍保留。',
|
|
8
14
|
LOCAL_USER_TURN_CHANGED: '本次输入已变化,本轮已停止,请确认对话后重新发送。',
|
|
9
15
|
LOCAL_USER_TURN_UNCONFIRMED: '无法核对本次员工输入,本轮已停止;请在当前会话重新发送。',
|
|
10
16
|
LOCAL_USER_TURN_ACCEPTANCE_INVALID: '本次输入的核验回执不一致,本轮已停止,请联系管理员。',
|
package/dist/provider.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { WorkspaceNoticePresentation } from './workspace-notice.ts';
|
|
|
12
12
|
import { StageTiming } from './stage-timing.ts';
|
|
13
13
|
import type { RecoveryJournal, AcpRecovery } from './recovery.ts';
|
|
14
14
|
import type { EmployeeProviderOptions } from './employee-provider.ts';
|
|
15
|
+
import { type WorkspaceSourceOptions, type SourceAdmissionStage } from './workspace-source.ts';
|
|
15
16
|
import type { LocalUserTurnAcceptance } from '@ai-workbench/connect/api';
|
|
16
17
|
import type { EmployeeRouting } from './employee-relay.ts';
|
|
17
18
|
export interface EmployeeExecution {
|
|
@@ -64,6 +65,7 @@ export interface Runtime {
|
|
|
64
65
|
employeePresentation?: string;
|
|
65
66
|
}
|
|
66
67
|
export interface RuntimeRequest extends Invocation {
|
|
68
|
+
recordEarlyFailure?: (error: unknown, stage: SourceAdmissionStage) => Promise<void>;
|
|
67
69
|
userInputIds?: readonly string[];
|
|
68
70
|
localAcceptance?: Readonly<LocalUserTurnAcceptance>;
|
|
69
71
|
localAdmissionCheck?: () => void;
|
|
@@ -135,7 +137,7 @@ type Result<T> = {
|
|
|
135
137
|
} & Pick<ConnectNeed, 'officialReason'>;
|
|
136
138
|
};
|
|
137
139
|
/** 消费正式connect-api 0.7公开子集;X不响应L登录票据、不直接调用登录。 */
|
|
138
|
-
export interface ConnectApi extends Pick<LConnectApi, 'getConnectionState' | 'getLoginPromptState' | 'requestLoginPrompt' | 'refreshLoginPrompts'>, Partial<NativeWorkspaceApi>, Partial<Pick<LConnectApi, 'getWorkspaceBindingState' | 'getModelCatalogTarget' | 'onConnectionState' | 'onLoginPromptState' | 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails' | 'getWorkspaceLocationState' | 'getLocalWorkspaceContext' | 'onWorkspaceLocationState' | 'acceptLocalWorkspaceUserTurn'>> {
|
|
140
|
+
export interface ConnectApi extends Pick<LConnectApi, 'getConnectionState' | 'getLoginPromptState' | 'requestLoginPrompt' | 'refreshLoginPrompts'>, Partial<NativeWorkspaceApi>, Partial<Pick<LConnectApi, 'getWorkspaceBindingState' | 'getModelCatalogTarget' | 'onConnectionState' | 'onLoginPromptState' | 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails' | 'getWorkspaceLocationState' | 'getLocalWorkspaceContext' | 'onWorkspaceLocationState' | 'acceptLocalWorkspaceUserTurn' | 'selectWorkspaceLocation'>> {
|
|
139
141
|
getState?(): ConnectState;
|
|
140
142
|
getSshTarget(): Promise<Result<ConnectTarget>>;
|
|
141
143
|
checkLogin(provider: LoginProvider): Promise<Result<{
|
|
@@ -189,6 +191,7 @@ export declare function connectProvider(get: () => ConnectApi | undefined, optio
|
|
|
189
191
|
agent?: AgentDefinition;
|
|
190
192
|
ask?: Interaction;
|
|
191
193
|
notice?: NoticeConsumer;
|
|
194
|
+
workspaceSource?: WorkspaceSourceOptions;
|
|
192
195
|
employee?: EmployeeProviderOptions;
|
|
193
196
|
local?: LocalRuntimeProvider;
|
|
194
197
|
details?: (request: RuntimeRequest, api: ConnectApi, connection: ConnectionState, target: ConnectTarget) => Promise<DetailChannel>;
|
package/dist/provider.js
CHANGED
|
@@ -3,6 +3,7 @@ import { NeedError } from "./remote.js";
|
|
|
3
3
|
import { CONNECTION_REQUIRED, TEST_HOST_NOTICE } from "./presentation.js";
|
|
4
4
|
import { CODEX_AGENT } from "./agents.js";
|
|
5
5
|
import { StageTiming } from "./stage-timing.js";
|
|
6
|
+
import { confirmLegacyWorkspaceSource } from "./workspace-source.js";
|
|
6
7
|
export function requireLoginCoordinator(api) {
|
|
7
8
|
if (!['getConnectionState', 'getLoginPromptState', 'requestLoginPrompt', 'refreshLoginPrompts'].every(key => typeof api[key] === 'function'))
|
|
8
9
|
throw new NeedError('CONNECT_API_MISMATCH', '我需要 connect-api 0.6.1 的统一登录询问与连接状态能力', 'L >=0.6.1');
|
|
@@ -168,28 +169,49 @@ export function connectProvider(get, options = {}) {
|
|
|
168
169
|
requireLoginCoordinator(api);
|
|
169
170
|
if (request.mode === 'one-shot' && !agent.oneShot)
|
|
170
171
|
throw new NeedError('MODE_UNSUPPORTED', '此远程模型仅支持会话模式', '使用会话模式');
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
const confirmedSource = options.workspaceSource ? await confirmLegacyWorkspaceSource(get, api, request, options.workspaceSource) : undefined;
|
|
173
|
+
let sourceStage = 'input-acceptance';
|
|
174
|
+
if (confirmedSource)
|
|
175
|
+
request.recordEarlyFailure = (error, stage) => confirmedSource.recordFailure(error, stage);
|
|
176
|
+
try {
|
|
177
|
+
confirmedSource?.assertCurrent();
|
|
178
|
+
if (options.employee) {
|
|
179
|
+
const module = await import("./employee-provider.js");
|
|
180
|
+
request.localAcceptance = await module.acceptEmployeeUserTurn(get, options.employee, agent, api, request);
|
|
181
|
+
}
|
|
182
|
+
confirmedSource?.assertCurrent();
|
|
183
|
+
if (confirmedSource) {
|
|
184
|
+
const previous = request.localAdmissionCheck;
|
|
185
|
+
request.localAdmissionCheck = () => { confirmedSource.assertCurrent(); previous?.(); };
|
|
186
|
+
}
|
|
187
|
+
sourceStage = 'login-readiness';
|
|
188
|
+
const { target, connection } = await request.timing.run('login-ready', async () => {
|
|
189
|
+
request.localAdmissionCheck?.();
|
|
190
|
+
const before = api.getConnectionState();
|
|
191
|
+
const target = await ensureReady(api, agent.loginProvider, request.signal);
|
|
192
|
+
validateTarget(target);
|
|
193
|
+
request.signal?.throwIfAborted();
|
|
194
|
+
if (get() !== api)
|
|
195
|
+
throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接');
|
|
196
|
+
const connection = await waitForLogin(api, agent.loginProvider, before, get, request.signal);
|
|
197
|
+
return { target, connection };
|
|
198
|
+
});
|
|
199
|
+
if (request.identity && request.identity !== identity(target))
|
|
200
|
+
throw new NeedError('SESSION_CONTEXT_MISMATCH', '员工主机与原会话不一致,请新建会话', '原员工主机');
|
|
201
|
+
if (options.employee && request.localAcceptance)
|
|
202
|
+
await (await import("./employee-provider.js")).validateEmployeeUserTurn(get, options.employee, api, request, connection);
|
|
176
203
|
request.localAdmissionCheck?.();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (options.employee && request.localAcceptance)
|
|
189
|
-
await (await import("./employee-provider.js")).validateEmployeeUserTurn(get, options.employee, api, request, connection);
|
|
190
|
-
request.localAdmissionCheck?.();
|
|
191
|
-
admissions.set(request, { api, target, connection });
|
|
192
|
-
await options.local?.admit?.(request, api, connection, target);
|
|
204
|
+
admissions.set(request, { api, target, connection });
|
|
205
|
+
sourceStage = 'local-admission';
|
|
206
|
+
await options.local?.admit?.(request, api, connection, target);
|
|
207
|
+
confirmedSource?.assertCurrent();
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
admissions.delete(request);
|
|
211
|
+
request.recordEarlyFailure = undefined;
|
|
212
|
+
await confirmedSource?.recordFailure(error, sourceStage);
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
193
215
|
};
|
|
194
216
|
const resolve = async (request) => {
|
|
195
217
|
if (!admissions.has(request))
|
|
@@ -199,6 +221,7 @@ export function connectProvider(get, options = {}) {
|
|
|
199
221
|
const { api, target, connection } = admitted;
|
|
200
222
|
const stable = () => request.timing.run('connection-confirm', () => waitForLogin(api, agent.loginProvider, connection, get, request.signal));
|
|
201
223
|
await stable();
|
|
224
|
+
request.localAdmissionCheck?.();
|
|
202
225
|
if (!request.sessionId)
|
|
203
226
|
throw new NeedError('PROJECT_BINDING_REQUIRED', '我需要当前会话和已选择的工作区,请使用原生工作区入口', 'DSH会话与原生工作区');
|
|
204
227
|
if (options.employee) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { WorkspaceSourceOptions } from './workspace-source.ts';
|
|
3
|
+
/** Official same-turn one-shot question; never changes policy or fabricates a tool call. */
|
|
4
|
+
export declare function nativeWorkspaceSourceConfirmation(ctx: Context): WorkspaceSourceOptions['confirm'];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
2
|
+
/** Official same-turn one-shot question; never changes policy or fabricates a tool call. */
|
|
3
|
+
export function nativeWorkspaceSourceConfirmation(ctx) {
|
|
4
|
+
return async (question, request) => {
|
|
5
|
+
const agent = request.sessionId ? ctx.get('agents')?.get(SessionId(request.sessionId)) : undefined;
|
|
6
|
+
const approval = ctx.get('approval');
|
|
7
|
+
const session = request.sessionId ? ctx.get('sessions')?.get(SessionId(request.sessionId)) : undefined;
|
|
8
|
+
if (!agent || !approval || !session || agent.session !== session)
|
|
9
|
+
return 'unavailable';
|
|
10
|
+
return approval.request({ agent, toolName: question.action, reason: question.question, signal: request.signal });
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ConnectApi, RuntimeRequest } from './provider.ts';
|
|
2
|
+
import type { DetailObservation } from './conversation-details.ts';
|
|
3
|
+
export type SourceDecision = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
|
|
4
|
+
export type SourceAdmissionStage = 'source-context' | 'source-confirmation' | 'input-acceptance' | 'login-readiness' | 'local-admission' | 'input-prepare' | 'model-catalog' | 'runtime-resolve';
|
|
5
|
+
export interface WorkspaceSourceAdmission {
|
|
6
|
+
assertCurrent(): void;
|
|
7
|
+
recordFailure(error: unknown, stage: SourceAdmissionStage): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export interface WorkspaceSourceOptions {
|
|
10
|
+
workspace(request: RuntimeRequest): Promise<{
|
|
11
|
+
id: string;
|
|
12
|
+
path: string;
|
|
13
|
+
}>;
|
|
14
|
+
currentUserTurn(request: RuntimeRequest): {
|
|
15
|
+
turn: number;
|
|
16
|
+
userMessageId: string;
|
|
17
|
+
};
|
|
18
|
+
confirm(question: {
|
|
19
|
+
question: string;
|
|
20
|
+
action: string;
|
|
21
|
+
}, request: RuntimeRequest): Promise<SourceDecision>;
|
|
22
|
+
record(request: RuntimeRequest, observation: DetailObservation): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/** Explicit human source transition only. No source selection is inferred from a folder path. */
|
|
25
|
+
export declare function confirmLegacyWorkspaceSource(get: () => ConnectApi | undefined, api: ConnectApi, request: RuntimeRequest, hooks: WorkspaceSourceOptions): Promise<WorkspaceSourceAdmission | undefined>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { sanitizeDisplayLabel } from "./conversation-details.js";
|
|
3
|
+
import { NeedError } from "./remote.js";
|
|
4
|
+
const fail = (code) => new NeedError(code, '工作区来源尚未确认,本轮未执行;你的原话仍保留。', '当前工作区来源及原始员工输入');
|
|
5
|
+
const codeOf = (value) => value instanceof NeedError ? value.code : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
|
|
6
|
+
const diagnosticFailure = (error, diagnostic) => Object.assign(new NeedError('CONVERSATION_DETAILS_FAILED', '工作区来源诊断未能保存,本轮已停止;原话保留。', '原错误代码 ' + codeOf(error) + ';诊断错误代码 ' + codeOf(diagnostic)), { cause: new AggregateError([error, diagnostic], 'WORKSPACE_SOURCE_AND_DIAGNOSTIC_FAILED') });
|
|
7
|
+
/** Explicit human source transition only. No source selection is inferred from a folder path. */
|
|
8
|
+
export async function confirmLegacyWorkspaceSource(get, api, request, hooks) {
|
|
9
|
+
let initialContext = { workspaceId: null, bindingId: null, source: null, status: null };
|
|
10
|
+
let diagnosticAttempted = false;
|
|
11
|
+
try {
|
|
12
|
+
request.signal?.throwIfAborted();
|
|
13
|
+
if (api.getContractInfo?.().workspaceLocations !== 1 || typeof api.getWorkspaceLocationState !== 'function')
|
|
14
|
+
return;
|
|
15
|
+
if (!api.getWorkspaceLocationState().items.some(item => item.source === 'legacy-host' && item.status === 'legacy-confirmation-required'))
|
|
16
|
+
return;
|
|
17
|
+
const workspace = { ...await hooks.workspace(request) };
|
|
18
|
+
request.signal?.throwIfAborted();
|
|
19
|
+
const snapshot = api.getWorkspaceLocationState(), rows = snapshot.items.filter(item => item.workspaceId === workspace.id), row = rows[0];
|
|
20
|
+
if (rows.length !== 1 || row?.status !== 'legacy-confirmation-required' || row.source !== 'legacy-host')
|
|
21
|
+
return;
|
|
22
|
+
const frozen = structuredClone(row);
|
|
23
|
+
initialContext = { workspaceId: frozen.workspaceId, bindingId: frozen.bindingId, source: frozen.source, status: frozen.status };
|
|
24
|
+
const connection = api.getConnectionState(), turn = { ...hooks.currentUserTurn(request) };
|
|
25
|
+
const name = sanitizeDisplayLabel(row.workspaceName).replace(/[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/g, '').trim();
|
|
26
|
+
if (!name || name.includes('[已脱敏:'))
|
|
27
|
+
throw fail('WORKSPACE_SOURCE_CONFIRMATION_UNAVAILABLE');
|
|
28
|
+
const question = '这个工作区以前用的是主机上的文件夹。要改用本机的『' + name + '』文件夹吗?';
|
|
29
|
+
const record = (code, outcome, current = frozen, stage = 'source-confirmation') => hooks.record(request, { kind: 'need', severity: outcome === 'confirmed' ? 'info' : 'action-required', code, summary: outcome === 'confirmation-required' ? question : outcome === 'confirmed' ? '已确认改用本机文件夹,继续处理本轮原始请求。' : '本轮工作区来源确认未完成;原始请求保留,未执行。', payload: { phase: 'workspace-source-admission', stage, workspaceId: workspace.id, bindingId: current.bindingId, source: current.source, status: current.status, outcome } });
|
|
30
|
+
// Persistence precedes the question; a failed diagnostic is not retried in this path.
|
|
31
|
+
diagnosticAttempted = true;
|
|
32
|
+
await record('WORKSPACE_SOURCE_CONFIRMATION_REQUIRED', 'confirmation-required');
|
|
33
|
+
let terminalDiagnostic = false;
|
|
34
|
+
async function stable(requireOldSource) {
|
|
35
|
+
request.signal?.throwIfAborted();
|
|
36
|
+
if (get() !== api || api.getContractInfo?.().workspaceLocations !== 1 || typeof api.getWorkspaceLocationState !== 'function')
|
|
37
|
+
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
38
|
+
const now = api.getConnectionState(), location = api.getWorkspaceLocationState();
|
|
39
|
+
if (now.phase !== 'connected' || connection.phase !== 'connected' || now.instanceId !== connection.instanceId || now.epoch !== connection.epoch || location.phase !== 'ready' || location.instanceId !== connection.instanceId || location.epoch !== connection.epoch || snapshot.phase !== 'ready' || snapshot.instanceId !== connection.instanceId || snapshot.epoch !== connection.epoch)
|
|
40
|
+
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
41
|
+
const currentWorkspace = await hooks.workspace(request);
|
|
42
|
+
request.signal?.throwIfAborted();
|
|
43
|
+
if (!isDeepStrictEqual(currentWorkspace, workspace))
|
|
44
|
+
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
45
|
+
if (!isDeepStrictEqual(hooks.currentUserTurn(request), turn))
|
|
46
|
+
throw fail('LOCAL_USER_TURN_CHANGED');
|
|
47
|
+
// Re-read after awaits. Only the explicit successful L selection may replace the frozen binding.
|
|
48
|
+
if (get() !== api)
|
|
49
|
+
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
50
|
+
const after = api.getConnectionState(), state = api.getWorkspaceLocationState();
|
|
51
|
+
if (after.phase !== 'connected' || after.instanceId !== connection.instanceId || after.epoch !== connection.epoch || state.phase !== 'ready' || state.instanceId !== connection.instanceId || state.epoch !== connection.epoch)
|
|
52
|
+
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
53
|
+
const current = state.items.filter(item => item.workspaceId === workspace.id);
|
|
54
|
+
if (current.length !== 1 || current[0].localPath !== workspace.path || requireOldSource && (current[0].bindingId !== frozen.bindingId || current[0].source !== frozen.source || current[0].status !== frozen.status))
|
|
55
|
+
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
56
|
+
return current[0];
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await stable(true);
|
|
60
|
+
if (typeof api.selectWorkspaceLocation !== 'function')
|
|
61
|
+
throw fail('DEPENDENCY_REQUIRED');
|
|
62
|
+
const decision = await hooks.confirm({ question, action: '改用本机文件夹' }, request);
|
|
63
|
+
request.signal?.throwIfAborted();
|
|
64
|
+
if (decision !== 'allowed-once') {
|
|
65
|
+
const code = decision === 'rejected' ? 'WORKSPACE_SOURCE_CONFIRMATION_DECLINED' : decision === 'cancelled' ? 'ABORTED' : 'WORKSPACE_SOURCE_CONFIRMATION_UNAVAILABLE';
|
|
66
|
+
terminalDiagnostic = true;
|
|
67
|
+
await record(code, decision === 'rejected' ? 'declined' : decision === 'cancelled' ? 'cancelled' : 'failed');
|
|
68
|
+
throw fail(code);
|
|
69
|
+
}
|
|
70
|
+
await stable(true);
|
|
71
|
+
// The await continuation itself is a boundary: already-observed cancellation,
|
|
72
|
+
// replacement input or source withdrawal must not start the L mutation.
|
|
73
|
+
request.signal?.throwIfAborted();
|
|
74
|
+
if (get() !== api || typeof api.getWorkspaceLocationState !== 'function')
|
|
75
|
+
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
76
|
+
const atChoice = api.getConnectionState(), choiceState = api.getWorkspaceLocationState(), choiceRows = choiceState.items.filter(item => item.workspaceId === workspace.id);
|
|
77
|
+
if (atChoice.phase !== 'connected' || atChoice.instanceId !== connection.instanceId || atChoice.epoch !== connection.epoch || choiceState.phase !== 'ready' || choiceState.instanceId !== connection.instanceId || choiceState.epoch !== connection.epoch || choiceRows.length !== 1 || choiceRows[0].bindingId !== frozen.bindingId || choiceRows[0].source !== frozen.source || choiceRows[0].status !== frozen.status || choiceRows[0].localPath !== workspace.path)
|
|
78
|
+
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
79
|
+
if (!isDeepStrictEqual(hooks.currentUserTurn(request), turn))
|
|
80
|
+
throw fail('LOCAL_USER_TURN_CHANGED');
|
|
81
|
+
const selected = await api.selectWorkspaceLocation({ instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.id, expectedBindingId: frozen.bindingId, source: 'local' });
|
|
82
|
+
request.signal?.throwIfAborted();
|
|
83
|
+
if (!selected.ok)
|
|
84
|
+
throw new NeedError(selected.error.code, selected.error.message, selected.error.requires);
|
|
85
|
+
const current = await stable(false), value = selected.value, returned = value.items.filter(item => item.workspaceId === workspace.id);
|
|
86
|
+
if (value.phase !== 'ready' || value.instanceId !== connection.instanceId || value.epoch !== connection.epoch || returned.length !== 1 || returned[0].source !== 'local' || returned[0].status !== 'selected' || !returned[0].bindingId || returned[0].bindingId === frozen.bindingId || returned[0].localPath !== workspace.path || current.source !== 'local' || current.status !== 'selected' || current.bindingId !== returned[0].bindingId)
|
|
87
|
+
throw fail('WORKSPACE_SOURCE_SELECTION_UNCONFIRMED');
|
|
88
|
+
const selectedBinding = returned[0].bindingId;
|
|
89
|
+
const assertSelected = () => {
|
|
90
|
+
request.signal?.throwIfAborted();
|
|
91
|
+
if (get() !== api || typeof api.getWorkspaceLocationState !== 'function')
|
|
92
|
+
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
93
|
+
const c = api.getConnectionState(), s = api.getWorkspaceLocationState(), items = s.items.filter(item => item.workspaceId === workspace.id);
|
|
94
|
+
if (c.phase !== 'connected' || c.instanceId !== connection.instanceId || c.epoch !== connection.epoch || s.phase !== 'ready' || s.instanceId !== connection.instanceId || s.epoch !== connection.epoch || items.length !== 1 || items[0].source !== 'local' || items[0].status !== 'selected' || items[0].bindingId !== selectedBinding || items[0].localPath !== workspace.path)
|
|
95
|
+
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
96
|
+
if (!isDeepStrictEqual(hooks.currentUserTurn(request), turn))
|
|
97
|
+
throw fail('LOCAL_USER_TURN_CHANGED');
|
|
98
|
+
};
|
|
99
|
+
terminalDiagnostic = true;
|
|
100
|
+
await record('WORKSPACE_SOURCE_CONFIRMED', 'confirmed', current);
|
|
101
|
+
terminalDiagnostic = false;
|
|
102
|
+
await stable(false);
|
|
103
|
+
assertSelected();
|
|
104
|
+
return { assertCurrent: assertSelected, async recordFailure(error, stage) {
|
|
105
|
+
try {
|
|
106
|
+
await record(request.signal?.aborted ? 'ABORTED' : codeOf(error), request.signal?.aborted ? 'cancelled' : 'failed', current, stage);
|
|
107
|
+
}
|
|
108
|
+
catch (diagnostic) {
|
|
109
|
+
throw diagnosticFailure(error, diagnostic);
|
|
110
|
+
}
|
|
111
|
+
} };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (!terminalDiagnostic) {
|
|
115
|
+
try {
|
|
116
|
+
await record(request.signal?.aborted ? 'ABORTED' : codeOf(error), request.signal?.aborted ? 'cancelled' : 'failed');
|
|
117
|
+
}
|
|
118
|
+
catch (diagnostic) {
|
|
119
|
+
throw diagnosticFailure(error, diagnostic);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (!diagnosticAttempted) {
|
|
127
|
+
try {
|
|
128
|
+
await hooks.record(request, { kind: 'need', severity: 'action-required', code: request.signal?.aborted ? 'ABORTED' : codeOf(error), summary: '无法核对本轮工作区来源,已停止;原话保留。', payload: { phase: 'workspace-source-admission', stage: 'source-context', ...initialContext, outcome: request.signal?.aborted ? 'cancelled' : 'failed' } });
|
|
129
|
+
}
|
|
130
|
+
catch (diagnostic) {
|
|
131
|
+
throw diagnosticFailure(error, diagnostic);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-workbench/remote-agent",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
"dist",
|
|
18
18
|
"locale",
|
|
19
19
|
"cordis.patch.yml",
|
|
20
|
-
"helpers"
|
|
21
|
-
"CANDIDATE-NOT-FOR-INSTALL.json"
|
|
20
|
+
"helpers"
|
|
22
21
|
],
|
|
23
22
|
"scripts": {
|
|
24
23
|
"test": "npm run build && node --test --test-concurrency=1 lib/test/*.test.js",
|
|
@@ -48,6 +47,7 @@
|
|
|
48
47
|
"@deepseek-ai/cordis-plugin-loader": "1.0.5",
|
|
49
48
|
"@deepseek-ai/cordis-plugin-timer": "1.1.6",
|
|
50
49
|
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-agent-loop": "0.2.0-rc.2",
|
|
51
51
|
"@deepseek-ai/dsh-api-gateway": "0.2.0-rc.2",
|
|
52
52
|
"@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
|
|
53
53
|
"@deepseek-ai/dsh-client-connection": "0.2.0-rc.2",
|