@ai-workbench/remote-agent 0.8.0 → 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.
@@ -1,5 +1,5 @@
1
1
  import { SessionId } from '@deepseek-ai/dsh-session';
2
- import { AssistantStreamAccumulator, BlockAssembler, createAssistantMessage, freezeMessage, expandAssistantStream } from '@deepseek-ai/dsh-llm';
2
+ import { BlockAssembler, expandAssistantStream } from '@deepseek-ai/dsh-llm';
3
3
  import { isDeepStrictEqual } from 'node:util';
4
4
  import { AGENTS } from "./agents.js";
5
5
  import { NeedError } from "./remote.js";
@@ -32,55 +32,86 @@ export function parseWorkspaceNoticeMarker(message) {
32
32
  }
33
33
  /** Only this plugin's complete, source-bound notice may be excluded from remote model history. */
34
34
  export function isWorkspaceNoticeMessage(message) { return parseWorkspaceNoticeMarker(message) !== undefined; }
35
+ function rawPresentation(message) {
36
+ const response = rawMarker(message), presentation = response && object(response.presentation) ? response.presentation : undefined;
37
+ return presentation && object(presentation.workspaceNotice) ? presentation.workspaceNotice : undefined;
38
+ }
39
+ function shape(value) {
40
+ if (!object(value))
41
+ return false;
42
+ const agent = AGENTS.find(a => a.id === value.provider);
43
+ return !!agent && value.kind === 'workspace-notice' && value.version === 2 && value.loginProvider === agent.loginProvider
44
+ && ['sessionId', 'noticeId', 'claimId', 'bindingId', 'workspaceId'].every(k => string(value[k])) && workspaceName(value.workspaceName)
45
+ && Number.isSafeInteger(value.turn) && value.turn > 0 && Number.isSafeInteger(value.step) && value.step > 0
46
+ && value.blockIndex === 0 && value.text === noticeText(value.workspaceName) + '\n\n';
47
+ }
48
+ export function workspaceNoticeFromMessage(message) {
49
+ if (!object(message) || message.role !== 'assistant' || !object(message.source) || message.source.kind !== 'model')
50
+ return;
51
+ const marker = rawPresentation(message), pointer = rawMarker(message);
52
+ if (!shape(marker) || message.source.provider !== marker.provider || pointer?.agent !== marker.provider || pointer.dshSessionId !== marker.sessionId
53
+ || pointer.bindingId !== marker.bindingId || pointer.workspaceId !== marker.workspaceId || !Array.isArray(message.content))
54
+ return;
55
+ const block = message.content[marker.blockIndex];
56
+ if (!object(block) || block.type !== 'text' || block.text !== marker.text)
57
+ return;
58
+ return marker;
59
+ }
60
+ /** Preserve the model answer and exact remote replay pointer; ordinary text is never searched. */
61
+ export function withoutWorkspaceNotice(message) {
62
+ const marker = workspaceNoticeFromMessage(message);
63
+ if (!marker)
64
+ return message;
65
+ return { ...message, content: message.content.filter((_block, index) => index !== marker.blockIndex) };
66
+ }
35
67
  function unknown(cause) {
36
- return Object.assign(new NeedError('PROJECT_NOTICE_DELIVERY_UNKNOWN', '工作区说明的会话持久回执无法核对;请联系管理员核对,本次未继续派发远端请求。', '完整的官方会话持久记录和 L 工作区说明回执'), cause === undefined ? {} : { cause });
68
+ return Object.assign(new NeedError('PROJECT_NOTICE_DELIVERY_UNKNOWN', '工作区说明的最终回复持久回执无法核对;请联系管理员核对,本次未继续派发远端请求。', '完整的官方最终会话记录和L工作区说明回执'), cause === undefined ? {} : { cause });
37
69
  }
