@nshipster/sosumi 1.0.8 → 1.1.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.
package/src/lib/a2a.ts CHANGED
@@ -1,10 +1,27 @@
1
+ import { z } from "zod"
2
+ import { resolveFetchEndpoint, resolveSearchEndpoint } from "./cli-endpoints"
1
3
  import { MCP_SERVER_INFO, TOOL_DEFINITIONS } from "./mcp"
2
4
 
3
5
  /**
4
6
  * The A2A protocol version each interface exposes, as `major.minor`.
5
7
  * See https://a2a-protocol.org/latest/specification/
6
8
  */
7
- const A2A_PROTOCOL_VERSION = "0.3"
9
+ export const A2A_PROTOCOL_VERSION = "1.0"
10
+
11
+ /** The HTTP+JSON media type registered by the A2A specification. */
12
+ export const A2A_MEDIA_TYPE = "application/a2a+json"
13
+
14
+ /** The synchronous HTTP+JSON operation exposed by Sosumi. */
15
+ export const A2A_MESSAGE_PATH = "/message:send"
16
+
17
+ /** The media type returned by every Sosumi A2A skill. */
18
+ export const A2A_OUTPUT_MEDIA_TYPE = "text/markdown"
19
+
20
+ /** Maximum wire size accepted for an unauthenticated A2A request body. */
21
+ export const A2A_MAX_REQUEST_BYTES = 32 * 1024
22
+
23
+ /** Maximum capability output that can be safely wrapped in an A2A JSON response. */
24
+ export const A2A_MAX_RESPONSE_BYTES = 8 * 1024 * 1024
8
25
 
9
26
  /**
10
27
  * The transport binding advertised for the agent's interface.
@@ -43,8 +60,6 @@ interface AgentInterface {
43
60
  protocolVersion: string
44
61
  /** Transport binding. Named `protocolBinding` by the current A2A proto schema. */
45
62
  protocolBinding: string
46
- /** Alias of `protocolBinding` for clients reading the published JSON schema's `transport`. */
47
- transport: string
48
63
  }
49
64
 
