@nylorun/runtime 0.36.1-beta → 0.38.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.
- package/CHANGELOG.md +70 -0
- package/README.md +10 -0
- package/dist/api/a2a/card.d.ts +10 -4
- package/dist/api/a2a/card.js +18 -6
- package/dist/api/a2a/endpoint.d.ts +5 -2
- package/dist/api/a2a/endpoint.js +31 -9
- package/dist/api/a2a/extensions.d.ts +41 -0
- package/dist/api/a2a/extensions.js +92 -0
- package/dist/api/a2a/list.d.ts +60 -0
- package/dist/api/a2a/list.js +169 -0
- package/dist/api/a2a/protocol.d.ts +105 -9
- package/dist/api/a2a/protocol.js +169 -18
- package/dist/api/a2a/push.d.ts +76 -0
- package/dist/api/a2a/push.js +305 -0
- package/dist/api/a2a/routes.d.ts +12 -6
- package/dist/api/a2a/routes.js +409 -112
- package/dist/api/a2a/tasks.d.ts +72 -4
- package/dist/api/a2a/tasks.js +305 -27
- package/dist/api/components.d.ts +538 -0
- package/dist/api/components.js +3 -1
- package/dist/api/http/app.js +2 -0
- package/dist/api/http/routes/artifacts.js +19 -0
- package/dist/api/http/routes/turns.d.ts +18 -0
- package/dist/api/http/routes/turns.js +67 -0
- package/dist/api/inbound-files.d.ts +12 -1
- package/dist/api/inbound-files.js +44 -0
- package/dist/api/openapi.js +7 -2
- package/dist/harness/image-engine.d.ts +14 -0
- package/dist/harness/image-engine.js +16 -0
- package/dist/harness/pod.d.ts +2 -0
- package/dist/harness/pod.js +5 -0
- package/dist/harness-api/ws-server.js +7 -1
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/main.js +1 -0
- package/dist/host/stack-config.d.ts +16 -0
- package/dist/host/stack-config.js +30 -0
- package/dist/management-openapi.json +37 -2
- package/dist/openapi.json +1485 -738
- package/dist/reads/types.d.ts +19 -0
- package/dist/sandbox/join.d.ts +7 -1
- package/dist/sandbox/join.js +5 -1
- package/dist/sandbox/pods/builtin.d.ts +18 -0
- package/dist/sandbox/pods/builtin.js +33 -0
- package/dist/sandbox/pods/client.d.ts +5 -4
- package/dist/sandbox/pods/spec.js +14 -1
- package/dist/store/postgres/drizzle/0039_a2a_push.sql +32 -0
- package/dist/store/postgres/drizzle/0040_turn_index.sql +23 -0
- package/dist/store/postgres/drizzle/0041_turn_index_backfill.sql +88 -0
- package/dist/store/postgres/drizzle/0042_worker_engine_image.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/reads.js +41 -1
- package/dist/store/postgres/schema.d.ts +1314 -533
- package/dist/store/postgres/schema.js +94 -0
- package/dist/store/postgres/store.js +135 -3
- package/dist/store/postgres/turns.d.ts +62 -0
- package/dist/store/postgres/turns.js +153 -0
- package/dist/store/types.d.ts +82 -3
- package/dist/tenant/runtime.js +7 -5
- package/dist/tenant/session-model.d.ts +11 -1
- package/dist/tenant/session-model.js +34 -5
- package/dist/tenant/sessions.d.ts +2 -1
- package/dist/tenant/sessions.js +8 -2
- package/dist/tenant/sweep.d.ts +3 -0
- package/dist/tenant/sweep.js +39 -11
- package/dist/tenant/types.d.ts +23 -0
- package/dist/tuning.d.ts +9 -0
- package/dist/tuning.js +11 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { encodeCursor } from "../../record/index.js";
|
|
2
|
+
import { historyCursor, readCursor } from "../../reads/cursor.js";
|
|
3
|
+
import { ENDED_TURN_STATUSES } from "../../store/postgres/turns.js";
|
|
4
|
+
import { loadSession } from "../../tenant/context.js";
|
|
5
|
+
import { HttpError } from "../../tenant/http.js";
|
|
6
|
+
import { readHistory } from "../../tenant/session-streams.js";
|
|
7
|
+
import { a2aFail, parseHistoryLength } from "./protocol.js";
|
|
8
|
+
import { contextSessionId } from "./tasks.js";
|
|
9
|
+
/** `ListTasks`' page size when the request names none, and the most it may ask. */
|
|
10
|
+
export const LIST_PAGE_DEFAULT = 50;
|
|
11
|
+
export const LIST_PAGE_MAX = 100;
|
|
12
|
+
/**
|
|
13
|
+
* The turn index states each A2A state is (profile v2 §1): a paused turn by what its first open
|
|
14
|
+
* interaction asks for. `SUBMITTED` (a queued message), `REJECTED` and anything else: none.
|
|
15
|
+
*/
|
|
16
|
+
const TURN_STATES = {
|
|
17
|
+
TASK_STATE_WORKING: [{ status: "running" }],
|
|
18
|
+
TASK_STATE_INPUT_REQUIRED: [{ status: "paused", waitingFor: ["question"] }],
|
|
19
|
+
TASK_STATE_AUTH_REQUIRED: [{ status: "paused", waitingFor: ["approval", "connection"] }],
|
|
20
|
+
TASK_STATE_COMPLETED: [{ status: "completed" }],
|
|
21
|
+
TASK_STATE_FAILED: [{ status: "failed" }],
|
|
22
|
+
TASK_STATE_CANCELED: [{ status: "cancelled" }],
|
|
23
|
+
};
|
|
24
|
+
const TASK_STATES = [
|
|
25
|
+
"TASK_STATE_SUBMITTED",
|
|
26
|
+
"TASK_STATE_WORKING",
|
|
27
|
+
"TASK_STATE_INPUT_REQUIRED",
|
|
28
|
+
"TASK_STATE_AUTH_REQUIRED",
|
|
29
|
+
"TASK_STATE_COMPLETED",
|
|
30
|
+
"TASK_STATE_FAILED",
|
|
31
|
+
"TASK_STATE_CANCELED",
|
|
32
|
+
"TASK_STATE_REJECTED",
|
|
33
|
+
];
|
|
34
|
+
const VISIBLE = /^[\x21-\x7e]{1,200}$/;
|
|
35
|
+
const present = (value) => value !== undefined && value !== null && value !== "";
|
|
36
|
+
/** `ListTasks` params (A2A 1.0 §3.1.4), checked in the order a client can fix them. */
|
|
37
|
+
export function parseListParams(params) {
|
|
38
|
+
const { contextId, status, statusTimestampAfter, pageSize, pageToken, includeArtifacts } = params;
|
|
39
|
+
if (present(contextId) && (typeof contextId !== "string" || !VISIBLE.test(contextId)))
|
|
40
|
+
a2aFail("invalidParams", "contextId must be 1 to 200 visible ASCII characters", {}, "contextId");
|
|
41
|
+
if (present(status) &&
|
|
42
|
+
status !== "TASK_STATE_UNSPECIFIED" &&
|
|
43
|
+
!TASK_STATES.includes(status))
|
|
44
|
+
a2aFail("invalidParams", "status must be a TaskState", {}, "status");
|
|
45
|
+
let since;
|
|
46
|
+
if (present(statusTimestampAfter)) {
|
|
47
|
+
const time = typeof statusTimestampAfter === "string" &&
|
|
48
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.test(statusTimestampAfter)
|
|
49
|
+
? new Date(statusTimestampAfter)
|
|
50
|
+
: undefined;
|
|
51
|
+
if (!time || Number.isNaN(time.getTime()))
|
|
52
|
+
a2aFail("invalidParams", "statusTimestampAfter must be an ISO 8601 timestamp", {}, "statusTimestampAfter");
|
|
53
|
+
since = time.toISOString();
|
|
54
|
+
}
|
|
55
|
+
if (present(pageSize) &&
|
|
56
|
+
(typeof pageSize !== "number" ||
|
|
57
|
+
!Number.isInteger(pageSize) ||
|
|
58
|
+
pageSize < 1 ||
|
|
59
|
+
pageSize > LIST_PAGE_MAX))
|
|
60
|
+
a2aFail("invalidParams", `pageSize must be an integer from 1 to ${LIST_PAGE_MAX}`, {}, "pageSize");
|
|
61
|
+
if (present(pageToken) && typeof pageToken !== "string")
|
|
62
|
+
a2aFail("invalidParams", "pageToken must be a string", {}, "pageToken");
|
|
63
|
+
if (includeArtifacts !== undefined && includeArtifacts !== null && typeof includeArtifacts !== "boolean")
|
|
64
|
+
a2aFail("invalidParams", "includeArtifacts must be a boolean", {}, "includeArtifacts");
|
|
65
|
+
const historyLength = parseHistoryLength(params.historyLength, "historyLength");
|
|
66
|
+
return {
|
|
67
|
+
...(present(contextId) ? { contextId: contextId } : {}),
|
|
68
|
+
...(present(status) && status !== "TASK_STATE_UNSPECIFIED" ? { status: status } : {}),
|
|
69
|
+
...(since !== undefined ? { statusTimestampAfter: since } : {}),
|
|
70
|
+
pageSize: present(pageSize) ? pageSize : LIST_PAGE_DEFAULT,
|
|
71
|
+
...(present(pageToken) ? { pageToken: pageToken } : {}),
|
|
72
|
+
...(historyLength !== undefined ? { historyLength } : {}),
|
|
73
|
+
includeArtifacts: includeArtifacts === true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* One turn's events, in stream order, read from its first lifecycle event: up to its last one
|
|
78
|
+
* when it has ended, else to the stream's tail. Never the session's whole history.
|
|
79
|
+
*/
|
|
80
|
+
export async function indexedTurnEvents(ctx, row) {
|
|
81
|
+
const after = row.firstSeq > 0 ? encodeCursor(row.sessionId, row.firstSeq - 1) : undefined;
|
|
82
|
+
const { items } = ENDED_TURN_STATUSES.has(row.status)
|
|
83
|
+
? await readHistory(ctx, row.sessionId, after === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: historyCursor(ctx.config.tenantId, row.sessionId).encode(after), undefined, row.lastSeq - row.firstSeq + 1)
|
|
86
|
+
: await readHistory(ctx, row.sessionId, after, undefined);
|
|
87
|
+
return items.filter((event) => event.turnId === row.turnId);
|
|
88
|
+
}
|
|
89
|
+
/** The events of a session's turn, from the turn index; none when the session never had it. */
|
|
90
|
+
export async function turnEventsOf(ctx, sessionId, turnId) {
|
|
91
|
+
const row = await ctx.store.tx((t) => t.turn(sessionId, turnId));
|
|
92
|
+
return row ? indexedTurnEvents(ctx, row) : [];
|
|
93
|
+
}
|
|
94
|
+
/** `ListTasks`: the caller's tasks with this Agent, from the turn index. */
|
|
95
|
+
export async function listTasks(caller, params, build) {
|
|
96
|
+
const { ctx, access, agentId } = caller;
|
|
97
|
+
const owner = access.owner;
|
|
98
|
+
const token = readCursor(ctx.config.tenantId, "a2a-tasks", {
|
|
99
|
+
owner,
|
|
100
|
+
agentId,
|
|
101
|
+
contextId: params.contextId ?? null,
|
|
102
|
+
status: params.status ?? null,
|
|
103
|
+
statusTimestampAfter: params.statusTimestampAfter ?? null,
|
|
104
|
+
});
|
|
105
|
+
let key;
|
|
106
|
+
try {
|
|
107
|
+
key = token.decode(params.pageToken);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (!(error instanceof HttpError))
|
|
111
|
+
throw error;
|
|
112
|
+
return a2aFail("invalidParams", "pageToken is not one this request returned", {}, "pageToken");
|
|
113
|
+
}
|
|
114
|
+
if (key && (key.length !== 3 || key.some((part) => typeof part !== "string")))
|
|
115
|
+
a2aFail("invalidParams", "pageToken is not one this request returned", {}, "pageToken");
|
|
116
|
+
const states = params.status === undefined ? undefined : (TURN_STATES[params.status] ?? []);
|
|
117
|
+
const { turns, total } = await ctx.store.tx((t) => t.listTurns({
|
|
118
|
+
ownerUserId: owner,
|
|
119
|
+
agentId,
|
|
120
|
+
a2a: true,
|
|
121
|
+
...(params.contextId === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { sessionId: contextSessionId(owner, agentId, params.contextId) }),
|
|
124
|
+
...(states === undefined ? {} : { states }),
|
|
125
|
+
...(params.statusTimestampAfter === undefined ? {} : { changedSince: params.statusTimestampAfter }),
|
|
126
|
+
...(key ? { after: { changedAt: key[0], sessionId: key[1], turnId: key[2] } } : {}),
|
|
127
|
+
limit: params.pageSize + 1,
|
|
128
|
+
}));
|
|
129
|
+
const page = turns.slice(0, params.pageSize);
|
|
130
|
+
const last = page.at(-1);
|
|
131
|
+
const tasks = [];
|
|
132
|
+
const sessions = new Map();
|
|
133
|
+
for (const row of page) {
|
|
134
|
+
// Only a context the A2A server opened for this caller: its session id is the context's.
|
|
135
|
+
if (contextSessionId(owner, agentId, row.a2aContextId) !== row.sessionId)
|
|
136
|
+
continue;
|
|
137
|
+
if (!sessions.has(row.sessionId))
|
|
138
|
+
sessions.set(row.sessionId, await sessionOf(caller, row.sessionId));
|
|
139
|
+
const session = sessions.get(row.sessionId);
|
|
140
|
+
const built = session ? await build(row, session, params.historyLength ?? 0) : undefined;
|
|
141
|
+
if (!built)
|
|
142
|
+
continue;
|
|
143
|
+
const { artifacts, ...task } = built;
|
|
144
|
+
tasks.push({
|
|
145
|
+
...task,
|
|
146
|
+
status: { ...task.status, timestamp: row.changedAt },
|
|
147
|
+
...(params.includeArtifacts && artifacts ? { artifacts } : {}),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
tasks,
|
|
152
|
+
nextPageToken: turns.length > params.pageSize && last
|
|
153
|
+
? token.encode([last.changedAt, last.sessionId, last.turnId])
|
|
154
|
+
: "",
|
|
155
|
+
pageSize: params.pageSize,
|
|
156
|
+
totalSize: total,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** The caller's session for a listed task, or undefined when it is gone. */
|
|
160
|
+
async function sessionOf(caller, sessionId) {
|
|
161
|
+
try {
|
|
162
|
+
return await loadSession(caller.ctx, sessionId, caller.access);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error instanceof HttpError && error.status === 404)
|
|
166
|
+
return undefined;
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -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
|
+
};
|
package/dist/api/a2a/protocol.js
CHANGED
|
@@ -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
|
|
4
|
-
* text, data and
|
|
5
|
-
*
|
|
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]
|
|
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
|
-
|
|
169
|
-
|
|
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) =>
|
|
185
|
-
|
|
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
|
-
|
|
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
|
+
}
|