38
70
  function requireServices(ctx, request) {
39
71
  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;
72
+ const registry = ctx.get('agents'), store = ctx.get('sessions'), persistence = ctx.get('sessionPersistence');
73
+ const live = id && typeof registry?.get === 'function' ? registry.get(id) : undefined, session = live?.session;
45
74
  if (!id || !live || live.id !== id || !session || session.id !== id || typeof store?.get !== 'function' || store.get(id) !== session
46
75
  || 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 公共能力');
76
+ throw new NeedError('DEPENDENCY_REQUIRED', '我需要当前真实会话及官方持久化读取能力,才能确认工作区说明;本次未启动远端。', 'DSH agents、sessions、sessionPersistence公共能力');
48
77
  }
49
78
  return { session, flush: () => store.flush(session), persistence };
50
79
  }
51
80
  function requireAcknowledge(api) {
52
81
  const method = api.acknowledgeWorkspaceNotice;
53
82
  if (typeof method !== 'function')
54
- throw new NeedError('DEPENDENCY_REQUIRED', '我需要远程主机连接提供工作区说明回执能力;本次未启动远端。', 'L acknowledgeWorkspaceNotice 公共接口');
83
+ throw new NeedError('DEPENDENCY_REQUIRED', '我需要工作区说明回执能力;本次未启动远端。', 'L acknowledgeWorkspaceNotice公共接口');
55
84
  return method.bind(api);
56
85
  }
57
- async function acknowledge(method, receipt) {
58
- const result = await method(receipt);
86
+ function receipt(marker, outcome, deliveryId) {
87
+ const base = { noticeId: marker.noticeId, surface: 'provider', claimId: marker.claimId, sessionId: marker.sessionId, provider: marker.loginProvider };
88
+ if (outcome === 'delivered' && string(deliveryId))
89
+ return { ...base, outcome, deliveryId };
90
+ return { ...base, outcome: 'unknown' };
91
+ }
92
+ async function acknowledge(method, value) {
93
+ const result = await method(value);
59
94
  if (!object(result) || result.ok !== true)
60
95
  throw unknown(result);
61
96
  }
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) {
97
+ async function reportUnknown(api, marker, cause) {
67
98
  let failure = cause;
68
99
  try {
69
- await acknowledge(method, receipt(marker, 'unknown'));
100
+ await acknowledge(requireAcknowledge(api), receipt(marker, 'unknown'));
70
101
  }
71
- catch (reportFailure) {
72
- failure = new AggregateError([cause, reportFailure], '工作区说明的核对和未知状态记录均未完成');
102
+ catch (error) {
103
+ failure = new AggregateError([cause, error], '说明核对和未知状态记录均未完成');
73
104
  }
74
105
  throw unknown(failure);
75
106
  }
76
- async function readDurable(services, offset, length) {
107
+ async function readDurable(services) {
77
108
  if (!await services.flush())
78
109
  throw unknown();
79
110
  const handle = await services.persistence.open(services.session.id, 'read');
80
111
  try {
81
112
  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
113
  throw unknown();
83
- const result = await handle.read(offset, length);
114
+ const result = await handle.read();
84
115
  if (!result || !Array.isArray(result.events))
85
116
  throw unknown();
86
117
  return result.events;
@@ -90,44 +121,22 @@ async function readDurable(services, offset, length) {
90
121
  await handle.close();
91
122
  }
92
123
  }
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))
124
+ function validateFinal(event, marker, events) {
125
+ if (event.surfaceOp !== 'append' || event.data.interrupted === true || !Array.isArray(event.data.stream)
126
+ || event.data.turn !== marker.turn || event.data.step !== marker.step || !isDeepStrictEqual(workspaceNoticeFromMessage(event.data.message), marker))
127
+ throw unknown();
128
+ // Chat renders the last final for a step. An earlier append is not enough.
129
+ if (events.some(e => e.type === 'assistant/message' && e.surfaceOp === 'append' && e.seq > event.seq && e.data.turn === marker.turn && e.data.step === marker.step))
96
130
  throw unknown();
97
131
  const assembler = new BlockAssembler();
98
132
  for (const member of expandAssistantStream(event.data.stream))
99
133
  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)
134
+ if (!['stop', 'max-tokens'].includes(assembler.finish.kind) || !isDeepStrictEqual(assembler.blocks(), event.data.message.content)
135
+ || !isDeepStrictEqual(assembler.replayState, event.data.message.source.replayState))
102
136
  throw unknown();
103
- return marker;
104
137
  }
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;
138
+ function activeRequest(session, agent) {
139
+ let turn, step, provider;
131
140
  for (const event of session.snapshotEvents())
132
141
  switch (event.type) {
133
142
  case 'turn/start':
@@ -148,107 +157,117 @@ function activeRequest(session, definition) {
148
157
  break;
149
158
  case 'request/header':
150
159
  provider = event.data.header.config.provider;
151
- model = event.data.header.config.model;
152
160
  break;
153
161
  }
154
- if (!Number.isSafeInteger(turn) || turn < 1 || !Number.isSafeInteger(step) || step < 1 || provider !== definition.id || !string(model))
162
+ if (!Number.isSafeInteger(turn) || turn < 1 || !Number.isSafeInteger(step) || step < 1 || provider !== agent.id)
155
163
  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 };
164
+ return { turn: turn, step: step };
173
165
  }
174
166
  export function createWorkspaceNoticeConsumer(ctx, agent) {
167
+ const pending = new Map();
175
168
  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;
169
+ const failures = new Map();
170
+ const verify = async (sessionId, api, marker, event) => {
180
171
  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))))
