@ai-workbench/remote-agent 0.8.0 → 0.8.1

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,3 +1,4 @@
1
+ import { SessionId } from '@deepseek-ai/dsh-session';
1
2
  import Schema from '@deepseek-ai/schemastery';
2
3
  import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
3
4
  import { fileURLToPath } from 'node:url';
@@ -14,6 +15,7 @@ import { prepareInput } from "./input.js";
14
15
  import { createWorkspaceNoticeConsumer, isWorkspaceNoticeMessage } from "./workspace-notice.js";
15
16
  import { NeedError } from "./remote.js";
16
17
  import { employeeFailure } from "./presentation.js";
18
+ import { createRemoteDetails } from "./remote-details.js";
17
19
  import { PROVIDER_NAMES, ProviderLocaleRemote } from "./provider-locale.js";
18
20
  export const name = 'ai-workbench-remote-agent';
19
21
  export const settingsNamespace = 'ai-workbench-codex';
@@ -116,6 +118,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
116
118
  let live;
117
119
  let succeeded = false;
118
120
  let remoteAttempted = false;
121
+ let activeMapper;
119
122
  const config = this.config();
120
123
  const mode = this.agent.oneShot ? config.mode : 'session';
121
124
  try {
@@ -150,9 +153,17 @@ export class CodexRemoteAdapter extends LlmAdapter {
150
153
  signal.throwIfAborted();
151
154
  if (previous && (previous.identity !== runtime.identity || previous.cwd !== runtime.cwd || previous.bindingId && previous.bindingId !== runtime.binding?.bindingId || previous.workspaceId && previous.workspaceId !== runtime.binding?.workspaceId))
152
155
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的员工主机或工作区已改变,请新建会话', '原会话的可信目标和工作区');
153
- const mapper = new StreamMapper(mode === 'session' ? (live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined);
156
+ const details = runtime.details;
157
+ if (!details)
158
+ throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
159
+ const mapper = new StreamMapper(mode === 'session' ? (live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
160
+ activeMapper = mapper;
154
161
  let id;
155
162
  let stop;
163
+ if (runtime.workspaceNotice) {
164
+ yield* mapper.text(runtime.workspaceNotice.text);
165
+ yield* mapper.flush();
166
+ }
156
167
  if (mode === 'session') {
157
168
  if (live && live.runtime !== runtime)
158
169
  throw new NeedError('SESSION_TRANSPORT_MISMATCH', '原会话的主机通道已更换,不能自动切换进程', '原主机会话通道');
@@ -169,7 +180,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
169
180
  catch (error) {
170
181
  failure = error;
171
182
  session?.close();
172
- } }, onResponse: response => push(mapper.acpResponse(response)), onNeed: need => push(mapper.text(`\n[需要处理] ${need.message}\n`)) };
183
+ } }, onResponse: response => push(mapper.acpResponse(response)), onDetail: detail => details.record(detail) };
173
184
  const fresh = !live;
174
185
  if (fresh) {
175
186
  const transport = runtime.acpTransport instanceof acp.AgentApp || runtime.acpTransport instanceof unstable.AgentApp ? runtime.acpTransport : acp.ndJsonStream(runtime.acpTransport.writable, runtime.acpTransport.readable, { maxMessageBytes: config.acpMaxMessageBytes });
@@ -193,8 +204,15 @@ export class CodexRemoteAdapter extends LlmAdapter {
193
204
  if (updated && this.agent.loginProvider === 'claude')
194
205
  throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
195
206
  }
207
+ await details.flush();
208
+ details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
196
209
  id = session.id;
197
- return await session.prompt(input.prompt, signal);
210
+ try {
211
+ return await session.prompt(input.prompt, signal);
212
+ }
213
+ finally {
214
+ details.endTurn();
215
+ }
198
216
  })();
199
217
  void task.then(value => { stop = value; }, error => { failure ??= runtime.broker?.error ?? error; }).finally(() => { done = true; wake?.(); wake = undefined; });
