@axiom-lattice/protocols 3.0.3 → 4.0.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.
@@ -1,190 +1,50 @@
1
1
  /**
2
- * A2AProtocol - Google Agent-to-Agent Protocol type definitions
3
- *
4
- * Based on the A2A open protocol spec for AI agent interoperability.
5
- * @see https://github.com/google/A2A
2
+ * A2AProtocol - re-exports standard A2A 0.3 types from @a2a-js/sdk
3
+ * plus Axiom-specific auth/exposure types.
6
4
  */
5
+ export type {
6
+ AgentCard,
7
+ AgentSkill as A2ASkill,
8
+ Task as A2ATask,
9
+ Message as A2AMessage,
10
+ Part as A2APart,
11
+ TaskState as A2ATaskState,
12
+ } from "@a2a-js/sdk";
7
13
 
8
- // ─── Agent Card ───────────────────────────────────────────────────────────
9
-
10
- export interface A2ASkill {
11
- id: string;
12
- name: string;
13
- description: string;
14
- tags: string[];
15
- examples: string[];
16
- }
17
-
18
- export interface A2ACapabilities {
19
- streaming: boolean;
20
- pushNotifications: boolean;
21
- stateTransitionHistory: boolean;
22
- }
23
-
24
- export interface A2AProvider {
25
- organization: string;
26
- url?: string;
27
- }
28
-
29
- export interface AgentCard {
30
- name: string;
31
- description: string;
32
- url: string;
33
- provider: A2AProvider;
34
- version: string;
35
- documentationUrl?: string;
36
- capabilities: A2ACapabilities;
37
- defaultInputModes: string[];
38
- defaultOutputModes: string[];
39
- skills: A2ASkill[];
40
- }
41
-
42
- // ─── Artifact / Part ──────────────────────────────────────────────────────
43
-
44
- export interface A2ATextPart {
45
- type: "text";
46
- text: string;
47
- }
48
-
49
- export interface A2AFilePart {
50
- type: "file";
51
- file: {
52
- name: string;
53
- mimeType: string;
54
- bytes?: string;
55
- uri?: string;
56
- };
57
- }
58
-
59
- export interface A2ADataPart {
60
- type: "data";
61
- data: Record<string, unknown>;
62
- }
63
-
64
- export type A2APart = A2ATextPart | A2AFilePart | A2ADataPart;
65
-
66
- // ─── Message ──────────────────────────────────────────────────────────────
67
-
68
- export interface A2AMessage {
69
- role: "user" | "agent";
70
- parts: A2APart[];
71
- messageId?: string;
72
- contextId?: string;
73
- referenceTaskIds?: string[];
74
- metadata?: Record<string, unknown>;
75
- }
76
-
77
- // ─── Task ─────────────────────────────────────────────────────────────────
78
-
79
- export type A2ATaskState =
80
- | "working"
81
- | "input-required"
82
- | "completed"
83
- | "failed"
84
- | "canceled"
85
- | "rejected";
86
-
87
- export interface A2ATaskStatus {
88
- state: A2ATaskState;
89
- message?: A2AMessage;
90
- timestamp: string;
91
- }
92
-
93
- export interface A2AArtifact {
94
- name?: string;
95
- description?: string;
96
- parts: A2APart[];
97
- metadata?: Record<string, unknown>;
98
- }
99
-
100
- export interface A2ATask {
101
- id: string;
102
- sessionId?: string;
103
- contextId?: string;
104
- status: A2ATaskStatus;
105
- artifacts: A2AArtifact[];
106
- history?: A2AMessage[];
107
- metadata?: Record<string, unknown>;
108
- }
109
-
110
- // ─── Request / Response ───────────────────────────────────────────────────
111
-
112
- export interface A2ATaskSendRequest {
113
- id?: string;
114
- sessionId?: string;
115
- message: A2AMessage;
116
- pushNotification?: A2APushNotification;
117
- historyLength?: number;
118
- metadata?: Record<string, unknown>;
119
- }
120
-
121
- export interface A2APushNotification {
122
- url: string;
123
- token?: string;
124
- }
125
-
126
- export interface A2ATaskUpdatePayload {
127
- id: string;
128
- sessionId?: string;
129
- contextId?: string;
130
- status: A2ATaskStatus;
131
- final?: boolean;
132
- metadata?: Record<string, unknown>;
133
- }
134
-
135
- export interface A2ATaskArtifactUpdatePayload {
136
- id: string;
137
- sessionId?: string;
138
- contextId?: string;
139
- artifact: A2AArtifact;
140
- final?: boolean;
141
- metadata?: Record<string, unknown>;
142
- }
143
-
144
- // ─── SSE Events ───────────────────────────────────────────────────────────
145
-
146
- export type A2ASSEEvent =
147
- | { event: "task"; data: A2ATaskUpdatePayload }
148
- | { event: "status-update"; data: A2ATaskUpdatePayload }
149
- | { event: "artifact-update"; data: A2ATaskArtifactUpdatePayload }
150
- | { event: "error"; data: { code: string; message: string } };
151
-
152
- // ─── Config ───────────────────────────────────────────────────────────────
153
-
154
- export interface A2AConfig {
155
- agentName: string;
156
- agentDescription: string;
157
- agentUrl: string;
158
- organization: string;
159
- version?: string;
160
- capabilities?: Partial<A2ACapabilities>;
161
- defaultInputModes?: string[];
162
- defaultOutputModes?: string[];
163
- skills?: A2ASkill[];
164
- apiKeyMap: Map<string, A2AApiKeyEntry>;
14
+ /**
15
+ * Per-agent A2A exposure configuration — controls whether an agent is
16
+ * reachable over A2A and which skills are advertised on its AgentCard.
17
+ */
18
+ export interface A2AExposure {
19
+ /** Whether this agent is exposed over the A2A protocol */
20
+ enabled: boolean;
21
+ /** Skills advertised on the AgentCard; defaults to a single generic skill when omitted */
22
+ skills?: Array<{ id: string; name: string; description: string; tags?: string[]; examples?: string[] }>;
23
+ /** Supported input modes (MIME types); defaults to text modes when omitted */
24
+ inputModes?: string[];
25
+ /** Supported output modes (MIME types); defaults to text modes when omitted */
26
+ outputModes?: string[];
165
27
  }
166
28
 
29
+ /**
30
+ * In-memory API key entry used for request authentication.
31
+ * Empty/undefined assistantIds means all exposed agents in the tenant.
32
+ */
167
33
  export interface A2AApiKeyEntry {
168
34
  key: string;
169
- tenantId?: string;
170
- projectId?: string;
171
- workspaceId?: string;
35
+ tenantId: string;
36
+ projectId: string;
37
+ assistantIds?: string[];
172
38
  }
173
39
 
174
- export const A2A_DEFAULT_CAPABILITIES: A2ACapabilities = {
175
- streaming: true,
176
- pushNotifications: false,
177
- stateTransitionHistory: false,
178
- };
179
-
180
- export const A2A_DEFAULT_INPUT_MODES = ["text", "text/plain"];
181
- export const A2A_DEFAULT_OUTPUT_MODES = ["text", "text/plain", "text/markdown"];
182
-
40
+ /**
41
+ * Authentication context attached to an incoming A2A request after key validation.
42
+ */
183
43
  export interface A2AAuthContext {
184
44
  authenticated: boolean;
185
45
  apiKey?: string;
186
46
  tenantId?: string;
187
47
  projectId?: string;
188
- workspaceId?: string;
48
+ assistantIds?: string[];
189
49
  source?: "bearer" | "x-api-key";
190
50
  }
@@ -16,7 +16,8 @@ export interface CreateEvalProjectRequest {
16
16
  name: string;
17
17
  description?: string;
18
18
  version?: string;
19
- judgeModelConfig: Record<string, unknown>;
19
+ /** Judge model config; omit to let the runner resolve its default model. */
20
+ judgeModelConfig?: Record<string, unknown>;
20
21
  targetServerConfig: Record<string, unknown>;
21
22
  concurrency?: number;
22
23
  reportConfig?: Record<string, unknown>;
@@ -250,4 +250,25 @@ export interface SkillStore {
250
250
  * @returns The resource content as string, or null if not found
251
251
  */
252
252
  loadSkillResource?(tenantId: string, id: string, resourcePath: string, context?: SkillStoreContext): Promise<string | null>;
253
+
254
+ /**
255
+ * Load a resource without text decoding.
256
+ * @returns The resource bytes, or null if the resource does not exist
257
+ */
258
+ loadSkillResourceRaw?(tenantId: string, id: string, resourcePath: string, context?: SkillStoreContext): Promise<Buffer | null>;
259
+
260
+ /**
261
+ * Write a resource file into a skill's resources directory
262
+ * @param tenantId Tenant identifier
263
+ * @param id Skill identifier
264
+ * @param resourcePath Path to the resource relative to resources/ directory
265
+ * @param content Resource content
266
+ * @param context Optional runtime context for sandbox resolution
267
+ */
268
+ writeSkillResource?(tenantId: string, id: string, resourcePath: string, content: string, context?: SkillStoreContext): Promise<void>;
269
+
270
+ /**
271
+ * Write a resource without text encoding.
272
+ */
273
+ writeSkillResourceRaw?(tenantId: string, id: string, resourcePath: string, data: Buffer, context?: SkillStoreContext): Promise<void>;
253
274
  }
@@ -0,0 +1,324 @@
1
+ /** A single canonical belief recorded in a task description. */
2
+ export interface TaskBeliefEntry {
3
+ key: string;
4
+ probability: number;
5
+ target: number;
6
+ basis: string;
7
+ }
8
+
9
+ /** The canonical belief snapshot embedded in task Markdown. */
10
+ export interface TaskBeliefState {
11
+ entries: TaskBeliefEntry[];
12
+ }
13
+
14
+ /** Stable diagnostic identifiers returned by the Belief State parser. */
15
+ export type TaskBeliefDiagnosticCode =
16
+ | "MISSING_BELIEF_STATE"
17
+ | "DUPLICATE_BELIEF_STATE"
18
+ | "INVALID_BELIEF_HEADERS"
19
+ | "MALFORMED_BELIEF_ROW"
20
+ | "INVALID_BELIEF_KEY"
21
+ | "INVALID_BELIEF_PERCENT"
22
+ | "DUPLICATE_BELIEF_KEY";
23
+
24
+ /** A structured failure produced while parsing a task Belief State. */
25
+ export interface TaskBeliefParseFailure {
26
+ success: false;
27
+ code: TaskBeliefDiagnosticCode;
28
+ message: string;
29
+ line?: number;
30
+ key?: string;
31
+ column?: "probability" | "target";
32
+ }
33
+
34
+ /** A successfully parsed task Belief State. */
35
+ export interface TaskBeliefParseSuccess {
36
+ success: true;
37
+ state: TaskBeliefState;
38
+ }
39
+
40
+ /** The discriminated result of parsing a task Belief State. */
41
+ export type TaskBeliefParseResult = TaskBeliefParseSuccess | TaskBeliefParseFailure;
42
+
43
+ interface MarkdownLine {
44
+ text: string;
45
+ start: number;
46
+ end: number;
47
+ fenced: boolean;
48
+ lineNumber: number;
49
+ }
50
+
51
+ interface SectionRange {
52
+ headingLine: number;
53
+ start: number;
54
+ end: number;
55
+ }
56
+
57
+ const BELIEF_HEADING = "## Belief State";
58
+ const ACCEPTANCE_HEADING = "## Acceptance Criteria";
59
+ const EXPECTED_HEADERS = ["Belief Key", "Probability", "Target", "Basis"];
60
+ const KEY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
61
+ const PERCENT_PATTERN = /^(?:100|[0-9]{1,2})%$/;
62
+
63
+ function markdownLines(markdown: string): MarkdownLine[] {
64
+ const lines: MarkdownLine[] = [];
65
+ let start = 0;
66
+ let fence: { marker: "`" | "~"; length: number } | undefined;
67
+
68
+ while (start <= markdown.length) {
69
+ const newline = markdown.indexOf("\n", start);
70
+ const end = newline === -1 ? markdown.length : newline + 1;
71
+ const textEnd = newline === -1 ? markdown.length : newline;
72
+ const rawText = markdown.slice(start, textEnd);
73
+ const text = rawText.endsWith("\r") ? rawText.slice(0, -1) : rawText;
74
+ const fenceMatch = text.match(/^ {0,3}(`{3,}|~{3,})/);
75
+ const fenced = fence !== undefined;
76
+
77
+ if (!fence && fenceMatch) {
78
+ const marker = fenceMatch[1][0] as "`" | "~";
79
+ fence = { marker, length: fenceMatch[1].length };
80
+ } else if (fence) {
81
+ const closePattern = new RegExp(`^ {0,3}\\${fence.marker}{${fence.length},}[ \\t]*$`);
82
+ if (closePattern.test(text)) fence = undefined;
83
+ }
84
+
85
+ lines.push({
86
+ text,
87
+ start,
88
+ end,
89
+ fenced: fenced || fenceMatch !== null,
90
+ lineNumber: lines.length + 1,
91
+ });
92
+ if (newline === -1) break;
93
+ start = end;
94
+ }
95
+
96
+ return lines;
97
+ }
98
+
99
+ function sectionRanges(markdown: string, heading: string): SectionRange[] {
100
+ const lines = markdownLines(markdown);
101
+ const ranges: SectionRange[] = [];
102
+
103
+ for (let index = 0; index < lines.length; index += 1) {
104
+ const line = lines[index];
105
+ if (line.fenced || line.text.trimEnd().replace(/^ {0,3}/, "") !== heading) continue;
106
+
107
+ let end = markdown.length;
108
+ for (let next = index + 1; next < lines.length; next += 1) {
109
+ if (!lines[next].fenced && /^ {0,3}#{1,2}(?:[ \t]+|$)/.test(lines[next].text)) {
110
+ end = lines[next].start;
111
+ break;
112
+ }
113
+ }
114
+ ranges.push({ headingLine: index, start: line.start, end });
115
+ }
116
+
117
+ return ranges;
118
+ }
119
+
120
+ function splitTableRow(line: string): string[] | undefined {
121
+ const trimmed = line.trim();
122
+ const finalPipe = trimmed.length - 1;
123
+ if (!trimmed.startsWith("|") || !trimmed.endsWith("|") || isEscapedPipe(trimmed, finalPipe)) {
124
+ return undefined;
125
+ }
126
+
127
+ const cells: string[] = [];
128
+ let cell = "";
129
+ for (let index = 1; index < trimmed.length - 1; index += 1) {
130
+ const character = trimmed[index];
131
+ if (character === "|" && isEscapedPipe(trimmed, index)) {
132
+ cell += character;
133
+ } else if (character === "|") {
134
+ cells.push(unescapeTableCell(cell.trim()));
135
+ cell = "";
136
+ } else {
137
+ cell += character;
138
+ }
139
+ }
140
+ cells.push(unescapeTableCell(cell.trim()));
141
+ return cells;
142
+ }
143
+
144
+ function unescapeTableCell(cell: string): string {
145
+ let unescaped = "";
146
+ for (let index = 0; index < cell.length; index += 1) {
147
+ if (cell[index] === "\\" && (cell[index + 1] === "\\" || cell[index + 1] === "|")) {
148
+ index += 1;
149
+ }
150
+ unescaped += cell[index];
151
+ }
152
+ return unescaped;
153
+ }
154
+
155
+ function isEscapedPipe(line: string, pipeIndex: number): boolean {
156
+ let backslashes = 0;
157
+ for (let index = pipeIndex - 1; index >= 0 && line[index] === "\\"; index -= 1) {
158
+ backslashes += 1;
159
+ }
160
+ return backslashes % 2 === 1;
161
+ }
162
+
163
+ function failure(
164
+ code: TaskBeliefDiagnosticCode,
165
+ message: string,
166
+ details: Pick<TaskBeliefParseFailure, "line" | "key" | "column"> = {},
167
+ ): TaskBeliefParseFailure {
168
+ return { success: false, code, message, ...details };
169
+ }
170
+
171
+ function normalizedText(value: string): string {
172
+ return value.trim().replace(/\s+/g, " ");
173
+ }
174
+
175
+ function validateTaskBeliefState(state: TaskBeliefState): void {
176
+ const keys = new Set<string>();
177
+ for (const entry of state.entries) {
178
+ if (!KEY_PATTERN.test(entry.key)) {
179
+ throw new Error("Belief keys must be kebab-case without backticks or newlines.");
180
+ }
181
+ if (keys.has(entry.key)) {
182
+ throw new Error(`Belief key '${entry.key}' appears more than once.`);
183
+ }
184
+ for (const field of ["probability", "target"] as const) {
185
+ const value = entry[field];
186
+ if (!Number.isInteger(value) || value < 0 || value > 100) {
187
+ throw new Error(`Belief ${field} must be an integer from 0 to 100.`);
188
+ }
189
+ }
190
+ if (entry.basis.trim().length === 0 || /[\r\n]/.test(entry.basis)) {
191
+ throw new Error("Belief basis must be a nonempty single line.");
192
+ }
193
+ keys.add(entry.key);
194
+ }
195
+ }
196
+
197
+ function formatTaskBeliefState(state: TaskBeliefState): string {
198
+ validateTaskBeliefState(state);
199
+ const rows = state.entries.map(({ key, probability, target, basis }) => {
200
+ const escapedBasis = normalizedText(basis).replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
201
+ return `| \`${key}\` | ${probability}% | ${target}% | ${escapedBasis} |`;
202
+ });
203
+ return [
204
+ BELIEF_HEADING,
205
+ "",
206
+ "| Belief Key | Probability | Target | Basis |",
207
+ "|---|---:|---:|---|",
208
+ ...rows,
209
+ ].join("\n");
210
+ }
211
+
212
+ /** Parses the unique non-code-fenced canonical Belief State section in Markdown. */
213
+ export function parseTaskBeliefState(markdown: string): TaskBeliefParseResult {
214
+ const sections = sectionRanges(markdown, BELIEF_HEADING);
215
+ if (sections.length === 0) {
216
+ return failure("MISSING_BELIEF_STATE", "Markdown does not contain a Belief State section.");
217
+ }
218
+ if (sections.length > 1) {
219
+ return failure("DUPLICATE_BELIEF_STATE", "Markdown contains more than one Belief State section.");
220
+ }
221
+
222
+ const lines = markdownLines(markdown);
223
+ const section = sections[0];
224
+ const content = lines
225
+ .slice(section.headingLine + 1)
226
+ .filter((line) => line.start < section.end && line.text.trim() !== "");
227
+ const header = content[0] && splitTableRow(content[0].text);
228
+ const separator = content[1] && splitTableRow(content[1].text);
229
+ if (
230
+ !header ||
231
+ header.length !== EXPECTED_HEADERS.length ||
232
+ header.some((cell, index) => cell !== EXPECTED_HEADERS[index]) ||
233
+ !separator ||
234
+ separator.length !== EXPECTED_HEADERS.length ||
235
+ separator.some((cell) => !/^:?-{3,}:?$/.test(cell))
236
+ ) {
237
+ return failure("INVALID_BELIEF_HEADERS", "Belief State must use the canonical four-column headers.");
238
+ }
239
+
240
+ const entries: TaskBeliefEntry[] = [];
241
+ const keys = new Set<string>();
242
+ for (const line of content.slice(2)) {
243
+ const cells = splitTableRow(line.text);
244
+ if (!cells || cells.length !== 4 || cells[3].length === 0) {
245
+ return failure("MALFORMED_BELIEF_ROW", "Belief State contains a malformed row.", {
246
+ line: line.lineNumber,
247
+ });
248
+ }
249
+
250
+ const keyMatch = cells[0].match(/^`([^`]+)`$/);
251
+ if (!keyMatch || !KEY_PATTERN.test(keyMatch[1])) {
252
+ return failure("INVALID_BELIEF_KEY", "Belief keys must be backtick-wrapped kebab-case.", {
253
+ line: line.lineNumber,
254
+ });
255
+ }
256
+ const key = keyMatch[1];
257
+ if (keys.has(key)) {
258
+ return failure("DUPLICATE_BELIEF_KEY", `Belief key '${key}' appears more than once.`, {
259
+ line: line.lineNumber,
260
+ key,
261
+ });
262
+ }
263
+
264
+ for (const [index, column] of [[1, "probability"], [2, "target"]] as const) {
265
+ if (!PERCENT_PATTERN.test(cells[index])) {
266
+ return failure("INVALID_BELIEF_PERCENT", `Belief ${column} must be an integer from 0% to 100%.`, {
267
+ line: line.lineNumber,
268
+ column,
269
+ });
270
+ }
271
+ }
272
+
273
+ keys.add(key);
274
+ entries.push({
275
+ key,
276
+ probability: Number.parseInt(cells[1], 10),
277
+ target: Number.parseInt(cells[2], 10),
278
+ basis: cells[3],
279
+ });
280
+ }
281
+
282
+ return { success: true, state: { entries } };
283
+ }
284
+
285
+ /** Compares two Belief States while ignoring entry order and insignificant whitespace. */
286
+ export function taskBeliefStatesEqual(left: TaskBeliefState, right: TaskBeliefState): boolean {
287
+ if (left.entries.length !== right.entries.length) return false;
288
+
289
+ const byKey = new Map(right.entries.map((entry) => [entry.key, entry]));
290
+ return left.entries.every((entry) => {
291
+ const other = byKey.get(entry.key);
292
+ return other !== undefined &&
293
+ entry.probability === other.probability &&
294
+ entry.target === other.target &&
295
+ normalizedText(entry.basis) === normalizedText(other.basis);
296
+ });
297
+ }
298
+
299
+ /**
300
+ * Replaces a unique Belief State section, or inserts one after Acceptance Criteria content.
301
+ *
302
+ * @throws {Error} If the state is noncanonical or the Markdown contains duplicate sections.
303
+ */
304
+ export function replaceTaskBeliefState(markdown: string, state: TaskBeliefState): string {
305
+ const replacement = formatTaskBeliefState(state);
306
+ const sections = sectionRanges(markdown, BELIEF_HEADING);
307
+ if (sections.length > 1) {
308
+ throw new Error("Cannot replace duplicate Belief State sections.");
309
+ }
310
+ if (sections.length === 1) {
311
+ const section = sections[0];
312
+ const originalSection = markdown.slice(section.start, section.end);
313
+ const trailingWhitespace = originalSection.match(/\s*$/)?.[0] ?? "";
314
+ return markdown.slice(0, section.start) + replacement + trailingWhitespace + markdown.slice(section.end);
315
+ }
316
+
317
+ const acceptance = sectionRanges(markdown, ACCEPTANCE_HEADING)[0];
318
+ const insertion = acceptance?.end ?? markdown.length;
319
+ const before = markdown.slice(0, insertion);
320
+ const after = markdown.slice(insertion);
321
+ const leadingBreaks = before.length === 0 ? "" : before.endsWith("\n\n") ? "" : before.endsWith("\n") ? "\n" : "\n\n";
322
+ const trailingBreaks = after.length === 0 ? "" : after.startsWith("\n") ? "\n" : "\n\n";
323
+ return before + leadingBreaks + replacement + trailingBreaks + after;
324
+ }