172
+ const services = requireServices(ctx, { mode: 'session', sessionId }), events = await readDurable(services);
173
+ const current = event ? events.find(e => e.seq === event.seq) : undefined;
174
+ if (!event || current?.type !== 'assistant/message' || !isDeepStrictEqual(current, event))
185
175
  throw unknown();
176
+ validateFinal(current, marker, events);
177
+ await acknowledge(requireAcknowledge(api), receipt(marker, 'delivered', current.data.message.id));
178
+ pending.delete(sessionId);
179
+ failures.delete(sessionId);
186
180
  }
187
181
  catch (error) {
188
- throw unknown(error);
182
+ return reportUnknown(api, marker, error);
189
183
  }
190
- for (const { marker } of stored) {
184
+ };
185
+ ctx.on('session/event', (session, event) => {
186
+ const preparation = pending.get(session.id);
187
+ if (!preparation)
188
+ return;
189
+ const { marker, api } = preparation;
190
+ let relevant;
191
+ if (event.type === 'assistant/message' && event.data.turn === marker.turn && event.data.step === marker.step && event.data.message.source.provider === agent.id)
192
+ relevant = event;
193
+ else if (event.type === 'turn/end' && event.data.turn === marker.turn && !session.snapshotEvents().some(e => e.type === 'assistant/message' && e.data.turn === marker.turn && e.data.step === marker.step && e.data.message.source.provider === agent.id))
194
+ relevant = undefined;
195
+ else
196
+ return;
197
+ if (flights.has(session.id))
198
+ return;
199
+ // Wait for the synchronous event dispatch to enqueue the writer, then flush.
200
+ const operation = Promise.resolve().then(() => verify(session.id, api, marker, relevant));
201
+ flights.set(session.id, operation);
202
+ void operation.catch(error => { failures.set(session.id, error); });
203
+ });
204
+ const reconcile = async (request, api) => {
205
+ request.signal?.throwIfAborted();
206
+ const services = requireServices(ctx, request);
207
+ requireAcknowledge(api);
208
+ const flight = flights.get(services.session.id);
209
+ if (flight) {
191
210
  try {
192
- await acknowledge(method, receipt(marker, 'delivered'));
211
+ await flight;
212
+ return;
213
+ }
214
+ finally {
215
+ if (flights.get(services.session.id) === flight)
216
+ flights.delete(services.session.id);
217
+ }
218
+ }
219
+ const events = await readDurable(services), seen = new Set();
220
+ let found = false;
221
+ for (const event of services.session.snapshotEvents()) {
222
+ if (event.type !== 'assistant/message')
223
+ continue;
224
+ const raw = rawPresentation(event.data.message);
225
+ if (!shape(raw) || raw.provider !== agent.id || raw.sessionId !== services.session.id)
226
+ continue;
227
+ if (seen.has(raw.noticeId))
228
+ return reportUnknown(api, raw, unknown());
229
+ seen.add(raw.noticeId);
230
+ found = true;
231
+ const durable = events.find(e => e.seq === event.seq);
232
+ if (durable?.type !== 'assistant/message' || !isDeepStrictEqual(durable, event))
233
+ return reportUnknown(api, raw, unknown());
234
+ try {
235
+ validateFinal(durable, raw, events);
236
+ await acknowledge(requireAcknowledge(api), receipt(raw, 'delivered', durable.data.message.id));
237
+ failures.delete(services.session.id);
238
+ pending.delete(services.session.id);
193
239
  }
194
240
  catch (error) {
195
- await reportUnknown(method, marker, error);
241
+ return reportUnknown(api, raw, error);
196
242
  }
197
243
  }
244
+ if (!found && failures.has(services.session.id))
245
+ throw unknown(failures.get(services.session.id));
198
246
  request.signal?.throwIfAborted();
199
247
  };
