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

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.
@@ -0,0 +1,254 @@
1
+ import { SessionId } from '@deepseek-ai/dsh-session';
2
+ import { AssistantStreamAccumulator, BlockAssembler, createAssistantMessage, freezeMessage, expandAssistantStream } from '@deepseek-ai/dsh-llm';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import { AGENTS } from "./agents.js";
5
+ import { NeedError } from "./remote.js";
6
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
7
+ const string = (value) => typeof value === 'string' && value.length > 0;
8
+ const workspaceName = (value) => string(value) && value !== '.' && value !== '..' && !/[\u0000-\u001f\u007f/\\]/u.test(value);
9
+ function noticeText(name) {
10
+ // The native reply renderer accepts Markdown; escape the precise verified name as ordinary text.
11
+ return `此工作区对应远程主机上的 ${name.replace(/[\\`*_{}\[\]()<>#+.!|~-]/gu, '\\$&')};本地文件不会自动上传。`;
12
+ }
13
+ function rawMarker(message) {
14
+ if (!object(message) || !object(message.source) || !object(message.source.replayState) || !object(message.source.replayState.response))
15
+ return;
16
+ return message.source.replayState.response;
17
+ }
18
+ export function parseWorkspaceNoticeMarker(message) {
19
+ if (!object(message) || message.role !== 'assistant' || !string(message.id) || !object(message.source))
20
+ return;
21
+ const source = message.source, marker = rawMarker(message), definition = AGENTS.find(item => item.id === source.provider);
22
+ if (!object(source.replayState) || Object.keys(source.replayState).some(key => key !== 'response'))
23
+ return;
24
+ if (!marker || !definition || source.kind !== 'model' || !string(source.model) || marker.kind !== 'workspace-notice' || marker.version !== 1
25
+ || marker.provider !== definition.id || marker.loginProvider !== definition.loginProvider
26
+ || !['sessionId', 'noticeId', 'claimId', 'deliveryId', 'messageId', 'bindingId', 'workspaceId'].every(key => string(marker[key]))
27
+ || marker.messageId !== message.id || marker.deliveryId !== message.id || !workspaceName(marker.workspaceName)
28
+ || !Array.isArray(message.content) || message.content.length !== 1 || !object(message.content[0])
29
+ || message.content[0].type !== 'text' || message.content[0].text !== noticeText(marker.workspaceName))
30
+ return;
31
+ return marker;
32
+ }
33
+ /** Only this plugin's complete, source-bound notice may be excluded from remote model history. */
34
+ export function isWorkspaceNoticeMessage(message) { return parseWorkspaceNoticeMarker(message) !== undefined; }
35
+ function unknown(cause) {
36
+ return Object.assign(new NeedError('PROJECT_NOTICE_DELIVERY_UNKNOWN', '工作区说明的会话持久回执无法核对;请联系管理员核对,本次未继续派发远端请求。', '完整的官方会话持久记录和 L 工作区说明回执'), cause === undefined ? {} : { cause });
37
+ }
38
+ function requireServices(ctx, request) {
39
+ const id = string(request.sessionId) ? SessionId(request.sessionId) : undefined;
40
+ const registry = ctx.get('agents'), store = ctx.get('sessions');
41
+ // The persistence definition is an optional host service, not an installed private writer.
42
+ const persistence = ctx.get('sessionPersistence');
43
+ const live = id && typeof registry?.get === 'function' ? registry.get(id) : undefined;
44
+ const session = live?.session;
45
+ if (!id || !live || live.id !== id || !session || session.id !== id || typeof store?.get !== 'function' || store.get(id) !== session
46
+ || typeof session.snapshotEvents !== 'function' || typeof session.append !== 'function' || typeof store.flush !== 'function' || typeof persistence?.open !== 'function') {
47
+ throw new NeedError('DEPENDENCY_REQUIRED', '我需要当前真实 DSH 会话及其官方持久化读取能力,才能确认工作区说明;本次未启动远端。', 'DSH agents、sessions、sessionPersistence 公共能力');
48
+ }
49
+ return { session, flush: () => store.flush(session), persistence };
50
+ }
51
+ function requireAcknowledge(api) {
52
+ const method = api.acknowledgeWorkspaceNotice;
53
+ if (typeof method !== 'function')
54
+ throw new NeedError('DEPENDENCY_REQUIRED', '我需要远程主机连接提供工作区说明回执能力;本次未启动远端。', 'L acknowledgeWorkspaceNotice 公共接口');
55
+ return method.bind(api);
56
+ }
57
+ async function acknowledge(method, receipt) {
58
+ const result = await method(receipt);
59
+ if (!object(result) || result.ok !== true)
60
+ throw unknown(result);
61
+ }
62
+ function receipt(marker, outcome) {
63
+ const base = { noticeId: marker.noticeId, surface: 'provider', claimId: marker.claimId, sessionId: marker.sessionId, provider: marker.loginProvider };
64
+ return outcome === 'delivered' ? { ...base, outcome, deliveryId: marker.deliveryId } : { ...base, outcome };
65
+ }
66
+ async function reportUnknown(method, marker, cause) {
67
+ let failure = cause;
68
+ try {
69
+ await acknowledge(method, receipt(marker, 'unknown'));
70
+ }
71
+ catch (reportFailure) {
72
+ failure = new AggregateError([cause, reportFailure], '工作区说明的核对和未知状态记录均未完成');
73
+ }
74
+ throw unknown(failure);
75
+ }
76
+ async function readDurable(services, offset, length) {
77
+ if (!await services.flush())
78
+ throw unknown();
79
+ const handle = await services.persistence.open(services.session.id, 'read');
80
+ try {
81
+ if (!handle || handle.id !== services.session.id || handle.header?.id !== services.session.id || handle.access !== 'read' || typeof handle.read !== 'function' || typeof handle.close !== 'function')
82
+ throw unknown();
83
+ const result = await handle.read(offset, length);
84
+ if (!result || !Array.isArray(result.events))
85
+ throw unknown();
86
+ return result.events;
87
+ }
88
+ finally {
89
+ if (typeof handle?.close === 'function')
90
+ await handle.close();
91
+ }
92
+ }
93
+ function validateRecord(event, sessionId) {
94
+ const marker = parseWorkspaceNoticeMarker(event.data.message);
95
+ if (!marker || marker.sessionId !== sessionId || event.surfaceOp !== 'append' || event.data.interrupted === true || !Array.isArray(event.data.stream))
96
+ throw unknown();
97
+ const assembler = new BlockAssembler();
98
+ for (const member of expandAssistantStream(event.data.stream))
99
+ assembler.push(member.chunk);
100
+ if (assembler.finish.kind !== 'stop' || !isDeepStrictEqual(assembler.blocks(), event.data.message.content)
101
+ || !isDeepStrictEqual(assembler.replayState, event.data.message.source.replayState) || assembler.usage !== undefined || event.data.usage !== undefined)
102
+ throw unknown();
103
+ return marker;
104
+ }
105
+ function ownRecords(events, sessionId, agent) {
106
+ const found = [];
107
+ for (const event of events) {
108
+ if (event.type !== 'assistant/message' || event.data.message.source.provider !== agent.id)
109
+ continue;
110
+ const raw = rawMarker(event.data.message);
111
+ if (raw?.kind !== 'workspace-notice')
112
+ continue;
113
+ // A fork may retain an ancestor's notice. It is never this Session's claim.
114
+ if (string(raw.sessionId) && raw.sessionId !== sessionId)
115
+ continue;
116
+ const marker = validateRecord(event, sessionId);
117
+ if (marker.provider !== agent.id || marker.loginProvider !== agent.loginProvider)
118
+ throw unknown();
119
+ found.push({ event, marker });
120
+ }
121
+ const notices = new Map();
122
+ for (const { marker } of found) {
123
+ if (notices.has(marker.noticeId))
124
+ throw unknown();
125
+ notices.set(marker.noticeId, marker.deliveryId);
126
+ }
127
+ return found;
128
+ }
129
+ function activeRequest(session, definition) {
130
+ let turn, step, model, provider;
131
+ for (const event of session.snapshotEvents())
132
+ switch (event.type) {
133
+ case 'turn/start':
134
+ turn = event.data.turn;
135
+ step = undefined;
136
+ break;
137
+ case 'turn/end':
138
+ turn = undefined;
139
+ step = undefined;
140
+ break;
141
+ case 'step/start':
142
+ if (event.data.turn !== turn)
143
+ throw unknown();
144
+ step = event.data.step;
145
+ break;
146
+ case 'step/end':
147
+ step = undefined;
148
+ break;
149
+ case 'request/header':
150
+ provider = event.data.header.config.provider;
151
+ model = event.data.header.config.model;
152
+ break;
153
+ }
154
+ if (!Number.isSafeInteger(turn) || turn < 1 || !Number.isSafeInteger(step) || step < 1 || provider !== definition.id || !string(model))
155
+ throw unknown();
156
+ return { turn: turn, step: step, model };
157
+ }
158
+ function buildRecord(services, definition, notice, resolved) {
159
+ const { session } = services, { turn, step, model } = activeRequest(session, definition), text = noticeText(notice.workspaceName);
160
+ const base = createAssistantMessage({ content: [{ type: 'text', text }], source: { provider: definition.id, model } });
161
+ const marker = { kind: 'workspace-notice', version: 1, provider: definition.id, loginProvider: definition.loginProvider,
162
+ sessionId: session.id, noticeId: notice.noticeId, claimId: notice.claimId, deliveryId: base.id, messageId: base.id,
163
+ workspaceName: notice.workspaceName, bindingId: resolved.bindingId, workspaceId: resolved.workspaceId };
164
+ const chunks = [{ type: 'block-start', index: 0, blockType: 'text' }, { type: 'text-delta', index: 0, text },
165
+ { type: 'block-end', index: 0, block: { type: 'text', text } }, { type: 'finish', reason: { kind: 'stop' }, replayState: { response: marker } }];
166
+ const accumulator = new AssistantStreamAccumulator(), assembler = new BlockAssembler();
167
+ for (const chunk of chunks)
168
+ assembler.push(accumulator.push({ time: Date.now(), chunk }).chunk);
169
+ const message = freezeMessage({ ...base, source: { ...base.source, replayState: assembler.replayState } });
170
+ // This public append is the actual ordinary-reply consumption point, not an emitted-yield guess.
171
+ const event = session.append('assistant/message', { turn, step, message, stream: [...accumulator.snapshot()] }, { surfaceOp: 'append' });
172
+ return { event, marker };
173
+ }
174
+ export function createWorkspaceNoticeConsumer(ctx, agent) {
175
+ const flights = new Map();
176
+ const reconcile = async (request, api) => {
177
+ request.signal?.throwIfAborted();
178
+ const services = requireServices(ctx, request), method = requireAcknowledge(api);
179
+ let stored;
180
+ try {
181
+ const events = await readDurable(services);
182
+ stored = ownRecords(events, services.session.id, agent);
183
+ const live = ownRecords(services.session.snapshotEvents(), services.session.id, agent);
184
+ if (live.some(item => !stored.some(record => record.marker.deliveryId === item.marker.deliveryId && isDeepStrictEqual(record.event, item.event))))
185
+ throw unknown();
186
+ }
187
+ catch (error) {
188
+ throw unknown(error);
189
+ }
190
+ for (const { marker } of stored) {
191
+ try {
192
+ await acknowledge(method, receipt(marker, 'delivered'));
193
+ }
194
+ catch (error) {
195
+ await reportUnknown(method, marker, error);
196
+ }
197
+ }
198
+ request.signal?.throwIfAborted();
199
+ };
200
+ const deliver = async (notice, resolved, request, api) => {
201
+ request.signal?.throwIfAborted();
202
+ const services = requireServices(ctx, request), method = requireAcknowledge(api);
203
+ if (!notice || !string(notice.noticeId) || !string(notice.claimId) || notice.messageKey !== 'remote-workspace-binding' || !workspaceName(notice.workspaceName)
204
+ || resolved.sessionId !== services.session.id || !string(resolved.bindingId) || !string(resolved.workspaceId) || resolved.project?.name !== notice.workspaceName)
205
+ throw unknown();
206
+ const key = JSON.stringify([services.session.id, notice.noticeId, notice.claimId]);
207
+ const current = flights.get(key);
208
+ if (current)
209
+ return current;
210
+ const operation = (async () => {
211
+ let pending;
212
+ try {
213
+ const snapshot = services.session.snapshotEvents();
214
+ for (const event of snapshot) {
215
+ if (event.type !== 'assistant/message' || event.data.message.source.provider === agent.id || !AGENTS.some(item => item.id === event.data.message.source.provider))
216
+ continue;
217
+ const raw = rawMarker(event.data.message);
218
+ if (raw?.kind === 'workspace-notice' && raw.noticeId === notice.noticeId && raw.sessionId === services.session.id)
219
+ throw unknown();
220
+ }
221
+ const records = ownRecords(snapshot, services.session.id, agent);
222
+ const prior = records.find(item => item.marker.noticeId === notice.noticeId);
223
+ if (prior) {
224
+ if (prior.marker.claimId !== notice.claimId || prior.marker.workspaceId !== resolved.workspaceId || prior.marker.bindingId !== resolved.bindingId || prior.marker.workspaceName !== notice.workspaceName)
225
+ throw unknown();
226
+ pending = prior;
227
+ }
228
+ else
229
+ pending = buildRecord(services, agent, notice, resolved);
230
+ const persisted = await readDurable(services, pending.event.seq, 1);
231
+ const event = persisted[0];
232
+ if (event?.type !== 'assistant/message' || !isDeepStrictEqual(event, pending.event))
233
+ throw unknown();
234
+ validateRecord(event, services.session.id);
235
+ await acknowledge(method, receipt(pending.marker, 'delivered'));
236
+ }
237
+ catch (error) {
238
+ if (pending)
239
+ return reportUnknown(method, pending.marker, error);
240
+ throw unknown(error);
241
+ }
242
+ request.signal?.throwIfAborted();
243
+ })();
244
+ flights.set(key, operation);
245
+ try {
246
+ await operation;
247
+ }
248
+ finally {
249
+ if (flights.get(key) === operation)
250
+ flights.delete(key);
251
+ }
252
+ };
253
+ return { reconcile, deliver };
254
+ }
package/locale/en.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "meta": {
3
+ "title": "Codex (Remote Host)",
4
+ "description": "Run Codex sessions through a remote host. Administrators: use Settings → Open configuration file to edit the existing plugin entry. See README for the six fields and approved defaults."
5
+ },
6
+ "configuration": {
7
+ "administratorNote": "Administrators: use Settings → Open configuration file to edit the existing plugin entry. See README for the six fields and approved defaults.",
8
+ "providerName": "Codex (Remote Host)"
9
+ }
10
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "meta": {
3
+ "title": "远程主机ACP",
4
+ "description": "通过远程主机运行Claude与Codex。首版不支持总是允许和歧义审批选项、已有Claude会话中立即生效的系统上下文变更;完整说明见README。管理员从官方设置打开配置文件修改已有条目。"
5
+ },
6
+ "configuration": {
7
+ "administratorNote": "管理员:从设置使用“打开配置文件”,修改已有插件条目;六个字段和已批准默认值见 README。",
8
+ "providerName": "Codex(远程主机)"
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,99 @@
1
1
  {
2
2
  "name": "@ai-workbench/remote-agent",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.8.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./client": "./dist/client.js",
13
+ "./package.json": "./package.json",
14
+ "./locale/*.json": "./locale/*.json"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "locale",
19
+ "cordis.patch.yml"
20
+ ],
21
+ "scripts": {
22
+ "test": "npm run build && node --test lib/test/*.test.js",
23
+ "typecheck": "node node_modules/typescript/bin/tsc --noEmit",
24
+ "build": "node node_modules/typescript/bin/tsc && node node_modules/typescript/bin/tsc -p tsconfig.build.json && chmod 755 lib/src/sdk-ssh-wrapper.js dist/sdk-ssh-wrapper.js && node tools/build-client.mjs",
25
+ "probe:host": "node tools/probe-host.ts",
26
+ "probe:broker": "node tools/probe-broker.ts",
27
+ "pack:artifact": "node tools/pack-artifact.mjs",
28
+ "probe:formal": "node tools/probe-formal.mjs"
29
+ },
30
+ "engines": {
31
+ "node": ">=24.13.1"
32
+ },
33
+ "dependencies": {
34
+ "@agentclientprotocol/sdk": "2.0.0",
35
+ "@openai/codex-sdk": "0.163.0",
36
+ "zod": "4.6.7"
37
+ },
38
+ "devDependencies": {
39
+ "@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.0.tgz",
40
+ "@deepseek-ai/cordis": "4.0.4",
41
+ "@deepseek-ai/cordis-plugin-group": "1.0.4",
42
+ "@deepseek-ai/cordis-plugin-include": "1.0.9",
43
+ "@deepseek-ai/cordis-plugin-loader": "1.0.5",
44
+ "@deepseek-ai/cordis-plugin-timer": "1.1.6",
45
+ "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
46
+ "@deepseek-ai/dsh-api-gateway": "0.2.0-rc.2",
47
+ "@deepseek-ai/dsh-client-connection": "0.2.0-rc.2",
48
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
49
+ "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
50
+ "@deepseek-ai/dsh-typert-registry": "0.2.0-rc.2",
51
+ "@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
52
+ "@deepseek-ai/schemastery": "3.18.4",
53
+ "@types/node": "24.10.0",
54
+ "typescript": "5.9.3",
55
+ "@deepseek-ai/dsh-session": "0.2.0-rc.2",
56
+ "@deepseek-ai/dsh-attachment": "0.2.0-rc.2"
57
+ },
58
+ "peerDependencies": {
59
+ "@ai-workbench/connect": "~0.7.0",
60
+ "@deepseek-ai/cordis": "4.0.4",
61
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
62
+ "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
63
+ "@deepseek-ai/schemastery": "3.18.4",
64
+ "@deepseek-ai/dsh-session": "0.2.0-rc.2",
65
+ "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
66
+ "@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
67
+ "@deepseek-ai/dsh-attachment": "0.2.0-rc.2"
68
+ },
69
+ "dsh": {
70
+ "bundle": {
71
+ "patch": "./cordis.patch.yml"
72
+ },
73
+ "client": {
74
+ "inject": [
75
+ "@deepseek-ai/dsh-client-locale",
76
+ "@deepseek-ai/dsh-client-connection"
77
+ ],
78
+ "platform": "web"
79
+ }
80
+ },
81
+ "peerDependenciesMeta": {
82
+ "@ai-workbench/connect": {
83
+ "optional": true
84
+ }
85
+ },
86
+ "description": "通过远程主机运行Claude与Codex会话。",
87
+ "publishConfig": {
88
+ "access": "public",
89
+ "registry": "https://registry.npmjs.org/"
90
+ },
91
+ "keywords": [
92
+ "dsh",
93
+ "dsh-plugin",
94
+ "deepseek-harness",
95
+ "acp",
96
+ "claude",
97
+ "codex"
98
+ ]
99
+ }