@ai-workbench/remote-agent 0.0.0-stage → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +58 -2
  2. package/cordis.patch.yml +3 -0
  3. package/dist/acp.d.ts +54 -0
  4. package/dist/acp.js +341 -0
  5. package/dist/agents.d.ts +15 -0
  6. package/dist/agents.js +4 -0
  7. package/dist/broker.d.ts +67 -0
  8. package/dist/broker.js +397 -0
  9. package/dist/client.js +36 -0
  10. package/dist/connect-contract.d.ts +26 -0
  11. package/dist/connect-contract.js +1 -0
  12. package/dist/conversation-details.d.ts +47 -0
  13. package/dist/conversation-details.js +319 -0
  14. package/dist/host-layout.d.ts +8 -0
  15. package/dist/host-layout.js +9 -0
  16. package/dist/index.d.ts +114 -0
  17. package/dist/index.js +360 -0
  18. package/dist/input.d.ts +12 -0
  19. package/dist/input.js +49 -0
  20. package/dist/interaction.d.ts +19 -0
  21. package/dist/interaction.js +1 -0
  22. package/dist/job.d.ts +17 -0
  23. package/dist/job.js +57 -0
  24. package/dist/mapping.d.ts +46 -0
  25. package/dist/mapping.js +230 -0
  26. package/dist/oneshot.d.ts +13 -0
  27. package/dist/oneshot.js +78 -0
  28. package/dist/presentation.d.ts +6 -0
  29. package/dist/presentation.js +45 -0
  30. package/dist/provider-locale.d.ts +19 -0
  31. package/dist/provider-locale.js +73 -0
  32. package/dist/provider.d.ts +144 -0
  33. package/dist/provider.js +164 -0
  34. package/dist/readiness.d.ts +28 -0
  35. package/dist/readiness.js +192 -0
  36. package/dist/remote-details.d.ts +26 -0
  37. package/dist/remote-details.js +250 -0
  38. package/dist/remote.d.ts +34 -0
  39. package/dist/remote.js +51 -0
  40. package/dist/sdk-ssh-wrapper.d.ts +2 -0
  41. package/dist/sdk-ssh-wrapper.js +34 -0
  42. package/dist/workspace-notice.d.ts +47 -0
  43. package/dist/workspace-notice.js +273 -0
  44. package/locale/en.json +10 -0
  45. package/locale/zh.json +10 -0
  46. package/package.json +97 -4