200
248
  const deliver = async (notice, resolved, request, api) => {
201
249
  request.signal?.throwIfAborted();
202
- const services = requireServices(ctx, request), method = requireAcknowledge(api);
250
+ const services = requireServices(ctx, request);
251
+ requireAcknowledge(api);
203
252
  if (!notice || !string(notice.noticeId) || !string(notice.claimId) || notice.messageKey !== 'remote-workspace-binding' || !workspaceName(notice.workspaceName)
204
253
  || resolved.sessionId !== services.session.id || !string(resolved.bindingId) || !string(resolved.workspaceId) || resolved.project?.name !== notice.workspaceName)
205
254
  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;
255
+ const { turn, step } = activeRequest(services.session, agent);
256
+ const marker = { kind: 'workspace-notice', version: 2, provider: agent.id, loginProvider: agent.loginProvider, sessionId: services.session.id,
257
+ noticeId: notice.noticeId, claimId: notice.claimId, workspaceName: notice.workspaceName, bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, turn, step, blockIndex: 0, text: noticeText(notice.workspaceName) + '\n\n' };
258
+ const prior = pending.get(services.session.id);
259
+ if (prior) {
260
+ if (!isDeepStrictEqual(prior.marker, marker))
261
+ throw unknown();
262
+ return prior.marker;
247
263
  }
248
- finally {
249
- if (flights.get(key) === operation)
250
- flights.delete(key);
264
+ const recorded = services.session.snapshotEvents().filter((e) => e.type === 'assistant/message').find(e => workspaceNoticeFromMessage(e.data.message)?.noticeId === notice.noticeId);
265
+ if (recorded) {
266
+ await reconcile(request, api);
267
+ return undefined;
251
268
  }
269
+ pending.set(services.session.id, { marker, api });
270
+ return marker;
252
271
  };
253
272
  return { reconcile, deliver };
254
273
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-workbench/remote-agent",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -36,7 +36,7 @@
36
36
  "zod": "4.6.7"
37
37
  },
38
38
  "devDependencies": {
39
- "@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.0.tgz",
39
+ "@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.9.tgz",
40
40
  "@deepseek-ai/cordis": "4.0.4",
41
41
  "@deepseek-ai/cordis-plugin-group": "1.0.4",
42
42
  "@deepseek-ai/cordis-plugin-include": "1.0.9",
@@ -56,7 +56,7 @@
56
56
  "@deepseek-ai/dsh-attachment": "0.2.0-rc.2"
57
57
  },
58
58
  "peerDependencies": {
59
- "@ai-workbench/connect": "~0.7.0",
59
+ "@ai-workbench/connect": "~0.7.9",
60
60
  "@deepseek-ai/cordis": "4.0.4",
61
61
  "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
62
62
  "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",