@tangle-network/agent-gateway 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +83 -2
  2. package/dist/chunk-GITV7CPT.js +84 -0
  3. package/dist/chunk-GITV7CPT.js.map +1 -0
  4. package/dist/chunk-J5SDVHOL.js +104 -0
  5. package/dist/chunk-J5SDVHOL.js.map +1 -0
  6. package/dist/chunk-MP6IIAIA.js +5651 -0
  7. package/dist/chunk-MP6IIAIA.js.map +1 -0
  8. package/dist/index.d.ts +70 -10
  9. package/dist/index.js +303 -21
  10. package/dist/index.js.map +1 -1
  11. package/dist/middleware.d.ts +7 -2
  12. package/dist/middleware.js +3 -2
  13. package/dist/nonce-store.d.ts +47 -11
  14. package/dist/nonce-store.js +9 -3
  15. package/dist/observer-types-A0RtA8uL.d.ts +95 -0
  16. package/dist/observer.d.ts +79 -0
  17. package/dist/observer.js +11 -0
  18. package/dist/observer.js.map +1 -0
  19. package/dist/{types-DEsMmS-X.d.ts → types-BHISsm7D.d.ts} +414 -170
  20. package/dist/types.d.ts +2 -1
  21. package/package.json +1 -1
  22. package/src/a2a/execution-fence.ts +162 -0
  23. package/src/a2a/handler.ts +506 -560
  24. package/src/a2a/message-send-execution.ts +241 -0
  25. package/src/a2a/message-stream-execution.ts +392 -0
  26. package/src/a2a/payment-recovery.ts +431 -0
  27. package/src/a2a/push-config-methods.ts +158 -0
  28. package/src/a2a/push-notifications.ts +172 -22
  29. package/src/a2a/task-cancellation.ts +50 -0
  30. package/src/a2a/task-finalization.ts +451 -0
  31. package/src/a2a/task-lifecycle.ts +54 -0
  32. package/src/a2a/task-methods.ts +163 -0
  33. package/src/a2a/task-push-delivery.ts +119 -0
  34. package/src/a2a/task-recovery.ts +11 -0
  35. package/src/a2a/task-state.ts +99 -0
  36. package/src/a2a/task-store-sql.ts +222 -24
  37. package/src/a2a/task-store.ts +58 -1
  38. package/src/a2a/task-submission-recovery.ts +178 -0
  39. package/src/a2a/types.ts +1 -0
  40. package/src/dispatch-authorization.ts +437 -0
  41. package/src/dispatch-payment-recovery.ts +248 -0
  42. package/src/dispatch-payment.ts +425 -0
  43. package/src/dispatch-pricing.ts +108 -0
  44. package/src/dispatch-sandbox.ts +422 -0
  45. package/src/dispatch-settlement.ts +139 -0
  46. package/src/dispatch-types.ts +81 -0
  47. package/src/dispatch.ts +35 -483
  48. package/src/index.ts +57 -1
  49. package/src/middleware.ts +307 -26
  50. package/src/mpp-payment.ts +117 -0
  51. package/src/nonce-store.ts +122 -20
  52. package/src/observer-types.ts +63 -0
  53. package/src/observer.ts +3 -63
  54. package/src/payment-operations.ts +485 -0
  55. package/src/payment-recovery-sql.ts +108 -0
  56. package/src/payment-recovery-worker.ts +488 -0
  57. package/src/payment-recovery.ts +331 -0
  58. package/src/payment-types.ts +48 -0
  59. package/src/types.ts +144 -46
  60. package/src/verify.ts +233 -71
  61. package/dist/chunk-M7ZJAK4K.js +0 -53
  62. package/dist/chunk-M7ZJAK4K.js.map +0 -1
  63. package/dist/chunk-Q4YAIEZY.js +0 -1763
  64. package/dist/chunk-Q4YAIEZY.js.map +0 -1