200
218
  while (!done || queue.length) {
@@ -214,6 +232,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
214
232
  else {
215
233
  if (!runtime.oneShot)
216
234
  throw new NeedError('DEPENDENCY_REQUIRED', '我需要主机单次运行助手', 'S安装的aw-codex-once');
235
+ details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
217
236
  const text = input.prompt.map(block => block.type === 'text' ? block.text : '').join('\n');
218
237
  for await (const event of runOneShot(runtime.oneShot, { wrapperPath: fileURLToPath(new URL('./sdk-ssh-wrapper.js', import.meta.url)), sandboxMode: config.oneShotSandbox, approvalPolicy: config.oneShotApproval }, text, previous?.sessionId, signal)) {
219
238
  if (event.type === 'thread.started')
@@ -222,17 +241,31 @@ export class CodexRemoteAdapter extends LlmAdapter {
222
241
  yield chunk;
223
242
  }
224
243
  }
244
+ details.endTurn();
245
+ await details.flush();
225
246
  if (!id)
226
247
  throw new NeedError('SESSION_ID_MISSING', '远端没有返回会话标识', '有效的远端会话标识');
227
248
  if (live)
228
249
  live.usageSnapshot = mapper.usageSnapshot;
229
- const pointer = { version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), mode, identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
250
+ yield* mapper.flush();
251
+ const pointer = { ...(runtime.workspaceNotice ? { presentation: { workspaceNotice: runtime.workspaceNotice } } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), mode, identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
230
252
  succeeded = true;
231
253
  yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
232
254
  }
233
255
  catch (error) {
234
256
  if (live && (remoteAttempted || runtime === live.runtime))
235
- live.failure = error;
257
+ live.failure = { cause: error };
258
+ let reportingFailure;
259
+ try {
260
+ await runtime?.details?.flush();
261
+ }
262
+ catch (failure) {
263
+ reportingFailure = { cause: failure };
264
+ }
265
+ if (activeMapper)
266
+ yield* activeMapper.flush();
267
+ if (reportingFailure !== undefined)
268
+ throw reportingFailure.cause;
236
269
  throw error;
237
270
  }
238
271
  finally {
@@ -269,7 +302,41 @@ export function apply(ctx, live) {
269
302
  throw new NeedError('ATTACHMENT_REQUIRED', '我需要官方附件组件读取这张图片', '官方attachments.readImage');
270
303
  return attachments.readImage(ref, signal);
271
304
  };
272
- const adapter = new RemoteAgentsAdapter(config, agent => connectProvider(get, { agent, notice: createWorkspaceNoticeConsumer(ctx, agent), open: brokerFactory(get, undefined, config) }), dshApproval(ctx), agent => agent.name[language], readImage);
305
+ const openByAgent = new Map(AGENTS.map(agent => [agent.id, brokerFactory(get, undefined, config)]));
306
+ const detailManagers = new Map();
307
+ const adapter = new RemoteAgentsAdapter(config, agent => connectProvider(get, { agent, notice: createWorkspaceNoticeConsumer(ctx, agent), details: async (request, api, connection, target) => {
308
+ const profile = ctx.get('profileContext');
309
+ if (typeof profile?.dir !== 'string')
310
+ throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用:缺少官方profile目录。', '官方profileContext.dir');
311
+ let manager = detailManagers.get(profile.dir);
312
+ if (!manager) {
313
+ manager = createRemoteDetails({ profileDir: profile.dir, getApi: get });
314
+ detailManagers.set(profile.dir, manager);
315
+ }
316
+ return manager.open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection });
317
+ }, open: async (...args) => {
318
+ const runtime = await openByAgent.get(agent.id)(...args);
319
+ runtime.detailContext = () => {
320
+ const events = args[2].sessionId ? ctx.get('agents')?.get(SessionId(args[2].sessionId))?.session.snapshotEvents() : undefined;
321
+ let turn = null, step = null;
322
+ for (const event of events ?? []) {
323
+ if (event.type === 'turn/start') {
324
+ turn = event.data.turn;
325
+ step = null;
326
+ }
327
+ else if (event.type === 'turn/end') {
328
+ turn = null;
329
+ step = null;
330
+ }
331
+ else if (event.type === 'step/start' && event.data.turn === turn)
332
+ step = event.data.step;
333
+ else if (event.type === 'step/end')
334
+ step = null;
335
+ }
336
+ return { turn, step };
337
+ };
338
+ return runtime;
339
+ } }), dshApproval(ctx), agent => agent.name[language], readImage);
273
340
  const ids = AGENTS.map(agent => agent.id);
274
341
  const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: settingsNamespace, settingsPath: [] }));
275
342
  const directory = ctx.llm.registerConfigurableProviders(entries());
package/dist/input.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { NeedError } from "./remote.js";
2
- import { isWorkspaceNoticeMessage } from "./workspace-notice.js";
2
+ import { isWorkspaceNoticeMessage, withoutWorkspaceNotice } from "./workspace-notice.js";
3
3
  /** The attachment service supplies verified bytes; client paths never cross SSH. */
