@amaster.ai/dsh-a2a 0.1.26 → 0.1.28
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 +29 -25
- package/lib/bridge.js +1 -3
- package/lib/inbox.js +430 -282
- package/lib/index.js +6 -1
- package/lib/stores/filesystem.js +86 -0
- package/lib/types/bridge.d.ts +1 -1
- package/lib/types/inbox.d.ts +21 -45
- package/lib/types/index.d.ts +16 -3
- package/lib/types/stores/filesystem.d.ts +15 -0
- package/package.json +22 -17
package/lib/inbox.js
CHANGED
|
@@ -5,48 +5,6 @@ import { AgentEvent, ServerCallContext, STATE_HEADERS_KEY, } from '@a2a-js/sdk/s
|
|
|
5
5
|
import { createUserMessage } from '@deepseek-ai/dsh-llm/message';
|
|
6
6
|
import { agentTextMessage, SessionTranslator, terminalStatusUpdate } from './translator.js';
|
|
7
7
|
export const INBOX_EXTENSION = 'https://github.com/TGYD-helige/dsh-plugins/blob/main/docs/protocol/inbox-v1.md';
|
|
8
|
-
/** Fold native input mutations; a claimed input is recoverable only before user/message. */
|
|
9
|
-
export function recoverInbox(events) {
|
|
10
|
-
const pending = new Map();
|
|
11
|
-
const lists = { 'next-step': [], 'next-turn': [] };
|
|
12
|
-
let paused = false;
|
|
13
|
-
let claimed = new Set();
|
|
14
|
-
const committed = new Set();
|
|
15
|
-
for (const event of events) {
|
|
16
|
-
if (event.type === 'agent/inbox/paused')
|
|
17
|
-
paused = event.data.paused;
|
|
18
|
-
else if (event.type === 'turn/start') {
|
|
19
|
-
claimed = new Set();
|
|
20
|
-
}
|
|
21
|
-
else if (event.type === 'turn/end') {
|
|
22
|
-
if (!paused &&
|
|
23
|
-
(event.data.reason.kind === 'error' ||
|
|
24
|
-
event.data.reason.kind === 'blocked' ||
|
|
25
|
-
(event.data.reason.kind === 'aborted' && event.data.reason.reason.kind !== 'disposed')))
|
|
26
|
-
for (const id of claimed)
|
|
27
|
-
pending.delete(id);
|
|
28
|
-
}
|
|
29
|
-
else if (event.type === 'agent/inbox/spliced') {
|
|
30
|
-
const { target, start, removedCount = 0, inserted, outcome } = event.data;
|
|
31
|
-
const removed = lists[target].splice(start, removedCount, ...inserted);
|
|
32
|
-
for (const message of removed) {
|
|
33
|
-
if (outcome === 'canceled')
|
|
34
|
-
pending.delete(message.id);
|
|
35
|
-
else
|
|
36
|
-
claimed.add(message.id);
|
|
37
|
-
}
|
|
38
|
-
for (const message of inserted)
|
|
39
|
-
if (message.source.kind === 'a2a')
|
|
40
|
-
pending.set(message.id, message);
|
|
41
|
-
}
|
|
42
|
-
else if (event.type === 'user/message') {
|
|
43
|
-
pending.delete(event.data.id);
|
|
44
|
-
if (event.data.source.kind === 'a2a')
|
|
45
|
-
committed.add(event.data.source.taskId);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return { paused, pending: [...pending.values()], committed };
|
|
49
|
-
}
|
|
50
8
|
export class NativeInbox {
|
|
51
9
|
ctx;
|
|
52
10
|
bridge;
|
|
@@ -55,17 +13,65 @@ export class NativeInbox {
|
|
|
55
13
|
resolving = new Map();
|
|
56
14
|
stopping = false;
|
|
57
15
|
shutdownPromise;
|
|
58
|
-
waking = new Map();
|
|
59
16
|
preadmitted = new WeakMap();
|
|
60
17
|
server;
|
|
61
18
|
constructor(ctx, bridge, store) {
|
|
62
19
|
this.ctx = ctx;
|
|
63
20
|
this.bridge = bridge;
|
|
64
21
|
this.store = store;
|
|
65
|
-
if (ctx.agents.nativeInboxVersion !== 1)
|
|
66
|
-
throw new Error('Native inbox requires a DSH factory with nativeInboxVersion 1. Upgrade DSH before enabling inbox.enabled.');
|
|
67
22
|
bridge.inbox = this;
|
|
68
|
-
ctx.on('agent/
|
|
23
|
+
ctx.on('agent/pre-step', async (detail, next) => {
|
|
24
|
+
await this.prepare(detail);
|
|
25
|
+
return next();
|
|
26
|
+
});
|
|
27
|
+
ctx.on('goal/changed', ({ agent, change }) => {
|
|
28
|
+
const run = this.contexts.get(agent.session.id);
|
|
29
|
+
if (!run)
|
|
30
|
+
return;
|
|
31
|
+
const previous = run.goalOwner;
|
|
32
|
+
if (change.goal?.phase === 'active' && change.goal.activation === 'armed') {
|
|
33
|
+
const task = previous?.goalId === change.ref.id ? previous.task : run.active;
|
|
34
|
+
if (task && !task.settled)
|
|
35
|
+
run.goalOwner = { goalId: change.ref.id, task };
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
run.goalOwner = undefined;
|
|
39
|
+
if (previous)
|
|
40
|
+
this.settle(run, previous.task);
|
|
41
|
+
});
|
|
42
|
+
ctx.on('agent/status', ({ agent, status }) => {
|
|
43
|
+
const run = this.contexts.get(agent.session.id);
|
|
44
|
+
if (status !== 'idle' || !run?.goalOwner)
|
|
45
|
+
return;
|
|
46
|
+
const { task } = run.goalOwner;
|
|
47
|
+
try {
|
|
48
|
+
const goal = ctx.get('goals')?.get(agent);
|
|
49
|
+
if (goal?.phase === 'active' && goal.activation === 'armed')
|
|
50
|
+
return;
|
|
51
|
+
run.goalOwner = undefined;
|
|
52
|
+
this.settle(run, task);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
run.goalOwner = undefined;
|
|
56
|
+
this.fail(task, agent.session.id, error);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
ctx.on('agent/inbox/claimed', ({ agent, message, turn }) => {
|
|
60
|
+
const run = this.contexts.get(agent.session.id);
|
|
61
|
+
if (!run)
|
|
62
|
+
return;
|
|
63
|
+
const task = this.owner(run, message);
|
|
64
|
+
if (task && !task.settled) {
|
|
65
|
+
run.claimed.add(task);
|
|
66
|
+
if (!run.active)
|
|
67
|
+
this.select(run, task);
|
|
68
|
+
if (run.active === task) {
|
|
69
|
+
const selectedAt = Date.now();
|
|
70
|
+
task.selectedAt.set(message.id, selectedAt);
|
|
71
|
+
this.observeInput('selected', run, message, { selectedAt, turn });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
69
75
|
}
|
|
70
76
|
content(message, contextId) {
|
|
71
77
|
return this.bridge.buildContent(message, contextId);
|
|
@@ -85,9 +91,7 @@ export class NativeInbox {
|
|
|
85
91
|
const admitted = { ...message, taskId, contextId: run.entry.sessionId };
|
|
86
92
|
task.pendingRequests++;
|
|
87
93
|
try {
|
|
88
|
-
await this.admit(run, task, admitted, task.context, options.target ?? 'next-turn');
|
|
89
|
-
if (options.wakeup !== false && !run.paused)
|
|
90
|
-
await this.wake(run.entry.sessionId, { context: task.context });
|
|
94
|
+
await this.admit(run, task, admitted, task.context, options.target ?? 'next-turn', options.wakeup !== false);
|
|
91
95
|
}
|
|
92
96
|
finally {
|
|
93
97
|
task.pendingRequests--;
|
|
@@ -97,7 +101,182 @@ export class NativeInbox {
|
|
|
97
101
|
}
|
|
98
102
|
throw new Error('Host input must address a live A2A task.');
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
// A non-canceled removal keeps an interrupted move distinguishable from explicit cancellation.
|
|
105
|
+
moveInput(agent, message, target) {
|
|
106
|
+
const from = target === 'next-step' ? 'next-turn' : 'next-step';
|
|
107
|
+
const items = from === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep;
|
|
108
|
+
const index = items.findIndex((item) => item.id === message.id);
|
|
109
|
+
if (index < 0)
|
|
110
|
+
throw new Error('Queued input has already been claimed.');
|
|
111
|
+
agent.session.append('agent/inbox/spliced', {
|
|
112
|
+
target: from,
|
|
113
|
+
start: index,
|
|
114
|
+
removedCount: 1,
|
|
115
|
+
inserted: [],
|
|
116
|
+
});
|
|
117
|
+
agent.inbox.append(target, message);
|
|
118
|
+
}
|
|
119
|
+
async promote(contextId, messageId, taskId) {
|
|
120
|
+
const run = await this.context(contextId);
|
|
121
|
+
const operation = run.admissionTail.then(() => this.promoteInput(run, messageId, taskId));
|
|
122
|
+
run.admissionTail = operation.catch(() => { });
|
|
123
|
+
run.promotions.set(messageId, operation);
|
|
124
|
+
const clear = () => {
|
|
125
|
+
if (run.promotions.get(messageId) === operation)
|
|
126
|
+
run.promotions.delete(messageId);
|
|
127
|
+
};
|
|
128
|
+
void operation.then(clear, clear);
|
|
129
|
+
return operation;
|
|
130
|
+
}
|
|
131
|
+
async promoteInput(run, messageId, taskId) {
|
|
132
|
+
const agent = run.entry.handle.agent;
|
|
133
|
+
const target = run.tasks.get(taskId);
|
|
134
|
+
const message = agent.inbox.nextTurn.find((item) => item.id === messageId);
|
|
135
|
+
if (message?.source.kind !== 'a2a')
|
|
136
|
+
throw new Error('Queued input has already been claimed.');
|
|
137
|
+
const source = run.tasks.get(message.source.taskId);
|
|
138
|
+
if (!target || !source)
|
|
139
|
+
throw new Error('Promotion requires live source and target tasks.');
|
|
140
|
+
const valid = () => !this.stopping &&
|
|
141
|
+
!this.bridge.isClearing(run.entry.sessionId) &&
|
|
142
|
+
!target.settled &&
|
|
143
|
+
!target.controller.signal.aborted &&
|
|
144
|
+
run.active === target &&
|
|
145
|
+
source !== target &&
|
|
146
|
+
!source.settled &&
|
|
147
|
+
!source.controller.signal.aborted &&
|
|
148
|
+
source.task.history.length === 1 &&
|
|
149
|
+
!source.pendingRequests &&
|
|
150
|
+
!run.claimed.has(source) &&
|
|
151
|
+
agent.inbox.nextTurn.some((item) => item === message);
|
|
152
|
+
if (!valid())
|
|
153
|
+
throw new Error('Promotion requires an unclaimed independent input and an active target.');
|
|
154
|
+
const original = Message.fromJSON(JSON.parse(message.source.message));
|
|
155
|
+
original.taskId = taskId;
|
|
156
|
+
original.referenceTaskIds = [...new Set([...original.referenceTaskIds, source.task.id])];
|
|
157
|
+
const promotionId = randomUUID();
|
|
158
|
+
const promoted = {
|
|
159
|
+
...message,
|
|
160
|
+
source: {
|
|
161
|
+
...message.source,
|
|
162
|
+
taskId,
|
|
163
|
+
target: 'next-step',
|
|
164
|
+
promotedFrom: source.task.id,
|
|
165
|
+
promotionId,
|
|
166
|
+
message: JSON.stringify(Message.toJSON(original)),
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
target.pendingRequests++;
|
|
170
|
+
try {
|
|
171
|
+
await this.checkpoint(target.task, target.context);
|
|
172
|
+
if (!valid())
|
|
173
|
+
throw new Error('Promotion raced input consumption or task settlement.');
|
|
174
|
+
target.task.history = [...target.task.history, original];
|
|
175
|
+
target.headers.set(message.id, source.headers.get(message.id));
|
|
176
|
+
if (!agent.inbox.replace(message.id, promoted))
|
|
177
|
+
throw new Error('Queued input has already been claimed.');
|
|
178
|
+
this.moveInput(agent, promoted, 'next-step');
|
|
179
|
+
try {
|
|
180
|
+
await this.flush(agent.session);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
// Until the session checkpoint succeeds, the independent task still owns this input.
|
|
184
|
+
const rollback = {
|
|
185
|
+
...message,
|
|
186
|
+
source: { ...message.source, promotedFrom: source.task.id },
|
|
187
|
+
};
|
|
188
|
+
run.rolledBack.add(promotionId);
|
|
189
|
+
const live = this.contexts.get(run.entry.sessionId) === run &&
|
|
190
|
+
!this.stopping &&
|
|
191
|
+
!this.bridge.isClearing(run.entry.sessionId);
|
|
192
|
+
if (live && !source.settled && !source.controller.signal.aborted) {
|
|
193
|
+
if (this.pending(run).some((item) => item.id === message.id)) {
|
|
194
|
+
agent.inbox.replace(message.id, rollback);
|
|
195
|
+
this.moveInput(agent, rollback, 'next-turn');
|
|
196
|
+
}
|
|
197
|
+
else
|
|
198
|
+
agent.inbox.append('next-turn', rollback);
|
|
199
|
+
}
|
|
200
|
+
else if (live)
|
|
201
|
+
agent.inbox.remove(message.id);
|
|
202
|
+
target.task.history = target.task.history.filter((item) => item.messageId !== original.messageId);
|
|
203
|
+
target.headers.delete(message.id);
|
|
204
|
+
try {
|
|
205
|
+
if (live)
|
|
206
|
+
await this.flush(agent.session);
|
|
207
|
+
}
|
|
208
|
+
catch (rollbackError) {
|
|
209
|
+
console.error('[dsh-a2a] promotion rollback checkpoint failed:', rollbackError);
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
if (this.stopping ||
|
|
214
|
+
this.contexts.get(run.entry.sessionId) !== run ||
|
|
215
|
+
this.bridge.isClearing(run.entry.sessionId))
|
|
216
|
+
throw new Error('Promotion was interrupted by context disposal.');
|
|
217
|
+
source.controller.abort();
|
|
218
|
+
try {
|
|
219
|
+
await this.checkpoint(target.task, target.context);
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
await this.cancel(source.task.id);
|
|
223
|
+
}
|
|
224
|
+
this.observeInput('promoted', run, promoted);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
target.pendingRequests--;
|
|
228
|
+
this.settle(run, target);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Repair only an incomplete ownership move, never an ordinary DSH claim. */
|
|
232
|
+
async recoverPromotions(run, context) {
|
|
233
|
+
const agent = run.entry.handle.agent;
|
|
234
|
+
const transfers = new Map();
|
|
235
|
+
for (const event of agent.session.snapshotEvents()) {
|
|
236
|
+
if (event.type !== 'agent/inbox/spliced')
|
|
237
|
+
continue;
|
|
238
|
+
for (const message of event.data.inserted) {
|
|
239
|
+
if (message.source.kind !== 'a2a' || !message.source.promotedFrom)
|
|
240
|
+
continue;
|
|
241
|
+
transfers.set(message.id, {
|
|
242
|
+
message: message,
|
|
243
|
+
placed: event.data.target === message.source.target,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const { message, placed } of transfers.values()) {
|
|
248
|
+
if (!placed) {
|
|
249
|
+
const queued = this.pending(run).find((item) => item.id === message.id);
|
|
250
|
+
if (queued)
|
|
251
|
+
this.moveInput(agent, message, message.source.target);
|
|
252
|
+
else
|
|
253
|
+
agent.inbox.append(message.source.target, message);
|
|
254
|
+
}
|
|
255
|
+
if (this.pending(run).some((item) => item.id === message.id)) {
|
|
256
|
+
const recipient = await this.store.load(message.source.taskId, context);
|
|
257
|
+
if (!recipient || recipient.contextId !== agent.session.id)
|
|
258
|
+
throw new Error('Promoted A2A task is not accessible.');
|
|
259
|
+
const original = Message.fromJSON(JSON.parse(message.source.message));
|
|
260
|
+
if (!recipient.history.some((item) => item.messageId === original.messageId)) {
|
|
261
|
+
recipient.history.push(original);
|
|
262
|
+
await this.checkpoint(recipient, context);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (message.source.promotedFrom === message.source.taskId)
|
|
266
|
+
continue;
|
|
267
|
+
const former = await this.store.load(message.source.promotedFrom, context);
|
|
268
|
+
if (!former || this.terminal(former))
|
|
269
|
+
continue;
|
|
270
|
+
former.status = {
|
|
271
|
+
state: TaskState.TASK_STATE_CANCELED,
|
|
272
|
+
message: undefined,
|
|
273
|
+
timestamp: new Date().toISOString(),
|
|
274
|
+
};
|
|
275
|
+
await this.flush(agent.session);
|
|
276
|
+
await this.checkpoint(former, context);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
context(contextId, context = new ServerCallContext()) {
|
|
101
280
|
if (this.stopping)
|
|
102
281
|
return Promise.reject(new Error('Native inbox is unloading.'));
|
|
103
282
|
const existing = this.contexts.get(contextId);
|
|
@@ -107,25 +286,32 @@ export class NativeInbox {
|
|
|
107
286
|
if (!creating) {
|
|
108
287
|
creating = (async () => {
|
|
109
288
|
const { entry } = await this.bridge.ensureTask(randomUUID(), contextId);
|
|
110
|
-
const agent = entry.handle.agent;
|
|
111
|
-
if (typeof agent.wake !== 'function' ||
|
|
112
|
-
typeof agent.inbox.setPaused !== 'function')
|
|
113
|
-
throw new Error('Native inbox requires DSH Agent.wake(), agent/before-assembly, and inbox-preserving disposal. See the native inbox compatibility notes.');
|
|
114
|
-
const recovered = recoverInbox(agent.session.snapshotEvents());
|
|
115
289
|
const run = {
|
|
116
290
|
entry,
|
|
117
|
-
paused: recovered.paused,
|
|
118
291
|
tasks: new Map(),
|
|
292
|
+
claimed: new Set(),
|
|
119
293
|
admissions: new Set(),
|
|
120
294
|
admissionTail: Promise.resolve(),
|
|
295
|
+
promotions: new Map(),
|
|
296
|
+
rolledBack: new Set(),
|
|
121
297
|
};
|
|
122
|
-
this.
|
|
123
|
-
const
|
|
124
|
-
for (const message of
|
|
125
|
-
if (
|
|
126
|
-
|
|
298
|
+
await this.recoverPromotions(run, context);
|
|
299
|
+
const snapshots = new Map();
|
|
300
|
+
for (const message of this.pending(run)) {
|
|
301
|
+
if (message.source.kind !== 'a2a' || snapshots.has(message.source.taskId))
|
|
302
|
+
continue;
|
|
303
|
+
const snapshot = await this.store.load(message.source.taskId, context);
|
|
304
|
+
if (!snapshot || snapshot.contextId !== contextId)
|
|
305
|
+
throw new Error('Queued A2A task is not accessible.');
|
|
306
|
+
if (this.terminal(snapshot))
|
|
307
|
+
entry.handle.agent.inbox.remove(message.id);
|
|
308
|
+
else
|
|
309
|
+
snapshots.set(snapshot.id, snapshot);
|
|
127
310
|
}
|
|
128
|
-
await this.flush(agent.session);
|
|
311
|
+
await this.flush(entry.handle.agent.session);
|
|
312
|
+
for (const snapshot of snapshots.values())
|
|
313
|
+
this.restoreTask(run, snapshot, context);
|
|
314
|
+
this.contexts.set(contextId, run);
|
|
129
315
|
this.observe('resolved', run);
|
|
130
316
|
for (const message of this.pending(run))
|
|
131
317
|
this.observeInput('restored', run, message);
|
|
@@ -144,7 +330,6 @@ export class NativeInbox {
|
|
|
144
330
|
contextId: run.entry.sessionId,
|
|
145
331
|
at: Date.now(),
|
|
146
332
|
pending: pending.length,
|
|
147
|
-
paused: run.paused,
|
|
148
333
|
agentStatus: run.entry.handle.agent.status,
|
|
149
334
|
taskIds: [
|
|
150
335
|
...new Set([...run.tasks.values()]
|
|
@@ -162,21 +347,22 @@ export class NativeInbox {
|
|
|
162
347
|
}
|
|
163
348
|
observeInput(phase, run, message, fields = {}) {
|
|
164
349
|
try {
|
|
165
|
-
|
|
350
|
+
const task = this.owner(run, message);
|
|
351
|
+
if (!task)
|
|
166
352
|
return;
|
|
167
|
-
const original =
|
|
353
|
+
const original = this.protocolMessage(run, task, message);
|
|
168
354
|
const position = this.pending(run).findIndex((input) => input.id === message.id);
|
|
169
355
|
this.observe(phase, run, {
|
|
170
|
-
target: message.source.target,
|
|
356
|
+
target: message.source.kind === 'a2a' ? message.source.target : 'next-turn',
|
|
171
357
|
input: original.parts
|
|
172
358
|
.flatMap((part) => (part.content?.$case === 'text' ? [part.content.value] : []))
|
|
173
359
|
.join('\n'),
|
|
174
360
|
position: position < 0 ? undefined : position + 1,
|
|
175
|
-
taskId:
|
|
361
|
+
taskId: task.task.id,
|
|
176
362
|
messageId: original.messageId,
|
|
177
363
|
referenceTaskIds: original.referenceTaskIds,
|
|
178
364
|
nativeMessageId: message.id,
|
|
179
|
-
queuedAt: message.source.queuedAt,
|
|
365
|
+
queuedAt: message.source.kind === 'a2a' ? message.source.queuedAt : undefined,
|
|
180
366
|
...fields,
|
|
181
367
|
});
|
|
182
368
|
}
|
|
@@ -198,6 +384,7 @@ export class NativeInbox {
|
|
|
198
384
|
task(task, bus, context) {
|
|
199
385
|
let complete;
|
|
200
386
|
let finished = false;
|
|
387
|
+
const selectedAt = new Map();
|
|
201
388
|
const done = new Promise((resolve) => {
|
|
202
389
|
complete = resolve;
|
|
203
390
|
});
|
|
@@ -209,6 +396,7 @@ export class NativeInbox {
|
|
|
209
396
|
const run = this.contexts.get(task.contextId);
|
|
210
397
|
if (phase && run)
|
|
211
398
|
this.observe(phase, run, { taskId: task.id });
|
|
399
|
+
selectedAt.clear();
|
|
212
400
|
complete();
|
|
213
401
|
};
|
|
214
402
|
return {
|
|
@@ -219,6 +407,7 @@ export class NativeInbox {
|
|
|
219
407
|
finish,
|
|
220
408
|
settled: false,
|
|
221
409
|
headers: new Map(),
|
|
410
|
+
selectedAt,
|
|
222
411
|
controller: new AbortController(),
|
|
223
412
|
pendingRequests: 0,
|
|
224
413
|
context: new ServerCallContext({ tenant: context.tenant, user: context.user }),
|
|
@@ -260,13 +449,13 @@ export class NativeInbox {
|
|
|
260
449
|
(message.contextId && snapshot.contextId !== message.contextId) ||
|
|
261
450
|
this.terminal(snapshot))
|
|
262
451
|
return;
|
|
263
|
-
const run = await this.context(snapshot.contextId);
|
|
452
|
+
const run = await this.context(snapshot.contextId, context);
|
|
264
453
|
let task = run.tasks.get(snapshot.id);
|
|
265
454
|
if (!task)
|
|
266
455
|
task = this.restoreTask(run, snapshot, context);
|
|
267
456
|
task.pendingRequests++;
|
|
268
457
|
try {
|
|
269
|
-
await this.admit(run, task, message, context, controls.target);
|
|
458
|
+
await this.admit(run, task, message, context, controls.target, controls.wakeup);
|
|
270
459
|
}
|
|
271
460
|
catch (error) {
|
|
272
461
|
task.pendingRequests--;
|
|
@@ -291,9 +480,12 @@ export class NativeInbox {
|
|
|
291
480
|
const snapshot = await this.store.load(taskId, context);
|
|
292
481
|
if (!snapshot || this.terminal(snapshot))
|
|
293
482
|
return;
|
|
294
|
-
const run = await this.context(snapshot.contextId);
|
|
295
|
-
|
|
296
|
-
|
|
483
|
+
const run = await this.context(snapshot.contextId, context);
|
|
484
|
+
const task = run.tasks.get(taskId) ?? this.restoreTask(run, snapshot, context);
|
|
485
|
+
if (task.settled) {
|
|
486
|
+
await task.done;
|
|
487
|
+
await this.checkpoint(structuredClone(task.task), task.context);
|
|
488
|
+
}
|
|
297
489
|
}
|
|
298
490
|
outcome(task) {
|
|
299
491
|
if (task.status?.state === TaskState.TASK_STATE_COMPLETED)
|
|
@@ -313,7 +505,7 @@ export class NativeInbox {
|
|
|
313
505
|
TaskState.TASK_STATE_REJECTED,
|
|
314
506
|
].includes(task.status.state));
|
|
315
507
|
}
|
|
316
|
-
async admit(run, task, a2aMessage, context, target) {
|
|
508
|
+
async admit(run, task, a2aMessage, context, target, wakeup, accepted) {
|
|
317
509
|
if (target === 'next-step' && run.active !== task)
|
|
318
510
|
throw new Error('Steering must address an active task.');
|
|
319
511
|
const duplicate = () => [...run.tasks.values()].some((owned) => owned.task.history.some((message) => message.messageId === a2aMessage.messageId));
|
|
@@ -364,9 +556,10 @@ export class NativeInbox {
|
|
|
364
556
|
abort.signal.throwIfAborted();
|
|
365
557
|
task.task.history = acceptedHistory;
|
|
366
558
|
task.headers.set(message.id, headers);
|
|
367
|
-
|
|
368
|
-
|
|
559
|
+
accepted?.();
|
|
560
|
+
run.entry.handle.agent.send(message, target, wakeup);
|
|
369
561
|
this.observeInput('admitted', run, message);
|
|
562
|
+
await this.flush(run.entry.handle.agent.session);
|
|
370
563
|
void this.ctx
|
|
371
564
|
.parallel('a2a/message-admitted', {
|
|
372
565
|
contextId: run.entry.sessionId,
|
|
@@ -403,7 +596,7 @@ export class NativeInbox {
|
|
|
403
596
|
let ownsTask = false;
|
|
404
597
|
try {
|
|
405
598
|
const controls = this.controls(userMessage, context);
|
|
406
|
-
const run = await this.context(contextId);
|
|
599
|
+
const run = await this.context(contextId, context);
|
|
407
600
|
task = run.tasks.get(taskId);
|
|
408
601
|
if (!task) {
|
|
409
602
|
task = this.task({
|
|
@@ -423,13 +616,12 @@ export class NativeInbox {
|
|
|
423
616
|
}
|
|
424
617
|
const preadmitted = this.preadmitted.get(context)?.delete(userMessage.messageId);
|
|
425
618
|
if (!preadmitted)
|
|
426
|
-
await this.admit(run, task, userMessage, context, controls.target);
|
|
427
|
-
else
|
|
619
|
+
await this.admit(run, task, userMessage, context, controls.target, controls.wakeup, () => bus.publish(AgentEvent.task(structuredClone(task.task))));
|
|
620
|
+
else {
|
|
428
621
|
task.pendingRequests--;
|
|
429
|
-
|
|
622
|
+
bus.publish(AgentEvent.task(structuredClone(task.task)));
|
|
623
|
+
}
|
|
430
624
|
this.settle(run, task);
|
|
431
|
-
if (controls.wakeup && !run.paused && !task.settled)
|
|
432
|
-
await this.wake(contextId, { context });
|
|
433
625
|
await task.done;
|
|
434
626
|
if (task.bus !== bus && task.final)
|
|
435
627
|
bus.publish(task.final);
|
|
@@ -455,13 +647,12 @@ export class NativeInbox {
|
|
|
455
647
|
metadata: undefined,
|
|
456
648
|
};
|
|
457
649
|
bus.publish(AgentEvent.task(snapshot));
|
|
458
|
-
bus.publish(terminalStatusUpdate(taskId, contextId, 'failed', text));
|
|
459
650
|
if (task) {
|
|
460
|
-
|
|
461
|
-
task.
|
|
462
|
-
task.controller.abort();
|
|
463
|
-
task.finish();
|
|
651
|
+
this.fail(task, contextId, error);
|
|
652
|
+
await task.done;
|
|
464
653
|
}
|
|
654
|
+
else
|
|
655
|
+
bus.publish(terminalStatusUpdate(taskId, contextId, 'failed', text));
|
|
465
656
|
}
|
|
466
657
|
finally {
|
|
467
658
|
bus.finished();
|
|
@@ -475,18 +666,80 @@ export class NativeInbox {
|
|
|
475
666
|
run.tasks.set(snapshot.id, restored);
|
|
476
667
|
void restored.done.finally(() => bus.finished());
|
|
477
668
|
bus.publish(AgentEvent.task(snapshot));
|
|
669
|
+
if (snapshot.status?.state !== TaskState.TASK_STATE_INPUT_REQUIRED &&
|
|
670
|
+
!this.pending(run).some((message) => message.source.kind === 'a2a' && message.source.taskId === snapshot.id))
|
|
671
|
+
this.fail(restored, snapshot.contextId, new Error('Task execution was interrupted; DSH has no pending input to resume.'));
|
|
478
672
|
return restored;
|
|
479
673
|
}
|
|
480
|
-
|
|
481
|
-
run.
|
|
674
|
+
select(run, task) {
|
|
675
|
+
run.active = task;
|
|
676
|
+
this.bridge.bindInboxTask(run.entry, task.task.id, task.translator, task.bus, task.done, task.controller);
|
|
677
|
+
if (run.turnStart) {
|
|
678
|
+
this.publish(task, run.turnStart);
|
|
679
|
+
run.turnStart = undefined;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
protocolMessage(run, task, message) {
|
|
683
|
+
if (message.source.kind === 'a2a')
|
|
684
|
+
return Message.fromJSON(JSON.parse(message.source.message));
|
|
685
|
+
return Message.fromJSON({
|
|
686
|
+
messageId: message.id,
|
|
687
|
+
taskId: task.task.id,
|
|
688
|
+
contextId: run.entry.sessionId,
|
|
689
|
+
role: 'ROLE_USER',
|
|
690
|
+
parts: message.content
|
|
691
|
+
.filter((block) => block.type === 'text')
|
|
692
|
+
.map((block) => ({ text: block.text })),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
owner(run, message) {
|
|
696
|
+
if (message.source.kind === 'a2a')
|
|
697
|
+
return run.tasks.get(message.source.taskId);
|
|
698
|
+
if (message.source.kind !== 'goal' || message.source.round <= 0)
|
|
699
|
+
return;
|
|
700
|
+
const goal = this.ctx.get('goals')?.get(run.entry.handle.agent);
|
|
701
|
+
if (run.goalOwner &&
|
|
702
|
+
goal &&
|
|
703
|
+
run.goalOwner.goalId === message.source.goalId &&
|
|
704
|
+
goal.id === message.source.goalId &&
|
|
705
|
+
goal.revision === message.source.revision &&
|
|
706
|
+
goal.phase === 'active' &&
|
|
707
|
+
goal.activation === 'armed')
|
|
708
|
+
return run.goalOwner.task;
|
|
482
709
|
}
|
|
483
710
|
async prepare(detail) {
|
|
484
711
|
const run = this.contexts.get(detail.agent.session.id);
|
|
485
712
|
if (!run)
|
|
486
713
|
return;
|
|
487
|
-
if (this.stopping
|
|
488
|
-
throw new Error('Inbox is
|
|
489
|
-
|
|
714
|
+
if (this.stopping)
|
|
715
|
+
throw new Error('Inbox is unloading.');
|
|
716
|
+
for (let index = detail.messages.length - 1; index >= 0; index--) {
|
|
717
|
+
const message = detail.messages[index];
|
|
718
|
+
if (message.source.kind !== 'a2a' || !message.source.promotionId)
|
|
719
|
+
continue;
|
|
720
|
+
const promotion = run.promotions.get(message.id);
|
|
721
|
+
if (promotion) {
|
|
722
|
+
let onAbort;
|
|
723
|
+
try {
|
|
724
|
+
await Promise.race([
|
|
725
|
+
promotion.catch(() => { }),
|
|
726
|
+
new Promise((_resolve, reject) => {
|
|
727
|
+
onAbort = () => reject(detail.signal.reason);
|
|
728
|
+
detail.signal.addEventListener('abort', onAbort, { once: true });
|
|
729
|
+
if (detail.signal.aborted)
|
|
730
|
+
onAbort();
|
|
731
|
+
}),
|
|
732
|
+
]);
|
|
733
|
+
detail.signal.throwIfAborted();
|
|
734
|
+
}
|
|
735
|
+
finally {
|
|
736
|
+
detail.signal.removeEventListener('abort', onAbort);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (run.rolledBack.delete(message.source.promotionId))
|
|
740
|
+
detail.messages.splice(index, 1);
|
|
741
|
+
}
|
|
742
|
+
if (!run.active && detail.messages.some((message) => !this.owner(run, message)))
|
|
490
743
|
throw new Error('Native host input requires task-attributed a2aInbox.enqueue().');
|
|
491
744
|
// Restarted steering and an ordinary queued turn may be claimed together by the core.
|
|
492
745
|
// Requeue other owners before committing any user message, keeping task lifecycles separate.
|
|
@@ -494,36 +747,36 @@ export class NativeInbox {
|
|
|
494
747
|
if (owner?.source.kind === 'a2a') {
|
|
495
748
|
const taskId = owner.source.taskId;
|
|
496
749
|
const deferred = detail.messages.filter((message) => message.source.kind === 'a2a' && message.source.taskId !== taskId);
|
|
497
|
-
for (const message of deferred.slice().reverse())
|
|
750
|
+
for (const message of deferred.slice().reverse()) {
|
|
498
751
|
detail.agent.inbox.prepend('next-turn', message);
|
|
752
|
+
if (message.source.kind === 'a2a') {
|
|
753
|
+
const task = run.tasks.get(message.source.taskId);
|
|
754
|
+
if (task)
|
|
755
|
+
run.claimed.delete(task);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
499
758
|
for (let index = detail.messages.length - 1; index >= 0; index--)
|
|
500
759
|
if (deferred.includes(detail.messages[index]))
|
|
501
760
|
detail.messages.splice(index, 1);
|
|
502
761
|
}
|
|
503
762
|
for (const message of detail.messages) {
|
|
504
|
-
|
|
763
|
+
const task = this.owner(run, message);
|
|
764
|
+
if (!task && message.source.kind !== 'a2a')
|
|
505
765
|
continue;
|
|
506
|
-
const task = run.tasks.get(message.source.taskId);
|
|
507
766
|
if (!task || task.settled)
|
|
508
767
|
throw new Error('Queued input has no live A2A task; restore it before waking.');
|
|
509
768
|
if (run.active && run.active !== task)
|
|
510
769
|
throw new Error('A model step cannot consume inputs from different A2A tasks.');
|
|
511
|
-
run
|
|
512
|
-
|
|
513
|
-
if (run.turnStart) {
|
|
514
|
-
this.publish(task, run.turnStart);
|
|
515
|
-
run.turnStart = undefined;
|
|
516
|
-
}
|
|
517
|
-
const selectedAt = Date.now();
|
|
770
|
+
this.select(run, task);
|
|
771
|
+
const selectedAt = task.selectedAt.get(message.id) ?? Date.now();
|
|
518
772
|
const timing = { selectedAt, turn: detail.turn, step: detail.step };
|
|
519
|
-
this.observeInput('selected', run, message, timing);
|
|
520
773
|
try {
|
|
521
774
|
await this.flush(detail.agent.session);
|
|
522
|
-
await this.ctx.serial('a2a/before-
|
|
775
|
+
await this.ctx.serial('a2a/before-step', {
|
|
523
776
|
agent: detail.agent,
|
|
524
777
|
contextId: detail.agent.session.id,
|
|
525
778
|
taskId: task.task.id,
|
|
526
|
-
a2aMessage:
|
|
779
|
+
a2aMessage: this.protocolMessage(run, task, message),
|
|
527
780
|
dshMessageId: message.id,
|
|
528
781
|
requestHeaders: task.headers.get(message.id),
|
|
529
782
|
signal: detail.signal,
|
|
@@ -548,15 +801,19 @@ export class NativeInbox {
|
|
|
548
801
|
const run = this.contexts.get(session.id);
|
|
549
802
|
if (!run)
|
|
550
803
|
return false;
|
|
551
|
-
if (event.type === 'agent/inbox/paused') {
|
|
552
|
-
run.paused = event.data.paused;
|
|
553
|
-
return true;
|
|
554
|
-
}
|
|
555
804
|
if (event.type === 'turn/start') {
|
|
805
|
+
run.claimed.clear();
|
|
556
806
|
run.turnStart = event;
|
|
557
807
|
return true;
|
|
558
808
|
}
|
|
559
809
|
const task = run.active;
|
|
810
|
+
if (event.type === 'turn/end') {
|
|
811
|
+
if (!this.stopping && ['error', 'blocked', 'aborted'].includes(event.data.reason.kind))
|
|
812
|
+
for (const owner of run.claimed)
|
|
813
|
+
if (owner !== task && !owner.settled)
|
|
814
|
+
this.fail(owner, session.id, new Error('Selected input was interrupted before its task step.'));
|
|
815
|
+
run.claimed.clear();
|
|
816
|
+
}
|
|
560
817
|
if (!task)
|
|
561
818
|
return true;
|
|
562
819
|
if (task.settled) {
|
|
@@ -573,19 +830,14 @@ export class NativeInbox {
|
|
|
573
830
|
if (this.stopping)
|
|
574
831
|
return true;
|
|
575
832
|
const outputs = task.translator.handle(event);
|
|
576
|
-
if (run.paused) {
|
|
577
|
-
void this.cancel(task.task.id, { retainInbox: true }).catch((error) => console.error('[dsh-a2a] pause cancellation failed:', error));
|
|
578
|
-
return true;
|
|
579
|
-
}
|
|
580
833
|
task.final = outputs.at(-1);
|
|
581
|
-
if (
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
.catch((error) => console.error('[dsh-a2a] queue continuation failed:', error));
|
|
834
|
+
if (task.final?.kind === 'statusUpdate' &&
|
|
835
|
+
[TaskState.TASK_STATE_FAILED, TaskState.TASK_STATE_CANCELED].includes(task.final.data.status.state)) {
|
|
836
|
+
// Official inbox edits append session events; wait until turn/end publication exits.
|
|
837
|
+
void Promise.resolve()
|
|
838
|
+
.then(() => this.settle(run, task))
|
|
839
|
+
.catch((error) => this.fail(task, session.id, error));
|
|
840
|
+
return true;
|
|
589
841
|
}
|
|
590
842
|
if (!task.settlementStarted) {
|
|
591
843
|
task.settlementStarted = true;
|
|
@@ -611,11 +863,16 @@ export class NativeInbox {
|
|
|
611
863
|
return true;
|
|
612
864
|
}
|
|
613
865
|
settle(run, task) {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
866
|
+
const terminal = task.final?.kind === 'statusUpdate' &&
|
|
867
|
+
[TaskState.TASK_STATE_FAILED, TaskState.TASK_STATE_CANCELED].includes(task.final.data.status.state);
|
|
868
|
+
if (task.settled || this.stopping)
|
|
869
|
+
return;
|
|
870
|
+
if (terminal) {
|
|
871
|
+
this.discard(run, task.task.id);
|
|
872
|
+
}
|
|
873
|
+
else if (!task.settlementReady ||
|
|
618
874
|
task.pendingRequests ||
|
|
875
|
+
run.goalOwner?.task === task ||
|
|
619
876
|
run.active === task ||
|
|
620
877
|
this.pending(run).some((message) => message.source.kind === 'a2a' && message.source.taskId === task.task.id))
|
|
621
878
|
return;
|
|
@@ -650,146 +907,44 @@ export class NativeInbox {
|
|
|
650
907
|
}
|
|
651
908
|
fail(task, contextId, error) {
|
|
652
909
|
console.error('[dsh-a2a] native inbox failed:', error);
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
if (task.final.kind === 'statusUpdate')
|
|
656
|
-
task.task.status = task.final.data.status;
|
|
657
|
-
task.bus.publish(task.final);
|
|
658
|
-
}
|
|
910
|
+
if (task.settled)
|
|
911
|
+
return;
|
|
659
912
|
task.settled = true;
|
|
660
913
|
task.controller.abort();
|
|
661
|
-
task.
|
|
662
|
-
task.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
for (const task of run.tasks.values())
|
|
673
|
-
if (!task.settled && task.task.status?.state === TaskState.TASK_STATE_WORKING)
|
|
674
|
-
await this.cancel(task.task.id, { retainInbox: true });
|
|
675
|
-
run.entry.handle.agent.cancel({ kind: 'user' }, { keepInbox: true });
|
|
676
|
-
await run.entry.handle.agent.whenIdle();
|
|
677
|
-
await this.flush(run.entry.handle.agent.session);
|
|
678
|
-
}
|
|
679
|
-
async wake(contextId, options = {}) {
|
|
680
|
-
const operation = (this.waking.get(contextId) ?? Promise.resolve())
|
|
681
|
-
.catch(() => { })
|
|
682
|
-
.then(() => this.wakeInner(contextId, options));
|
|
683
|
-
this.waking.set(contextId, operation);
|
|
684
|
-
const release = () => {
|
|
685
|
-
if (this.waking.get(contextId) === operation)
|
|
686
|
-
this.waking.delete(contextId);
|
|
687
|
-
};
|
|
688
|
-
void operation.then(release, release);
|
|
689
|
-
return operation;
|
|
690
|
-
}
|
|
691
|
-
async wakeInner(contextId, options) {
|
|
692
|
-
const run = await this.context(contextId);
|
|
693
|
-
if (run.paused && !options.resume)
|
|
694
|
-
return [];
|
|
695
|
-
if (options.resume && run.entry.handle.agent.status === 'idle') {
|
|
696
|
-
const recovered = recoverInbox(run.entry.handle.agent.session.snapshotEvents());
|
|
697
|
-
const queued = new Set(this.pending(run).map((message) => message.id));
|
|
698
|
-
for (const message of recovered.pending.slice().reverse())
|
|
699
|
-
if (!queued.has(message.id) && message.source.kind === 'a2a')
|
|
700
|
-
run.entry.handle.agent.inbox.prepend(message.source.target, message);
|
|
701
|
-
}
|
|
702
|
-
const context = options.context ?? new ServerCallContext();
|
|
703
|
-
const tasks = new Set();
|
|
704
|
-
const reassigned = new Map();
|
|
705
|
-
for (const message of this.pending(run)) {
|
|
706
|
-
if (message.source.kind !== 'a2a')
|
|
707
|
-
throw new Error('Cannot wake input without A2A task attribution.');
|
|
708
|
-
const oldId = message.source.taskId;
|
|
709
|
-
let id = oldId;
|
|
710
|
-
if (run.tasks.has(id) && !run.tasks.get(id).settled) {
|
|
711
|
-
tasks.add(id);
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
714
|
-
if (!this.server)
|
|
715
|
-
throw new Error('A2A task recovery requires the running server.');
|
|
716
|
-
let task = run.tasks.get(id)?.task ?? (await this.store.load(id, context));
|
|
717
|
-
if (!task || task.contextId !== contextId)
|
|
718
|
-
throw new Error('Queued A2A task is not accessible.');
|
|
719
|
-
if (this.terminal(task)) {
|
|
720
|
-
if (!options.resume)
|
|
721
|
-
throw new Error('Retained input needs explicit resume under a new task.');
|
|
722
|
-
let replacement = reassigned.get(oldId);
|
|
723
|
-
const original = Message.fromJSON(JSON.parse(message.source.message));
|
|
724
|
-
if (!replacement) {
|
|
725
|
-
replacement = {
|
|
726
|
-
...task,
|
|
727
|
-
id: randomUUID(),
|
|
728
|
-
history: [],
|
|
729
|
-
artifacts: [],
|
|
730
|
-
status: {
|
|
731
|
-
state: TaskState.TASK_STATE_SUBMITTED,
|
|
732
|
-
timestamp: new Date().toISOString(),
|
|
733
|
-
message: undefined,
|
|
734
|
-
},
|
|
735
|
-
};
|
|
736
|
-
reassigned.set(oldId, replacement);
|
|
914
|
+
task.final = terminalStatusUpdate(task.task.id, contextId, 'failed', error instanceof Error ? error.message : String(error));
|
|
915
|
+
if (task.final.kind === 'statusUpdate')
|
|
916
|
+
task.task.status = task.final.data.status;
|
|
917
|
+
// Cleanup may append inbox events, so it must run outside session publication.
|
|
918
|
+
queueMicrotask(() => {
|
|
919
|
+
try {
|
|
920
|
+
const run = this.contexts.get(contextId);
|
|
921
|
+
if (run) {
|
|
922
|
+
this.discard(run, task.task.id);
|
|
923
|
+
if (run.active === task)
|
|
924
|
+
run.entry.handle.agent.cancel({ kind: 'user' }, { keepInbox: true });
|
|
737
925
|
}
|
|
738
|
-
id = replacement.id;
|
|
739
|
-
original.taskId = id;
|
|
740
|
-
original.referenceTaskIds = [...new Set([...original.referenceTaskIds, oldId])];
|
|
741
|
-
replacement.history.push(original);
|
|
742
|
-
await this.checkpoint(replacement, context);
|
|
743
|
-
this.observe(this.outcome(task), run, { taskId: oldId });
|
|
744
|
-
const replacementInput = {
|
|
745
|
-
...message,
|
|
746
|
-
source: {
|
|
747
|
-
...message.source,
|
|
748
|
-
taskId: id,
|
|
749
|
-
message: JSON.stringify(Message.toJSON(original)),
|
|
750
|
-
},
|
|
751
|
-
};
|
|
752
|
-
run.entry.handle.agent.inbox.replace(message.id, replacementInput);
|
|
753
|
-
this.observeInput('reassigned', run, replacementInput);
|
|
754
|
-
task = replacement;
|
|
755
926
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
(owned && (owned.settled || owned.settlementStarted || run.active === owned)))
|
|
766
|
-
continue;
|
|
767
|
-
const interrupted = owned?.task ?? (await this.store.load(id, context));
|
|
768
|
-
if (!interrupted || interrupted.contextId !== contextId || this.terminal(interrupted))
|
|
769
|
-
continue;
|
|
770
|
-
const failed = owned ?? this.restoreTask(run, interrupted, context);
|
|
771
|
-
this.fail(failed, contextId, new Error('Committed input has no durable terminal task checkpoint; automatic replay is disabled.'));
|
|
772
|
-
await this.checkpoint(structuredClone(failed.task), failed.context);
|
|
773
|
-
}
|
|
774
|
-
if (this.stopping || this.bridge.isClearing(contextId))
|
|
775
|
-
return [];
|
|
776
|
-
if (options.resume) {
|
|
777
|
-
run.paused = false;
|
|
778
|
-
run.entry.handle.agent.inbox.setPaused(false);
|
|
779
|
-
}
|
|
780
|
-
await this.flush(run.entry.handle.agent.session);
|
|
781
|
-
this.observe(options.resume ? 'resume' : 'wake', run);
|
|
782
|
-
if (tasks.size)
|
|
783
|
-
this.drive(run);
|
|
784
|
-
return [...tasks];
|
|
927
|
+
catch (cleanupError) {
|
|
928
|
+
console.error('[dsh-a2a] failed task cleanup failed:', cleanupError);
|
|
929
|
+
}
|
|
930
|
+
finally {
|
|
931
|
+
task.headers.clear();
|
|
932
|
+
task.bus.publish(task.final);
|
|
933
|
+
task.finish();
|
|
934
|
+
}
|
|
935
|
+
});
|
|
785
936
|
}
|
|
786
|
-
|
|
937
|
+
discard(run, taskId) {
|
|
938
|
+
for (const message of this.pending(run))
|
|
939
|
+
if (message.source.kind === 'a2a' && message.source.taskId === taskId)
|
|
940
|
+
run.entry.handle.agent.inbox.remove(message.id);
|
|
941
|
+
}
|
|
942
|
+
async cancel(taskId) {
|
|
787
943
|
if (this.stopping)
|
|
788
944
|
throw new Error('Native inbox is unloading.');
|
|
789
945
|
for (const [contextId, run] of this.contexts) {
|
|
790
946
|
if (!run.tasks.has(taskId))
|
|
791
947
|
continue;
|
|
792
|
-
await this.waking.get(contextId);
|
|
793
948
|
const task = run.tasks.get(taskId);
|
|
794
949
|
if (!task)
|
|
795
950
|
continue;
|
|
@@ -799,7 +954,7 @@ export class NativeInbox {
|
|
|
799
954
|
return contextId;
|
|
800
955
|
let operation = task.cancellation;
|
|
801
956
|
if (!operation) {
|
|
802
|
-
operation = this.cancelTask(run, task
|
|
957
|
+
operation = this.cancelTask(run, task);
|
|
803
958
|
task.cancellation = operation;
|
|
804
959
|
void operation.catch(() => {
|
|
805
960
|
if (task.cancellation === operation)
|
|
@@ -811,19 +966,10 @@ export class NativeInbox {
|
|
|
811
966
|
}
|
|
812
967
|
return undefined;
|
|
813
968
|
}
|
|
814
|
-
async cancelTask(run, task
|
|
969
|
+
async cancelTask(run, task) {
|
|
815
970
|
const contextId = run.entry.sessionId;
|
|
816
971
|
const taskId = task.task.id;
|
|
817
|
-
|
|
818
|
-
run.paused = true;
|
|
819
|
-
run.entry.handle.agent.inbox.setPaused(true);
|
|
820
|
-
this.observe('paused', run);
|
|
821
|
-
}
|
|
822
|
-
else {
|
|
823
|
-
for (const message of this.pending(run))
|
|
824
|
-
if (message.source.kind === 'a2a' && message.source.taskId === taskId)
|
|
825
|
-
run.entry.handle.agent.inbox.remove(message.id);
|
|
826
|
-
}
|
|
972
|
+
this.discard(run, taskId);
|
|
827
973
|
task.controller.abort();
|
|
828
974
|
task.settled = true;
|
|
829
975
|
task.final = terminalStatusUpdate(taskId, contextId, 'canceled', 'Task canceled by request.');
|
|
@@ -831,13 +977,6 @@ export class NativeInbox {
|
|
|
831
977
|
task.task.status = task.final.data.status;
|
|
832
978
|
if (run.active === task) {
|
|
833
979
|
run.entry.handle.agent.cancel({ kind: 'user' }, { keepInbox: true });
|
|
834
|
-
void run.entry.handle.agent
|
|
835
|
-
.whenIdle()
|
|
836
|
-
.then(() => {
|
|
837
|
-
if (!run.paused && !this.stopping)
|
|
838
|
-
this.drive(run);
|
|
839
|
-
})
|
|
840
|
-
.catch((error) => console.error('[dsh-a2a] cancellation failed:', error));
|
|
841
980
|
}
|
|
842
981
|
try {
|
|
843
982
|
try {
|
|
@@ -853,7 +992,6 @@ export class NativeInbox {
|
|
|
853
992
|
}
|
|
854
993
|
}
|
|
855
994
|
async clear(contextId) {
|
|
856
|
-
await this.waking.get(contextId);
|
|
857
995
|
await this.resolving.get(contextId);
|
|
858
996
|
const run = this.contexts.get(contextId);
|
|
859
997
|
if (!run)
|
|
@@ -864,7 +1002,6 @@ export class NativeInbox {
|
|
|
864
1002
|
.map((task) => task.task.id)
|
|
865
1003
|
.concat(this.pending(run).flatMap((message) => message.source.kind === 'a2a' ? [message.source.taskId] : []))),
|
|
866
1004
|
];
|
|
867
|
-
run.paused = true;
|
|
868
1005
|
for (const admission of run.admissions)
|
|
869
1006
|
admission.abort();
|
|
870
1007
|
run.entry.handle.agent.inbox.clear();
|
|
@@ -885,15 +1022,20 @@ export class NativeInbox {
|
|
|
885
1022
|
const run = this.contexts.get(session.id);
|
|
886
1023
|
if (!run)
|
|
887
1024
|
return;
|
|
888
|
-
this.observe('disposed', run);
|
|
889
1025
|
for (const admission of run.admissions)
|
|
890
1026
|
admission.abort();
|
|
891
1027
|
for (const task of run.tasks.values()) {
|
|
892
|
-
|
|
1028
|
+
if (!task.settled) {
|
|
1029
|
+
const final = terminalStatusUpdate(task.task.id, session.id, 'canceled', 'Agent disposed.');
|
|
1030
|
+
if (final.kind === 'statusUpdate')
|
|
1031
|
+
task.task.status = final.data.status;
|
|
1032
|
+
task.bus.publish(final);
|
|
1033
|
+
}
|
|
893
1034
|
task.settled = true;
|
|
894
1035
|
task.controller.abort();
|
|
895
1036
|
task.finish();
|
|
896
1037
|
}
|
|
1038
|
+
this.observe('disposed', run);
|
|
897
1039
|
this.contexts.delete(session.id);
|
|
898
1040
|
}
|
|
899
1041
|
shutdown() {
|
|
@@ -902,7 +1044,7 @@ export class NativeInbox {
|
|
|
902
1044
|
this.stopping = true;
|
|
903
1045
|
this.shutdownPromise = (async () => {
|
|
904
1046
|
const failures = [];
|
|
905
|
-
await Promise.allSettled([...this.
|
|
1047
|
+
await Promise.allSettled([...this.resolving.values()]);
|
|
906
1048
|
const cancellations = [...this.contexts.values()].flatMap((run) => [...run.tasks.values()].flatMap((task) => (task.cancellation ? [task.cancellation] : [])));
|
|
907
1049
|
for (const outcome of await Promise.allSettled(cancellations))
|
|
908
1050
|
if (outcome.status === 'rejected')
|
|
@@ -911,7 +1053,7 @@ export class NativeInbox {
|
|
|
911
1053
|
for (const admission of run.admissions)
|
|
912
1054
|
admission.abort();
|
|
913
1055
|
try {
|
|
914
|
-
run.entry.handle.agent.cancel({ kind: 'disposed' }
|
|
1056
|
+
run.entry.handle.agent.cancel({ kind: 'disposed' });
|
|
915
1057
|
}
|
|
916
1058
|
catch (error) {
|
|
917
1059
|
failures.push(error);
|
|
@@ -929,11 +1071,17 @@ export class NativeInbox {
|
|
|
929
1071
|
failures.push(error);
|
|
930
1072
|
}
|
|
931
1073
|
finally {
|
|
932
|
-
this.observe('disposed', run);
|
|
933
1074
|
for (const task of run.tasks.values()) {
|
|
934
|
-
|
|
1075
|
+
try {
|
|
1076
|
+
if (!task.settled)
|
|
1077
|
+
await this.cancelTask(run, task);
|
|
1078
|
+
}
|
|
1079
|
+
catch (error) {
|
|
1080
|
+
failures.push(error);
|
|
1081
|
+
}
|
|
935
1082
|
task.finish();
|
|
936
1083
|
}
|
|
1084
|
+
this.observe('disposed', run);
|
|
937
1085
|
}
|
|
938
1086
|
}
|
|
939
1087
|
if (failures.length)
|