@cleocode/lafs-protocol 1.3.2 → 1.4.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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  LAFS defines a standard envelope format for structured responses from LLM-powered agents and tools. It complements transport protocols like [MCP](https://modelcontextprotocol.io/) and [A2A](https://github.com/google/A2A) by standardizing what comes back — not how it gets there.
6
6
 
7
- **Current version:** 1.3.1 | [📚 Documentation](https://codluv.gitbook.io/lafs-protocol/) | [Spec](lafs.md) | [Migration Guides](migrations/)
7
+ **Current version:** 1.4.1 | [📚 Documentation](https://codluv.gitbook.io/lafs-protocol/) | [Spec](lafs.md) | [Migration Guides](migrations/)
8
8
 
9
9
  [![GitBook](https://img.shields.io/badge/docs-gitbook-blue)](https://codluv.gitbook.io/lafs-protocol/)
10
10
  [![npm](https://img.shields.io/npm/v/@cleocode/lafs-protocol)](https://www.npmjs.com/package/@cleocode/lafs-protocol)
@@ -33,15 +33,35 @@ npm install @cleocode/lafs-protocol
33
33
 
34
34
  ```typescript
35
35
  import {
36
+ createEnvelope,
37
+ parseLafsResponse,
38
+ LafsError,
36
39
  validateEnvelope,
37
40
  runEnvelopeConformance,
38
41
  isRegisteredErrorCode,
39
42
  } from "@cleocode/lafs-protocol";
40
43
 
44
+ // Build envelope with defaults
45
+ const envelope = createEnvelope({
46
+ success: true,
47
+ result: { items: [] },
48
+ meta: { operation: "example.list", requestId: "req_1" },
49
+ });
50
+
41
51
  // Validate an envelope against the schema
42
- const result = validateEnvelope(envelope);
43
- if (!result.valid) {
44
- console.error(result.errors);
52
+ const validation = validateEnvelope(envelope);
53
+ if (!validation.valid) {
54
+ console.error(validation.errors);
55
+ }
56
+
57
+ // Parse envelope responses with one function
58
+ try {
59
+ const parsed = parseLafsResponse(envelope);
60
+ console.log(parsed);
61
+ } catch (error) {
62
+ if (error instanceof LafsError) {
63
+ console.error(error.code, error.message);
64
+ }
45
65
  }
46
66
 
47
67
  // Run full conformance suite (schema + invariants + error codes + strict mode + pagination)
@@ -49,6 +69,13 @@ const report = runEnvelopeConformance(envelope);
49
69
  console.log(report.ok); // true if all checks pass
50
70
  ```
51
71
 
72
+ ## LLM-agent implementation guides
73
+
74
+ - `docs/guides/llm-agent-guide.md` - parser, success/error handling, strict JSON policy
75
+ - `docs/guides/schema-extension.md` - operation-specific result validation on top of core schema
76
+ - `docs/guides/compliance-pipeline.md` - generation middleware with validate + conformance gates
77
+ - `docs/llms.txt` - LLM-oriented index and canonical sources
78
+
52
79
  ## CLI
53
80
 
54
81
  ```bash
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "version": "1.0.0",
4
+ "tiers": {
5
+ "core": [
6
+ "envelope_schema_valid",
7
+ "envelope_invariants",
8
+ "error_code_registered"
9
+ ],
10
+ "standard": [
11
+ "envelope_schema_valid",
12
+ "envelope_invariants",
13
+ "error_code_registered",
14
+ "transport_mapping_consistent",
15
+ "meta_mvi_present",
16
+ "meta_strict_present",
17
+ "strict_mode_behavior",
18
+ "pagination_mode_consistent",
19
+ "strict_mode_enforced"
20
+ ],
21
+ "complete": [
22
+ "envelope_schema_valid",
23
+ "envelope_invariants",
24
+ "error_code_registered",
25
+ "transport_mapping_consistent",
26
+ "context_mutation_failure",
27
+ "context_preservation_valid",
28
+ "meta_mvi_present",
29
+ "meta_strict_present",
30
+ "strict_mode_behavior",
31
+ "pagination_mode_consistent",
32
+ "strict_mode_enforced"
33
+ ]
34
+ }
35
+ }
@@ -29,3 +29,7 @@ export declare const A2A_ERROR_MAPPINGS: ReadonlyMap<A2AErrorType, ErrorCodeMapp
29
29
  */
30
30
  export declare function getErrorCodeMapping(errorType: A2AErrorType): ErrorCodeMapping;
31
31
  export { A2A_GRPC_ERROR_REASONS } from './grpc.js';
32
+ export declare const SUPPORTED_A2A_VERSIONS: readonly ["1.0"];
33
+ export declare const DEFAULT_A2A_VERSION: "1.0";
34
+ export declare function parseA2AVersionHeader(headerValue: string | undefined): string[];
35
+ export declare function negotiateA2AVersion(requestedVersions: string[]): string | null;
@@ -52,3 +52,28 @@ export function getErrorCodeMapping(errorType) {
52
52
  // Also export A2A_GRPC_ERROR_REASONS for convenience (re-exported from grpc.ts via *)
53
53
  // but explicitly reference it here so the cross-binding module is self-documenting
54
54
  export { A2A_GRPC_ERROR_REASONS } from './grpc.js';
55
+ // ============================================================================
56
+ // Version Negotiation
57
+ // ============================================================================
58
+ export const SUPPORTED_A2A_VERSIONS = ['1.0'];
59
+ export const DEFAULT_A2A_VERSION = '1.0';
60
+ export function parseA2AVersionHeader(headerValue) {
61
+ if (!headerValue)
62
+ return [];
63
+ return headerValue
64
+ .split(',')
65
+ .map((v) => v.trim())
66
+ .filter(Boolean);
67
+ }
68
+ export function negotiateA2AVersion(requestedVersions) {
69
+ if (requestedVersions.length === 0) {
70
+ return DEFAULT_A2A_VERSION;
71
+ }
72
+ const supported = new Set(SUPPORTED_A2A_VERSIONS);
73
+ for (const version of requestedVersions) {
74
+ if (supported.has(version)) {
75
+ return version;
76
+ }
77
+ }
78
+ return null;
79
+ }
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * Reference: specs/external/specification.md
8
8
  */
9
- import type { Task, TaskState, TaskStatus, Artifact, Part, Message, AgentCard, MessageSendConfiguration, SendMessageResponse, SendMessageSuccessResponse, JSONRPCErrorResponse, AGENT_CARD_PATH, HTTP_EXTENSION_HEADER } from '@a2a-js/sdk';
9
+ import { AGENT_CARD_PATH, HTTP_EXTENSION_HEADER } from '@a2a-js/sdk';
10
+ import type { Task, TaskState, TaskStatus, Artifact, Part, Message, AgentCard, MessageSendConfiguration, SendMessageResponse, SendMessageSuccessResponse, JSONRPCErrorResponse } from '@a2a-js/sdk';
10
11
  import type { LAFSEnvelope } from '../types.js';
11
12
  /**
12
13
  * Configuration for LAFS A2A integration
@@ -7,6 +7,10 @@
7
7
  * Reference: specs/external/specification.md
8
8
  */
9
9
  // ============================================================================
10
+ // Imports - Use official A2A SDK types
11
+ // ============================================================================
12
+ import { AGENT_CARD_PATH, HTTP_EXTENSION_HEADER, } from '@a2a-js/sdk';
13
+ // ============================================================================
10
14
  // Result Wrapper
11
15
  // ============================================================================
12
16
  /**
@@ -267,3 +271,16 @@ export function isExtensionRequired(agentCard, extensionUri) {
267
271
  export function getExtensionParams(agentCard, extensionUri) {
268
272
  return agentCard.capabilities?.extensions?.find(ext => ext.uri === extensionUri)?.params;
269
273
  }
274
+ // ============================================================================
275
+ // Constants
276
+ // ============================================================================
277
+ /**
278
+ * A2A Agent Card well-known path
279
+ * Reference: specs/external/agent-discovery.md
280
+ */
281
+ export { AGENT_CARD_PATH };
282
+ /**
283
+ * HTTP header for A2A Extensions
284
+ * Reference: specs/external/extensions.md
285
+ */
286
+ export { HTTP_EXTENSION_HEADER };
@@ -17,7 +17,9 @@ export interface LafsExtensionParams {
17
17
  supportsContextLedger: boolean;
18
18
  supportsTokenBudgets: boolean;
19
19
  envelopeSchema: string;
20
+ kind?: ExtensionKind;
20
21
  }
22
+ export type ExtensionKind = 'data-only' | 'profile' | 'method' | 'state-machine';
21
23
  /** Result of extension negotiation between client and agent */
22
24
  export interface ExtensionNegotiationResult {
23
25
  /** URIs requested by the client */
@@ -28,6 +30,8 @@ export interface ExtensionNegotiationResult {
28
30
  unsupported: string[];
29
31
  /** Agent-required URIs not present in client request */
30
32
  missingRequired: string[];
33
+ /** Activated extensions grouped by declared kind (when provided) */
34
+ activatedByKind: Partial<Record<ExtensionKind, string[]>>;
31
35
  }
32
36
  /**
33
37
  * Parse A2A-Extensions header value into URI array.
@@ -57,6 +61,19 @@ export interface BuildLafsExtensionOptions {
57
61
  * Suitable for inclusion in Agent Card capabilities.extensions[].
58
62
  */
59
63
  export declare function buildLafsExtension(options?: BuildLafsExtensionOptions): AgentExtension;
64
+ export interface BuildExtensionOptions {
65
+ uri: string;
66
+ description: string;
67
+ required?: boolean;
68
+ kind: ExtensionKind;
69
+ params?: Record<string, unknown>;
70
+ }
71
+ export declare function buildExtension(options: BuildExtensionOptions): AgentExtension;
72
+ export declare function isValidExtensionKind(kind: string): kind is ExtensionKind;
73
+ export declare function validateExtensionDeclaration(extension: AgentExtension): {
74
+ valid: boolean;
75
+ error?: string;
76
+ };
60
77
  /**
61
78
  * Error thrown when required A2A extensions are not supported by the client.
62
79
  * Code -32008 (not in SDK, which stops at -32007).
@@ -18,6 +18,7 @@ export const A2A_EXTENSIONS_HEADER = 'A2A-Extensions';
18
18
  * Middleware checks both for SDK compatibility.
19
19
  */
20
20
  const SDK_EXTENSIONS_HEADER = 'x-a2a-extensions';
21
+ const VALID_EXTENSION_KINDS = ['data-only', 'profile', 'method', 'state-machine'];
21
22
  // ============================================================================
22
23
  // Functions
23
24
  // ============================================================================
@@ -57,7 +58,21 @@ export function negotiateExtensions(requestedUris, agentExtensions) {
57
58
  missingRequired.push(ext.uri);
58
59
  }
59
60
  }
60
- return { requested: requestedUris, activated, unsupported, missingRequired };
61
+ const activatedByKind = {};
62
+ for (const uri of activated) {
63
+ const ext = declared.get(uri);
64
+ const kind = ext?.params && typeof ext.params === 'object'
65
+ ? ext.params['kind']
66
+ : undefined;
67
+ if (typeof kind === 'string' && VALID_EXTENSION_KINDS.includes(kind)) {
68
+ const typedKind = kind;
69
+ if (!activatedByKind[typedKind]) {
70
+ activatedByKind[typedKind] = [];
71
+ }
72
+ activatedByKind[typedKind].push(uri);
73
+ }
74
+ }
75
+ return { requested: requestedUris, activated, unsupported, missingRequired, activatedByKind };
61
76
  }
62
77
  /**
63
78
  * Format activated extension URIs into header value.
@@ -79,9 +94,36 @@ export function buildLafsExtension(options) {
79
94
  supportsContextLedger: options?.supportsContextLedger ?? false,
80
95
  supportsTokenBudgets: options?.supportsTokenBudgets ?? false,
81
96
  envelopeSchema: options?.envelopeSchema ?? 'https://lafs.dev/schemas/v1/envelope.schema.json',
97
+ kind: 'profile',
82
98
  },
83
99
  };
84
100
  }
101
+ export function buildExtension(options) {
102
+ return {
103
+ uri: options.uri,
104
+ description: options.description,
105
+ required: options.required ?? false,
106
+ params: {
107
+ kind: options.kind,
108
+ ...(options.params ?? {}),
109
+ },
110
+ };
111
+ }
112
+ export function isValidExtensionKind(kind) {
113
+ return VALID_EXTENSION_KINDS.includes(kind);
114
+ }
115
+ export function validateExtensionDeclaration(extension) {
116
+ const kind = extension.params && typeof extension.params === 'object'
117
+ ? extension.params['kind']
118
+ : undefined;
119
+ if (kind === undefined) {
120
+ return { valid: true };
121
+ }
122
+ if (typeof kind !== 'string' || !isValidExtensionKind(kind)) {
123
+ return { valid: false, error: `invalid extension kind: ${String(kind)}` };
124
+ }
125
+ return { valid: true };
126
+ }
85
127
  // ============================================================================
86
128
  // Error Class
87
129
  // ============================================================================
@@ -31,7 +31,9 @@
31
31
  export { LafsA2AResult, createLafsArtifact, createTextArtifact, createFileArtifact, isExtensionRequired, getExtensionParams, AGENT_CARD_PATH, HTTP_EXTENSION_HEADER, } from './bridge.js';
32
32
  export { LAFS_EXTENSION_URI, A2A_EXTENSIONS_HEADER, parseExtensionsHeader, negotiateExtensions, formatExtensionsHeader, buildLafsExtension, ExtensionSupportRequiredError, extensionNegotiationMiddleware, } from './extensions.js';
33
33
  export type { LafsExtensionParams, ExtensionNegotiationResult, BuildLafsExtensionOptions, ExtensionNegotiationMiddlewareOptions, } from './extensions.js';
34
- export { TERMINAL_STATES, INTERRUPTED_STATES, VALID_TRANSITIONS, isValidTransition, isTerminalState, isInterruptedState, InvalidStateTransitionError, TaskImmutabilityError, TaskNotFoundError, TaskManager, attachLafsEnvelope, } from './task-lifecycle.js';
34
+ export { TERMINAL_STATES, INTERRUPTED_STATES, VALID_TRANSITIONS, isValidTransition, isTerminalState, isInterruptedState, InvalidStateTransitionError, TaskImmutabilityError, TaskNotFoundError, TaskRefinementError, TaskManager, attachLafsEnvelope, } from './task-lifecycle.js';
35
+ export { TaskEventBus, PushNotificationConfigStore, PushNotificationDispatcher, TaskArtifactAssembler, streamTaskEvents, } from './streaming.js';
36
+ export type { TaskStreamEvent, StreamIteratorOptions, PushNotificationDeliveryResult, PushTransport, } from './streaming.js';
35
37
  export type { CreateTaskOptions, ListTasksOptions, ListTasksResult, } from './task-lifecycle.js';
36
38
  export * from './bindings/index.js';
37
39
  export type { LafsA2AConfig, LafsSendMessageParams, } from './bridge.js';
@@ -37,7 +37,9 @@ LafsA2AResult,
37
37
  // Artifact helpers
38
38
  createLafsArtifact, createTextArtifact, createFileArtifact,
39
39
  // Extension helpers
40
- isExtensionRequired, getExtensionParams, } from './bridge.js';
40
+ isExtensionRequired, getExtensionParams,
41
+ // Constants
42
+ AGENT_CARD_PATH, HTTP_EXTENSION_HEADER, } from './bridge.js';
41
43
  // ============================================================================
42
44
  // Extensions (T098)
43
45
  // ============================================================================
@@ -59,12 +61,16 @@ TERMINAL_STATES, INTERRUPTED_STATES, VALID_TRANSITIONS,
59
61
  // State functions
60
62
  isValidTransition, isTerminalState, isInterruptedState,
61
63
  // Error classes
62
- InvalidStateTransitionError, TaskImmutabilityError, TaskNotFoundError,
64
+ InvalidStateTransitionError, TaskImmutabilityError, TaskNotFoundError, TaskRefinementError,
63
65
  // Task manager
64
66
  TaskManager,
65
67
  // LAFS integration
66
68
  attachLafsEnvelope, } from './task-lifecycle.js';
67
69
  // ============================================================================
70
+ // Streaming and Async (T101)
71
+ // ============================================================================
72
+ export { TaskEventBus, PushNotificationConfigStore, PushNotificationDispatcher, TaskArtifactAssembler, streamTaskEvents, } from './streaming.js';
73
+ // ============================================================================
68
74
  // Protocol Bindings (T100)
69
75
  // ============================================================================
70
76
  export * from './bindings/index.js';
@@ -0,0 +1,74 @@
1
+ /**
2
+ * A2A streaming and async runtime primitives (T101).
3
+ *
4
+ * Provides an in-memory event bus for task status/artifact updates and
5
+ * push-notification config storage helpers.
6
+ */
7
+ import type { Artifact, PushNotificationConfig, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from "@a2a-js/sdk";
8
+ export type TaskStreamEvent = TaskStatusUpdateEvent | TaskArtifactUpdateEvent;
9
+ type StreamListener = (event: TaskStreamEvent) => void;
10
+ /**
11
+ * In-memory event bus for task lifecycle streaming events.
12
+ */
13
+ export declare class TaskEventBus {
14
+ private history;
15
+ private listeners;
16
+ publishStatusUpdate(event: TaskStatusUpdateEvent): void;
17
+ publishArtifactUpdate(event: TaskArtifactUpdateEvent): void;
18
+ publish(event: TaskStreamEvent): void;
19
+ subscribe(taskId: string, listener: StreamListener): () => void;
20
+ getHistory(taskId: string): TaskStreamEvent[];
21
+ }
22
+ export interface StreamIteratorOptions {
23
+ timeoutMs?: number;
24
+ }
25
+ /**
26
+ * Build an async iterator for real-time task stream events.
27
+ */
28
+ export declare function streamTaskEvents(bus: TaskEventBus, taskId: string, options?: StreamIteratorOptions): AsyncGenerator<TaskStreamEvent>;
29
+ /**
30
+ * In-memory manager for async push-notification configs.
31
+ */
32
+ export declare class PushNotificationConfigStore {
33
+ private configs;
34
+ set(taskId: string, configId: string, config: PushNotificationConfig): void;
35
+ get(taskId: string, configId: string): PushNotificationConfig | undefined;
36
+ list(taskId: string): PushNotificationConfig[];
37
+ delete(taskId: string, configId: string): boolean;
38
+ }
39
+ export interface PushNotificationDeliveryResult {
40
+ configId: string;
41
+ ok: boolean;
42
+ status?: number;
43
+ error?: string;
44
+ }
45
+ export type PushTransport = (input: string, init?: {
46
+ method?: string;
47
+ headers?: Record<string, string>;
48
+ body?: string;
49
+ }) => Promise<{
50
+ ok: boolean;
51
+ status: number;
52
+ }>;
53
+ /**
54
+ * Deliver task updates to registered push-notification webhooks.
55
+ */
56
+ export declare class PushNotificationDispatcher {
57
+ private readonly store;
58
+ private readonly transport;
59
+ constructor(store: PushNotificationConfigStore, transport?: PushTransport);
60
+ dispatch(taskId: string, event: TaskStreamEvent): Promise<PushNotificationDeliveryResult[]>;
61
+ private buildHeaders;
62
+ }
63
+ /**
64
+ * Applies append/lastChunk artifact deltas into task-local snapshots.
65
+ */
66
+ export declare class TaskArtifactAssembler {
67
+ private artifacts;
68
+ applyUpdate(event: TaskArtifactUpdateEvent): Artifact;
69
+ get(taskId: string, artifactId: string): Artifact | undefined;
70
+ list(taskId: string): Artifact[];
71
+ private mergeArtifact;
72
+ private withLastChunk;
73
+ }
74
+ export {};
@@ -0,0 +1,265 @@
1
+ /**
2
+ * A2A streaming and async runtime primitives (T101).
3
+ *
4
+ * Provides an in-memory event bus for task status/artifact updates and
5
+ * push-notification config storage helpers.
6
+ */
7
+ function resolveTaskId(event) {
8
+ const candidate = event;
9
+ const taskId = candidate.taskId ?? candidate.task?.id;
10
+ if (!taskId) {
11
+ throw new Error("Task stream event is missing task identifier");
12
+ }
13
+ return taskId;
14
+ }
15
+ /**
16
+ * In-memory event bus for task lifecycle streaming events.
17
+ */
18
+ export class TaskEventBus {
19
+ history = new Map();
20
+ listeners = new Map();
21
+ publishStatusUpdate(event) {
22
+ this.publish(event);
23
+ }
24
+ publishArtifactUpdate(event) {
25
+ this.publish(event);
26
+ }
27
+ publish(event) {
28
+ const taskId = resolveTaskId(event);
29
+ const events = this.history.get(taskId) ?? [];
30
+ events.push(event);
31
+ this.history.set(taskId, events);
32
+ const listeners = this.listeners.get(taskId);
33
+ if (listeners) {
34
+ for (const listener of listeners) {
35
+ listener(event);
36
+ }
37
+ }
38
+ }
39
+ subscribe(taskId, listener) {
40
+ let set = this.listeners.get(taskId);
41
+ if (!set) {
42
+ set = new Set();
43
+ this.listeners.set(taskId, set);
44
+ }
45
+ set.add(listener);
46
+ return () => {
47
+ const active = this.listeners.get(taskId);
48
+ if (!active)
49
+ return;
50
+ active.delete(listener);
51
+ if (active.size === 0) {
52
+ this.listeners.delete(taskId);
53
+ }
54
+ };
55
+ }
56
+ getHistory(taskId) {
57
+ return [...(this.history.get(taskId) ?? [])];
58
+ }
59
+ }
60
+ /**
61
+ * Build an async iterator for real-time task stream events.
62
+ */
63
+ export async function* streamTaskEvents(bus, taskId, options = {}) {
64
+ const queue = [];
65
+ let wakeUp = null;
66
+ const unsubscribe = bus.subscribe(taskId, (event) => {
67
+ queue.push(event);
68
+ if (wakeUp) {
69
+ wakeUp();
70
+ wakeUp = null;
71
+ }
72
+ });
73
+ const timeoutMs = options.timeoutMs ?? 30_000;
74
+ try {
75
+ // Emit existing history first for catch-up behavior.
76
+ for (const event of bus.getHistory(taskId)) {
77
+ yield event;
78
+ }
79
+ while (true) {
80
+ if (queue.length > 0) {
81
+ const event = queue.shift();
82
+ if (event) {
83
+ yield event;
84
+ continue;
85
+ }
86
+ }
87
+ await new Promise((resolve) => {
88
+ const timer = setTimeout(() => {
89
+ if (wakeUp === resolve) {
90
+ wakeUp = null;
91
+ }
92
+ resolve();
93
+ }, timeoutMs);
94
+ wakeUp = () => {
95
+ clearTimeout(timer);
96
+ resolve();
97
+ };
98
+ });
99
+ }
100
+ }
101
+ finally {
102
+ unsubscribe();
103
+ }
104
+ }
105
+ /**
106
+ * In-memory manager for async push-notification configs.
107
+ */
108
+ export class PushNotificationConfigStore {
109
+ configs = new Map();
110
+ set(taskId, configId, config) {
111
+ let taskConfigs = this.configs.get(taskId);
112
+ if (!taskConfigs) {
113
+ taskConfigs = new Map();
114
+ this.configs.set(taskId, taskConfigs);
115
+ }
116
+ taskConfigs.set(configId, config);
117
+ }
118
+ get(taskId, configId) {
119
+ return this.configs.get(taskId)?.get(configId);
120
+ }
121
+ list(taskId) {
122
+ return [...(this.configs.get(taskId)?.values() ?? [])];
123
+ }
124
+ delete(taskId, configId) {
125
+ const taskConfigs = this.configs.get(taskId);
126
+ if (!taskConfigs) {
127
+ return false;
128
+ }
129
+ const removed = taskConfigs.delete(configId);
130
+ if (taskConfigs.size === 0) {
131
+ this.configs.delete(taskId);
132
+ }
133
+ return removed;
134
+ }
135
+ }
136
+ /**
137
+ * Deliver task updates to registered push-notification webhooks.
138
+ */
139
+ export class PushNotificationDispatcher {
140
+ store;
141
+ transport;
142
+ constructor(store, transport = async (input, init) => {
143
+ if (typeof fetch !== "function") {
144
+ throw new Error("Global fetch is not available for push dispatch");
145
+ }
146
+ return fetch(input, init);
147
+ }) {
148
+ this.store = store;
149
+ this.transport = transport;
150
+ }
151
+ async dispatch(taskId, event) {
152
+ const configs = this.store.list(taskId);
153
+ if (configs.length === 0) {
154
+ return [];
155
+ }
156
+ const payload = {
157
+ taskId,
158
+ event,
159
+ timestamp: new Date().toISOString(),
160
+ };
161
+ const deliveries = configs.map(async (config, index) => {
162
+ const configId = config.id ?? `${taskId}:${index}`;
163
+ if (!config.url) {
164
+ return {
165
+ configId,
166
+ ok: false,
167
+ error: "Push notification config is missing url",
168
+ };
169
+ }
170
+ const headers = this.buildHeaders(config);
171
+ try {
172
+ const response = await this.transport(config.url, {
173
+ method: "POST",
174
+ headers,
175
+ body: JSON.stringify(payload),
176
+ });
177
+ return {
178
+ configId,
179
+ ok: response.ok,
180
+ status: response.status,
181
+ ...(response.ok ? {} : { error: `HTTP ${response.status}` }),
182
+ };
183
+ }
184
+ catch (error) {
185
+ return {
186
+ configId,
187
+ ok: false,
188
+ error: error instanceof Error ? error.message : String(error),
189
+ };
190
+ }
191
+ });
192
+ return Promise.all(deliveries);
193
+ }
194
+ buildHeaders(config) {
195
+ const headers = {
196
+ "content-type": "application/json",
197
+ };
198
+ if (config.token) {
199
+ headers["x-a2a-task-token"] = config.token;
200
+ }
201
+ const scheme = config.authentication?.schemes?.[0];
202
+ const credentials = config.authentication?.credentials;
203
+ if (scheme && credentials) {
204
+ headers.authorization = `${scheme} ${credentials}`;
205
+ }
206
+ else if (config.token) {
207
+ headers.authorization = `Bearer ${config.token}`;
208
+ }
209
+ return headers;
210
+ }
211
+ }
212
+ /**
213
+ * Applies append/lastChunk artifact deltas into task-local snapshots.
214
+ */
215
+ export class TaskArtifactAssembler {
216
+ artifacts = new Map();
217
+ applyUpdate(event) {
218
+ if (!event.artifact?.artifactId) {
219
+ throw new Error("Task artifact update is missing artifactId");
220
+ }
221
+ const taskId = resolveTaskId(event);
222
+ const artifactId = event.artifact.artifactId;
223
+ let taskArtifacts = this.artifacts.get(taskId);
224
+ if (!taskArtifacts) {
225
+ taskArtifacts = new Map();
226
+ this.artifacts.set(taskId, taskArtifacts);
227
+ }
228
+ const prior = taskArtifacts.get(artifactId);
229
+ const merged = this.mergeArtifact(prior, event);
230
+ taskArtifacts.set(artifactId, merged);
231
+ return merged;
232
+ }
233
+ get(taskId, artifactId) {
234
+ return this.artifacts.get(taskId)?.get(artifactId);
235
+ }
236
+ list(taskId) {
237
+ return [...(this.artifacts.get(taskId)?.values() ?? [])];
238
+ }
239
+ mergeArtifact(prior, event) {
240
+ const next = event.artifact;
241
+ const append = Boolean(event.append);
242
+ const lastChunk = Boolean(event.lastChunk);
243
+ if (!append || !prior) {
244
+ return {
245
+ ...next,
246
+ metadata: this.withLastChunk(next.metadata, lastChunk),
247
+ };
248
+ }
249
+ return {
250
+ ...prior,
251
+ ...next,
252
+ parts: [...(prior.parts ?? []), ...(next.parts ?? [])],
253
+ metadata: this.withLastChunk({
254
+ ...(prior.metadata ?? {}),
255
+ ...(next.metadata ?? {}),
256
+ }, lastChunk),
257
+ };
258
+ }
259
+ withLastChunk(metadata, lastChunk) {
260
+ return {
261
+ ...(metadata ?? {}),
262
+ "a2a:last_chunk": lastChunk,
263
+ };
264
+ }
265
+ }