@cleocode/lafs-protocol 1.2.2 → 1.3.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.
@@ -1,36 +1,70 @@
1
1
  /**
2
- * LAFS Agent-to-Agent (A2A) Integration
2
+ * LAFS Agent-to-Agent (A2A) Integration v2.0
3
3
  *
4
- * This module provides integration between LAFS and the official
5
- * @a2a-js/sdk for Agent-to-Agent communication.
4
+ * Full integration with the official @a2a-js/sdk for Agent-to-Agent communication.
5
+ * Implements A2A Protocol v1.0+ specification.
6
+ *
7
+ * Reference: specs/external/specification.md
6
8
  *
7
9
  * @example
8
10
  * ```typescript
11
+ * import type { AgentCard, Task } from '@cleocode/lafs-protocol/a2a';
12
+ * import {
13
+ * createLafsArtifact,
14
+ * createTextArtifact,
15
+ * LafsA2AResult,
16
+ * isExtensionRequired
17
+ * } from '@cleocode/lafs-protocol/a2a';
18
+ *
19
+ * // Use A2A SDK directly for client operations
9
20
  * import { ClientFactory } from '@a2a-js/sdk/client';
10
- * import { withLafsEnvelope } from '@cleocode/lafs-protocol/a2a';
11
21
  *
12
- * // Create official A2A client
13
22
  * const factory = new ClientFactory();
14
- * const a2aClient = await factory.createFromUrl('http://agent.example.com');
15
- *
16
- * // Wrap with LAFS support
17
- * const client = withLafsEnvelope(a2aClient, {
18
- * defaultBudget: { maxTokens: 4000 }
19
- * });
20
- *
21
- * // Send message
22
- * const result = await client.sendMessage({
23
- * message: {
24
- * role: 'user',
25
- * parts: [{ text: 'Analyze data' }]
26
- * }
27
- * });
23
+ * const client = await factory.createFromUrl('https://agent.example.com');
24
+ * const result = await client.sendMessage({...});
28
25
  *
29
- * // Extract LAFS envelope from response
30
- * const envelope = result.getLafsEnvelope();
31
- * if (envelope) {
32
- * console.log(envelope._meta._tokenEstimate);
33
- * }
26
+ * // Wrap result with LAFS helpers
27
+ * const lafsResult = new LafsA2AResult(result, {}, 'req-001');
28
+ * const envelope = lafsResult.getLafsEnvelope();
34
29
  * ```
35
30
  */
