@ai-workbench/remote-agent 0.8.29 → 0.9.0-toolbridge.3
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 +12 -2
- package/dist/acp.d.ts +7 -0
- package/dist/acp.js +40 -4
- package/dist/agents.js +1 -1
- package/dist/claude-local.js +2 -1
- package/dist/employee-mcp.d.ts +2 -0
- package/dist/employee-mcp.js +3 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.js +107 -15
- package/dist/provider.d.ts +2 -0
- package/dist/toolbridge-loop.d.ts +19 -0
- package/dist/toolbridge-loop.js +251 -0
- package/dist/toolbridge-table.d.ts +10 -0
- package/dist/toolbridge-table.js +27 -0
- package/dist/toolbridge.d.ts +114 -0
- package/dist/toolbridge.js +205 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
# AI Workbench 远程助手 · @ai-workbench/remote-agent@0.
|
|
1
|
+
# AI Workbench 远程助手 · @ai-workbench/remote-agent@0.9.0-toolbridge.3 · 插件 → 添加插件
|
|
2
|
+
|
|
3
|
+
## 含工具桥预发布线
|
|
4
|
+
|
|
5
|
+
本包为 `0.9.0-toolbridge.3`,在独立 `toolbridge` 分支开发;稳定 main/0.8.x 和原总包保持原样。员工安装由连接组件提供的「AI 写作工作台(含工具桥)」总包,与不含工具桥的版本二选一。预发布渠道为 `toolbridge`,本轮仅提供本地固定包,不发布 npm。
|
|
6
|
+
|
|
7
|
+
选择本机文件夹后,Claude 和 Codex 可以把 DSH 已启用的 MCP、web、skill、ask_user、todo/present/goal 等工具交给 DSH 原生执行和审批。内置文件/命令、子代理/agent-team、run_code 不在桥接表;原有文件和命令执行方式保留。plan 不自动放行桥接权限;default 下 Claude 的桥接原生权限由插件单次应答,员工只在 DSH 审批实际工具。主机文件夹不开放工具桥。
|
|
8
|
+
|
|
9
|
+
工具表按每次主机连接生成快照,新增/移除在下一次连接生效;不承诺 Codex 当前 MCP 会话热刷新。审批期间的插话等当前主机 prompt 结束后再发,停泊中切换助手会停止原轮次并明确提示。工具主动结束DSH轮次时,已提交结果仍交回原agent;确认原生工具收到后结束原prompt,下一条沿原线程继续,不在已关闭步骤追加模型回复。取消和主机结束等待后的结果不重放;工具已执行的副作用不能因取消而撤销,请按说明核对。图片结果由官方附件组件读取后交回。
|
|
10
|
+
|
|
11
|
+
Codex 需要 S 后续安装的独立受保护命令 `/usr/local/aw-host/bin/codex-acp-toolbridge`(aw.8),不会替换原 codex-acp/aw.6;Claude命令不变。指定桥接 server 的等待为 owner 确认的 **1800 秒**、审批交 DSH;不改其他server的调用时限,不新增插件设置项。含桥的本机Codex会话按原生规则等各扩展现有启动时限结算后再捕获工具;启动失败会明确说明,文件和命令仍按原方式执行,本轮不自动重试。N0.4.9、L0.7.37沿既有公开合同;真实界面/员工验收由编排安排。完整合同和本轮启动修复证据见 `docs/contracts/codex-toolbridge.md`、`docs/evidence/2026-10-27-toolbridge-startup/REPORT.md`。
|
|
2
12
|
|
|
3
13
|
通过官方 DSH 插件入口使用 Claude(远程主机)与 Codex(远程主机)。登录和工作区来源由“远程主机连接”组件提供;员工在原生工作区和模型菜单中选择文件夹与型号。
|
|
4
14
|
|
|
5
15
|
文件和命令在所选的本机或主机目录执行,订阅凭据留在主机。插件不改写 DSH 的默认模型,不为员工创建同名主机目录,已有主机会话只提取一次原有主机来源并保存。
|
|
6
16
|
|
|
7
|
-
需要远程主机连接组件 0.7.
|
|
17
|
+
需要远程主机连接组件 0.7.37 或更新的 0.7.x、联网助手 0.4.9,以及支持公开会话归属接口的主机组件 S0.4.26。主机来源保存于官方 storage.domain 的会话元数据表,每轮仅由连接组件检查一次主机目录,不检查本机占位目录。
|
|
8
18
|
|
|
9
19
|
Claude 本机 Bash 会单独消费员工桥代理的控制诊断,不把它交给模型当作命令输出;真实传输中断仍提示结果未知。命令自身的标准输出、标准错误与退出码照常返回。
|
|
10
20
|
|
package/dist/acp.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface EmployeeExecutionMeta {
|
|
|
37
37
|
acpMaxMessageBytes: number;
|
|
38
38
|
}
|
|
39
39
|
export interface AcpStartContext {
|
|
40
|
+
toolBridgeServers?: EmployeeRouting['mcpServers'];
|
|
40
41
|
systemPrompt?: string;
|
|
41
42
|
codexNewSessionOnly?: true;
|
|
42
43
|
employeeExecution?: EmployeeExecutionMeta;
|
|
@@ -95,6 +96,12 @@ export declare class AcpSession {
|
|
|
95
96
|
private detail;
|
|
96
97
|
private employeeNoticeNames;
|
|
97
98
|
private employeeNoticeUnshown;
|
|
99
|
+
private employeeBridge;
|
|
100
|
+
private pendingBridgeStartupFailures;
|
|
101
|
+
private bridgeStartupFailed;
|
|
102
|
+
private bridgeStartupNoticeDelivered;
|
|
103
|
+
private confirmBridgeStartupFailure;
|
|
104
|
+
private flushBridgeStartupNotice;
|
|
98
105
|
private employeeInfo;
|
|
99
106
|
private need;
|
|
100
107
|
constructor(transport: acp.Stream | acp.AgentApp | stable.AgentApp, hooks: AcpHooks);
|
package/dist/acp.js
CHANGED
|
@@ -226,7 +226,31 @@ export class AcpSession {
|
|
|
226
226
|
}
|
|
227
227
|
employeeNoticeNames = new Set();
|
|
228
228
|
employeeNoticeUnshown = false;
|
|
229
|
+
employeeBridge = false;
|
|
230
|
+
pendingBridgeStartupFailures = new Set();
|
|
231
|
+
bridgeStartupFailed = false;
|
|
232
|
+
bridgeStartupNoticeDelivered = false;
|
|
233
|
+
confirmBridgeStartupFailure() {
|
|
234
|
+
if (!this.sessionId)
|
|
235
|
+
return;
|
|
236
|
+
const failed = this.pendingBridgeStartupFailures.has(this.sessionId);
|
|
237
|
+
this.pendingBridgeStartupFailures.clear();
|
|
238
|
+
if (failed && !this.bridgeStartupFailed) {
|
|
239
|
+
this.bridgeStartupFailed = true;
|
|
240
|
+
this.detail({ kind: 'extension', severity: 'warning', code: 'TOOLBRIDGE_STARTUP_FAILED', summary: '本轮DSH扩展工具桥启动失败;文件和命令继续使用原生路径,没有自动重试。', method: 'session/update', payload: { server: 'employee', status: 'failed', source: 'codex-acp/mcp-startup-report', nativeSessionId: this.sessionId } });
|
|
241
|
+
}
|
|
242
|
+
this.flushBridgeStartupNotice();
|
|
243
|
+
}
|
|
244
|
+
flushBridgeStartupNotice() {
|
|
245
|
+
if (!this.bridgeStartupFailed || this.bridgeStartupNoticeDelivered || !this.busy || this.closeRequested || this.turnSignal?.aborted || !this.hooks.onEmployeeNotice)
|
|
246
|
+
return;
|
|
247
|
+
this.hooks.onEmployeeNotice('本轮DSH里的扩展工具未能连接到Codex,暂时不可用。文件和命令仍按原方式执行;具体原因见技术详情。本轮不会自动重试。');
|
|
248
|
+
this.bridgeStartupNoticeDelivered = true;
|
|
249
|
+
}
|
|
229
250
|
employeeInfo(value) {
|
|
251
|
+
const bridge = record(record(value)?.mcpBridge);
|
|
252
|
+
if (bridge)
|
|
253
|
+
this.detail({ kind: 'session', severity: 'info', code: 'TOOLBRIDGE_HOST_POLICY', summary: '主机回报本轮指定桥接server的等待与审批策略;不改变其他工具。', payload: { toolTimeoutSec: typeof bridge.toolTimeoutSec === 'number' && Number.isFinite(bridge.toolTimeoutSec) ? bridge.toolTimeoutSec : null, approval: bridge.approval === 'approve' ? 'approve' : 'UNKNOWN', source: bridge.source === 'owner-2026-10-26' ? bridge.source : 'UNKNOWN' } });
|
|
230
254
|
const info = employeeExecutionInfo(value);
|
|
231
255
|
if (!info)
|
|
232
256
|
return;
|
|
@@ -260,6 +284,11 @@ export class AcpSession {
|
|
|
260
284
|
this.detail({ kind: 'session', severity: 'warning', code: 'ACP_FOREIGN_SESSION_UPDATE', summary: '收到不属于当前远端会话的通知,未作为正文处理', method: 'session/update', payload: notification });
|
|
261
285
|
return;
|
|
262
286
|
}
|
|
287
|
+
const update = notification.update;
|
|
288
|
+
if (this.employeeBridge && !this.closeRequested && (update.sessionUpdate === 'tool_call' || update.sessionUpdate === 'tool_call_update') && update.status === 'failed' && update.title === 'mcp__employee__startup' && update.toolCallId.startsWith('mcp_startup.employee.') && update.toolCallId.length > 'mcp_startup.employee.'.length) {
|
|
289
|
+
this.pendingBridgeStartupFailures.add(notification.sessionId);
|
|
290
|
+
this.confirmBridgeStartupFailure();
|
|
291
|
+
}
|
|
263
292
|
const meta = record(notification._meta);
|
|
264
293
|
if (notification.update.sessionUpdate === 'current_mode_update') {
|
|
265
294
|
this.nativeModeId = notification.update.currentModeId;
|
|
@@ -479,20 +508,21 @@ export class AcpSession {
|
|
|
479
508
|
throw new NeedError('SESSION_ALREADY_STARTED', '此连接已用于建立会话,不能转用工作区或身份', '新的独立远端连接');
|
|
480
509
|
this.startAttempted = true;
|
|
481
510
|
this.employeeMeta = context?.employeeExecution;
|
|
511
|
+
this.employeeBridge = !!this.employeeMeta && !!context?.toolBridgeServers;
|
|
482
512
|
const abort = () => this.close({ layer: 'acp', initiator: 'acp.start-signal', reason: 'ABORTED', action: 'abort' }, new NeedError('ABORTED', '会话建立已取消', '重新连接主机'));
|
|
483
513
|
signal?.throwIfAborted();
|
|
484
514
|
signal?.addEventListener('abort', abort, { once: true });
|
|
485
515
|
try {
|
|
486
516
|
const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
|
|
487
|
-
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.
|
|
488
|
-
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1, closeReason: 1 } } } : {}) },
|
|
517
|
+
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.9.0-toolbridge.3' },
|
|
518
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1, closeReason: 1, ...(context?.toolBridgeServers ? { mcpBridge: 1 } : {}) } } } : {}) },
|
|
489
519
|
}, { cancellationSignal: signal }));
|
|
490
520
|
if (hello.protocolVersion !== acp.PROTOCOL_VERSION)
|
|
491
521
|
throw new NeedError('ACP_VERSION_MISMATCH', 'ACP 协议版本不兼容', '支持 ACP v1 的远端 agent');
|
|
492
522
|
this.negotiated = hello.agentCapabilities ?? {};
|
|
493
523
|
if (this.employeeMeta) {
|
|
494
524
|
const policy = record(hello._meta?.awEmployeeExecution);
|
|
495
|
-
if (!this.hooks.employeeExecution || !policy || Object.keys(policy).length !== 5 || policy.version !== 1 || policy.nativeCliVersion !== '0.160.1' || policy.native !== true || policy.perRpcGate !== true || policy.noHostFallback !== true)
|
|
525
|
+
if (!this.hooks.employeeExecution || !policy || Object.keys(policy).length !== (context?.toolBridgeServers ? 6 : 5) || context?.toolBridgeServers && policy.mcpBridge !== 1 || policy.version !== 1 || policy.nativeCliVersion !== '0.160.1' || policy.native !== true || policy.perRpcGate !== true || policy.noHostFallback !== true)
|
|
496
526
|
throw new NeedError('EMPLOYEE_EXECUTION_UNSUPPORTED', '当前主机尚不支持在你的电脑执行Codex原生工具,本轮没有执行。', 'Codex员工端原生执行适配器与明确能力声明');
|
|
497
527
|
this.employeeNegotiated = true;
|
|
498
528
|
this.detail({ kind: 'session', severity: 'info', code: 'EMPLOYEE_EXECUTION_SELECTED', summary: '文件和命令在员工电脑的选定目录执行;订阅凭据留在主机,本机扩展的忽略详情单独记录。', payload: { bindingId: this.employeeMeta.bindingId, workspaceId: this.employeeMeta.workspaceId, cwd: this.employeeMeta.cwd, nativeCliVersion: '0.160.1', noHostFallback: true, agentConfigSource: 'host CODEX_HOME', unsupportedLocalFeatures: ['http-mcp-execution', 'hooks', 'plugins', 'cold-resume', 'slash-commands', 'host-file-reports'] } });
|
|
@@ -503,7 +533,7 @@ export class AcpSession {
|
|
|
503
533
|
this.validatePrompt(prompt);
|
|
504
534
|
const routing = context?.employeeRouting ? structuredClone(context.employeeRouting) : undefined;
|
|
505
535
|
this.employeeRouting = routing;
|
|
506
|
-
const params = { cwd, mcpServers: routing ? structuredClone(routing.mcpServers) : [], ...((routing || this.employeeMeta) ? { _meta: { ...(this.employeeMeta ? { awEmployeeExecution: this.employeeMeta } : {}), ...(routing ? { claudeCode: { options: employeeClaudeOptions(routing) } } : {}) } } : {}), ...(systemPrompt && !(resumeId && this.codexNewSessionOnly) ? { systemPrompt: { mode: 'append', content: [{ type: 'text', text: systemPrompt }] } } : {}) };
|
|
536
|
+
const params = { cwd, mcpServers: context?.toolBridgeServers ? structuredClone(context.toolBridgeServers) : routing ? structuredClone(routing.mcpServers) : [], ...((routing || this.employeeMeta) ? { _meta: { ...(this.employeeMeta ? { awEmployeeExecution: this.employeeMeta } : {}), ...(routing ? { claudeCode: { options: employeeClaudeOptions(routing) } } : {}) } } : {}), ...(systemPrompt && !(resumeId && this.codexNewSessionOnly) ? { systemPrompt: { mode: 'append', content: [{ type: 'text', text: systemPrompt }] } } : {}) };
|
|
507
537
|
const configRevision = this.resetConfiguration();
|
|
508
538
|
const restore = async (operation) => {
|
|
509
539
|
try {
|
|
@@ -529,6 +559,8 @@ export class AcpSession {
|
|
|
529
559
|
this.sessionId = resumeId;
|
|
530
560
|
if (this.negotiated.sessionCapabilities?.resume) {
|
|
531
561
|
const result = await restore(() => this.stage('acp-session-resume', () => this.connection.agent.request(acp.methods.agent.session.resume, { ...params, sessionId: resumeId }, { cancellationSignal: signal })));
|
|
562
|
+
if (this.employeeMeta)
|
|
563
|
+
this.employeeInfo(result._meta?.awEmployeeExecutionInfo);
|
|
532
564
|
this.lifecycleConfiguration(result, configRevision);
|
|
533
565
|
this.method = acp.methods.agent.session.resume;
|
|
534
566
|
}
|
|
@@ -536,6 +568,8 @@ export class AcpSession {
|
|
|
536
568
|
if (!this.negotiated.loadSession)
|
|
537
569
|
throw new NeedError('RESUME_UNSUPPORTED', '远端未声明会话恢复或加载能力,不能恢复原会话', '支持 session/resume 或 session/load 的 agent');
|
|
538
570
|
const result = await restore(() => this.stage('acp-session-load', () => this.connection.agent.request(acp.methods.agent.session.load, { ...params, sessionId: resumeId }, { cancellationSignal: signal })));
|
|
571
|
+
if (this.employeeMeta)
|
|
572
|
+
this.employeeInfo(result._meta?.awEmployeeExecutionInfo);
|
|
539
573
|
this.lifecycleConfiguration(result, configRevision);
|
|
540
574
|
this.method = acp.methods.agent.session.load;
|
|
541
575
|
}
|
|
@@ -548,6 +582,7 @@ export class AcpSession {
|
|
|
548
582
|
this.lifecycleConfiguration(result, configRevision);
|
|
549
583
|
this.method = acp.methods.agent.session.new;
|
|
550
584
|
}
|
|
585
|
+
this.confirmBridgeStartupFailure();
|
|
551
586
|
await this.hooks.sessionOwnership?.bind(this.sessionId, signal);
|
|
552
587
|
this.ready = true;
|
|
553
588
|
this.cwd = cwd;
|
|
@@ -599,6 +634,7 @@ export class AcpSession {
|
|
|
599
634
|
};
|
|
600
635
|
signal?.addEventListener('abort', cancel, { once: true });
|
|
601
636
|
try {
|
|
637
|
+
this.flushBridgeStartupNotice();
|
|
602
638
|
const result = await this.stage('acp-prompt', () => {
|
|
603
639
|
this.promptPermissions.set(params, { current, native });
|
|
604
640
|
const request = this.connection.agent.request(acp.methods.agent.session.prompt, params, { cancellationSignal: signal });
|
package/dist/agents.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const CODEX_AGENT = Object.freeze({ id: 'codex-remote', name: Object.freeze({ zh: 'Codex(远程主机)', en: 'Codex (Remote Host)' }), command: '/usr/local/aw-host/bin/codex-acp', loginProvider: 'codex' });
|
|
1
|
+
export const CODEX_AGENT = Object.freeze({ id: 'codex-remote', name: Object.freeze({ zh: 'Codex(远程主机)', en: 'Codex (Remote Host)' }), command: '/usr/local/aw-host/bin/codex-acp-toolbridge', loginProvider: 'codex' });
|
|
2
2
|
export const CLAUDE_AGENT = Object.freeze({ id: 'claude-code-workbench', name: Object.freeze({ zh: 'Claude(远程主机)', en: 'Claude (Remote Host)' }), command: '/usr/local/aw-host/bin/claude-agent-acp', loginProvider: 'claude' });
|
|
3
3
|
export const AGENTS = Object.freeze([CODEX_AGENT, CLAUDE_AGENT]);
|
|
4
4
|
export function agentForProvider(id) { return AGENTS.find(agent => agent.id === id); }
|
package/dist/claude-local.js
CHANGED
|
@@ -115,7 +115,8 @@ export function createClaudeLocalProvider(options) {
|
|
|
115
115
|
const network = options.getNetwork();
|
|
116
116
|
lease = await (options.ports?.lease ?? openEmployeeLease)({ closeDiagnostics, getConnect: () => options.getConnect(), getNetwork: options.getNetwork, readWorkspace: () => options.readWorkspace(request.sessionId), host: target, sessionId: request.sessionId, control: async (_target, command, signal) => (await transport()).run(command, signal), detail: unscoped }, request.signal);
|
|
117
117
|
const shell = (options.ports?.shell ?? createEmployeeShell)({ closeDiagnostics, target: lease.target, sshArgs: lease.sshArgs, maxBytes: maximum, killGraceMs: grace, attach: async (spec) => (await transport()).attach([spec.command, ...spec.args].map(shellQuote).join(' ')) });
|
|
118
|
-
|
|
118
|
+
request.toolBridge?.enable(event => details.record(event));
|
|
119
|
+
mcp = await (options.ports?.mcp ?? openEmployeeMcp)({ bridge: request.toolBridge, closeDiagnostics, lease, maxBytes: maximum, killGraceMs: grace, runShell: shell, activeSignal: () => entry?.active?.signal, captureDetail: () => { const sink = entry?.active?.details; if (!sink)
|
|
119
120
|
throw failure('EMPLOYEE_NO_ACTIVE_TURN'); return observation => sink.record(observation); }, detail: unscoped });
|
|
120
121
|
entry = { runtime: undefined, scope, lease, details, request };
|
|
121
122
|
const row = entry;
|
package/dist/employee-mcp.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToolBridge } from './toolbridge.ts';
|
|
1
2
|
import { type CloseCause, type CloseDiagnostics, type CloseObserver } from './close-diagnostics.ts';
|
|
2
3
|
import { type Socket } from 'node:net';
|
|
3
4
|
import { type JSONRPCMessage, type Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
@@ -28,6 +29,7 @@ export declare class EmployeeSocketTransport implements Transport {
|
|
|
28
29
|
close(cause?: CloseCause): Promise<void>;
|
|
29
30
|
}
|
|
30
31
|
export interface EmployeeMcpOptions {
|
|
32
|
+
bridge?: ToolBridge;
|
|
31
33
|
closeDiagnostics?: CloseDiagnostics;
|
|
32
34
|
lease: EmployeeLease;
|
|
33
35
|
maxBytes: number;
|
package/dist/employee-mcp.js
CHANGED
|
@@ -136,12 +136,14 @@ export async function openEmployeeMcp(options) {
|
|
|
136
136
|
const mcp = new Server({ name: 'ai-workbench-employee', version: '0.8.14' }, { capabilities: { tools: {} } }), transport = new EmployeeSocketTransport(socket, options.maxBytes, observeConnection);
|
|
137
137
|
clients.add(mcp);
|
|
138
138
|
transports.add(transport);
|
|
139
|
-
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: EMPLOYEE_MCP_TOOLS }));
|
|
139
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...EMPLOYEE_MCP_TOOLS, ...options.bridge?.tools ?? []] }));
|
|
140
140
|
mcp.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
141
141
|
const active = options.activeSignal();
|
|
142
142
|
if (!active || active.aborted)
|
|
143
143
|
return { isError: true, content: [{ type: 'text', text: 'EMPLOYEE_NO_ACTIVE_TURN:当前没有已接纳的用户轮次,本机工具未执行。' }] };
|
|
144
144
|
const signal = AbortSignal.any([active, extra.signal, transport.signal, options.lease.signal]);
|
|
145
|
+
if (options.bridge?.isBridge(request.params.name))
|
|
146
|
+
return options.bridge.call(request.params.name, request.params.arguments ?? {}, signal);
|
|
145
147
|
let sink;
|
|
146
148
|
try {
|
|
147
149
|
sink = options.captureDetail?.() ?? options.detail;
|
package/dist/index.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
57
57
|
private readonly directory?;
|
|
58
58
|
private readonly preflightDiagnostics?;
|
|
59
59
|
private readonly permissions?;
|
|
60
|
+
private bridgeLoop;
|
|
60
61
|
private config;
|
|
61
62
|
private sessions;
|
|
62
63
|
private turns;
|
|
@@ -65,7 +66,8 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
65
66
|
private detached;
|
|
66
67
|
private modelPresentation;
|
|
67
68
|
private employeeNoticeSessions;
|
|
68
|
-
|
|
69
|
+
private pendingEmployeeNotices;
|
|
70
|
+
constructor(config: PluginConfig | (() => PluginConfig), resolveRuntime: RuntimeProvider, approve?: InvocationApproval | undefined, providerName?: () => string, agent?: AgentDefinition, readImage?: ImageReader | undefined, reportLoginFailure?: LoginFailureReporter | undefined, directory?: ModelDirectory | undefined, preflightDiagnostics?: PreflightDiagnostics | undefined, permissions?: PermissionSource | undefined, bridgeStopped?: (sessionId: string, notice: string) => void);
|
|
69
71
|
providerInfo(provider: string): {
|
|
70
72
|
id: string;
|
|
71
73
|
name: string;
|
|
@@ -88,6 +90,8 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
88
90
|
jitterRatio: number;
|
|
89
91
|
};
|
|
90
92
|
private cleanupRuntime;
|
|
93
|
+
completeDshTurn(sessionId: string, messages: GenerateOptions['messages']): Promise<void>;
|
|
94
|
+
hasBridgeSession(sessionId: string): boolean;
|
|
91
95
|
disposeSession(sessionId: string): Promise<void>;
|
|
92
96
|
detachSession(sessionId: string): Promise<void>;
|
|
93
97
|
dispose(): Promise<void>;
|
|
@@ -97,7 +101,7 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
97
101
|
/** One official registration owns both routes, preserving opaque replay ownership. */
|
|
98
102
|
export declare class RemoteAgentsAdapter extends LlmAdapter {
|
|
99
103
|
private adapters;
|
|
100
|
-
constructor(config: () => PluginConfig, makeRuntime: (agent: AgentDefinition) => RuntimeProvider, approve: InvocationApproval, nameFor: (agent: AgentDefinition) => string, readImage?: ImageReader, reportLoginFailure?: LoginFailureReporter, directory?: ModelDirectory, preflightDiagnostics?: PreflightDiagnostics, permissions?: PermissionSource);
|
|
104
|
+
constructor(config: () => PluginConfig, makeRuntime: (agent: AgentDefinition) => RuntimeProvider, approve: InvocationApproval, nameFor: (agent: AgentDefinition) => string, readImage?: ImageReader, reportLoginFailure?: LoginFailureReporter, directory?: ModelDirectory, preflightDiagnostics?: PreflightDiagnostics, permissions?: PermissionSource, bridgeStopped?: (sessionId: string, notice: string) => void);
|
|
101
105
|
private adapter;
|
|
102
106
|
providerInfo(provider: string): {
|
|
103
107
|
id: string;
|
|
@@ -108,8 +112,8 @@ export declare class RemoteAgentsAdapter extends LlmAdapter {
|
|
|
108
112
|
})[]>;
|
|
109
113
|
resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
110
114
|
prepareCall(provider: string, model: string, signal?: AbortSignal): Promise<{
|
|
111
|
-
model: LlmResolvedModelInfo;
|
|
112
115
|
stream: (options: GenerateOptions) => AsyncGenerator<StreamChunk, any, any>;
|
|
116
|
+
model: LlmResolvedModelInfo;
|
|
113
117
|
}>;
|
|
114
118
|
providerRetryPolicy(): {
|
|
115
119
|
mode: "normal";
|
|
@@ -120,6 +124,7 @@ export declare class RemoteAgentsAdapter extends LlmAdapter {
|
|
|
120
124
|
jitterRatio: number;
|
|
121
125
|
};
|
|
122
126
|
stream(options: GenerateOptions): AsyncGenerator<StreamChunk, any, any>;
|
|
127
|
+
completeDshTurn(sessionId: string, messages: GenerateOptions['messages']): Promise<void>;
|
|
123
128
|
disposeSession(sessionId: string): Promise<void>;
|
|
124
129
|
detachSession(sessionId: string): Promise<void>;
|
|
125
130
|
dispose(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { ToolBridgeLoop } from "./toolbridge-loop.js";
|
|
2
|
+
import { openToolBridgeMcp, TOOLBRIDGE_NOTICE } from "./toolbridge.js";
|
|
1
3
|
import { connectNeedMessage } from "./connect-need.js";
|
|
2
4
|
import { CloseDiagnostics, withFirstFailureEvidence, failureEvidenceSummary, closeCode } from "./close-diagnostics.js";
|
|
3
5
|
import { withRemoteFailureEvidence } from "./failure-evidence.js";
|
|
4
6
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
7
|
+
import { createAssistantMessage } from '@deepseek-ai/dsh-llm';
|
|
5
8
|
import Schema from '@deepseek-ai/schemastery';
|
|
6
9
|
import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
|
|
7
10
|
import { createHash } from 'node:crypto';
|
|
@@ -91,6 +94,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
91
94
|
directory;
|
|
92
95
|
preflightDiagnostics;
|
|
93
96
|
permissions;
|
|
97
|
+
bridgeLoop;
|
|
94
98
|
config;
|
|
95
99
|
sessions = new Map();
|
|
96
100
|
turns = new Map();
|
|
@@ -99,7 +103,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
99
103
|
detached = new Set();
|
|
100
104
|
modelPresentation = new ModelPresentation();
|
|
101
105
|
employeeNoticeSessions = new Set();
|
|
102
|
-
|
|
106
|
+
pendingEmployeeNotices = new Set();
|
|
107
|
+
constructor(config, resolveRuntime, approve, providerName = () => CODEX_AGENT.name.zh, agent = CODEX_AGENT, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions, bridgeStopped) {
|
|
103
108
|
super();
|
|
104
109
|
this.resolveRuntime = resolveRuntime;
|
|
105
110
|
this.approve = approve;
|
|
@@ -110,6 +115,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
110
115
|
this.directory = directory;
|
|
111
116
|
this.preflightDiagnostics = preflightDiagnostics;
|
|
112
117
|
this.permissions = permissions;
|
|
118
|
+
this.bridgeLoop = new ToolBridgeLoop(readImage, bridgeStopped);
|
|
113
119
|
this.config = typeof config === 'function' ? config : () => config;
|
|
114
120
|
}
|
|
115
121
|
providerInfo(provider) { return { id: provider, name: this.providerName() }; }
|
|
@@ -156,7 +162,10 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
156
162
|
void Promise.resolve().then(() => runtime.dispose?.(cause)).catch(() => console.warn('[remote-agent] 旧主机会话暂未清理,将由主机自动回收'));
|
|
157
163
|
return Promise.resolve();
|
|
158
164
|
}
|
|
165
|
+
completeDshTurn(sessionId, messages) { this.sessions.get(sessionId)?.runtime.execution?.endTurn(); return this.bridgeLoop.conclude(sessionId, messages); }
|
|
166
|
+
hasBridgeSession(sessionId) { return this.bridgeLoop.active(sessionId); }
|
|
159
167
|
async disposeSession(sessionId) {
|
|
168
|
+
await this.bridgeLoop.cancel(sessionId);
|
|
160
169
|
this.turns.get(sessionId)?.abort();
|
|
161
170
|
const current = this.sessions.get(sessionId);
|
|
162
171
|
if (!current)
|
|
@@ -166,6 +175,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
166
175
|
await this.cleanupRuntime(current.runtime);
|
|
167
176
|
}
|
|
168
177
|
async detachSession(sessionId) {
|
|
178
|
+
await this.bridgeLoop.cancel(sessionId);
|
|
169
179
|
const current = this.sessions.get(sessionId), turn = this.turns.get(sessionId);
|
|
170
180
|
if (!current && !turn)
|
|
171
181
|
return;
|
|
@@ -188,8 +198,10 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
188
198
|
for (const live of this.sessions.values())
|
|
189
199
|
live.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.dispose', reason: 'PLUGIN_DISPOSED', action: 'dispose', changes: [{ field: 'adapter.shutting-down', before: this.shuttingDown, after: true }] });
|
|
190
200
|
this.shuttingDown = true;
|
|
201
|
+
await this.bridgeLoop.dispose();
|
|
191
202
|
this.modelPresentation.clear();
|
|
192
203
|
this.employeeNoticeSessions.clear();
|
|
204
|
+
this.pendingEmployeeNotices.clear();
|
|
193
205
|
const results = await Promise.allSettled([...new Set([...this.sessions.keys(), ...this.turns.keys()])].map(id => this.detachSession(id)));
|
|
194
206
|
this.lifetime.abort();
|
|
195
207
|
const failure = results.find(result => result.status === 'rejected');
|
|
@@ -197,9 +209,15 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
197
209
|
throw failure.reason;
|
|
198
210
|
}
|
|
199
211
|
async *stream(options) {
|
|
200
|
-
let promptDispatched =
|
|
212
|
+
let promptDispatched = !!options.sessionId && (this.sessions.get(options.sessionId)?.session.promptDispatchCount ?? 0) > 0;
|
|
201
213
|
try {
|
|
202
|
-
|
|
214
|
+
for await (const chunk of this.bridgeLoop.stream(options, (o, b) => this.generate(o, () => { promptDispatched = true; }, b))) {
|
|
215
|
+
if (options.sessionId && this.pendingEmployeeNotices.has(options.sessionId) && chunk.type === 'text-delta' && chunk.text.startsWith(EMPLOYEE_EXECUTION_NOTICE)) {
|
|
216
|
+
this.employeeNoticeSessions.add(options.sessionId);
|
|
217
|
+
this.pendingEmployeeNotices.delete(options.sessionId);
|
|
218
|
+
}
|
|
219
|
+
yield chunk;
|
|
220
|
+
}
|
|
203
221
|
}
|
|
204
222
|
catch (error) {
|
|
205
223
|
if (error instanceof ModelConfigurationError)
|
|
@@ -223,8 +241,12 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
223
241
|
}
|
|
224
242
|
throw new LlmError(employeeFailure('TURN_FAILED', this.agent.loginProvider, promptDispatched), 'TURN_FAILED', { cause: error });
|
|
225
243
|
}
|
|
244
|
+
finally {
|
|
245
|
+
if (options.sessionId)
|
|
246
|
+
this.pendingEmployeeNotices.delete(options.sessionId);
|
|
247
|
+
}
|
|
226
248
|
}
|
|
227
|
-
async *generate(original, onPromptDispatched) {
|
|
249
|
+
async *generate(original, onPromptDispatched, bridge) {
|
|
228
250
|
if (original.purpose)
|
|
229
251
|
throw new NeedError('PURPOSE_UNSUPPORTED', '此远程模型不处理后台标题或上下文整理请求,未发送模型请求', '独立的后台请求能力');
|
|
230
252
|
if (this.lifetime.signal.aborted)
|
|
@@ -238,6 +260,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
238
260
|
const signal = AbortSignal.any([this.lifetime.signal, turn.signal, ...(original.signal ? [original.signal] : [])]);
|
|
239
261
|
const options = { ...original, signal };
|
|
240
262
|
const abortOffs = [];
|
|
263
|
+
let bridgeMcp;
|
|
241
264
|
let request;
|
|
242
265
|
let earlyStage = 'input-prepare';
|
|
243
266
|
let runtime;
|
|
@@ -261,7 +284,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
261
284
|
runtime.details.record(entry); };
|
|
262
285
|
const config = this.config();
|
|
263
286
|
const mode = 'session';
|
|
264
|
-
request = { sessionId: options.sessionId, mode: mode ?? 'session', signal };
|
|
287
|
+
request = { sessionId: options.sessionId, mode: mode ?? 'session', signal, toolBridge: bridge };
|
|
265
288
|
const observeAbort = (source, initiator, reason) => { if (!source)
|
|
266
289
|
return; const record = () => { (request?.closeDiagnostics ?? runtime?.closeDiagnostics)?.record({ layer: 'adapter', initiator, reason, action: 'abort', changes: [{ field: 'adapter.turn-aborted', before: false, after: source.aborted }] }); }; source.addEventListener('abort', record, { once: true }); abortOffs.push(() => source.removeEventListener('abort', record)); };
|
|
267
290
|
observeAbort(original.signal, 'signal.caller', 'CALLER_ABORTED');
|
|
@@ -329,6 +352,10 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
329
352
|
if (runtime.hostWorkspace && mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
|
|
330
353
|
throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
|
|
331
354
|
const details = runtime.details ?? await createRemoteDetails({ warn: line => console.warn(line) }).open({ sessionId: options.sessionId ?? 'unscoped', provider: this.agent.loginProvider, identity: runtime.identity, connection: runtime.connection });
|
|
355
|
+
if (bridge && runtime.employeeExecutionMeta && !runtime.hostWorkspace && bridge.table.size) {
|
|
356
|
+
bridge.enable(event => details.record(event));
|
|
357
|
+
bridgeMcp = await openToolBridgeMcp(bridge, config.acpMaxMessageBytes);
|
|
358
|
+
}
|
|
332
359
|
recordInformation();
|
|
333
360
|
request.recordEarlyFailure = undefined;
|
|
334
361
|
if (input.historyTransfer)
|
|
@@ -339,16 +366,22 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
339
366
|
activeMapper = mapper;
|
|
340
367
|
let id;
|
|
341
368
|
let stop;
|
|
369
|
+
const pointerRuntime = runtime;
|
|
370
|
+
const makePointer = (nativeId) => ({ ...(pointerRuntime.hostSource ? { hostSource: pointerRuntime.hostSource } : {}), ...(modelConfiguration ? { modelConfiguration } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), identity: pointerRuntime.identity, cwd: pointerRuntime.cwd, sessionId: nativeId, ...(pointerRuntime.binding ? { bindingId: pointerRuntime.binding.bindingId, workspaceId: pointerRuntime.binding.workspaceId } : {}) });
|
|
342
371
|
if (runtime.employeePresentation)
|
|
343
372
|
details.record({ kind: 'session', severity: 'warning', code: 'EMPLOYEE_EXECUTION_SCOPE', summary: runtime.employeePresentation, payload: { source: 'local' } });
|
|
344
373
|
if (runtime.execution)
|
|
345
374
|
details.record({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_SCOPE', summary: runtime.execution.notice, payload: runtime.execution.scope });
|
|
346
375
|
if ((runtime.employeePresentation || runtime.execution) && !employeeNoticeShown) {
|
|
347
|
-
for (const chunk of mapper.text(EMPLOYEE_EXECUTION_NOTICE + '\n\n')) {
|
|
376
|
+
for (const chunk of mapper.text(EMPLOYEE_EXECUTION_NOTICE + (bridge?.enabled ? '\n\n' + TOOLBRIDGE_NOTICE : '') + '\n\n')) {
|
|
348
377
|
if (chunk.type === 'text-delta') {
|
|
349
378
|
employeeNoticeShown = true;
|
|
350
|
-
if (key)
|
|
351
|
-
|
|
379
|
+
if (key) {
|
|
380
|
+
if (bridge)
|
|
381
|
+
this.pendingEmployeeNotices.add(key);
|
|
382
|
+
else
|
|
383
|
+
this.employeeNoticeSessions.add(key);
|
|
384
|
+
}
|
|
352
385
|
}
|
|
353
386
|
yield chunk;
|
|
354
387
|
}
|
|
@@ -363,6 +396,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
363
396
|
let failure;
|
|
364
397
|
const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
|
|
365
398
|
let promptPermit;
|
|
399
|
+
let confirmedNativeMode;
|
|
366
400
|
const hooks = { closeDiagnostics: runtime.closeDiagnostics, broker: runtime.broker, sessionOwnership: runtime.sessionOwnership, employeeExecution: runtime.employeeExecution, ...this.permissions ? { promptGuard: () => {
|
|
367
401
|
if (!promptPermit)
|
|
368
402
|
throw new NeedError('PERMISSION_POLICY_CHANGED', '当前权限没有保持确认,本次没有继续派发。', '当前权限代次和同值回执');
|
|
@@ -373,6 +407,17 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
373
407
|
} } : {}, approve: async (permission, approvalSignal) => {
|
|
374
408
|
const execution = runtime.execution;
|
|
375
409
|
const toolName = permission.toolCall.name ?? permission.toolCall._meta?.claudeCode?.toolName;
|
|
410
|
+
if (execution && bridge?.enabled && bridge.isBridge(toolName)) {
|
|
411
|
+
const current = this.permissions ? permissionMode('claude', this.permissions.read(options.sessionId)) : undefined;
|
|
412
|
+
if (current === 'plan') {
|
|
413
|
+
details.record({ kind: 'tool', severity: 'info', code: 'TOOLBRIDGE_PLAN_PERMISSION_REJECTED', summary: '本轮为plan档,桥接原生权限请求未放行。' });
|
|
414
|
+
return 'rejected';
|
|
415
|
+
}
|
|
416
|
+
if (current === 'default' && confirmedNativeMode === 'default') {
|
|
417
|
+
details.record({ kind: 'tool', severity: 'info', code: 'TOOLBRIDGE_NATIVE_PERMISSION_ANSWERED', summary: '已确认桥接工具仅在DSH审批,原生请求自动答复一次。', payload: { tool: toolName } });
|
|
418
|
+
return 'allowed-once';
|
|
419
|
+
}
|
|
420
|
+
}
|
|
376
421
|
if (execution) {
|
|
377
422
|
if (toolName && ['Agent', 'Task', 'TaskOutput', 'TaskStop', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash'].includes(toolName)) {
|
|
378
423
|
details.record({ kind: 'need', severity: 'warning', code: 'CLAUDE_HOST_TOOL_UNSUPPORTED', summary: '这类原生调用没有经过员工端别名,本机来源不支持;本次审批已取消', payload: { operation: toolName } });
|
|
@@ -383,6 +428,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
383
428
|
const answer = await this.approve?.(permission, approvalSignal, options, execution && toolName?.startsWith('mcp__employee__') ? 'employee-local' : undefined, runtime.hostWorkspace) ?? 'unavailable';
|
|
384
429
|
return answer;
|
|
385
430
|
}, onEmployeeNotice: notice => push(mapper.text(notice + '\n\n')), onUpdate: notification => { try {
|
|
431
|
+
bridge?.observe(notification.update);
|
|
386
432
|
push(mapper.acp(notification.update));
|
|
387
433
|
}
|
|
388
434
|
catch (error) {
|
|
@@ -412,6 +458,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
412
458
|
const remoteMode = permissionMode(this.agent.loginProvider, policy);
|
|
413
459
|
details.record({ kind: 'session', severity: 'info', code: 'DSH_PERMISSION_POLICY', summary: '按DSH当前会话权限同步远程工具;不会把关闭审批提示解释为自动批准。', payload: { ...policy, remoteMode } });
|
|
414
460
|
await session.selectPermissionMode(remoteMode, modelSignal, this.agent.loginProvider);
|
|
461
|
+
confirmedNativeMode = remoteMode;
|
|
415
462
|
permissionKey = next;
|
|
416
463
|
});
|
|
417
464
|
};
|
|
@@ -437,7 +484,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
437
484
|
const task = (async () => {
|
|
438
485
|
remoteAttempted = true;
|
|
439
486
|
try {
|
|
440
|
-
await session.start(runtime.execution?.hostCwd ?? runtime.cwd, previous?.sessionId, modelSignal, { systemPrompt: input.systemPrompt, ...(runtime.execution ? { employeeRouting: runtime.execution.routing } : {}), ...(codex ? { codexNewSessionOnly: true } : {}), ...(runtime.employeeExecutionMeta ? { employeeExecution: runtime.employeeExecutionMeta } : {}) }, input.prompt);
|
|
487
|
+
await session.start(runtime.execution?.hostCwd ?? runtime.cwd, previous?.sessionId, modelSignal, { systemPrompt: input.systemPrompt, ...(bridgeMcp ? { toolBridgeServers: bridgeMcp.mcpServers } : {}), ...(runtime.execution ? { employeeRouting: runtime.execution.routing } : {}), ...(codex ? { codexNewSessionOnly: true } : {}), ...(runtime.employeeExecutionMeta ? { employeeExecution: runtime.employeeExecutionMeta } : {}) }, input.prompt);
|
|
441
488
|
}
|
|
442
489
|
catch (error) {
|
|
443
490
|
details.record({ kind: 'session', severity: 'action-required', code: 'SESSION_START_FAILED', summary: '远端会话建立或恢复失败,本轮用户输入未发送。', payload: { operation: previous ? 'resume-or-load' : 'new', reason: closeCode(error, 'ACP_FAILED'), promptDispatched: false } });
|
|
@@ -488,9 +535,14 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
488
535
|
recordInformation();
|
|
489
536
|
details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
|
|
490
537
|
await runtime.execution?.beginTurn(modelSignal);
|
|
538
|
+
if (bridge?.enabled)
|
|
539
|
+
bridge.replayState = { response: makePointer(session.id) };
|
|
540
|
+
bridge?.begin(modelSignal);
|
|
491
541
|
id = session.id;
|
|
492
542
|
try {
|
|
493
543
|
const result = await dispatchPrompt();
|
|
544
|
+
if (bridge?.unsettled)
|
|
545
|
+
bridge.abandon('HOST_PROMPT_ENDED_WITH_PENDING_CALL');
|
|
494
546
|
permissionOff?.();
|
|
495
547
|
permissionOff = undefined;
|
|
496
548
|
await permissionTail;
|
|
@@ -499,6 +551,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
499
551
|
finally {
|
|
500
552
|
permissionOff?.();
|
|
501
553
|
permissionOff = undefined;
|
|
554
|
+
bridge?.end();
|
|
502
555
|
runtime.execution?.endTurn();
|
|
503
556
|
details.endTurn();
|
|
504
557
|
}
|
|
@@ -533,13 +586,15 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
533
586
|
}
|
|
534
587
|
}
|
|
535
588
|
yield* mapper.flush();
|
|
536
|
-
const pointer =
|
|
589
|
+
const pointer = makePointer(id);
|
|
537
590
|
succeeded = true;
|
|
538
591
|
yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
|
|
539
592
|
}
|
|
540
593
|
catch (error) {
|
|
541
594
|
if (session && session.promptDispatchCount > promptBaseline)
|
|
542
595
|
onPromptDispatched();
|
|
596
|
+
if (bridge?.unsettled && !signal.aborted)
|
|
597
|
+
bridge.abandon('HOST_PROMPT_FAILED');
|
|
543
598
|
turnFailure = error;
|
|
544
599
|
runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.turn-failure', reason: closeCode(error, signal.aborted ? 'ABORTED' : 'TURN_FAILED'), action: 'failure' });
|
|
545
600
|
recordInformation();
|
|
@@ -559,6 +614,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
559
614
|
for (const off of abortOffs)
|
|
560
615
|
off();
|
|
561
616
|
runtime?.execution?.endTurn();
|
|
617
|
+
if (bridgeMcp)
|
|
618
|
+
await bridgeMcp.close();
|
|
562
619
|
if (runtime) {
|
|
563
620
|
session?.close({ layer: 'adapter', initiator: 'adapter.turn-finally', reason: succeeded ? 'TURN_FINISHED' : 'TURN_FAILED', action: 'close' });
|
|
564
621
|
if (key && this.sessions.get(key) === live)
|
|
@@ -575,16 +632,25 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
575
632
|
/** One official registration owns both routes, preserving opaque replay ownership. */
|
|
576
633
|
export class RemoteAgentsAdapter extends LlmAdapter {
|
|
577
634
|
adapters = new Map();
|
|
578
|
-
constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions) { super(); for (const agent of AGENTS)
|
|
579
|
-
this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions)); }
|
|
635
|
+
constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions, bridgeStopped) { super(); for (const agent of AGENTS)
|
|
636
|
+
this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions, bridgeStopped)); }
|
|
580
637
|
adapter(provider) { const adapter = this.adapters.get(provider); if (!adapter)
|
|
581
638
|
throw new LlmError('远程模型未注册', 'PROVIDER_NOT_FOUND'); return adapter; }
|
|
582
639
|
providerInfo(provider) { return this.adapter(provider).providerInfo(provider); }
|
|
583
640
|
listModels(provider) { return this.adapter(provider).listModels(provider); }
|
|
584
641
|
resolveModel(provider, model, signal) { return this.adapter(provider).resolveModel(provider, model, signal); }
|
|
585
|
-
prepareCall(provider, model, signal) {
|
|
642
|
+
async prepareCall(provider, model, signal) { const prepared = await this.adapter(provider).prepareCall(provider, model, signal); return { ...prepared, stream: (options) => this.stream(options) }; }
|
|
586
643
|
providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
|
|
587
|
-
stream(options) {
|
|
644
|
+
stream(options) {
|
|
645
|
+
if (options.sessionId)
|
|
646
|
+
for (const [provider, adapter] of this.adapters)
|
|
647
|
+
if (provider !== options.provider && adapter.hasBridgeSession(options.sessionId)) {
|
|
648
|
+
void adapter.disposeSession(options.sessionId);
|
|
649
|
+
throw new LlmError('原主机还在等待DSH工具结果,切换助手已停止原轮次;请核对已执行的工具后重新发送,结果不会重放。', 'TOOLBRIDGE_ROUTE_CHANGED');
|
|
650
|
+
}
|
|
651
|
+
return this.adapter(options.provider).stream(options);
|
|
652
|
+
}
|
|
653
|
+
async completeDshTurn(sessionId, messages) { await Promise.all([...this.adapters.values()].map(adapter => adapter.completeDshTurn(sessionId, messages))); }
|
|
588
654
|
async disposeSession(sessionId) { await Promise.all([...this.adapters.values()].map(adapter => adapter.disposeSession(sessionId))); }
|
|
589
655
|
async detachSession(sessionId) { await Promise.all([...this.adapters.values()].map(adapter => adapter.detachSession(sessionId))); }
|
|
590
656
|
async dispose() { await Promise.all([...this.adapters.values()].map(adapter => adapter.dispose())); }
|
|
@@ -688,7 +754,33 @@ export function apply(ctx, live) {
|
|
|
688
754
|
step = null;
|
|
689
755
|
}
|
|
690
756
|
return managerForProfile().recordLoginFailure({ sessionId, code, diagnostic, context: { turn, step } });
|
|
691
|
-
}, modelDirectory, preflightDiagnostics, dshPermissionSource(ctx))
|
|
757
|
+
}, modelDirectory, preflightDiagnostics, dshPermissionSource(ctx), (sessionId, notice) => {
|
|
758
|
+
const agent = ctx.get('agents')?.get(SessionId(sessionId));
|
|
759
|
+
if (!agent)
|
|
760
|
+
return;
|
|
761
|
+
let turn = null, step = null;
|
|
762
|
+
for (const e of agent.session.snapshotEvents()) {
|
|
763
|
+
if (e.type === 'turn/start') {
|
|
764
|
+
turn = e.data.turn;
|
|
765
|
+
step = null;
|
|
766
|
+
}
|
|
767
|
+
else if (e.type === 'turn/end') {
|
|
768
|
+
turn = null;
|
|
769
|
+
step = null;
|
|
770
|
+
}
|
|
771
|
+
else if (e.type === 'step/start')
|
|
772
|
+
step = e.data.step;
|
|
773
|
+
else if (e.type === 'step/end')
|
|
774
|
+
step = null;
|
|
775
|
+
}
|
|
776
|
+
if (turn !== null && step !== null)
|
|
777
|
+
agent.session.append('assistant/message', { turn, step, message: createAssistantMessage({ content: [{ type: 'text', text: notice }], source: { provider: 'ai-workbench-toolbridge', model: 'notice' } }), stream: [] }, { surfaceOp: 'append' });
|
|
778
|
+
agent.cancel({ kind: 'hook', reason: 'DSH_BRIDGE_CALL_ABANDONED_BY_HOST' }, { keepInbox: true });
|
|
779
|
+
});
|
|
780
|
+
ctx.on('session/event', (session, event) => { if (event.type === 'turn/end') {
|
|
781
|
+
const operation = event.data.reason.kind === 'completed' ? adapter.completeDshTurn(session.id, session.deriveMessages()) : adapter.disposeSession(session.id);
|
|
782
|
+
void operation.catch(() => ctx.logger('remote-agent').warn('TOOLBRIDGE_RESULT_DELIVERY_UNCONFIRMED'));
|
|
783
|
+
} });
|
|
692
784
|
const ids = AGENTS.map(agent => agent.id);
|
|
693
785
|
const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: actualSettingsNamespace ?? settingsNamespace, settingsPath: [] }));
|
|
694
786
|
const directory = ctx.llm.registerConfigurableProviders(entries());
|
package/dist/provider.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToolBridge } from './toolbridge.ts';
|
|
1
2
|
import { type SessionSource } from './host-workspace.ts';
|
|
2
3
|
import type { CloseCause, CloseDiagnostics } from './close-diagnostics.ts';
|
|
3
4
|
import type { AgentApp } from '@agentclientprotocol/sdk';
|
|
@@ -66,6 +67,7 @@ export interface Runtime {
|
|
|
66
67
|
closeDiagnostics?: CloseDiagnostics;
|
|
67
68
|
}
|
|
68
69
|
export interface RuntimeRequest extends Invocation {
|
|
70
|
+
toolBridge?: ToolBridge;
|
|
69
71
|
workspaceSource?: SessionSource;
|
|
70
72
|
hostSource?: Extract<SessionSource, {
|
|
71
73
|
source: 'host';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { ToolBridge } from './toolbridge.ts';
|
|
3
|
+
import type { ImageReader } from './input.ts';
|
|
4
|
+
type Producer = (options: GenerateOptions, bridge: ToolBridge) => AsyncGenerator<StreamChunk>;
|
|
5
|
+
/** Owns the original prompt across official generate/tool/generate steps. No tool execution. */
|
|
6
|
+
export declare class ToolBridgeLoop {
|
|
7
|
+
private readImage?;
|
|
8
|
+
private stopped?;
|
|
9
|
+
private runs;
|
|
10
|
+
private concluding;
|
|
11
|
+
constructor(readImage?: ImageReader | undefined, stopped?: ((sessionId: string, notice: string) => void) | undefined);
|
|
12
|
+
private launch;
|
|
13
|
+
conclude(id: string, messages: GenerateOptions['messages']): Promise<void>;
|
|
14
|
+
active(id: string): boolean;
|
|
15
|
+
cancel(id: string): void;
|
|
16
|
+
dispose(): Promise<void>;
|
|
17
|
+
stream(options: GenerateOptions, producer: Producer): AsyncGenerator<StreamChunk>;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { createAssistantMessage } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { ToolBridge } from "./toolbridge.js";
|
|
3
|
+
import { NeedError } from "./remote.js";
|
|
4
|
+
/** Rebase native text blocks at each DSH step; a parked stream must end all open blocks. */
|
|
5
|
+
class Projection {
|
|
6
|
+
blocks = new Map();
|
|
7
|
+
next = 0;
|
|
8
|
+
chunks(chunk) {
|
|
9
|
+
if (chunk.type === 'block-start') {
|
|
10
|
+
if (chunk.blockType !== 'text' && chunk.blockType !== 'reasoning')
|
|
11
|
+
throw Error('TOOLBRIDGE_NATIVE_BLOCK_UNSUPPORTED');
|
|
12
|
+
this.blocks.set(chunk.index, { type: chunk.blockType, text: '' });
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
if (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta') {
|
|
16
|
+
const block = this.blocks.get(chunk.index);
|
|
17
|
+
if (!block)
|
|
18
|
+
throw Error('TOOLBRIDGE_NATIVE_BLOCK_MISSING');
|
|
19
|
+
const result = [];
|
|
20
|
+
if (block.index === undefined) {
|
|
21
|
+
block.index = this.next++;
|
|
22
|
+
result.push({ type: 'block-start', index: block.index, blockType: block.type });
|
|
23
|
+
}
|
|
24
|
+
const text = chunk.type === 'text-delta' ? chunk.text : chunk.text;
|
|
25
|
+
block.text += text;
|
|
26
|
+
result.push({ ...chunk, index: block.index });
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
if (chunk.type === 'block-end') {
|
|
30
|
+
const block = this.blocks.get(chunk.index);
|
|
31
|
+
this.blocks.delete(chunk.index);
|
|
32
|
+
return block?.index === undefined ? [] : [{ type: 'block-end', index: block.index, block: { type: block.type, text: block.text } }];
|
|
33
|
+
}
|
|
34
|
+
return [chunk];
|
|
35
|
+
}
|
|
36
|
+
park() { const chunks = []; for (const block of this.blocks.values())
|
|
37
|
+
if (block.index !== undefined) {
|
|
38
|
+
chunks.push({ type: 'block-end', index: block.index, block: { type: block.type, text: block.text } });
|
|
39
|
+
block.index = undefined;
|
|
40
|
+
block.text = '';
|
|
41
|
+
} return chunks; }
|
|
42
|
+
}
|
|
43
|
+
/** Owns the original prompt across official generate/tool/generate steps. No tool execution. */
|
|
44
|
+
export class ToolBridgeLoop {
|
|
45
|
+
readImage;
|
|
46
|
+
stopped;
|
|
47
|
+
runs = new Map();
|
|
48
|
+
concluding = new Map();
|
|
49
|
+
constructor(readImage, stopped) {
|
|
50
|
+
this.readImage = readImage;
|
|
51
|
+
this.stopped = stopped;
|
|
52
|
+
}
|
|
53
|
+
launch(options, producer) {
|
|
54
|
+
const controller = new AbortController(), bridge = new ToolBridge(options, this.readImage);
|
|
55
|
+
const run = { bridge, controller, options, latest: options, queue: [], wake: Promise.withResolvers(), done: false, task: undefined, seen: new Set(options.messages.flatMap(m => 'id' in m && typeof m.id === 'string' ? [m.id] : [])), deferred: [], text: '', reading: false, projection: new Projection(), off: () => { } };
|
|
56
|
+
const signal = AbortSignal.any([controller.signal, ...options.signal ? [options.signal] : []]);
|
|
57
|
+
const changed = () => { run.wake.resolve(); };
|
|
58
|
+
bridge.changed = changed;
|
|
59
|
+
bridge.onAbandon = notice => { controller.abort(); if (options.sessionId)
|
|
60
|
+
this.stopped?.(options.sessionId, notice); };
|
|
61
|
+
const abort = () => { bridge.close(); changed(); };
|
|
62
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
63
|
+
run.off = () => signal.removeEventListener('abort', abort);
|
|
64
|
+
run.task = (async () => {
|
|
65
|
+
try {
|
|
66
|
+
for await (const chunk of producer({ ...options, signal }, bridge)) {
|
|
67
|
+
if (chunk.type === 'finish') {
|
|
68
|
+
run.finish = chunk;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (chunk.type === 'text-delta')
|
|
72
|
+
run.text += chunk.text;
|
|
73
|
+
run.queue.push(chunk);
|
|
74
|
+
changed();
|
|
75
|
+
}
|
|
76
|
+
if (bridge.unsettled)
|
|
77
|
+
bridge.abandon('HOST_PROMPT_ENDED_WITH_PENDING_CALL');
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
run.error = error;
|
|
81
|
+
if (bridge.unsettled)
|
|
82
|
+
bridge.abandon('HOST_PROMPT_FAILED');
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
run.done = true;
|
|
86
|
+
changed();
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
return run;
|
|
90
|
+
}
|
|
91
|
+
async conclude(id, messages) {
|
|
92
|
+
const existing = this.concluding.get(id);
|
|
93
|
+
if (existing)
|
|
94
|
+
return existing.flight;
|
|
95
|
+
const run = this.runs.get(id);
|
|
96
|
+
if (!run || !run.bridge.enabled || !run.bridge.unsettled) {
|
|
97
|
+
this.cancel(id);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.runs.delete(id);
|
|
101
|
+
run.bridge.beginConclude();
|
|
102
|
+
run.bridge.onConcludeAcknowledged = () => run.controller.abort();
|
|
103
|
+
const flight = (async () => {
|
|
104
|
+
try {
|
|
105
|
+
await run.bridge.results({ ...run.latest, messages, signal: run.controller.signal });
|
|
106
|
+
await run.task;
|
|
107
|
+
if (run.error && !run.bridge.concludeAcknowledged)
|
|
108
|
+
throw run.error;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
run.bridge.close();
|
|
112
|
+
run.controller.abort();
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
run.off();
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
this.concluding.set(id, { run, flight });
|
|
120
|
+
void flight.then(() => { if (this.concluding.get(id)?.flight === flight)
|
|
121
|
+
this.concluding.delete(id); }, () => { });
|
|
122
|
+
await flight;
|
|
123
|
+
}
|
|
124
|
+
active(id) { return (this.runs.get(id)?.bridge.enabled ?? false) || this.concluding.has(id); }
|
|
125
|
+
cancel(id) {
|
|
126
|
+
const closing = this.concluding.get(id);
|
|
127
|
+
if (closing) {
|
|
128
|
+
this.concluding.delete(id);
|
|
129
|
+
closing.run.bridge.close();
|
|
130
|
+
closing.run.controller.abort();
|
|
131
|
+
closing.run.off();
|
|
132
|
+
}
|
|
133
|
+
const run = this.runs.get(id);
|
|
134
|
+
if (!run)
|
|
135
|
+
return;
|
|
136
|
+
this.runs.delete(id);
|
|
137
|
+
run.bridge.close();
|
|
138
|
+
run.controller.abort();
|
|
139
|
+
run.off();
|
|
140
|
+
}
|
|
141
|
+
async dispose() { for (const id of new Set([...this.runs.keys(), ...this.concluding.keys()]))
|
|
142
|
+
this.cancel(id); }
|
|
143
|
+
async *stream(options, producer) {
|
|
144
|
+
const key = options.sessionId ?? '';
|
|
145
|
+
const closing = this.concluding.get(key);
|
|
146
|
+
if (closing) {
|
|
147
|
+
let aborted;
|
|
148
|
+
try {
|
|
149
|
+
await Promise.race([closing.flight, ...(options.signal ? [new Promise((_r, reject) => { aborted = () => reject(new NeedError('ABORTED', '本轮等待已取消,尚未发送新输入。', '原主机prompt收尾')); options.signal.addEventListener('abort', aborted, { once: true }); if (options.signal.aborted)
|
|
150
|
+
aborted(); })] : [])]);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (this.concluding.get(key) === closing)
|
|
154
|
+
this.cancel(key);
|
|
155
|
+
else {
|
|
156
|
+
closing.run.bridge.close();
|
|
157
|
+
closing.run.controller.abort();
|
|
158
|
+
closing.run.off();
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
if (aborted)
|
|
164
|
+
options.signal?.removeEventListener('abort', aborted);
|
|
165
|
+
if (this.concluding.get(key) === closing)
|
|
166
|
+
this.concluding.delete(key);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let run = this.runs.get(key), boundary = false;
|
|
170
|
+
if (run?.reading)
|
|
171
|
+
throw new NeedError('TURN_BUSY', '工具桥仍在处理当前生成步骤。', '等待当前步骤结束');
|
|
172
|
+
if (!run) {
|
|
173
|
+
run = this.launch(options, producer);
|
|
174
|
+
if (key)
|
|
175
|
+
this.runs.set(key, run);
|
|
176
|
+
}
|
|
177
|
+
run.reading = true;
|
|
178
|
+
run.projection.next = 0;
|
|
179
|
+
try {
|
|
180
|
+
if (options.signal?.aborted)
|
|
181
|
+
throw new NeedError('ABORTED', '本轮已取消,工具结果没有重放。', '确认主机取消状态');
|
|
182
|
+
if (run.latest !== options) {
|
|
183
|
+
for (const m of options.messages)
|
|
184
|
+
if (m.role === 'user' && 'source' in m && m.source?.kind === 'user' && 'id' in m && typeof m.id === 'string' && !run.seen.has(m.id)) {
|
|
185
|
+
run.seen.add(m.id);
|
|
186
|
+
run.deferred.push(m);
|
|
187
|
+
}
|
|
188
|
+
run.latest = options;
|
|
189
|
+
await run.bridge.results(options);
|
|
190
|
+
}
|
|
191
|
+
for (;;) {
|
|
192
|
+
const wake = run.wake;
|
|
193
|
+
run.wake = Promise.withResolvers();
|
|
194
|
+
while (run.queue.length)
|
|
195
|
+
yield* run.projection.chunks(run.queue.shift());
|
|
196
|
+
if (run.error)
|
|
197
|
+
throw run.error;
|
|
198
|
+
if (run.bridge.pending.length && !run.done) {
|
|
199
|
+
yield* run.projection.park();
|
|
200
|
+
for (const p of run.bridge.pending) {
|
|
201
|
+
p.exported = true;
|
|
202
|
+
const index = run.projection.next++;
|
|
203
|
+
yield { type: 'block-start', index, blockType: 'tool-call' };
|
|
204
|
+
yield { type: 'tool-call-delta', index, id: p.block.id, name: p.block.name, argumentsDelta: p.block.arguments };
|
|
205
|
+
yield { type: 'block-end', index, block: p.block };
|
|
206
|
+
}
|
|
207
|
+
boundary = true;
|
|
208
|
+
yield { type: 'finish', reason: { kind: 'tool-calls' }, ...(run.bridge.replayState ? { replayState: run.bridge.replayState } : {}) };
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (run.done) {
|
|
212
|
+
yield* run.projection.park();
|
|
213
|
+
run.off();
|
|
214
|
+
if (run.deferred.length) {
|
|
215
|
+
const deferred = run.deferred, ids = new Set(deferred.flatMap(m => 'id' in m ? [m.id] : []));
|
|
216
|
+
const last = createAssistantMessage({ content: run.text ? [{ type: 'text', text: run.text }] : [], source: { provider: run.options.provider, model: run.options.model, ...(run.finish?.replayState ? { replayState: { response: run.finish.replayState.response } } : {}) } });
|
|
217
|
+
const next = { ...options, messages: [...run.latest.messages.filter(m => !('id' in m && ids.has(m.id))), last, ...deferred] };
|
|
218
|
+
const nextIndex = run.projection.next;
|
|
219
|
+
run.bridge.close();
|
|
220
|
+
run = this.launch(next, producer);
|
|
221
|
+
run.projection.next = nextIndex;
|
|
222
|
+
run.reading = true;
|
|
223
|
+
if (key)
|
|
224
|
+
this.runs.set(key, run);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (key)
|
|
228
|
+
this.runs.delete(key);
|
|
229
|
+
run.bridge.close();
|
|
230
|
+
boundary = true;
|
|
231
|
+
yield run.finish ?? { type: 'finish', reason: { kind: 'stop' } };
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (options.signal?.aborted)
|
|
235
|
+
throw new NeedError('ABORTED', '本轮已取消,工具结果没有重放。', '确认主机取消状态');
|
|
236
|
+
await Promise.race([wake.promise, run.wake.promise]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
run.reading = false;
|
|
241
|
+
if (!boundary || options.signal?.aborted) {
|
|
242
|
+
run.bridge.close();
|
|
243
|
+
run.controller.abort();
|
|
244
|
+
run.off();
|
|
245
|
+
if (key && this.runs.get(key) === run)
|
|
246
|
+
this.runs.delete(key);
|
|
247
|
+
await run.task;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ToolSchema } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
/** Fixed official tool names; no description heuristics or plugin-name branches. */
|
|
4
|
+
export declare const TOOLBRIDGE_EXCLUDED: readonly ["read", "write", "edit", "read_image", "glob", "grep", "bash", "pwsh", "str_replace_editor", "job_output", "job_list", "job_kill", "load_workspace_dependencies", "terminal_open", "terminal_send", "terminal_read", "terminal_signal", "terminal_close", "terminal_list", "subagent", "list_subagent_models", "send_message", "interrupt_agent", "spawn_teammate", "list_agents", "wait_agent", "team_task_create", "team_task_list", "team_task_get", "team_task_update", "run_code"];
|
|
5
|
+
export interface BridgeTool {
|
|
6
|
+
alias: string;
|
|
7
|
+
schema: ToolSchema;
|
|
8
|
+
wire: Tool;
|
|
9
|
+
}
|
|
10
|
+
export declare function bridgeTable(tools?: readonly ToolSchema[]): ReadonlyMap<string, BridgeTool>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { NeedError } from "./remote.js";
|
|
3
|
+
/** Fixed official tool names; no description heuristics or plugin-name branches. */
|
|
4
|
+
export const TOOLBRIDGE_EXCLUDED = Object.freeze([
|
|
5
|
+
'read', 'write', 'edit', 'read_image', 'glob', 'grep', 'bash', 'pwsh', 'str_replace_editor',
|
|
6
|
+
'job_output', 'job_list', 'job_kill', 'load_workspace_dependencies',
|
|
7
|
+
'terminal_open', 'terminal_send', 'terminal_read', 'terminal_signal', 'terminal_close', 'terminal_list',
|
|
8
|
+
'subagent', 'list_subagent_models', 'send_message', 'interrupt_agent', 'spawn_teammate', 'list_agents', 'wait_agent',
|
|
9
|
+
'team_task_create', 'team_task_list', 'team_task_get', 'team_task_update', 'run_code',
|
|
10
|
+
]);
|
|
11
|
+
const excluded = new Set(TOOLBRIDGE_EXCLUDED);
|
|
12
|
+
export function bridgeTable(tools = []) {
|
|
13
|
+
const table = new Map(), names = new Set();
|
|
14
|
+
for (const tool of tools) {
|
|
15
|
+
if (excluded.has(tool.name))
|
|
16
|
+
continue;
|
|
17
|
+
if (names.has(tool.name))
|
|
18
|
+
throw new NeedError('TOOLBRIDGE_TOOL_DUPLICATE', '当前工具表存在重名,工具桥未开始。', '官方唯一工具名称');
|
|
19
|
+
names.add(tool.name);
|
|
20
|
+
const alias = 'dsh_' + createHash('sha256').update(tool.name).digest('hex').slice(0, 32);
|
|
21
|
+
if (table.has(alias))
|
|
22
|
+
throw new NeedError('TOOLBRIDGE_NAME_COLLISION', '工具桥名称冲突,未执行。', '唯一工具桥名称');
|
|
23
|
+
const schema = structuredClone(tool);
|
|
24
|
+
table.set(alias, { alias, schema, wire: { name: alias, description: 'DSH工具 ' + schema.name + ':' + schema.description, inputSchema: structuredClone(schema.parameters) } });
|
|
25
|
+
}
|
|
26
|
+
return table;
|
|
27
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import type { GenerateOptions, ToolCallBlock } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import type { SessionUpdate } from '@agentclientprotocol/sdk/unstable';
|
|
4
|
+
import { type BridgeTool } from './toolbridge-table.ts';
|
|
5
|
+
import type { ImageReader } from './input.ts';
|
|
6
|
+
import type { DetailObservation } from './conversation-details.ts';
|
|
7
|
+
export declare const TOOLBRIDGE_TIMEOUT_SECONDS = 1800;
|
|
8
|
+
export declare const TOOLBRIDGE_NOTICE = "\u542B\u5DE5\u5177\u6865\uFF1A\u672C\u673A\u6587\u4EF6\u5939\u7684\u6269\u5C55\u5DE5\u5177\u7531DSH\u6267\u884C\u5E76\u6309\u5F53\u524D\u6743\u9650\u5BA1\u6279\uFF1B\u6587\u4EF6\u548C\u547D\u4EE4\u4FDD\u6301\u539F\u6267\u884C\u65B9\u5F0F\u3002\u5DE5\u5177\u8868\u5728\u65B0\u4E00\u8F6E\u4E3B\u673A\u8FDE\u63A5\u65F6\u66F4\u65B0\u3002";
|
|
9
|
+
export declare const TOOLBRIDGE_ENDED = "\u4E3B\u673A\u5DF2\u7ED3\u675F\u8FD9\u6B21\u5DE5\u5177\u7B49\u5F85\uFF1B\u672C\u8F6E\u5DF2\u505C\u6B62\uFF0C\u8FDF\u5230\u7ED3\u679C\u4E0D\u4F1A\u4EA4\u56DE\uFF0C\u4E5F\u4E0D\u4F1A\u91CD\u65B0\u6267\u884C\u3002\u8BF7\u6838\u5BF9\u5DF2\u53D1\u751F\u7684\u64CD\u4F5C\u3002";
|
|
10
|
+
interface Pending {
|
|
11
|
+
block: ToolCallBlock;
|
|
12
|
+
alias: string;
|
|
13
|
+
reply: ReturnType<typeof Promise.withResolvers<CallToolResult>>;
|
|
14
|
+
exported: boolean;
|
|
15
|
+
settled: boolean;
|
|
16
|
+
off(): void;
|
|
17
|
+
}
|
|
18
|
+
/** Only a protocol queue. Tool execution and authorization belong to DSH. */
|
|
19
|
+
export declare class ToolBridge {
|
|
20
|
+
private options;
|
|
21
|
+
private readImage?;
|
|
22
|
+
readonly controller: AbortController;
|
|
23
|
+
private snapshot?;
|
|
24
|
+
get table(): ReadonlyMap<string, BridgeTool>;
|
|
25
|
+
readonly calls: Map<string, Pending>;
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
ended: boolean;
|
|
28
|
+
notice?: string;
|
|
29
|
+
replayState?: {
|
|
30
|
+
response: unknown;
|
|
31
|
+
};
|
|
32
|
+
localTurnEnded: boolean;
|
|
33
|
+
concludeAcknowledged: boolean;
|
|
34
|
+
onConcludeAcknowledged: () => void;
|
|
35
|
+
private nativeCompleted;
|
|
36
|
+
private nativeTerminated;
|
|
37
|
+
private concludeNative;
|
|
38
|
+
beginConclude(): void;
|
|
39
|
+
private active?;
|
|
40
|
+
begin(signal: AbortSignal): void;
|
|
41
|
+
end(): void;
|
|
42
|
+
changed: () => void;
|
|
43
|
+
record: (event: DetailObservation) => void;
|
|
44
|
+
onAbandon: (notice: string) => void;
|
|
45
|
+
private nativeBridgeIds;
|
|
46
|
+
constructor(options: GenerateOptions, readImage?: ImageReader | undefined);
|
|
47
|
+
enable(record: (event: DetailObservation) => void): void;
|
|
48
|
+
get tools(): {
|
|
49
|
+
inputSchema: {
|
|
50
|
+
[x: string]: unknown;
|
|
51
|
+
type: "object";
|
|
52
|
+
properties?: {
|
|
53
|
+
[x: string]: object;
|
|
54
|
+
} | undefined;
|
|
55
|
+
required?: string[] | undefined;
|
|
56
|
+
};
|
|
57
|
+
name: string;
|
|
58
|
+
description?: string | undefined;
|
|
59
|
+
outputSchema?: {
|
|
60
|
+
[x: string]: unknown;
|
|
61
|
+
type: "object";
|
|
62
|
+
properties?: {
|
|
63
|
+
[x: string]: object;
|
|
64
|
+
} | undefined;
|
|
65
|
+
required?: string[] | undefined;
|
|
66
|
+
} | undefined;
|
|
67
|
+
annotations?: {
|
|
68
|
+
title?: string | undefined;
|
|
69
|
+
readOnlyHint?: boolean | undefined;
|
|
70
|
+
destructiveHint?: boolean | undefined;
|
|
71
|
+
idempotentHint?: boolean | undefined;
|
|
72
|
+
openWorldHint?: boolean | undefined;
|
|
73
|
+
} | undefined;
|
|
74
|
+
execution?: {
|
|
75
|
+
taskSupport?: "optional" | "required" | "forbidden" | undefined;
|
|
76
|
+
} | undefined;
|
|
77
|
+
_meta?: {
|
|
78
|
+
[x: string]: unknown;
|
|
79
|
+
} | undefined;
|
|
80
|
+
icons?: {
|
|
81
|
+
src: string;
|
|
82
|
+
mimeType?: string | undefined;
|
|
83
|
+
sizes?: string[] | undefined;
|
|
84
|
+
theme?: "light" | "dark" | undefined;
|
|
85
|
+
}[] | undefined;
|
|
86
|
+
title?: string | undefined;
|
|
87
|
+
}[];
|
|
88
|
+
isBridge(name: unknown): boolean;
|
|
89
|
+
call(alias: string, args: Record<string, unknown>, signal: AbortSignal): Promise<CallToolResult>;
|
|
90
|
+
get pending(): Pending[];
|
|
91
|
+
get unsettled(): boolean;
|
|
92
|
+
get waiting(): boolean;
|
|
93
|
+
results(options: GenerateOptions): Promise<void>;
|
|
94
|
+
private mapResult;
|
|
95
|
+
private concludeAck;
|
|
96
|
+
observe(update: SessionUpdate): void;
|
|
97
|
+
/** A failed native bridge cancels the entire parked round; never guess between parallel identical calls. */
|
|
98
|
+
abandon(reason: string): void;
|
|
99
|
+
close(): void;
|
|
100
|
+
}
|
|
101
|
+
/** Codex executes nc in its existing employee environment, never on the host. */
|
|
102
|
+
export declare function openToolBridgeMcp(bridge: ToolBridge, maxBytes: number): Promise<{
|
|
103
|
+
mcpServers: {
|
|
104
|
+
name: string;
|
|
105
|
+
command: string;
|
|
106
|
+
args: string[];
|
|
107
|
+
env: {
|
|
108
|
+
name: string;
|
|
109
|
+
value: string;
|
|
110
|
+
}[];
|
|
111
|
+
}[];
|
|
112
|
+
close: () => Promise<void>;
|
|
113
|
+
}>;
|
|
114
|
+
export {};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:net';
|
|
3
|
+
import { mkdtemp, lstat, rm } from 'node:fs/promises';
|
|
4
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { EmployeeSocketTransport } from "./employee-mcp.js";
|
|
7
|
+
import { bridgeTable } from "./toolbridge-table.js";
|
|
8
|
+
import { NeedError } from "./remote.js";
|
|
9
|
+
export const TOOLBRIDGE_TIMEOUT_SECONDS = 1800;
|
|
10
|
+
export const TOOLBRIDGE_NOTICE = '含工具桥:本机文件夹的扩展工具由DSH执行并按当前权限审批;文件和命令保持原执行方式。工具表在新一轮主机连接时更新。';
|
|
11
|
+
export const TOOLBRIDGE_ENDED = '主机已结束这次工具等待;本轮已停止,迟到结果不会交回,也不会重新执行。请核对已发生的操作。';
|
|
12
|
+
const errorResult = (text) => ({ isError: true, content: [{ type: 'text', text }] });
|
|
13
|
+
/** Only a protocol queue. Tool execution and authorization belong to DSH. */
|
|
14
|
+
export class ToolBridge {
|
|
15
|
+
options;
|
|
16
|
+
readImage;
|
|
17
|
+
controller = new AbortController();
|
|
18
|
+
snapshot;
|
|
19
|
+
get table() { return this.snapshot ??= bridgeTable(this.options.tools); }
|
|
20
|
+
calls = new Map();
|
|
21
|
+
enabled = false;
|
|
22
|
+
ended = false;
|
|
23
|
+
notice;
|
|
24
|
+
replayState;
|
|
25
|
+
localTurnEnded = false;
|
|
26
|
+
concludeAcknowledged = false;
|
|
27
|
+
onConcludeAcknowledged = () => { };
|
|
28
|
+
nativeCompleted = new Set();
|
|
29
|
+
nativeTerminated = new Set();
|
|
30
|
+
concludeNative = new Set();
|
|
31
|
+
beginConclude() { this.localTurnEnded = true; this.concludeNative = new Set([...this.nativeBridgeIds].filter(id => !this.nativeTerminated.has(id))); }
|
|
32
|
+
active;
|
|
33
|
+
begin(signal) { this.active = signal; }
|
|
34
|
+
end() { this.active = undefined; }
|
|
35
|
+
changed = () => { };
|
|
36
|
+
record = () => { };
|
|
37
|
+
onAbandon = () => { };
|
|
38
|
+
nativeBridgeIds = new Set();
|
|
39
|
+
constructor(options, readImage) {
|
|
40
|
+
this.options = options;
|
|
41
|
+
this.readImage = readImage;
|
|
42
|
+
}
|
|
43
|
+
enable(record) { if (!this.table.size)
|
|
44
|
+
return; this.enabled = true; this.record = record; record({ kind: 'session', severity: 'info', code: 'TOOLBRIDGE_SELECTED', summary: TOOLBRIDGE_NOTICE, payload: { toolCount: this.table.size, toolTimeoutSec: TOOLBRIDGE_TIMEOUT_SECONDS, timeoutSource: 'owner-2026-10-26', snapshot: 'native-session-open', excluded: 'official-file-command-subagent-team-run_code' } }); }
|
|
45
|
+
get tools() { return [...this.table.values()].map(t => t.wire); }
|
|
46
|
+
isBridge(name) { return typeof name === 'string' && this.table.has(name.replace(/^mcp__employee__/, '')); }
|
|
47
|
+
async call(alias, args, signal) {
|
|
48
|
+
const tool = this.table.get(alias);
|
|
49
|
+
if (!this.enabled || this.ended || this.localTurnEnded || !this.active || this.active.aborted || !tool || signal.aborted)
|
|
50
|
+
return errorResult('工具桥当前不可用,未执行。');
|
|
51
|
+
const id = ('awb-' + randomUUID());
|
|
52
|
+
const abort = () => this.abandon('MCP_REQUEST_CANCELLED');
|
|
53
|
+
const pending = { block: { type: 'tool-call', id, name: tool.schema.name, arguments: JSON.stringify(args) }, alias, reply: Promise.withResolvers(), exported: false, settled: false, off: () => signal.removeEventListener('abort', abort) };
|
|
54
|
+
this.calls.set(id, pending);
|
|
55
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
56
|
+
this.record({ kind: 'tool', severity: 'info', code: 'TOOLBRIDGE_CALL_PARKED', summary: '主机工具调用交给DSH执行与审批,原prompt继续等待。', payload: { callId: id, tool: tool.schema.name } });
|
|
57
|
+
this.changed();
|
|
58
|
+
if (signal.aborted)
|
|
59
|
+
abort();
|
|
60
|
+
return pending.reply.promise;
|
|
61
|
+
}
|
|
62
|
+
get pending() { return [...this.calls.values()].filter(p => !p.settled && !p.exported); }
|
|
63
|
+
get unsettled() { return [...this.calls.values()].some(p => !p.settled); }
|
|
64
|
+
get waiting() { return [...this.calls.values()].some(p => p.exported && !p.settled); }
|
|
65
|
+
async results(options) {
|
|
66
|
+
const batch = [];
|
|
67
|
+
for (const p of this.calls.values()) {
|
|
68
|
+
if (!p.exported || p.settled)
|
|
69
|
+
continue;
|
|
70
|
+
const matches = options.messages.filter((m) => m.role === 'tool' && m.toolCallId === p.block.id);
|
|
71
|
+
if (matches.length !== 1)
|
|
72
|
+
throw new NeedError('TOOLBRIDGE_RESULT_MISSING', '工具桥没有收到唯一匹配的DSH结果,本轮已停止;不会重跑。', '原toolCallId对应的官方tool结果');
|
|
73
|
+
batch.push({ pending: p, result: await this.mapResult(matches[0], options.signal) });
|
|
74
|
+
}
|
|
75
|
+
// Map the whole DSH batch before releasing any native Promise. A normal MCP
|
|
76
|
+
// isError must not race an unfinished image mapping from this same batch.
|
|
77
|
+
if (this.ended) {
|
|
78
|
+
for (const { pending: p } of batch)
|
|
79
|
+
this.record({ kind: 'tool', severity: 'warning', code: 'DSH_BRIDGE_LATE_RESULT_DROPPED', summary: TOOLBRIDGE_ENDED, payload: { callId: p.block.id } });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (this.localTurnEnded)
|
|
83
|
+
for (const p of this.calls.values())
|
|
84
|
+
if (!p.exported && !p.settled) {
|
|
85
|
+
batch.push({ pending: p, result: errorResult('DSH工具已结束本轮,这个尚未派发的调用没有执行。') });
|
|
86
|
+
this.record({ kind: 'tool', severity: 'info', code: 'TOOLBRIDGE_CONCLUDED_UNDISPATCHED', summary: 'DSH工具结束了本轮,尚未交给DSH的调用没有执行。', payload: { callId: p.block.id } });
|
|
87
|
+
}
|
|
88
|
+
const ready = batch.filter(row => !row.pending.settled);
|
|
89
|
+
for (const { pending: p } of ready) {
|
|
90
|
+
p.settled = true;
|
|
91
|
+
p.off();
|
|
92
|
+
}
|
|
93
|
+
for (const { pending: p, result } of ready)
|
|
94
|
+
p.reply.resolve(result);
|
|
95
|
+
this.concludeAck();
|
|
96
|
+
}
|
|
97
|
+
async mapResult(message, signal) {
|
|
98
|
+
const content = [];
|
|
99
|
+
for (const block of message.content) {
|
|
100
|
+
if (block.type === 'text') {
|
|
101
|
+
content.push({ type: 'text', text: block.text });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (block.type === 'image') {
|
|
105
|
+
if (block.offloaded)
|
|
106
|
+
return errorResult('这张图片已按DSH策略移出上下文,工具桥未将占位内容冒充图片结果。');
|
|
107
|
+
if (!this.readImage)
|
|
108
|
+
throw new NeedError('ATTACHMENT_REQUIRED', '我需要官方附件组件读取工具返回的图片。', 'attachments.readImage');
|
|
109
|
+
const image = await this.readImage(block.attachment, signal);
|
|
110
|
+
signal?.throwIfAborted();
|
|
111
|
+
if (image.ref.attachmentId !== block.attachment.attachmentId || image.ref.bytes !== image.data.byteLength || !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.ref.mediaType))
|
|
112
|
+
throw new NeedError('ATTACHMENT_INVALID', '工具图片的附件内容无法核对,本轮没有交回。', '官方核验的图片字节与媒体类型');
|
|
113
|
+
content.push({ type: 'image', mimeType: image.ref.mediaType, data: Buffer.from(image.data).toString('base64') });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
return errorResult('工具返回了本版工具桥尚不支持的内容类型:' + block.type + ';没有静默丢弃或重新执行。');
|
|
117
|
+
}
|
|
118
|
+
return { content, isError: !!message.isError };
|
|
119
|
+
}
|
|
120
|
+
concludeAck() {
|
|
121
|
+
if (this.localTurnEnded && !this.unsettled && !this.concludeAcknowledged && this.concludeNative.size && [...this.concludeNative].every(id => this.nativeCompleted.has(id))) {
|
|
122
|
+
this.concludeAcknowledged = true;
|
|
123
|
+
this.record({ kind: 'tool', severity: 'info', code: 'TOOLBRIDGE_CONCLUDED_RESULT_DELIVERED', summary: 'DSH工具结束了本轮,原生工具已确认收到结果;结束原prompt,不再派发新工具。' });
|
|
124
|
+
this.onConcludeAcknowledged();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
observe(update) {
|
|
128
|
+
if (!this.enabled || this.ended)
|
|
129
|
+
return;
|
|
130
|
+
if (update.sessionUpdate !== 'tool_call' && update.sessionUpdate !== 'tool_call_update')
|
|
131
|
+
return;
|
|
132
|
+
const u = update;
|
|
133
|
+
const raw = u.rawInput;
|
|
134
|
+
const name = u.name ?? u._meta?.claudeCode?.toolName;
|
|
135
|
+
if (this.isBridge(name) || raw?.server === 'employee' && this.isBridge(raw.tool) || this.isBridge(u.title))
|
|
136
|
+
this.nativeBridgeIds.add(u.toolCallId);
|
|
137
|
+
if ((u.status === 'completed' || u.status === 'failed') && this.nativeBridgeIds.has(u.toolCallId))
|
|
138
|
+
this.nativeTerminated.add(u.toolCallId);
|
|
139
|
+
if (u.status === 'failed' && this.nativeBridgeIds.has(u.toolCallId) && !this.unsettled && u.rawOutput?.result?.isError === true && Array.isArray(u.rawOutput.result.content)) {
|
|
140
|
+
this.nativeCompleted.add(u.toolCallId);
|
|
141
|
+
this.concludeAck();
|
|
142
|
+
}
|
|
143
|
+
if (u.status === 'completed' && this.nativeBridgeIds.has(u.toolCallId)) {
|
|
144
|
+
this.nativeCompleted.add(u.toolCallId);
|
|
145
|
+
this.concludeAck();
|
|
146
|
+
}
|
|
147
|
+
if (u.status === 'failed' && this.nativeBridgeIds.has(u.toolCallId) && (!this.localTurnEnded || this.concludeNative.has(u.toolCallId)) && this.unsettled)
|
|
148
|
+
this.abandon('HOST_TOOL_FAILED');
|
|
149
|
+
}
|
|
150
|
+
/** A failed native bridge cancels the entire parked round; never guess between parallel identical calls. */
|
|
151
|
+
abandon(reason) {
|
|
152
|
+
if (this.ended)
|
|
153
|
+
return;
|
|
154
|
+
this.notice = TOOLBRIDGE_ENDED;
|
|
155
|
+
this.record({ kind: 'tool', severity: 'warning', code: 'DSH_BRIDGE_CALL_ABANDONED_BY_HOST', summary: this.notice, payload: { reason, toolTimeoutSec: TOOLBRIDGE_TIMEOUT_SECONDS, fixedTimeoutReasonAvailable: false, scope: 'entire-parked-round', replayed: false } });
|
|
156
|
+
this.close();
|
|
157
|
+
this.onAbandon(this.notice);
|
|
158
|
+
}
|
|
159
|
+
close() {
|
|
160
|
+
if (this.ended)
|
|
161
|
+
return;
|
|
162
|
+
this.ended = true;
|
|
163
|
+
this.controller.abort();
|
|
164
|
+
for (const p of this.calls.values())
|
|
165
|
+
if (!p.settled) {
|
|
166
|
+
p.settled = true;
|
|
167
|
+
p.off();
|
|
168
|
+
p.reply.resolve(errorResult('本轮工具等待已结束;结果不重放。'));
|
|
169
|
+
}
|
|
170
|
+
this.changed();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Codex executes nc in its existing employee environment, never on the host. */
|
|
174
|
+
export async function openToolBridgeMcp(bridge, maxBytes) {
|
|
175
|
+
const root = await mkdtemp('/private/tmp/aw-tb-'), socketPath = root + '/mcp';
|
|
176
|
+
const clients = new Set();
|
|
177
|
+
let closed = false;
|
|
178
|
+
const listener = createServer(socket => {
|
|
179
|
+
if (closed) {
|
|
180
|
+
socket.destroy();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const server = new Server({ name: 'ai-workbench-toolbridge', version: '0.9.0' }, { capabilities: { tools: {} } });
|
|
184
|
+
clients.add(server);
|
|
185
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: bridge.tools }));
|
|
186
|
+
server.setRequestHandler(CallToolRequestSchema, (r, extra) => bridge.call(r.params.name, r.params.arguments ?? {}, extra.signal));
|
|
187
|
+
server.onclose = () => clients.delete(server);
|
|
188
|
+
server.onerror = () => bridge.abandon('MCP_TRANSPORT_FAILED');
|
|
189
|
+
void server.connect(new EmployeeSocketTransport(socket, maxBytes)).catch(() => { socket.destroy(); bridge.abandon('MCP_CONNECT_FAILED'); });
|
|
190
|
+
});
|
|
191
|
+
const close = async () => { if (closed)
|
|
192
|
+
return; closed = true; bridge.close(); await Promise.allSettled([...clients].map(c => c.close())); if (listener.listening)
|
|
193
|
+
await new Promise(r => listener.close(() => r())); await rm(root, { recursive: true }); };
|
|
194
|
+
try {
|
|
195
|
+
const stat = await lstat(root);
|
|
196
|
+
if (stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0)
|
|
197
|
+
throw Error('TOOLBRIDGE_PRIVATE_DIRECTORY_REQUIRED');
|
|
198
|
+
await new Promise((yes, no) => { listener.once('error', no); listener.listen(socketPath, () => { listener.removeListener('error', no); yes(); }); });
|
|
199
|
+
return { mcpServers: [{ name: 'employee', command: '/usr/bin/nc', args: ['-U', socketPath], env: [] }], close };
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
await close();
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-workbench/remote-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-toolbridge.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"zod": "4.6.7"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.
|
|
39
|
+
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.37.tgz",
|
|
40
40
|
"@ai-workbench/network": "file:test/fixtures/ai-workbench-network-0.4.9.tgz",
|
|
41
41
|
"@deepseek-ai/cordis": "4.0.4",
|
|
42
42
|
"@deepseek-ai/cordis-plugin-group": "1.0.4",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@ai-workbench/connect": "~0.7.
|
|
70
|
+
"@ai-workbench/connect": "~0.7.37",
|
|
71
71
|
"@ai-workbench/network": "~0.4.9",
|
|
72
72
|
"@deepseek-ai/cordis": "4.0.4",
|
|
73
73
|
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
@@ -102,10 +102,11 @@
|
|
|
102
102
|
"optional": true
|
|
103
103
|
}
|
|
104
104
|
},
|
|
105
|
-
"description": "
|
|
105
|
+
"description": "AI 写作工作台(含工具桥):远程Claude与Codex,本机目录工具由DSH执行与审批。",
|
|
106
106
|
"publishConfig": {
|
|
107
107
|
"access": "public",
|
|
108
|
-
"registry": "https://registry.npmjs.org/"
|
|
108
|
+
"registry": "https://registry.npmjs.org/",
|
|
109
|
+
"tag": "toolbridge"
|
|
109
110
|
},
|
|
110
111
|
"keywords": [
|
|
111
112
|
"dsh",
|