@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.
Files changed (194) hide show
  1. package/README.md +42 -7
  2. package/dist/acp.d.ts +46 -1
  3. package/dist/acp.js +296 -36
  4. package/dist/approval-label.d.ts +6 -0
  5. package/dist/approval-label.js +57 -0
  6. package/dist/broker-runtime.d.ts +10 -0
  7. package/dist/broker-runtime.js +334 -0
  8. package/dist/broker.d.ts +29 -6
  9. package/dist/broker.js +146 -142
  10. package/dist/claude-local.d.ts +37 -0
  11. package/dist/claude-local.js +207 -0
  12. package/dist/client.js +71 -2
  13. package/dist/conversation-details.d.ts +1 -1
  14. package/dist/conversation-details.js +88 -8
  15. package/dist/employee-endpoint.d.ts +91 -0
  16. package/dist/employee-endpoint.js +141 -0
  17. package/dist/employee-exec.d.ts +53 -0
  18. package/dist/employee-exec.js +423 -0
  19. package/dist/employee-location.d.ts +66 -0
  20. package/dist/employee-location.js +51 -0
  21. package/dist/employee-mcp.d.ts +42 -0
  22. package/dist/employee-mcp.js +181 -0
  23. package/dist/employee-native.d.ts +18 -0
  24. package/dist/employee-native.js +38 -0
  25. package/dist/employee-provider.d.ts +42 -0
  26. package/dist/employee-provider.js +326 -0
  27. package/dist/employee-relay.d.ts +18 -0
  28. package/dist/employee-relay.js +49 -0
  29. package/dist/employee-shell.d.ts +26 -0
  30. package/dist/employee-shell.js +89 -0
  31. package/dist/employee-ssh.d.ts +22 -0
  32. package/dist/employee-ssh.js +123 -0
  33. package/dist/employee-tools.d.ts +117 -0
  34. package/dist/employee-tools.js +292 -0
  35. package/dist/employee-workspace.d.ts +26 -0
  36. package/dist/employee-workspace.js +61 -0
  37. package/dist/index.d.ts +31 -14
  38. package/dist/index.js +335 -42
  39. package/dist/input.d.ts +15 -0
  40. package/dist/input.js +65 -24
  41. package/dist/local-user-turn.d.ts +24 -0
  42. package/dist/local-user-turn.js +103 -0
  43. package/dist/model-catalog.d.ts +55 -0
  44. package/dist/model-catalog.js +254 -0
  45. package/dist/model-configuration.d.ts +51 -0
  46. package/dist/model-configuration.js +212 -0
  47. package/dist/presentation.d.ts +1 -1
  48. package/dist/presentation.js +50 -2
  49. package/dist/protocol-boundary.d.ts +13 -0
  50. package/dist/protocol-boundary.js +49 -0
  51. package/dist/provider.d.ts +54 -2
  52. package/dist/provider.js +92 -29
  53. package/dist/readiness.js +1 -1
  54. package/dist/recovery-checkpoint.d.ts +6 -0
  55. package/dist/recovery-checkpoint.js +71 -0
  56. package/dist/recovery.d.ts +47 -0
  57. package/dist/recovery.js +61 -0
  58. package/dist/remote-details.d.ts +10 -0
  59. package/dist/remote-details.js +14 -4
  60. package/dist/rpc-namespace.d.ts +3 -0
  61. package/dist/rpc-namespace.js +50 -0
  62. package/dist/stage-timing.d.ts +27 -0
  63. package/dist/stage-timing.js +39 -0
  64. package/helpers/ripgrep/LICENSE-vscode +13 -0
  65. package/helpers/ripgrep/PROVENANCE.md +9 -0
  66. package/helpers/ripgrep/darwin-arm64/rg +0 -0
  67. package/helpers/ripgrep/darwin-x64/rg +0 -0
  68. package/helpers/ripgrep/manifest.json +56 -0
  69. package/helpers/ripgrep/notices/COPYING-ripgrep +3 -0
  70. package/helpers/ripgrep/notices/DEPENDENCIES.json +946 -0
  71. package/helpers/ripgrep/notices/LICENCE-PCRE2.md +103 -0
  72. package/helpers/ripgrep/notices/LICENSE-MIT-ripgrep +21 -0
  73. package/helpers/ripgrep/notices/UNLICENSE-ripgrep +24 -0
  74. package/helpers/ripgrep/notices/aho-corasick-1.1.3/COPYING +3 -0
  75. package/helpers/ripgrep/notices/aho-corasick-1.1.3/LICENSE-MIT +21 -0
  76. package/helpers/ripgrep/notices/aho-corasick-1.1.3/UNLICENSE +24 -0
  77. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-APACHE +176 -0
  78. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-MIT +23 -0
  79. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-APACHE +201 -0
  80. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-MIT +27 -0
  81. package/helpers/ripgrep/notices/bstr-1.12.0/COPYING +8 -0
  82. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-APACHE +201 -0
  83. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-MIT +21 -0
  84. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-APACHE +201 -0
  85. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-MIT +25 -0
  86. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-APACHE +201 -0
  87. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-MIT +25 -0
  88. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-APACHE +201 -0
  89. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-MIT +27 -0
  90. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-THIRD-PARTY +593 -0
  91. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-APACHE +201 -0
  92. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-MIT +27 -0
  93. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-APACHE +201 -0
  94. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-MIT +27 -0
  95. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-APACHE +201 -0
  96. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-MIT +27 -0
  97. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-APACHE +201 -0
  98. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-MIT +27 -0
  99. package/helpers/ripgrep/notices/encoding_rs-0.8.35/COPYRIGHT +17 -0
  100. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-APACHE +202 -0
  101. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-MIT +25 -0
  102. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-WHATWG +26 -0
  103. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/COPYING +8 -0
  104. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-APACHE +201 -0
  105. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-MIT +21 -0
  106. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-APACHE +201 -0
  107. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-MIT +25 -0
  108. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-APACHE +201 -0
  109. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-MIT +26 -0
  110. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-APACHE +201 -0
  111. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-MIT +25 -0
  112. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-APACHE +176 -0
  113. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-MIT +23 -0
  114. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-APACHE +201 -0
  115. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-MIT +25 -0
  116. package/helpers/ripgrep/notices/lexopt-0.3.1/LICENSE +19 -0
  117. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-APACHE +176 -0
  118. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-MIT +25 -0
  119. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-APACHE +201 -0
  120. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-MIT +25 -0
  121. package/helpers/ripgrep/notices/memchr-2.7.6/COPYING +3 -0
  122. package/helpers/ripgrep/notices/memchr-2.7.6/LICENSE-MIT +21 -0
  123. package/helpers/ripgrep/notices/memchr-2.7.6/UNLICENSE +24 -0
  124. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-APACHE +201 -0
  125. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-MIT +26 -0
  126. package/helpers/ripgrep/notices/msvc_spectre_libs-0.1.3/LICENSE +21 -0
  127. package/helpers/ripgrep/notices/pcre2-0.2.11/COPYING +3 -0
  128. package/helpers/ripgrep/notices/pcre2-0.2.11/LICENSE-MIT +21 -0
  129. package/helpers/ripgrep/notices/pcre2-0.2.11/UNLICENSE +24 -0
  130. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/COPYING +3 -0
  131. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/LICENSE-MIT +21 -0
  132. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/UNLICENSE +24 -0
  133. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-APACHE +201 -0
  134. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-MIT +25 -0
  135. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-APACHE +176 -0
  136. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-MIT +23 -0
  137. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-APACHE +176 -0
  138. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-MIT +23 -0
  139. package/helpers/ripgrep/notices/r-efi-5.3.0/AUTHORS +74 -0
  140. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-APACHE +201 -0
  141. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-MIT +25 -0
  142. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-APACHE +201 -0
  143. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-MIT +25 -0
  144. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-APACHE +201 -0
  145. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-MIT +25 -0
  146. package/helpers/ripgrep/notices/regex-syntax-0.8.8/src/unicode_tables/LICENSE-UNICODE +57 -0
  147. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-APACHE +176 -0
  148. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-BOOST +23 -0
  149. package/helpers/ripgrep/notices/same-file-1.0.6/COPYING +3 -0
  150. package/helpers/ripgrep/notices/same-file-1.0.6/LICENSE-MIT +21 -0
  151. package/helpers/ripgrep/notices/same-file-1.0.6/UNLICENSE +24 -0
  152. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-APACHE +176 -0
  153. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-MIT +23 -0
  154. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-APACHE +176 -0
  155. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-MIT +23 -0
  156. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-APACHE +176 -0
  157. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-MIT +23 -0
  158. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-APACHE +176 -0
  159. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-MIT +23 -0
  160. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-APACHE +13 -0
  161. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-MIT +21 -0
  162. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-APACHE +176 -0
  163. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-MIT +23 -0
  164. package/helpers/ripgrep/notices/termcolor-1.4.1/COPYING +3 -0
  165. package/helpers/ripgrep/notices/termcolor-1.4.1/LICENSE-MIT +21 -0
  166. package/helpers/ripgrep/notices/termcolor-1.4.1/UNLICENSE +24 -0
  167. package/helpers/ripgrep/notices/textwrap-0.16.2/LICENSE +21 -0
  168. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-APACHE +201 -0
  169. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-MIT +25 -0
  170. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/jemalloc/COPYING +27 -0
  171. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-APACHE +201 -0
  172. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-MIT +25 -0
  173. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-APACHE +176 -0
  174. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-MIT +23 -0
  175. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-UNICODE +39 -0
  176. package/helpers/ripgrep/notices/walkdir-2.5.0/COPYING +3 -0
  177. package/helpers/ripgrep/notices/walkdir-2.5.0/LICENSE-MIT +21 -0
  178. package/helpers/ripgrep/notices/walkdir-2.5.0/UNLICENSE +24 -0
  179. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-APACHE +201 -0
  180. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  181. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-MIT +23 -0
  182. package/helpers/ripgrep/notices/winapi-util-0.1.11/COPYING +3 -0
  183. package/helpers/ripgrep/notices/winapi-util-0.1.11/LICENSE-MIT +21 -0
  184. package/helpers/ripgrep/notices/winapi-util-0.1.11/UNLICENSE +24 -0
  185. package/helpers/ripgrep/notices/windows-link-0.2.1/license-apache-2.0 +201 -0
  186. package/helpers/ripgrep/notices/windows-link-0.2.1/license-mit +21 -0
  187. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-apache-2.0 +201 -0
  188. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-mit +21 -0
  189. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-APACHE +201 -0
  190. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  191. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-MIT +23 -0
  192. package/locale/en.json +1 -1
  193. package/locale/zh.json +2 -2
  194. package/package.json +30 -12