36
- export { withLafsEnvelope, LafsA2AClient, LafsA2AResult, createLafsArtifact } from './bridge.js';
31
+ // ============================================================================
32
+ // Core Exports
33
+ // ============================================================================
34
+ export {
35
+ // Result wrapper
36
+ LafsA2AResult,
37
+ // Artifact helpers
38
+ createLafsArtifact, createTextArtifact, createFileArtifact,
39
+ // Extension helpers
40
+ isExtensionRequired, getExtensionParams, } from './bridge.js';
41
+ // ============================================================================
42
+ // Extensions (T098)
43
+ // ============================================================================
44
+ export {
45
+ // Constants
46
+ LAFS_EXTENSION_URI, A2A_EXTENSIONS_HEADER,
47
+ // Functions
48
+ parseExtensionsHeader, negotiateExtensions, formatExtensionsHeader, buildLafsExtension,
49
+ // Error class
50
+ ExtensionSupportRequiredError,
51
+ // Middleware
52
+ extensionNegotiationMiddleware, } from './extensions.js';
53
+ // ============================================================================
54
+ // Task Lifecycle (T099)
55
+ // ============================================================================
56
+ export {
57
+ // State constants
58
+ TERMINAL_STATES, INTERRUPTED_STATES, VALID_TRANSITIONS,
59
+ // State functions
60
+ isValidTransition, isTerminalState, isInterruptedState,
61
+ // Error classes
62
+ InvalidStateTransitionError, TaskImmutabilityError, TaskNotFoundError,
63
+ // Task manager
64
+ TaskManager,
65
+ // LAFS integration
66
+ attachLafsEnvelope, } from './task-lifecycle.js';
67
+ // ============================================================================
68
+ // Protocol Bindings (T100)
69
+ // ============================================================================
70
+ export * from './bindings/index.js';
@@ -0,0 +1,98 @@
1
+ /**
2
+ * A2A Task Lifecycle Management
3
+ *
4
+ * State machine enforcement, task CRUD, and LAFS integration
5
+ * for A2A Protocol v1.0+ compliance.
6
+ *
7
+ * Reference: A2A spec Section 6 (Task Lifecycle)
8
+ */
9
+ import type { Task, TaskState, Artifact, Message } from '@a2a-js/sdk';
10
+ import type { LAFSEnvelope } from '../types.js';
11
+ /** States from which no further transitions are possible */
12
+ export declare const TERMINAL_STATES: ReadonlySet<TaskState>;
13
+ /** States where the task is paused awaiting external input */
14
+ export declare const INTERRUPTED_STATES: ReadonlySet<TaskState>;
15
+ /** Valid state transitions (adjacency map). Terminal states have empty outgoing sets. */
16
+ export declare const VALID_TRANSITIONS: ReadonlyMap<TaskState, ReadonlySet<TaskState>>;
17
+ /** Check if a transition from one state to another is valid */
18
+ export declare function isValidTransition(from: TaskState, to: TaskState): boolean;
19
+ /** Check if a state is terminal (no further transitions allowed) */
20
+ export declare function isTerminalState(state: TaskState): boolean;
21
+ /** Check if a state is interrupted (paused awaiting input) */
22
+ export declare function isInterruptedState(state: TaskState): boolean;
23
+ /** Thrown when attempting an invalid state transition */
24
+ export declare class InvalidStateTransitionError extends Error {
25
+ readonly taskId: string;
26
+ readonly fromState: TaskState;
27
+ readonly toState: TaskState;
28
+ constructor(taskId: string, fromState: TaskState, toState: TaskState);
29
+ }
30
+ /** Thrown when attempting to modify a task in a terminal state */
31
+ export declare class TaskImmutabilityError extends Error {
32
+ readonly taskId: string;
33
+ readonly terminalState: TaskState;
34
+ constructor(taskId: string, terminalState: TaskState);
35
+ }
36
+ /** Thrown when a task is not found */
37
+ export declare class TaskNotFoundError extends Error {
38
+ readonly taskId: string;
39
+ constructor(taskId: string);
40
+ }
41
+ /** Options for creating a new task */
42
+ export interface CreateTaskOptions {
43
+ contextId?: string;
44
+ metadata?: Record<string, unknown>;
45
+ }
46
+ /** Options for listing tasks */
47
+ export interface ListTasksOptions {
48
+ contextId?: string;
49
+ state?: TaskState;
50
+ limit?: number;
51
+ pageToken?: string;
52
+ }
53
+ /** Paginated result from listTasks */
54
+ export interface ListTasksResult {
55
+ tasks: Task[];
56
+ nextPageToken?: string;
57
+ }
58
+ /**
59
+ * In-memory task manager implementing A2A task lifecycle.
60
+ * Enforces valid state transitions and terminal state immutability.
61
+ */
62
+ export declare class TaskManager {
63
+ private tasks;
64
+ private contextIndex;
65
+ /** Create a new task in the submitted state */
66
+ createTask(options?: CreateTaskOptions): Task;
67
+ /** Get a task by ID. Throws TaskNotFoundError if not found. */
68
+ getTask(taskId: string): Task;
69
+ /** List tasks with optional filtering and pagination */
70
+ listTasks(options?: ListTasksOptions): ListTasksResult;
71
+ /**
72
+ * Update task status. Enforces valid transitions and terminal state immutability.
73
+ * @throws InvalidStateTransitionError if the transition is not valid
74
+ * @throws TaskImmutabilityError if the task is in a terminal state
75
+ */
76
+ updateTaskStatus(taskId: string, state: TaskState, message?: Message): Task;
77
+ /**
78
+ * Add an artifact to a task.
79
+ * @throws TaskImmutabilityError if the task is in a terminal state
80
+ */
81
+ addArtifact(taskId: string, artifact: Artifact): Task;
82
+ /**
83
+ * Add a message to task history.
84
+ * @throws TaskImmutabilityError if the task is in a terminal state
85
+ */
86
+ addHistory(taskId: string, message: Message): Task;
87
+ /** Cancel a task by transitioning to canceled state */
88
+ cancelTask(taskId: string): Task;
89
+ /** Get all tasks in a given context */
90
+ getTasksByContext(contextId: string): Task[];
91
+ /** Check if a task is in a terminal state */
92
+ isTerminal(taskId: string): boolean;
93
+ }
94
+ /**
95
+ * Attach a LAFS envelope as an artifact to an A2A task.
96
+ * Uses createLafsArtifact() from bridge.ts to wrap the envelope.
97
+ */
98
+ export declare function attachLafsEnvelope(manager: TaskManager, taskId: string, envelope: LAFSEnvelope): Task;
@@ -0,0 +1,263 @@
1
+ /**
2
+ * A2A Task Lifecycle Management
3
+ *
4
+ * State machine enforcement, task CRUD, and LAFS integration
5
+ * for A2A Protocol v1.0+ compliance.
6
+ *
7
+ * Reference: A2A spec Section 6 (Task Lifecycle)
8
+ */
9
+ import { createLafsArtifact } from './bridge.js';
10
+ // ============================================================================
11
+ // State Constants
12
+ // ============================================================================
13
+ /** States from which no further transitions are possible */
14
+ export const TERMINAL_STATES = new Set([
15
+ 'completed',
16
+ 'failed',
17
+ 'canceled',
18
+ 'rejected',
19
+ ]);
20
+ /** States where the task is paused awaiting external input */
21
+ export const INTERRUPTED_STATES = new Set([
22
+ 'input-required',
23
+ 'auth-required',
24
+ ]);
25
+ /** Valid state transitions (adjacency map). Terminal states have empty outgoing sets. */
26
+ export const VALID_TRANSITIONS = new Map([
27
+ ['submitted', new Set(['working', 'canceled', 'rejected', 'failed'])],
28
+ ['working', new Set(['completed', 'failed', 'canceled', 'input-required', 'auth-required'])],
29
+ ['input-required', new Set(['working', 'canceled', 'failed'])],
30
+ ['auth-required', new Set(['working', 'canceled', 'failed'])],
31
+ ['completed', new Set()],
32
+ ['failed', new Set()],
33
+ ['canceled', new Set()],
34
+ ['rejected', new Set()],
35
+ ['unknown', new Set(['submitted', 'working', 'input-required', 'completed', 'canceled', 'failed', 'rejected', 'auth-required'])],
36
+ ]);
37
+ // ============================================================================
38
+ // State Functions
39
+ // ============================================================================
40
+ /** Check if a transition from one state to another is valid */
41
+ export function isValidTransition(from, to) {
42
+ const allowed = VALID_TRANSITIONS.get(from);
43
+ return allowed ? allowed.has(to) : false;
44
+ }
45
+ /** Check if a state is terminal (no further transitions allowed) */
46
+ export function isTerminalState(state) {
47
+ return TERMINAL_STATES.has(state);
48
+ }
49
+ /** Check if a state is interrupted (paused awaiting input) */
50
+ export function isInterruptedState(state) {
51
+ return INTERRUPTED_STATES.has(state);
52
+ }
53
+ // ============================================================================
54
+ // Error Classes
55
+ // ============================================================================
56
+ /** Thrown when attempting an invalid state transition */
57
+ export class InvalidStateTransitionError extends Error {
58
+ taskId;
59
+ fromState;
60
+ toState;
61
+ constructor(taskId, fromState, toState) {
62
+ super(`Invalid transition for task ${taskId}: ${fromState} -> ${toState}`);
63
+ this.name = 'InvalidStateTransitionError';
64
+ this.taskId = taskId;
65
+ this.fromState = fromState;
66
+ this.toState = toState;
67
+ }
68
+ }
69
+ /** Thrown when attempting to modify a task in a terminal state */
70
+ export class TaskImmutabilityError extends Error {
71
+ taskId;
72
+ terminalState;
73
+ constructor(taskId, terminalState) {
74
+ super(`Task ${taskId} is in terminal state ${terminalState} and cannot be modified`);
75
+ this.name = 'TaskImmutabilityError';
76
+ this.taskId = taskId;
77
+ this.terminalState = terminalState;
78
+ }
79
+ }
80
+ /** Thrown when a task is not found */
81
+ export class TaskNotFoundError extends Error {
82
+ taskId;
83
+ constructor(taskId) {
84
+ super(`Task not found: ${taskId}`);
85
+ this.name = 'TaskNotFoundError';
86
+ this.taskId = taskId;
87
+ }
88
+ }
89
+ // ============================================================================
90
+ // ID Generation
91
+ // ============================================================================
92
+ function generateId() {
93
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
94
+ const r = (Math.random() * 16) | 0;
95
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
96
+ return v.toString(16);
97
+ });
98
+ }
99
+ /**
100
+ * In-memory task manager implementing A2A task lifecycle.
101
+ * Enforces valid state transitions and terminal state immutability.
102
+ */
103
+ export class TaskManager {
104
+ tasks = new Map();
105
+ contextIndex = new Map();
106
+ /** Create a new task in the submitted state */
107
+ createTask(options) {
108
+ const id = generateId();
109
+ const contextId = options?.contextId ?? generateId();
110
+ const task = {
111
+ id,
112
+ contextId,
113
+ kind: 'task',
114
+ status: {
115
+ state: 'submitted',
116
+ timestamp: new Date().toISOString(),
117
+ },
118
+ ...(options?.metadata && { metadata: options.metadata }),
119
+ };
120
+ this.tasks.set(id, task);
121
+ // Index by contextId
122
+ let contextTasks = this.contextIndex.get(contextId);
123
+ if (!contextTasks) {
124
+ contextTasks = new Set();
125
+ this.contextIndex.set(contextId, contextTasks);
126
+ }
127
+ contextTasks.add(id);
128
+ return structuredClone(task);
129
+ }
130
+ /** Get a task by ID. Throws TaskNotFoundError if not found. */
131
+ getTask(taskId) {
132
+ const task = this.tasks.get(taskId);
133
+ if (!task) {
134
+ throw new TaskNotFoundError(taskId);
135
+ }
136
+ return structuredClone(task);
137
+ }
138
+ /** List tasks with optional filtering and pagination */
139
+ listTasks(options) {
140
+ let taskIds;
141
+ if (options?.contextId) {
142
+ const contextTasks = this.contextIndex.get(options.contextId);
143
+ taskIds = contextTasks ? [...contextTasks] : [];
144
+ }
145
+ else {
146
+ taskIds = [...this.tasks.keys()];
147
+ }
148
+ // Filter by state
149
+ if (options?.state) {
150
+ taskIds = taskIds.filter(id => {
151
+ const task = this.tasks.get(id);
152
+ return task && task.status.state === options.state;
153
+ });
154
+ }
155
+ // Sort for deterministic pagination
156
+ taskIds.sort();
157
+ // Apply page token (cursor-based: token is the last seen task ID)
158
+ if (options?.pageToken) {
159
+ const startIdx = taskIds.indexOf(options.pageToken);
160
+ if (startIdx >= 0) {
161
+ taskIds = taskIds.slice(startIdx + 1);
162
+ }
163
+ }
164
+ // Apply limit
165
+ const limit = options?.limit ?? taskIds.length;
166
+ const pageTaskIds = taskIds.slice(0, limit);
167
+ const hasMore = taskIds.length > limit;
168
+ const tasks = pageTaskIds.map(id => structuredClone(this.tasks.get(id)));
169
+ const nextPageToken = hasMore ? pageTaskIds[pageTaskIds.length - 1] : undefined;
170
+ return { tasks, nextPageToken };
171
+ }
172
+ /**
173
+ * Update task status. Enforces valid transitions and terminal state immutability.
174
+ * @throws InvalidStateTransitionError if the transition is not valid
175
+ * @throws TaskImmutabilityError if the task is in a terminal state
176
+ */
177
+ updateTaskStatus(taskId, state, message) {
178
+ const task = this.tasks.get(taskId);
179
+ if (!task) {
180
+ throw new TaskNotFoundError(taskId);
181
+ }
182
+ const currentState = task.status.state;
183
+ if (isTerminalState(currentState)) {
184
+ throw new TaskImmutabilityError(taskId, currentState);
185
+ }
186
+ if (!isValidTransition(currentState, state)) {
187
+ throw new InvalidStateTransitionError(taskId, currentState, state);
188
+ }
189
+ const status = {
190
+ state,
191
+ timestamp: new Date().toISOString(),
192
+ ...(message && { message }),
193
+ };
194
+ task.status = status;
195
+ return structuredClone(task);
196
+ }
197
+ /**
198
+ * Add an artifact to a task.
199
+ * @throws TaskImmutabilityError if the task is in a terminal state
200
+ */
201
+ addArtifact(taskId, artifact) {
202
+ const task = this.tasks.get(taskId);
203
+ if (!task) {
204
+ throw new TaskNotFoundError(taskId);
205
+ }
206
+ if (isTerminalState(task.status.state)) {
207
+ throw new TaskImmutabilityError(taskId, task.status.state);
208
+ }
209
+ if (!task.artifacts) {
210
+ task.artifacts = [];
211
+ }
212
+ task.artifacts.push(artifact);
213
+ return structuredClone(task);
214
+ }
215
+ /**
216
+ * Add a message to task history.
217
+ * @throws TaskImmutabilityError if the task is in a terminal state
218
+ */
219
+ addHistory(taskId, message) {
220
+ const task = this.tasks.get(taskId);
221
+ if (!task) {
222
+ throw new TaskNotFoundError(taskId);
223
+ }
224
+ if (isTerminalState(task.status.state)) {
225
+ throw new TaskImmutabilityError(taskId, task.status.state);
226
+ }
227
+ if (!task.history) {
228
+ task.history = [];
229
+ }
230
+ task.history.push(message);
231
+ return structuredClone(task);
232
+ }
233
+ /** Cancel a task by transitioning to canceled state */
234
+ cancelTask(taskId) {
235
+ return this.updateTaskStatus(taskId, 'canceled');
236
+ }
237
+ /** Get all tasks in a given context */
238
+ getTasksByContext(contextId) {
239
+ const taskIds = this.contextIndex.get(contextId);
240
+ if (!taskIds)
241
+ return [];
242
+ return [...taskIds].map(id => structuredClone(this.tasks.get(id)));
243
+ }
244
+ /** Check if a task is in a terminal state */
245
+ isTerminal(taskId) {
246
+ const task = this.tasks.get(taskId);
247
+ if (!task) {
248
+ throw new TaskNotFoundError(taskId);
249
+ }
250
+ return isTerminalState(task.status.state);
251
+ }
252
+ }
253
+ // ============================================================================
254
+ // LAFS Integration
255
+ // ============================================================================
256
+ /**
257
+ * Attach a LAFS envelope as an artifact to an A2A task.
258
+ * Uses createLafsArtifact() from bridge.ts to wrap the envelope.
259
+ */
260
+ export function attachLafsEnvelope(manager, taskId, envelope) {
261
+ const artifact = createLafsArtifact(envelope);
262
+ return manager.addArtifact(taskId, artifact);
263
+ }