50
65
  /**
@@ -57,7 +72,6 @@ export function buildAgentCard(origin: string) {
57
72
  url: origin,
58
73
  protocolVersion: A2A_PROTOCOL_VERSION,
59
74
  protocolBinding: TRANSPORT,
60
- transport: TRANSPORT,
61
75
  }
62
76
 
63
77
  const skills = Object.values(TOOL_DEFINITIONS).map((def) => ({
@@ -84,7 +98,664 @@ export function buildAgentCard(origin: string) {
84
98
  pushNotifications: false,
85
99
  },
86
100
  defaultInputModes: ["text/plain"],
87
- defaultOutputModes: ["text/markdown", "text/plain"],
101
+ defaultOutputModes: [A2A_OUTPUT_MEDIA_TYPE],
88
102
  skills,
89
103
  }
90
104
  }
105
+
106
+ type A2AHttpStatus = 400 | 403 | 404 | 413 | 500 | 503
107
+
108
+ const INT32_MAX = 2_147_483_647
109
+ const metadataSchema = z.record(z.unknown())
110
+
111
+ const nonNegativeInt32Schema = z.preprocess((value) => {
112
+ if (typeof value === "string" && /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(value)) {
113
+ return Number(value)
114
+ }
115
+ return value
116
+ }, z.number().int().nonnegative().max(INT32_MAX))
117
+
118
+ const authenticationInfoSchema = z
119
+ .object({
120
+ scheme: z.string().min(1),
121
+ credentials: z.string().optional(),
122
+ })
123
+ .strict()
124
+
125
+ const taskPushNotificationConfigSchema = z
126
+ .object({
127
+ tenant: z.string().optional(),
128
+ id: z.string().optional(),
129
+ taskId: z.string().optional(),
130
+ url: z.string().url(),
131
+ token: z.string().optional(),
132
+ authentication: authenticationInfoSchema.optional(),
133
+ })
134
+ .strict()
135
+
136
+ const sendMessageConfigurationSchema = z
137
+ .object({
138
+ acceptedOutputModes: z.array(z.string()).optional(),
139
+ taskPushNotificationConfig: taskPushNotificationConfigSchema.optional(),
140
+ historyLength: nonNegativeInt32Schema.optional(),
141
+ returnImmediately: z.boolean().optional(),
142
+ })
143
+ .strict()
144
+
145
+ const partSchema = z
146
+ .object({
147
+ text: z.string().optional(),
148
+ raw: z.string().optional(),
149
+ url: z.string().optional(),
150
+ data: z.unknown().optional(),
151
+ metadata: metadataSchema.optional(),
152
+ filename: z.string().optional(),
153
+ mediaType: z.string().min(1).optional(),
154
+ })
155
+ .strict()
156
+
157
+ const messageSchema = z
158
+ .object({
159
+ messageId: z.string().min(1),
160
+ contextId: z.string().min(1).optional(),
161
+ taskId: z.string().min(1).optional(),
162
+ role: z.enum(["ROLE_UNSPECIFIED", "ROLE_USER", "ROLE_AGENT"]),
163
+ parts: z.array(partSchema).min(1),
164
+ metadata: metadataSchema.optional(),
165
+ extensions: z.array(z.string().min(1)).optional(),
166
+ referenceTaskIds: z.array(z.string().min(1)).optional(),
167
+ })
168
+ .strict()
169
+
170
+ const sendMessageRequestSchema = z
171
+ .object({
172
+ tenant: z.string().optional(),
173
+ message: messageSchema,
174
+ configuration: sendMessageConfigurationSchema.optional(),
175
+ metadata: metadataSchema.optional(),
176
+ })
177
+ .strict()
178
+
179
+ const LIST_TASK_QUERY_FIELDS = new Set([
180
+ "A2A-Version",
181
+ "tenant",
182
+ "contextId",
183
+ "status",
184
+ "pageSize",
185
+ "pageToken",
186
+ "historyLength",
187
+ "statusTimestampAfter",
188
+ "includeArtifacts",
189
+ ])
190
+
191
+ const TASK_STATES = new Set([
192
+ "TASK_STATE_SUBMITTED",
193
+ "TASK_STATE_WORKING",
194
+ "TASK_STATE_COMPLETED",
195
+ "TASK_STATE_FAILED",
196
+ "TASK_STATE_CANCELED",
197
+ "TASK_STATE_INPUT_REQUIRED",
198
+ "TASK_STATE_REJECTED",
199
+ "TASK_STATE_AUTH_REQUIRED",
200
+ ])
201
+
202
+ interface ParsedA2AMessage {
203
+ contextId?: string
204
+ prompt: string
205
+ outputMode: typeof A2A_OUTPUT_MEDIA_TYPE
206
+ }
207
+
208
+ export interface A2AMessageResponse {
209
+ message: {
210
+ messageId: string
211
+ contextId: string
212
+ role: "ROLE_AGENT"
213
+ parts: Array<{
214
+ text: string
215
+ mediaType: typeof A2A_OUTPUT_MEDIA_TYPE
216
+ }>
217
+ }
218
+ }
219
+
220
+ export interface A2AErrorBody {
221
+ error: {
222
+ code: A2AHttpStatus
223
+ status: string
224
+ message: string
225
+ details: Array<{
226
+ "@type": "type.googleapis.com/google.rpc.ErrorInfo"
227
+ reason: string
228
+ domain: "a2a-protocol.org"
229
+ metadata?: Record<string, string>
230
+ }>
231
+ }
232
+ }
233
+
234
+ /** A protocol-aware error that can be serialized as google.rpc.Status JSON. */
235
+ export class A2AError extends Error {
236
+ constructor(
237
+ readonly statusCode: A2AHttpStatus,
238
+ readonly status: string,
239
+ readonly reason: string | null,
240
+ message: string,
241
+ readonly metadata?: Record<string, string>,
242
+ ) {
243
+ super(message)
244
+ this.name = "A2AError"
245
+ }
246
+ }
247
+
248
+ /** Validate the transport headers required by the advertised A2A 1.0 interface. */
249
+ export function validateA2ARequestHeaders(
250
+ contentType: string | null,
251
+ requestedVersion: string | null,
252
+ ): void {
253
+ if (contentType) {
254
+ const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase()
255
+ if (mediaType !== "application/json" && mediaType !== A2A_MEDIA_TYPE) {
256
+ throw new A2AError(
257
+ 400,
258
+ "INVALID_ARGUMENT",
259
+ "CONTENT_TYPE_NOT_SUPPORTED",
260
+ `Unsupported Content-Type "${contentType}"; expected application/json or ${A2A_MEDIA_TYPE}.`,
261
+ )
262
+ }
263
+ }
264
+
265
+ // Per A2A 1.0, an omitted version header means the legacy 0.3 protocol.
266
+ const version = requestedVersion?.trim() || "0.3"
267
+ if (version !== A2A_PROTOCOL_VERSION) {
268
+ throw new A2AError(
269
+ 400,
270
+ "FAILED_PRECONDITION",
271
+ "VERSION_NOT_SUPPORTED",
272
+ `The requested A2A protocol version "${version}" is not supported.`,
273
+ { requestedVersion: version, supportedVersions: A2A_PROTOCOL_VERSION },
274
+ )
275
+ }
276
+ }
277
+
278
+ /** Convert an execution or validation failure to the A2A HTTP error envelope. */
279
+ export function toA2AErrorResponse(error: unknown): {
280
+ statusCode: A2AHttpStatus
281
+ body: A2AErrorBody
282
+ } {
283
+ const protocolError =
284
+ error instanceof A2AError
285
+ ? error
286
+ : new A2AError(500, "INTERNAL", null, "The agent could not complete the request.")
287
+
288
+ return {
289
+ statusCode: protocolError.statusCode,
290
+ body: {
291
+ error: {
292
+ code: protocolError.statusCode,
293
+ status: protocolError.status,
294
+ message: protocolError.message,
295
+ details: protocolError.reason
296
+ ? [
297
+ {
298
+ "@type": "type.googleapis.com/google.rpc.ErrorInfo",
299
+ reason: protocolError.reason,
300
+ domain: "a2a-protocol.org",
301
+ ...(protocolError.metadata ? { metadata: protocolError.metadata } : {}),
302
+ },
303
+ ]
304
+ : [],
305
+ },
306
+ },
307
+ }
308
+ }
309
+
310
+ /** Serialize a failure with the response headers required by the HTTP+JSON binding. */
311
+ export function createA2AErrorResponse(error: unknown): Response {
312
+ const failure = toA2AErrorResponse(error)
313
+ return new Response(JSON.stringify(failure.body), {
314
+ status: failure.statusCode,
315
+ headers: {
316
+ "Content-Type": A2A_MEDIA_TYPE,
317
+ "Cache-Control": "no-store",
318
+ },
319
+ })
320
+ }
321
+
322
+ /** Read and parse a JSON request without buffering more than the A2A body limit. */
323
+ export async function readA2AJsonBody(request: Request): Promise<unknown> {
324
+ const contentLength = request.headers.get("Content-Length")
325
+ if (contentLength !== null) {
326
+ const declaredBytes = Number(contentLength)
327
+ if (!Number.isSafeInteger(declaredBytes) || declaredBytes < 0) {
328
+ throw malformed("Content-Length must be a non-negative integer.")
329
+ }
330
+ if (declaredBytes > A2A_MAX_REQUEST_BYTES) {
331
+ throw requestTooLarge()
332
+ }
333
+ }
334
+
335
+ if (!request.body) {
336
+ throw malformed("A JSON payload is required.")
337
+ }
338
+
339
+ const bytes = await readLimitedBody(request.body, A2A_MAX_REQUEST_BYTES, requestTooLarge, () =>
340
+ malformed("The JSON payload could not be read."),
341
+ )
342
+
343
+ try {
344
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes))
345
+ } catch {
346
+ throw malformed("Invalid JSON payload.")
347
+ }
348
+ }
349
+
350
+ /** Read a capability response without buffering an unbounded document. */
351
+ export async function readA2ACapabilityText(response: Response): Promise<string> {
352
+ const contentLength = response.headers.get("Content-Length")
353
+ if (contentLength !== null) {
354
+ const declaredBytes = Number(contentLength)
355
+ if (Number.isSafeInteger(declaredBytes) && declaredBytes > A2A_MAX_RESPONSE_BYTES) {
356
+ throw capabilityResponseTooLarge()
357
+ }
358
+ }
359
+
360
+ if (!response.body) {
361
+ return ""
362
+ }
363
+
364
+ const bytes = await readLimitedBody(
365
+ response.body,
366
+ A2A_MAX_RESPONSE_BYTES,
367
+ capabilityResponseTooLarge,
368
+ () => new A2AError(503, "UNAVAILABLE", null, "The capability response could not be read."),
369
+ )
370
+ try {
371
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes)
372
+ } catch {
373
+ throw new A2AError(503, "UNAVAILABLE", null, "The capability returned invalid UTF-8.")
374
+ }
375
+ }
376
+
377
+ /** Return the canonical error for an operation against unknown stateless task data. */
378
+ export function taskNotFound(taskId: string): A2AError {
379
+ return new A2AError(
380
+ 404,
381
+ "NOT_FOUND",
382
+ "TASK_NOT_FOUND",
383
+ `Task "${taskId}" was not created by this stateless agent.`,
384
+ { taskId },
385
+ )
386
+ }
387
+
388
+ /** Return the canonical error for an operation disabled by the Agent Card. */
389
+ export function unsupportedOperation(operation: string): A2AError {
390
+ return new A2AError(
391
+ 400,
392
+ "FAILED_PRECONDITION",
393
+ "UNSUPPORTED_OPERATION",
394
+ `${operation} is not supported by this stateless agent.`,
395
+ )
396
+ }
397
+
398
+ /** Return the canonical error for push operations disabled by the Agent Card. */
399
+ export function pushNotificationsNotSupported(): A2AError {
400
+ return new A2AError(
401
+ 400,
402
+ "FAILED_PRECONDITION",
403
+ "PUSH_NOTIFICATION_NOT_SUPPORTED",
404
+ "Sosumi does not support A2A push notifications.",
405
+ )
406
+ }
407
+
408
+ /** Validate a ListTasks query and return the page size used by the empty stateless result. */
409
+ export function parseA2AListTasksQuery(query: URLSearchParams): number {
410
+ for (const key of query.keys()) {
411
+ if (!LIST_TASK_QUERY_FIELDS.has(key)) {
412
+ throw malformed(`Unsupported ListTasks query field "${key}".`)
413
+ }
414
+ if (query.getAll(key).length > 1) {
415
+ throw malformed(`ListTasks query field "${key}" must not be repeated.`)
416
+ }
417
+ }
418
+
419
+ if (query.get("tenant")) {
420
+ throw malformed("tenant is not supported by this interface.")
421
+ }
422
+
423
+ const status = query.get("status")
424
+ if (status !== null && !TASK_STATES.has(status)) {
425
+ throw malformed(`Invalid task status "${status}".`)
426
+ }
427
+
428
+ parseIntegerQuery(query.get("historyLength"), "historyLength", 0, INT32_MAX)
429
+
430
+ const timestamp = query.get("statusTimestampAfter")
431
+ if (timestamp !== null && !isValidProtobufTimestamp(timestamp)) {
432
+ throw malformed("statusTimestampAfter must be a valid RFC 3339 protobuf Timestamp.")
433
+ }
434
+
435
+ const includeArtifacts = query.get("includeArtifacts")
436
+ if (includeArtifacts !== null && includeArtifacts !== "true" && includeArtifacts !== "false") {
437
+ throw malformed('includeArtifacts must be "true" or "false".')
438
+ }
439
+
440
+ return parseIntegerQuery(query.get("pageSize"), "pageSize", 1, 100) ?? 50
441
+ }
442
+
443
+ /**
444
+ * Execute a synchronous A2A message using one of Sosumi's existing HTTP
445
+ * capabilities and return a direct Message response (no persistent Task).
446
+ */
447
+ export async function handleA2AMessage(
448
+ input: unknown,
449
+ invoke: (endpoint: string, outputMode: typeof A2A_OUTPUT_MEDIA_TYPE) => Promise<string>,
450
+ ): Promise<A2AMessageResponse> {
451
+ const request = parseA2AMessage(input)
452
+ const endpoint = resolveA2AEndpoint(request.prompt)
453
+ const text = await invoke(endpoint, request.outputMode)
454
+
455
+ return {
456
+ message: {
457
+ messageId: crypto.randomUUID(),
458
+ contextId: request.contextId ?? crypto.randomUUID(),
459
+ role: "ROLE_AGENT",
460
+ parts: [{ text, mediaType: request.outputMode }],
461
+ },
462
+ }
463
+ }
464
+
465
+ /** Infer the existing Sosumi capability endpoint described by a text prompt. */
466
+ export function resolveA2AEndpoint(prompt: string): string {
467
+ const fetchTarget = extractFetchTarget(prompt)
468
+ if (fetchTarget) {
469
+ try {
470
+ const endpoint = resolveFetchEndpoint(fetchTarget)
471
+ if (endpoint.startsWith("/external/")) {
472
+ const target = new URL(endpoint.slice("/external/".length))
473
+ target.hash = ""
474
+ return `/external/${encodeURIComponent(target.toString())}`
475
+ }
476
+ return endpoint
477
+ } catch (error) {
478
+ throw malformed(error instanceof Error ? error.message : "Invalid fetch target.")
479
+ }
480
+ }
481
+
482
+ const query = prompt
483
+ .replace(
484
+ /^(?:please\s+)?(?:search|find|look up)(?:\s+(?:the\s+)?apple(?: developer)?\s+documentation)?(?:\s+(?:for|about))?\s+/i,
485
+ "",
486
+ )
487
+ .trim()
488
+
489
+ try {
490
+ return resolveSearchEndpoint(query || prompt)
491
+ } catch (error) {
492
+ throw malformed(error instanceof Error ? error.message : "Invalid search query.")
493
+ }
494
+ }
495
+
496
+ /** Map an existing capability's HTTP failure into the A2A error envelope. */
497
+ export function capabilityError(statusCode: number, message: string): A2AError {
498
+ if (statusCode === 400) {
499
+ return new A2AError(400, "INVALID_ARGUMENT", null, message)
500
+ }
501
+ if (statusCode === 403) {
502
+ return new A2AError(403, "PERMISSION_DENIED", null, message)
503
+ }
504
+ if (statusCode === 404) {
505
+ return new A2AError(404, "NOT_FOUND", null, message)
506
+ }
507
+ return new A2AError(503, "UNAVAILABLE", null, message)
508
+ }
509
+
510
+ function parseA2AMessage(input: unknown): ParsedA2AMessage {
511
+ const parsed = sendMessageRequestSchema.safeParse(input)
512
+ if (!parsed.success) {
513
+ const issue = parsed.error.issues[0]
514
+ const field = issue?.path.length ? issue.path.join(".") : "request"
515
+ throw malformed(`${field}: ${issue?.message ?? "Invalid A2A request."}`)
516
+ }
517
+
518
+ const request = parsed.data
519
+ if (request.tenant) {
520
+ throw malformed("tenant is not supported by this interface.")
521
+ }
522
+ const message = request.message
523
+ if (message.role !== "ROLE_USER") {
524
+ throw malformed('message.role must be "ROLE_USER".')
525
+ }
526
+ if (message.taskId !== undefined) {
527
+ throw taskNotFound(message.taskId)
528
+ }
529
+ const referencedTaskId = message.referenceTaskIds?.[0]
530
+ if (referencedTaskId) {
531
+ throw taskNotFound(referencedTaskId)
532
+ }
533
+
534
+ const textParts = message.parts.map(parseTextPart)
535
+ const prompt = textParts.join("\n").trim()
536
+ if (!prompt) {
537
+ throw malformed("message.parts must contain non-empty text.")
538
+ }
539
+ if (prompt.length > 10_000) {
540
+ throw malformed("Combined message text must not exceed 10,000 characters.")
541
+ }
542
+
543
+ return {
544
+ contextId: message.contextId,
545
+ prompt,
546
+ outputMode: selectOutputMode(request.configuration),
547
+ }
548
+ }
549
+
550
+ function parseTextPart(part: z.infer<typeof partSchema>): string {
551
+ const contentFields = ["text", "raw", "url", "data"].filter((field) => field in part)
552
+ if (contentFields.length !== 1 || contentFields[0] !== "text" || typeof part.text !== "string") {
553
+ throw new A2AError(
554
+ 400,
555
+ "INVALID_ARGUMENT",
556
+ "CONTENT_TYPE_NOT_SUPPORTED",
557
+ "Sosumi accepts text message parts only.",
558
+ )
559
+ }
560
+ if (part.mediaType !== undefined && part.mediaType !== "text/plain") {
561
+ throw new A2AError(
562
+ 400,
563
+ "INVALID_ARGUMENT",
564
+ "CONTENT_TYPE_NOT_SUPPORTED",
565
+ `Unsupported input media type "${String(part.mediaType)}"; expected text/plain.`,
566
+ )
567
+ }
568
+
569
+ return part.text
570
+ }
571
+
572
+ function selectOutputMode(
573
+ configuration: z.infer<typeof sendMessageConfigurationSchema> | undefined,
574
+ ): typeof A2A_OUTPUT_MEDIA_TYPE {
575
+ if (configuration === undefined) {
576
+ return A2A_OUTPUT_MEDIA_TYPE
577
+ }
578
+ if (configuration.taskPushNotificationConfig !== undefined) {
579
+ throw pushNotificationsNotSupported()
580
+ }
581
+
582
+ const modes = configuration.acceptedOutputModes
583
+ if (modes === undefined || (Array.isArray(modes) && modes.length === 0)) {
584
+ return A2A_OUTPUT_MEDIA_TYPE
585
+ }
586
+ if (!Array.isArray(modes) || !modes.every((mode) => typeof mode === "string")) {
587
+ throw malformed("configuration.acceptedOutputModes must be an array of media types.")
588
+ }
589
+ if (modes.includes(A2A_OUTPUT_MEDIA_TYPE)) {
590
+ return A2A_OUTPUT_MEDIA_TYPE
591
+ }
592
+
593
+ throw new A2AError(
594
+ 400,
595
+ "INVALID_ARGUMENT",
596
+ "CONTENT_TYPE_NOT_SUPPORTED",
597
+ `Sosumi returns ${A2A_OUTPUT_MEDIA_TYPE} only.`,
598
+ )
599
+ }
600
+
601
+ function extractFetchTarget(prompt: string): string | null {
602
+ const url = prompt.match(/https:\/\/[^\s<>"'`]+/i)?.[0]
603
+ if (url) {
604
+ return trimTargetPunctuation(url)
605
+ }
606
+
607
+ const pathPrefixes = ["/documentation/", "/design/human-interface-guidelines", "/videos/play/"]
608
+ const lowerPrompt = prompt.toLowerCase()
609
+ for (const prefix of pathPrefixes) {
610
+ const index = lowerPrompt.indexOf(prefix)
611
+ if (index >= 0) {
612
+ return trimTargetPunctuation(prompt.slice(index).split(/\s/, 1)[0] ?? "")
613
+ }
614
+ }
615
+
616
+ return null
617
+ }
618
+
619
+ function trimTargetPunctuation(target: string): string {
620
+ return target.replace(/[`'",.;:!?\])}]+$/g, "")
621
+ }
622
+
623
+ function malformed(message: string): A2AError {
624
+ return new A2AError(400, "INVALID_ARGUMENT", null, message)
625
+ }
626
+
627
+ function requestTooLarge(): A2AError {
628
+ return new A2AError(
629
+ 413,
630
+ "RESOURCE_EXHAUSTED",
631
+ null,
632
+ `Request body exceeds the ${A2A_MAX_REQUEST_BYTES}-byte limit.`,
633
+ )
634
+ }
635
+
636
+ function parseIntegerQuery(
637
+ value: string | null,
638
+ name: string,
639
+ minimum: number,
640
+ maximum = Number.MAX_SAFE_INTEGER,
641
+ ): number | undefined {
642
+ if (value === null) {
643
+ return undefined
644
+ }
645
+ if (!/^\d+$/.test(value)) {
646
+ throw malformed(`${name} must be an integer.`)
647
+ }
648
+
649
+ const parsed = Number(value)
650
+ if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
651
+ throw malformed(`${name} must be between ${minimum} and ${maximum}.`)
652
+ }
653
+ return parsed
654
+ }
655
+
656
+ function isValidProtobufTimestamp(value: string): boolean {
657
+ const match =
658
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|([+-])(\d{2}):(\d{2}))$/.exec(
659
+ value,
660
+ )
661
+ if (!match) {
662
+ return false
663
+ }
664
+
665
+ const year = Number(match[1])
666
+ const month = Number(match[2])
667
+ const day = Number(match[3])
668
+ const hour = Number(match[4])
669
+ const minute = Number(match[5])
670
+ const second = Number(match[6])
671
+ const fraction = match[7] ?? ""
672
+ const offsetHour = Number(match[10] ?? 0)
673
+ const offsetMinute = Number(match[11] ?? 0)
674
+
675
+ if (
676
+ year < 1 ||
677
+ month < 1 ||
678
+ month > 12 ||
679
+ day < 1 ||
680
+ day > daysInMonth(year, month) ||
681
+ hour > 23 ||
682
+ minute > 59 ||
683
+ second > 59 ||
684
+ offsetHour > 23 ||
685
+ offsetMinute > 59
686
+ ) {
687
+ return false
688
+ }
689
+
690
+ const localTime = new Date(0)
691
+ localTime.setUTCFullYear(year, month - 1, day)
692
+ localTime.setUTCHours(hour, minute, second, Number(fraction.padEnd(3, "0").slice(0, 3)))
693
+
694
+ const offsetSign = match[9] === "-" ? -1 : 1
695
+ const offsetMilliseconds = offsetSign * (offsetHour * 60 + offsetMinute) * 60_000
696
+ const instant = localTime.getTime() - offsetMilliseconds
697
+ return instant >= -62_135_596_800_000 && instant <= 253_402_300_799_999
698
+ }
699
+
700
+ function daysInMonth(year: number, month: number): number {
701
+ if (month === 2) {
702
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
703
+ return leapYear ? 29 : 28
704
+ }
705
+ return [4, 6, 9, 11].includes(month) ? 30 : 31
706
+ }
707
+
708
+ function capabilityResponseTooLarge(): A2AError {
709
+ return new A2AError(
710
+ 503,
711
+ "UNAVAILABLE",
712
+ null,
713
+ `Capability response exceeds the ${A2A_MAX_RESPONSE_BYTES}-byte A2A limit.`,
714
+ )
715
+ }
716
+
717
+ async function readLimitedBody(
718
+ body: ReadableStream<Uint8Array>,
719
+ maximumBytes: number,
720
+ tooLarge: () => A2AError,
721
+ readFailure: () => A2AError,
722
+ ): Promise<Uint8Array> {
723
+ const reader = body.getReader()
724
+ const chunks: Uint8Array[] = []
725
+ let byteLength = 0
726
+
727
+ try {
728
+ while (true) {
729
+ const { done, value } = await reader.read()
730
+ if (done) {
731
+ break
732
+ }
733
+
734
+ byteLength += value.byteLength
735
+ if (byteLength > maximumBytes) {
736
+ try {
737
+ await reader.cancel()
738
+ } catch {
739
+ // The bounded error below is more useful than a cancellation failure.
740
+ }
741
+ throw tooLarge()
742
+ }
743
+ chunks.push(value)
744
+ }
745
+ } catch (error) {
746
+ if (error instanceof A2AError) {
747
+ throw error
748
+ }
749
+ throw readFailure()
750
+ } finally {
751
+ reader.releaseLock()
752
+ }
753
+
754
+ const bytes = new Uint8Array(byteLength)
755
+ let offset = 0
756
+ for (const chunk of chunks) {
757
+ bytes.set(chunk, offset)
758
+ offset += chunk.byteLength
759
+ }
760
+ return bytes
761
+ }