@@ -0,0 +1,212 @@
1
+ export class ModelConfigurationError extends Error {
2
+ code = 'MODEL_CATALOG_UNAVAILABLE';
3
+ constructor(reason, options) {
4
+ super(`无法获取模型列表:${reason}`, options);
5
+ this.name = 'ModelConfigurationError';
6
+ }
7
+ }
8
+ function unavailable(reason) { throw new ModelConfigurationError(reason); }
9
+ function record(value) {
10
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
11
+ }
12
+ function text(value) { return typeof value === 'string' && value.trim().length > 0; }
13
+ function label(value) {
14
+ if (!text(value.name))
15
+ unavailable('配置名称为空或无效');
16
+ if (value.description !== undefined && value.description !== null && typeof value.description !== 'string')
17
+ unavailable('配置说明无效');
18
+ }
19
+ function select(option) {
20
+ if (!text(option.currentValue) || !Array.isArray(option.options) || !option.options.length)
21
+ unavailable('选择器缺少实际当前值或可选列表');
22
+ const values = [];
23
+ const seen = new Set();
24
+ const add = (value) => {
25
+ if (!record(value) || !text(value.value) || 'group' in value || 'options' in value)
26
+ unavailable('可选值格式无效');
27
+ label(value);
28
+ if (seen.has(value.value))
29
+ unavailable('选择器包含重复可选值');
30
+ seen.add(value.value);
31
+ values.push({ value: value.value, name: value.name, ...(value.description === undefined ? {} : { description: value.description }) });
32
+ };
33
+ const grouped = record(option.options[0]) && 'group' in option.options[0];
34
+ const groups = new Set();
35
+ for (const entry of option.options) {
36
+ if (grouped) {
37
+ if (!record(entry) || !text(entry.group) || 'value' in entry || !Array.isArray(entry.options) || !entry.options.length)
38
+ unavailable('分组选择器格式无效或分组为空');
39
+ label(entry);
40
+ if (groups.has(entry.group))
41
+ unavailable('选择器包含重复分组标识');
42
+ groups.add(entry.group);
43
+ for (const value of entry.options)
44
+ add(value);
45
+ }
46
+ else
47
+ add(entry);
48
+ }
49
+ if (!seen.has(option.currentValue))
50
+ unavailable('实际当前值不在远端可选列表中');
51
+ return { id: option.id, currentValue: option.currentValue, values };
52
+ }
53
+ function parse(options) {
54
+ if (!Array.isArray(options) || !options.length)
55
+ unavailable('远端没有提供完整配置列表');
56
+ const ids = new Set();
57
+ const models = [];
58
+ const efforts = [];
59
+ for (const option of options) {
60
+ if (!record(option) || !text(option.id))
61
+ unavailable('配置标识为空或无效');
62
+ label(option);
63
+ if (ids.has(option.id))
64
+ unavailable('配置列表包含重复标识');
65
+ ids.add(option.id);
66
+ if (option.category !== undefined && option.category !== null && typeof option.category !== 'string')
67
+ unavailable('配置类别无效');
68
+ if (option.type === 'select') {
69
+ const selector = select(option);
70
+ if (option.category === 'model')
71
+ models.push(selector);
72
+ if (option.category === 'thought_level')
73
+ efforts.push(selector);
74
+ }
75
+ else if (option.type === 'boolean') {
76
+ if (typeof option.currentValue !== 'boolean')
77
+ unavailable('布尔配置当前值无效');
78
+ if (option.category === 'model' || option.category === 'thought_level')
79
+ unavailable('模型或推理等级配置不是选择器');
80
+ }
81
+ else
82
+ unavailable('配置类型无法识别');
83
+ }
84
+ if (models.length !== 1)
85
+ unavailable(models.length ? '模型选择器不唯一' : '没有可识别的模型选择器');
86
+ if (efforts.length > 1)
87
+ unavailable('推理等级选择器不唯一');
88
+ return { model: models[0], effort: efforts[0] };
89
+ }
90
+ function freeze(value, seen = new WeakSet()) {
91
+ if (value !== null && typeof value === 'object' && !seen.has(value)) {
92
+ seen.add(value);
93
+ for (const item of Object.values(value))
94
+ freeze(item, seen);
95
+ Object.freeze(value);
96
+ }
97
+ return value;
98
+ }
99
+ /** Pure ACP configuration state. All IDs and values come from the given peer data. */
100
+ export class ModelConfiguration {
101
+ rpcSet;
102
+ state;
103
+ parsed;
104
+ revision = 0;
105
+ busy = false;
106
+ constructor(options, rpcSet) {
107
+ this.rpcSet = rpcSet;
108
+ this.update(options);
109
+ }
110
+ get models() {
111
+ if (!this.parsed)
112
+ throw new ModelConfigurationError('当前远端配置列表无效');
113
+ return this.state.models;
114
+ }
115
+ get snapshot() { return this.state; }
116
+ update(options) {
117
+ this.revision++;
118
+ let copy = [];
119
+ try {
120
+ // The wire is JSON data; clone before validation so caller mutations cannot race ACK checks.
121
+ const copied = structuredClone(options);
122
+ if (Array.isArray(copied))
123
+ copy = copied;
124
+ const parsed = parse(copied);
125
+ this.parsed = parsed;
126
+ this.state = freeze({
127
+ configOptions: copy, models: parsed.model.values,
128
+ modelConfigId: parsed.model.id, model: parsed.model.currentValue,
129
+ ...(parsed.effort ? { effortConfigId: parsed.effort.id, effort: parsed.effort.currentValue } : {}),
130
+ effortAvailable: !!parsed.effort, confirmed: false,
131
+ });
132
+ }
133
+ catch (cause) {
134
+ const error = cause instanceof ModelConfigurationError ? cause : new ModelConfigurationError('配置数据无法复制或解析', { cause });
135
+ this.parsed = undefined;
136
+ this.state = freeze({ configOptions: copy, models: [], effortAvailable: false, confirmed: false, failure: { code: error.code, message: error.message } });
137
+ throw error;
138
+ }
139
+ }
140
+ fail(error) {
141
+ this.state = freeze({ ...this.state, confirmed: false, failure: { code: error.code, message: error.message } });
142
+ }
143
+ checkAckRevision(revision, stage) {
144
+ if (revision !== this.revision)
145
+ unavailable(`${stage}回执之后收到其他配置更新,原选择未获确认`);
146
+ }
147
+ async set(id, value, stage) {
148
+ const revision = this.revision;
149
+ let response;
150
+ try {
151
+ response = await this.rpcSet(id, value);
152
+ }
153
+ catch (cause) {
154
+ throw new ModelConfigurationError(`${stage}配置请求失败,未获得远端确认`, { cause });
155
+ }
156
+ if (revision !== this.revision)
157
+ unavailable(`${stage}配置请求期间收到其他配置更新,无法确认回执顺序`);
158
+ if (!record(response) || !Array.isArray(response.configOptions))
159
+ unavailable(`${stage}配置回执缺少完整配置列表`);
160
+ this.update(response.configOptions);
161
+ return this.revision;
162
+ }
163
+ async select(model, effort) {
164
+ if (this.busy)
165
+ throw new ModelConfigurationError('另一个配置选择尚未完成');
166
+ this.busy = true;
167
+ this.state = freeze({ ...this.state, confirmed: false });
168
+ try {
169
+ if (!this.parsed)
170
+ unavailable('当前远端配置列表无效');
171
+ if (!this.parsed.model.values.some(entry => entry.value === model))
172
+ unavailable('所选模型不在实际远端列表中');
173
+ let ackRevision = await this.set(this.parsed.model.id, model, '模型');
174
+ this.checkAckRevision(ackRevision, '模型');
175
+ if (this.state.model !== model)
176
+ unavailable('模型配置回执与所选模型不一致');
177
+ if (effort !== undefined) {
178
+ const selector = this.parsed?.effort;
179
+ if (!selector)
180
+ unavailable('远端没有提供实际推理等级选择器');
181
+ if (!selector.values.some(entry => entry.value === effort))
182
+ unavailable('所选推理等级不在模型回执的实际列表中');
183
+ ackRevision = await this.set(selector.id, effort, '推理等级');
184
+ this.checkAckRevision(ackRevision, '推理等级');
185
+ if (this.state.model !== model)
186
+ unavailable('推理等级回执中的模型发生变化');
187
+ if (!this.state.effortAvailable || this.state.effort !== effort)
188
+ unavailable('推理等级配置回执与所选等级不一致');
189
+ }
190
+ this.checkAckRevision(ackRevision, '选择');
191
+ this.state = freeze({ ...this.state, confirmed: true });
192
+ return this.state;
193
+ }
194
+ catch (cause) {
195
+ const error = cause instanceof ModelConfigurationError ? cause : new ModelConfigurationError('配置选择未能完成核对', { cause });
196
+ this.fail(error);
197
+ throw error;
198
+ }
199
+ finally {
200
+ this.busy = false;
201
+ }
202
+ }
203
+ /** Call immediately before prompt; an idle/new/load/resume update withdraws the ACK. */
204
+ assertSelection(model, effort) {
205
+ if (this.busy || !this.parsed || !this.state.confirmed)
206
+ unavailable('当前模型选择尚未获得完整远端确认');
207
+ if (this.state.model !== model)
208
+ unavailable('当前实际模型与待发送选择不一致');
209
+ if (effort !== undefined && (!this.state.effortAvailable || this.state.effort !== effort))
210
+ unavailable('当前实际推理等级与待发送选择不一致');
211
+ }
212
+ }
@@ -3,4 +3,4 @@ export declare const REMOTE_CODEX_NAME = "Codex\uFF08\u8FDC\u7A0B\u4E3B\u673A\uF
3
3
  export declare const CONNECTION_REQUIRED = "\u8FDC\u7A0B\u4E3B\u673A\u8FDE\u63A5\u7EC4\u4EF6\u6CA1\u6709\u88C5\u597D\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u3002";