4
4
  export async function prepareInput(options, readImage) {
5
5
  options.signal?.throwIfAborted();
6
- const messages = options.messages.filter(message => !isWorkspaceNoticeMessage(message));
6
+ const messages = options.messages.filter(message => !isWorkspaceNoticeMessage(message)).map(withoutWorkspaceNotice);
7
7
  const context = [];
8
8
  if (options.system)
9
9
  context.push('系统上下文:\n' + options.system);
package/dist/mapping.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { StreamChunk } from '@deepseek-ai/dsh-llm';
2
2
  import type { PromptResponse, SessionUpdate } from '@agentclientprotocol/sdk/unstable';
3
3
  import type { ThreadEvent } from '@openai/codex-sdk';
4
+ import { type SanitizedObservation } from './conversation-details.ts';
4
5
  /** Persist alongside the exact remote session identity; null means no trusted baseline. */
5
6
  export type UsageSnapshot = {
6
7
  version: 1;
@@ -18,23 +19,28 @@ export type UsageSnapshot = {
18
19
  /** A response was missing or unusable; the next complete response only re-establishes a baseline. */
19
20
  gap?: true;
20
21
  };
21
- /** Remote tools are reported as text; they must never become local DSH tool invocations. */
22
+ /** Only model text enters DSH blocks; remote operations remain diagnostic observations. */
22
23
  export declare class StreamMapper {
23
24
  private index;
25
+ private active;
24
26
  private seen;
25
27
  private baseline;
28
+ private readonly observations;
29
+ private readonly onDiagnostic;
26
30
  /** Omitted argument is exclusively for a known new session; pass null on unknown resume. */
27
- constructor(previousUsage?: UsageSnapshot | null);
31
+ constructor(previousUsage?: UsageSnapshot | null, onDiagnostic?: (observation: Readonly<SanitizedObservation>) => void);
28
32
  get usageSnapshot(): UsageSnapshot | null;
33
+ get diagnostics(): readonly Readonly<SanitizedObservation>[];
34
+ private diagnostic;
35
+ private usageWarning;
29
36
  text(text: string, reasoning?: boolean): StreamChunk[];
37
+ /** Close the current text or reasoning block once, before the stream settles. */
38
+ flush(): StreamChunk[];
30
39
  acp(update: SessionUpdate): StreamChunk[];
31
40
  /** ACP usage and the fixed Claude quota both accumulate across the session, not per turn. */
32
41
  acpResponse(response: PromptResponse): StreamChunk[];
33
42
  private usageGap;
34
43
  private cumulativeUsage;
35
44
  private usage;
36
- private content;
37
- private toolContent;
38
- private meta;
39
45
  sdk(event: ThreadEvent): StreamChunk[];
40
46
  }
package/dist/mapping.js CHANGED
@@ -1,89 +1,117 @@
1
1
  import { NeedError } from "./remote.js";
2
+ import { sanitizeObservation } from "./conversation-details.js";
2
3
  const usageCounters = ['inputTokens', 'outputTokens', 'cachedReadTokens', 'cachedWriteTokens', 'thoughtTokens', 'totalTokens'];
3
4
  const optionalUsageCounters = ['cachedReadTokens', 'cachedWriteTokens', 'thoughtTokens', 'totalTokens'];
4
- /** Remote tools are reported as text; they must never become local DSH tool invocations. */
5
+ /** Only model text enters DSH blocks; remote operations remain diagnostic observations. */
5
6
  export class StreamMapper {
6
7
  index = 0;
8
+ active;
7
9
  seen = new Map();
8
10
  baseline;
11
+ observations = [];
12
+ onDiagnostic;
9
13
  /** Omitted argument is exclusively for a known new session; pass null on unknown resume. */
10
- constructor(previousUsage = { version: 1, kind: 'zero' }) {
14
+ constructor(previousUsage = { version: 1, kind: 'zero' }, onDiagnostic) {
11
15
  if (previousUsage !== null && !validUsageSnapshot(previousUsage))
12
16
  throw new NeedError('USAGE_BASELINE_INVALID', '已保存的远端累计用量基线无效,不能重复结算历史用量', '与原远端会话关联的有效用量快照');
13
17
  this.baseline = previousUsage === null ? null : { ...previousUsage };
18
+ this.onDiagnostic = onDiagnostic;
14
19
  }
15
20
  get usageSnapshot() { return this.baseline === null ? null : { ...this.baseline }; }
21
+ get diagnostics() {
22
+ return Object.freeze(this.observations.map(observation => sanitizeObservation(observation)));
23
+ }
24
+ diagnostic(observation) {
25
+ const sanitized = sanitizeObservation(observation);
26
+ this.observations.push(sanitized);
27
+ this.onDiagnostic?.(sanitized);
28
+ return [];
29
+ }
30
+ usageWarning(summary) {
31
+ return this.diagnostic({ kind: 'usage', severity: 'warning', code: 'USAGE_UNAVAILABLE', summary });
32
+ }
16
33
  text(text, reasoning = false) {
17
34
  if (!text)
18
35
  return [];
19
- const index = this.index++;
20
- return [{ type: 'block-start', index, blockType: reasoning ? 'reasoning' : 'text' },
21
- reasoning ? { type: 'reasoning-delta', index, text } : { type: 'text-delta', index, text },
22
- { type: 'block-end', index, block: { type: reasoning ? 'reasoning' : 'text', text } }];
36
+ const type = reasoning ? 'reasoning' : 'text';
37
+ const chunks = this.active && this.active.type !== type ? this.flush() : [];
38
+ if (!this.active) {
39
+ this.active = { index: this.index++, type, text: '' };
40
+ chunks.push({ type: 'block-start', index: this.active.index, blockType: type });
41
+ }
42
+ const { index } = this.active;
43
+ this.active.text += text;
44
+ chunks.push(reasoning ? { type: 'reasoning-delta', index, text } : { type: 'text-delta', index, text });
45
+ return chunks;
46
+ }
47
+ /** Close the current text or reasoning block once, before the stream settles. */
48
+ flush() {
49
+ if (!this.active)
50
+ return [];
51
+ const { index, type, text } = this.active;
52
+ this.active = undefined;
53
+ return [{ type: 'block-end', index, block: { type, text } }];
23
54
  }
24
55
  acp(update) {
25
- let chunks;
56
+ if (update.sessionUpdate === 'agent_message_chunk' || update.sessionUpdate === 'agent_thought_chunk') {
57
+ if (update.content.type === 'text' && typeof update.content.text === 'string') {
58
+ // The whole update preserves every extension layer without inventing a display mapping.
59
+ if (Object.keys(update).some(key => key !== 'sessionUpdate' && key !== 'content')
60
+ || Object.keys(update.content).some(key => key !== 'type' && key !== 'text')) {
61
+ this.diagnostic({ kind: 'extension', severity: 'info', code: 'ACP_MODEL_METADATA', summary: '远端模型消息扩展', method: 'session/update', payload: update });
62
+ }
63
+ return this.text(update.content.text, update.sessionUpdate === 'agent_thought_chunk');
64
+ }
65
+ return this.diagnostic({ kind: 'extension', severity: 'warning', code: 'ACP_CONTENT_UNSUPPORTED', summary: '远端非文本内容,需要对应的公开显示映射', method: 'session/update', payload: update });
66
+ }
67
+ let kind = 'session';
68
+ let code = 'ACP_SESSION_UPDATE';
69
+ let summary = '远端会话事件';
70
+ let severity = 'info';
26
71
  switch (update.sessionUpdate) {
27
- case 'agent_message_chunk':
28
- case 'agent_thought_chunk':
29
- chunks = this.content(update.content, update.sessionUpdate === 'agent_thought_chunk');
30
- break;
31
- case 'user_message_chunk':
32
- chunks = this.text('\n[远端输入回显]\n').concat(this.content(update.content));
72
+ case 'available_commands_update':
73
+ kind = 'commands';
74
+ code = 'ACP_COMMANDS';
75
+ summary = '远端命令与技能目录';
33
76
  break;
34
77
  case 'tool_call':
35
78
  case 'tool_call_update':
36
- chunks = this.text(`\n[远端工具] ${update.title ?? update.toolCallId} · ${toolStatus(update.status)}\n`);
37
- for (const content of update.content ?? [])
38
- chunks.push(...this.toolContent(content));
79
+ kind = 'tool';
80
+ code = 'ACP_TOOL';
81
+ summary = '远端工具事件';
39
82
  break;
40
83
  case 'usage_update':
41
- chunks = this.text(`\n[远端上下文] ${update.used}/${update.size} 令牌${update.cost ? ` · 累计费用 ${update.cost.amount} ${update.cost.currency}` : ''};生成用量尚未提供,等待远端本轮真实计数。\n`);
84
+ kind = 'usage';
85
+ code = 'ACP_USAGE_CONTEXT';
86
+ summary = '远端上下文占用,不替代生成用量';
42
87
  break;
43
- case 'plan':
44
- chunks = this.text('\n[远端计划]\n' + update.entries.map(e => `${planStatus(e.status)}: ${e.content}`).join('\n') + '\n');
88
+ case 'notice':
89
+ kind = 'notice';
90
+ code = 'ACP_NOTICE';
91
+ summary = '远端通知';
45
92
  break;
93
+ case 'user_message_chunk':
94
+ case 'plan':
46
95
  case 'plan_update':
47
- chunks = this.text('\n[远端计划更新]\n' + (update.plan.type === 'markdown' ? update.plan.content : update.plan.type === 'file' ? `远端计划 URI(仅供核查):${plainUri(update.plan.uri)}` : update.plan.entries.map(e => `${planStatus(e.status)}: ${e.content}`).join('\n')) + '\n');
48
- break;
49
96
  case 'plan_removed':
50
- chunks = this.text(`\n[远端计划] 已移除计划 ${update.planId}\n`);
51
- break;
52
- case 'notice':
53
- chunks = this.text(`\n[远端通知] ${update.title}${update.description ? ': ' + update.description : ''}\n`);
54
- break;
55
97
  case 'current_mode_update':
56
- chunks = this.text(`\n[远端模式] ${update.currentModeId}\n`);
57
- break;
58
- case 'available_commands_update':
59
- chunks = this.text('\n[远端命令] ' + update.availableCommands.map(c => `${c.name}${c.description ? `:${c.description}` : ''}`).join(';') + '\n');
60
- break;
61
98
  case 'config_option_update':
62
- chunks = this.text('\n[远端配置] ' + update.configOptions.map(c => c.name).join('、') + '已更新;首版通过远端官方配置管理。\n');
63
- break;
64
99
  case 'session_info_update':
65
- chunks = this.text(`\n[远端会话信息] ${update.title ?? '会话信息已更新'}\n`);
66
- break;
67
100
  case 'compaction_update':
68
- chunks = this.text(`\n[远端上下文整理] ${update.status}\n`);
69
- break;
70
101
  case 'compaction_summary_chunk':
71
- chunks = this.text('\n[远端上下文摘要]\n').concat(this.content(update.content));
72
- break;
73
102
  case 'session_message_chunk':
74
- chunks = this.text('\n[远端会话消息]\n').concat(this.content(update.content));
75
- break;
76
- case 'session_message':
77
- chunks = this.text('\n[远端会话消息]\n');
78
- for (const content of update.content ?? [])
79
- chunks.push(...this.content(content));
80
- break;
81
- default: chunks = this.text(`\n[远端事件] 首版不支持此事件:${safeLabel(update.sessionUpdate)};需要对应的公开显示映射。\n`);
103
+ case 'session_message': break;
104
+ default:
105
+ kind = 'extension';
106
+ code = 'ACP_EVENT_UNSUPPORTED';
107
+ severity = 'warning';
108
+ summary = '远端未知事件,需要对应的公开显示映射';
82
109
  }
83
- return chunks.concat(this.meta(update._meta));
110
+ return this.diagnostic({ kind, severity, code, summary, method: 'session/update', payload: update });
84
111
  }
85
112
  /** ACP usage and the fixed Claude quota both accumulate across the session, not per turn. */
86
113
  acpResponse(response) {
114
+ this.diagnostic({ kind: 'usage', severity: 'info', code: 'ACP_PROMPT_RESPONSE', summary: '远端响应与真实用量', method: 'session/prompt', payload: response });
87
115
  const quota = record(response._meta?.quota);
88
116
  const tokens = record(quota?.token_count);
89
117
  const usage = response.usage;
@@ -98,16 +126,16 @@ export class StreamMapper {
98
126
  }
99
127
  else {
100
128
  this.usageGap();
101
- chunks = this.text('\n[远端用量] 本轮生成用量未提供;上下文占用不能替代输入或输出计数。\n');
129
+ chunks = this.usageWarning('\n[远端用量] 本轮生成用量未提供;上下文占用不能替代输入或输出计数。\n');
102
130
  }
103
- return chunks.concat(this.meta(response._meta, ['quota']));
131
+ return chunks;
104
132
  }
105
133
  usageGap() { this.baseline = this.baseline?.kind === 'cumulative' ? { ...this.baseline, gap: true } : null; }
106
134
  cumulativeUsage(source, input, output, read, write, thought, total) {
107
135
  const values = [input, output, ...(read == null ? [] : [read]), ...(write == null ? [] : [write]), ...(thought == null ? [] : [thought]), ...(total == null ? [] : [total])];
108
136
  if (!values.every(validCount)) {
109
137
  this.usageGap();
110
- return this.text('\n[远端用量] 远端累计计数无效,本轮未记录生成用量;需要有效的输入、输出与缓存计数。\n');
138
+ return this.usageWarning('\n[远端用量] 远端累计计数无效,本轮未记录生成用量;需要有效的输入、输出与缓存计数。\n');
111
139
  }
112
140
  const current = { version: 1, kind: 'cumulative', source, inputTokens: input, outputTokens: output,
113
141
  ...(read == null ? {} : { cachedReadTokens: read }), ...(write == null ? {} : { cachedWriteTokens: write }),
@@ -116,21 +144,21 @@ export class StreamMapper {
116
144
  if (previous?.kind === 'cumulative' && previous.source === source) {
117
145
  if (usageCounters.some(key => current[key] !== undefined && previous[key] !== undefined && current[key] < previous[key])) {
118
146
  this.usageGap();
119
- return this.text('\n[远端用量] 远端累计计数发生回退,本轮未记录生成用量;已保留此前高水位,需要核对远端会话计数。\n');
147
+ return this.usageWarning('\n[远端用量] 远端累计计数发生回退,本轮未记录生成用量;已保留此前高水位,需要核对远端会话计数。\n');
120
148
  }
121
149
  if (optionalUsageCounters.some(key => previous[key] !== undefined && current[key] === undefined)) {
122
150
  this.baseline = { ...previous, ...current, gap: true };
123
- return this.text('\n[远端用量] 可选累计计数缺失,本轮未记录生成用量;未把缺失值补为零,等待完整计数重建基线。\n');
151
+ return this.usageWarning('\n[远端用量] 可选累计计数缺失,本轮未记录生成用量;未把缺失值补为零,等待完整计数重建基线。\n');
124
152
  }
125
153
  }
126
154
  const sum = current.inputTokens + current.outputTokens + (current.cachedReadTokens ?? 0) + (current.cachedWriteTokens ?? 0);
127
155
  if (!Number.isSafeInteger(sum) || (current.totalTokens !== undefined && current.totalTokens !== sum) || (current.thoughtTokens !== undefined && current.thoughtTokens > current.outputTokens)) {
128
156
  this.usageGap();
129
- return this.text('\n[远端用量] 输入、输出、缓存与合计计数不一致,本轮未记录生成用量;需要远端提供语义一致的真实计数。\n');
157
+ return this.usageWarning('\n[远端用量] 输入、输出、缓存与合计计数不一致,本轮未记录生成用量;需要远端提供语义一致的真实计数。\n');
130
158
  }
131
159
  this.baseline = current;
132
160
  if (previous === null || previous.kind === 'cumulative' && (previous.source !== source || previous.gap || optionalUsageCounters.some(key => previous[key] === undefined && current[key] !== undefined))) {
133
- return this.text('\n[远端用量] 已建立当前累计用量基线;本轮未记录生成用量,避免把无法核对的历史累计重复结算。\n');
161
+ return this.usageWarning('\n[远端用量] 已建立当前累计用量基线;本轮未记录生成用量,避免把无法核对的历史累计重复结算。\n');
134
162
  }
135
163
  if (previous.kind === 'zero')
136
164
  return this.usage(current.inputTokens, current.outputTokens, current.cachedReadTokens, current.cachedWriteTokens, current.thoughtTokens, current.totalTokens);
@@ -142,59 +170,37 @@ export class StreamMapper {
142
170
  usage(input, output, read, write, thought, total) {
143
171
  const values = [input, output, ...(read == null ? [] : [read]), ...(write == null ? [] : [write]), ...(thought == null ? [] : [thought]), ...(total == null ? [] : [total])];
144
172
  if (!values.every(n => typeof n === 'number' && Number.isSafeInteger(n) && n >= 0)) {
145
- return this.text('\n[远端用量] 远端真实计数无效,本轮未记录生成用量;需要有效的输入、输出与缓存计数。\n');
173
+ return this.usageWarning('\n[远端用量] 远端真实计数无效,本轮未记录生成用量;需要有效的输入、输出与缓存计数。\n');
146
174
  }
147
175
  const sum = input + output + (read ?? 0) + (write ?? 0);
148
176
  if ((total != null && total !== sum) || (thought != null && thought > output)) {
149
- return this.text('\n[远端用量] 输入、输出、缓存与合计计数不一致,本轮未记录生成用量;需要远端提供语义一致的真实计数。\n');
177
+ return this.usageWarning('\n[远端用量] 输入、输出、缓存与合计计数不一致,本轮未记录生成用量;需要远端提供语义一致的真实计数。\n');
150
178
  }
151
179
  return [{ type: 'usage', usage: { inputTokens: input, outputTokens: output,
152
180
  ...(read == null ? {} : { cacheReadTokens: read }), ...(write == null ? {} : { cacheWriteTokens: write }),
153
181
  ...(thought == null ? {} : { reasoningTokens: thought }) } }];
154
182
  }
155
- content(content, reasoning = false) {
156
- let chunks;
157
- switch (content.type) {
158
- case 'text':
159
- chunks = this.text(content.text, reasoning);
160
- break;
161
- case 'image':
162
- chunks = this.text(`\n[远端图片] 收到 ${content.mimeType} 图片内容;需要持久附件引用才能在官方界面显示,首版未生成附件。${content.uri ? `远端 URI(仅供核查):${plainUri(content.uri)}` : ''}\n`);
163
- break;
164
- case 'audio':
165
- chunks = this.text(`\n[远端音频] 收到 ${content.mimeType} 音频内容;首版尚无持久音频附件适配,需要远端提供文字或可持久引用。\n`);
166
- break;
167
- case 'resource_link':
168
- chunks = this.text(`\n[远端资源] ${content.name}${content.description ? `:${content.description}` : ''};URI(仅供核查,不作为员工设备本地资源打开):${plainUri(content.uri)}\n`);
169
- break;
170
- case 'resource':
171
- chunks = this.text(`\n[远端嵌入资源] URI(仅供核查):${plainUri(content.resource.uri)}\n`);
172
- if ('text' in content.resource)
173
- chunks.push(...this.text(content.resource.text));
174
- else
175
- chunks.push(...this.text('远端返回二进制资源;首版需要持久附件适配,尚未展示其字节内容。\n'));
176
- chunks.push(...this.meta(content.resource._meta));
177
- break;
178
- default: chunks = this.text(`\n[远端内容] 首版不支持此内容类型:${safeLabel(content.type)};需要对应的公开内容映射。\n`);
179
- }
180
- return chunks.concat(this.meta(content._meta));
181
- }
182
- toolContent(content) {
183
- switch (content.type) {
184
- case 'content': return this.content(content.content).concat(this.meta(content._meta));
185
- case 'diff': return this.text(`\n[远端文件修改] ${content.path}\n原内容:\n${content.oldText ?? '远端未提供'}\n新内容:\n${content.newText}\n`).concat(this.meta(content._meta));
186
- case 'terminal': return this.text(`\n[远端终端] ${content.terminalId};首版未创建员工设备本地终端,需要远端提供文本输出。\n`).concat(this.meta(content._meta));
187
- default: return this.text('\n[远端工具内容] 首版不支持此内容,需要对应的公开显示映射。\n');
188
- }
189
- }
190
- meta(meta, known = []) {
191
- const fields = record(meta);
192
- if (!fields)
193
- return [];
194
- const names = Object.keys(fields).filter(key => !known.includes(key));
195
- return names.length ? this.text(`\n[远端扩展] 首版不支持这些扩展字段:${names.map(safeLabel).join('、')};扩展值未展开,需要公开的对应映射。\n`) : [];
196
- }
197
183
  sdk(event) {
184
+ if (event.type === 'item.started' || event.type === 'item.updated' || event.type === 'item.completed') {
185
+ const item = event.item;
186
+ if ((item.type === 'agent_message' || item.type === 'reasoning') && typeof item.text === 'string') {
187
+ if (Object.keys(event).some(key => key !== 'type' && key !== 'item')
188
+ || Object.keys(item).some(key => key !== 'id' && key !== 'type' && key !== 'text')) {
189
+ this.diagnostic({ kind: 'extension', severity: 'info', code: 'SDK_MODEL_METADATA', summary: '远端模型消息扩展', method: event.type, payload: event });
190
+ }
191
+ const previous = this.seen.get(item.id) ?? '';
192
+ if (!item.text.startsWith(previous))
193
+ throw new NeedError('EVENT_REWRITE_UNSUPPORTED', '远端更改了已输出的文本,当前流不能无损替换', '支持文本替换的展示适配器');
194
+ this.seen.set(item.id, item.text);
195
+ return this.text(item.text.slice(previous.length), item.type === 'reasoning');
196
+ }
197
+ const tool = item.type === 'command_execution' || item.type === 'file_change' || item.type === 'mcp_tool_call';
198
+ this.diagnostic({ kind: tool ? 'tool' : 'session', severity: item.type === 'error' ? 'warning' : 'info', code: tool ? 'SDK_TOOL' : 'SDK_ITEM', summary: '远端非模型事件', method: event.type, payload: event });
199
+ if (item.type === 'error')
200
+ throw new NeedError('ONESHOT_FAILED', 'Codex 返回了错误,根因未查明', '远端诊断');
201
+ return [];
202
+ }
203
+ this.diagnostic({ kind: event.type === 'turn.completed' ? 'usage' : 'session', severity: event.type === 'error' || event.type === 'turn.failed' ? 'warning' : 'info', code: 'SDK_EVENT', summary: '远端会话事件', method: event.type, payload: event });
198
204
  if (event.type === 'turn.completed') {
199
205
  const u = event.usage;
200
206
  const write = u.cache_write_input_tokens ?? 0;
@@ -205,30 +211,6 @@ export class StreamMapper {
205
211
  ...(u.cache_write_input_tokens !== undefined ? { cacheWriteTokens: write } : {}),
206
212
  ...(u.reasoning_output_tokens !== undefined ? { reasoningTokens: u.reasoning_output_tokens } : {}) } }];
207
213
  }
208
- if (event.type !== 'item.started' && event.type !== 'item.updated' && event.type !== 'item.completed')
209
- return [];
210
- const item = event.item;
211
- if (item.type === 'agent_message' || item.type === 'reasoning') {
212
- const previous = this.seen.get(item.id) ?? '';
213
- if (!item.text.startsWith(previous))
214
- throw new NeedError('EVENT_REWRITE_UNSUPPORTED', '远端更改了已输出的文本,当前流不能无损替换', '支持文本替换的展示适配器');
215
- this.seen.set(item.id, item.text);
216
- return this.text(item.text.slice(previous.length), item.type === 'reasoning');
217
- }
218
- if (item.type === 'command_execution' || item.type === 'file_change' || item.type === 'mcp_tool_call') {
219
- const key = `tool:${item.id}`;
220
- if (this.seen.get(key) === item.status)
221
- return [];
222
- this.seen.set(key, item.status);
223
- const title = item.type === 'command_execution' ? item.command : item.type === 'file_change' ? item.changes.map(c => c.path).join(', ') : `${item.server}/${item.tool}`;
224
- return this.text(`\n[远端工具] ${title} · ${item.status}\n`);
225
- }
226
- if (item.type === 'web_search')
227
- return this.text(`\n[远端搜索] ${item.query}\n`);
228
- if (item.type === 'todo_list')
229
- return this.text('\n[远端计划]\n' + item.items.map(i => `${i.completed ? '完成' : '待做'}: ${i.text}`).join('\n') + '\n');
230
- if (item.type === 'error')
231
- throw new NeedError('ONESHOT_FAILED', 'Codex 返回了错误,根因未查明', '远端诊断');
232
214
  return [];
233
215
  }
234
216
  }
@@ -246,21 +228,3 @@ function validUsageSnapshot(value) {
246
228
  return snapshot.gap === true || Number.isSafeInteger(sum) && (snapshot.totalTokens === undefined || snapshot.totalTokens === sum) && (snapshot.thoughtTokens === undefined || snapshot.thoughtTokens <= snapshot.outputTokens);
247
229
  }
248
230
  function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined; }
249
- function safeLabel(value) { return value.replaceAll(/[\r\n\x00-\x1f\x7f]/g, ' '); }
250
- function plainUri(uri) {
251
- let result = uri;
252
- try {
253
- const parsed = new URL(uri);
254
- parsed.username = '';
255
- parsed.password = '';
256
- parsed.search = '';
257
- parsed.hash = '';
258
- result = parsed.toString();
259
- }
260
- catch {
261
- result = uri.split(/[?#]/)[0];
262
- }
263
- return '`' + safeLabel(result).replaceAll('`', '\u02cb') + '`';
264
- }
265
- function toolStatus(status) { return { pending: '待处理', in_progress: '处理中', completed: '完成', failed: '失败' }[status ?? ''] ?? '更新'; }
266
- function planStatus(status) { return { pending: '待做', in_progress: '进行中', completed: '完成' }[status] ?? safeLabel(status); }
@@ -5,6 +5,10 @@ export const TEST_HOST_NOTICE = '远程主机正在使用测试配置,遇到
5
5
  /** Employee failures use business language; diagnostic codes remain separately available. */
6
6
  export function employeeFailure(code) {
7
7
  const messages = {
8
+ TECHNICAL_DETAILS_UNAVAILABLE: '技术详情暂不可用,请更新远程主机连接组件。',
9
+ REMOTE_DETAIL_STORAGE_FAILED: '技术详情保存失败,本轮已停止,请联系管理员检查。',
10
+ REMOTE_DETAIL_DELIVERY_UNKNOWN: '技术详情的持久结果无法核对,本轮已停止。',
11
+ REMOTE_DETAIL_TOO_LARGE: '技术详情超过已配置的单事件容量,本轮已停止;请联系管理员调整conversationDetails.maxEventBytes。',
8
12
  DEPENDENCY_REQUIRED: CONNECTION_REQUIRED,
9
13
  LOGIN_REQUIRED: '需要登录,请到设置 → 远程主机连接 点登录',
10
14
  LOGIN_COMPLETED: '登录已完成,请重新发送本轮内容。',
@@ -7,6 +7,8 @@ import type { BrokerTransport, BrokerSettings } from './broker.ts';
7
7
  import type { Interaction, Invocation } from './interaction.ts';
8
8
  import { type AgentDefinition } from './agents.ts';
9
9
  import type { NativeWorkspaceApi, SessionProjectTarget, WorkspaceNotice, LoginProvider } from './connect-contract.ts';
10
+ import type { DetailChannel } from './remote-details.ts';
11
+ import type { WorkspaceNoticePresentation } from './workspace-notice.ts';
10
12
  export interface Runtime {
11
13
  identity: string;
12
14
  cwd: string;
@@ -24,6 +26,12 @@ export interface Runtime {
24
26
  revision: number;
25
27
  };
26
28
  connection?: ConnectionState;
29
+ workspaceNotice?: WorkspaceNoticePresentation;
30
+ details?: DetailChannel;
31
+ detailContext?: () => {
32
+ turn: string | number | null;
33
+ step: string | number | null;
34
+ };
27
35
  }
28
36
  export interface RuntimeRequest extends Invocation {
29
37
  mode: 'session' | 'one-shot';
@@ -80,7 +88,7 @@ type Result<T> = {
80
88
  };
81
89
  };
82
90
  /** 消费正式connect-api 0.7公开子集;X不响应L登录票据、不直接调用登录。 */
83
- export interface ConnectApi extends Pick<LConnectApi, 'getConnectionState' | 'getLoginPromptState' | 'requestLoginPrompt' | 'refreshLoginPrompts'>, Partial<NativeWorkspaceApi> {
91
+ export interface ConnectApi extends Pick<LConnectApi, 'getConnectionState' | 'getLoginPromptState' | 'requestLoginPrompt' | 'refreshLoginPrompts'>, Partial<NativeWorkspaceApi>, Partial<Pick<LConnectApi, 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails'>> {
84
92
  getState?(): ConnectState;
85
93
  getSshTarget(): Promise<Result<ConnectTarget>>;
86
94
  checkLogin(provider: LoginProvider): Promise<Result<{
@@ -124,12 +132,13 @@ export declare function requireNativeWorkspace(api: ConnectApi): asserts api is
124
132
  export declare function offerLogin(connect: ConnectApi, request: Invocation, currentApi?: () => ConnectApi | undefined, agent?: AgentDefinition): Promise<void>;
125
133
  export interface NoticeConsumer {
126
134
  reconcile(request: RuntimeRequest, api: ConnectApi): Promise<void>;
127
- deliver(notice: WorkspaceNotice, resolved: SessionProjectTarget, request: RuntimeRequest, api: ConnectApi): Promise<void>;
135
+ deliver(notice: WorkspaceNotice, resolved: SessionProjectTarget, request: RuntimeRequest, api: ConnectApi): Promise<WorkspaceNoticePresentation | void>;
128
136
  }
129
137
  export declare function sameTarget(a: ConnectTarget, b: ConnectTarget): boolean;
130
138
  export declare function connectProvider(get: () => ConnectApi | undefined, options?: {
131
139
  agent?: AgentDefinition;
132
140
  ask?: Interaction;
133
141
  notice?: NoticeConsumer;
142
+ details?: (request: RuntimeRequest, api: ConnectApi, connection: ConnectionState, target: ConnectTarget) => Promise<DetailChannel>;
134
143
  open?: (target: ConnectTarget, project: Project, request: RuntimeRequest) => Promise<Runtime>;
135
144
  }): RuntimeProvider;