@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/README.md +6 -1
- package/dist/acp.d.ts +8 -0
- package/dist/acp.js +59 -21
- package/dist/conversation-details.d.ts +47 -0
- package/dist/conversation-details.js +319 -0
- package/dist/index.js +73 -6
- package/dist/input.js +2 -2
- package/dist/mapping.d.ts +11 -5
- package/dist/mapping.js +105 -141
- package/dist/presentation.js +4 -0
- package/dist/provider.d.ts +11 -2
- package/dist/provider.js +7 -1
- package/dist/remote-details.d.ts +26 -0
- package/dist/remote-details.js +250 -0
- package/dist/workspace-notice.d.ts +22 -1
- package/dist/workspace-notice.js +148 -129
- package/package.json +3 -3
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
|
|
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)),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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 '
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
79
|
+
kind = 'tool';
|
|
80
|
+
code = 'ACP_TOOL';
|
|
81
|
+
summary = '远端工具事件';
|
|
39
82
|
break;
|
|
40
83
|
case 'usage_update':
|
|
41
|
-
|
|
84
|
+
kind = 'usage';
|
|
85
|
+
code = 'ACP_USAGE_CONTEXT';
|
|
86
|
+
summary = '远端上下文占用,不替代生成用量';
|
|
42
87
|
break;
|
|
43
|
-
case '
|
|
44
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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.
|
|
129
|
+
chunks = this.usageWarning('\n[远端用量] 本轮生成用量未提供;上下文占用不能替代输入或输出计数。\n');
|
|
102
130
|
}
|
|
103
|
-
return chunks
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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); }
|
package/dist/presentation.js
CHANGED
|
@@ -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: '登录已完成,请重新发送本轮内容。',
|
package/dist/provider.d.ts
CHANGED
|
@@ -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;
|