@amaster.ai/dsh-a2a 0.1.34 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +279 -7
- package/lib/admission.js +25 -0
- package/lib/bridge.js +45 -23
- package/lib/content.js +163 -30
- package/lib/executor.js +38 -3
- package/lib/inbox-api.js +117 -0
- package/lib/inbox-changes.js +116 -0
- package/lib/inbox.js +558 -44
- package/lib/index.js +30 -3
- package/lib/legacy-stream.js +20 -5
- package/lib/server.js +159 -26
- package/lib/stores/gcs.js +6 -5
- package/lib/stores/redis.js +5 -4
- package/lib/stream-connection.js +75 -0
- package/lib/task-store.js +22 -4
- package/lib/types/admission.d.ts +10 -0
- package/lib/types/bridge.d.ts +8 -0
- package/lib/types/content.d.ts +7 -1
- package/lib/types/executor.d.ts +2 -0
- package/lib/types/inbox-api.d.ts +5 -0
- package/lib/types/inbox-changes.d.ts +25 -0
- package/lib/types/inbox.d.ts +58 -2
- package/lib/types/index.d.ts +197 -142
- package/lib/types/server.d.ts +13 -1
- package/lib/types/stream-connection.d.ts +8 -0
- package/package.json +25 -15
package/lib/inbox.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
/** Native DSH input ownership with standard A2A task delivery. */
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { Message, TaskState } from '@a2a-js/sdk';
|
|
3
|
+
import { Message, Role, TaskState } from '@a2a-js/sdk';
|
|
4
|
+
import { RequestMalformedError } from '@a2a-js/sdk/errors';
|
|
4
5
|
import { AgentEvent, ServerCallContext, STATE_HEADERS_KEY, } from '@a2a-js/sdk/server';
|
|
5
6
|
import { createUserMessage } from '@deepseek-ai/dsh-llm/message';
|
|
7
|
+
import { InboxChanges } from './inbox-changes.js';
|
|
6
8
|
import { commitInboxInput, markInboxInputPending } from './task-store.js';
|
|
7
9
|
import { agentTextMessage, SessionTranslator, terminalStatusUpdate } from './translator.js';
|
|
8
|
-
export const INBOX_EXTENSION = 'https://github.com/TGYD-helige/dsh-plugins/blob/main/
|
|
10
|
+
export const INBOX_EXTENSION = 'https://github.com/TGYD-helige/dsh-plugins/blob/main/packages/dsh-a2a/README.md';
|
|
9
11
|
const RETAINED_INPUTS = `${INBOX_EXTENSION}#retained-inputs`;
|
|
12
|
+
export function validateInboxPage(page) {
|
|
13
|
+
if (page === undefined)
|
|
14
|
+
return;
|
|
15
|
+
if (!page ||
|
|
16
|
+
typeof page !== 'object' ||
|
|
17
|
+
Array.isArray(page) ||
|
|
18
|
+
(page.pageSize !== undefined &&
|
|
19
|
+
(!Number.isInteger(page.pageSize) || page.pageSize < 1 || page.pageSize > 100)) ||
|
|
20
|
+
(page.pageToken !== undefined && typeof page.pageToken !== 'string'))
|
|
21
|
+
throw new RequestMalformedError('pageSize must be an integer from 1 to 100 and pageToken must be a string.');
|
|
22
|
+
}
|
|
10
23
|
export class NativeInbox {
|
|
11
24
|
ctx;
|
|
12
25
|
bridge;
|
|
13
26
|
store;
|
|
14
27
|
retainPending;
|
|
28
|
+
changes;
|
|
15
29
|
contexts = new Map();
|
|
16
30
|
resolving = new Map();
|
|
17
31
|
stopping = false;
|
|
@@ -19,17 +33,20 @@ export class NativeInbox {
|
|
|
19
33
|
preadmitted = new WeakMap();
|
|
20
34
|
server;
|
|
21
35
|
persistence;
|
|
22
|
-
constructor(ctx, bridge, store, retainPending = false) {
|
|
36
|
+
constructor(ctx, bridge, store, retainPending = false, notifications = false) {
|
|
23
37
|
this.ctx = ctx;
|
|
24
38
|
this.bridge = bridge;
|
|
25
39
|
this.store = store;
|
|
26
40
|
this.retainPending = retainPending;
|
|
41
|
+
this.changes = new InboxChanges(ctx, (contextId) => this.contexts.has(contextId), notifications);
|
|
27
42
|
bridge.inbox = this;
|
|
28
43
|
this.persistence = ctx.get('sessionPersistence');
|
|
29
44
|
if (retainPending)
|
|
30
45
|
ctx.on('session/event', (session, event) => {
|
|
31
46
|
const run = this.contexts.get(session.id);
|
|
32
|
-
|
|
47
|
+
// Shutdown drains in-flight commits before disposal; those splices must
|
|
48
|
+
// still update retained input or recovery would see history without a task.
|
|
49
|
+
if (!run || event.type !== 'agent/inbox/spliced' || this.unloading())
|
|
33
50
|
return;
|
|
34
51
|
try {
|
|
35
52
|
const splice = event.data;
|
|
@@ -110,6 +127,236 @@ export class NativeInbox {
|
|
|
110
127
|
async resolve(contextId) {
|
|
111
128
|
return (await this.context(contextId)).entry.handle.agent;
|
|
112
129
|
}
|
|
130
|
+
async get(contextId, page) {
|
|
131
|
+
validateInboxPage(page);
|
|
132
|
+
let offset = 0;
|
|
133
|
+
let cursorVersion;
|
|
134
|
+
if (page?.pageToken) {
|
|
135
|
+
try {
|
|
136
|
+
const cursor = JSON.parse(Buffer.from(page.pageToken, 'base64url').toString('utf8'));
|
|
137
|
+
if (!Array.isArray(cursor) ||
|
|
138
|
+
cursor.length !== 3 ||
|
|
139
|
+
cursor[0] !== contextId ||
|
|
140
|
+
!Number.isSafeInteger(cursor[1]) ||
|
|
141
|
+
!Number.isSafeInteger(cursor[2]) ||
|
|
142
|
+
cursor[2] < 0)
|
|
143
|
+
throw new Error();
|
|
144
|
+
[, cursorVersion, offset] = cursor;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
throw new RequestMalformedError('Invalid inbox page token.');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const run = await this.context(contextId);
|
|
151
|
+
const agent = run.entry.handle.agent;
|
|
152
|
+
const version = agent.session.seq - 1;
|
|
153
|
+
const steps = agent.inbox.nextStep;
|
|
154
|
+
const turns = agent.inbox.nextTurn;
|
|
155
|
+
const totalSize = steps.length + turns.length;
|
|
156
|
+
if ((cursorVersion !== undefined && cursorVersion !== version) || offset > totalSize)
|
|
157
|
+
throw new RequestMalformedError('Inbox page is stale; read again without pageToken.');
|
|
158
|
+
const limit = page === undefined ? totalSize : (page.pageSize ?? 50);
|
|
159
|
+
const entry = (message) => {
|
|
160
|
+
const task = this.owner(run, message);
|
|
161
|
+
const protocol = task
|
|
162
|
+
? this.protocolMessage(run, task, message)
|
|
163
|
+
: Message.fromJSON({
|
|
164
|
+
messageId: message.id,
|
|
165
|
+
contextId,
|
|
166
|
+
role: 'ROLE_USER',
|
|
167
|
+
parts: message.content
|
|
168
|
+
.filter((block) => block.type === 'text')
|
|
169
|
+
.map((block) => ({ text: block.text })),
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
id: message.id,
|
|
173
|
+
message: Message.toJSON(protocol),
|
|
174
|
+
editable: message.source.kind === 'a2a',
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const nextStep = [];
|
|
178
|
+
const nextTurn = [];
|
|
179
|
+
let count = 0;
|
|
180
|
+
let bytes = 2;
|
|
181
|
+
for (let index = offset; index < totalSize && count < limit; index++) {
|
|
182
|
+
if (page !== undefined && count && bytes >= 1024 * 1024)
|
|
183
|
+
break;
|
|
184
|
+
const step = index < steps.length;
|
|
185
|
+
const item = entry(step ? steps[index] : turns[index - steps.length]);
|
|
186
|
+
if (page !== undefined) {
|
|
187
|
+
const size = Buffer.byteLength(JSON.stringify(item)) + (count ? 1 : 0);
|
|
188
|
+
// Keep each A2A Message intact, allowing one oversized item to make progress.
|
|
189
|
+
if (count && bytes + size > 1024 * 1024)
|
|
190
|
+
break;
|
|
191
|
+
bytes += size;
|
|
192
|
+
}
|
|
193
|
+
(step ? nextStep : nextTurn).push(item);
|
|
194
|
+
count++;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
contextId,
|
|
198
|
+
version,
|
|
199
|
+
nextTurn,
|
|
200
|
+
nextStep,
|
|
201
|
+
totalSize,
|
|
202
|
+
nextPageToken: offset + count < totalSize
|
|
203
|
+
? Buffer.from(JSON.stringify([contextId, version, offset + count])).toString('base64url')
|
|
204
|
+
: '',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async editQueue(contextId, edit, page) {
|
|
208
|
+
validateInboxPage(page);
|
|
209
|
+
if (page?.pageToken)
|
|
210
|
+
throw new RequestMalformedError('pageToken is only supported by inbox get.');
|
|
211
|
+
const run = await this.context(contextId);
|
|
212
|
+
const operation = run.admissionTail.then(async () => {
|
|
213
|
+
if (this.stopping || this.bridge.isClearing(contextId))
|
|
214
|
+
throw new RequestMalformedError('Inbox is unloading or clearing.');
|
|
215
|
+
await edit(run);
|
|
216
|
+
await this.flush(run.entry.handle.agent.session);
|
|
217
|
+
});
|
|
218
|
+
run.admissionTail = operation.catch(() => { });
|
|
219
|
+
await operation;
|
|
220
|
+
return this.get(contextId, page);
|
|
221
|
+
}
|
|
222
|
+
queued(run, id) {
|
|
223
|
+
const agent = run.entry.handle.agent;
|
|
224
|
+
const target = agent.inbox.nextTurn.some((message) => message.id === id)
|
|
225
|
+
? 'next-turn'
|
|
226
|
+
: 'next-step';
|
|
227
|
+
const items = target === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep;
|
|
228
|
+
const index = items.findIndex((message) => message.id === id);
|
|
229
|
+
const message = items[index];
|
|
230
|
+
if (!message)
|
|
231
|
+
throw new RequestMalformedError('Input is no longer pending.');
|
|
232
|
+
return { agent, target, items, index, message };
|
|
233
|
+
}
|
|
234
|
+
async update(contextId, id, message, page) {
|
|
235
|
+
return this.editQueue(contextId, async (run) => {
|
|
236
|
+
const queued = this.queued(run, id);
|
|
237
|
+
const task = this.owner(run, queued.message);
|
|
238
|
+
if (queued.message.source.kind !== 'a2a' || !task)
|
|
239
|
+
throw new RequestMalformedError('Only task-attributed A2A input can be edited.');
|
|
240
|
+
const original = this.protocolMessage(run, task, queued.message);
|
|
241
|
+
if (message.messageId !== original.messageId ||
|
|
242
|
+
message.taskId !== original.taskId ||
|
|
243
|
+
message.contextId !== contextId ||
|
|
244
|
+
message.role !== Role.ROLE_USER ||
|
|
245
|
+
!message.parts.length ||
|
|
246
|
+
message.parts.some((part) => !part.content))
|
|
247
|
+
throw new RequestMalformedError('A user Message with unchanged messageId, taskId and contextId is required.');
|
|
248
|
+
const content = await this.content(message, contextId);
|
|
249
|
+
if (this.stopping ||
|
|
250
|
+
this.bridge.isClearing(contextId) ||
|
|
251
|
+
this.queued(run, id).message !== queued.message)
|
|
252
|
+
throw new RequestMalformedError('Input changed while editing.');
|
|
253
|
+
await this.ctx.serial('a2a/before-followup', {
|
|
254
|
+
agent: queued.agent,
|
|
255
|
+
contextId,
|
|
256
|
+
taskId: task.task.id,
|
|
257
|
+
a2aMessage: message,
|
|
258
|
+
dshMessageId: id,
|
|
259
|
+
requestHeaders: task.headers.get(id),
|
|
260
|
+
signal: task.controller.signal,
|
|
261
|
+
});
|
|
262
|
+
if (this.stopping ||
|
|
263
|
+
this.bridge.isClearing(contextId) ||
|
|
264
|
+
this.queued(run, id).message !== queued.message)
|
|
265
|
+
throw new RequestMalformedError('Input changed while editing.');
|
|
266
|
+
const replacement = {
|
|
267
|
+
...queued.message,
|
|
268
|
+
content,
|
|
269
|
+
source: {
|
|
270
|
+
...queued.message.source,
|
|
271
|
+
message: JSON.stringify(Message.toJSON(message)),
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
if (!queued.agent.inbox.replace(queued.message.id, replacement))
|
|
275
|
+
throw new RequestMalformedError('Input has already been claimed.');
|
|
276
|
+
task.task.history = task.task.history.map((item) => item.messageId === original.messageId
|
|
277
|
+
? markInboxInputPending(structuredClone(message))
|
|
278
|
+
: item);
|
|
279
|
+
await this.checkpoint(task.task, task.context);
|
|
280
|
+
}, page);
|
|
281
|
+
}
|
|
282
|
+
async move(contextId, id, index, page) {
|
|
283
|
+
return this.editQueue(contextId, (run) => this.reorder(this.queued(run, id), index), page);
|
|
284
|
+
}
|
|
285
|
+
reorder(queued, index) {
|
|
286
|
+
if (!Number.isInteger(index) || index < 0 || index >= queued.items.length)
|
|
287
|
+
throw new RequestMalformedError('index must address the same pending list.');
|
|
288
|
+
if (index === queued.index)
|
|
289
|
+
return;
|
|
290
|
+
const start = Math.min(queued.index, index);
|
|
291
|
+
const items = queued.items.slice(start, Math.max(queued.index, index) + 1);
|
|
292
|
+
items.splice(queued.index - start, 1);
|
|
293
|
+
items.splice(index - start, 0, queued.message);
|
|
294
|
+
// One atomic native splice records only the affected interval.
|
|
295
|
+
queued.agent.inbox.splice(queued.target, start, items.length, items);
|
|
296
|
+
}
|
|
297
|
+
async removed(run, messages) {
|
|
298
|
+
const removed = new Map();
|
|
299
|
+
for (const message of messages) {
|
|
300
|
+
const task = this.owner(run, message);
|
|
301
|
+
if (!task || message.source.kind !== 'a2a')
|
|
302
|
+
continue;
|
|
303
|
+
let ids = removed.get(task);
|
|
304
|
+
if (!ids) {
|
|
305
|
+
ids = new Set();
|
|
306
|
+
removed.set(task, ids);
|
|
307
|
+
}
|
|
308
|
+
ids.add(Message.fromJSON(JSON.parse(message.source.message)).messageId);
|
|
309
|
+
}
|
|
310
|
+
// One queue durability barrier precedes all terminal task publications.
|
|
311
|
+
await this.flush(run.entry.handle.agent.session);
|
|
312
|
+
const pending = new Set(this.pending(run).map((message) => this.owner(run, message)));
|
|
313
|
+
const entries = removed.entries();
|
|
314
|
+
const errors = [];
|
|
315
|
+
await Promise.all(Array.from({ length: Math.min(4, removed.size) }, async () => {
|
|
316
|
+
for (const [task, ids] of entries) {
|
|
317
|
+
if (task.settled)
|
|
318
|
+
continue;
|
|
319
|
+
try {
|
|
320
|
+
task.task.history = task.task.history.filter((message) => !ids.has(message.messageId));
|
|
321
|
+
if (run.active !== task && !run.claimed.has(task) && !pending.has(task))
|
|
322
|
+
await this.cancelTask(run, task, false);
|
|
323
|
+
else
|
|
324
|
+
await this.checkpoint(task.task, task.context);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
if (!errors.length)
|
|
328
|
+
errors.push(error);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}));
|
|
332
|
+
if (errors.length)
|
|
333
|
+
throw errors[0];
|
|
334
|
+
}
|
|
335
|
+
async remove(contextId, id, page) {
|
|
336
|
+
return this.editQueue(contextId, async (run) => {
|
|
337
|
+
const { agent, message } = this.queued(run, id);
|
|
338
|
+
agent.inbox.remove(message.id);
|
|
339
|
+
await this.removed(run, [message]);
|
|
340
|
+
}, page);
|
|
341
|
+
}
|
|
342
|
+
async clearPending(contextId, page) {
|
|
343
|
+
return this.editQueue(contextId, async (run) => {
|
|
344
|
+
const messages = this.pending(run);
|
|
345
|
+
run.entry.handle.agent.inbox.clear();
|
|
346
|
+
await this.removed(run, messages);
|
|
347
|
+
}, page);
|
|
348
|
+
}
|
|
349
|
+
async executePending(contextId, id, page) {
|
|
350
|
+
return this.editQueue(contextId, async (run) => {
|
|
351
|
+
if (id !== undefined) {
|
|
352
|
+
const queued = this.queued(run, id);
|
|
353
|
+
if (queued.target !== 'next-turn')
|
|
354
|
+
throw new RequestMalformedError('Execute selects next-turn input; use steer for next-step input.');
|
|
355
|
+
this.reorder(queued, 0);
|
|
356
|
+
}
|
|
357
|
+
await this.wake(contextId);
|
|
358
|
+
}, page);
|
|
359
|
+
}
|
|
113
360
|
async enqueue(taskId, message, options = {}) {
|
|
114
361
|
if (this.stopping)
|
|
115
362
|
throw new Error('Native inbox is unloading.');
|
|
@@ -138,7 +385,7 @@ export class NativeInbox {
|
|
|
138
385
|
const items = from === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep;
|
|
139
386
|
const index = items.findIndex((item) => item.id === message.id);
|
|
140
387
|
if (index < 0)
|
|
141
|
-
throw new
|
|
388
|
+
throw new RequestMalformedError('Queued input has already been claimed.');
|
|
142
389
|
agent.session.append('agent/inbox/spliced', {
|
|
143
390
|
target: from,
|
|
144
391
|
start: index,
|
|
@@ -164,10 +411,10 @@ export class NativeInbox {
|
|
|
164
411
|
const target = run.tasks.get(taskId);
|
|
165
412
|
const message = agent.inbox.nextTurn.find((item) => item.id === messageId);
|
|
166
413
|
if (message?.source.kind !== 'a2a')
|
|
167
|
-
throw new
|
|
414
|
+
throw new RequestMalformedError('Queued input has already been claimed.');
|
|
168
415
|
const source = run.tasks.get(message.source.taskId);
|
|
169
416
|
if (!target || !source)
|
|
170
|
-
throw new
|
|
417
|
+
throw new RequestMalformedError('Promotion requires live source and target tasks.');
|
|
171
418
|
const valid = () => !this.stopping &&
|
|
172
419
|
!this.bridge.isClearing(run.entry.sessionId) &&
|
|
173
420
|
!target.settled &&
|
|
@@ -181,7 +428,7 @@ export class NativeInbox {
|
|
|
181
428
|
!run.claimed.has(source) &&
|
|
182
429
|
agent.inbox.nextTurn.some((item) => item === message);
|
|
183
430
|
if (!valid())
|
|
184
|
-
throw new
|
|
431
|
+
throw new RequestMalformedError('Promotion requires an unclaimed independent input and an active target.');
|
|
185
432
|
const original = Message.fromJSON(JSON.parse(message.source.message));
|
|
186
433
|
original.taskId = taskId;
|
|
187
434
|
original.referenceTaskIds = [...new Set([...original.referenceTaskIds, source.task.id])];
|
|
@@ -201,11 +448,11 @@ export class NativeInbox {
|
|
|
201
448
|
try {
|
|
202
449
|
await this.checkpoint(target.task, target.context);
|
|
203
450
|
if (!valid())
|
|
204
|
-
throw new
|
|
451
|
+
throw new RequestMalformedError('Promotion raced input consumption or task settlement.');
|
|
205
452
|
target.task.history = [...target.task.history, markInboxInputPending(original)];
|
|
206
453
|
target.headers.set(message.id, source.headers.get(message.id));
|
|
207
454
|
if (!agent.inbox.replace(message.id, promoted))
|
|
208
|
-
throw new
|
|
455
|
+
throw new RequestMalformedError('Queued input has already been claimed.');
|
|
209
456
|
this.moveInput(agent, promoted, 'next-step');
|
|
210
457
|
try {
|
|
211
458
|
await this.flush(agent.session);
|
|
@@ -244,7 +491,7 @@ export class NativeInbox {
|
|
|
244
491
|
if (this.stopping ||
|
|
245
492
|
this.contexts.get(run.entry.sessionId) !== run ||
|
|
246
493
|
this.bridge.isClearing(run.entry.sessionId))
|
|
247
|
-
throw new
|
|
494
|
+
throw new RequestMalformedError('Promotion was interrupted by context disposal.');
|
|
248
495
|
source.controller.abort();
|
|
249
496
|
try {
|
|
250
497
|
await this.checkpoint(target.task, target.context);
|
|
@@ -259,6 +506,59 @@ export class NativeInbox {
|
|
|
259
506
|
this.settle(run, target);
|
|
260
507
|
}
|
|
261
508
|
}
|
|
509
|
+
async resume(contextId, taskId) {
|
|
510
|
+
const context = new ServerCallContext();
|
|
511
|
+
const previous = await this.store.load(taskId, context);
|
|
512
|
+
if (!previous || previous.contextId !== contextId || !this.terminal(previous))
|
|
513
|
+
throw new Error('Resume requires a terminal retained task.');
|
|
514
|
+
const retained = previous.metadata?.[RETAINED_INPUTS];
|
|
515
|
+
if (typeof retained !== 'string')
|
|
516
|
+
throw new Error('Task has no retained input to resume.');
|
|
517
|
+
const inputs = JSON.parse(retained);
|
|
518
|
+
if (!inputs.length)
|
|
519
|
+
throw new Error('Task has no retained input to resume.');
|
|
520
|
+
const run = await this.context(contextId, context);
|
|
521
|
+
const id = randomUUID();
|
|
522
|
+
const replacement = {
|
|
523
|
+
...previous,
|
|
524
|
+
id,
|
|
525
|
+
status: {
|
|
526
|
+
state: TaskState.TASK_STATE_SUBMITTED,
|
|
527
|
+
timestamp: new Date().toISOString(),
|
|
528
|
+
message: undefined,
|
|
529
|
+
},
|
|
530
|
+
history: [],
|
|
531
|
+
metadata: { ...previous.metadata },
|
|
532
|
+
};
|
|
533
|
+
delete replacement.metadata[RETAINED_INPUTS];
|
|
534
|
+
const retainedInputs = inputs.map((input) => {
|
|
535
|
+
if (input.message.source.kind !== 'a2a')
|
|
536
|
+
throw new Error('Retained input has invalid task ownership.');
|
|
537
|
+
const original = Message.fromJSON(JSON.parse(input.message.source.message));
|
|
538
|
+
original.taskId = id;
|
|
539
|
+
original.referenceTaskIds = [...new Set([...original.referenceTaskIds, taskId])];
|
|
540
|
+
replacement.history.push(markInboxInputPending(original));
|
|
541
|
+
return {
|
|
542
|
+
...input,
|
|
543
|
+
message: {
|
|
544
|
+
...input.message,
|
|
545
|
+
source: {
|
|
546
|
+
...input.message.source,
|
|
547
|
+
taskId: id,
|
|
548
|
+
message: JSON.stringify(Message.toJSON(original)),
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
});
|
|
553
|
+
await this.checkpoint(replacement, context);
|
|
554
|
+
for (const [index, input] of retainedInputs.entries()) {
|
|
555
|
+
if (this.pending(run).some((pending) => pending.id === input.message.id))
|
|
556
|
+
run.entry.handle.agent.inbox.remove(input.message.id);
|
|
557
|
+
run.entry.handle.agent.send(input.message, input.target, index === retainedInputs.length - 1);
|
|
558
|
+
}
|
|
559
|
+
this.restoreTask(run, replacement, context);
|
|
560
|
+
return id;
|
|
561
|
+
}
|
|
262
562
|
/** Repair only an incomplete ownership move, never an ordinary DSH claim. */
|
|
263
563
|
async recoverPromotions(run, context) {
|
|
264
564
|
const agent = run.entry.handle.agent;
|
|
@@ -335,9 +635,11 @@ export class NativeInbox {
|
|
|
335
635
|
const retained = task.metadata?.[RETAINED_INPUTS];
|
|
336
636
|
if (typeof retained !== 'string')
|
|
337
637
|
continue;
|
|
338
|
-
snapshots
|
|
638
|
+
// Terminal snapshots belong to explicit resume(); do not consume their
|
|
639
|
+
// retained metadata during ordinary context recovery.
|
|
339
640
|
if (this.terminal(task))
|
|
340
641
|
continue;
|
|
642
|
+
snapshots.push(task);
|
|
341
643
|
for (const input of JSON.parse(retained)) {
|
|
342
644
|
const { message } = input;
|
|
343
645
|
if (message.source.kind !== 'a2a' || message.source.taskId !== task.id)
|
|
@@ -348,18 +650,87 @@ export class NativeInbox {
|
|
|
348
650
|
}
|
|
349
651
|
pageToken = page.nextPageToken;
|
|
350
652
|
} while (pageToken);
|
|
351
|
-
|
|
653
|
+
const queuedAtOf = (input) => input.message.source.kind === 'a2a' ? (input.message.source.queuedAt ?? 0) : 0;
|
|
654
|
+
// Equal per-task positions must not inherit the store's timestamp ordering.
|
|
655
|
+
const ordered = inputs.sort((a, b) => a.position - b.position || queuedAtOf(a) - queuedAtOf(b));
|
|
656
|
+
const destinationIds = {
|
|
657
|
+
'next-step': new Set(agent.inbox.nextStep.map((message) => message.id)),
|
|
658
|
+
'next-turn': new Set(agent.inbox.nextTurn.map((message) => message.id)),
|
|
659
|
+
};
|
|
660
|
+
const existingIds = new Set([...destinationIds['next-step'], ...destinationIds['next-turn']]);
|
|
661
|
+
const byPosition = new Map();
|
|
662
|
+
for (const input of ordered) {
|
|
663
|
+
const positions = byPosition.get(input.target) ?? new Map();
|
|
664
|
+
const group = positions.get(input.position) ?? [];
|
|
665
|
+
group.push(input);
|
|
666
|
+
positions.set(input.position, group);
|
|
667
|
+
byPosition.set(input.target, positions);
|
|
668
|
+
}
|
|
669
|
+
const nextInGroup = new Map();
|
|
670
|
+
const nextPosition = new Map();
|
|
671
|
+
for (const [target, positions] of byPosition) {
|
|
672
|
+
nextInGroup.set(target, new Map());
|
|
673
|
+
nextPosition.set(target, new Map());
|
|
674
|
+
const orderedPositions = [...positions.keys()].sort((a, b) => a - b);
|
|
675
|
+
for (const position of orderedPositions) {
|
|
676
|
+
const group = positions.get(position);
|
|
677
|
+
const stack = [];
|
|
678
|
+
for (let index = 0; index < group.length; index++) {
|
|
679
|
+
while (stack.length && queuedAtOf(group[stack.at(-1)]) <= queuedAtOf(group[index]))
|
|
680
|
+
stack.pop();
|
|
681
|
+
if (stack.length)
|
|
682
|
+
nextInGroup.get(target).set(group[index].message.id, group[stack.at(-1)]);
|
|
683
|
+
stack.push(index);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
for (let index = orderedPositions.length - 1; index >= 0; index--) {
|
|
687
|
+
const position = orderedPositions[index];
|
|
688
|
+
const next = orderedPositions[index + 1];
|
|
689
|
+
nextPosition.get(target).set(position, next);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const laterFor = (input) => {
|
|
693
|
+
let candidate = nextInGroup.get(input.target)?.get(input.message.id);
|
|
694
|
+
while (candidate && !destinationIds[input.target].has(candidate.message.id))
|
|
695
|
+
candidate = nextInGroup.get(input.target)?.get(candidate.message.id);
|
|
696
|
+
if (candidate)
|
|
697
|
+
return candidate;
|
|
698
|
+
let position = nextPosition.get(input.target)?.get(input.position);
|
|
699
|
+
const queuedAt = queuedAtOf(input);
|
|
700
|
+
while (position !== undefined) {
|
|
701
|
+
const candidate = byPosition
|
|
702
|
+
.get(input.target)
|
|
703
|
+
?.get(position)
|
|
704
|
+
?.find((item) => queuedAtOf(item) > queuedAt && destinationIds[input.target].has(item.message.id));
|
|
705
|
+
if (candidate)
|
|
706
|
+
return candidate;
|
|
707
|
+
position = nextPosition.get(input.target)?.get(position);
|
|
708
|
+
}
|
|
709
|
+
return undefined;
|
|
710
|
+
};
|
|
711
|
+
for (const input of ordered) {
|
|
712
|
+
const { message, target } = input;
|
|
352
713
|
if (message.source.kind !== 'a2a')
|
|
353
714
|
continue;
|
|
354
|
-
const
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
const destination = target === 'next-step' ? agent.inbox.nextStep : agent.inbox.nextTurn;
|
|
358
|
-
if (!destination.some((pending) => pending.id === message.id))
|
|
715
|
+
const destination = target === 'next-step' ? agent.inbox.nextStep : agent.inbox.nextTurn;
|
|
716
|
+
if (existingIds.has(message.id)) {
|
|
717
|
+
if (!destinationIds[target].has(message.id)) {
|
|
359
718
|
this.moveInput(agent, message, target);
|
|
719
|
+
const from = target === 'next-step' ? 'next-turn' : 'next-step';
|
|
720
|
+
destinationIds[from].delete(message.id);
|
|
721
|
+
destinationIds[target].add(message.id);
|
|
722
|
+
}
|
|
723
|
+
continue;
|
|
360
724
|
}
|
|
361
|
-
|
|
362
|
-
|
|
725
|
+
// Keep the recovered public order; only an interrupted earlier input is
|
|
726
|
+
// inserted before the first retained message queued after it.
|
|
727
|
+
const later = laterFor(input);
|
|
728
|
+
const insertAt = later
|
|
729
|
+
? destination.findIndex((pending) => pending.id === later.message.id)
|
|
730
|
+
: destination.length;
|
|
731
|
+
agent.inbox.splice(target, Math.min(insertAt, destination.length), 0, [message]);
|
|
732
|
+
existingIds.add(message.id);
|
|
733
|
+
destinationIds[target].add(message.id);
|
|
363
734
|
}
|
|
364
735
|
if (!snapshots.length)
|
|
365
736
|
return;
|
|
@@ -381,14 +752,21 @@ export class NativeInbox {
|
|
|
381
752
|
const { entry } = await this.bridge.ensureTask(randomUUID(), contextId);
|
|
382
753
|
const run = {
|
|
383
754
|
entry,
|
|
755
|
+
paused: false,
|
|
384
756
|
tasks: new Map(),
|
|
385
757
|
claimed: new Set(),
|
|
386
758
|
admissions: new Set(),
|
|
759
|
+
commits: new Set(),
|
|
387
760
|
admissionTail: Promise.resolve(),
|
|
388
761
|
promotions: new Map(),
|
|
389
762
|
rolledBack: new Set(),
|
|
390
763
|
retained: { 'next-step': [], 'next-turn': [] },
|
|
391
764
|
};
|
|
765
|
+
const pause = [...run.entry.handle.agent.session.snapshotEvents()]
|
|
766
|
+
.reverse()
|
|
767
|
+
.find((event) => event.type === 'agent/inbox/paused');
|
|
768
|
+
if (pause)
|
|
769
|
+
run.paused = pause.data.paused;
|
|
392
770
|
await this.recoverRetained(run, context);
|
|
393
771
|
await this.recoverPromotions(run, context);
|
|
394
772
|
const snapshots = new Map();
|
|
@@ -471,8 +849,15 @@ export class NativeInbox {
|
|
|
471
849
|
console.error('[dsh-a2a] inbox observer failed:', error);
|
|
472
850
|
}
|
|
473
851
|
}
|
|
474
|
-
flush(session) {
|
|
475
|
-
|
|
852
|
+
async flush(session) {
|
|
853
|
+
// Core teardown closes handles concurrently; the service-wide barrier drains only live writers.
|
|
854
|
+
if (this.stopping && this.persistence)
|
|
855
|
+
return this.persistence.flush();
|
|
856
|
+
const sessions = this.ctx.get('sessions');
|
|
857
|
+
if (sessions)
|
|
858
|
+
await sessions.flush(session);
|
|
859
|
+
else
|
|
860
|
+
await this.ctx.parallel('session/flush', session);
|
|
476
861
|
}
|
|
477
862
|
checkpoint(task, context) {
|
|
478
863
|
return this.store.checkpoint
|
|
@@ -607,6 +992,8 @@ export class NativeInbox {
|
|
|
607
992
|
].includes(task.status.state));
|
|
608
993
|
}
|
|
609
994
|
async admit(run, task, a2aMessage, context, target, wakeup, accepted) {
|
|
995
|
+
if (run.paused)
|
|
996
|
+
throw new Error('Inbox is paused.');
|
|
610
997
|
if (target === 'next-step' && run.active !== task)
|
|
611
998
|
throw new Error('Steering must address an active task.');
|
|
612
999
|
const duplicate = () => [...run.tasks.values()].some((owned) => owned.task.history.some((message) => message.messageId === a2aMessage.messageId));
|
|
@@ -654,11 +1041,21 @@ export class NativeInbox {
|
|
|
654
1041
|
? history
|
|
655
1042
|
: [...history, markInboxInputPending(a2aMessage)];
|
|
656
1043
|
await this.checkpoint({ ...task.task, history: acceptedHistory }, context);
|
|
657
|
-
abort
|
|
1044
|
+
// The durable checkpoint is the admission commit point. A later abort may
|
|
1045
|
+
// reject the caller, but must not strand history without its native input.
|
|
658
1046
|
task.task.history = acceptedHistory;
|
|
659
1047
|
task.headers.set(message.id, headers);
|
|
660
1048
|
accepted?.();
|
|
1049
|
+
if (this.stopping && run.entry.borrowed) {
|
|
1050
|
+
if (this.retainPending && !task.discardInput && !run.discardInput)
|
|
1051
|
+
run.retained[target].push(structuredClone(message));
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
661
1054
|
run.entry.handle.agent.send(message, target, wakeup);
|
|
1055
|
+
// Explicit discard markers survive cancel/clear state resets. Disposal
|
|
1056
|
+
// and retain-pending shutdown deliberately leave them unset.
|
|
1057
|
+
if (task.discardInput || run.discardInput)
|
|
1058
|
+
run.entry.handle.agent.inbox.remove(message.id);
|
|
662
1059
|
this.observeInput('admitted', run, message);
|
|
663
1060
|
await this.flush(run.entry.handle.agent.session);
|
|
664
1061
|
void this.ctx
|
|
@@ -681,6 +1078,10 @@ export class NativeInbox {
|
|
|
681
1078
|
onAbort();
|
|
682
1079
|
}),
|
|
683
1080
|
]);
|
|
1081
|
+
// Admission order follows the abortable wait, but shutdown separately drains
|
|
1082
|
+
// every full commit so an interrupted caller cannot strand durable input.
|
|
1083
|
+
run.commits.add(work);
|
|
1084
|
+
void work.finally(() => run.commits.delete(work)).catch(() => { });
|
|
684
1085
|
run.admissionTail = admitted.catch(() => { });
|
|
685
1086
|
try {
|
|
686
1087
|
await admitted;
|
|
@@ -818,6 +1219,8 @@ export class NativeInbox {
|
|
|
818
1219
|
const run = this.contexts.get(detail.agent.session.id);
|
|
819
1220
|
if (!run)
|
|
820
1221
|
return;
|
|
1222
|
+
if (run.paused)
|
|
1223
|
+
throw new Error('Inbox is paused.');
|
|
821
1224
|
if (this.stopping)
|
|
822
1225
|
throw new Error('Inbox is unloading.');
|
|
823
1226
|
for (let index = detail.messages.length - 1; index >= 0; index--) {
|
|
@@ -846,8 +1249,6 @@ export class NativeInbox {
|
|
|
846
1249
|
if (run.rolledBack.delete(message.source.promotionId))
|
|
847
1250
|
detail.messages.splice(index, 1);
|
|
848
1251
|
}
|
|
849
|
-
if (!run.active && detail.messages.some((message) => !this.owner(run, message)))
|
|
850
|
-
throw new Error('Native host input requires task-attributed a2aInbox.enqueue().');
|
|
851
1252
|
// Restarted steering and an ordinary queued turn may be claimed together by the core.
|
|
852
1253
|
// Requeue other owners before committing any user message, keeping task lifecycles separate.
|
|
853
1254
|
const owner = detail.messages.find((message) => message.source.kind === 'a2a');
|
|
@@ -875,6 +1276,7 @@ export class NativeInbox {
|
|
|
875
1276
|
if (run.active && run.active !== task)
|
|
876
1277
|
throw new Error('A model step cannot consume inputs from different A2A tasks.');
|
|
877
1278
|
this.select(run, task);
|
|
1279
|
+
task.claimedInputs = detail.messages.filter((candidate) => this.owner(run, candidate) === task);
|
|
878
1280
|
const selectedAt = task.selectedAt.get(message.id) ?? Date.now();
|
|
879
1281
|
const timing = { selectedAt, turn: detail.turn, step: detail.step };
|
|
880
1282
|
try {
|
|
@@ -914,6 +1316,12 @@ export class NativeInbox {
|
|
|
914
1316
|
return true;
|
|
915
1317
|
}
|
|
916
1318
|
const task = run.active;
|
|
1319
|
+
if (event.type === 'turn/end' && run.paused) {
|
|
1320
|
+
// Paused turns must never discard retained input, including a driver
|
|
1321
|
+
// started accidentally before wake(). Requeue before any failure cleanup.
|
|
1322
|
+
this.requeueClaimed(run);
|
|
1323
|
+
return true;
|
|
1324
|
+
}
|
|
917
1325
|
if (event.type === 'turn/end') {
|
|
918
1326
|
if (!this.stopping && ['error', 'blocked', 'aborted'].includes(event.data.reason.kind))
|
|
919
1327
|
for (const owner of run.claimed)
|
|
@@ -923,6 +1331,8 @@ export class NativeInbox {
|
|
|
923
1331
|
}
|
|
924
1332
|
if (!task)
|
|
925
1333
|
return true;
|
|
1334
|
+
if (event.type === 'turn/end')
|
|
1335
|
+
task.claimedInputs = undefined;
|
|
926
1336
|
if (task.settled) {
|
|
927
1337
|
if (event.type === 'turn/end')
|
|
928
1338
|
run.active = undefined;
|
|
@@ -1091,9 +1501,10 @@ export class NativeInbox {
|
|
|
1091
1501
|
}
|
|
1092
1502
|
return undefined;
|
|
1093
1503
|
}
|
|
1094
|
-
async cancelTask(run, task) {
|
|
1504
|
+
async cancelTask(run, task, flush = true) {
|
|
1095
1505
|
const contextId = run.entry.sessionId;
|
|
1096
1506
|
const taskId = task.task.id;
|
|
1507
|
+
task.discardInput = true;
|
|
1097
1508
|
this.discard(run, taskId);
|
|
1098
1509
|
task.controller.abort();
|
|
1099
1510
|
task.settled = true;
|
|
@@ -1105,7 +1516,8 @@ export class NativeInbox {
|
|
|
1105
1516
|
}
|
|
1106
1517
|
try {
|
|
1107
1518
|
try {
|
|
1108
|
-
|
|
1519
|
+
if (flush)
|
|
1520
|
+
await this.flush(run.entry.handle.agent.session);
|
|
1109
1521
|
}
|
|
1110
1522
|
finally {
|
|
1111
1523
|
await this.checkpoint(structuredClone(task.task), task.context);
|
|
@@ -1116,11 +1528,64 @@ export class NativeInbox {
|
|
|
1116
1528
|
task.finish();
|
|
1117
1529
|
}
|
|
1118
1530
|
}
|
|
1531
|
+
requeueClaimed(run) {
|
|
1532
|
+
const task = run.active;
|
|
1533
|
+
if (!task?.claimedInputs)
|
|
1534
|
+
return;
|
|
1535
|
+
const committed = new Set(run.entry.handle.agent.session
|
|
1536
|
+
.snapshotEvents()
|
|
1537
|
+
.flatMap((event) => (event.type === 'user/message' ? [event.data.id] : [])));
|
|
1538
|
+
for (const message of [...task.claimedInputs].reverse()) {
|
|
1539
|
+
if (message.source.kind !== 'a2a' || committed.has(message.id))
|
|
1540
|
+
continue;
|
|
1541
|
+
if (!this.pending(run).some((pending) => pending.id === message.id))
|
|
1542
|
+
run.entry.handle.agent.inbox.prepend(message.source.target, message);
|
|
1543
|
+
}
|
|
1544
|
+
task.claimedInputs = undefined;
|
|
1545
|
+
}
|
|
1546
|
+
async pause(contextId) {
|
|
1547
|
+
const run = await this.context(contextId);
|
|
1548
|
+
if (run.paused)
|
|
1549
|
+
return;
|
|
1550
|
+
run.paused = true;
|
|
1551
|
+
run.entry.handle.agent.session.append('agent/inbox/paused', { paused: true });
|
|
1552
|
+
for (const admission of run.admissions)
|
|
1553
|
+
admission.abort();
|
|
1554
|
+
run.entry.handle.agent.cancel({ kind: 'user' }, { keepInbox: true });
|
|
1555
|
+
await run.entry.handle.agent.whenIdle();
|
|
1556
|
+
this.requeueClaimed(run);
|
|
1557
|
+
}
|
|
1558
|
+
async wake(contextId) {
|
|
1559
|
+
const run = await this.context(contextId);
|
|
1560
|
+
const agent = run.entry.handle.agent;
|
|
1561
|
+
const pending = this.pending(run);
|
|
1562
|
+
run.paused = false;
|
|
1563
|
+
agent.session.append('agent/inbox/paused', { paused: false });
|
|
1564
|
+
const target = agent.inbox.nextTurn.length ? 'next-turn' : 'next-step';
|
|
1565
|
+
const items = target === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep;
|
|
1566
|
+
const input = items.at(-1);
|
|
1567
|
+
if (input) {
|
|
1568
|
+
// Agent.send is the public wake seam; reinsert only the tail, without canceling it.
|
|
1569
|
+
agent.session.append('agent/inbox/spliced', {
|
|
1570
|
+
target,
|
|
1571
|
+
start: items.length - 1,
|
|
1572
|
+
removedCount: 1,
|
|
1573
|
+
inserted: [],
|
|
1574
|
+
});
|
|
1575
|
+
agent.send(input, target, true);
|
|
1576
|
+
}
|
|
1577
|
+
const taskIds = [
|
|
1578
|
+
...new Set(pending.flatMap((input) => (input.source.kind === 'a2a' ? [input.source.taskId] : []))),
|
|
1579
|
+
];
|
|
1580
|
+
this.observe('woken', run, { taskIds });
|
|
1581
|
+
return taskIds;
|
|
1582
|
+
}
|
|
1119
1583
|
async clear(contextId) {
|
|
1120
1584
|
await this.resolving.get(contextId);
|
|
1121
1585
|
const run = this.contexts.get(contextId);
|
|
1122
1586
|
if (!run)
|
|
1123
1587
|
return;
|
|
1588
|
+
run.discardInput = true;
|
|
1124
1589
|
const taskIds = [
|
|
1125
1590
|
...new Set([...run.tasks.values()]
|
|
1126
1591
|
.filter((task) => !task.settled)
|
|
@@ -1165,27 +1630,69 @@ export class NativeInbox {
|
|
|
1165
1630
|
this.contexts.delete(session.id);
|
|
1166
1631
|
}
|
|
1167
1632
|
shutdown() {
|
|
1633
|
+
this.changes.close();
|
|
1168
1634
|
if (this.shutdownPromise)
|
|
1169
1635
|
return this.shutdownPromise;
|
|
1170
1636
|
this.stopping = true;
|
|
1637
|
+
const failures = [];
|
|
1638
|
+
const borrowed = new Set();
|
|
1639
|
+
const detachBorrowed = () => {
|
|
1640
|
+
for (const run of this.contexts.values()) {
|
|
1641
|
+
if (!run.entry.borrowed)
|
|
1642
|
+
continue;
|
|
1643
|
+
borrowed.add(run);
|
|
1644
|
+
this.contexts.delete(run.entry.sessionId);
|
|
1645
|
+
// Freeze retention before detaching owned input from a still-live native driver.
|
|
1646
|
+
try {
|
|
1647
|
+
const agent = run.entry.handle.agent;
|
|
1648
|
+
if (run.active) {
|
|
1649
|
+
agent.cancel({ kind: 'disposed' }, { keepInbox: true });
|
|
1650
|
+
run.active = undefined;
|
|
1651
|
+
}
|
|
1652
|
+
for (const target of ['next-step', 'next-turn']) {
|
|
1653
|
+
const items = target === 'next-step' ? agent.inbox.nextStep : agent.inbox.nextTurn;
|
|
1654
|
+
if (!items.some((message) => message.source.kind === 'a2a'))
|
|
1655
|
+
continue;
|
|
1656
|
+
agent.session.append('agent/inbox/spliced', {
|
|
1657
|
+
target,
|
|
1658
|
+
start: 0,
|
|
1659
|
+
removedCount: items.length,
|
|
1660
|
+
inserted: items.filter((message) => message.source.kind !== 'a2a'),
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
catch (error) {
|
|
1665
|
+
failures.push(error);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
detachBorrowed();
|
|
1171
1670
|
this.shutdownPromise = (async () => {
|
|
1172
|
-
const failures = [];
|
|
1173
1671
|
await Promise.allSettled([...this.resolving.values()]);
|
|
1174
|
-
|
|
1672
|
+
detachBorrowed();
|
|
1673
|
+
const runs = [...borrowed, ...this.contexts.values()];
|
|
1674
|
+
const cancellations = runs.flatMap((run) => [...run.tasks.values()].flatMap((task) => (task.cancellation ? [task.cancellation] : [])));
|
|
1175
1675
|
for (const outcome of await Promise.allSettled(cancellations))
|
|
1176
1676
|
if (outcome.status === 'rejected')
|
|
1177
1677
|
failures.push(outcome.reason);
|
|
1178
|
-
for (const run of
|
|
1678
|
+
for (const run of runs) {
|
|
1179
1679
|
for (const admission of run.admissions)
|
|
1180
1680
|
admission.abort();
|
|
1681
|
+
// An aborted caller may already have settled, but its durable commit can
|
|
1682
|
+
// still be in flight. Finish that work before agent/session disposal.
|
|
1683
|
+
for (const outcome of await Promise.allSettled([...run.commits]))
|
|
1684
|
+
if (outcome.status === 'rejected')
|
|
1685
|
+
failures.push(outcome.reason);
|
|
1181
1686
|
try {
|
|
1182
|
-
run.entry.
|
|
1687
|
+
if (!run.entry.borrowed)
|
|
1688
|
+
run.entry.handle.agent.cancel({ kind: 'disposed' }, { keepInbox: this.retainPending });
|
|
1183
1689
|
}
|
|
1184
1690
|
catch (error) {
|
|
1185
1691
|
failures.push(error);
|
|
1186
1692
|
}
|
|
1187
1693
|
try {
|
|
1188
|
-
|
|
1694
|
+
if (!run.entry.borrowed)
|
|
1695
|
+
await run.entry.handle.agent.whenIdle();
|
|
1189
1696
|
}
|
|
1190
1697
|
catch (error) {
|
|
1191
1698
|
failures.push(error);
|
|
@@ -1201,22 +1708,29 @@ export class NativeInbox {
|
|
|
1201
1708
|
failures.push(error);
|
|
1202
1709
|
}
|
|
1203
1710
|
finally {
|
|
1711
|
+
const targets = ['next-step', 'next-turn'];
|
|
1712
|
+
const committed = this.retainPending
|
|
1713
|
+
? new Set(run.entry.handle.agent.session
|
|
1714
|
+
.snapshotEvents()
|
|
1715
|
+
.flatMap((event) => (event.type === 'user/message' ? [event.data.id] : [])))
|
|
1716
|
+
: new Set();
|
|
1717
|
+
const retainedByTask = new Map();
|
|
1718
|
+
let position = 0;
|
|
1719
|
+
for (const target of targets) {
|
|
1720
|
+
for (const message of run.retained[target]) {
|
|
1721
|
+
const input = { message, target, position: position++ };
|
|
1722
|
+
if (message.source.kind !== 'a2a' || committed.has(message.id))
|
|
1723
|
+
continue;
|
|
1724
|
+
const group = retainedByTask.get(message.source.taskId) ?? [];
|
|
1725
|
+
group.push(input);
|
|
1726
|
+
retainedByTask.set(message.source.taskId, group);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1204
1729
|
for (const task of run.tasks.values()) {
|
|
1205
1730
|
try {
|
|
1206
1731
|
if (!task.settled) {
|
|
1207
1732
|
if (this.retainPending) {
|
|
1208
|
-
const
|
|
1209
|
-
.snapshotEvents()
|
|
1210
|
-
.flatMap((event) => (event.type === 'user/message' ? [event.data.id] : [])));
|
|
1211
|
-
let position = 0;
|
|
1212
|
-
const pending = ['next-step', 'next-turn'].flatMap((target) => run.retained[target].flatMap((message) => {
|
|
1213
|
-
const input = { message, target, position: position++ };
|
|
1214
|
-
return message.source.kind === 'a2a' &&
|
|
1215
|
-
message.source.taskId === task.task.id &&
|
|
1216
|
-
!committed.has(message.id)
|
|
1217
|
-
? [input]
|
|
1218
|
-
: [];
|
|
1219
|
-
}));
|
|
1733
|
+
const pending = retainedByTask.get(task.task.id) ?? [];
|
|
1220
1734
|
if (pending.length) {
|
|
1221
1735
|
task.task.metadata = {
|
|
1222
1736
|
...task.task.metadata,
|