@nylorun/runtime 0.36.0-beta → 0.37.0-beta
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/CHANGELOG.md +60 -0
- package/README.md +10 -0
- package/dist/api/a2a/card.d.ts +10 -4
- package/dist/api/a2a/card.js +18 -6
- package/dist/api/a2a/endpoint.d.ts +5 -2
- package/dist/api/a2a/endpoint.js +31 -9
- package/dist/api/a2a/extensions.d.ts +41 -0
- package/dist/api/a2a/extensions.js +92 -0
- package/dist/api/a2a/list.d.ts +60 -0
- package/dist/api/a2a/list.js +169 -0
- package/dist/api/a2a/protocol.d.ts +105 -9
- package/dist/api/a2a/protocol.js +169 -18
- package/dist/api/a2a/push.d.ts +76 -0
- package/dist/api/a2a/push.js +305 -0
- package/dist/api/a2a/routes.d.ts +12 -6
- package/dist/api/a2a/routes.js +409 -112
- package/dist/api/a2a/tasks.d.ts +72 -4
- package/dist/api/a2a/tasks.js +305 -27
- package/dist/api/components.d.ts +532 -0
- package/dist/api/components.js +3 -1
- package/dist/api/http/app.js +2 -0
- package/dist/api/http/routes/artifacts.js +19 -0
- package/dist/api/http/routes/turns.d.ts +18 -0
- package/dist/api/http/routes/turns.js +67 -0
- package/dist/api/inbound-files.d.ts +12 -1
- package/dist/api/inbound-files.js +44 -0
- package/dist/api/openapi.js +7 -2
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/main.js +1 -0
- package/dist/host/stack-config.d.ts +16 -0
- package/dist/host/stack-config.js +30 -0
- package/dist/management-openapi.json +5 -2
- package/dist/openapi.json +1485 -738
- package/dist/reads/types.d.ts +19 -0
- package/dist/store/postgres/drizzle/0039_a2a_push.sql +32 -0
- package/dist/store/postgres/drizzle/0040_turn_index.sql +23 -0
- package/dist/store/postgres/drizzle/0041_turn_index_backfill.sql +88 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +21 -0
- package/dist/store/postgres/reads.js +41 -1
- package/dist/store/postgres/schema.d.ts +1296 -534
- package/dist/store/postgres/schema.js +92 -0
- package/dist/store/postgres/store.js +133 -3
- package/dist/store/postgres/turns.d.ts +62 -0
- package/dist/store/postgres/turns.js +153 -0
- package/dist/store/types.d.ts +73 -3
- package/dist/tenant/runtime.js +7 -5
- package/dist/tenant/session-model.d.ts +11 -1
- package/dist/tenant/session-model.js +34 -5
- package/dist/tenant/sessions.d.ts +2 -1
- package/dist/tenant/sessions.js +8 -2
- package/dist/tenant/sweep.d.ts +3 -0
- package/dist/tenant/sweep.js +39 -11
- package/dist/tenant/types.d.ts +23 -0
- package/dist/tuning.d.ts +9 -0
- package/dist/tuning.js +11 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A2A 1.0 over JSON-RPC: the wire types the Runtime answers with, the envelope, the error
|
|
3
|
-
* codes and the parameter checks (Host feature `a2a-endpoint`). Only what v1 serves is typed:
|
|
4
|
-
* text, data and file parts, tasks, the card. Field names are the spec's ProtoJSON (camelCase,
|
|
5
|
-
* enum values such as `TASK_STATE_WORKING`). No Tenant state is read here.
|
|
6
|
-
*/
|
|
7
1
|
/** The A2A version this endpoint speaks (`A2A-Version`, Major.Minor). */
|
|
8
2
|
export declare const A2A_VERSION = "1.0";
|
|
9
3
|
export type TaskState = "TASK_STATE_SUBMITTED" | "TASK_STATE_WORKING" | "TASK_STATE_INPUT_REQUIRED" | "TASK_STATE_AUTH_REQUIRED" | "TASK_STATE_COMPLETED" | "TASK_STATE_FAILED" | "TASK_STATE_CANCELED" | "TASK_STATE_REJECTED";
|
|
@@ -34,11 +28,15 @@ export interface Message {
|
|
|
34
28
|
contextId?: string;
|
|
35
29
|
taskId?: string;
|
|
36
30
|
metadata?: Record<string, unknown>;
|
|
31
|
+
/** The extensions whose data the message carries (A2A §4.6). */
|
|
32
|
+
extensions?: string[];
|
|
37
33
|
}
|
|
38
34
|
export interface Artifact {
|
|
39
35
|
artifactId: string;
|
|
40
36
|
name?: string;
|
|
41
37
|
parts: Part[];
|
|
38
|
+
metadata?: Record<string, unknown>;
|
|
39
|
+
extensions?: string[];
|
|
42
40
|
}
|
|
43
41
|
export interface TaskStatus {
|
|
44
42
|
state: TaskState;
|
|
@@ -51,6 +49,54 @@ export interface Task {
|
|
|
51
49
|
status: TaskStatus;
|
|
52
50
|
artifacts?: Artifact[];
|
|
53
51
|
history?: Message[];
|
|
52
|
+
/** Profile v2 data about the whole task: `progress/v1`, `telemetry/v1`, `effects/v1`. */
|
|
53
|
+
metadata?: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
/** A change of a task's status, as a push delivers it (`StreamResponse.statusUpdate`). */
|
|
56
|
+
export interface TaskStatusUpdateEvent {
|
|
57
|
+
taskId: string;
|
|
58
|
+
contextId: string;
|
|
59
|
+
status: TaskStatus;
|
|
60
|
+
metadata?: Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
/** An artifact of a task, as a push delivers it (`StreamResponse.artifactUpdate`). */
|
|
63
|
+
export interface TaskArtifactUpdateEvent {
|
|
64
|
+
taskId: string;
|
|
65
|
+
contextId: string;
|
|
66
|
+
artifact: Artifact;
|
|
67
|
+
append?: boolean;
|
|
68
|
+
lastChunk?: boolean;
|
|
69
|
+
metadata?: Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
/** One push's body: A2A's `StreamResponse`, one of its cases. */
|
|
72
|
+
export type StreamResponse = {
|
|
73
|
+
statusUpdate: TaskStatusUpdateEvent;
|
|
74
|
+
} | {
|
|
75
|
+
artifactUpdate: TaskArtifactUpdateEvent;
|
|
76
|
+
} | {
|
|
77
|
+
task: Task;
|
|
78
|
+
};
|
|
79
|
+
/** How a push authenticates to its receiver (A2A `AuthenticationInfo`): `Authorization: <scheme> <credentials>`. */
|
|
80
|
+
export interface PushAuthentication {
|
|
81
|
+
scheme: string;
|
|
82
|
+
credentials: string;
|
|
83
|
+
}
|
|
84
|
+
/** A push notification config as a client gives it (A2A `TaskPushNotificationConfig`). */
|
|
85
|
+
export interface PushConfigInput {
|
|
86
|
+
/** The config's id within its task; the Runtime makes one when absent. */
|
|
87
|
+
id?: string;
|
|
88
|
+
url: string;
|
|
89
|
+
/** Sent back as `X-A2A-Notification-Token` with every push. */
|
|
90
|
+
token?: string;
|
|
91
|
+
authentication?: PushAuthentication;
|
|
92
|
+
}
|
|
93
|
+
/** A push notification config as the Runtime answers it. */
|
|
94
|
+
export interface TaskPushNotificationConfig {
|
|
95
|
+
id: string;
|
|
96
|
+
taskId: string;
|
|
97
|
+
url: string;
|
|
98
|
+
token?: string;
|
|
99
|
+
authentication?: PushAuthentication;
|
|
54
100
|
}
|
|
55
101
|
/** The standard JSON-RPC codes and the A2A codes (spec §5.4, §9.5). */
|
|
56
102
|
export declare const A2A_ERRORS: {
|
|
@@ -106,9 +152,13 @@ export declare class A2aError extends Error {
|
|
|
106
152
|
readonly metadata: Readonly<Record<string, string>>;
|
|
107
153
|
/** `google.rpc.BadRequest` field violations, for invalid parameters. */
|
|
108
154
|
readonly field?: string | undefined;
|
|
155
|
+
/** `ErrorInfo.reason` when it is more precise than the kind's, as `CONTEXT_BUSY`. */
|
|
156
|
+
readonly reason?: string | undefined;
|
|
109
157
|
constructor(kind: A2aErrorKind, message: string, metadata?: Readonly<Record<string, string>>,
|
|
110
158
|
/** `google.rpc.BadRequest` field violations, for invalid parameters. */
|
|
111
|
-
field?: string | undefined
|
|
159
|
+
field?: string | undefined,
|
|
160
|
+
/** `ErrorInfo.reason` when it is more precise than the kind's, as `CONTEXT_BUSY`. */
|
|
161
|
+
reason?: string | undefined);
|
|
112
162
|
}
|
|
113
163
|
export declare const a2aFail: (kind: A2aErrorKind, message: string, metadata?: Record<string, string>, field?: string) => never;
|
|
114
164
|
export type JsonRpcId = string | number | null;
|
|
@@ -136,9 +186,15 @@ export interface RawFile {
|
|
|
136
186
|
readonly mediaType?: string;
|
|
137
187
|
readonly filename?: string;
|
|
138
188
|
}
|
|
189
|
+
/** A file sent by URL: fetched by the Runtime (`guardedFetch`), then stored as a `raw` one is. */
|
|
190
|
+
export interface UrlFile {
|
|
191
|
+
readonly url: string;
|
|
192
|
+
readonly mediaType?: string;
|
|
193
|
+
readonly filename?: string;
|
|
194
|
+
}
|
|
139
195
|
/**
|
|
140
196
|
* A client's message reduced to what the Runtime runs: text, one JSON value, or text and files
|
|
141
|
-
* sent inline.
|
|
197
|
+
* sent inline or by URL.
|
|
142
198
|
*/
|
|
143
199
|
export type MessageInput = {
|
|
144
200
|
content: string;
|
|
@@ -147,7 +203,17 @@ export type MessageInput = {
|
|
|
147
203
|
} | {
|
|
148
204
|
files: readonly ({
|
|
149
205
|
text: string;
|
|
150
|
-
} | RawFile)[];
|
|
206
|
+
} | RawFile | UrlFile)[];
|
|
207
|
+
};
|
|
208
|
+
/** What a message to a busy context does (profile v2 §1): queued, or refused with `CONTEXT_BUSY`. */
|
|
209
|
+
export type Delivery = "queue" | "reject";
|
|
210
|
+
/** A caller's answer to an open interaction (`interactions/v1` on the reply message). */
|
|
211
|
+
export type InteractionAnswer = {
|
|
212
|
+
interactionId: string;
|
|
213
|
+
approved: boolean;
|
|
214
|
+
} | {
|
|
215
|
+
interactionId: string;
|
|
216
|
+
value: unknown;
|
|
151
217
|
};
|
|
152
218
|
export interface SendParams {
|
|
153
219
|
messageId: string;
|
|
@@ -156,8 +222,20 @@ export interface SendParams {
|
|
|
156
222
|
input: MessageInput;
|
|
157
223
|
returnImmediately: boolean;
|
|
158
224
|
historyLength?: number;
|
|
225
|
+
/** `configuration.taskPushNotificationConfig`: where this task's updates go. */
|
|
226
|
+
push?: PushConfigInput;
|
|
227
|
+
/** `metadata.delivery`; default `queue`. */
|
|
228
|
+
delivery: Delivery;
|
|
229
|
+
/** The answer the message carries under `interactions/v1`. */
|
|
230
|
+
answer?: InteractionAnswer;
|
|
159
231
|
}
|
|
160
232
|
export declare function parseHistoryLength(value: unknown, field: string): number | undefined;
|
|
233
|
+
/**
|
|
234
|
+
* A push notification config (A2A `TaskPushNotificationConfig`) at `field`: an http(s) `url`, an
|
|
235
|
+
* optional `id`, `token` and `authentication` (`scheme`, `credentials`). Its `taskId`, when
|
|
236
|
+
* present, is the caller's to check.
|
|
237
|
+
*/
|
|
238
|
+
export declare function parsePushConfig(value: unknown, field: string): PushConfigInput;
|
|
161
239
|
/** `SendMessage` params (spec §3.2.1), checked in the order a client can fix them. */
|
|
162
240
|
export declare function parseSendParams(params: Record<string, unknown>): SendParams;
|
|
163
241
|
/** `GetTask` and `CancelTask` params: the task id (and `historyLength` for `GetTask`). */
|
|
@@ -165,3 +243,21 @@ export declare function parseTaskParams(params: Record<string, unknown>): {
|
|
|
165
243
|
id: string;
|
|
166
244
|
historyLength?: number;
|
|
167
245
|
};
|
|
246
|
+
/** `CreateTaskPushNotificationConfig` params: a `TaskPushNotificationConfig` naming its task. */
|
|
247
|
+
export declare function parseCreatePushParams(params: Record<string, unknown>): {
|
|
248
|
+
taskId: string;
|
|
249
|
+
config: PushConfigInput;
|
|
250
|
+
};
|
|
251
|
+
/** `GetTaskPushNotificationConfig` and `DeleteTaskPushNotificationConfig` params. */
|
|
252
|
+
export declare function parsePushConfigRef(params: Record<string, unknown>): {
|
|
253
|
+
taskId: string;
|
|
254
|
+
id: string;
|
|
255
|
+
};
|
|
256
|
+
/** The most configs one page of `ListTaskPushNotificationConfigs` holds. */
|
|
257
|
+
export declare const PUSH_CONFIG_PAGE = 100;
|
|
258
|
+
/** `ListTaskPushNotificationConfigs` params: the task, and paging. */
|
|
259
|
+
export declare function parseListPushParams(params: Record<string, unknown>): {
|
|
260
|
+
taskId: string;
|
|
261
|
+
pageSize: number;
|
|
262
|
+
pageToken?: string;
|
|
263
|
+
};
|
package/dist/api/a2a/protocol.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A2A 1.0 over JSON-RPC: the wire types the Runtime answers with, the envelope, the error
|
|
3
|
-
* codes and the parameter checks (Host
|
|
4
|
-
* text, data and
|
|
5
|
-
*
|
|
3
|
+
* codes and the parameter checks (Host features `a2a-endpoint` and `a2a-profile-v2`). Only what
|
|
4
|
+
* the Runtime serves is typed: text, data, `raw` and `url` parts, tasks, their status and
|
|
5
|
+
* artifact updates (push), push configs, the card. Field names are the spec's ProtoJSON
|
|
6
|
+
* (camelCase, enum values such as `TASK_STATE_WORKING`). No Tenant state is read here.
|
|
6
7
|
*/
|
|
8
|
+
import { A2A_EXTENSIONS } from "./extensions.js";
|
|
7
9
|
/** The A2A version this endpoint speaks (`A2A-Version`, Major.Minor). */
|
|
8
10
|
export const A2A_VERSION = "1.0";
|
|
9
11
|
export const TERMINAL_STATES = new Set([
|
|
@@ -31,13 +33,17 @@ export class A2aError extends Error {
|
|
|
31
33
|
kind;
|
|
32
34
|
metadata;
|
|
33
35
|
field;
|
|
36
|
+
reason;
|
|
34
37
|
constructor(kind, message, metadata = {},
|
|
35
38
|
/** `google.rpc.BadRequest` field violations, for invalid parameters. */
|
|
36
|
-
field
|
|
39
|
+
field,
|
|
40
|
+
/** `ErrorInfo.reason` when it is more precise than the kind's, as `CONTEXT_BUSY`. */
|
|
41
|
+
reason) {
|
|
37
42
|
super(message);
|
|
38
43
|
this.kind = kind;
|
|
39
44
|
this.metadata = metadata;
|
|
40
45
|
this.field = field;
|
|
46
|
+
this.reason = reason;
|
|
41
47
|
this.name = "A2aError";
|
|
42
48
|
}
|
|
43
49
|
}
|
|
@@ -52,7 +58,7 @@ export function jsonRpcError(id, error) {
|
|
|
52
58
|
const data = [
|
|
53
59
|
{
|
|
54
60
|
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
|
|
55
|
-
reason,
|
|
61
|
+
reason: error.reason ?? reason,
|
|
56
62
|
domain: "a2a-protocol.org",
|
|
57
63
|
metadata: { ...error.metadata },
|
|
58
64
|
},
|
|
@@ -98,10 +104,30 @@ export function parseEnvelope(text) {
|
|
|
98
104
|
request: {
|
|
99
105
|
id: safeId,
|
|
100
106
|
method: body.method,
|
|
101
|
-
params: body.params ?? {},
|
|
107
|
+
params: protoJsonKeys(body.params ?? {}),
|
|
102
108
|
},
|
|
103
109
|
};
|
|
104
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Params with ProtoJSON's original field names (`task_id`, `return_immediately`) read as their
|
|
113
|
+
* lowerCamel JSON names, as ProtoJSON parsers must accept both. A part's `data` and any
|
|
114
|
+
* `metadata` are the caller's own values and keep their keys.
|
|
115
|
+
*/
|
|
116
|
+
function protoJsonKeys(value) {
|
|
117
|
+
if (Array.isArray(value))
|
|
118
|
+
return value.map(protoJsonKeys);
|
|
119
|
+
if (!isObject(value))
|
|
120
|
+
return value;
|
|
121
|
+
const out = {};
|
|
122
|
+
for (const [key, child] of Object.entries(value)) {
|
|
123
|
+
const name = key.includes("_") ? key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase()) : key;
|
|
124
|
+
// The lowerCamel name wins when a client sends both.
|
|
125
|
+
if (name !== key && Object.hasOwn(value, name))
|
|
126
|
+
continue;
|
|
127
|
+
out[name] = name === "data" || name === "metadata" ? child : protoJsonKeys(child);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
105
131
|
/** `A2A-Version` as Major.Minor; empty means 0.3 (spec §3.6.2). */
|
|
106
132
|
export function checkVersion(value) {
|
|
107
133
|
const trimmed = (value ?? "").trim();
|
|
@@ -142,10 +168,89 @@ function partKind(part, field) {
|
|
|
142
168
|
return a2aFail("invalidParams", `${field}.text must be a string`, {}, `${field}.text`);
|
|
143
169
|
if (kinds[0] === "raw" && typeof part.raw !== "string")
|
|
144
170
|
return a2aFail("invalidParams", `${field}.raw must be base64 bytes`, {}, `${field}.raw`);
|
|
171
|
+
if (kinds[0] === "url" && !isHttpUrl(part.url))
|
|
172
|
+
return a2aFail("invalidParams", `${field}.url must be an http or https URL`, {}, `${field}.url`);
|
|
145
173
|
for (const key of ["mediaType", "filename"])
|
|
146
174
|
if (part[key] !== undefined && typeof part[key] !== "string")
|
|
147
175
|
return a2aFail("invalidParams", `${field}.${key} must be a string`, {}, `${field}.${key}`);
|
|
148
|
-
return kinds[0]
|
|
176
|
+
return kinds[0];
|
|
177
|
+
}
|
|
178
|
+
function isHttpUrl(value) {
|
|
179
|
+
if (typeof value !== "string" || value.length > 2048)
|
|
180
|
+
return false;
|
|
181
|
+
try {
|
|
182
|
+
const url = new URL(value);
|
|
183
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** The longest push token and credentials kept: a header value, not a document. */
|
|
190
|
+
const MAX_SECRET = 1024;
|
|
191
|
+
/**
|
|
192
|
+
* A push notification config (A2A `TaskPushNotificationConfig`) at `field`: an http(s) `url`, an
|
|
193
|
+
* optional `id`, `token` and `authentication` (`scheme`, `credentials`). Its `taskId`, when
|
|
194
|
+
* present, is the caller's to check.
|
|
195
|
+
*/
|
|
196
|
+
export function parsePushConfig(value, field) {
|
|
197
|
+
if (!isObject(value))
|
|
198
|
+
return a2aFail("invalidParams", `${field} must be an object`, {}, field);
|
|
199
|
+
if (!isHttpUrl(value.url))
|
|
200
|
+
a2aFail("invalidParams", `${field}.url must be an http or https URL`, {}, `${field}.url`);
|
|
201
|
+
const id = value.id === undefined || value.id === "" ? undefined : checkId(value.id, `${field}.id`, true);
|
|
202
|
+
if (value.token !== undefined && (typeof value.token !== "string" || value.token.length > MAX_SECRET))
|
|
203
|
+
a2aFail("invalidParams", `${field}.token must be a string of at most ${MAX_SECRET} characters`, {}, `${field}.token`);
|
|
204
|
+
let authentication;
|
|
205
|
+
if (value.authentication !== undefined && value.authentication !== null) {
|
|
206
|
+
const auth = value.authentication;
|
|
207
|
+
if (!isObject(auth) ||
|
|
208
|
+
typeof auth.scheme !== "string" ||
|
|
209
|
+
!/^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/.test(auth.scheme) ||
|
|
210
|
+
typeof auth.credentials !== "string" ||
|
|
211
|
+
auth.credentials.length > MAX_SECRET ||
|
|
212
|
+
/[\r\n]/.test(auth.credentials))
|
|
213
|
+
a2aFail("invalidParams", `${field}.authentication needs an HTTP scheme and credentials`, {}, `${field}.authentication`);
|
|
214
|
+
authentication = {
|
|
215
|
+
scheme: auth.scheme,
|
|
216
|
+
credentials: auth.credentials,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
url: value.url,
|
|
221
|
+
...(id !== undefined ? { id } : {}),
|
|
222
|
+
...(typeof value.token === "string" && value.token !== "" ? { token: value.token } : {}),
|
|
223
|
+
...(authentication ? { authentication } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** `metadata.delivery`: `queue` (the default) or `reject`. */
|
|
227
|
+
function parseDelivery(metadata) {
|
|
228
|
+
const delivery = metadata.delivery;
|
|
229
|
+
if (delivery === undefined || delivery === null)
|
|
230
|
+
return "queue";
|
|
231
|
+
if (delivery !== "queue" && delivery !== "reject")
|
|
232
|
+
return a2aFail("invalidParams", 'message.metadata.delivery must be "queue" or "reject"', {}, "message.metadata.delivery");
|
|
233
|
+
return delivery;
|
|
234
|
+
}
|
|
235
|
+
/** The answer under `interactions/v1`, when the message carries one. */
|
|
236
|
+
function parseAnswer(metadata) {
|
|
237
|
+
const field = `message.metadata["${A2A_EXTENSIONS.interactions}"]`;
|
|
238
|
+
const raw = metadata[A2A_EXTENSIONS.interactions];
|
|
239
|
+
if (raw === undefined || raw === null)
|
|
240
|
+
return undefined;
|
|
241
|
+
if (!isObject(raw))
|
|
242
|
+
return a2aFail("invalidParams", `${field} must be an object`, {}, field);
|
|
243
|
+
const interactionId = checkId(raw.interactionId, `${field}.interactionId`, true);
|
|
244
|
+
if (raw.approved !== undefined) {
|
|
245
|
+
if (typeof raw.approved !== "boolean")
|
|
246
|
+
a2aFail("invalidParams", `${field}.approved must be a boolean`, {}, `${field}.approved`);
|
|
247
|
+
if (raw.value !== undefined)
|
|
248
|
+
a2aFail("invalidParams", `${field} takes approved or value, not both`, {}, field);
|
|
249
|
+
return { interactionId, approved: raw.approved };
|
|
250
|
+
}
|
|
251
|
+
if (!("value" in raw))
|
|
252
|
+
return a2aFail("invalidParams", `${field} needs approved or value`, {}, field);
|
|
253
|
+
return { interactionId, value: raw.value };
|
|
149
254
|
}
|
|
150
255
|
/** `SendMessage` params (spec §3.2.1), checked in the order a client can fix them. */
|
|
151
256
|
export function parseSendParams(params) {
|
|
@@ -161,12 +266,19 @@ export function parseSendParams(params) {
|
|
|
161
266
|
a2aFail("invalidParams", "At least one part is required", {}, "message.parts");
|
|
162
267
|
const parts = message.parts;
|
|
163
268
|
const kinds = parts.map((part, index) => partKind(part, `message.parts[${index}]`));
|
|
269
|
+
if (message.metadata !== undefined && message.metadata !== null && !isObject(message.metadata))
|
|
270
|
+
a2aFail("invalidParams", "message.metadata must be an object", {}, "message.metadata");
|
|
271
|
+
const metadata = (message.metadata ?? {});
|
|
272
|
+
// On the message (the profile's place), or on the request's own metadata.
|
|
273
|
+
const delivery = parseDelivery(metadata.delivery === undefined && isObject(params.metadata) ? params.metadata : metadata);
|
|
274
|
+
const answer = parseAnswer(metadata);
|
|
164
275
|
const configuration = params.configuration;
|
|
165
|
-
if (configuration !== undefined && !isObject(configuration))
|
|
276
|
+
if (configuration !== undefined && configuration !== null && !isObject(configuration))
|
|
166
277
|
a2aFail("invalidParams", "configuration must be an object", {}, "configuration");
|
|
167
278
|
const config = (configuration ?? {});
|
|
168
|
-
|
|
169
|
-
|
|
279
|
+
const push = config.taskPushNotificationConfig === undefined || config.taskPushNotificationConfig === null
|
|
280
|
+
? undefined
|
|
281
|
+
: parsePushConfig(config.taskPushNotificationConfig, "configuration.taskPushNotificationConfig");
|
|
170
282
|
if (config.returnImmediately !== undefined && typeof config.returnImmediately !== "boolean")
|
|
171
283
|
a2aFail("invalidParams", "configuration.returnImmediately must be a boolean", {}, "configuration.returnImmediately");
|
|
172
284
|
const historyLength = parseHistoryLength(config.historyLength, "configuration.historyLength");
|
|
@@ -175,21 +287,24 @@ export function parseSendParams(params) {
|
|
|
175
287
|
input = { content: parts.map((part) => part.text).join("\n") };
|
|
176
288
|
else if (kinds.length === 1 && kinds[0] === "data")
|
|
177
289
|
input = { data: parts[0].data };
|
|
178
|
-
else if (kinds.includes("url"))
|
|
179
|
-
return a2aFail("contentTypeNotSupported", "File URLs are not fetched yet; send the bytes inline as raw", { accepted: "raw" });
|
|
180
290
|
else if (kinds.includes("data"))
|
|
181
291
|
return a2aFail("contentTypeNotSupported", "This agent accepts text and file parts, or a single data part", { accepted: "text/plain,application/json" });
|
|
182
292
|
else
|
|
183
293
|
input = {
|
|
184
|
-
files: parts.map((part, index) =>
|
|
185
|
-
|
|
186
|
-
: {
|
|
187
|
-
raw: part.raw,
|
|
294
|
+
files: parts.map((part, index) => {
|
|
295
|
+
const named = {
|
|
188
296
|
...(part.mediaType !== undefined ? { mediaType: part.mediaType } : {}),
|
|
189
297
|
...(part.filename !== undefined ? { filename: part.filename } : {}),
|
|
190
|
-
}
|
|
298
|
+
};
|
|
299
|
+
return kinds[index] === "text"
|
|
300
|
+
? { text: part.text }
|
|
301
|
+
: kinds[index] === "url"
|
|
302
|
+
? { url: part.url, ...named }
|
|
303
|
+
: { raw: part.raw, ...named };
|
|
304
|
+
}),
|
|
191
305
|
};
|
|
192
|
-
|
|
306
|
+
// An answer may carry its value only in metadata: its parts are then what a person reads.
|
|
307
|
+
if ("content" in input && input.content.trim() === "" && answer === undefined)
|
|
193
308
|
a2aFail("invalidParams", "The message has no text", {}, "message.parts");
|
|
194
309
|
return {
|
|
195
310
|
messageId,
|
|
@@ -198,6 +313,9 @@ export function parseSendParams(params) {
|
|
|
198
313
|
input,
|
|
199
314
|
returnImmediately: config.returnImmediately === true,
|
|
200
315
|
...(historyLength !== undefined ? { historyLength } : {}),
|
|
316
|
+
...(push ? { push } : {}),
|
|
317
|
+
delivery,
|
|
318
|
+
...(answer ? { answer } : {}),
|
|
201
319
|
};
|
|
202
320
|
}
|
|
203
321
|
/** `GetTask` and `CancelTask` params: the task id (and `historyLength` for `GetTask`). */
|
|
@@ -207,3 +325,36 @@ export function parseTaskParams(params) {
|
|
|
207
325
|
const historyLength = parseHistoryLength(params.historyLength, "historyLength");
|
|
208
326
|
return { id: params.id, ...(historyLength !== undefined ? { historyLength } : {}) };
|
|
209
327
|
}
|
|
328
|
+
/** `taskId` of a push config method's params. */
|
|
329
|
+
function pushTaskId(params) {
|
|
330
|
+
if (typeof params.taskId !== "string" || params.taskId === "")
|
|
331
|
+
return a2aFail("invalidParams", "taskId is required", {}, "taskId");
|
|
332
|
+
return params.taskId;
|
|
333
|
+
}
|
|
334
|
+
/** `CreateTaskPushNotificationConfig` params: a `TaskPushNotificationConfig` naming its task. */
|
|
335
|
+
export function parseCreatePushParams(params) {
|
|
336
|
+
const taskId = pushTaskId(params);
|
|
337
|
+
// A2A 1.0 sends the config itself; an earlier draft nested it under `config`.
|
|
338
|
+
const config = isObject(params.config) ? { ...params.config, ...(params.id ? { id: params.id } : {}) } : params;
|
|
339
|
+
return { taskId, config: parsePushConfig(config, isObject(params.config) ? "config" : "params") };
|
|
340
|
+
}
|
|
341
|
+
/** `GetTaskPushNotificationConfig` and `DeleteTaskPushNotificationConfig` params. */
|
|
342
|
+
export function parsePushConfigRef(params) {
|
|
343
|
+
const taskId = pushTaskId(params);
|
|
344
|
+
const id = checkId(params.id, "id", true);
|
|
345
|
+
return { taskId, id };
|
|
346
|
+
}
|
|
347
|
+
/** The most configs one page of `ListTaskPushNotificationConfigs` holds. */
|
|
348
|
+
export const PUSH_CONFIG_PAGE = 100;
|
|
349
|
+
/** `ListTaskPushNotificationConfigs` params: the task, and paging. */
|
|
350
|
+
export function parseListPushParams(params) {
|
|
351
|
+
const taskId = pushTaskId(params);
|
|
352
|
+
const size = params.pageSize;
|
|
353
|
+
if (size !== undefined && size !== null && size !== 0 && (typeof size !== "number" || !Number.isInteger(size) || size < 1))
|
|
354
|
+
a2aFail("invalidParams", "pageSize must be a positive integer", {}, "pageSize");
|
|
355
|
+
const pageSize = Math.min(typeof size === "number" && size > 0 ? size : PUSH_CONFIG_PAGE, PUSH_CONFIG_PAGE);
|
|
356
|
+
const token = params.pageToken;
|
|
357
|
+
if (token !== undefined && token !== null && token !== "" && typeof token !== "string")
|
|
358
|
+
a2aFail("invalidParams", "pageToken must be a string", {}, "pageToken");
|
|
359
|
+
return { taskId, pageSize, ...(typeof token === "string" && token !== "" ? { pageToken: token } : {}) };
|
|
360
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { A2aPushConfigRow, OutboxPush, Tx } from "../../store/types.js";
|
|
2
|
+
import type { TenantContext } from "../../tenant/context.js";
|
|
3
|
+
import { type OutboundPolicy } from "../../tenant/outbound.js";
|
|
4
|
+
import { type PushConfigInput, type TaskPushNotificationConfig } from "./protocol.js";
|
|
5
|
+
/** The most push configs one task keeps. A constant of the profile, not a setting. */
|
|
6
|
+
export declare const MAX_PUSH_CONFIGS = 10;
|
|
7
|
+
/** Whether this Runtime serves push notifications: it has a secret to sign them with. */
|
|
8
|
+
export declare function pushEnabled(ctx: Pick<TenantContext, "config">): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The address policy a push, or a fetched file `url` part, is sent under: the Host's delivery
|
|
11
|
+
* policy, with private addresses refused unless `url`'s `host:port` is in `a2a.push.allowHosts`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function pushPolicy(ctx: Pick<TenantContext, "config">, target: string): OutboundPolicy;
|
|
14
|
+
/** Why the Runtime will not send to `target`, checked before a config is stored; else undefined. */
|
|
15
|
+
export declare function pushRefusal(ctx: Pick<TenantContext, "config">, target: string): string | undefined;
|
|
16
|
+
/** A config as A2A answers it. */
|
|
17
|
+
export declare function configView(row: A2aPushConfigRow): TaskPushNotificationConfig;
|
|
18
|
+
/** Where a config starts: the turn it follows and the first event it reads. */
|
|
19
|
+
export interface PushStart {
|
|
20
|
+
readonly sessionId: string;
|
|
21
|
+
readonly contextId: string;
|
|
22
|
+
readonly taskId: string;
|
|
23
|
+
/** The turn, or a queued message's id until a turn takes it. */
|
|
24
|
+
readonly turnId: string;
|
|
25
|
+
readonly nextSeq: number;
|
|
26
|
+
readonly traceId?: string;
|
|
27
|
+
/** The task already ended: the config is stored, and nothing is pushed. */
|
|
28
|
+
readonly done?: boolean;
|
|
29
|
+
readonly baseUrl: string;
|
|
30
|
+
/** The extensions the caller activated; none named means all of them. */
|
|
31
|
+
readonly extensions: ReadonlySet<string>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Stores `config` for a task (replacing one with its id), in `t`; the pump sends its updates.
|
|
35
|
+
* A URL the Runtime will not call is `InvalidParamsError`; past `MAX_PUSH_CONFIGS` too.
|
|
36
|
+
*/
|
|
37
|
+
export declare function savePushConfig(ctx: Pick<TenantContext, "config">, t: Tx, start: PushStart, config: PushConfigInput, configId: string): Promise<TaskPushNotificationConfig>;
|
|
38
|
+
/**
|
|
39
|
+
* Pumps session `sessionId` on this process: at once, or again after the pump running now, so
|
|
40
|
+
* one process never pumps a session twice at the same time. Never rejects.
|
|
41
|
+
*/
|
|
42
|
+
export declare function kickPushes(ctx: TenantContext, sessionId: string): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Translates the events of session `sessionId` its configs have not read into push rows: reads
|
|
45
|
+
* the record from the lowest `nextSeq`, then, under the configs' locks, writes each config's
|
|
46
|
+
* updates and moves it on. A config stored behind what was read waits for the next pump.
|
|
47
|
+
*/
|
|
48
|
+
export declare function pumpSession(ctx: TenantContext, sessionId: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* The Tenant sweep's push step: pumps every session with a config still following its task
|
|
51
|
+
* (commits on other processes reach them here), and deletes configs done longer than the Host
|
|
52
|
+
* tuning's `a2a.pushConfigRetentionMs`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function pumpPushes(ctx: TenantContext): Promise<void>;
|
|
55
|
+
/** A push the receiver did not take: the sweep sends it again. */
|
|
56
|
+
export declare class PushFailed extends Error {
|
|
57
|
+
constructor(message: string);
|
|
58
|
+
}
|
|
59
|
+
/** The headers of one push attempt: Standard Webhooks, the config's token and authentication. */
|
|
60
|
+
export declare function pushHeaders(secret: string, push: Pick<OutboxPush, "id" | "body">, config: {
|
|
61
|
+
token?: string | null;
|
|
62
|
+
authentication?: unknown;
|
|
63
|
+
}, now?: number): Record<string, string>;
|
|
64
|
+
/**
|
|
65
|
+
* Sends one push row (the Tenant's `send` for outbox kind `push`). Resolves `true` once it is
|
|
66
|
+
* done with: delivered (2xx), its config deleted, the receiver gone (`410`), or its URL refused
|
|
67
|
+
* by policy. Rejects with `PushFailed` (or the network's error) when it should be sent again.
|
|
68
|
+
*/
|
|
69
|
+
export declare function deliverPush(ctx: TenantContext, push: OutboxPush): Promise<boolean>;
|
|
70
|
+
/**
|
|
71
|
+
* What the sweep does with a push that failed `attempts` times and was written at `createdAt`:
|
|
72
|
+
* try again after the backoff, or drop it once it would be past `a2a.pushMaxAgeMs`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function pushRetry(attempts: number, createdAt: string, now?: number): {
|
|
75
|
+
retryInMs: number;
|
|
76
|
+
} | "drop";
|