@tangle-network/agent-gateway 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -3
- package/dist/chunk-C7Z2BRYV.js +5693 -0
- package/dist/chunk-C7Z2BRYV.js.map +1 -0
- package/dist/chunk-GITV7CPT.js +84 -0
- package/dist/chunk-GITV7CPT.js.map +1 -0
- package/dist/chunk-J5SDVHOL.js +104 -0
- package/dist/chunk-J5SDVHOL.js.map +1 -0
- package/dist/index.d.ts +70 -10
- package/dist/index.js +303 -21
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +7 -2
- package/dist/middleware.js +3 -2
- package/dist/nonce-store.d.ts +47 -11
- package/dist/nonce-store.js +9 -3
- package/dist/observer-types-A0RtA8uL.d.ts +95 -0
- package/dist/observer.d.ts +79 -0
- package/dist/observer.js +11 -0
- package/dist/observer.js.map +1 -0
- package/dist/{types-DEsMmS-X.d.ts → types-oQ58UakD.d.ts} +447 -172
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/a2a/execution-fence.ts +162 -0
- package/src/a2a/handler.ts +506 -560
- package/src/a2a/message-send-execution.ts +241 -0
- package/src/a2a/message-stream-execution.ts +392 -0
- package/src/a2a/payment-recovery.ts +431 -0
- package/src/a2a/push-config-methods.ts +158 -0
- package/src/a2a/push-notifications.ts +172 -22
- package/src/a2a/task-cancellation.ts +50 -0
- package/src/a2a/task-finalization.ts +451 -0
- package/src/a2a/task-lifecycle.ts +54 -0
- package/src/a2a/task-methods.ts +163 -0
- package/src/a2a/task-push-delivery.ts +119 -0
- package/src/a2a/task-recovery.ts +11 -0
- package/src/a2a/task-state.ts +99 -0
- package/src/a2a/task-store-sql.ts +222 -24
- package/src/a2a/task-store.ts +58 -1
- package/src/a2a/task-submission-recovery.ts +178 -0
- package/src/a2a/types.ts +1 -0
- package/src/dispatch-authorization.ts +468 -0
- package/src/dispatch-payment-recovery.ts +248 -0
- package/src/dispatch-payment.ts +425 -0
- package/src/dispatch-pricing.ts +108 -0
- package/src/dispatch-sandbox.ts +424 -0
- package/src/dispatch-settlement.ts +139 -0
- package/src/dispatch-types.ts +84 -0
- package/src/dispatch.ts +35 -483
- package/src/index.ts +59 -1
- package/src/middleware.ts +339 -35
- package/src/mpp-payment.ts +117 -0
- package/src/nonce-store.ts +122 -20
- package/src/observer-types.ts +63 -0
- package/src/observer.ts +3 -63
- package/src/payment-operations.ts +485 -0
- package/src/payment-recovery-sql.ts +108 -0
- package/src/payment-recovery-worker.ts +488 -0
- package/src/payment-recovery.ts +331 -0
- package/src/payment-types.ts +48 -0
- package/src/types.ts +188 -49
- package/src/verify.ts +240 -71
- package/dist/chunk-M7ZJAK4K.js +0 -53
- package/dist/chunk-M7ZJAK4K.js.map +0 -1
- package/dist/chunk-Q4YAIEZY.js +0 -1763
- 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
|
+
}
|