@ai-workbench/remote-agent 0.8.2 → 0.8.10
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 +42 -7
- package/dist/acp.d.ts +46 -1
- package/dist/acp.js +296 -36
- package/dist/approval-label.d.ts +6 -0
- package/dist/approval-label.js +57 -0
- package/dist/broker-runtime.d.ts +10 -0
- package/dist/broker-runtime.js +334 -0
- package/dist/broker.d.ts +29 -6
- package/dist/broker.js +146 -142
- package/dist/claude-local.d.ts +37 -0
- package/dist/claude-local.js +207 -0
- package/dist/client.js +71 -2
- package/dist/conversation-details.d.ts +1 -1
- package/dist/conversation-details.js +88 -8
- package/dist/employee-endpoint.d.ts +91 -0
- package/dist/employee-endpoint.js +141 -0
- package/dist/employee-exec.d.ts +53 -0
- package/dist/employee-exec.js +423 -0
- package/dist/employee-location.d.ts +66 -0
- package/dist/employee-location.js +51 -0
- package/dist/employee-mcp.d.ts +42 -0
- package/dist/employee-mcp.js +181 -0
- package/dist/employee-native.d.ts +18 -0
- package/dist/employee-native.js +38 -0
- package/dist/employee-provider.d.ts +42 -0
- package/dist/employee-provider.js +326 -0
- package/dist/employee-relay.d.ts +18 -0
- package/dist/employee-relay.js +49 -0
- package/dist/employee-shell.d.ts +26 -0
- package/dist/employee-shell.js +89 -0
- package/dist/employee-ssh.d.ts +22 -0
- package/dist/employee-ssh.js +123 -0
- package/dist/employee-tools.d.ts +117 -0
- package/dist/employee-tools.js +292 -0
- package/dist/employee-workspace.d.ts +26 -0
- package/dist/employee-workspace.js +61 -0
- package/dist/index.d.ts +31 -14
- package/dist/index.js +335 -42
- package/dist/input.d.ts +15 -0
- package/dist/input.js +65 -24
- package/dist/local-user-turn.d.ts +24 -0
- package/dist/local-user-turn.js +103 -0
- package/dist/model-catalog.d.ts +55 -0
- package/dist/model-catalog.js +254 -0
- package/dist/model-configuration.d.ts +51 -0
- package/dist/model-configuration.js +212 -0
- package/dist/presentation.d.ts +1 -1
- package/dist/presentation.js +50 -2
- package/dist/protocol-boundary.d.ts +13 -0
- package/dist/protocol-boundary.js +49 -0
- package/dist/provider.d.ts +54 -2
- package/dist/provider.js +92 -29
- package/dist/readiness.js +1 -1
- package/dist/recovery-checkpoint.d.ts +6 -0
- package/dist/recovery-checkpoint.js +71 -0
- package/dist/recovery.d.ts +47 -0
- package/dist/recovery.js +61 -0
- package/dist/remote-details.d.ts +10 -0
- package/dist/remote-details.js +14 -4
- package/dist/rpc-namespace.d.ts +3 -0
- package/dist/rpc-namespace.js +50 -0
- package/dist/stage-timing.d.ts +27 -0
- package/dist/stage-timing.js +39 -0
- package/helpers/ripgrep/LICENSE-vscode +13 -0
- package/helpers/ripgrep/PROVENANCE.md +9 -0
- package/helpers/ripgrep/darwin-arm64/rg +0 -0
- package/helpers/ripgrep/darwin-x64/rg +0 -0
- package/helpers/ripgrep/manifest.json +56 -0
- package/helpers/ripgrep/notices/COPYING-ripgrep +3 -0
- package/helpers/ripgrep/notices/DEPENDENCIES.json +946 -0
- package/helpers/ripgrep/notices/LICENCE-PCRE2.md +103 -0
- package/helpers/ripgrep/notices/LICENSE-MIT-ripgrep +21 -0
- package/helpers/ripgrep/notices/UNLICENSE-ripgrep +24 -0
- package/helpers/ripgrep/notices/aho-corasick-1.1.3/COPYING +3 -0
- package/helpers/ripgrep/notices/aho-corasick-1.1.3/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/aho-corasick-1.1.3/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/bstr-1.12.0/COPYING +8 -0
- package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-THIRD-PARTY +593 -0
- package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/COPYRIGHT +17 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-APACHE +202 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-WHATWG +26 -0
- package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/COPYING +8 -0
- package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-MIT +26 -0
- package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/lexopt-0.3.1/LICENSE +19 -0
- package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/log-0.4.28/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/log-0.4.28/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/memchr-2.7.6/COPYING +3 -0
- package/helpers/ripgrep/notices/memchr-2.7.6/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/memchr-2.7.6/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-MIT +26 -0
- package/helpers/ripgrep/notices/msvc_spectre_libs-0.1.3/LICENSE +21 -0
- package/helpers/ripgrep/notices/pcre2-0.2.11/COPYING +3 -0
- package/helpers/ripgrep/notices/pcre2-0.2.11/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/pcre2-0.2.11/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/pcre2-sys-0.2.10/COPYING +3 -0
- package/helpers/ripgrep/notices/pcre2-sys-0.2.10/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/pcre2-sys-0.2.10/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/r-efi-5.3.0/AUTHORS +74 -0
- package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/regex-syntax-0.8.8/src/unicode_tables/LICENSE-UNICODE +57 -0
- package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-BOOST +23 -0
- package/helpers/ripgrep/notices/same-file-1.0.6/COPYING +3 -0
- package/helpers/ripgrep/notices/same-file-1.0.6/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/same-file-1.0.6/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-APACHE +13 -0
- package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/termcolor-1.4.1/COPYING +3 -0
- package/helpers/ripgrep/notices/termcolor-1.4.1/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/termcolor-1.4.1/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/textwrap-0.16.2/LICENSE +21 -0
- package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/jemalloc/COPYING +27 -0
- package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-UNICODE +39 -0
- package/helpers/ripgrep/notices/walkdir-2.5.0/COPYING +3 -0
- package/helpers/ripgrep/notices/walkdir-2.5.0/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/walkdir-2.5.0/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
- package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/winapi-util-0.1.11/COPYING +3 -0
- package/helpers/ripgrep/notices/winapi-util-0.1.11/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/winapi-util-0.1.11/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/windows-link-0.2.1/license-apache-2.0 +201 -0
- package/helpers/ripgrep/notices/windows-link-0.2.1/license-mit +21 -0
- package/helpers/ripgrep/notices/windows-sys-0.61.2/license-apache-2.0 +201 -0
- package/helpers/ripgrep/notices/windows-sys-0.61.2/license-mit +21 -0
- package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
- package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-MIT +23 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +2 -2
- package/package.json +30 -12
package/dist/acp.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as acp from '@agentclientprotocol/sdk/unstable';
|
|
2
2
|
import { NeedError } from "./remote.js";
|
|
3
|
+
import { employeeFailure } from "./presentation.js";
|
|
4
|
+
import { StageTiming } from "./stage-timing.js";
|
|
5
|
+
import { isolateRpcIds } from "./rpc-namespace.js";
|
|
6
|
+
import { recoveryCapabilities, supportsCodexContextPolicy } from "./recovery.js";
|
|
3
7
|
import { sanitizeObservation } from "./conversation-details.js";
|
|
8
|
+
import { ModelConfiguration, ModelConfigurationError } from "./model-configuration.js";
|
|
4
9
|
/** ACP v1 with explicitly negotiated SDK 2.0 lifecycle/context extensions. */
|
|
5
10
|
export class AcpSession {
|
|
6
11
|
connection;
|
|
@@ -16,35 +21,136 @@ export class AcpSession {
|
|
|
16
21
|
updating = false;
|
|
17
22
|
cwd;
|
|
18
23
|
systemPrompt = '';
|
|
24
|
+
codexNewSessionOnly = false;
|
|
25
|
+
creatingReplacement = false;
|
|
26
|
+
employeeMeta;
|
|
27
|
+
employeeNegotiated = false;
|
|
28
|
+
configuration;
|
|
29
|
+
configRevision = 0;
|
|
30
|
+
selected;
|
|
31
|
+
selecting = false;
|
|
32
|
+
resolvedEvidence;
|
|
33
|
+
pendingConfigurations = new Map();
|
|
34
|
+
get modelConfiguration() { return this.configuration?.snapshot; }
|
|
35
|
+
get resolvedModelEvidence() { return this.resolvedEvidence; }
|
|
36
|
+
get models() { if (!this.configuration)
|
|
37
|
+
throw new ModelConfigurationError('远端没有提供完整配置列表'); return this.configuration.models; }
|
|
38
|
+
receiveConfiguration(options) {
|
|
39
|
+
this.configRevision++;
|
|
40
|
+
if (this.configuration)
|
|
41
|
+
this.configuration.update(options);
|
|
42
|
+
else
|
|
43
|
+
this.configuration = new ModelConfiguration(options, async (configId, value) => {
|
|
44
|
+
const result = await this.connection.agent.request(acp.methods.agent.session.setConfigOption, { sessionId: this.sessionId, configId, value }, { cancellationSignal: this.turnSignal });
|
|
45
|
+
if (this.resolvedEvidence && (this.configuration?.snapshot.model !== this.resolvedEvidence.modelValue || (configId === this.configuration?.snapshot.modelConfigId && value !== this.resolvedEvidence.modelValue)))
|
|
46
|
+
this.resolvedEvidence = undefined;
|
|
47
|
+
this.hooks.broker?.acknowledge(acp.methods.agent.session.setConfigOption);
|
|
48
|
+
return result;
|
|
49
|
+
});
|
|
50
|
+
if (this.resolvedEvidence && this.configuration.snapshot.model !== this.resolvedEvidence.modelValue)
|
|
51
|
+
this.resolvedEvidence = undefined;
|
|
52
|
+
}
|
|
53
|
+
resetConfiguration() {
|
|
54
|
+
this.selected = undefined;
|
|
55
|
+
this.resolvedEvidence = undefined;
|
|
56
|
+
if (this.configuration) {
|
|
57
|
+
try {
|
|
58
|
+
this.receiveConfiguration([]);
|
|
59
|
+
}
|
|
60
|
+
catch { /* Invalidated until this lifecycle operation supplies new evidence. */ }
|
|
61
|
+
}
|
|
62
|
+
return this.configRevision;
|
|
63
|
+
}
|
|
64
|
+
lifecycleConfiguration(result, revision) {
|
|
65
|
+
const pending = this.pendingConfigurations.get(this.sessionId);
|
|
66
|
+
this.pendingConfigurations.clear();
|
|
67
|
+
if (pending)
|
|
68
|
+
this.receiveConfiguration(pending);
|
|
69
|
+
// A notification observed during the operation is newer than its in-flight response.
|
|
70
|
+
else if (this.configRevision === revision && result.configOptions)
|
|
71
|
+
this.receiveConfiguration(result.configOptions);
|
|
72
|
+
const model = this.configuration?.snapshot.model;
|
|
73
|
+
if (!pending && this.configRevision === revision + 1 && model && typeof result.models?.currentModelId === 'string' && result.models.currentModelId)
|
|
74
|
+
this.resolvedEvidence = { modelValue: model, resolvedModel: result.models.currentModelId };
|
|
75
|
+
}
|
|
76
|
+
async selectModel(model, effort, signal) {
|
|
77
|
+
if (!this.ready || !this.sessionId)
|
|
78
|
+
throw new ModelConfigurationError('远端会话尚未建立');
|
|
79
|
+
if (this.busy || this.updating || this.selecting)
|
|
80
|
+
throw new ModelConfigurationError('上一项会话操作尚未完成');
|
|
81
|
+
signal?.throwIfAborted();
|
|
82
|
+
this.checkReporting();
|
|
83
|
+
this.selecting = true;
|
|
84
|
+
this.selected = { model, ...(effort === undefined ? {} : { effort }) };
|
|
85
|
+
this.turnSignal = signal;
|
|
86
|
+
try {
|
|
87
|
+
if (!this.configuration)
|
|
88
|
+
throw new ModelConfigurationError('远端没有提供完整配置列表');
|
|
89
|
+
const state = await this.configuration.select(model, effort);
|
|
90
|
+
signal?.throwIfAborted();
|
|
91
|
+
this.checkReporting();
|
|
92
|
+
return state;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
this.turnSignal = undefined;
|
|
96
|
+
this.selecting = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
assertModelSelection(model, effort) {
|
|
100
|
+
if (!this.configuration)
|
|
101
|
+
throw new ModelConfigurationError('远端没有提供完整配置列表');
|
|
102
|
+
this.configuration.assertSelection(model, effort);
|
|
103
|
+
}
|
|
19
104
|
reports = [];
|
|
20
105
|
reportingFailure;
|
|
106
|
+
timing = new StageTiming();
|
|
107
|
+
stage(phase, operation) { return (this.hooks.timing ?? this.timing).run(phase, operation); }
|
|
21
108
|
get diagnostics() { return this.reports.map(entry => sanitizeObservation(entry)); }
|
|
22
|
-
detail(observation) {
|
|
109
|
+
detail(observation, hooks = this.hooks) {
|
|
23
110
|
const safe = sanitizeObservation(observation);
|
|
24
111
|
this.reports.push(safe);
|
|
25
112
|
try {
|
|
26
|
-
|
|
113
|
+
hooks.onDetail?.(safe);
|
|
27
114
|
}
|
|
28
115
|
catch (cause) {
|
|
29
116
|
this.reportingFailure ??= { cause };
|
|
30
117
|
this.connection?.close();
|
|
31
118
|
}
|
|
32
119
|
}
|
|
33
|
-
need(need) {
|
|
34
|
-
this.detail({ kind: 'need', severity: 'action-required', code: need.code, summary: need.message, payload: { requires: need.requires } });
|
|
35
|
-
|
|
120
|
+
need(need, hooks = this.hooks) {
|
|
121
|
+
this.detail({ kind: 'need', severity: 'action-required', code: need.code, summary: need.message, payload: { requires: need.requires } }, hooks);
|
|
122
|
+
hooks.onNeed?.(need);
|
|
36
123
|
}
|
|
37
124
|
checkReporting() { if (this.reportingFailure)
|
|
38
125
|
throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能记录;本轮未继续发送', '可用的技术详情记录渠道'); }
|
|
39
126
|
constructor(transport, hooks) {
|
|
40
127
|
this.hooks = hooks;
|
|
128
|
+
this.timing.bind(detail => this.detail(detail));
|
|
41
129
|
const app = acp.client({ name: '@ai-workbench/remote-agent' })
|
|
42
130
|
.onNotification(acp.methods.client.session.update, ({ params: notification }) => {
|
|
131
|
+
if (this.creatingReplacement) {
|
|
132
|
+
if (notification.sessionId !== this.sessionId && notification.update.sessionUpdate === 'config_option_update')
|
|
133
|
+
this.pendingConfigurations.set(notification.sessionId, notification.update.configOptions);
|
|
134
|
+
this.detail({ kind: 'session', severity: 'info', code: 'ACP_IDLE_UPDATE', summary: '新会话建立期间的轨迹,未作为正文处理', method: 'session/update', payload: notification });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
43
137
|
if (this.sessionId && notification.sessionId !== this.sessionId) {
|
|
44
138
|
this.detail({ kind: 'session', severity: 'warning', code: 'ACP_FOREIGN_SESSION_UPDATE', summary: '收到不属于当前远端会话的通知,未作为正文处理', method: 'session/update', payload: notification });
|
|
45
139
|
return;
|
|
46
140
|
}
|
|
47
141
|
const meta = record(notification._meta);
|
|
142
|
+
if (notification.update.sessionUpdate === 'config_option_update') {
|
|
143
|
+
this.resolvedEvidence = undefined;
|
|
144
|
+
if (!this.sessionId)
|
|
145
|
+
this.pendingConfigurations.set(notification.sessionId, notification.update.configOptions);
|
|
146
|
+
else
|
|
147
|
+
try {
|
|
148
|
+
this.receiveConfiguration(notification.update.configOptions);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
this.detail({ kind: 'session', severity: 'action-required', code: 'MODEL_CONFIGURATION_INVALID', summary: '远端模型配置无效,本轮不可发送', method: 'session/update', payload: { reason: error instanceof ModelConfigurationError ? error.message : '配置无法解析' } });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
48
154
|
if (meta && Object.keys(meta).length)
|
|
49
155
|
this.detail({ kind: 'extension', severity: 'info', code: 'ACP_NOTIFICATION_META', summary: '远端通知扩展', method: 'session/update', payload: notification._meta });
|
|
50
156
|
if (this.busy) {
|
|
@@ -57,12 +163,14 @@ export class AcpSession {
|
|
|
57
163
|
this.detail({ kind: notification.update.sessionUpdate === 'available_commands_update' ? 'commands' : 'session', severity: 'info', code: 'ACP_IDLE_UPDATE', summary: '远端会话建立、恢复或空闲时的轨迹', method: 'session/update', payload: notification });
|
|
58
164
|
})
|
|
59
165
|
.onRequest(acp.methods.client.session.requestPermission, async ({ params: request, signal: rpcSignal, requestId }) => {
|
|
166
|
+
const hooks = this.hooks, detail = (value) => this.detail(value, hooks), need = (value) => this.need(value, hooks);
|
|
60
167
|
if (request.sessionId !== this.sessionId || !this.busy || this.turnSignal?.aborted)
|
|
61
168
|
return { outcome: { outcome: 'cancelled' } };
|
|
62
|
-
|
|
169
|
+
hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
|
|
63
170
|
const toolName = request.toolCall.name ?? record(request.toolCall._meta?.claudeCode)?.toolName;
|
|
171
|
+
detail({ kind: 'tool', severity: 'info', code: 'ACP_PERMISSION_REQUEST', summary: '远端工具审批标识', method: 'session/request_permission', payload: { sessionId: request.sessionId, toolCall: { toolCallId: request.toolCall.toolCallId, ...(typeof toolName === 'string' ? { name: toolName } : {}) } } });
|
|
64
172
|
if (toolName === 'AskUserQuestion') {
|
|
65
|
-
|
|
173
|
+
need({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问;已取消,未提交答案', requires: '可承载此提问的官方交互接口' });
|
|
66
174
|
return { outcome: { outcome: 'cancelled' } };
|
|
67
175
|
}
|
|
68
176
|
const eligible = request.options.filter(option => {
|
|
@@ -70,11 +178,11 @@ export class AcpSession {
|
|
|
70
178
|
const sameKind = request.options.filter(candidate => candidate.kind === option.kind);
|
|
71
179
|
const unique = single && sameKind.length === 1 && request.options.filter(candidate => candidate.optionId === option.optionId).length === 1;
|
|
72
180
|
if (!unique)
|
|
73
|
-
|
|
181
|
+
detail({ kind: 'tool', severity: 'info', code: 'APPROVAL_OPTION_NOT_OFFERED', summary: '此远端审批选项未映射到单次审批', method: 'session/request_permission', payload: option });
|
|
74
182
|
return unique;
|
|
75
183
|
});
|
|
76
184
|
if (!eligible.length) {
|
|
77
|
-
|
|
185
|
+
need({ code: 'APPROVAL_OPTION_UNSUPPORTED', message: '远端没有可唯一映射的单次审批选项,此动作未获授权', requires: '唯一对应的允许一次或拒绝一次选项' });
|
|
78
186
|
return { outcome: { outcome: 'cancelled' } };
|
|
79
187
|
}
|
|
80
188
|
const presented = { ...request, options: eligible };
|
|
@@ -82,7 +190,7 @@ export class AcpSession {
|
|
|
82
190
|
const id = this.sessionId;
|
|
83
191
|
let outcome = 'unavailable';
|
|
84
192
|
const approvalSignal = this.turnSignal ? AbortSignal.any([this.turnSignal, rpcSignal]) : rpcSignal;
|
|
85
|
-
const broker =
|
|
193
|
+
const broker = hooks.broker;
|
|
86
194
|
let generation;
|
|
87
195
|
// A transport disconnect withdraws only the UI question, not the ACP RPC.
|
|
88
196
|
// Late UI answers lose their lease; a fresh online attachment asks again.
|
|
@@ -96,7 +204,7 @@ export class AcpSession {
|
|
|
96
204
|
const abort = () => interrupted.resolve('cancelled');
|
|
97
205
|
signal.addEventListener('abort', abort, { once: true });
|
|
98
206
|
try {
|
|
99
|
-
outcome = await Promise.race([
|
|
207
|
+
outcome = await Promise.race([hooks.approve?.(presented, signal) ?? Promise.resolve('unavailable'), interrupted.promise]);
|
|
100
208
|
}
|
|
101
209
|
finally {
|
|
102
210
|
signal.removeEventListener('abort', abort);
|
|
@@ -107,7 +215,7 @@ export class AcpSession {
|
|
|
107
215
|
}
|
|
108
216
|
}
|
|
109
217
|
catch {
|
|
110
|
-
|
|
218
|
+
need({ code: 'APPROVAL_FAILED', message: '审批渠道失败或连接已结束;此动作未获授权', requires: '可用的 DSH 审批渠道和原连接' });
|
|
111
219
|
}
|
|
112
220
|
const kind = outcome === 'allowed-once' ? 'allow_once' : outcome === 'rejected' ? 'reject_once' : undefined;
|
|
113
221
|
const option = kind && eligible.find(item => item.kind === kind);
|
|
@@ -116,7 +224,7 @@ export class AcpSession {
|
|
|
116
224
|
broker.authorizeResponse(requestId, generation);
|
|
117
225
|
return { outcome: { outcome: 'selected', optionId: option.optionId } };
|
|
118
226
|
}
|
|
119
|
-
|
|
227
|
+
need({ code: outcome === 'allowed-once' ? 'APPROVAL_OPTION_UNSUPPORTED' : 'APPROVAL_UNAVAILABLE',
|
|
120
228
|
message: '此动作未获授权;审批已取消、不可用或没有可映射的单次选项', requires: '本次动作的有效审批' });
|
|
121
229
|
return { outcome: { outcome: 'cancelled' } };
|
|
122
230
|
})
|
|
@@ -126,6 +234,20 @@ export class AcpSession {
|
|
|
126
234
|
})
|
|
127
235
|
.onNotification(acp.methods.client.elicitation.complete, () => {
|
|
128
236
|
this.need({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问完成通知;未提交答案', requires: '可承载此提问的官方交互接口' });
|
|
237
|
+
})
|
|
238
|
+
.onRequest('_aw/employee-exec', (params) => params, async ({ params, signal }) => {
|
|
239
|
+
if (!this.employeeNegotiated || !this.employeeMeta || !this.hooks.employeeExecution)
|
|
240
|
+
throw new acp.RequestError(-32601, 'EMPLOYEE_EXECUTION_UNSUPPORTED');
|
|
241
|
+
try {
|
|
242
|
+
return await this.hooks.employeeExecution(params, signal);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (error instanceof NeedError) {
|
|
246
|
+
this.need(error);
|
|
247
|
+
throw new acp.RequestError(-32603, error.code);
|
|
248
|
+
}
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
129
251
|
})
|
|
130
252
|
.onRequest('*', ({ method, params }) => {
|
|
131
253
|
this.detail({ kind: 'extension', severity: 'action-required', code: 'ACP_UNKNOWN_REQUEST', summary: '未知远端交互请求未获授权', method, payload: params });
|
|
@@ -136,14 +258,14 @@ export class AcpSession {
|
|
|
136
258
|
this.detail({ kind: 'extension', severity: 'warning', code: 'ACP_UNKNOWN_NOTIFICATION', summary: '收到未知远端扩展通知,已保留技术记录', method, payload: params });
|
|
137
259
|
});
|
|
138
260
|
if ('readable' in transport && 'writable' in transport)
|
|
139
|
-
this.connection = app.connect(this.observe(transport));
|
|
261
|
+
this.connection = app.connect(isolateRpcIds(this.observe(transport)));
|
|
140
262
|
else {
|
|
141
263
|
// Stable and unstable apps use distinct private symbols. Bridge only their
|
|
142
264
|
// public stream interfaces so either SDK entry point remains supported.
|
|
143
265
|
const outward = new TransformStream();
|
|
144
266
|
const inward = new TransformStream();
|
|
145
267
|
const peer = transport.connect({ readable: outward.readable, writable: inward.writable });
|
|
146
|
-
this.connection = app.connect(this.observe({ readable: inward.readable, writable: outward.writable }));
|
|
268
|
+
this.connection = app.connect(isolateRpcIds(this.observe({ readable: inward.readable, writable: outward.writable })));
|
|
147
269
|
void this.connection.closed.then(() => peer.close());
|
|
148
270
|
void peer.closed.then(() => this.connection.close());
|
|
149
271
|
}
|
|
@@ -155,6 +277,13 @@ export class AcpSession {
|
|
|
155
277
|
const raw = record(message);
|
|
156
278
|
const params = record(raw?.params);
|
|
157
279
|
const update = record(params?.update);
|
|
280
|
+
if (raw)
|
|
281
|
+
this.hooks.broker?.processedMessage(raw);
|
|
282
|
+
if (typeof raw?.method === 'string' && Object.hasOwn(raw, 'id') && this.hooks.broker && !this.hooks.broker.promptInFlight && !(raw.method === '_aw/employee-exec' && this.hooks.employeeExecution && this.employeeMeta)) {
|
|
283
|
+
const error = new NeedError('RECOVERY_UNSAFE_OUTPUT', '主机会话有不属于当前输入的旧审批,未自动回答', '安全结束旧会话并新建');
|
|
284
|
+
this.hooks.broker.fail(error);
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
158
287
|
if (raw?.method === acp.methods.client.session.update && typeof update?.sessionUpdate === 'string' && !knownUpdates.has(update.sessionUpdate)) {
|
|
159
288
|
this.detail({ kind: 'extension', severity: 'warning', code: 'ACP_UNKNOWN_UPDATE', summary: '收到未知远端会话事件,已保留结构', method: 'session/update', payload: params });
|
|
160
289
|
return;
|
|
@@ -166,6 +295,35 @@ export class AcpSession {
|
|
|
166
295
|
get id() { return this.sessionId; }
|
|
167
296
|
get capabilities() { return this.negotiated; }
|
|
168
297
|
get startMethod() { return this.method; }
|
|
298
|
+
get checkpoint() { return this.ready && !this.busy && !this.updating && !this.selecting && this.sessionId ? { sessionId: this.sessionId, capabilities: recoveryCapabilities(this.negotiated), ...(this.configuration ? { modelConfiguration: { configOptions: JSON.parse(JSON.stringify(this.configuration.snapshot.configOptions)), ...(this.resolvedEvidence ? { resolvedEvidence: { ...this.resolvedEvidence } } : {}) } } : {}) } : undefined; }
|
|
299
|
+
restore(cwd, saved, context, verifiedSameContext = false) {
|
|
300
|
+
if (context.employeeRouting)
|
|
301
|
+
throw new NeedError('EMPLOYEE_CHECKPOINT_STALE', '本机工具通道需要重新建立,旧审批没有代答。', '按原主机实例安全结束后新建本机会话');
|
|
302
|
+
if (this.startAttempted)
|
|
303
|
+
throw new NeedError('SESSION_ALREADY_STARTED', '此客户端已建立会话', '新的客户端连接');
|
|
304
|
+
this.startAttempted = true;
|
|
305
|
+
this.sessionId = saved.sessionId;
|
|
306
|
+
this.negotiated = recoveryCapabilities(saved.capabilities);
|
|
307
|
+
this.cwd = cwd;
|
|
308
|
+
this.systemPrompt = context.systemPrompt ?? '';
|
|
309
|
+
this.codexNewSessionOnly = !!context.codexNewSessionOnly;
|
|
310
|
+
// A verified same-context reattach does not create or update native context.
|
|
311
|
+
// Old journals may lack new-only metadata; never manufacture it for them.
|
|
312
|
+
if (this.codexNewSessionOnly && !verifiedSameContext)
|
|
313
|
+
this.validateNewContext(this.systemPrompt, context.systemPrompt !== undefined);
|
|
314
|
+
if (saved.modelConfiguration) {
|
|
315
|
+
this.receiveConfiguration(saved.modelConfiguration.configOptions);
|
|
316
|
+
this.resolvedEvidence = saved.modelConfiguration.resolvedEvidence;
|
|
317
|
+
for (const update of saved.configUpdates ?? []) {
|
|
318
|
+
this.resolvedEvidence = undefined;
|
|
319
|
+
this.receiveConfiguration(update);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
this.hooks.broker?.finishRecovery();
|
|
323
|
+
this.detail({ kind: 'need', severity: 'info', code: 'ACP_CLIENT_REATTACHED', summary: '已接回原模型会话', payload: { sessionId: saved.sessionId } });
|
|
324
|
+
this.checkReporting();
|
|
325
|
+
this.ready = true;
|
|
326
|
+
}
|
|
169
327
|
validatePrompt(prompt) {
|
|
170
328
|
const capability = this.negotiated.promptCapabilities;
|
|
171
329
|
for (const block of prompt)
|
|
@@ -175,55 +333,86 @@ export class AcpSession {
|
|
|
175
333
|
validateContext(context) {
|
|
176
334
|
if ((context.systemPrompt ?? '') === this.systemPrompt)
|
|
177
335
|
return;
|
|
336
|
+
if (this.codexNewSessionOnly)
|
|
337
|
+
throw new NeedError('SYSTEM_PROMPT_NEW_SESSION_REQUIRED', 'Codex会话要求已变化,需要保留原会话并新建会话承接', '新会话及完整公开历史承接');
|
|
178
338
|
if (!this.negotiated.sessionCapabilities?.systemPrompt)
|
|
179
339
|
throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文更新能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
|
|
180
340
|
if (!this.negotiated.sessionCapabilities?.resume && !this.negotiated.loadSession)
|
|
181
341
|
throw new NeedError('SYSTEM_PROMPT_UPDATE_UNSUPPORTED', '远端没有可更新原会话上下文的恢复或加载能力', '支持带 systemPrompt 的 session/resume 或 session/load');
|
|
182
342
|
}
|
|
343
|
+
validateNewSessionContext(context) {
|
|
344
|
+
if (!this.codexNewSessionOnly)
|
|
345
|
+
throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '此会话没有使用Codex新建政策', 'Codex新建政策');
|
|
346
|
+
this.validateNewContext(context.systemPrompt ?? '', true);
|
|
347
|
+
}
|
|
183
348
|
setHooks(hooks) {
|
|
184
|
-
if (this.busy || this.updating)
|
|
185
|
-
throw new NeedError('TURN_BUSY', '
|
|
349
|
+
if (this.busy || this.updating || this.selecting)
|
|
350
|
+
throw new NeedError('TURN_BUSY', '上一轮或配置更新结束前不能更换会话钩子', '上一轮结束');
|
|
186
351
|
this.hooks = hooks;
|
|
187
352
|
}
|
|
188
353
|
get closed() { return this.connection.closed; }
|
|
189
|
-
|
|
354
|
+
validateNewContext(text, requirePolicy = false) {
|
|
355
|
+
if ((text || requirePolicy) && !this.negotiated.sessionCapabilities?.systemPrompt)
|
|
356
|
+
throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
|
|
357
|
+
if (this.codexNewSessionOnly && (text || requirePolicy)) {
|
|
358
|
+
if (!supportsCodexContextPolicy(this.negotiated))
|
|
359
|
+
throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '远端没有明确声明Codex上下文仅新建生效的兼容政策,本轮未发送', 'systemPrompt与精确的aiWorkbenchSystemPrompt新建政策');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async start(cwd, resumeId, signal, context, prompt) {
|
|
190
363
|
if (this.startAttempted)
|
|
191
364
|
throw new NeedError('SESSION_ALREADY_STARTED', '此连接已用于建立会话,不能转用工作区或身份', '新的独立远端连接');
|
|
192
365
|
this.startAttempted = true;
|
|
366
|
+
this.employeeMeta = context?.employeeExecution;
|
|
193
367
|
const abort = () => this.connection.close(new NeedError('ABORTED', '会话建立已取消', '重新连接主机'));
|
|
194
368
|
signal?.throwIfAborted();
|
|
195
369
|
signal?.addEventListener('abort', abort, { once: true });
|
|
196
370
|
try {
|
|
197
|
-
const hello = await this.connection.agent.request(acp.methods.agent.initialize, {
|
|
198
|
-
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.
|
|
199
|
-
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} } },
|
|
200
|
-
}, { cancellationSignal: signal });
|
|
371
|
+
const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
|
|
372
|
+
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.10' },
|
|
373
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1 } } } : {}) },
|
|
374
|
+
}, { cancellationSignal: signal }));
|
|
201
375
|
this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
|
|
202
376
|
if (hello.protocolVersion !== acp.PROTOCOL_VERSION)
|
|
203
377
|
throw new NeedError('ACP_VERSION_MISMATCH', 'ACP 协议版本不兼容', '支持 ACP v1 的远端 agent');
|
|
204
378
|
this.checkReporting();
|
|
205
379
|
this.negotiated = hello.agentCapabilities ?? {};
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
380
|
+
if (this.employeeMeta) {
|
|
381
|
+
const policy = record(hello._meta?.awEmployeeExecution);
|
|
382
|
+
if (!this.hooks.employeeExecution || !policy || Object.keys(policy).length !== 5 || policy.version !== 1 || policy.nativeCliVersion !== '0.160.1' || policy.native !== true || policy.perRpcGate !== true || policy.noHostFallback !== true)
|
|
383
|
+
throw new NeedError('EMPLOYEE_EXECUTION_UNSUPPORTED', '当前主机尚不支持在你的电脑执行Codex原生工具,本轮没有执行。', 'Codex员工端原生执行适配器与明确能力声明');
|
|
384
|
+
this.employeeNegotiated = true;
|
|
385
|
+
this.detail({ kind: 'session', severity: 'info', code: 'EMPLOYEE_EXECUTION_SELECTED', summary: '文件和命令在员工电脑的选定目录执行,Git使用本机仓库;模型、登录和全局助手要求仍在主机。本机MCP、hooks、plugins、冷恢复、快捷命令和主机文件报告尚不支持。外层ACP冷恢复已拒绝;原生子代理回收/缺少存活父会话时的恢复仅做源码检查,未实测。', payload: { bindingId: this.employeeMeta.bindingId, workspaceId: this.employeeMeta.workspaceId, cwd: this.employeeMeta.cwd, nativeCliVersion: '0.160.1', noHostFallback: true, agentConfigSource: 'host CODEX_HOME', unsupportedLocalFeatures: ['mcp', 'hooks', 'plugins', 'cold-resume', 'slash-commands', 'host-file-reports'] } });
|
|
209
386
|
}
|
|
210
|
-
const
|
|
387
|
+
const systemPrompt = context?.systemPrompt;
|
|
388
|
+
this.codexNewSessionOnly = !!context?.codexNewSessionOnly;
|
|
389
|
+
this.validateNewContext(systemPrompt ?? '', this.codexNewSessionOnly && systemPrompt !== undefined);
|
|
390
|
+
if (prompt)
|
|
391
|
+
this.validatePrompt(prompt);
|
|
392
|
+
if (context?.employeeRouting && resumeId)
|
|
393
|
+
throw new NeedError('EMPLOYEE_CHECKPOINT_STALE', '本机工具连接已变化,不能沿用旧会话。', '安全结束原主机实例并重新建立本机工具通道');
|
|
394
|
+
const routing = context?.employeeRouting;
|
|
395
|
+
const params = { cwd, mcpServers: routing ? structuredClone(routing.mcpServers) : [], ...((routing || this.employeeMeta) ? { _meta: { ...(this.employeeMeta ? { awEmployeeExecution: this.employeeMeta } : {}), ...(routing ? { claudeCode: { options: { toolAliases: structuredClone(routing.toolAliases) } } } : {}) } } : {}), ...(systemPrompt && !(resumeId && this.codexNewSessionOnly) ? { systemPrompt: { mode: 'append', content: [{ type: 'text', text: systemPrompt }] } } : {}) };
|
|
396
|
+
const configRevision = this.resetConfiguration();
|
|
211
397
|
if (resumeId) {
|
|
212
398
|
this.sessionId = resumeId;
|
|
213
399
|
if (this.negotiated.sessionCapabilities?.resume) {
|
|
214
|
-
await this.connection.agent.request(acp.methods.agent.session.resume, { ...params, sessionId: resumeId }, { cancellationSignal: signal });
|
|
400
|
+
const result = await this.stage('acp-session-resume', () => this.connection.agent.request(acp.methods.agent.session.resume, { ...params, sessionId: resumeId }, { cancellationSignal: signal }));
|
|
401
|
+
this.lifecycleConfiguration(result, configRevision);
|
|
215
402
|
this.method = acp.methods.agent.session.resume;
|
|
216
403
|
}
|
|
217
404
|
else {
|
|
218
405
|
if (!this.negotiated.loadSession)
|
|
219
406
|
throw new NeedError('RESUME_UNSUPPORTED', '远端未声明会话恢复或加载能力,不能恢复原会话', '支持 session/resume 或 session/load 的 agent');
|
|
220
|
-
await this.connection.agent.request(acp.methods.agent.session.load, { ...params, sessionId: resumeId }, { cancellationSignal: signal });
|
|
407
|
+
const result = await this.stage('acp-session-load', () => this.connection.agent.request(acp.methods.agent.session.load, { ...params, sessionId: resumeId }, { cancellationSignal: signal }));
|
|
408
|
+
this.lifecycleConfiguration(result, configRevision);
|
|
221
409
|
this.method = acp.methods.agent.session.load;
|
|
222
410
|
}
|
|
223
411
|
}
|
|
224
412
|
else {
|
|
225
|
-
const result = await this.connection.agent.request(acp.methods.agent.session.new, params, { cancellationSignal: signal });
|
|
413
|
+
const result = await this.stage('acp-session-new', () => this.connection.agent.request(acp.methods.agent.session.new, params, { cancellationSignal: signal }));
|
|
226
414
|
this.sessionId = result.sessionId;
|
|
415
|
+
this.lifecycleConfiguration(result, configRevision);
|
|
227
416
|
this.method = acp.methods.agent.session.new;
|
|
228
417
|
}
|
|
229
418
|
this.hooks.broker?.acknowledge(this.method);
|
|
@@ -236,10 +425,59 @@ export class AcpSession {
|
|
|
236
425
|
catch (error) {
|
|
237
426
|
if (signal?.aborted)
|
|
238
427
|
throw new NeedError('ABORTED', '会话建立已取消;尚无本轮输出', '重新连接主机');
|
|
428
|
+
if (error instanceof ModelConfigurationError) {
|
|
429
|
+
this.checkReporting();
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
this.checkReporting();
|
|
433
|
+
throw acpNeed(error);
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
signal?.removeEventListener('abort', abort);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/** Codex context is immutable after creation; keep the old remote session intact. */
|
|
440
|
+
async newContextSession(context, signal) {
|
|
441
|
+
this.checkReporting();
|
|
442
|
+
if (!this.ready || !this.sessionId || !this.cwd)
|
|
443
|
+
throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
|
|
444
|
+
if (!this.codexNewSessionOnly)
|
|
445
|
+
throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '此会话没有使用Codex新建政策', 'Codex新建政策');
|
|
446
|
+
if (this.busy || this.updating || this.selecting)
|
|
447
|
+
throw new NeedError('TURN_BUSY', '上一项会话操作尚未完成', '上一项操作结束');
|
|
448
|
+
const text = context.systemPrompt ?? '';
|
|
449
|
+
this.validateNewSessionContext(context);
|
|
450
|
+
signal?.throwIfAborted();
|
|
451
|
+
const oldId = this.sessionId;
|
|
452
|
+
this.updating = true;
|
|
453
|
+
this.ready = false;
|
|
454
|
+
this.creatingReplacement = true;
|
|
455
|
+
this.pendingConfigurations.clear();
|
|
456
|
+
const abort = () => this.connection.close(new NeedError('ABORTED', '新会话建立已取消,远端状态需确认', '管理员核对远端状态'));
|
|
457
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
458
|
+
try {
|
|
459
|
+
const revision = this.resetConfiguration();
|
|
460
|
+
const result = await this.stage('acp-session-new', () => this.connection.agent.request(acp.methods.agent.session.new, { cwd: this.cwd, mcpServers: [], ...(this.employeeMeta ? { _meta: { awEmployeeExecution: this.employeeMeta } } : {}), ...(text ? { systemPrompt: { mode: 'append', content: [{ type: 'text', text }] } } : {}) }, { cancellationSignal: signal }));
|
|
461
|
+
if (!result.sessionId || result.sessionId === oldId)
|
|
462
|
+
throw new NeedError('SESSION_STATE_INVALID', '新会话没有返回独立的远端标识,原会话状态需确认', '独立的新远端会话标识');
|
|
463
|
+
this.sessionId = result.sessionId;
|
|
464
|
+
this.lifecycleConfiguration(result, revision);
|
|
465
|
+
this.hooks.broker?.acknowledge(acp.methods.agent.session.new);
|
|
466
|
+
this.checkReporting();
|
|
467
|
+
this.systemPrompt = text;
|
|
468
|
+
this.method = acp.methods.agent.session.new;
|
|
469
|
+
this.ready = true;
|
|
470
|
+
return this.sessionId;
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
239
473
|
this.checkReporting();
|
|
474
|
+
if (error instanceof ModelConfigurationError)
|
|
475
|
+
throw error;
|
|
240
476
|
throw acpNeed(error);
|
|
241
477
|
}
|
|
242
478
|
finally {
|
|
479
|
+
this.creatingReplacement = false;
|
|
480
|
+
this.updating = false;
|
|
243
481
|
signal?.removeEventListener('abort', abort);
|
|
244
482
|
}
|
|
245
483
|
}
|
|
@@ -247,8 +485,8 @@ export class AcpSession {
|
|
|
247
485
|
this.checkReporting();
|
|
248
486
|
if (!this.ready || !this.sessionId || !this.cwd)
|
|
249
487
|
throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
|
|
250
|
-
if (this.busy || this.updating)
|
|
251
|
-
throw new NeedError('TURN_BUSY', '
|
|
488
|
+
if (this.busy || this.updating || this.selecting)
|
|
489
|
+
throw new NeedError('TURN_BUSY', '此远端会话正在处理上一轮或更新配置', '上一轮结束');
|
|
252
490
|
const text = context.systemPrompt ?? '';
|
|
253
491
|
if (text === this.systemPrompt)
|
|
254
492
|
return undefined;
|
|
@@ -262,7 +500,9 @@ export class AcpSession {
|
|
|
262
500
|
try {
|
|
263
501
|
const method = resume ? acp.methods.agent.session.resume : acp.methods.agent.session.load;
|
|
264
502
|
const params = { sessionId: this.sessionId, cwd: this.cwd, mcpServers: [], systemPrompt: { mode: 'append', content: [{ type: 'text', text }] } };
|
|
265
|
-
|
|
503
|
+
const configRevision = this.resetConfiguration();
|
|
504
|
+
const result = await this.stage(resume ? 'acp-session-resume' : 'acp-session-load', () => this.connection.agent.request(method, params, { cancellationSignal: signal }));
|
|
505
|
+
this.lifecycleConfiguration(result, configRevision);
|
|
266
506
|
this.hooks.broker?.acknowledge(method);
|
|
267
507
|
this.systemPrompt = text;
|
|
268
508
|
this.ready = true;
|
|
@@ -273,6 +513,10 @@ export class AcpSession {
|
|
|
273
513
|
catch (error) {
|
|
274
514
|
if (signal?.aborted)
|
|
275
515
|
throw new NeedError('ABORTED', '上下文更新已取消;原会话状态需确认', '重新连接原会话');
|
|
516
|
+
if (error instanceof ModelConfigurationError) {
|
|
517
|
+
this.checkReporting();
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
276
520
|
this.checkReporting();
|
|
277
521
|
throw acpNeed(error);
|
|
278
522
|
}
|
|
@@ -284,12 +528,14 @@ export class AcpSession {
|
|
|
284
528
|
async prompt(input, signal) {
|
|
285
529
|
if (!this.ready || !this.sessionId)
|
|
286
530
|
throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
|
|
287
|
-
if (this.busy || this.updating)
|
|
288
|
-
throw new NeedError('TURN_BUSY', '
|
|
531
|
+
if (this.busy || this.updating || this.selecting)
|
|
532
|
+
throw new NeedError('TURN_BUSY', '此远端会话正在处理上一轮或更新配置', '上一轮结束');
|
|
289
533
|
signal?.throwIfAborted();
|
|
290
534
|
const prompt = typeof input === 'string' ? [{ type: 'text', text: input }] : input;
|
|
291
535
|
this.validatePrompt(prompt);
|
|
292
536
|
this.checkReporting();
|
|
537
|
+
if (this.selected)
|
|
538
|
+
this.assertModelSelection(this.selected.model, this.selected.effort);
|
|
293
539
|
this.busy = true;
|
|
294
540
|
this.turnSignal = signal;
|
|
295
541
|
this.turnToken = Symbol('turn');
|
|
@@ -303,8 +549,10 @@ export class AcpSession {
|
|
|
303
549
|
};
|
|
304
550
|
signal?.addEventListener('abort', cancel, { once: true });
|
|
305
551
|
try {
|
|
306
|
-
const
|
|
307
|
-
|
|
552
|
+
const result = await this.stage('acp-prompt', () => {
|
|
553
|
+
const request = this.connection.agent.request(acp.methods.agent.session.prompt, { sessionId: this.sessionId, prompt }, { cancellationSignal: signal });
|
|
554
|
+
return signal ? Promise.race([request, interrupted]) : request;
|
|
555
|
+
});
|
|
308
556
|
this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
|
|
309
557
|
signal?.throwIfAborted();
|
|
310
558
|
this.hooks.broker?.completePrompt();
|
|
@@ -330,6 +578,17 @@ export class AcpSession {
|
|
|
330
578
|
export function acpNeed(error) {
|
|
331
579
|
if (error instanceof NeedError)
|
|
332
580
|
return error;
|
|
581
|
+
if (error instanceof acp.RequestError) {
|
|
582
|
+
// Host SDK1.9 uses Internal error + data.details for thrown adapter errors.
|
|
583
|
+
// Only fixed codes cross this boundary; arbitrary detail strings are never copied.
|
|
584
|
+
for (const value of [error.message, record(error.data)?.details]) {
|
|
585
|
+
if (typeof value !== 'string')
|
|
586
|
+
continue;
|
|
587
|
+
const code = value.split(':', 1)[0];
|
|
588
|
+
if (employeeAdapterCodes.has(code))
|
|
589
|
+
return new NeedError(code, employeeFailure(code), `员工电脑执行适配器明确拒绝:${code};RPC 错误码 ${error.code}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
333
592
|
if (error instanceof acp.RequestError && error.code === -32000)
|
|
334
593
|
return new NeedError('LOGIN_REQUIRED', '需要登录,请到设置 → 远程主机连接 点登录', '该员工自己的远端模型登录');
|
|
335
594
|
if (error instanceof acp.RequestError)
|
|
@@ -339,3 +598,4 @@ export function acpNeed(error) {
|
|
|
339
598
|
function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined; }
|
|
340
599
|
const knownUpdates = new Set(['user_message_chunk', 'agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update', 'plan', 'plan_update', 'plan_removed', 'available_commands_update', 'current_mode_update', 'config_option_update', 'session_info_update', 'usage_update', 'notice', 'compaction_update', 'compaction_summary_chunk', 'subagent_update', 'session_message', 'session_message_chunk']);
|
|
341
600
|
function safeLabel(value) { return value.replaceAll(/[\r\n\x00-\x1f\x7f]/g, ' '); }
|
|
601
|
+
const employeeAdapterCodes = new Set(["AW_EMPLOYEE_ADDITIONAL_DIRECTORIES_UNSUPPORTED", "AW_EMPLOYEE_SLASH_COMMAND_UNSUPPORTED", "AW_EMPLOYEE_HOST_FILE_REPORT_UNSUPPORTED", "AW_EMPLOYEE_CONFIG_DISCOVERY_OVERRIDE_UNSUPPORTED", "AW_EMPLOYEE_CONFIG_ADMISSION_INVALID", "AW_EMPLOYEE_CONFIG_ADMISSION_FAILED", "AW_EMPLOYEE_HOST_PROJECT_CONFIG_UNSUPPORTED", "AW_EMPLOYEE_EXECUTOR_MCP_UNSUPPORTED", "AW_EMPLOYEE_EXECUTOR_CONFIG_PROTOCOL_INVALID", "AW_EMPLOYEE_EXECUTOR_CONFIG_READ_FAILED", "AW_EMPLOYEE_AGENT_CONFIG_UNAVAILABLE", "AW_EMPLOYEE_BINDING_INVALID", "AW_EMPLOYEE_BINDING_MISMATCH", "AW_EMPLOYEE_CAPABILITY_REQUIRED", "AW_EMPLOYEE_CLOSE_FAILED", "AW_EMPLOYEE_CLOSE_RESPONSE_INVALID", "AW_EMPLOYEE_COLD_RESUME_UNSUPPORTED", "AW_EMPLOYEE_CWD_MISMATCH", "AW_EMPLOYEE_ENVIRONMENT_REGISTRATION_FAILED", "AW_EMPLOYEE_ENVIRONMENT_REQUIRED", "AW_EMPLOYEE_EXECUTION_CLOSED", "AW_EMPLOYEE_EXECUTION_ID_REQUIRED", "AW_EMPLOYEE_HOST_AUXILIARY_CONFIG_UNSUPPORTED", "AW_EMPLOYEE_HOST_AUXILIARY_UNSUPPORTED", "AW_EMPLOYEE_HOST_FALLBACK_FORBIDDEN", "AW_EMPLOYEE_MCP_UNSUPPORTED", "AW_EMPLOYEE_MESSAGE_INVALID", "AW_EMPLOYEE_MESSAGE_TOO_LARGE", "AW_EMPLOYEE_OPEN_RESPONSE_INVALID", "AW_EMPLOYEE_READ_MESSAGE_INVALID", "AW_EMPLOYEE_READ_RESPONSE_INVALID", "AW_EMPLOYEE_RELAY_ADDRESS_INVALID", "AW_EMPLOYEE_RELAY_FAILED", "AW_EMPLOYEE_REQUEST_INVALID", "AW_EMPLOYEE_SESSION_UNKNOWN", "AW_EMPLOYEE_WIRE_FAILED", "AW_EMPLOYEE_WRITE_RESPONSE_INVALID"]);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RequestPermissionRequest } from '@agentclientprotocol/sdk/unstable';
|
|
2
|
+
/** ACP diff.oldText=null means a new file. Write alone may overwrite a file. */
|
|
3
|
+
export declare function approvalLabel(request: RequestPermissionRequest, location?: 'host' | 'local'): {
|
|
4
|
+
toolName: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
} | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { sanitizeDisplayLabel } from "./conversation-details.js";
|
|
2
|
+
function display(value) {
|
|
3
|
+
const safe = sanitizeDisplayLabel(sanitizeDisplayLabel(value).replace(/[\u202a-\u202e\u2066-\u2069]/g, '')).trim();
|
|
4
|
+
// Redacted instructions/targets cannot support an informed approval.
|
|
5
|
+
return safe && !safe.includes('[已脱敏:') ? safe : undefined;
|
|
6
|
+
}
|
|
7
|
+
/** ACP diff.oldText=null means a new file. Write alone may overwrite a file. */
|
|
8
|
+
export function approvalLabel(request, location = 'host') {
|
|
9
|
+
const tool = request.toolCall;
|
|
10
|
+
const diffs = tool.content?.filter(item => item.type === 'diff') ?? [];
|
|
11
|
+
const originalName = tool.name ?? tool._meta?.claudeCode?.toolName ?? /^(Write|Edit|MultiEdit) /.exec(tool.title ?? '')?.[1];
|
|
12
|
+
const employee = location === 'local' && originalName?.startsWith('mcp__employee__');
|
|
13
|
+
const name = employee ? originalName.slice('mcp__employee__'.length) : originalName;
|
|
14
|
+
const fileAction = diffs.length > 0 || name === 'Write' || name === 'Edit' || name === 'MultiEdit';
|
|
15
|
+
const paths = diffs.length ? diffs.map(diff => diff.path) : tool.locations?.map(location => location.path) ?? [];
|
|
16
|
+
let title = tool.title ?? '';
|
|
17
|
+
const raw = employee && tool.rawInput && typeof tool.rawInput === 'object' && !Array.isArray(tool.rawInput) ? tool.rawInput : undefined;
|
|
18
|
+
if (employee && raw) {
|
|
19
|
+
if ((fileAction || name === 'Read') && !paths.length && typeof raw.file_path === 'string')
|
|
20
|
+
paths.push(raw.file_path);
|
|
21
|
+
if ((name === 'Glob' || name === 'Grep') && !paths.length)
|
|
22
|
+
paths.push(typeof raw.path === 'string' ? raw.path : '当前文件夹');
|
|
23
|
+
title = typeof raw.description === 'string' ? raw.description : typeof raw.command === 'string' ? raw.command : '';
|
|
24
|
+
if ((name === 'Glob' || name === 'Grep') && typeof raw.pattern === 'string')
|
|
25
|
+
title = raw.pattern;
|
|
26
|
+
}
|
|
27
|
+
if (name && title.startsWith(name + ' '))
|
|
28
|
+
title = title.slice(name.length + 1);
|
|
29
|
+
else if (title === name || title === tool.toolCallId)
|
|
30
|
+
title = '';
|
|
31
|
+
if (fileAction) {
|
|
32
|
+
const action = diffs.length && diffs.every(diff => diff.oldText === null) ? '新建文件'
|
|
33
|
+
: (diffs.length && diffs.every(diff => typeof diff.oldText === 'string')) || name === 'Edit' || name === 'MultiEdit' ? '修改文件' : '写入文件';
|
|
34
|
+
// Observed Write/Edit title is a display hint, never used as a host path.
|
|
35
|
+
if (!paths.length && name && ['Write', 'Edit', 'MultiEdit'].includes(name) && title)
|
|
36
|
+
paths.push(title);
|
|
37
|
+
const unique = [...new Set(paths)];
|
|
38
|
+
const safe = unique.map(display);
|
|
39
|
+
if (!safe.length || safe.some(path => path === undefined) || new Set(safe).size !== safe.length)
|
|
40
|
+
return;
|
|
41
|
+
const names = safe.map(path => path.split(/[\\/]/).at(-1)?.trim());
|
|
42
|
+
if (names.some(name => !name))
|
|
43
|
+
return;
|
|
44
|
+
const files = safe.map((path, index) => names.filter(name => name === names[index]).length > 1 ? path : names[index]);
|
|
45
|
+
return { toolName: action, reason: action + ' ' + files.join('、') };
|
|
46
|
+
}
|
|
47
|
+
const action = (employee ? { Read: '读取文件', Glob: '查找文件', Grep: '搜索内容', Bash: '执行本机命令' }[name] : undefined) ?? { read: '读取文件', edit: '修改文件', delete: '删除文件', move: '移动文件', search: '搜索', execute: employee ? '执行本机命令' : '执行主机命令', think: '分析', fetch: '获取内容', other: '远程操作' }[tool.kind ?? ''] ?? '远程操作';
|
|
48
|
+
const safePaths = paths.map(display);
|
|
49
|
+
if (safePaths.some(path => path === undefined))
|
|
50
|
+
return;
|
|
51
|
+
const description = title ? display(title) : undefined;
|
|
52
|
+
if (title && !description)
|
|
53
|
+
return;
|
|
54
|
+
if (!safePaths.length && !description)
|
|
55
|
+
return;
|
|
56
|
+
return { toolName: action, reason: action + (safePaths.length ? ' ' + safePaths.join('、') : '') + (description ? ':' + description : '') };
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type SshArgv, type BrokerIo, type BrokerSettings } from './broker.ts';
|
|
2
|
+
import { type ConnectApi, type ConnectTarget, type Project, type Runtime, type RuntimeRequest } from './provider.ts';
|
|
3
|
+
import type { Interaction } from './interaction.ts';
|
|
4
|
+
import { type RecoveryJournal } from './recovery.ts';
|
|
5
|
+
/** Test ports are explicit; deployed code always supplies the official-profile journal. */
|
|
6
|
+
export declare function brokerFactory(get: () => ConnectApi | undefined, _ask: Interaction | undefined, configuration: BrokerSettings | (() => BrokerSettings), provided?: {
|
|
7
|
+
argv?: SshArgv;
|
|
8
|
+
io?: (target: ConnectTarget) => BrokerIo;
|
|
9
|
+
journal?: (key: string) => RecoveryJournal;
|
|
10
|
+
}): (target: ConnectTarget, project: Project, initialRequest: RuntimeRequest) => Promise<Runtime>;
|