4
4
  export declare const TEST_HOST_NOTICE = "\u8FDC\u7A0B\u4E3B\u673A\u6B63\u5728\u4F7F\u7528\u6D4B\u8BD5\u914D\u7F6E\uFF0C\u9047\u5230\u95EE\u9898\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u3002";
5
5
  /** Employee failures use business language; diagnostic codes remain separately available. */
6
- export declare function employeeFailure(code: string): string;
6
+ export declare function employeeFailure(code: string, brand?: 'codex' | 'claude'): string;
@@ -3,11 +3,19 @@ export const REMOTE_CODEX_NAME = 'Codex(远程主机)';
3
3
  export const CONNECTION_REQUIRED = '远程主机连接组件没有装好,请联系管理员。';
4
4
  export const TEST_HOST_NOTICE = '远程主机正在使用测试配置,遇到问题请联系管理员。';
5
5
  /** Employee failures use business language; diagnostic codes remain separately available. */
6
- export function employeeFailure(code) {
6
+ export function employeeFailure(code, brand = 'codex') {
7
7
  const messages = {
8
+ LOCAL_USER_TURN_CHANGED: '本次输入已变化,本轮已停止,请确认对话后重新发送。',
9
+ LOCAL_USER_TURN_UNCONFIRMED: '无法核对本次员工输入,本轮已停止;请在当前会话重新发送。',
10
+ LOCAL_USER_TURN_ACCEPTANCE_INVALID: '本次输入的核验回执不一致,本轮已停止,请联系管理员。',
8
11
  TECHNICAL_DETAILS_UNAVAILABLE: '技术详情暂不可用,请更新远程主机连接组件。',
9
12
  REMOTE_DETAIL_STORAGE_FAILED: '技术详情保存失败,本轮已停止,请联系管理员检查。',
10
13
  REMOTE_DETAIL_DELIVERY_UNKNOWN: '技术详情的持久结果无法核对,本轮已停止。',
14
+ REMOTE_DETAIL_SCOPE_INVALID: '无法核对当前会话的技术详情,本轮已停止;我需要更新后的远程主机连接组件。',
15
+ EMPLOYEE_CONTEXT_UNAVAILABLE: '我需要远程主机连接组件提供当前本机工作区,本轮未执行。',
16
+ EMPLOYEE_ENDPOINT_UNAVAILABLE: '我需要员工电脑的执行连接就绪,本轮未执行;请联系管理员检查连接组件。',
17
+ EMPLOYEE_CONTEXT_CHANGED: '本机文件夹或连接已改变,原工具已停止;请在当前工作区重新开始。',
18
+ EMPLOYEE_WORKSPACE_INVALID: '无法核对这条对话所属的本机文件夹,本轮未执行;请在工作区中重新打开对话。',
11
19
  REMOTE_DETAIL_TOO_LARGE: '技术详情超过已配置的单事件容量,本轮已停止;请联系管理员调整conversationDetails.maxEventBytes。',
12
20
  DEPENDENCY_REQUIRED: CONNECTION_REQUIRED,
13
21
  LOGIN_REQUIRED: '需要登录,请到设置 → 远程主机连接 点登录',
@@ -33,6 +41,12 @@ export function employeeFailure(code) {
33
41
  CONTENT_UNSUPPORTED: '远程 Codex 暂不支持这类附件,请联系管理员。',
34
42
  SYSTEM_PROMPT_UPDATE_PENDING: 'Claude原会话无法保证立即应用已变化的系统或开发者要求,本轮未发送;请新建会话应用新要求。',
35
43
  SYSTEM_PROMPT_UNSUPPORTED: '远端没有声明系统与开发者上下文能力,本轮未发送;需要管理员提供支持该能力的远端适配器。',
44
+ SYSTEM_CONTEXT_TYPE_UNSUPPORTED: '系统或开发者要求包含暂不支持的非文本内容,本轮未新建或发送;请使用文本要求或联系管理员。',
45
+ RECOVERY_CONTEXT_POLICY_UNSUPPORTED: '旧版会话恢复记录仍绑定模型要求,无法安全接回;本轮未发送,请联系管理员核对原会话。',
46
+ SYSTEM_PROMPT_POLICY_UNSUPPORTED: '远端还不支持保留原会话并按新要求继续,请联系管理员更新远端适配器;本轮未发送。',
47
+ SYSTEM_PROMPT_NEW_SESSION_REQUIRED: '会话要求已变化,需要新建 Codex 会话并承接历史后继续;本轮未发送。',
48
+ SESSION_CONTEXT_DIGEST_INVALID: '原会话的要求记录无法核对,本轮未发送;请联系管理员核对原会话。',
49
+ HISTORY_CONTENT_UNSUPPORTED: '此前对话含有暂时无法完整承接的内容,本轮未新建或发送;请联系管理员检查历史内容类型。',
36
50
  SYSTEM_PROMPT_UPDATE_UNSUPPORTED: '远端不能更新原会话的系统与开发者要求,本轮未发送;请新建会话。',
37
51
  PROMPT_CONTENT_UNSUPPORTED: '远端没有声明这类输入能力,本轮未发送;请使用受支持的内容或联系管理员。',
38
52
  ATTACHMENT_REQUIRED: '我需要官方附件组件读取这张图片,请联系管理员检查组件。',
@@ -40,6 +54,40 @@ export function employeeFailure(code) {
40
54
  ACP_MESSAGE_TOO_LARGE: '本轮输入超过会话消息容量,未截断或发送;请联系管理员调整acpMaxMessageBytes。',
41
55
  ACP_CLIENT_LOST: '我需要管理员核对并结束遗留的主机会话;当前保留原进程,没有代答旧审批。',
42
56
  ONESHOT_CONTEXT_REQUIRED: '首版单次模式不支持图片或系统与开发者上下文,请改用会话模式。',
57
+ MODEL_CONFIGURATION_UNSUPPORTED: '单次模式不支持确认当前模型与推理等级,本轮未发送;请改用会话模式。',
58
+ MODEL_SELECTION_REQUIRED: '旧远端恢复记录没有明确模型选择,本轮未发送;请新建会话并选择模型。',
59
+ MODEL_RESOLUTION_UNCONFIRMED: '所选模型的具体型号尚未获得当前会话确认,本轮未发送;请重新选择模型或联系管理员。',
60
+ REMOTE_CLEANUP_UNCONFIRMED: '原主机会话的清理结果尚未确认,已阻断继续发送;请联系管理员核对,不能自动重开。',
43
61
  };
44
- return messages[code] ?? `远程主机暂时无法完成这次操作,请联系管理员。技术详情:诊断代码 ${code}`;
62
+ if (messages[code])
63
+ return messages[code];
64
+ if (code === 'EMPLOYEE_OPERATION_UNSUPPORTED')
65
+ return '当前本机工作区暂不支持单次运行和断开后的冷恢复;请使用会话模式新建会话,原记录保留可查。';
66
+ if (code === 'AW_EMPLOYEE_COLD_RESUME_UNSUPPORTED' || code === 'EMPLOYEE_RECOVERY_UNSUPPORTED')
67
+ return '当前版本暂不支持恢复已关闭的本机执行会话;请新建会话,原记录保留可查。';
68
+ if (code === 'AW_EMPLOYEE_HOST_AUXILIARY_CONFIG_UNSUPPORTED')
69
+ return '当前主机启用的MCP、hooks或插件尚未支持改到员工电脑,本轮没有执行,请联系管理员。';
70
+ if (code === 'AW_EMPLOYEE_HOST_PROJECT_CONFIG_UNSUPPORTED')
71
+ return '主机上存在与本机工作区路径重合的项目配置,当前版本无法安全应用,本轮没有执行,请联系管理员。';
72
+ if (code === 'AW_EMPLOYEE_EXECUTOR_MCP_UNSUPPORTED')
73
+ return '本机工作区配置了附加MCP工具,当前版本尚不支持这类工具,本轮没有继续执行。';
74
+ if (['AW_EMPLOYEE_CONFIG_ADMISSION_INVALID', 'AW_EMPLOYEE_CONFIG_ADMISSION_FAILED', 'AW_EMPLOYEE_EXECUTOR_CONFIG_PROTOCOL_INVALID', 'AW_EMPLOYEE_EXECUTOR_CONFIG_READ_FAILED'].includes(code))
75
+ return '无法核验这次本机工作区的助手配置,本轮没有继续执行,请联系管理员查看技术详情。';
76
+ if (code === 'AW_EMPLOYEE_MCP_UNSUPPORTED')
77
+ return '当前版本尚不支持把附加MCP工具改到员工电脑,本轮没有执行。';
78
+ if (code === 'AW_EMPLOYEE_HOST_AUXILIARY_UNSUPPORTED')
79
+ return '这项辅助操作尚未支持在员工电脑执行,本轮没有执行。';
80
+ if (code === 'AW_EMPLOYEE_SLASH_COMMAND_UNSUPPORTED')
81
+ return '这项快捷命令暂不支持本机执行,请直接描述任务。';
82
+ if (code === 'AW_EMPLOYEE_HOST_FILE_REPORT_UNSUPPORTED')
83
+ return '这项文件报告暂不支持本机工作区;请查看对话和技术详情中的已有执行记录。';
84
+ if (code === 'AW_EMPLOYEE_CONFIG_DISCOVERY_OVERRIDE_UNSUPPORTED')
85
+ return '当前助手的项目配置覆盖与本机执行不兼容,本轮没有执行,请联系管理员。';
86
+ if (code === 'EMPLOYEE_PLATFORM_UNSUPPORTED')
87
+ return brand === 'claude' ? '本机执行目前只支持Mac;当前设备尚不支持,本轮未执行。' : '当前版本的Codex本机执行仅验证了Apple芯片Mac,本轮没有执行。';
88
+ if (code.startsWith('AW_EMPLOYEE_BINARY_'))
89
+ return 'Codex本机执行组件缺失或校验失败,本轮没有执行,请联系管理员。';
90
+ if (code.startsWith('AW_EMPLOYEE_') || code.startsWith('EMPLOYEE_'))
91
+ return '员工电脑的执行连接不可用或工作区已变化,本轮已停止;在途操作结果尚待确认,请查看技术详情。';
92
+ return `远程主机暂时无法完成这次操作,请联系管理员。技术详情:诊断代码 ${code}`;
45
93
  }
@@ -0,0 +1,13 @@
1
+ /** Byte fragments remain only in memory. A cursor is clean only after SDK consumption. */
2
+ export declare class ProtocolBoundary {
3
+ private readonly maxBytes;
4
+ private partial;
5
+ private pending;
6
+ private requests;
7
+ constructor(maxBytes: number);
8
+ receive(bytes: Uint8Array, gate?: (message: Record<string, unknown>) => void): void;
9
+ consumed(message: Record<string, unknown>): void;
10
+ responded(id: unknown): void;
11
+ get complete(): boolean;
12
+ get wholeLine(): boolean;
13
+ }
@@ -0,0 +1,49 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { NeedError } from "./remote.js";
3
+ const fingerprint = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
4
+ /** Byte fragments remain only in memory. A cursor is clean only after SDK consumption. */
5
+ export class ProtocolBoundary {
6
+ maxBytes;
7
+ partial = Buffer.alloc(0);
8
+ pending = [];
9
+ requests = new Set();
10
+ constructor(maxBytes) {
11
+ this.maxBytes = maxBytes;
12
+ }
13
+ receive(bytes, gate) {
14
+ this.partial = Buffer.concat([this.partial, bytes]);
15
+ let end;
16
+ while ((end = this.partial.indexOf(10)) >= 0) {
17
+ if (end > this.maxBytes)
18
+ throw new NeedError('ACP_MESSAGE_TOO_LARGE', '主机消息超过具名会话容量', 'acpMaxMessageBytes');
19
+ const line = this.partial.subarray(0, end).toString('utf8').trim();
20
+ this.partial = this.partial.subarray(end + 1);
21
+ if (!line)
22
+ continue;
23
+ let message;
24
+ try {
25
+ message = JSON.parse(line);
26
+ }
27
+ catch {
28
+ throw new NeedError('ACP_OUTPUT_INVALID', '主机消息不是有效JSON', '完整ACP消息');
29
+ }
30
+ if (!message || typeof message !== 'object' || Array.isArray(message))
31
+ throw new NeedError('ACP_OUTPUT_INVALID', '主机消息结构无效', '完整ACP消息');
32
+ if (gate)
33
+ gate(message);
34
+ else
35
+ this.pending.push(fingerprint(message));
36
+ }
37
+ if (this.partial.length > this.maxBytes)
38
+ throw new NeedError('ACP_MESSAGE_TOO_LARGE', '主机消息超过具名会话容量', 'acpMaxMessageBytes');
39
+ }
40
+ consumed(message) {
41
+ if (this.pending.shift() !== fingerprint(message))
42
+ throw new NeedError('ACP_BOUNDARY_UNCONFIRMED', '主机消息处理位置无法确认', '完整按序的ACP消息');
43
+ if (typeof message.method === 'string' && Object.hasOwn(message, 'id'))
44
+ this.requests.add(JSON.stringify(message.id));
45
+ }
46
+ responded(id) { this.requests.delete(JSON.stringify(id)); }
47
+ get complete() { return this.partial.length === 0 && this.pending.length === 0 && this.requests.size === 0; }
48
+ get wholeLine() { return this.partial.length === 0; }
49
+ }
@@ -9,9 +9,25 @@ import { type AgentDefinition } from './agents.ts';
9
9
  import type { NativeWorkspaceApi, SessionProjectTarget, WorkspaceNotice, LoginProvider } from './connect-contract.ts';
10
10
  import type { DetailChannel } from './remote-details.ts';
11
11
  import type { WorkspaceNoticePresentation } from './workspace-notice.ts';
12
+ import { StageTiming } from './stage-timing.ts';
13
+ import type { RecoveryJournal, AcpRecovery } from './recovery.ts';
14
+ import type { EmployeeProviderOptions } from './employee-provider.ts';
15
+ import type { LocalUserTurnAcceptance } from '@ai-workbench/connect/api';
16
+ import type { EmployeeRouting } from './employee-relay.ts';
17
+ export interface EmployeeExecution {
18
+ hostCwd: string;
19
+ routing: EmployeeRouting;
20
+ notice: string;
21
+ scope: Record<string, unknown>;
22
+ signal?: AbortSignal;
23
+ beginTurn(signal: AbortSignal): Promise<void>;
24
+ endTurn(): void;
25
+ validate(signal?: AbortSignal): Promise<void>;
26
+ }
12
27
  export interface Runtime {
13
28
  identity: string;
14
29
  cwd: string;
30
+ execution?: EmployeeExecution;
15
31
  acpTransport?: {
16
32
  writable: WritableStream<Uint8Array>;
17
33
  readable: ReadableStream<Uint8Array>;
@@ -28,12 +44,43 @@ export interface Runtime {
28
44
  connection?: ConnectionState;
29
45
  workspaceNotice?: WorkspaceNoticePresentation;
30
46
  details?: DetailChannel;
47
+ recoveredSession?: AcpRecovery;
48
+ restartNotice?: string;
49
+ detach?: () => Promise<void>;
50
+ restartInputIds?: readonly string[];
51
+ saveCheckpoint?: (id: string, acp: AcpRecovery) => Promise<boolean>;
31
52
  detailContext?: () => {
32
53
  turn: string | number | null;
33
54
  step: string | number | null;
34
55
  };
56
+ employeeExecutionMeta?: {
57
+ version: 1;
58
+ bindingId: string;
59
+ workspaceId: string;
60
+ cwd: string;
61
+ acpMaxMessageBytes: number;
62
+ };
63
+ employeeExecution?: (params: unknown, signal?: AbortSignal) => Promise<Record<string, unknown>>;
64
+ employeePresentation?: string;
35
65
  }
36
66
  export interface RuntimeRequest extends Invocation {
67
+ userInputIds?: readonly string[];
68
+ localAcceptance?: Readonly<LocalUserTurnAcceptance>;
69
+ localAdmissionCheck?: () => void;
70
+ readonly localAcceptanceId?: string;
71
+ userMessageIds?: readonly string[];
72
+ recoveryJournal?: RecoveryJournal;
73
+ detailChannel?: DetailChannel;
74
+ validateContext?: (ignoreCancellation?: boolean) => Promise<void>;
75
+ verifyRecovery?: () => Promise<boolean>;
76
+ acpMaxMessageBytes?: number;
77
+ employeeScope?: string;
78
+ contextSha256?: string;
79
+ resumeSessionId?: string;
80
+ recoveryCheckpointId?: string;
81
+ bindingRevision?: number;
82
+ currentInputIds?: readonly string[];
83
+ timing?: StageTiming;
37
84
  mode: 'session' | 'one-shot';
38
85
  cwd?: string;
39
86
  identity?: string;
@@ -88,7 +135,7 @@ type Result<T> = {
88
135
  } & Pick<ConnectNeed, 'officialReason'>;
89
136
  };
90
137
  /** 消费正式connect-api 0.7公开子集;X不响应L登录票据、不直接调用登录。 */
91
- export interface ConnectApi extends Pick<LConnectApi, 'getConnectionState' | 'getLoginPromptState' | 'requestLoginPrompt' | 'refreshLoginPrompts'>, Partial<NativeWorkspaceApi>, Partial<Pick<LConnectApi, 'onConnectionState' | 'onLoginPromptState' | 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails'>> {
138
+ export interface ConnectApi extends Pick<LConnectApi, 'getConnectionState' | 'getLoginPromptState' | 'requestLoginPrompt' | 'refreshLoginPrompts'>, Partial<NativeWorkspaceApi>, Partial<Pick<LConnectApi, 'getWorkspaceBindingState' | 'getModelCatalogTarget' | 'onConnectionState' | 'onLoginPromptState' | 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails' | 'getWorkspaceLocationState' | 'getLocalWorkspaceContext' | 'onWorkspaceLocationState' | 'acceptLocalWorkspaceUserTurn'>> {
92
139
  getState?(): ConnectState;
93
140
  getSshTarget(): Promise<Result<ConnectTarget>>;
94
141
  checkLogin(provider: LoginProvider): Promise<Result<{
@@ -112,7 +159,7 @@ export declare function requireLoginCoordinator(api: ConnectApi): void;
112
159
  export declare function requireLoginState(state: LoginPromptState): void;
113
160
  declare module '@deepseek-ai/cordis' {
114
161
  interface Context {
115
- aiWorkbenchConnect: ConnectApi;
162
+ aiWorkbenchConnect: LConnectApi;
116
163
  }
117
164
  interface Events {
118
165
  'ai-workbench/connection-state': (state: ConnectionState) => void;
@@ -134,11 +181,16 @@ export interface NoticeConsumer {
134
181
  reconcile(request: RuntimeRequest, api: ConnectApi): Promise<void>;
135
182
  deliver(notice: WorkspaceNotice, resolved: SessionProjectTarget, request: RuntimeRequest, api: ConnectApi): Promise<WorkspaceNoticePresentation | void>;
136
183
  }
184
+ export type LocalRuntimeProvider = ((request: RuntimeRequest, api: ConnectApi, connection: ConnectionState, target: ConnectTarget) => Promise<Runtime | undefined>) & {
185
+ admit?: (request: RuntimeRequest, api: ConnectApi, connection: ConnectionState, target: ConnectTarget) => Promise<void>;
186
+ };
137
187
  export declare function sameTarget(a: ConnectTarget, b: ConnectTarget): boolean;
138
188
  export declare function connectProvider(get: () => ConnectApi | undefined, options?: {
139
189
  agent?: AgentDefinition;
140
190
  ask?: Interaction;
141
191
  notice?: NoticeConsumer;
192
+ employee?: EmployeeProviderOptions;
193
+ local?: LocalRuntimeProvider;
142
194
  details?: (request: RuntimeRequest, api: ConnectApi, connection: ConnectionState, target: ConnectTarget) => Promise<DetailChannel>;
143
195
  open?: (target: ConnectTarget, project: Project, request: RuntimeRequest) => Promise<Runtime>;
144
196
  }): RuntimeProvider;
package/dist/provider.js CHANGED
@@ -2,6 +2,7 @@ import { loginDiagnostic, loginReason } from "./login-diagnostic.js";
2
2
  import { NeedError } from "./remote.js";
3
3
  import { CONNECTION_REQUIRED, TEST_HOST_NOTICE } from "./presentation.js";
4
4
  import { CODEX_AGENT } from "./agents.js";
5
+ import { StageTiming } from "./stage-timing.js";
5
6
  export function requireLoginCoordinator(api) {
6
7
  if (!['getConnectionState', 'getLoginPromptState', 'requestLoginPrompt', 'refreshLoginPrompts'].every(key => typeof api[key] === 'function'))
7
8
  throw new NeedError('CONNECT_API_MISMATCH', '我需要 connect-api 0.6.1 的统一登录询问与连接状态能力', 'L >=0.6.1');
@@ -153,8 +154,12 @@ export function sameTarget(a, b) {
153
154
  }
154
155
  export function connectProvider(get, options = {}) {
155
156
  const agent = options.agent ?? CODEX_AGENT;
157
+ // Keep the legacy source SDK wrapper's strip-only import graph free of broker runtime syntax.
158
+ // Cache the initialization promise so concurrent sessions share this provider's binding lifetimes.
159
+ let employee;
156
160
  const admissions = new WeakMap();
157
161
  const preflight = async (request) => {
162
+ request.timing ??= new StageTiming();
158
163
  request.signal?.throwIfAborted();
159
164
  const api = get();
160
165
  if (!api)
@@ -163,16 +168,28 @@ export function connectProvider(get, options = {}) {
163
168
  requireLoginCoordinator(api);
164
169
  if (request.mode === 'one-shot' && !agent.oneShot)
165
170
  throw new NeedError('MODE_UNSUPPORTED', '此远程模型仅支持会话模式', '使用会话模式');
166
- const before = api.getConnectionState();
167
- const target = await ensureReady(api, agent.loginProvider, request.signal);
168
- validateTarget(target);
169
- request.signal?.throwIfAborted();
170
- if (get() !== api)
171
- throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接');
172
- const connection = await waitForLogin(api, agent.loginProvider, before, get, request.signal);
171
+ if (options.employee) {
172
+ const module = await import("./employee-provider.js");
173
+ request.localAcceptance = await module.acceptEmployeeUserTurn(get, options.employee, agent, api, request);
174
+ }
175
+ const { target, connection } = await request.timing.run('login-ready', async () => {
176
+ request.localAdmissionCheck?.();
177
+ const before = api.getConnectionState();
178
+ const target = await ensureReady(api, agent.loginProvider, request.signal);
179
+ validateTarget(target);
180
+ request.signal?.throwIfAborted();
181
+ if (get() !== api)
182
+ throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接');
183
+ const connection = await waitForLogin(api, agent.loginProvider, before, get, request.signal);
184
+ return { target, connection };
185
+ });
173
186
  if (request.identity && request.identity !== identity(target))
174
187
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '员工主机与原会话不一致,请新建会话', '原员工主机');
188
+ if (options.employee && request.localAcceptance)
189
+ await (await import("./employee-provider.js")).validateEmployeeUserTurn(get, options.employee, api, request, connection);
190
+ request.localAdmissionCheck?.();
175
191
  admissions.set(request, { api, target, connection });
192
+ await options.local?.admit?.(request, api, connection, target);
176
193
  };
177
194
  const resolve = async (request) => {
178
195
  if (!admissions.has(request))
@@ -180,13 +197,34 @@ export function connectProvider(get, options = {}) {
180
197
  const admitted = admissions.get(request);
181
198
  admissions.delete(request);
182
199
  const { api, target, connection } = admitted;
183
- const stable = () => waitForLogin(api, agent.loginProvider, connection, get, request.signal);
200
+ const stable = () => request.timing.run('connection-confirm', () => waitForLogin(api, agent.loginProvider, connection, get, request.signal));
184
201
  await stable();
185
202
  if (!request.sessionId)
186
203
  throw new NeedError('PROJECT_BINDING_REQUIRED', '我需要当前会话和已选择的工作区,请使用原生工作区入口', 'DSH会话与原生工作区');
204
+ if (options.employee) {
205
+ employee ??= import("./employee-provider.js").then(module => module.createEmployeeProvider(get, options, agent));
206
+ const local = await (await employee)({ api, target, connection, request, stable: signal => request.timing.run('connection-confirm', () => waitForLogin(api, agent.loginProvider, connection, get, signal)) });
207
+ if (local) {
208
+ local.loginRequired = () => offerLogin(api, request, get, agent);
209
+ return local;
210
+ }
211
+ }
212
+ if (agent.loginProvider === 'claude' && options.local) {
213
+ const runtime = await options.local(request, api, connection, target);
214
+ if (runtime) {
215
+ try {
216
+ await stable();
217
+ }
218
+ catch (error) {
219
+ await runtime.dispose?.();
220
+ throw error;
221
+ }
222
+ return runtime;
223
+ }
224
+ }
187
225
  await options.notice?.reconcile(request, api);
188
226
  await stable();
189
- const resolved = unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal));
227
+ const resolved = await request.timing.run('workspace-resolve', async () => unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal)));
190
228
  await stable();
191
229
  if (!resolved || resolved.instanceId !== connection.instanceId || resolved.epoch !== connection.epoch)
192
230
  throw new NeedError('CONNECTION_LOST', '工作区解析回复已失效', '当前连接代次');
@@ -196,32 +234,57 @@ export function connectProvider(get, options = {}) {
196
234
  throw new NeedError('PROJECT_BINDING_INVALID', '工作区绑定与当前会话或可信主机不一致', 'L核验的原生工作区绑定');
197
235
  if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
198
236
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
199
- const details = await options.details?.(request, api, connection, target);
200
- await stable();
201
- let workspaceNotice = undefined;
202
- if (resolved.notice) {
203
- if (!options.notice)
204
- throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
205
- workspaceNotice = await options.notice.deliver(resolved.notice, resolved, request, api);
206
- await stable();
207
- }
208
- if (!options.open)
209
- throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
210
- const runtime = await options.open(resolved.target, project, { ...request, agent });
237
+ const details = await request.timing.run('details-open', async () => options.details?.(request, api, connection, target));
211
238
  try {
239
+ if (details)
240
+ request.timing.bind(detail => details.record(detail));
212
241
  await stable();
242
+ let workspaceNotice = undefined;
243
+ if (resolved.notice) {
244
+ if (!options.notice)
245
+ throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
246
+ workspaceNotice = await request.timing.run('workspace-notice', () => options.notice.deliver(resolved.notice, resolved, request, api));
247
+ await stable();
248
+ }
249
+ if (!options.open)
250
+ throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
251
+ const observedBinding = api.getWorkspaceBindingState?.();
252
+ const validateContext = async (ignoreCancellation = false) => {
253
+ const check = () => waitForLogin(api, agent.loginProvider, connection, get, ignoreCancellation ? undefined : request.signal);
254
+ await check();
255
+ if (observedBinding) {
256
+ const current = api.getWorkspaceBindingState();
257
+ if (current.instanceId !== observedBinding.instanceId || !current.items.some(item => item.bindingId === resolved.bindingId && item.workspaceId === resolved.workspaceId && item.status === 'ready'))
258
+ throw new NeedError('SESSION_CONTEXT_MISMATCH', '当前工作区绑定已改变,原会话操作未执行', '原生会话原绑定');
259
+ }
260
+ await check();
261
+ };
262
+ const runtime = await options.open(resolved.target, project, { ...request, agent, bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, bindingRevision: resolved.bindingRevision, detailChannel: details, validateContext });
263
+ try {
264
+ await stable();
265
+ }
266
+ catch (error) {
267
+ await runtime.dispose?.();
268
+ throw error;
269
+ }
270
+ // The transport can outlive a turn; presentation belongs only to this invocation.
271
+ runtime.workspaceNotice = workspaceNotice || undefined;
272
+ runtime.details = details ?? runtime.details;
273
+ runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
274
+ runtime.connection = { ...connection };
275
+ runtime.loginRequired = () => offerLogin(api, request, get, agent);
276
+ return runtime;
213
277
  }
214
278
  catch (error) {
215
- await runtime.dispose?.();
279
+ try {
280
+ await details?.flush();
281
+ }
282
+ catch (diagnostic) {
283
+ const code = (value) => value instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(value.code) ? value.code : 'UNKNOWN_FAILURE';
284
+ throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能保存;本轮已停止', `原错误代码 ${code(error)};诊断错误代码 ${code(diagnostic)}`);
285
+ }
216
286
  throw error;
217
287
  }
218
- // The transport can outlive a turn; presentation belongs only to this invocation.
219
- runtime.workspaceNotice = workspaceNotice || undefined;
220
- runtime.details = details ?? runtime.details;
221
- runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
222
- runtime.connection = { ...connection };
223
- runtime.loginRequired = () => offerLogin(api, request, get, agent);
224
- return runtime;
225
288
  };
226
289
  resolve.preflight = preflight;
227
290
  return resolve;
package/dist/readiness.js CHANGED
@@ -46,7 +46,7 @@ function details(target) {
46
46
  return [{ label: '主机配置', value: h.enforcement }, { label: '检查退出码', value: String(h.checkExitCode) }, ...h.failedChecks.map(item => ({ label: '未通过检查', value: `${item.id}(${item.status})` })), ...h.unverifiedChecks.map(item => ({ label: '未核实检查', value: `${item.id}(${item.status})` }))];
47
47
  }
48
48
  function blocked(code, technical = [], agent = CODEX_AGENT) {
49
- return { phase: 'blocked', loginStatus: 'unknown', title: `无法确认 ${agent.name.zh} 登录状态`, message: employeeFailure(code), technical: [...technical, { label: '诊断代码', value: code }] };
49
+ return { phase: 'blocked', loginStatus: 'unknown', title: `无法确认 ${agent.name.zh} 登录状态`, message: employeeFailure(code, agent.loginProvider), technical: [...technical, { label: '诊断代码', value: code }] };
50
50
  }
51
51
  /** L owns both evidence and inquiry. Missing per-brand evidence is never false. */
52
52
  function snapshot(api, agent = CODEX_AGENT) {