@nylorun/runtime 0.36.1-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.
Files changed (59) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +10 -0
  3. package/dist/api/a2a/card.d.ts +10 -4
  4. package/dist/api/a2a/card.js +18 -6
  5. package/dist/api/a2a/endpoint.d.ts +5 -2
  6. package/dist/api/a2a/endpoint.js +31 -9
  7. package/dist/api/a2a/extensions.d.ts +41 -0
  8. package/dist/api/a2a/extensions.js +92 -0
  9. package/dist/api/a2a/list.d.ts +60 -0
  10. package/dist/api/a2a/list.js +169 -0
  11. package/dist/api/a2a/protocol.d.ts +105 -9
  12. package/dist/api/a2a/protocol.js +169 -18
  13. package/dist/api/a2a/push.d.ts +76 -0
  14. package/dist/api/a2a/push.js +305 -0
  15. package/dist/api/a2a/routes.d.ts +12 -6
  16. package/dist/api/a2a/routes.js +409 -112
  17. package/dist/api/a2a/tasks.d.ts +72 -4
  18. package/dist/api/a2a/tasks.js +305 -27
  19. package/dist/api/components.d.ts +532 -0
  20. package/dist/api/components.js +3 -1
  21. package/dist/api/http/app.js +2 -0
  22. package/dist/api/http/routes/artifacts.js +19 -0
  23. package/dist/api/http/routes/turns.d.ts +18 -0
  24. package/dist/api/http/routes/turns.js +67 -0
  25. package/dist/api/inbound-files.d.ts +12 -1
  26. package/dist/api/inbound-files.js +44 -0
  27. package/dist/api/openapi.js +7 -2
  28. package/dist/host/config-for.d.ts +2 -0
  29. package/dist/host/config-for.js +1 -0
  30. package/dist/host/main.js +1 -0
  31. package/dist/host/stack-config.d.ts +16 -0
  32. package/dist/host/stack-config.js +30 -0
  33. package/dist/management-openapi.json +5 -2
  34. package/dist/openapi.json +1485 -738
  35. package/dist/reads/types.d.ts +19 -0
  36. package/dist/store/postgres/drizzle/0039_a2a_push.sql +32 -0
  37. package/dist/store/postgres/drizzle/0040_turn_index.sql +23 -0
  38. package/dist/store/postgres/drizzle/0041_turn_index_backfill.sql +88 -0
  39. package/dist/store/postgres/drizzle/meta/_journal.json +21 -0
  40. package/dist/store/postgres/reads.js +41 -1
  41. package/dist/store/postgres/schema.d.ts +1296 -534
  42. package/dist/store/postgres/schema.js +92 -0
  43. package/dist/store/postgres/store.js +133 -3
  44. package/dist/store/postgres/turns.d.ts +62 -0
  45. package/dist/store/postgres/turns.js +153 -0
  46. package/dist/store/types.d.ts +73 -3
  47. package/dist/tenant/runtime.js +7 -5
  48. package/dist/tenant/session-model.d.ts +11 -1
  49. package/dist/tenant/session-model.js +34 -5
  50. package/dist/tenant/sessions.d.ts +2 -1
  51. package/dist/tenant/sessions.js +8 -2
  52. package/dist/tenant/sweep.d.ts +3 -0
  53. package/dist/tenant/sweep.js +39 -11
  54. package/dist/tenant/types.d.ts +23 -0
  55. package/dist/tuning.d.ts +9 -0
  56. package/dist/tuning.js +11 -0
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. 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
+ };
@@ -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 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.
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] === "raw" ? "raw" : kinds[0] === "url" ? "url" : 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
- if (config.taskPushNotificationConfig !== undefined && config.taskPushNotificationConfig !== null)
169
- a2aFail("pushNotSupported", "Push notifications are not supported");
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) => kinds[index] === "text"
185
- ? { text: part.text }
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
- if ("content" in input && input.content.trim() === "")
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";