@ai-workbench/remote-agent 0.8.28 → 0.9.0-toolbridge.2

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/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
- constructor(config, resolveRuntime, approve, providerName = () => CODEX_AGENT.name.zh, agent = CODEX_AGENT, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions) {
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 = false;
212
+ let promptDispatched = !!options.sessionId && (this.sessions.get(options.sessionId)?.session.promptDispatchCount ?? 0) > 0;
201
213
  try {
202
- yield* this.generate(options, () => { promptDispatched = true; });
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
- this.employeeNoticeSessions.add(key);
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,7 +396,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
363
396
  let failure;
364
397
  const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
365
398
  let promptPermit;
366
- const hooks = { closeDiagnostics: runtime.closeDiagnostics, broker: runtime.broker, employeeExecution: runtime.employeeExecution, ...this.permissions ? { promptGuard: () => {
399
+ let confirmedNativeMode;
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', '当前权限没有保持确认,本次没有继续派发。', '当前权限代次和同值回执');
369
403
  return promptPermit;
@@ -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 = { ...(runtime.hostSource ? { hostSource: runtime.hostSource } : {}), ...(modelConfiguration ? { modelConfiguration } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
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) { return this.adapter(provider).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) { return this.adapter(options.provider).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());
@@ -42,6 +42,9 @@ export function employeeFailure(code, brand = 'codex', promptDispatched = true)
42
42
  POLICY_DENIED: '这个账号暂时不能使用,请联系公司账号管理员。',
43
43
  SESSION_CONTEXT_MISMATCH: '远程主机连接已改变,请重新开始本次检查。',
44
44
  REMOTE_SESSION_BUSY: '原会话仍被主机上的上一轮占用,' + (promptDispatched ? '本轮请求未能完成' : '本轮输入未发送') + ';请稍后在原会话重新发送。若持续占用,请联系管理员检查上一轮主机会话。',
45
+ BROKER_OWNERSHIP_UNAVAILABLE: '主机会话组件缺少安全续接能力,本轮输入未发送;请联系管理员更新主机组件。',
46
+ BROKER_OWNERSHIP_UNCONFIRMED: '主机尚未确认这条会话的归属,本轮输入未发送;请检查远程主机连接后在原会话重试。',
47
+ BROKER_OWNERSHIP_INVALID: '远端会话编号无法确认,本轮输入未发送;请联系管理员查看技术详情。',
45
48
  ABORTED: promptDispatched ? '本轮已停止。操作可能已部分执行,请先确认文件;恢复连接后可在原会话继续。' : '本轮已停止,本轮输入未发送;恢复连接后可在原会话重新发送。',
46
49
  CONFIRMATION_REQUIRED: '请先选择是否打开授权窗口。',
47
50
  CONTEXT_UNSUPPORTED: '远程 Codex 暂不支持这类会话内容,请联系管理员。',
@@ -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';
@@ -14,6 +15,7 @@ import type { SourceAdmissionStage } from './workspace-source.ts';
14
15
  import type { LocalWorkspaceContext } from '@ai-workbench/connect/api';
15
16
  import type { EmployeeRouting } from './employee-relay.ts';
16
17
  import type { HostWorkspacePresentation } from './input.ts';
18
+ import type { SessionOwnership } from './broker-ownership.ts';
17
19
  export interface EmployeeExecution {
18
20
  hostCwd: string;
19
21
  routing: EmployeeRouting;
@@ -37,6 +39,7 @@ export interface Runtime {
37
39
  readable: ReadableStream<Uint8Array>;
38
40
  } | AgentApp | ExtendedAgentApp;
39
41
  broker?: BrokerTransport;
42
+ sessionOwnership?: SessionOwnership;
40
43
  loginRequired?: () => Promise<void>;
41
44
  dispose?: (cause?: CloseCause) => Promise<void>;
42
45
  binding?: {
@@ -64,6 +67,7 @@ export interface Runtime {
64
67
  closeDiagnostics?: CloseDiagnostics;
65
68
  }
66
69
  export interface RuntimeRequest extends Invocation {
70
+ toolBridge?: ToolBridge;
67
71
  workspaceSource?: SessionSource;
68
72
  hostSource?: Extract<SessionSource, {
69
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>;