package/dist/index.js ADDED
@@ -0,0 +1,360 @@
1
+ import { SessionId } from '@deepseek-ai/dsh-session';
2
+ import Schema from '@deepseek-ai/schemastery';
3
+ import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { createHash } from 'node:crypto';
6
+ import * as unstable from '@agentclientprotocol/sdk/unstable';
7
+ import * as acp from '@agentclientprotocol/sdk';
8
+ import { AcpSession } from "./acp.js";
9
+ import { runOneShot } from "./oneshot.js";
10
+ import { StreamMapper } from "./mapping.js";
11
+ import { connectProvider } from "./provider.js";
12
+ import { brokerFactory } from "./broker.js";
13
+ import { AGENTS, CODEX_AGENT } from "./agents.js";
14
+ import { prepareInput } from "./input.js";
15
+ import { createWorkspaceNoticeConsumer, isWorkspaceNoticeMessage } from "./workspace-notice.js";
16
+ import { NeedError } from "./remote.js";
17
+ import { employeeFailure } from "./presentation.js";
18
+ import { createRemoteDetails } from "./remote-details.js";
19
+ import { PROVIDER_NAMES, ProviderLocaleRemote } from "./provider-locale.js";
20
+ export const name = 'ai-workbench-remote-agent';
21
+ export const settingsNamespace = 'ai-workbench-codex';
22
+ // L can be installed/enabled after X; keeping X mounted lets it explain the prerequisite.
23
+ export const inject = ['llm'];
24
+ /** Owner-approved defaults; administrators may override through the existing settings. */
25
+ export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never' });
26
+ export const Config = Schema.object({
27
+ mode: Schema.union([Schema.const('session').i18n({ en: 'Session', zh: '会话模式' }), Schema.const('one-shot').i18n({ en: 'One-shot', zh: '单次模式' })]).i18n({ en: 'Run mode; start a new session after switching modes', zh: 'Codex运行模式;Claude固定会话模式;切换后请新建会话' }).default(DEFAULT_CONFIG.mode).volatile(),
28
+ acpMaxMessageBytes: Schema.number().min(1).step(1).i18n({ en: 'Session message limit; default 4 MiB, adjustable by administrators', zh: '会话消息容量;默认4 MiB,管理员可调整' }).default(DEFAULT_CONFIG.acpMaxMessageBytes).volatile(),
29
+ brokerMaxFrameBytes: Schema.number().min(1).step(1).i18n({ en: 'Host output chunk limit; default 2 MiB, adjustable by administrators', zh: '主机回传单段容量;默认2 MiB,管理员可调整' }).default(DEFAULT_CONFIG.brokerMaxFrameBytes).volatile(),
30
+ brokerControlMaxBytes: Schema.number().min(1).step(1).i18n({ en: 'Host session control reply limit; default 1 MiB, adjustable by administrators', zh: '主机会话管理回复容量;默认1 MiB,管理员可调整' }).default(DEFAULT_CONFIG.brokerControlMaxBytes).volatile(),
31
+ oneShotSandbox: Schema.union(['read-only', 'workspace-write', 'danger-full-access']).i18n({ en: 'One-shot sandbox; workspace changes are allowed by default', zh: '单次任务沙箱;默认允许工作区内修改' }).default(DEFAULT_CONFIG.oneShotSandbox).volatile(),
32
+ oneShotApproval: Schema.const('never').i18n({ en: 'Non-interactive approval; permissions follow the launch policy, never automatic approval', zh: '单次模式无交互审批;权限由启动策略决定,不表示自动批准' }).default(DEFAULT_CONFIG.oneShotApproval).volatile(),
33
+ });
34
+ /** DSH volatile settings are handles; adapters freeze their values once per invocation. */
35
+ export function readConfig(config) {
36
+ return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, typeof value === 'object' && value !== null && 'get' in value ? value.get() : value]));
37
+ }
38
+ function readPointer(options, agent) {
39
+ const last = options.messages.findLast(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
40
+ if (!last || last.role !== 'assistant')
41
+ return;
42
+ if (last.source.provider !== options.provider || last.source.replayState === undefined)
43
+ throw new NeedError('SESSION_STATE_MISSING', '已有对话缺少当前模型的远端恢复标识,请新建会话', '当前模型的远端恢复记录');
44
+ const envelope = last.source.replayState;
45
+ const pointer = envelope?.response;
46
+ if (pointer?.version !== 2 || pointer.agent !== agent.id || (pointer.mode !== 'session' && pointer.mode !== 'one-shot') || ![pointer.identity, pointer.cwd, pointer.sessionId].every(value => typeof value === 'string' && value.length > 0))
47
+ throw new NeedError('SESSION_STATE_INVALID', '原对话缺少新版工作区/模型绑定,不能静默迁移,请新建会话', '有效的新一代远端会话恢复记录');
48
+ if (options.sessionId && pointer.dshSessionId !== options.sessionId)
49
+ throw new NeedError('SESSION_CONTEXT_MISMATCH', '此恢复记录属于另一会话,复制或分叉后不能沿用原远端进程,请新建会话', '当前DSH会话自己的远端恢复记录');
50
+ return pointer;
51
+ }
52
+ export class CodexRemoteAdapter extends LlmAdapter {
53
+ resolveRuntime;
54
+ approve;
55
+ providerName;
56
+ agent;
57
+ readImage;
58
+ config;
59
+ sessions = new Map();
60
+ turns = new Map();
61
+ lifetime = new AbortController();
62
+ constructor(config, resolveRuntime, approve, providerName = () => PROVIDER_NAMES.en, agent = CODEX_AGENT, readImage) {
63
+ super();
64
+ this.resolveRuntime = resolveRuntime;
65
+ this.approve = approve;
66
+ this.providerName = providerName;
67
+ this.agent = agent;
68
+ this.readImage = readImage;
69
+ this.config = typeof config === 'function' ? config : () => config;
70
+ }
71
+ providerInfo(provider) { return { id: provider, name: this.providerName() }; }
72
+ async listModels(provider) { return [{ provider, id: 'remote', name: this.providerName(), inputModalities: ((this.agent.oneShot && this.config().mode === 'one-shot') ? ['text'] : ['text', 'image']) }]; }
73
+ providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
74
+ async disposeSession(sessionId) {
75
+ this.turns.get(sessionId)?.abort();
76
+ const current = this.sessions.get(sessionId);
77
+ if (!current)
78
+ return;
79
+ this.sessions.delete(sessionId);
80
+ current.session.close();
81
+ await current.runtime.dispose?.();
82
+ }
83
+ async dispose() {
84
+ this.lifetime.abort();
85
+ const results = await Promise.allSettled([...this.sessions.keys()].map(id => this.disposeSession(id)));
86
+ const failure = results.find(result => result.status === 'rejected');
87
+ if (failure?.status === 'rejected')
88
+ throw failure.reason;
89
+ }
90
+ async *stream(options) {
91
+ try {
92
+ yield* this.generate(options);
93
+ }
94
+ catch (error) {
95
+ if (error instanceof NeedError) {
96
+ const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
97
+ const message = employeeFailure(error.code) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '');
98
+ throw new LlmError(message, error.code, { cause: new Error(`诊断代码:${error.code};${error.requires}`) });
99
+ }
100
+ throw error;
101
+ }
102
+ }
103
+ async *generate(original) {
104
+ if (original.purpose)
105
+ throw new NeedError('PURPOSE_UNSUPPORTED', '此远程模型不处理后台标题或上下文整理请求,未发送模型请求', '独立的后台请求能力');
106
+ if (this.lifetime.signal.aborted)
107
+ throw new NeedError('DISPOSED', '远程模型插件已关闭', '启用远程模型插件');
108
+ const key = original.sessionId;
109
+ if (key && this.turns.has(key))
110
+ throw new NeedError('TURN_BUSY', '当前会话正在处理上一轮', '等待上一轮结束');
111
+ const turn = new AbortController();
112
+ if (key)
113
+ this.turns.set(key, turn);
114
+ const signal = AbortSignal.any([this.lifetime.signal, turn.signal, ...(original.signal ? [original.signal] : [])]);
115
+ const options = { ...original, signal };
116
+ let runtime;
117
+ let session;
118
+ let live;
119
+ let succeeded = false;
120
+ let remoteAttempted = false;
121
+ let activeMapper;
122
+ const config = this.config();
123
+ const mode = this.agent.oneShot ? config.mode : 'session';
124
+ try {
125
+ if (!mode || (mode === 'session' && (!Number.isSafeInteger(config.acpMaxMessageBytes) || config.acpMaxMessageBytes < 1)))
126
+ throw new NeedError('SETTINGS_REQUIRED', '远程模型连接设置无效,请联系管理员检查', '有效的mode/acpMaxMessageBytes');
127
+ const previous = readPointer(options, this.agent);
128
+ if (previous && previous.mode !== mode)
129
+ throw new NeedError('SESSION_CONTEXT_MISMATCH', '运行模式与原会话不一致,请新建会话', '原会话模式');
130
+ live = key ? this.sessions.get(key) : undefined;
131
+ if (live?.failure)
132
+ throw new NeedError('SESSION_STATE_UNCONFIRMED', '上轮远端状态未确认,不能自动重发或重开,请新建会话并核对项目', '管理员核对原会话状态');
133
+ if (live && !previous)
134
+ throw new NeedError('SESSION_STATE_MISSING', '上一轮回复尚无持久恢复记录,不能继续当前远端会话', '原会话的持久回复记录');
135
+ const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes]);
136
+ if (live && live.configuration !== configuration)
137
+ throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
138
+ const request = { sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, retainProcess: !!key && mode === 'session', signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) };
139
+ await this.resolveRuntime.preflight?.(request);
140
+ const input = await prepareInput(options, this.readImage);
141
+ const contextSha256 = createHash('sha256').update(input.systemPrompt ?? '').digest('hex');
142
+ if (previous && this.agent.loginProvider === 'claude' && previous.contextSha256 !== contextSha256)
143
+ throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
144
+ if (live) {
145
+ live.session.validateContext({ systemPrompt: input.systemPrompt });
146
+ live.session.validatePrompt(input.prompt);
147
+ }
148
+ if (mode === 'one-shot' && (input.systemPrompt || input.prompt.some(block => block.type !== 'text')))
149
+ throw new NeedError('ONESHOT_CONTEXT_REQUIRED', '首版单次模式不支持图片或系统/开发者上下文,请使用会话模式', 'Codex会话模式');
150
+ if (mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
151
+ throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
152
+ runtime = await this.resolveRuntime(request);
153
+ signal.throwIfAborted();
154
+ if (previous && (previous.identity !== runtime.identity || previous.cwd !== runtime.cwd || previous.bindingId && previous.bindingId !== runtime.binding?.bindingId || previous.workspaceId && previous.workspaceId !== runtime.binding?.workspaceId))
155
+ throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的员工主机或工作区已改变,请新建会话', '原会话的可信目标和工作区');
156
+ const details = runtime.details;
157
+ if (!details)
158
+ throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
159
+ const mapper = new StreamMapper(mode === 'session' ? (live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
160
+ activeMapper = mapper;
161
+ let id;
162
+ let stop;
163
+ if (runtime.workspaceNotice) {
164
+ yield* mapper.text(runtime.workspaceNotice.text);
165
+ yield* mapper.flush();
166
+ }
167
+ if (mode === 'session') {
168
+ if (live && live.runtime !== runtime)
169
+ throw new NeedError('SESSION_TRANSPORT_MISMATCH', '原会话的主机通道已更换,不能自动切换进程', '原主机会话通道');
170
+ if (!runtime.acpTransport)
171
+ throw new NeedError('DEPENDENCY_REQUIRED', '我需要该员工主机上的ACP通道', 'S提供的ACP命令与aw-session');
172
+ const queue = [];
173
+ let wake;
174
+ let done = false;
175
+ let failure;
176
+ const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
177
+ const hooks = { broker: runtime.broker, approve: (permission, approvalSignal) => this.approve?.(permission, approvalSignal, options) ?? Promise.resolve('unavailable'), onUpdate: notification => { try {
178
+ push(mapper.acp(notification.update));
179
+ }
180
+ catch (error) {
181
+ failure = error;
182
+ session?.close();
183
+ } }, onResponse: response => push(mapper.acpResponse(response)), onDetail: detail => details.record(detail) };
184
+ const fresh = !live;
185
+ if (fresh) {
186
+ const transport = runtime.acpTransport instanceof acp.AgentApp || runtime.acpTransport instanceof unstable.AgentApp ? runtime.acpTransport : acp.ndJsonStream(runtime.acpTransport.writable, runtime.acpTransport.readable, { maxMessageBytes: config.acpMaxMessageBytes });
187
+ session = new AcpSession(transport, hooks);
188
+ live = { session, runtime, configuration, usageSnapshot: mapper.usageSnapshot };
189
+ if (key)
190
+ this.sessions.set(key, live);
191
+ }
192
+ else {
193
+ session = live.session;
194
+ session.setHooks(hooks);
195
+ if (previous?.sessionId !== session.id)
196
+ throw new NeedError('SESSION_STATE_INVALID', '持久会话标识与当前进程不一致', '原会话的恢复标识');
197
+ }
198
+ const task = (async () => {
199
+ remoteAttempted = true;
200
+ if (fresh)
201
+ await session.start(runtime.cwd, previous?.sessionId, signal, { systemPrompt: input.systemPrompt });
202
+ else {
203
+ const updated = await session.updateContext({ systemPrompt: input.systemPrompt }, signal);
204
+ if (updated && this.agent.loginProvider === 'claude')
205
+ throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
206
+ }
207
+ await details.flush();
208
+ details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
209
+ id = session.id;
210
+ try {
211
+ return await session.prompt(input.prompt, signal);
212
+ }
213
+ finally {
214
+ details.endTurn();
215
+ }
216
+ })();
217
+ void task.then(value => { stop = value; }, error => { failure ??= runtime.broker?.error ?? error; }).finally(() => { done = true; wake?.(); wake = undefined; });
218
+ while (!done || queue.length) {
219
+ if (queue.length)
220
+ yield queue.shift();
221
+ else
222
+ await new Promise(resolve => { wake = resolve; });
223
+ }
224
+ if (failure)
225
+ throw failure;
226
+ if (stop === 'cancelled')
227
+ throw new NeedError('ABORTED', '远端本轮已取消', '核对主机任务状态');
228
+ if (stop === 'refusal')
229
+ for (const chunk of mapper.text('\n[远端拒绝] 此请求未执行。\n'))
230
+ yield chunk;
231
+ }
232
+ else {
233
+ if (!runtime.oneShot)
234
+ throw new NeedError('DEPENDENCY_REQUIRED', '我需要主机单次运行助手', 'S安装的aw-codex-once');
235
+ details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
236
+ const text = input.prompt.map(block => block.type === 'text' ? block.text : '').join('\n');
237
+ for await (const event of runOneShot(runtime.oneShot, { wrapperPath: fileURLToPath(new URL('./sdk-ssh-wrapper.js', import.meta.url)), sandboxMode: config.oneShotSandbox, approvalPolicy: config.oneShotApproval }, text, previous?.sessionId, signal)) {
238
+ if (event.type === 'thread.started')
239
+ id = event.thread_id;
240
+ for (const chunk of mapper.sdk(event))
241
+ yield chunk;
242
+ }
243
+ }
244
+ details.endTurn();
245
+ await details.flush();
246
+ if (!id)
247
+ throw new NeedError('SESSION_ID_MISSING', '远端没有返回会话标识', '有效的远端会话标识');
248
+ if (live)
249
+ live.usageSnapshot = mapper.usageSnapshot;
250
+ yield* mapper.flush();
251
+ const pointer = { ...(runtime.workspaceNotice ? { presentation: { workspaceNotice: runtime.workspaceNotice } } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), mode, identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
252
+ succeeded = true;
253
+ yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
254
+ }
255
+ catch (error) {
256
+ if (live && (remoteAttempted || runtime === live.runtime))
257
+ live.failure = { cause: error };
258
+ let reportingFailure;
259
+ try {
260
+ await runtime?.details?.flush();
261
+ }
262
+ catch (failure) {
263
+ reportingFailure = { cause: failure };
264
+ }
265
+ if (activeMapper)
266
+ yield* activeMapper.flush();
267
+ if (reportingFailure !== undefined)
268
+ throw reportingFailure.cause;
269
+ throw error;
270
+ }
271
+ finally {
272
+ if (!succeeded || !key || mode !== 'session') {
273
+ session?.close();
274
+ await runtime?.dispose?.();
275
+ }
276
+ if (key && this.turns.get(key) === turn)
277
+ this.turns.delete(key);
278
+ }
279
+ }
280
+ }
281
+ /** One official registration owns both routes, preserving opaque replay ownership. */
282
+ export class RemoteAgentsAdapter extends LlmAdapter {
283
+ adapters = new Map();
284
+ constructor(config, makeRuntime, approve, nameFor, readImage) { super(); for (const agent of AGENTS)
285
+ this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage)); }
286
+ adapter(provider) { const adapter = this.adapters.get(provider); if (!adapter)
287
+ throw new LlmError('远程模型未注册', 'PROVIDER_NOT_FOUND'); return adapter; }
288
+ providerInfo(provider) { return this.adapter(provider).providerInfo(provider); }
289
+ listModels(provider) { return this.adapter(provider).listModels(provider); }
290
+ providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
291
+ stream(options) { return this.adapter(options.provider).stream(options); }
292
+ async disposeSession(sessionId) { await Promise.all([...this.adapters.values()].map(adapter => adapter.disposeSession(sessionId))); }
293
+ async dispose() { await Promise.all([...this.adapters.values()].map(adapter => adapter.dispose())); }
294
+ }
295
+ export function apply(ctx, live) {
296
+ const config = () => readConfig(live);
297
+ const get = () => ctx.get('aiWorkbenchConnect');
298
+ let language = 'en';
299
+ const readImage = async (ref, signal) => {
300
+ const attachments = ctx.get('attachments');
301
+ if (typeof attachments?.readImage !== 'function')
302
+ throw new NeedError('ATTACHMENT_REQUIRED', '我需要官方附件组件读取这张图片', '官方attachments.readImage');
303
+ return attachments.readImage(ref, signal);
304
+ };
305
+ const openByAgent = new Map(AGENTS.map(agent => [agent.id, brokerFactory(get, undefined, config)]));
306
+ const detailManagers = new Map();
307
+ const adapter = new RemoteAgentsAdapter(config, agent => connectProvider(get, { agent, notice: createWorkspaceNoticeConsumer(ctx, agent), details: async (request, api, connection, target) => {
308
+ const profile = ctx.get('profileContext');
309
+ if (typeof profile?.dir !== 'string')
310
+ throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用:缺少官方profile目录。', '官方profileContext.dir');
311
+ let manager = detailManagers.get(profile.dir);
312
+ if (!manager) {
313
+ manager = createRemoteDetails({ profileDir: profile.dir, getApi: get });
314
+ detailManagers.set(profile.dir, manager);
315
+ }
316
+ return manager.open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection });
317
+ }, open: async (...args) => {
318
+ const runtime = await openByAgent.get(agent.id)(...args);
319
+ runtime.detailContext = () => {
320
+ const events = args[2].sessionId ? ctx.get('agents')?.get(SessionId(args[2].sessionId))?.session.snapshotEvents() : undefined;
321
+ let turn = null, step = null;
322
+ for (const event of events ?? []) {
323
+ if (event.type === 'turn/start') {
324
+ turn = event.data.turn;
325
+ step = null;
326
+ }
327
+ else if (event.type === 'turn/end') {
328
+ turn = null;
329
+ step = null;
330
+ }
331
+ else if (event.type === 'step/start' && event.data.turn === turn)
332
+ step = event.data.step;
333
+ else if (event.type === 'step/end')
334
+ step = null;
335
+ }
336
+ return { turn, step };
337
+ };
338
+ return runtime;
339
+ } }), dshApproval(ctx), agent => agent.name[language], readImage);
340
+ const ids = AGENTS.map(agent => agent.id);
341
+ const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: settingsNamespace, settingsPath: [] }));
342
+ const directory = ctx.llm.registerConfigurableProviders(entries());
343
+ const registration = ctx.llm.registerAdapter(ids, adapter);
344
+ new ProviderLocaleRemote(ctx, next => { language = next; directory.replace(entries()); registration.replace(ids); });
345
+ const cleanup = (id) => adapter.disposeSession(id);
346
+ ctx.on('agent/disposed', ({ agent }) => cleanup(agent.session.id));
347
+ ctx.on('session/disposed', session => cleanup(session.id));
348
+ ctx.on('workspace/session-stop', ((request) => cleanup(request.sessionId)));
349
+ ctx.effect(() => () => adapter.dispose());
350
+ }
351
+ export function dshApproval(ctx) {
352
+ return async (request, signal, options, context) => {
353
+ const agent = options.sessionId ? ctx.get('agents')?.get(options.sessionId) : undefined;
354
+ const approval = ctx.get('approval');
355
+ if (!agent || !approval)
356
+ return 'unavailable';
357
+ return approval.request({ agent, toolName: request.toolCall.name ?? request.toolCall.title ?? '远端工具',
358
+ reason: `${context ?? '主机'} · 会话 ${request.sessionId}:${request.toolCall.title ?? request.toolCall.toolCallId}`, signal });
359
+ };
360
+ }
@@ -0,0 +1,12 @@
1
+ import type { ContentBlock as AcpContentBlock } from '@agentclientprotocol/sdk';
2
+ import type { GenerateOptions, ImageBlock } from '@deepseek-ai/dsh-llm';
3
+ export type ImageReader = (ref: ImageBlock['attachment'], signal?: AbortSignal) => Promise<{
4
+ ref: ImageBlock['attachment'];
5
+ data: Uint8Array;
6
+ }>;
7
+ export interface PreparedInput {
8
+ prompt: AcpContentBlock[];
9
+ systemPrompt?: string;
10
+ }
11
+ /** The attachment service supplies verified bytes; client paths never cross SSH. */
12
+ export declare function prepareInput(options: GenerateOptions, readImage?: ImageReader): Promise<PreparedInput>;
package/dist/input.js ADDED
@@ -0,0 +1,49 @@
1
+ import { NeedError } from "./remote.js";
2
+ import { isWorkspaceNoticeMessage, withoutWorkspaceNotice } from "./workspace-notice.js";
3
+ /** The attachment service supplies verified bytes; client paths never cross SSH. */
4
+ export async function prepareInput(options, readImage) {
5
+ options.signal?.throwIfAborted();
6
+ const messages = options.messages.filter(message => !isWorkspaceNoticeMessage(message)).map(withoutWorkspaceNotice);
7
+ const context = [];
8
+ if (options.system)
9
+ context.push('系统上下文:\n' + options.system);
10
+ for (const message of messages) {
11
+ if (message.role !== 'system' && message.role !== 'developer')
12
+ continue;
13
+ const parts = message.content.map(block => block.type === 'text' ? block.text : JSON.stringify(block));
14
+ if (parts.length)
15
+ context.push((message.role === 'system' ? '系统上下文:' : '开发者上下文:') + '\n' + parts.join('\n'));
16
+ }
17
+ const after = messages.findLastIndex(message => message.role === 'assistant');
18
+ const users = messages.slice(after + 1).filter(message => message.role === 'user');
19
+ if (!users.length)
20
+ throw new NeedError('INPUT_REQUIRED', '我需要本轮用户输入', '用户输入');
21
+ const prompt = [];
22
+ for (const message of users) {
23
+ for (const block of message.content) {
24
+ options.signal?.throwIfAborted();
25
+ if (block.type === 'text') {
26
+ prompt.push({ type: 'text', text: block.text });
27
+ continue;
28
+ }
29
+ if (block.type === 'image') {
30
+ if (block.offloaded) {
31
+ prompt.push({ type: 'text', text: `图片 ${block.attachment.name ?? block.attachment.attachmentId} 已按官方会话策略移出当前上下文。` });
32
+ continue;
33
+ }
34
+ if (!readImage)
35
+ throw new NeedError('ATTACHMENT_REQUIRED', '我需要官方附件组件读取这张图片', '官方attachments.readImage');
36
+ const image = await readImage(block.attachment, options.signal);
37
+ options.signal?.throwIfAborted();
38
+ if (!image || !(image.data instanceof Uint8Array) || image.ref?.attachmentId !== block.attachment.attachmentId || !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.ref.mediaType) || image.ref.bytes !== image.data.byteLength)
39
+ throw new NeedError('ATTACHMENT_INVALID', '图片附件没有返回匹配的已核验内容', '官方核验的图片字节与媒体类型');
40
+ prompt.push({ type: 'image', mimeType: image.ref.mediaType, data: Buffer.from(image.data).toString('base64') });
41
+ continue;
42
+ }
43
+ throw new NeedError('INPUT_TYPE_REQUIRED', `我需要此输入类型的远程转接能力:${block.type}`, '对应类型的公开ACP输入映射');
44
+ }
45
+ }
46
+ if (!prompt.length)
47
+ throw new NeedError('INPUT_REQUIRED', '我需要本轮用户输入', '用户输入');
48
+ return { prompt, ...(context.length ? { systemPrompt: context.join('\n\n') } : {}) };
49
+ }
@@ -0,0 +1,19 @@
1
+ /** Legacy programmatic test seam only. Production X never opens user questions. */
2
+ export interface Invocation {
3
+ sessionId?: string;
4
+ signal?: AbortSignal;
5
+ }
6
+ export interface InteractionQuestion {
7
+ id: string;
8
+ question: string;
9
+ detail?: string;
10
+ options?: {
11
+ label: string;
12
+ }[];
13
+ }
14
+ export interface InteractionAnswer {
15
+ id: string;
16
+ selected: string[];
17
+ custom?: string;
18
+ }
19
+ export type Interaction = (question: InteractionQuestion, invocation: Invocation) => Promise<InteractionAnswer | undefined>;
@@ -0,0 +1 @@
1
+ export {};
package/dist/job.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ import { type DevelopmentSshTarget } from './remote.ts';
3
+ import { type ConnectTarget } from './provider.ts';
4
+ export interface JobSpec {
5
+ target: DevelopmentSshTarget | ConnectTarget;
6
+ cwd: string;
7
+ runner: string;
8
+ runnerKind: 'executable' | 'node-source';
9
+ root: string;
10
+ codex: string;
11
+ id: string;
12
+ /** Test/programmatic transport seam only; never an employee setting. */
13
+ sshExecutable?: string;
14
+ }
15
+ export type OneShotRuntime = Omit<JobSpec, 'id' | 'sshExecutable'>;
16
+ export declare function jobCommand(spec: JobSpec, op: 'prepare' | 'stage' | 'run' | 'kill'): string;
17
+ export declare function executeJob(spec: JobSpec, op: 'prepare' | 'stage' | 'run' | 'kill', input?: string, onSpawn?: (child: ChildProcess) => void, inheritOutput?: boolean, signal?: AbortSignal): Promise<number | null>;
package/dist/job.js ADDED
@@ -0,0 +1,57 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { spawn } from 'node:child_process';
10
+ import { shellQuote, sshArgs, NeedError } from "./remote.js";
11
+ import { validateTarget } from "./provider.js";
12
+ import { protectedOneShotLayout } from "./host-layout.js";
13
+ export function jobCommand(spec, op) {
14
+ if (!/^[a-f0-9]{32}$/.test(spec.id) || !['executable', 'node-source'].includes(spec.runnerKind) || ![spec.cwd, spec.root, spec.runner, spec.codex].every(p => p.startsWith('/') && !p.includes('\0'))) {
15
+ throw new NeedError('INVALID_RUNTIME', '我需要主机提供有效的单次运行上下文', '主机程序与工作目录');
16
+ }
17
+ if (!('kind' in spec.target)) {
18
+ const layout = protectedOneShotLayout(spec.target);
19
+ if (spec.runnerKind !== layout.runnerKind || spec.runner !== layout.runner || spec.root !== layout.root || spec.codex !== layout.codex)
20
+ throw new NeedError('INVALID_RUNTIME', '远程主机的单次运行位置需要管理员核对', 'S安装的受保护单次运行路径');
21
+ }
22
+ return [...(spec.runnerKind === 'node-source' ? ['node'] : []), spec.runner, op, spec.root, spec.id].map(shellQuote).join(' ');
23
+ }
24
+ export async function executeJob(spec, op, input, onSpawn, inheritOutput = false, signal) {
25
+ signal?.throwIfAborted();
26
+ const command = jobCommand(spec, op);
27
+ let args;
28
+ if ('kind' in spec.target) {
29
+ args = sshArgs(spec.target, command, op !== 'stage');
30
+ }
31
+ else {
32
+ validateTarget(spec.target);
33
+ if (spec.runnerKind !== 'executable')
34
+ throw new NeedError('INVALID_RUNTIME', '主机单次助手必须使用管理员安装的启动器', 'host-layout 单次启动器');
35
+ let supplied;
36
+ try {
37
+ const moduleName = '@ai-workbench/connect';
38
+ supplied = await import(__rewriteRelativeImportExtension(moduleName));
39
+ if (typeof supplied.sshArgs !== 'function')
40
+ throw Error('missing sshArgs');
41
+ }
42
+ catch {
43
+ throw new NeedError('DEPENDENCY_REQUIRED', '我需要连接组件提供可信连接', '@ai-workbench/connect sshArgs');
44
+ }
45
+ args = [...(op === 'stage' ? [] : ['-n']), ...supplied.sshArgs(spec.target), command];
46
+ }
47
+ signal?.throwIfAborted();
48
+ const child = spawn(spec.sshExecutable ?? 'ssh', args, { stdio: ['pipe', inheritOutput ? 'inherit' : 'ignore', 'ignore'], signal });
49
+ onSpawn?.(child);
50
+ child.stdin.on('error', () => { }); // Exit status owns transport failure; EPIPE cannot mean success.
51
+ child.stdin.end(input);
52
+ return new Promise((resolve, reject) => {
53
+ let failed = false;
54
+ child.on('error', () => { failed = true; });
55
+ child.on('close', code => failed ? reject(new NeedError('SSH_FAILED', 'SSH 启动失败或已取消,根因未查明', '可用的员工连接')) : resolve(code));
56
+ });
57
+ }
@@ -0,0 +1,46 @@
1
+ import type { StreamChunk } from '@deepseek-ai/dsh-llm';
2
+ import type { PromptResponse, SessionUpdate } from '@agentclientprotocol/sdk/unstable';
3
+ import type { ThreadEvent } from '@openai/codex-sdk';
4
+ import { type SanitizedObservation } from './conversation-details.ts';
5
+ /** Persist alongside the exact remote session identity; null means no trusted baseline. */
6
+ export type UsageSnapshot = {
7
+ version: 1;
8
+ kind: 'zero';
9
+ } | {
10
+ version: 1;
11
+ kind: 'cumulative';
12
+ source: 'usage' | 'quota';
13
+ inputTokens: number;
14
+ outputTokens: number;
15
+ cachedReadTokens?: number;
16
+ cachedWriteTokens?: number;
17
+ thoughtTokens?: number;
18
+ totalTokens?: number;
19
+ /** A response was missing or unusable; the next complete response only re-establishes a baseline. */
20
+ gap?: true;
21
+ };
22
+ /** Only model text enters DSH blocks; remote operations remain diagnostic observations. */
23
+ export declare class StreamMapper {
24
+ private index;
25
+ private active;
26
+ private seen;
27
+ private baseline;
28
+ private readonly observations;
29
+ private readonly onDiagnostic;
30
+ /** Omitted argument is exclusively for a known new session; pass null on unknown resume. */
31
+ constructor(previousUsage?: UsageSnapshot | null, onDiagnostic?: (observation: Readonly<SanitizedObservation>) => void);
32
+ get usageSnapshot(): UsageSnapshot | null;
33
+ get diagnostics(): readonly Readonly<SanitizedObservation>[];
34
+ private diagnostic;
35
+ private usageWarning;
36
+ text(text: string, reasoning?: boolean): StreamChunk[];
37
+ /** Close the current text or reasoning block once, before the stream settles. */
38
+ flush(): StreamChunk[];
39
+ acp(update: SessionUpdate): StreamChunk[];
40
+ /** ACP usage and the fixed Claude quota both accumulate across the session, not per turn. */
41
+ acpResponse(response: PromptResponse): StreamChunk[];
42
+ private usageGap;
43
+ private cumulativeUsage;
44
+ private usage;
45
+ sdk(event: ThreadEvent): StreamChunk[];
46
+ }