@@ -0,0 +1,241 @@
1
+ import type { Context } from 'hono'
2
+ import {
3
+ type AuthorizedRequest,
4
+ dispatchSandboxStreamRich,
5
+ beginPaymentExecution,
6
+ markPaymentExecutionStarted,
7
+ renewPaymentExecution,
8
+ } from '../dispatch'
9
+ import { claimTaskExecution, renewTaskExecution } from './execution-fence'
10
+ import type { GatewayConfig, SandboxUsageReceipt } from '../types'
11
+ import type { TaskStore } from './task-store'
12
+ import { A2A_ERROR_CODES, type JSONRPCRequest, type Task } from './types'
13
+ import { clearTaskSubmission } from './task-submission-recovery'
14
+ import {
15
+ buildFinalizationRecord,
16
+ clearFinalizationMarker,
17
+ completeCanceledTask,
18
+ markUsageRecorded,
19
+ retainFinalizationForRecovery,
20
+ withFinalizationRecord,
21
+ } from './task-finalization'
22
+ import { clearPaymentRecoveryMarker, releaseTaskPayment } from './payment-recovery'
23
+ import {
24
+ agentMessage,
25
+ asError,
26
+ compareAndSetTask,
27
+ isTerminal,
28
+ nowIso,
29
+ persistTaskIfCurrent,
30
+ shouldPreserveTask,
31
+ withStatus,
32
+ } from './task-state'
33
+ import { fail, ok } from './jsonrpc'
34
+ import { responseTextToArtifact } from './translate'
35
+ import type { TaskFinalizationDependencies } from './task-finalization'
36
+ import type { PaymentRecoveryDependencies } from './payment-recovery'
37
+
38
+ export interface MessageExecutionDependencies {
39
+ taskStore: TaskStore
40
+ config: GatewayConfig
41
+ payment: PaymentRecoveryDependencies
42
+ finalization: TaskFinalizationDependencies
43
+ }
44
+
45
+ export async function executeMessageSend(
46
+ c: Context,
47
+ req: JSONRPCRequest,
48
+ deps: MessageExecutionDependencies,
49
+ authz: AuthorizedRequest,
50
+ task: Task,
51
+ signal: AbortSignal,
52
+ ): Promise<Response> {
53
+ if (isTerminal(task.status.state)) return c.json(ok(req.id, task))
54
+ const taskWithoutSubmission = clearTaskSubmission(task)
55
+ let workingTask: Task = task.status.state === 'working'
56
+ ? taskWithoutSubmission
57
+ : { ...taskWithoutSubmission, status: { state: 'working', timestamp: nowIso() } }
58
+ if (
59
+ JSON.stringify(task) !== JSON.stringify(workingTask) &&
60
+ !await compareAndSetTask(deps.taskStore, task, workingTask)
61
+ ) {
62
+ await releaseTaskPayment(authz, task, deps.payment, 'A2A task changed before execution started', false)
63
+ if (signal.aborted) {
64
+ const canceled = await deps.taskStore.get(task.id)
65
+ if (canceled?.status.state === 'canceled') {
66
+ await deps.finalization.deliverPush(canceled)
67
+ return c.json(ok(req.id, canceled))
68
+ }
69
+ }
70
+ return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, `task '${task.id}' changed before execution`))
71
+ }
72
+
73
+ let responseText = ''
74
+ let usage: SandboxUsageReceipt | undefined
75
+ let workObserved = false
76
+ let inputRequiredPrompt: string | undefined
77
+ let inputRequiredSeen = false
78
+ let finalizationLeaseId: string | undefined
79
+ try {
80
+ for await (const event of dispatchSandboxStreamRich(
81
+ authz.agent,
82
+ authz.userMessage,
83
+ authz.consumerId,
84
+ deps.config,
85
+ signal,
86
+ task.id,
87
+ authz.maxOutputTokens,
88
+ async () => {
89
+ workingTask = await claimTaskExecution(deps.taskStore, workingTask, authz.requestId)
90
+ await beginPaymentExecution(authz, deps.config)
91
+ },
92
+ authz.paymentOperation !== undefined || authz.mppChargeOperation !== undefined,
93
+ async () => {
94
+ workObserved = true
95
+ await markPaymentExecutionStarted(authz, deps.config)
96
+ },
97
+ authz.executionBudget.maxInputTokens,
98
+ async () => {
99
+ workingTask = await renewTaskExecution(deps.taskStore, task.id, authz.requestId)
100
+ await renewPaymentExecution(authz, deps.config)
101
+ },
102
+ )) {
103
+ if (event.kind === 'text') {
104
+ responseText += event.delta
105
+ workObserved = true
106
+ } else if (event.kind === 'activity') {
107
+ workObserved = true
108
+ } else if (event.kind === 'usage') {
109
+ usage = event.usage
110
+ } else {
111
+ inputRequiredSeen = true
112
+ inputRequiredPrompt = event.prompt
113
+ workObserved = true
114
+ }
115
+ }
116
+ } catch (err) {
117
+ const releasedTask = await releaseTaskPayment(
118
+ authz,
119
+ workingTask,
120
+ deps.payment,
121
+ err instanceof Error ? err.message : String(err),
122
+ workObserved || usage !== undefined,
123
+ )
124
+ const currentTask = await deps.taskStore.get(task.id) ?? releasedTask
125
+ const failed = shouldPreserveTask(currentTask)
126
+ ? currentTask
127
+ : withStatus(clearTaskSubmission(currentTask), 'failed')
128
+ try {
129
+ const persisted = await persistTaskIfCurrent(deps.taskStore, currentTask, failed)
130
+ await deps.finalization.deliverPush(persisted)
131
+ } catch (taskError) {
132
+ console.error(
133
+ `[a2a] failed to persist failed task ${task.id}:`,
134
+ taskError instanceof Error ? taskError.message : String(taskError),
135
+ )
136
+ }
137
+ return c.json(fail(req.id, A2A_ERROR_CODES.INTERNAL_ERROR, err instanceof Error ? err.message : String(err)))
138
+ }
139
+
140
+ if (signal.aborted) {
141
+ const canceled = await completeCanceledTask(
142
+ authz,
143
+ workingTask,
144
+ responseText,
145
+ usage,
146
+ workObserved,
147
+ deps.finalization,
148
+ )
149
+ return c.json(ok(req.id, canceled))
150
+ }
151
+
152
+ try {
153
+ if (!usage) throw new Error('sandbox did not provide a usage receipt')
154
+ const finalizationArtifact = responseText
155
+ ? responseTextToArtifact(responseText, `${task.id}-artifact-0`)
156
+ : task.artifacts?.[0] ?? null
157
+ const finalization = buildFinalizationRecord(
158
+ authz,
159
+ usage,
160
+ finalizationArtifact,
161
+ inputRequiredSeen,
162
+ inputRequiredPrompt,
163
+ )
164
+ const finalizingTask = withFinalizationRecord(workingTask, finalization)
165
+ if (!await compareAndSetTask(deps.taskStore, workingTask, finalizingTask)) {
166
+ const currentTask = await deps.taskStore.get(task.id)
167
+ if (currentTask?.status.state === 'canceled') {
168
+ const canceled = await completeCanceledTask(
169
+ authz,
170
+ currentTask,
171
+ responseText,
172
+ usage,
173
+ workObserved,
174
+ deps.finalization,
175
+ )
176
+ return c.json(ok(req.id, canceled))
177
+ }
178
+ throw new Error('A2A task changed before payment settlement')
179
+ }
180
+ finalizationLeaseId = finalization.lease.id
181
+ let usageRecordedTask = finalizingTask
182
+ await deps.finalization.settle(authz, usage, {
183
+ onUsageRecorded: async () => {
184
+ usageRecordedTask = await markUsageRecorded(deps.taskStore, usageRecordedTask)
185
+ },
186
+ })
187
+ usageRecordedTask = await markUsageRecorded(deps.taskStore, usageRecordedTask)
188
+ const settledBase = clearPaymentRecoveryMarker(clearFinalizationMarker(usageRecordedTask))
189
+ const result = inputRequiredSeen
190
+ ? withStatus(
191
+ settledBase,
192
+ 'input-required',
193
+ inputRequiredPrompt ? agentMessage(task, inputRequiredPrompt) : undefined,
194
+ responseText ? [responseTextToArtifact(responseText, `${task.id}-artifact-0`)] : task.artifacts,
195
+ )
196
+ : withStatus(settledBase, 'completed', undefined, [
197
+ responseTextToArtifact(responseText, `${task.id}-artifact-0`),
198
+ ])
199
+ if (!await compareAndSetTask(deps.taskStore, usageRecordedTask, result)) {
200
+ const currentTask = await deps.taskStore.get(task.id)
201
+ if (currentTask && (isTerminal(currentTask.status.state) || currentTask.status.state === 'input-required')) {
202
+ return c.json(ok(req.id, currentTask))
203
+ }
204
+ throw new Error('A2A task changed after payment settlement')
205
+ }
206
+ if (inputRequiredSeen) return c.json(ok(req.id, result))
207
+ await deps.finalization.deliverPush(result)
208
+ return c.json(ok(req.id, result))
209
+ } catch (err) {
210
+ const releasedTask = await releaseTaskPayment(
211
+ authz,
212
+ workingTask,
213
+ deps.payment,
214
+ err instanceof Error ? err.message : String(err),
215
+ workObserved || usage !== undefined,
216
+ )
217
+ if (finalizationLeaseId) {
218
+ await retainFinalizationForRecovery(
219
+ deps.taskStore,
220
+ task.id,
221
+ finalizationLeaseId,
222
+ asError(err),
223
+ )
224
+ return c.json(fail(req.id, A2A_ERROR_CODES.INTERNAL_ERROR, 'Payment settlement failed'))
225
+ }
226
+ const currentTask = await deps.taskStore.get(task.id) ?? releasedTask
227
+ const failed = shouldPreserveTask(currentTask)
228
+ ? currentTask
229
+ : withStatus(clearTaskSubmission(currentTask), 'failed')
230
+ try {
231
+ const persisted = await persistTaskIfCurrent(deps.taskStore, currentTask, failed)
232
+ await deps.finalization.deliverPush(persisted)
233
+ } catch (taskError) {
234
+ console.error(
235
+ `[a2a] failed to persist failed task ${task.id}:`,
236
+ taskError instanceof Error ? taskError.message : String(taskError),
237
+ )
238
+ }
239
+ return c.json(fail(req.id, A2A_ERROR_CODES.INTERNAL_ERROR, 'Payment settlement failed'))
240
+ }
241
+ }
@@ -0,0 +1,392 @@
1
+ import type { Context } from 'hono'
2
+ import {
3
+ type AuthorizedRequest,
4
+ beginPaymentExecution,
5
+ dispatchSandboxStreamRich,
6
+ markPaymentExecutionStarted,
7
+ renewPaymentExecution,
8
+ } from '../dispatch'
9
+ import type { GatewayConfig, SandboxUsageReceipt } from '../types'
10
+ import { claimTaskExecution, renewTaskExecution } from './execution-fence'
11
+ import { fail, ok } from './jsonrpc'
12
+ import {
13
+ clearPaymentRecoveryMarker,
14
+ releaseTaskPayment,
15
+ } from './payment-recovery'
16
+ import {
17
+ buildFinalizationRecord,
18
+ clearFinalizationMarker,
19
+ completeCanceledTask,
20
+ markUsageRecorded,
21
+ retainFinalizationForRecovery,
22
+ withFinalizationRecord,
23
+ } from './task-finalization'
24
+ import { clearTaskSubmission } from './task-submission-recovery'
25
+ import { bindRequestAbort, type TaskCancellationRegistry } from './task-cancellation'
26
+ import type { TaskLifecycle } from './task-lifecycle'
27
+ import {
28
+ agentMessage,
29
+ asError,
30
+ compareAndSetTask,
31
+ isTerminal,
32
+ nowIso,
33
+ persistTaskIfCurrent,
34
+ shouldPreserveTask,
35
+ withStatus,
36
+ } from './task-state'
37
+ import type { TaskStateStore } from './task-state'
38
+ import {
39
+ A2A_ERROR_CODES,
40
+ type JSONRPCRequest,
41
+ type StreamingEvent,
42
+ type Task,
43
+ type TaskArtifactUpdateEvent,
44
+ type TaskStatusUpdateEvent,
45
+ } from './types'
46
+ import { responseTextToArtifact } from './translate'
47
+
48
+ export interface MessageStreamExecutionDependencies {
49
+ taskStore: TaskStateStore
50
+ config: GatewayConfig
51
+ lifecycle: TaskLifecycle
52
+ cancels: TaskCancellationRegistry
53
+ deliverPush: (task: Task) => Promise<void>
54
+ reportStreamError: (authz: AuthorizedRequest, error: unknown) => Promise<void>
55
+ }
56
+
57
+ export async function executeMessageStream(
58
+ c: Context,
59
+ req: JSONRPCRequest,
60
+ deps: MessageStreamExecutionDependencies,
61
+ authz: AuthorizedRequest,
62
+ task: Task,
63
+ ): Promise<Response> {
64
+ const controller = deps.cancels.register(task.id)
65
+ const lifecycle = deps.lifecycle
66
+ const detachRequestAbort = bindRequestAbort(c.req.raw.signal, controller)
67
+ const workingStatus: TaskStatusUpdateEvent = {
68
+ kind: 'status-update',
69
+ taskId: task.id,
70
+ contextId: task.contextId,
71
+ status: { state: 'working', timestamp: nowIso() },
72
+ final: false,
73
+ }
74
+ if (isTerminal(task.status.state)) {
75
+ detachRequestAbort()
76
+ deps.cancels.clear(task.id)
77
+ return c.json(ok(req.id, task))
78
+ }
79
+ let workingTask: Task = task.status.state === 'working'
80
+ ? task
81
+ : { ...task, status: workingStatus.status }
82
+ if (task.status.state !== 'working' && !await compareAndSetTask(deps.taskStore, task, workingTask)) {
83
+ detachRequestAbort()
84
+ deps.cancels.clear(task.id)
85
+ const current = await releaseTaskPayment(
86
+ authz,
87
+ await deps.taskStore.get(task.id) ?? task,
88
+ lifecycle.payment,
89
+ 'A2A task changed before execution started',
90
+ false,
91
+ )
92
+ if (current.status.state === 'canceled') return c.json(ok(req.id, current))
93
+ return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, `task '${task.id}' changed before execution`))
94
+ }
95
+ let responseText = ''
96
+ let usage: SandboxUsageReceipt | undefined
97
+ let workObserved = false
98
+
99
+ const stream = new ReadableStream({
100
+ start(ctrl) {
101
+ void (async () => {
102
+ const encoder = new TextEncoder()
103
+ const send = (event: StreamingEvent) => {
104
+ if (ctrl.desiredSize === null) return
105
+ try {
106
+ ctrl.enqueue(encoder.encode(`data: ${JSON.stringify(ok(req.id, event))}\n\n`))
107
+ } catch {
108
+ // The client can cancel between the desiredSize check and enqueue.
109
+ }
110
+ }
111
+
112
+ let inputRequiredPrompt: string | undefined
113
+ let inputRequiredSeen = false
114
+ let finalizationLeaseId: string | undefined
115
+ try {
116
+ send(workingStatus)
117
+
118
+ for await (const event of dispatchSandboxStreamRich(
119
+ authz.agent,
120
+ authz.userMessage,
121
+ authz.consumerId,
122
+ deps.config,
123
+ controller.signal,
124
+ task.id,
125
+ authz.maxOutputTokens,
126
+ async () => {
127
+ workingTask = await claimTaskExecution(deps.taskStore, workingTask, authz.requestId)
128
+ await beginPaymentExecution(authz, deps.config)
129
+ },
130
+ authz.paymentOperation !== undefined || authz.mppChargeOperation !== undefined,
131
+ async () => {
132
+ workObserved = true
133
+ await markPaymentExecutionStarted(authz, deps.config)
134
+ },
135
+ authz.executionBudget.maxInputTokens,
136
+ async () => {
137
+ workingTask = await renewTaskExecution(deps.taskStore, task.id, authz.requestId)
138
+ await renewPaymentExecution(authz, deps.config)
139
+ },
140
+ )) {
141
+ if (event.kind === 'text') {
142
+ responseText += event.delta
143
+ workObserved = true
144
+ const artifactEvent: TaskArtifactUpdateEvent = {
145
+ kind: 'artifact-update',
146
+ taskId: task.id,
147
+ contextId: task.contextId,
148
+ artifact: {
149
+ artifactId: `${task.id}-artifact-0`,
150
+ name: 'response',
151
+ parts: [{ kind: 'text', text: event.delta }],
152
+ },
153
+ append: true,
154
+ }
155
+ send(artifactEvent)
156
+ } else if (event.kind === 'activity') {
157
+ workObserved = true
158
+ } else if (event.kind === 'usage') {
159
+ usage = event.usage
160
+ } else {
161
+ inputRequiredSeen = true
162
+ inputRequiredPrompt = event.prompt
163
+ workObserved = true
164
+ }
165
+ }
166
+
167
+ if (controller.signal.aborted) {
168
+ const canceled = await completeCanceledTask(
169
+ authz,
170
+ workingTask,
171
+ responseText,
172
+ usage,
173
+ workObserved,
174
+ lifecycle.finalization,
175
+ )
176
+ send({
177
+ kind: 'status-update',
178
+ taskId: task.id,
179
+ contextId: task.contextId,
180
+ status: canceled.status,
181
+ final: true,
182
+ })
183
+ return
184
+ }
185
+
186
+ if (!usage) throw new Error('sandbox did not provide a usage receipt')
187
+ const finalizationArtifact = responseTextToArtifact(responseText, `${task.id}-artifact-0`)
188
+ const finalization = buildFinalizationRecord(
189
+ authz,
190
+ usage,
191
+ finalizationArtifact,
192
+ inputRequiredSeen,
193
+ inputRequiredPrompt,
194
+ )
195
+ // Let the durable task-store CAS decide the cancellation race.
196
+ const finalizingTask = withFinalizationRecord(workingTask, finalization)
197
+ if (!await compareAndSetTask(deps.taskStore, workingTask, finalizingTask)) {
198
+ const currentTask = await deps.taskStore.get(task.id)
199
+ if (currentTask?.status.state === 'canceled') {
200
+ const canceled = await completeCanceledTask(
201
+ authz,
202
+ currentTask,
203
+ responseText,
204
+ usage,
205
+ workObserved,
206
+ lifecycle.finalization,
207
+ )
208
+ send({
209
+ kind: 'status-update',
210
+ taskId: task.id,
211
+ contextId: task.contextId,
212
+ status: canceled.status,
213
+ final: true,
214
+ })
215
+ return
216
+ }
217
+ await releaseTaskPayment(
218
+ authz,
219
+ task,
220
+ lifecycle.payment,
221
+ 'A2A task changed before payment settlement',
222
+ workObserved || usage !== undefined,
223
+ )
224
+ return
225
+ }
226
+ finalizationLeaseId = finalization.lease.id
227
+ deps.cancels.beginFinalization(task.id)
228
+ let usageRecordedTask = finalizingTask
229
+ await lifecycle.finalization.settle(authz, usage, {
230
+ onUsageRecorded: async () => {
231
+ usageRecordedTask = await markUsageRecorded(deps.taskStore, usageRecordedTask)
232
+ },
233
+ })
234
+ usageRecordedTask = await markUsageRecorded(deps.taskStore, usageRecordedTask)
235
+
236
+ if (inputRequiredSeen) {
237
+ const paused = withStatus(
238
+ clearPaymentRecoveryMarker(clearFinalizationMarker(usageRecordedTask)),
239
+ 'input-required',
240
+ inputRequiredPrompt ? agentMessage(task, inputRequiredPrompt) : undefined,
241
+ responseText
242
+ ? [responseTextToArtifact(responseText, `${task.id}-artifact-0`)]
243
+ : task.artifacts,
244
+ )
245
+ if (!await compareAndSetTask(deps.taskStore, usageRecordedTask, paused)) {
246
+ const currentTask = await deps.taskStore.get(task.id)
247
+ if (currentTask) {
248
+ send({
249
+ kind: 'status-update',
250
+ taskId: task.id,
251
+ contextId: task.contextId,
252
+ status: currentTask.status,
253
+ final: isTerminal(currentTask.status.state) || currentTask.status.state === 'input-required',
254
+ })
255
+ }
256
+ return
257
+ }
258
+ send({
259
+ kind: 'status-update',
260
+ taskId: task.id,
261
+ contextId: task.contextId,
262
+ status: paused.status,
263
+ final: true,
264
+ })
265
+ return
266
+ }
267
+
268
+ const completed = withStatus(
269
+ clearPaymentRecoveryMarker(clearFinalizationMarker(usageRecordedTask)),
270
+ 'completed',
271
+ undefined,
272
+ [responseTextToArtifact(responseText, `${task.id}-artifact-0`)],
273
+ )
274
+ if (!await compareAndSetTask(deps.taskStore, usageRecordedTask, completed)) {
275
+ const currentTask = await deps.taskStore.get(task.id)
276
+ if (currentTask) {
277
+ send({
278
+ kind: 'status-update',
279
+ taskId: task.id,
280
+ contextId: task.contextId,
281
+ status: currentTask.status,
282
+ final: isTerminal(currentTask.status.state) || currentTask.status.state === 'input-required',
283
+ })
284
+ }
285
+ return
286
+ }
287
+ send({
288
+ kind: 'artifact-update',
289
+ taskId: task.id,
290
+ contextId: task.contextId,
291
+ artifact: {
292
+ artifactId: `${task.id}-artifact-0`,
293
+ name: 'response',
294
+ parts: [{ kind: 'text', text: '' }],
295
+ },
296
+ append: true,
297
+ lastChunk: true,
298
+ })
299
+ send({
300
+ kind: 'status-update',
301
+ taskId: task.id,
302
+ contextId: task.contextId,
303
+ status: completed.status,
304
+ final: true,
305
+ })
306
+ await lifecycle.finalization.deliverPush(completed)
307
+ } catch (err) {
308
+ const releasedTask = await releaseTaskPayment(
309
+ authz,
310
+ task,
311
+ lifecycle.payment,
312
+ err instanceof Error ? err.message : String(err),
313
+ workObserved || usage !== undefined,
314
+ )
315
+ if (finalizationLeaseId) {
316
+ const retained = await retainFinalizationForRecovery(
317
+ deps.taskStore,
318
+ task.id,
319
+ finalizationLeaseId,
320
+ asError(err),
321
+ )
322
+ if (retained) {
323
+ send({
324
+ kind: 'status-update',
325
+ taskId: task.id,
326
+ contextId: task.contextId,
327
+ status: retained.status,
328
+ final: false,
329
+ })
330
+ }
331
+ return
332
+ }
333
+ const currentTask = await deps.taskStore.get(task.id) ?? releasedTask
334
+ const failed = shouldPreserveTask(currentTask)
335
+ ? currentTask
336
+ : withStatus(clearTaskSubmission(currentTask), 'failed')
337
+ try {
338
+ const persisted = await persistTaskIfCurrent(deps.taskStore, currentTask, failed)
339
+ send({
340
+ kind: 'status-update',
341
+ taskId: task.id,
342
+ contextId: task.contextId,
343
+ status: persisted.status,
344
+ final: true,
345
+ })
346
+ await deps.deliverPush(persisted)
347
+ } catch (taskError) {
348
+ console.error(
349
+ `[a2a] failed to persist failed task ${task.id}:`,
350
+ taskError instanceof Error ? taskError.message : String(taskError),
351
+ )
352
+ }
353
+ try {
354
+ await deps.reportStreamError(authz, err)
355
+ } catch (observerError) {
356
+ console.error(
357
+ `[a2a] stream observer failed for ${authz.requestId}:`,
358
+ observerError instanceof Error ? observerError.message : String(observerError),
359
+ )
360
+ }
361
+ } finally {
362
+ detachRequestAbort()
363
+ deps.cancels.clear(task.id)
364
+ try {
365
+ if (ctrl.desiredSize !== null) ctrl.close()
366
+ } catch {
367
+ // The response may already be closed by client cancellation.
368
+ }
369
+ }
370
+ })()
371
+ },
372
+ cancel() {
373
+ controller.abort()
374
+ },
375
+ })
376
+
377
+ return new Response(stream, {
378
+ headers: {
379
+ 'Content-Type': 'text/event-stream',
380
+ 'Cache-Control': 'no-cache',
381
+ 'X-Request-Id': authz.requestId,
382
+ 'X-Agent-Slug': authz.agent.slug,
383
+ 'X-Task-Id': task.id,
384
+ ...(authz.mppChargeOperation
385
+ ? { 'Payment-Receipt': authz.mppChargeOperation.receipt }
386
+ : {}),
387
+ ...(authz.paymentRecoveryId
388
+ ? { 'X-Payment-Operation-Id': authz.paymentRecoveryId }
389
+ : {}),
390
+ },
391
+ })
392
+ }