@ai-workbench/remote-agent 0.8.18 → 0.8.21
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 +47 -11
- package/dist/acp.d.ts +5 -0
- package/dist/acp.js +34 -7
- package/dist/approval-label.d.ts +2 -1
- package/dist/approval-label.js +9 -1
- package/dist/claude-local.js +3 -2
- package/dist/client.js +3 -2
- package/dist/close-diagnostics.d.ts +1 -1
- package/dist/conversation-details.js +12 -7
- package/dist/employee-endpoint.d.ts +2 -0
- package/dist/employee-endpoint.js +43 -10
- package/dist/employee-exec.d.ts +5 -0
- package/dist/employee-exec.js +12 -2
- package/dist/employee-info.d.ts +15 -0
- package/dist/employee-info.js +38 -0
- package/dist/employee-mcp.js +1 -1
- package/dist/employee-provider.js +5 -4
- package/dist/employee-reasons.d.ts +4 -0
- package/dist/employee-reasons.js +61 -0
- package/dist/failure-evidence.d.ts +2 -0
- package/dist/failure-evidence.js +14 -3
- package/dist/host-workspace.js +5 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +23 -9
- package/dist/input.d.ts +11 -0
- package/dist/input.js +48 -5
- package/dist/mapping.d.ts +5 -0
- package/dist/mapping.js +31 -2
- package/dist/presentation.js +1 -1
- package/dist/provider.d.ts +5 -0
- package/dist/provider.js +3 -2
- package/dist/stage-timing.d.ts +4 -0
- package/dist/stage-timing.js +27 -5
- package/locale/en.json +2 -2
- package/locale/zh.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Optional protocol information only. Malformed metadata cannot stop a turn. */
|
|
2
|
+
export interface EmployeeExecutionInfo {
|
|
3
|
+
version: 1;
|
|
4
|
+
httpMcpPolicy: 'reject' | 'ignore';
|
|
5
|
+
ignoredStdio: string[];
|
|
6
|
+
ignoredHttp: string[];
|
|
7
|
+
ignoredNativeHttp: string[];
|
|
8
|
+
ignoredStdioUnshown: number;
|
|
9
|
+
ignoredHttpUnshown: number;
|
|
10
|
+
ignoredNativeHttpUnshown: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function employeeExecutionInfo(value: unknown): EmployeeExecutionInfo | undefined;
|
|
13
|
+
export declare function employeeMcpSummary(info: EmployeeExecutionInfo): string;
|
|
14
|
+
/** Employee-facing execution fact; protocol terms and stdio names remain in details. */
|
|
15
|
+
export declare function employeeHttpNotice(info: EmployeeExecutionInfo, sanitizeLabel: (name: string) => string, showUnshown: boolean): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
2
|
+
const safeName = (value) => typeof value === 'string' && value.length > 0 && !/[\p{C}\r\n]/u.test(value);
|
|
3
|
+
export function employeeExecutionInfo(value) {
|
|
4
|
+
if (!object(value) || value.version !== 1 || (value.httpMcpPolicy !== 'reject' && value.httpMcpPolicy !== 'ignore'))
|
|
5
|
+
return;
|
|
6
|
+
const lists = {};
|
|
7
|
+
for (const key of ['ignoredStdio', 'ignoredHttp', 'ignoredNativeHttp']) {
|
|
8
|
+
const entries = value[key];
|
|
9
|
+
if (!Array.isArray(entries))
|
|
10
|
+
return;
|
|
11
|
+
lists[key] = entries.filter(safeName);
|
|
12
|
+
const unshown = value[key + 'Unshown'];
|
|
13
|
+
lists[key + 'Unshown'] = (Number.isSafeInteger(unshown) && Number(unshown) >= 0 ? Number(unshown) : 0) + entries.filter(entry => !safeName(entry)).length;
|
|
14
|
+
}
|
|
15
|
+
return { version: 1, httpMcpPolicy: value.httpMcpPolicy, ...lists };
|
|
16
|
+
}
|
|
17
|
+
// Code-span delimiter exceeds every backtick run, so names cannot inject Markdown/HTML.
|
|
18
|
+
const quoted = (names) => names.map(name => { let longest = 0; for (const run of name.matchAll(/`+/g))
|
|
19
|
+
longest = Math.max(longest, run[0].length); const delimiter = '`'.repeat(longest + 1); return delimiter + ' ' + name + ' ' + delimiter; }).join('、');
|
|
20
|
+
export function employeeMcpSummary(info) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
for (const [key, label] of [['ignoredStdio', '已忽略本机 stdio MCP'], ['ignoredHttp', info.httpMcpPolicy === 'ignore' ? '已按设置忽略本机 HTTP MCP' : '本机 HTTP MCP 未获支持,当前设置为拒绝'], ['ignoredNativeHttp', '已按原生能力忽略本机 HTTP MCP']]) {
|
|
23
|
+
const names = info[key], unshown = info[key + 'Unshown'];
|
|
24
|
+
if (names.length || unshown)
|
|
25
|
+
parts.push(label + ': ' + [...(names.length ? [quoted(names)] : []), ...(unshown ? [`${unshown} 个名称含不可显示内容`] : [])].join(';'));
|
|
26
|
+
}
|
|
27
|
+
return parts.join('。');
|
|
28
|
+
}
|
|
29
|
+
/** Employee-facing execution fact; protocol terms and stdio names remain in details. */
|
|
30
|
+
export function employeeHttpNotice(info, sanitizeLabel, showUnshown) {
|
|
31
|
+
const rawNames = [...new Set([...(info.httpMcpPolicy === 'ignore' ? info.ignoredHttp : []), ...info.ignoredNativeHttp])];
|
|
32
|
+
const names = rawNames.filter(name => sanitizeLabel(name) === name && !name.includes('[已脱敏:'));
|
|
33
|
+
const unshown = showUnshown && (rawNames.length - names.length + (info.httpMcpPolicy === 'ignore' ? info.ignoredHttpUnshown : 0) + info.ignoredNativeHttpUnshown) > 0;
|
|
34
|
+
if (!names.length)
|
|
35
|
+
return unshown ? '你电脑上的一项本地扩展本次不可用,详情见技术详情。' : '';
|
|
36
|
+
const named = names.map(name => '『' + quoted([name]) + '』').join('、');
|
|
37
|
+
return '你电脑上的' + named + '扩展本次不可用。' + (unshown ? '另有本地扩展本次不可用,详情见技术详情。' : '');
|
|
38
|
+
}
|
package/dist/employee-mcp.js
CHANGED
|
@@ -88,7 +88,7 @@ export async function openEmployeeMcp(options) {
|
|
|
88
88
|
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1)
|
|
89
89
|
throw new NeedError('SETTINGS_REQUIRED', '本机工具消息容量无效。', 'acpMaxMessageBytes');
|
|
90
90
|
await access('/usr/bin/nc', constants.X_OK);
|
|
91
|
-
const tools = createEmployeeTools({ closeDiagnostics: options.closeDiagnostics, cwd: options.lease.target.local.directory.path, shell: options.lease.target.endpoint.endpointShell, uid: options.lease.target.endpoint.remoteUid, maxBytes: options.maxBytes, killGraceMs: options.killGraceMs, detail: observe, runShell: options.runShell, validate: signal => options.lease.
|
|
91
|
+
const tools = createEmployeeTools({ closeDiagnostics: options.closeDiagnostics, cwd: options.lease.target.local.directory.path, shell: options.lease.target.endpoint.endpointShell, uid: options.lease.target.endpoint.remoteUid, maxBytes: options.maxBytes, killGraceMs: options.killGraceMs, detail: observe, runShell: options.runShell, validate: signal => options.lease.validateDirectory(signal).then(() => { }) });
|
|
92
92
|
const root = await mkdtemp('/private/tmp/aw-x87-'), socketPath = root + '/mcp';
|
|
93
93
|
try {
|
|
94
94
|
const info = await lstat(root);
|
|
@@ -210,7 +210,7 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
210
210
|
const scopedIo = { run: (command, signal) => io.run(command, signal), attach: async (command) => {
|
|
211
211
|
const observe = closeDiagnostics.capture();
|
|
212
212
|
const child = await io.attach(command);
|
|
213
|
-
child.once('close', (code, signal) => { observe({ layer: 'relay', initiator: 'ssh.process.close', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'exit', exitCode: code, signal }); if (candidate && !candidate.ended)
|
|
213
|
+
child.once('close', (code, signal) => { observe({ layer: 'relay', initiator: 'ssh.process.close', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'exit', exitCode: code, signal }); if (candidate && !candidate.ended && !candidate.execution.remoteClose)
|
|
214
214
|
void candidate.dispose(fail('EMPLOYEE_EXEC_UNKNOWN'), { layer: 'runtime', initiator: 'provider.child-close', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'dispose' }).catch(() => { }); });
|
|
215
215
|
return child;
|
|
216
216
|
} };
|
|
@@ -255,7 +255,8 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
255
255
|
}
|
|
256
256
|
const disposers = [];
|
|
257
257
|
const execution = new EmployeeExecution({ closeDiagnostics, bindingId: admitted.bindingId, acpMaxMessageBytes: request.acpMaxMessageBytes, validate, transport: signal => createEmployeeSshTransport(ssh, signal) });
|
|
258
|
-
const runtime = { ...host, closeDiagnostics, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, saveCheckpoint: undefined, recoveredSession: undefined, employeeExecutionMeta: { version: 1, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, cwd: admitted.cwd, acpMaxMessageBytes: request.acpMaxMessageBytes }, employeePresentation: CODEX_EMPLOYEE_SCOPE_NOTICE };
|
|
258
|
+
const runtime = { ...host, closeDiagnostics, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, saveCheckpoint: undefined, recoveredSession: undefined, employeeExecutionMeta: { version: 1, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, cwd: admitted.cwd, acpMaxMessageBytes: request.acpMaxMessageBytes, httpMcpPolicy: request.employeeHttpMcpPolicy ?? 'ignore' }, employeePresentation: CODEX_EMPLOYEE_SCOPE_NOTICE };
|
|
259
|
+
runtime.employeeRemoteCloseReason = () => execution.remoteClose?.reason;
|
|
259
260
|
entry = { scope, runtime, current: input, details, execution, network, ended: false, dispose: (reason, cause = { layer: 'runtime', initiator: 'provider.dispose', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', action: 'dispose' }) => {
|
|
260
261
|
closeDiagnostics.record({ ...cause, changes: cause.changes ?? [{ field: 'provider.ended', before: entry.ended, after: true }] });
|
|
261
262
|
if (entry.closing)
|
|
@@ -310,13 +311,13 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
310
311
|
runtime.employeeExecution = async (params, signal) => {
|
|
311
312
|
try {
|
|
312
313
|
const result = await execution.handle(params, signal);
|
|
313
|
-
if (result.closed)
|
|
314
|
+
if (result.closed && !execution.remoteClose)
|
|
314
315
|
void entry.dispose(undefined, { layer: 'runtime', initiator: 'provider.rpc-closed-result', reason: 'EMPLOYEE_EXEC_CLOSED', operation: typeof params === 'object' && params && 'operation' in params && typeof params.operation === 'string' ? params.operation : 'unknown', action: 'dispose' }).catch(cleanup => closeDiagnostics.record({ layer: 'runtime', initiator: 'provider.rpc-cleanup', reason: closeCode(cleanup, 'REMOTE_CLEANUP_UNCONFIRMED'), action: 'failure' }));
|
|
315
316
|
return { ...result };
|
|
316
317
|
}
|
|
317
318
|
catch (error) {
|
|
318
319
|
// EmployeeExecution reserves ABORTED for an unconsumed read wait. Lost responses are UNKNOWN.
|
|
319
|
-
if (!(params && typeof params === 'object' && 'operation' in params && params.operation === 'read' && errorCode(error) === 'ABORTED'))
|
|
320
|
+
if (!execution.remoteClose && !(params && typeof params === 'object' && 'operation' in params && params.operation === 'read' && errorCode(error) === 'ABORTED'))
|
|
320
321
|
void entry.dispose(asNeed(error), { layer: 'runtime', initiator: 'provider.rpc-failure', reason: closeCode(error), action: 'dispose' }).catch(cleanup => closeDiagnostics.record({ layer: 'runtime', initiator: 'provider.rpc-cleanup', reason: closeCode(cleanup, 'REMOTE_CLEANUP_UNCONFIRMED'), action: 'failure' }));
|
|
321
322
|
throw withFirstFailureEvidence(error, closeDiagnostics.first?.reason, errorCode(error));
|
|
322
323
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Approved protocol codes only; remote exception text is never a reason. */
|
|
2
|
+
export declare const employeeAdapterCodes: Set<string>;
|
|
3
|
+
export declare const employeeOpenCauseCodes: Set<string>;
|
|
4
|
+
export declare function trustedEmployeeReason(value: unknown): string | undefined;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Approved protocol codes only; remote exception text is never a reason. */
|
|
2
|
+
export const employeeAdapterCodes = new Set([
|
|
3
|
+
"AW_EMPLOYEE_ADDITIONAL_DIRECTORIES_UNSUPPORTED",
|
|
4
|
+
"AW_EMPLOYEE_SLASH_COMMAND_UNSUPPORTED",
|
|
5
|
+
"AW_EMPLOYEE_HOST_FILE_REPORT_UNSUPPORTED",
|
|
6
|
+
"AW_EMPLOYEE_CONFIG_DISCOVERY_OVERRIDE_UNSUPPORTED",
|
|
7
|
+
"AW_EMPLOYEE_CONFIG_ADMISSION_INVALID",
|
|
8
|
+
"AW_EMPLOYEE_CONFIG_ADMISSION_FAILED",
|
|
9
|
+
"AW_EMPLOYEE_HOST_PROJECT_CONFIG_UNSUPPORTED",
|
|
10
|
+
"AW_EMPLOYEE_EXECUTOR_MCP_UNSUPPORTED",
|
|
11
|
+
"AW_EMPLOYEE_EXECUTOR_CONFIG_PROTOCOL_INVALID",
|
|
12
|
+
"AW_EMPLOYEE_EXECUTOR_CONFIG_READ_FAILED",
|
|
13
|
+
"AW_EMPLOYEE_AGENT_CONFIG_UNAVAILABLE",
|
|
14
|
+
"AW_EMPLOYEE_BINDING_INVALID",
|
|
15
|
+
"AW_EMPLOYEE_BINDING_MISMATCH",
|
|
16
|
+
"AW_EMPLOYEE_CAPABILITY_REQUIRED",
|
|
17
|
+
"AW_EMPLOYEE_CLOSE_FAILED",
|
|
18
|
+
"AW_EMPLOYEE_CLOSE_RESPONSE_INVALID",
|
|
19
|
+
"AW_EMPLOYEE_COLD_RESUME_UNSUPPORTED",
|
|
20
|
+
"AW_EMPLOYEE_CWD_MISMATCH",
|
|
21
|
+
"AW_EMPLOYEE_ENVIRONMENT_REGISTRATION_FAILED",
|
|
22
|
+
"AW_EMPLOYEE_ENVIRONMENT_REQUIRED",
|
|
23
|
+
"AW_EMPLOYEE_EXECUTION_CLOSED",
|
|
24
|
+
"AW_EMPLOYEE_EXECUTION_ID_REQUIRED",
|
|
25
|
+
"AW_EMPLOYEE_HOST_AUXILIARY_CONFIG_UNSUPPORTED",
|
|
26
|
+
"AW_EMPLOYEE_HOST_AUXILIARY_UNSUPPORTED",
|
|
27
|
+
"AW_EMPLOYEE_HOST_FALLBACK_FORBIDDEN",
|
|
28
|
+
"AW_EMPLOYEE_MCP_UNSUPPORTED",
|
|
29
|
+
"AW_EMPLOYEE_MESSAGE_INVALID",
|
|
30
|
+
"AW_EMPLOYEE_MESSAGE_TOO_LARGE",
|
|
31
|
+
"AW_EMPLOYEE_OPEN_RESPONSE_INVALID",
|
|
32
|
+
"AW_EMPLOYEE_READ_MESSAGE_INVALID",
|
|
33
|
+
"AW_EMPLOYEE_READ_RESPONSE_INVALID",
|
|
34
|
+
"AW_EMPLOYEE_RELAY_ADDRESS_INVALID",
|
|
35
|
+
"AW_EMPLOYEE_RELAY_FAILED",
|
|
36
|
+
"AW_EMPLOYEE_REQUEST_INVALID",
|
|
37
|
+
"AW_EMPLOYEE_SESSION_UNKNOWN",
|
|
38
|
+
"AW_EMPLOYEE_WIRE_FAILED",
|
|
39
|
+
"AW_EMPLOYEE_WRITE_RESPONSE_INVALID"
|
|
40
|
+
]);
|
|
41
|
+
export const employeeOpenCauseCodes = new Set([
|
|
42
|
+
"AW_EMPLOYEE_BINARY_UNAVAILABLE",
|
|
43
|
+
"AW_EMPLOYEE_BINARY_HASH_FAILED",
|
|
44
|
+
"AW_EMPLOYEE_BINARY_HASH_MISMATCH",
|
|
45
|
+
"AW_EMPLOYEE_BINARY_VERSION_FAILED",
|
|
46
|
+
"AW_EMPLOYEE_BINARY_VERSION_MISMATCH",
|
|
47
|
+
"EMPLOYEE_CONTEXT_CHANGED",
|
|
48
|
+
"EMPLOYEE_ENDPOINT_INVALID",
|
|
49
|
+
"EMPLOYEE_BINARY_REQUIRED",
|
|
50
|
+
"EMPLOYEE_EXECUTION_UNSUPPORTED",
|
|
51
|
+
"EMPLOYEE_EXEC_PROTOCOL_INVALID",
|
|
52
|
+
"EMPLOYEE_EXEC_IDENTITY_MISMATCH",
|
|
53
|
+
"EMPLOYEE_EXEC_CLOSED",
|
|
54
|
+
"EMPLOYEE_EXEC_UNKNOWN",
|
|
55
|
+
"EMPLOYEE_EXEC_ALREADY_OPEN",
|
|
56
|
+
"EMPLOYEE_EXEC_READ_BUSY",
|
|
57
|
+
"ACP_MESSAGE_TOO_LARGE",
|
|
58
|
+
"ABORTED",
|
|
59
|
+
"SETTINGS_REQUIRED"
|
|
60
|
+
]);
|
|
61
|
+
export function trustedEmployeeReason(value) { return typeof value === 'string' && (employeeAdapterCodes.has(value) || employeeOpenCauseCodes.has(value)) ? value : undefined; }
|
|
@@ -6,6 +6,8 @@ export declare function closeErrorCodes(error: unknown, fallback?: string): stri
|
|
|
6
6
|
export declare function closeFailure(code: string, message: string, requires: string, codes: readonly string[]): NeedError;
|
|
7
7
|
/** Saving succeeded: retain the original trusted error type, code and presentation. */
|
|
8
8
|
export declare function withFirstFailureEvidence<T>(primary: T, firstReason: unknown, primaryCode: string): T;
|
|
9
|
+
/** A negotiated close reason is independent evidence, never a replacement error or new gate. */
|
|
10
|
+
export declare function withRemoteFailureEvidence<T>(primary: T, remoteReason: unknown, primaryCode: string): T;
|
|
9
11
|
/** A known turn failure is final even while its original resource is still closing. */
|
|
10
12
|
export declare function withPendingCleanupEvidence<T>(primary: T, firstReason: unknown, primaryCode: string): T;
|
|
11
13
|
/** Preserve causal roles without copying arbitrary exception text into employee output. */
|
package/dist/failure-evidence.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NeedError } from "./remote.js";
|
|
2
|
+
import { trustedEmployeeReason } from "./employee-reasons.js";
|
|
2
3
|
/** Only typed codes, never exception messages, causes, stderr or RPC data. */
|
|
3
4
|
export function closeCode(error, fallback = 'CLOSE_REASON_UNKNOWN') {
|
|
4
5
|
return error instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(error.code) ? error.code : fallback;
|
|
@@ -58,7 +59,16 @@ function retainedEvidence(error) {
|
|
|
58
59
|
export function withFirstFailureEvidence(primary, firstReason, primaryCode) {
|
|
59
60
|
if (primary && typeof primary === 'object' && safeCode(firstReason) && safeCode(primaryCode)) {
|
|
60
61
|
const previous = retainedEvidence(primary);
|
|
61
|
-
evidence.set(primary, Object.freeze({ first: previous?.first ?? firstReason, original: previous?.original ?? Object.freeze([primaryCode]), diagnostic: previous?.diagnostic ?? Object.freeze([]), cleanup: previous?.cleanup ?? Object.freeze([]) }));
|
|
62
|
+
evidence.set(primary, Object.freeze({ ...previous, first: previous?.first ?? firstReason, original: previous?.original ?? Object.freeze([primaryCode]), diagnostic: previous?.diagnostic ?? Object.freeze([]), cleanup: previous?.cleanup ?? Object.freeze([]) }));
|
|
63
|
+
}
|
|
64
|
+
return primary;
|
|
65
|
+
}
|
|
66
|
+
/** A negotiated close reason is independent evidence, never a replacement error or new gate. */
|
|
67
|
+
export function withRemoteFailureEvidence(primary, remoteReason, primaryCode) {
|
|
68
|
+
const remote = trustedEmployeeReason(remoteReason);
|
|
69
|
+
if (primary && typeof primary === 'object' && remote) {
|
|
70
|
+
const previous = retainedEvidence(primary);
|
|
71
|
+
evidence.set(primary, Object.freeze({ ...previous, remote: previous?.remote ?? remote, original: previous?.original ?? Object.freeze([safeCode(primaryCode) ? primaryCode : 'TURN_FAILED']), diagnostic: previous?.diagnostic ?? Object.freeze([]), cleanup: previous?.cleanup ?? Object.freeze([]) }));
|
|
62
72
|
}
|
|
63
73
|
return primary;
|
|
64
74
|
}
|
|
@@ -75,7 +85,8 @@ export function withFailureEvidence(primary, secondary, kind, firstReason, prima
|
|
|
75
85
|
const previous = retainedEvidence(primary), first = previous?.first ?? (safeCode(firstReason) ? firstReason : undefined);
|
|
76
86
|
const original = previous?.original ?? (safeCode(primaryCode) ? [primaryCode] : closeErrorCodes(primary, 'TURN_FAILED'));
|
|
77
87
|
const secondaryFacts = secondary.map(retainedEvidence), added = [...new Set(secondary.flatMap((error, index) => secondaryFacts[index] ? secondaryFacts[index].original.filter(code => !original.includes(code)) : closeErrorCodes(error, kind === 'diagnostic' ? 'DIAGNOSTIC_WRITE_FAILED' : 'REMOTE_CLEANUP_UNCONFIRMED')))];
|
|
78
|
-
const
|
|
88
|
+
const remote = previous?.remote ?? secondaryFacts.find(f => f?.remote)?.remote;
|
|
89
|
+
const facts = Object.freeze({ ...(first ? { first } : {}), ...(remote ? { remote } : {}), original: Object.freeze(original), diagnostic: Object.freeze([...new Set([...(previous?.diagnostic ?? []), ...secondaryFacts.flatMap(f => f?.diagnostic ?? []), ...(kind === 'diagnostic' ? added : [])])]), cleanup: Object.freeze([...new Set([...(previous?.cleanup ?? []), ...secondaryFacts.flatMap(f => f?.cleanup ?? []), ...(kind === 'cleanup' ? added : [])])]) });
|
|
79
90
|
const code = kind === 'cleanup' ? 'REMOTE_CLEANUP_UNCONFIRMED' : !secondary.length ? primary instanceof NeedError ? primary.code : safeCode(primaryCode) ? primaryCode : 'TURN_FAILED' : primary instanceof NeedError && facts.diagnostic.length > 0 && facts.diagnostic.every(code => code === primary.code) && !facts.cleanup.length && !previous ? primary.code : 'CONVERSATION_DETAILS_FAILED';
|
|
80
91
|
const failure = Object.assign(new NeedError(code, '本轮已停止;运行原因与诊断或清理结果分别保留。', '核对原始原因及未确认的保存/清理结果'), { cause: new AggregateError([primary, ...(safeCode(primaryCode) && !(primary instanceof NeedError) ? [new NeedError(primaryCode, '运行错误代码', '可信调用点提供的固定代码')] : []), ...secondary, ...(first ? [new NeedError(first, '首次观察关闭代码', '不是已证明根因')] : [])], 'TURN_FAILURE_EVIDENCE') });
|
|
81
92
|
evidence.set(failure, facts);
|
|
@@ -87,5 +98,5 @@ export function failureEvidenceSummary(error) {
|
|
|
87
98
|
const facts = retainedEvidence(error);
|
|
88
99
|
if (!facts)
|
|
89
100
|
return '';
|
|
90
|
-
return '技术详情:' + [...(facts.first ? ['首次观察关闭 ' + facts.first] : []), '运行原因 ' + facts.original.join('、'), ...(facts.diagnostic.length ? ['详情保存未确认 ' + facts.diagnostic.join('、')] : []), ...(facts.cleanup.length ? ['清理未确认 ' + facts.cleanup.join('、')] : facts.cleanupPending ? ['正在确认主机清理,原资源清理结果尚待确认'] : [])].join(';');
|
|
101
|
+
return '技术详情:' + [...(facts.first ? ['首次观察关闭 ' + facts.first] : []), '运行原因 ' + facts.original.join('、'), ...(facts.remote ? ['远端关闭原因 ' + facts.remote] : []), ...(facts.diagnostic.length ? ['详情保存未确认 ' + facts.diagnostic.join('、')] : []), ...(facts.cleanup.length ? ['清理未确认 ' + facts.cleanup.join('、')] : facts.cleanupPending ? ['正在确认主机清理,原资源清理结果尚待确认'] : [])].join(';');
|
|
91
102
|
}
|
package/dist/host-workspace.js
CHANGED
|
@@ -96,8 +96,12 @@ export async function hostWorkspaceGuard(get, api, connection, target, request,
|
|
|
96
96
|
return value;
|
|
97
97
|
}
|
|
98
98
|
catch (error) {
|
|
99
|
-
if (!request.signal?.aborted)
|
|
99
|
+
if (!request.signal?.aborted) {
|
|
100
|
+
const cause = revoked.signal.reason;
|
|
100
101
|
revoke(error);
|
|
102
|
+
if (cause instanceof NeedError)
|
|
103
|
+
throw cause;
|
|
104
|
+
}
|
|
101
105
|
throw error;
|
|
102
106
|
}
|
|
103
107
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai
|
|
|
5
5
|
import { type AcpHooks } from './acp.ts';
|
|
6
6
|
import { type RuntimeProvider } from './provider.ts';
|
|
7
7
|
import { type AgentDefinition } from './agents.ts';
|
|
8
|
-
import { type ImageReader } from './input.ts';
|
|
8
|
+
import { type ImageReader, type HostWorkspacePresentation } from './input.ts';
|
|
9
9
|
import type { LoginDiagnostic } from './login-diagnostic.ts';
|
|
10
10
|
import { type ModelDirectory, type HostModel } from './model-catalog.ts';
|
|
11
11
|
import { type PreflightDiagnostics } from './preflight-diagnostic.ts';
|
|
@@ -21,8 +21,10 @@ export declare const DEFAULT_CONFIG: Readonly<{
|
|
|
21
21
|
readonly oneShotSandbox: "workspace-write";
|
|
22
22
|
readonly oneShotApproval: "never";
|
|
23
23
|
readonly employeeShellKillGraceMs: 1000;
|
|
24
|
+
readonly employeeHttpMcpPolicy: "ignore";
|
|
24
25
|
}>;
|
|
25
26
|
export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
27
|
+
employeeHttpMcpPolicy: Schema<"reject" | "ignore", "reject" | "ignore", "volatile-defined">;
|
|
26
28
|
mode: Schema<"session" | "one-shot", "session" | "one-shot", "volatile-defined">;
|
|
27
29
|
acpMaxMessageBytes: Schema<number, number, "volatile-defined">;
|
|
28
30
|
brokerMaxFrameBytes: Schema<number, number, "volatile-defined">;
|
|
@@ -31,6 +33,7 @@ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
|
31
33
|
oneShotApproval: Schema<"never", "never", "volatile-defined">;
|
|
32
34
|
employeeShellKillGraceMs: Schema<number, number, "volatile-defined">;
|
|
33
35
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
36
|
+
employeeHttpMcpPolicy: Schema<"reject" | "ignore", "reject" | "ignore", "volatile-defined">;
|
|
34
37
|
mode: Schema<"session" | "one-shot", "session" | "one-shot", "volatile-defined">;
|
|
35
38
|
acpMaxMessageBytes: Schema<number, number, "volatile-defined">;
|
|
36
39
|
brokerMaxFrameBytes: Schema<number, number, "volatile-defined">;
|
|
@@ -47,6 +50,7 @@ export interface PluginConfig {
|
|
|
47
50
|
oneShotSandbox?: 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
48
51
|
oneShotApproval?: 'never';
|
|
49
52
|
employeeShellKillGraceMs?: number;
|
|
53
|
+
employeeHttpMcpPolicy?: 'reject' | 'ignore';
|
|
50
54
|
}
|
|
51
55
|
type LiveConfig = {
|
|
52
56
|
[K in keyof PluginConfig]: PluginConfig[K] | {
|
|
@@ -150,6 +154,6 @@ export declare class RemoteAgentsAdapter extends LlmAdapter {
|
|
|
150
154
|
dispose(): Promise<void>;
|
|
151
155
|
}
|
|
152
156
|
export declare function apply(ctx: Context, live: LiveConfig): void;
|
|
153
|
-
type InvocationApproval = (request: Parameters<NonNullable<AcpHooks['approve']>>[0], signal: AbortSignal | undefined, options: GenerateOptions, context?: string) => ReturnType<NonNullable<AcpHooks['approve']>>;
|
|
157
|
+
type InvocationApproval = (request: Parameters<NonNullable<AcpHooks['approve']>>[0], signal: AbortSignal | undefined, options: GenerateOptions, context?: string, hostWorkspace?: HostWorkspacePresentation) => ReturnType<NonNullable<AcpHooks['approve']>>;
|
|
154
158
|
export declare function dshApproval(ctx: Context): InvocationApproval;
|
|
155
159
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { connectNeedMessage } from "./connect-need.js";
|
|
2
2
|
import { CloseDiagnostics, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, closeCode, closeErrorCodes, closeFailure } from "./close-diagnostics.js";
|
|
3
|
-
import { withPendingCleanupEvidence } from "./failure-evidence.js";
|
|
3
|
+
import { withPendingCleanupEvidence, withRemoteFailureEvidence } from "./failure-evidence.js";
|
|
4
4
|
import { runCatalog } from "./catalog-ssh.js";
|
|
5
5
|
import { createCatalogDiagnostics, CatalogDiagnosticsRemote } from "./catalog-diagnostics.js";
|
|
6
6
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
@@ -17,7 +17,7 @@ import { StreamMapper } from "./mapping.js";
|
|
|
17
17
|
import { connectProvider } from "./provider.js";
|
|
18
18
|
import { brokerFactory } from "./broker.js";
|
|
19
19
|
import { AGENTS, CODEX_AGENT } from "./agents.js";
|
|
20
|
-
import { prepareInput, prepareContinuation } from "./input.js";
|
|
20
|
+
import { prepareInput, prepareContinuation, projectHostWorkspaceInput } from "./input.js";
|
|
21
21
|
import { createWorkspaceNoticeConsumer, isWorkspaceNoticeMessage } from "./workspace-notice.js";
|
|
22
22
|
import { NeedError } from "./remote.js";
|
|
23
23
|
import { employeeFailure } from "./presentation.js";
|
|
@@ -45,8 +45,9 @@ export const settingsNamespace = 'ai-workbench-codex';
|
|
|
45
45
|
// L can be installed/enabled after X; keeping X mounted lets it explain the prerequisite.
|
|
46
46
|
export const inject = ['llm'];
|
|
47
47
|
/** Owner-approved defaults; administrators may override through the existing settings. */
|
|
48
|
-
export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never', employeeShellKillGraceMs: 1000 });
|
|
48
|
+
export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never', employeeShellKillGraceMs: 1000, employeeHttpMcpPolicy: 'ignore' });
|
|
49
49
|
export const Config = Schema.object({
|
|
50
|
+
employeeHttpMcpPolicy: Schema.union([Schema.const('reject').i18n({ zh: '拒绝本机 HTTP MCP', en: '拒绝本机 HTTP MCP' }), Schema.const('ignore').i18n({ zh: '忽略本机 HTTP MCP 后继续(默认)', en: '忽略本机 HTTP MCP 后继续(默认)' })]).i18n({ zh: '本机 HTTP MCP 策略;默认忽略;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略,名称仅在技术详情说明;HTTP 忽略在正文按扩展名称提示', en: '本机 HTTP MCP 策略;默认忽略;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略,名称仅在技术详情说明;HTTP 忽略在正文按扩展名称提示' }).default(DEFAULT_CONFIG.employeeHttpMcpPolicy).volatile(),
|
|
50
51
|
mode: Schema.union([Schema.const('session').i18n({ en: 'Session', zh: '会话模式' }), Schema.const('one-shot').i18n({ en: 'One-shot', zh: '单次模式' })]).i18n({ en: 'Run mode; start a new session after switching modes', zh: 'Codex运行模式;Claude固定会话模式;切换后请新建会话' }).default(DEFAULT_CONFIG.mode).volatile(),
|
|
51
52
|
acpMaxMessageBytes: Schema.number().min(1).step(1).i18n({ en: 'Session message limit; default 4 MiB, adjustable by administrators', zh: '会话消息容量;默认4 MiB,管理员可调整' }).default(DEFAULT_CONFIG.acpMaxMessageBytes).volatile(),
|
|
52
53
|
brokerMaxFrameBytes: Schema.number().min(1).step(1).i18n({ en: 'Host output chunk limit; default 2 MiB, adjustable by administrators', zh: '主机回传单段容量;默认2 MiB,管理员可调整' }).default(DEFAULT_CONFIG.brokerMaxFrameBytes).volatile(),
|
|
@@ -340,7 +341,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
340
341
|
let turnFailure;
|
|
341
342
|
let employeeNoticeShown = false;
|
|
342
343
|
const information = [];
|
|
343
|
-
const recordInformation = () => { if (runtime?.details)
|
|
344
|
+
const recordInformation = () => { timing.summarize(); if (runtime?.details)
|
|
344
345
|
for (const entry of information.splice(0))
|
|
345
346
|
runtime.details.record(entry); };
|
|
346
347
|
const config = this.config();
|
|
@@ -386,7 +387,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
386
387
|
const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
|
|
387
388
|
if (live && live.configuration !== configuration)
|
|
388
389
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
|
|
389
|
-
Object.assign(request, { timing, acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, recoveryCheckpointId: previous?.recoveryCheckpointId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, retainProcess: !!key && mode === 'session', signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) });
|
|
390
|
+
Object.assign(request, { timing, employeeHttpMcpPolicy: config.employeeHttpMcpPolicy ?? DEFAULT_CONFIG.employeeHttpMcpPolicy, acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, recoveryCheckpointId: previous?.recoveryCheckpointId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, retainProcess: !!key && mode === 'session', signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) });
|
|
390
391
|
if (requireFreshSession) {
|
|
391
392
|
request.requireFreshSession = true;
|
|
392
393
|
request.resumeSessionId = undefined;
|
|
@@ -487,12 +488,17 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
487
488
|
if (contextChanged)
|
|
488
489
|
input = await timing.run('input-prepare', () => prepareContinuation(options, input, this.readImage));
|
|
489
490
|
}
|
|
491
|
+
input = projectHostWorkspaceInput(input, runtime.hostWorkspace);
|
|
492
|
+
if (runtime.hostWorkspace && mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
|
|
493
|
+
throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
|
|
490
494
|
const details = runtime.details;
|
|
491
495
|
if (!details)
|
|
492
496
|
throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
|
|
493
497
|
recordInformation();
|
|
494
498
|
timing.bind(detail => details.record(detail));
|
|
495
499
|
request.recordEarlyFailure = undefined;
|
|
500
|
+
if (runtime.hostWorkspace)
|
|
501
|
+
details.record({ kind: 'session', severity: 'info', code: 'HOST_WORKSPACE_CONTEXT', summary: '本轮模型工作区上下文及审批会话目录使用已确认的主机目录。', payload: { source: 'host', cwd: runtime.hostWorkspace.cwd } });
|
|
496
502
|
const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (runtime.restartNotice ? undefined : live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
|
|
497
503
|
activeMapper = mapper;
|
|
498
504
|
let id;
|
|
@@ -541,11 +547,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
541
547
|
}
|
|
542
548
|
await execution.validate(approvalSignal);
|
|
543
549
|
}
|
|
544
|
-
const answer = await this.approve?.(permission, approvalSignal, options, execution && toolName?.startsWith('mcp__employee__') ? 'employee-local' : undefined) ?? 'unavailable';
|
|
550
|
+
const answer = await this.approve?.(permission, approvalSignal, options, execution && toolName?.startsWith('mcp__employee__') ? 'employee-local' : undefined, runtime.hostWorkspace) ?? 'unavailable';
|
|
545
551
|
if (execution && answer === 'allowed-once')
|
|
546
552
|
await execution.validate(approvalSignal);
|
|
547
553
|
return answer;
|
|
548
|
-
}, onUpdate: notification => { try {
|
|
554
|
+
}, onEmployeeNotice: notice => push(mapper.text(notice + '\n\n')), onUpdate: notification => { try {
|
|
549
555
|
push(mapper.acp(notification.update));
|
|
550
556
|
}
|
|
551
557
|
catch (error) {
|
|
@@ -663,6 +669,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
663
669
|
yield chunk;
|
|
664
670
|
}
|
|
665
671
|
}
|
|
672
|
+
mapper.flushMetadata();
|
|
666
673
|
recordInformation();
|
|
667
674
|
details.endTurn();
|
|
668
675
|
await timing.run('details-flush', () => details.flush());
|
|
@@ -704,9 +711,16 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
704
711
|
runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.turn-failure', reason: closeCode(error, signal.aborted ? 'ABORTED' : 'TURN_FAILED'), action: 'failure' });
|
|
705
712
|
recordInformation();
|
|
706
713
|
failureCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.code : 'TURN_PREPARATION_FAILED';
|
|
714
|
+
withRemoteFailureEvidence(error, runtime?.employeeRemoteCloseReason?.(), failureCode);
|
|
707
715
|
if (live && session && session.promptDispatchCount > promptBaseline)
|
|
708
716
|
live.failure = { cause: error };
|
|
709
717
|
const reporting = [];
|
|
718
|
+
try {
|
|
719
|
+
activeMapper?.flushMetadata();
|
|
720
|
+
}
|
|
721
|
+
catch (failure) {
|
|
722
|
+
reporting.push(failure);
|
|
723
|
+
}
|
|
710
724
|
const reportFailure = async () => {
|
|
711
725
|
if (request?.recordEarlyFailure) {
|
|
712
726
|
const record = request.recordEarlyFailure;
|
|
@@ -969,12 +983,12 @@ export function apply(ctx, live) {
|
|
|
969
983
|
ctx.effect(() => () => { modelDirectory.dispose(); return adapter.dispose(); });
|
|
970
984
|
}
|
|
971
985
|
export function dshApproval(ctx) {
|
|
972
|
-
return async (request, signal, options, context) => {
|
|
986
|
+
return async (request, signal, options, context, hostWorkspace) => {
|
|
973
987
|
const agent = options.sessionId ? ctx.get('agents')?.get(options.sessionId) : undefined;
|
|
974
988
|
const approval = ctx.get('approval');
|
|
975
989
|
if (!agent || !approval)
|
|
976
990
|
return 'unavailable';
|
|
977
|
-
const label = approvalLabel(request, context === 'employee-local' ? 'local' : 'host');
|
|
991
|
+
const label = approvalLabel(request, context === 'employee-local' ? 'local' : 'host', hostWorkspace);
|
|
978
992
|
if (!label)
|
|
979
993
|
return 'unavailable';
|
|
980
994
|
return approval.request({ agent, ...label, signal });
|
package/dist/input.d.ts
CHANGED
|
@@ -12,6 +12,13 @@ export interface HistoryTransfer {
|
|
|
12
12
|
omittedReasoningBlocks: number;
|
|
13
13
|
reasoningOmissionReason: 'dsh-internal-reasoning';
|
|
14
14
|
}
|
|
15
|
+
export interface HostWorkspacePresentation {
|
|
16
|
+
readonly cwd: string;
|
|
17
|
+
}
|
|
18
|
+
type RuntimeSection = {
|
|
19
|
+
name: string;
|
|
20
|
+
text: string;
|
|
21
|
+
};
|
|
15
22
|
export interface PreparedInput {
|
|
16
23
|
prompt: AcpContentBlock[];
|
|
17
24
|
users: {
|
|
@@ -20,8 +27,12 @@ export interface PreparedInput {
|
|
|
20
27
|
}[];
|
|
21
28
|
systemPrompt?: string;
|
|
22
29
|
historyTransfer?: HistoryTransfer;
|
|
30
|
+
runtimeSnapshots?: ReadonlyMap<AcpContentBlock, readonly RuntimeSection[]>;
|
|
23
31
|
}
|
|
32
|
+
/** Invocation-only model context; never modifies the native session/history. */
|
|
33
|
+
export declare function projectHostWorkspaceInput(input: PreparedInput, host?: HostWorkspacePresentation): PreparedInput;
|
|
24
34
|
/** Role-labelled public history only; never serialize source/replay/tool internals. */
|
|
25
35
|
export declare function prepareContinuation(options: GenerateOptions, input: PreparedInput, readImage?: ImageReader): Promise<PreparedInput>;
|
|
26
36
|
/** The attachment service supplies verified bytes; client paths never cross SSH. */
|
|
27
37
|
export declare function prepareInput(options: GenerateOptions, readImage?: ImageReader): Promise<PreparedInput>;
|
|
38
|
+
export {};
|
package/dist/input.js
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
import { NeedError } from "./remote.js";
|
|
2
2
|
import { isWorkspaceNoticeMessage, withoutWorkspaceNotice } from "./workspace-notice.js";
|
|
3
|
+
function runtimeSections(message) {
|
|
4
|
+
const source = message.source;
|
|
5
|
+
if (message.role !== 'user' || source?.kind !== 'runtime-context' || source.form !== 'snapshot' || !Array.isArray(source.sections) || message.content.length !== 1 || message.content[0].type !== 'text')
|
|
6
|
+
return;
|
|
7
|
+
if (!source.sections.every(s => s && typeof s.name === 'string' && typeof s.text === 'string'))
|
|
8
|
+
return;
|
|
9
|
+
const sections = source.sections, body = sections.map(s => s.text).join('\n\n');
|
|
10
|
+
// Only official, structurally attributed snapshot text is projected. Employee
|
|
11
|
+
// text and opaque contributions are never searched for or rewritten as paths.
|
|
12
|
+
if (body && message.content[0].text.endsWith('\n\n' + body))
|
|
13
|
+
return sections;
|
|
14
|
+
}
|
|
15
|
+
/** Invocation-only model context; never modifies the native session/history. */
|
|
16
|
+
export function projectHostWorkspaceInput(input, host) {
|
|
17
|
+
if (!host)
|
|
18
|
+
return input;
|
|
19
|
+
const context = `当前会话使用远程主机上的工作区:${JSON.stringify(host.cwd)}。文件和命令在该主机目录执行,受主机账号的操作系统权限约束;本机 DSH 文件沙箱的工作区路径不适用于这些远程操作。`;
|
|
20
|
+
const mapped = new Map();
|
|
21
|
+
for (const [block, sections] of input.runtimeSnapshots ?? []) {
|
|
22
|
+
if (block.type !== 'text' || !sections.some(s => s.name === 'sandbox:policy'))
|
|
23
|
+
continue;
|
|
24
|
+
const body = sections.map(s => s.text).join('\n\n');
|
|
25
|
+
mapped.set(block, { ...block, text: block.text.slice(0, -body.length) + sections.map(s => s.name === 'sandbox:policy' ? context : s.text).join('\n\n') });
|
|
26
|
+
}
|
|
27
|
+
const project = (blocks) => blocks.map(block => mapped.get(block) ?? block);
|
|
28
|
+
const prompt = project(input.prompt);
|
|
29
|
+
// Confirm this invocation after all current and historical text. A projected
|
|
30
|
+
// historical snapshot must not suppress host facts for an opaque current one;
|
|
31
|
+
// warm steps also need no new native snapshot to retain their host location.
|
|
32
|
+
prompt.push({ type: 'text', text: context });
|
|
33
|
+
return { ...input, prompt, users: input.users.map(user => ({ ...user, prompt: project(user.prompt) })) };
|
|
34
|
+
}
|
|
3
35
|
async function publicBlock(block, options, readImage, history = false) {
|
|
4
36
|
options.signal?.throwIfAborted();
|
|
5
37
|
if (block.type === 'text')
|
|
@@ -26,6 +58,7 @@ export async function prepareContinuation(options, input, readImage) {
|
|
|
26
58
|
throw new NeedError('HISTORY_CONTENT_UNSUPPORTED', `此前含有无法可靠承接的历史角色:${message.role};本轮未新建或发送`, '完整公开的用户及助手历史;工具记录的公开内容映射');
|
|
27
59
|
const after = messages.findLastIndex(message => message.role === 'assistant');
|
|
28
60
|
const prompt = [];
|
|
61
|
+
const runtimeSnapshots = new Map(input.runtimeSnapshots);
|
|
29
62
|
const historyTransfer = { scope: 'visible-user-assistant-text-and-images', textBlocks: 0, imageBlocks: 0, offloadedImagePlaceholders: 0, omittedReasoningBlocks: 0, reasoningOmissionReason: 'dsh-internal-reasoning' };
|
|
30
63
|
for (const message of messages.slice(0, after + 1)) {
|
|
31
64
|
if (message.role === 'system' || message.role === 'developer')
|
|
@@ -33,13 +66,17 @@ export async function prepareContinuation(options, input, readImage) {
|
|
|
33
66
|
if (message.role !== 'user' && message.role !== 'assistant')
|
|
34
67
|
throw new NeedError('HISTORY_CONTENT_UNSUPPORTED', `此前含有无法可靠承接的历史角色:${message.role};本轮未新建或发送`, '完整公开的用户及助手历史;工具记录的公开内容映射');
|
|
35
68
|
prompt.push({ type: 'text', text: `此前可见对话(历史角色:${message.role};仅为历史记录):\n` });
|
|
69
|
+
const sections = runtimeSections(message);
|
|
36
70
|
for (const block of message.content) {
|
|
37
71
|
// DSH explicitly distinguishes internal thinking from visible user text.
|
|
38
72
|
if (block.type === 'reasoning') {
|
|
39
73
|
historyTransfer.omittedReasoningBlocks++;
|
|
40
74
|
continue;
|
|
41
75
|
}
|
|
42
|
-
|
|
76
|
+
const prepared = await publicBlock(block, options, readImage, true);
|
|
77
|
+
prompt.push(prepared);
|
|
78
|
+
if (sections)
|
|
79
|
+
runtimeSnapshots.set(prepared, sections);
|
|
43
80
|
if (block.type === 'text')
|
|
44
81
|
historyTransfer.textBlocks++;
|
|
45
82
|
else if (block.type === 'image') {
|
|
@@ -52,7 +89,7 @@ export async function prepareContinuation(options, input, readImage) {
|
|
|
52
89
|
prompt.push({ type: 'text', text: `\n(历史角色:${message.role} 的记录结束)\n` });
|
|
53
90
|
}
|
|
54
91
|
prompt.push({ type: 'text', text: '此前可见对话结束。以下为当前用户输入:\n' }, ...input.prompt);
|
|
55
|
-
return { ...input, prompt, historyTransfer };
|
|
92
|
+
return { ...input, prompt, historyTransfer, runtimeSnapshots };
|
|
56
93
|
}
|
|
57
94
|
/** The attachment service supplies verified bytes; client paths never cross SSH. */
|
|
58
95
|
export async function prepareInput(options, readImage) {
|
|
@@ -78,13 +115,19 @@ export async function prepareInput(options, readImage) {
|
|
|
78
115
|
throw new NeedError('INPUT_REQUIRED', '我需要本轮用户输入', '用户输入');
|
|
79
116
|
const prompt = [];
|
|
80
117
|
const preparedUsers = [];
|
|
118
|
+
const runtimeSnapshots = new Map();
|
|
81
119
|
for (const message of users) {
|
|
82
120
|
const before = prompt.length;
|
|
83
|
-
|
|
84
|
-
|
|
121
|
+
const sections = runtimeSections(message);
|
|
122
|
+
for (const block of message.content) {
|
|
123
|
+
const prepared = await publicBlock(block, options, readImage);
|
|
124
|
+
prompt.push(prepared);
|
|
125
|
+
if (sections)
|
|
126
|
+
runtimeSnapshots.set(prepared, sections);
|
|
127
|
+
}
|
|
85
128
|
preparedUsers.push({ id: message.id, prompt: prompt.slice(before) });
|
|
86
129
|
}
|
|
87
130
|
if (!prompt.length)
|
|
88
131
|
throw new NeedError('INPUT_REQUIRED', '我需要本轮用户输入', '用户输入');
|
|
89
|
-
return { prompt, users: preparedUsers, ...(context.length ? { systemPrompt: context.join('\n\n') } : {}) };
|
|
132
|
+
return { prompt, users: preparedUsers, runtimeSnapshots, ...(context.length ? { systemPrompt: context.join('\n\n') } : {}) };
|
|
90
133
|
}
|
package/dist/mapping.d.ts
CHANGED
|
@@ -26,6 +26,9 @@ export declare class StreamMapper {
|
|
|
26
26
|
private seen;
|
|
27
27
|
private baseline;
|
|
28
28
|
private readonly observations;
|
|
29
|
+
private readonly modelMetadata;
|
|
30
|
+
private anonymousMetadata;
|
|
31
|
+
private lastMetadataType;
|
|
29
32
|
private readonly onDiagnostic;
|
|
30
33
|
/** Omitted argument is exclusively for a known new session; pass null on unknown resume. */
|
|
31
34
|
constructor(previousUsage?: UsageSnapshot | null, onDiagnostic?: (observation: Readonly<SanitizedObservation>) => void);
|
|
@@ -33,6 +36,8 @@ export declare class StreamMapper {
|
|
|
33
36
|
get diagnostics(): readonly Readonly<SanitizedObservation>[];
|
|
34
37
|
private diagnostic;
|
|
35
38
|
private usageWarning;
|
|
39
|
+
/** One durable observation per official message; keep ordered, sanitized fragment snapshots. */
|
|
40
|
+
flushMetadata(): void;
|
|
36
41
|
text(text: string, reasoning?: boolean): StreamChunk[];
|
|
37
42
|
/** Close the current text or reasoning block once, before the stream settles. */
|
|
38
43
|
flush(): StreamChunk[];
|
package/dist/mapping.js
CHANGED
|
@@ -9,6 +9,9 @@ export class StreamMapper {
|
|
|
9
9
|
seen = new Map();
|
|
10
10
|
baseline;
|
|
11
11
|
observations = [];
|
|
12
|
+
modelMetadata = new Map();
|
|
13
|
+
anonymousMetadata = 0;
|
|
14
|
+
lastMetadataType;
|
|
12
15
|
onDiagnostic;
|
|
13
16
|
/** Omitted argument is exclusively for a known new session; pass null on unknown resume. */
|
|
14
17
|
constructor(previousUsage = { version: 1, kind: 'zero' }, onDiagnostic) {
|
|
@@ -30,6 +33,15 @@ export class StreamMapper {
|
|
|
30
33
|
usageWarning(summary) {
|
|
31
34
|
return this.diagnostic({ kind: 'usage', severity: 'warning', code: 'USAGE_UNAVAILABLE', summary });
|
|
32
35
|
}
|
|
36
|
+
/** One durable observation per official message; keep ordered, sanitized fragment snapshots. */
|
|
37
|
+
flushMetadata() {
|
|
38
|
+
for (const [key, payload] of this.modelMetadata) {
|
|
39
|
+
// A throwing sink may already have enqueued its write. Never replay that message.
|
|
40
|
+
this.modelMetadata.delete(key);
|
|
41
|
+
this.diagnostic({ kind: 'extension', severity: 'info', code: 'ACP_MODEL_METADATA', summary: '远端模型消息扩展(按消息汇总)', method: 'session/update', payload });
|
|
42
|
+
}
|
|
43
|
+
this.lastMetadataType = undefined;
|
|
44
|
+
}
|
|
33
45
|
text(text, reasoning = false) {
|
|
34
46
|
if (!text)
|
|
35
47
|
return [];
|
|
@@ -55,15 +67,31 @@ export class StreamMapper {
|
|
|
55
67
|
acp(update) {
|
|
56
68
|
if (update.sessionUpdate === 'agent_message_chunk' || update.sessionUpdate === 'agent_thought_chunk') {
|
|
57
69
|
if (update.content.type === 'text' && typeof update.content.text === 'string') {
|
|
58
|
-
|
|
70
|
+
const messageId = typeof update.messageId === 'string' ? update.messageId : undefined;
|
|
71
|
+
if (messageId !== undefined)
|
|
72
|
+
this.lastMetadataType = undefined;
|
|
73
|
+
else {
|
|
74
|
+
if (this.lastMetadataType !== update.sessionUpdate)
|
|
75
|
+
this.anonymousMetadata++;
|
|
76
|
+
this.lastMetadataType = update.sessionUpdate;
|
|
77
|
+
}
|
|
78
|
+
// Capture each extension layer now, before the caller can mutate it. Text mapping is unchanged.
|
|
59
79
|
if (Object.keys(update).some(key => key !== 'sessionUpdate' && key !== 'content')
|
|
60
80
|
|| Object.keys(update.content).some(key => key !== 'type' && key !== 'text')) {
|
|
61
|
-
|
|
81
|
+
const key = JSON.stringify(messageId !== undefined ? ['message', messageId] : ['segment', this.anonymousMetadata]);
|
|
82
|
+
let pending = this.modelMetadata.get(key);
|
|
83
|
+
if (!pending) {
|
|
84
|
+
pending = { sessionUpdate: update.sessionUpdate, ...(messageId !== undefined ? { messageId } : {}), fragments: [] };
|
|
85
|
+
this.modelMetadata.set(key, pending);
|
|
86
|
+
}
|
|
87
|
+
pending.fragments.push(sanitizeObservation({ kind: 'extension', severity: 'info', code: 'ACP_MODEL_METADATA', summary: '远端模型消息扩展', payload: update }).payload);
|
|
62
88
|
}
|
|
63
89
|
return this.text(update.content.text, update.sessionUpdate === 'agent_thought_chunk');
|
|
64
90
|
}
|
|
91
|
+
this.lastMetadataType = undefined;
|
|
65
92
|
return this.diagnostic({ kind: 'extension', severity: 'warning', code: 'ACP_CONTENT_UNSUPPORTED', summary: '远端非文本内容,需要对应的公开显示映射', method: 'session/update', payload: update });
|
|
66
93
|
}
|
|
94
|
+
this.lastMetadataType = undefined;
|
|
67
95
|
let kind = 'session';
|
|
68
96
|
let code = 'ACP_SESSION_UPDATE';
|
|
69
97
|
let summary = '远端会话事件';
|
|
@@ -111,6 +139,7 @@ export class StreamMapper {
|
|
|
111
139
|
}
|
|
112
140
|
/** ACP usage and the fixed Claude quota both accumulate across the session, not per turn. */
|
|
113
141
|
acpResponse(response) {
|
|
142
|
+
this.flushMetadata();
|
|
114
143
|
this.diagnostic({ kind: 'usage', severity: 'info', code: 'ACP_PROMPT_RESPONSE', summary: '远端响应与真实用量', method: 'session/prompt', payload: response });
|
|
115
144
|
const quota = record(response._meta?.quota);
|
|
116
145
|
const tokens = record(quota?.token_count);
|