@ai-workbench/remote-agent 0.8.0 → 0.8.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/README.md +11 -1
- package/dist/acp.d.ts +8 -0
- package/dist/acp.js +59 -21
- package/dist/auth-reason.d.ts +1 -0
- package/dist/auth-reason.js +27 -0
- package/dist/connect-contract.d.ts +2 -1
- package/dist/conversation-details.d.ts +47 -0
- package/dist/conversation-details.js +319 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +110 -10
- package/dist/input.js +2 -2
- package/dist/login-diagnostic.d.ts +38 -0
- package/dist/login-diagnostic.js +29 -0
- package/dist/mapping.d.ts +11 -5
- package/dist/mapping.js +105 -141
- package/dist/presentation.js +4 -0
- package/dist/provider.d.ts +13 -4
- package/dist/provider.js +87 -17
- package/dist/readiness.js +7 -5
- package/dist/remote-details.d.ts +39 -0
- package/dist/remote-details.js +381 -0
- package/dist/remote.d.ts +3 -0
- package/dist/remote.js +2 -0
- package/dist/workspace-notice.d.ts +22 -1
- package/dist/workspace-notice.js +148 -129
- package/package.json +3 -3
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { validOfficialReason } from "./auth-reason.js";
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
const code = (value) => typeof value === 'string' && /^[A-Z][A-Z0-9_]*$/u.test(value) ? value : null;
|
|
4
|
+
export function loginDiagnostic(source, reason, provider, connection, state, officialReason) {
|
|
5
|
+
const value = (brand) => typeof state.logins?.[brand] === 'boolean' ? state.logins[brand] : state.logins?.[brand] === undefined ? 'absent' : 'invalid';
|
|
6
|
+
const rawReason = officialReason ?? state.providerNeeds?.[provider]?.officialReason;
|
|
7
|
+
const safeReason = typeof rawReason !== 'string' ? {} : validOfficialReason(rawReason) ? { officialReason: rawReason } : { officialReasonRejected: { bytes: Buffer.byteLength(rawReason, 'utf8'), sha256: createHash('sha256').update(rawReason).digest('hex') } };
|
|
8
|
+
return { version: 1, source, reason, provider, ...safeReason, connection: { instanceId: connection.instanceId, phase: connection.phase, epoch: connection.epoch }, login: { instanceId: state.instanceId, phase: state.phase, revision: state.revision, provider: state.provider ?? null, logins: { claude: value('claude'), codex: value('codex') }, queuedProviders: state.queuedProviders ?? [], checkingProviders: Array.isArray(state.checkingProviders) ? state.checkingProviders.filter(p => p === 'claude' || p === 'codex') : [], providerNeeds: { claude: code(state.providerNeeds?.claude?.code), codex: code(state.providerNeeds?.codex?.code) }, code: code(state.technicalDetails?.code) } };
|
|
9
|
+
}
|
|
10
|
+
export function loginReason(state, connection, provider) {
|
|
11
|
+
if (state.instanceId !== connection.instanceId)
|
|
12
|
+
return 'LOGIN_INSTANCE_MISMATCH';
|
|
13
|
+
if (state.phase === 'blocked')
|
|
14
|
+
return 'LOGIN_PHASE_BLOCKED';
|
|
15
|
+
if (state.phase === 'disposed')
|
|
16
|
+
return 'LOGIN_PHASE_DISPOSED';
|
|
17
|
+
if (state.queuedProviders?.includes(provider))
|
|
18
|
+
return 'LOGIN_QUEUED';
|
|
19
|
+
if (state.checkingProviders.includes(provider))
|
|
20
|
+
return 'LOGIN_CHECKING';
|
|
21
|
+
if (state.phase === 'authorizing' && state.provider === provider)
|
|
22
|
+
return 'LOGIN_AUTHORIZING';
|
|
23
|
+
if (state.providerNeeds[provider])
|
|
24
|
+
return 'LOGIN_PROVIDER_NEED';
|
|
25
|
+
if (typeof state.logins?.[provider] !== 'boolean')
|
|
26
|
+
return 'LOGIN_VALUE_INVALID';
|
|
27
|
+
if (!state.logins[provider])
|
|
28
|
+
return 'LOGIN_REQUIRED';
|
|
29
|
+
}
|
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
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { AgentApp } from '@agentclientprotocol/sdk';
|
|
2
2
|
import type { AgentApp as ExtendedAgentApp } from '@agentclientprotocol/sdk/unstable';
|
|
3
|
-
import type { ConnectState, ConnectApi as LConnectApi, ConnectionState, LoginPromptState } from '@ai-workbench/connect/api';
|
|
3
|
+
import type { ConnectState, ConnectNeed, ConnectApi as LConnectApi, ConnectionState, LoginPromptState } from '@ai-workbench/connect/api';
|
|
4
4
|
export type { ConnectionState, LoginPromptState } from '@ai-workbench/connect/api';
|
|
5
5
|
import type { OneShotRuntime } from './job.ts';
|
|
6
6
|
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';
|
|
@@ -77,10 +85,10 @@ type Result<T> = {
|
|
|
77
85
|
code: string;
|
|
78
86
|
message: string;
|
|
79
87
|
requires: string;
|
|
80
|
-
}
|
|
88
|
+
} & Pick<ConnectNeed, 'officialReason'>;
|
|
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, 'onConnectionState' | 'onLoginPromptState' | '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;
|