@nylorun/runtime 0.36.1-beta → 0.37.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +10 -0
  3. package/dist/api/a2a/card.d.ts +10 -4
  4. package/dist/api/a2a/card.js +18 -6
  5. package/dist/api/a2a/endpoint.d.ts +5 -2
  6. package/dist/api/a2a/endpoint.js +31 -9
  7. package/dist/api/a2a/extensions.d.ts +41 -0
  8. package/dist/api/a2a/extensions.js +92 -0
  9. package/dist/api/a2a/list.d.ts +60 -0
  10. package/dist/api/a2a/list.js +169 -0
  11. package/dist/api/a2a/protocol.d.ts +105 -9
  12. package/dist/api/a2a/protocol.js +169 -18
  13. package/dist/api/a2a/push.d.ts +76 -0
  14. package/dist/api/a2a/push.js +305 -0
  15. package/dist/api/a2a/routes.d.ts +12 -6
  16. package/dist/api/a2a/routes.js +409 -112
  17. package/dist/api/a2a/tasks.d.ts +72 -4
  18. package/dist/api/a2a/tasks.js +305 -27
  19. package/dist/api/components.d.ts +532 -0
  20. package/dist/api/components.js +3 -1
  21. package/dist/api/http/app.js +2 -0
  22. package/dist/api/http/routes/artifacts.js +19 -0
  23. package/dist/api/http/routes/turns.d.ts +18 -0
  24. package/dist/api/http/routes/turns.js +67 -0
  25. package/dist/api/inbound-files.d.ts +12 -1
  26. package/dist/api/inbound-files.js +44 -0
  27. package/dist/api/openapi.js +7 -2
  28. package/dist/host/config-for.d.ts +2 -0
  29. package/dist/host/config-for.js +1 -0
  30. package/dist/host/main.js +1 -0
  31. package/dist/host/stack-config.d.ts +16 -0
  32. package/dist/host/stack-config.js +30 -0
  33. package/dist/management-openapi.json +5 -2
  34. package/dist/openapi.json +1485 -738
  35. package/dist/reads/types.d.ts +19 -0
  36. package/dist/store/postgres/drizzle/0039_a2a_push.sql +32 -0
  37. package/dist/store/postgres/drizzle/0040_turn_index.sql +23 -0
  38. package/dist/store/postgres/drizzle/0041_turn_index_backfill.sql +88 -0
  39. package/dist/store/postgres/drizzle/meta/_journal.json +21 -0
  40. package/dist/store/postgres/reads.js +41 -1
  41. package/dist/store/postgres/schema.d.ts +1296 -534
  42. package/dist/store/postgres/schema.js +92 -0
  43. package/dist/store/postgres/store.js +133 -3
  44. package/dist/store/postgres/turns.d.ts +62 -0
  45. package/dist/store/postgres/turns.js +153 -0
  46. package/dist/store/types.d.ts +73 -3
  47. package/dist/tenant/runtime.js +7 -5
  48. package/dist/tenant/session-model.d.ts +11 -1
  49. package/dist/tenant/session-model.js +34 -5
  50. package/dist/tenant/sessions.d.ts +2 -1
  51. package/dist/tenant/sessions.js +8 -2
  52. package/dist/tenant/sweep.d.ts +3 -0
  53. package/dist/tenant/sweep.js +39 -11
  54. package/dist/tenant/types.d.ts +23 -0
  55. package/dist/tuning.d.ts +9 -0
  56. package/dist/tuning.js +11 -0
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
1
  import type { LiveEvent } from "@nylorun/core/contracts";
2
- import type { Message, Task } from "./protocol.js";
2
+ import type { Artifact, Message, StreamResponse, Task, TaskState } from "./protocol.js";
3
3
  export declare function taskIdOf(contextId: string, turnId: string): string;
4
4
  /** The context and turn a task id names, or undefined for anything this Runtime never issued. */
5
5
  export declare function parseTaskId(id: string): {
@@ -22,18 +22,75 @@ export interface SessionSnapshot {
22
22
  readonly lastOutput?: unknown;
23
23
  readonly waits?: unknown;
24
24
  }
25
- /** An interaction the paused turn waits for. */
25
+ /**
26
+ * An interaction the paused turn waits for, as profile v2 names it (`interactions/v1`):
27
+ * `approval` (a held tool call or an engine approval, answered with `approve`), `question` (an
28
+ * ask, answered with `respond`) or `connection` (an ask whose metadata says `kind: "connection"`:
29
+ * the agent needs an account the person has not connected; answered with `respond`).
30
+ */
26
31
  export interface PendingInteraction {
27
32
  readonly id: string;
28
- readonly kind: string;
33
+ readonly kind: "approval" | "question" | "connection";
34
+ /** The engine's kind: which command answers it. */
35
+ readonly engineKind: "approval" | "response";
29
36
  readonly prompt: string;
37
+ /** What a person needs to decide: a held call's server, tool and reason; an ask's metadata. */
38
+ readonly detail?: Record<string, unknown>;
30
39
  }
40
+ /** An interaction from a session's waits or a `turn.paused` payload; never a call's input. */
41
+ export declare function interactionOf(raw: unknown): PendingInteraction | undefined;
42
+ /** The interactions a paused session waits for, in order. */
43
+ export declare function pendingInteractions(session: SessionSnapshot): PendingInteraction[];
31
44
  /** The first interaction a paused session waits for. */
32
45
  export declare function pendingInteraction(session: SessionSnapshot): PendingInteraction | undefined;
46
+ /** The state a task waiting on `interaction` is in (profile v2 §1). */
47
+ export declare function interruptedState(interaction: PendingInteraction): TaskState;
48
+ /** The status message that shows an open interaction, with its `interactions/v1` data. */
49
+ export declare function interactionMessage(interaction: PendingInteraction, contextId: string, taskId: string): Message;
50
+ export declare const ENDED: ReadonlySet<string>;
33
51
  /** Events after which a blocking `SendMessage` returns: the turn ended or is interrupted. */
34
52
  export declare const SETTLED_EVENTS: ReadonlySet<string>;
35
53
  /** The turn's conversation as A2A messages: what the caller sent and what the agent said. */
36
54
  export declare function historyOf(events: readonly LiveEvent[], contextId: string, taskId: string): Message[];
55
+ /** How the A2A server shows one task: the ids its caller knows, and where its files are served. */
56
+ export interface TaskView {
57
+ readonly contextId: string;
58
+ /** The task id the caller knows: a queued message's own until a turn takes it. */
59
+ readonly taskId: string;
60
+ /** The Runtime's public URL, for file artifacts' `url` parts (`/v1/a2a/files/…`). */
61
+ readonly baseUrl?: string;
62
+ /** `telemetry/v1`'s trace id: the caller's `traceparent`'s, else the Runtime's own. */
63
+ readonly traceId?: string;
64
+ }
65
+ /**
66
+ * `progress/v1` from a `plan.updated` payload (`{ steps }`, the whole plan each time): at most
67
+ * 20 steps, each a label, a state and an optional note, cut to their lengths. Undefined when the
68
+ * payload holds no plan.
69
+ */
70
+ export declare function progressOf(payload: unknown): {
71
+ steps: Record<string, string>[];
72
+ } | undefined;
73
+ /** `telemetry/v1` from a terminal event's end facts (`totals`) and the task's trace id. */
74
+ export declare function telemetryOf(payload: unknown, traceId: string | undefined): Record<string, unknown> | undefined;
75
+ /** The URL a caller downloads a file artifact's version from (scoped to the caller's subject). */
76
+ export declare function fileUrl(baseUrl: string | undefined, artifactId: string, version: number): string;
77
+ /**
78
+ * The A2A artifact of a file the turn wrote (`artifact.created`, `artifact.version.created`), with
79
+ * `files/v1`, or undefined for the caller's own uploads. A folder is served as its zip.
80
+ */
81
+ export declare function fileArtifact(payload: unknown, baseUrl: string | undefined): Artifact | undefined;
82
+ /**
83
+ * The single translator from session events to A2A updates (profile v2, pushes): what one event
84
+ * of the task's turn changes, as `StreamResponse`s. Every status update carries the event's time
85
+ * as `status.timestamp`; clients order by it. Tool calls, arguments, results and delegations
86
+ * never show (profile v2 §1): only states, step text, plans, interactions, files and the end.
87
+ */
88
+ export declare function updatesOf(event: LiveEvent, view: TaskView): StreamResponse[];
89
+ /**
90
+ * The trace id of a turn the caller started with a `traceparent` (its `command.message`), or
91
+ * undefined: the Runtime's own then (`traceIdOf`).
92
+ */
93
+ export declare function callerTraceId(events: readonly LiveEvent[]): string | undefined;
37
94
  export interface BuildTaskInput {
38
95
  readonly contextId: string;
39
96
  readonly turnId: string;
@@ -41,10 +98,21 @@ export interface BuildTaskInput {
41
98
  /** Every event of the turn, in stream order. */
42
99
  readonly events: readonly LiveEvent[];
43
100
  readonly historyLength?: number;
101
+ /** The id the caller knows the task by, when not the turn's own (a queued message's). */
102
+ readonly taskId?: string;
103
+ readonly baseUrl?: string;
104
+ readonly traceId?: string;
44
105
  }
45
106
  /**
46
107
  * The task for one turn, or undefined when the session never had that turn. An active turn
47
108
  * takes its state from the session row; an ended one from its terminal event, or from the row
48
- * while that event has not reached the stream yet.
109
+ * while that event has not reached the stream yet. Its metadata holds the latest plan
110
+ * (`progress/v1`), the uncertain effect (`effects/v1`) and, once it ended, `telemetry/v1`; its
111
+ * artifacts are the output and the files the turn wrote (`files/v1`).
49
112
  */
50
113
  export declare function buildTask(input: BuildTaskInput): Task | undefined;
114
+ /**
115
+ * A queued message's task while it waits (profile v2 §1): `SUBMITTED`, from its `message.queued`
116
+ * event, until a turn takes it.
117
+ */
118
+ export declare function submittedTask(contextId: string, taskId: string, queued: LiveEvent): Task;
@@ -2,10 +2,14 @@
2
2
  * A2A tasks over Nylorun sessions: a context is one session per subject, agent and
3
3
  * `contextId`; a task is one turn of it. The task id names both (`t1.<context>.<turn>`) until
4
4
  * the turn index lets a turn id stand alone; it is parsed, never trusted, because the session
5
- * is always derived from the caller's own subject. `buildTask` reads the state from the session
6
- * row and the turn's events; nothing here touches the store.
5
+ * is always derived from the caller's own subject; a queued message's task is named by the
6
+ * message's queue id until a turn takes it. `buildTask` reads the state from the session row and
7
+ * the turn's events, with the profile v2 data (interactions, progress, telemetry, effects,
8
+ * files); `updatesOf` is the one translator from a session event to the A2A updates pushes carry.
9
+ * Nothing here touches the store.
7
10
  */
8
11
  import { createHash } from "node:crypto";
12
+ import { A2A_EXTENSIONS } from "./extensions.js";
9
13
  const TASK_ID_PREFIX = "t1";
10
14
  export function taskIdOf(contextId, turnId) {
11
15
  return `${TASK_ID_PREFIX}.${Buffer.from(contextId, "utf8").toString("base64url")}.${turnId}`;
@@ -41,23 +45,73 @@ export function newContextId(subject, agentId, messageId) {
41
45
  .digest("hex");
42
46
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-8${hex.slice(13, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
43
47
  }
44
- /** The first interaction a paused session waits for. */
45
- export function pendingInteraction(session) {
46
- if (!Array.isArray(session.waits))
48
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
49
+ /** An interaction from a session's waits or a `turn.paused` payload; never a call's input. */
50
+ export function interactionOf(raw) {
51
+ if (!isRecord(raw) || typeof raw.id !== "string")
47
52
  return undefined;
48
- for (const call of session.waits) {
49
- const interaction = call?.interaction;
50
- if (call?.status !== "interaction" || typeof interaction?.id !== "string")
51
- continue;
52
- return {
53
- id: interaction.id,
54
- kind: typeof interaction.kind === "string" ? interaction.kind : "response",
55
- prompt: typeof interaction.prompt === "string" ? interaction.prompt : "",
56
- };
53
+ const engineKind = raw.kind === "approval" ? "approval" : "response";
54
+ const metadata = isRecord(raw.metadata) ? raw.metadata : undefined;
55
+ const kind = raw.kind === "connection" || metadata?.kind === "connection"
56
+ ? "connection"
57
+ : engineKind === "approval"
58
+ ? "approval"
59
+ : "question";
60
+ let detail;
61
+ if (engineKind === "approval" && kind === "approval") {
62
+ // A held tool call names its tool; its arguments stay with the Runtime (profile v2 §1).
63
+ const held = Object.fromEntries(["server", "tool", "reason"].flatMap((key) => typeof raw[key] === "string" ? [[key, raw[key]]] : []));
64
+ detail = Object.keys(held).length > 0 ? held : metadata;
57
65
  }
58
- return undefined;
66
+ else if (metadata) {
67
+ const { kind: _kind, ...rest } = metadata;
68
+ detail = Object.keys(rest).length > 0 ? rest : undefined;
69
+ }
70
+ return {
71
+ id: raw.id,
72
+ kind,
73
+ engineKind,
74
+ prompt: typeof raw.prompt === "string" ? raw.prompt : "",
75
+ ...(detail ? { detail } : {}),
76
+ };
59
77
  }
60
- const ENDED = new Set(["turn.completed", "turn.failed", "turn.cancelled"]);
78
+ /** The interactions a paused session waits for, in order. */
79
+ export function pendingInteractions(session) {
80
+ if (!Array.isArray(session.waits))
81
+ return [];
82
+ return session.waits.flatMap((call) => {
83
+ const interaction = call?.status === "interaction" ? interactionOf(call.interaction) : undefined;
84
+ return interaction ? [interaction] : [];
85
+ });
86
+ }
87
+ /** The first interaction a paused session waits for. */
88
+ export function pendingInteraction(session) {
89
+ return pendingInteractions(session)[0];
90
+ }
91
+ /** The state a task waiting on `interaction` is in (profile v2 §1). */
92
+ export function interruptedState(interaction) {
93
+ return interaction.kind === "question" ? "TASK_STATE_INPUT_REQUIRED" : "TASK_STATE_AUTH_REQUIRED";
94
+ }
95
+ /** The status message that shows an open interaction, with its `interactions/v1` data. */
96
+ export function interactionMessage(interaction, contextId, taskId) {
97
+ return {
98
+ messageId: interaction.id,
99
+ role: "ROLE_AGENT",
100
+ parts: [{ text: interaction.prompt }],
101
+ contextId,
102
+ taskId,
103
+ extensions: [A2A_EXTENSIONS.interactions],
104
+ metadata: {
105
+ [A2A_EXTENSIONS.interactions]: {
106
+ interactionId: interaction.id,
107
+ kind: interaction.kind,
108
+ prompt: interaction.prompt,
109
+ ...(interaction.detail ? { detail: interaction.detail } : {}),
110
+ },
111
+ },
112
+ };
113
+ }
114
+ export const ENDED = new Set(["turn.completed", "turn.failed", "turn.cancelled"]);
61
115
  /** Events after which a blocking `SendMessage` returns: the turn ended or is interrupted. */
62
116
  export const SETTLED_EVENTS = new Set([
63
117
  ...ENDED,
@@ -121,14 +175,200 @@ function failureText(code) {
121
175
  ? `The agent failed (${code}).`
122
176
  : "The agent failed.";
123
177
  }
178
+ const UNCERTAIN_TEXT = "An action's outcome is uncertain; the agent's operator must resolve it before the task continues.";
179
+ // ---------------------------------------------------------------------------
180
+ // Profile v2 data
181
+ /** The states of a plan step (`progress/v1`, Contract 1). */
182
+ const STEP_STATES = new Set(["pending", "active", "done", "blocked", "skipped"]);
183
+ const MAX_STEPS = 20;
184
+ const MAX_LABEL = 120;
185
+ const MAX_NOTE = 280;
186
+ /**
187
+ * `progress/v1` from a `plan.updated` payload (`{ steps }`, the whole plan each time): at most
188
+ * 20 steps, each a label, a state and an optional note, cut to their lengths. Undefined when the
189
+ * payload holds no plan.
190
+ */
191
+ export function progressOf(payload) {
192
+ if (!isRecord(payload) || !Array.isArray(payload.steps))
193
+ return undefined;
194
+ const steps = payload.steps.slice(0, MAX_STEPS).flatMap((step) => {
195
+ if (!isRecord(step) || typeof step.label !== "string")
196
+ return [];
197
+ const state = typeof step.state === "string" && STEP_STATES.has(step.state) ? step.state : "pending";
198
+ return [
199
+ {
200
+ label: step.label.slice(0, MAX_LABEL),
201
+ state,
202
+ ...(typeof step.note === "string" && step.note !== "" ? { note: step.note.slice(0, MAX_NOTE) } : {}),
203
+ },
204
+ ];
205
+ });
206
+ return { steps };
207
+ }
208
+ /** `telemetry/v1` from a terminal event's end facts (`totals`) and the task's trace id. */
209
+ export function telemetryOf(payload, traceId) {
210
+ const totals = isRecord(payload) && isRecord(payload.totals) ? payload.totals : undefined;
211
+ const value = {
212
+ ...(totals
213
+ ? {
214
+ totals: {
215
+ steps: Number(totals.steps ?? 0),
216
+ tokens: Number(totals.tokens ?? 0),
217
+ costUsd: Number(totals.costUsd ?? 0),
218
+ },
219
+ }
220
+ : {}),
221
+ ...(traceId ? { traceId } : {}),
222
+ };
223
+ return Object.keys(value).length > 0 ? value : undefined;
224
+ }
225
+ /** The URL a caller downloads a file artifact's version from (scoped to the caller's subject). */
226
+ export function fileUrl(baseUrl, artifactId, version) {
227
+ return `${baseUrl ?? ""}/v1/a2a/files/${encodeURIComponent(artifactId)}/versions/${version}`;
228
+ }
229
+ /**
230
+ * The A2A artifact of a file the turn wrote (`artifact.created`, `artifact.version.created`), with
231
+ * `files/v1`, or undefined for the caller's own uploads. A folder is served as its zip.
232
+ */
233
+ export function fileArtifact(payload, baseUrl) {
234
+ if (!isRecord(payload) || typeof payload.artifactId !== "string" || typeof payload.name !== "string")
235
+ return undefined;
236
+ if (payload.source === "upload")
237
+ return undefined;
238
+ const version = Number(payload.version);
239
+ const folder = payload.kind === "folder";
240
+ return {
241
+ artifactId: `${payload.artifactId}:${payload.name}`,
242
+ name: payload.name,
243
+ parts: [
244
+ {
245
+ url: fileUrl(baseUrl, payload.artifactId, version),
246
+ mediaType: folder ? "application/zip" : String(payload.contentType ?? "application/octet-stream"),
247
+ filename: folder ? `${payload.name}.zip` : payload.name,
248
+ },
249
+ ],
250
+ extensions: [A2A_EXTENSIONS.files],
251
+ metadata: {
252
+ [A2A_EXTENSIONS.files]: {
253
+ kind: folder ? "folder" : "file",
254
+ path: payload.name,
255
+ version,
256
+ size: Number(payload.size ?? 0),
257
+ sha256: String(payload.sha256 ?? ""),
258
+ ...(typeof payload.fileCount === "number" ? { fileCount: payload.fileCount } : {}),
259
+ },
260
+ },
261
+ };
262
+ }
263
+ const FILE_EVENTS = new Set(["artifact.created", "artifact.version.created"]);
264
+ function outputArtifact(output) {
265
+ return output === undefined || output === null
266
+ ? undefined
267
+ : { artifactId: "output", name: "output", parts: [outputPart(output)] };
268
+ }
269
+ /**
270
+ * The single translator from session events to A2A updates (profile v2, pushes): what one event
271
+ * of the task's turn changes, as `StreamResponse`s. Every status update carries the event's time
272
+ * as `status.timestamp`; clients order by it. Tool calls, arguments, results and delegations
273
+ * never show (profile v2 §1): only states, step text, plans, interactions, files and the end.
274
+ */
275
+ export function updatesOf(event, view) {
276
+ const { contextId, taskId } = view;
277
+ const payload = (event.payload ?? {});
278
+ const status = (state, extra = {}) => ({
279
+ statusUpdate: {
280
+ taskId,
281
+ contextId,
282
+ status: { state, ...(extra.message ? { message: extra.message } : {}), timestamp: event.time },
283
+ ...(extra.metadata ? { metadata: extra.metadata } : {}),
284
+ },
285
+ });
286
+ const artifact = (value) => ({
287
+ artifactUpdate: { taskId, contextId, artifact: value, append: false, lastChunk: true },
288
+ });
289
+ switch (event.type) {
290
+ case "command.message":
291
+ case "command.approve":
292
+ case "command.respond":
293
+ return [status("TASK_STATE_WORKING")];
294
+ case "plan.updated": {
295
+ const progress = progressOf(payload);
296
+ return progress ? [status("TASK_STATE_WORKING", { metadata: { [A2A_EXTENSIONS.progress]: progress } })] : [];
297
+ }
298
+ case "message.assistant":
299
+ // The agent's own step text; an agent used as a tool speaks inside the turn.
300
+ return !payload.agent && typeof payload.text === "string" && payload.text.trim() !== ""
301
+ ? [status("TASK_STATE_WORKING", { message: agentMessage(event.eventId, payload.text, contextId, taskId) })]
302
+ : [];
303
+ case "effect.uncertain":
304
+ return [
305
+ status("TASK_STATE_WORKING", {
306
+ message: agentMessage(`${event.eventId}:uncertain`, UNCERTAIN_TEXT, contextId, taskId),
307
+ metadata: { [A2A_EXTENSIONS.effects]: { uncertain: String(payload.effectId ?? "") } },
308
+ }),
309
+ ];
310
+ case "turn.paused":
311
+ return (Array.isArray(payload.interactions) ? payload.interactions : []).flatMap((entry) => {
312
+ const interaction = interactionOf(entry?.interaction);
313
+ return interaction
314
+ ? [status(interruptedState(interaction), { message: interactionMessage(interaction, contextId, taskId) })]
315
+ : [];
316
+ });
317
+ case "artifact.created":
318
+ case "artifact.version.created": {
319
+ const file = fileArtifact(payload, view.baseUrl);
320
+ return file ? [artifact(file)] : [];
321
+ }
322
+ case "turn.completed":
323
+ case "turn.failed":
324
+ case "turn.cancelled": {
325
+ const telemetry = telemetryOf(payload, view.traceId);
326
+ const metadata = telemetry ? { [A2A_EXTENSIONS.telemetry]: telemetry } : undefined;
327
+ if (event.type === "turn.completed") {
328
+ const output = outputArtifact(payload.output);
329
+ return [
330
+ ...(output ? [artifact(output)] : []),
331
+ status("TASK_STATE_COMPLETED", metadata ? { metadata } : {}),
332
+ ];
333
+ }
334
+ if (event.type === "turn.cancelled")
335
+ return [status("TASK_STATE_CANCELED", metadata ? { metadata } : {})];
336
+ return [
337
+ status("TASK_STATE_FAILED", {
338
+ message: agentMessage(`${event.turnId}:failed`, failureText(payload.error?.code), contextId, taskId),
339
+ ...(metadata ? { metadata } : {}),
340
+ }),
341
+ ];
342
+ }
343
+ default:
344
+ return [];
345
+ }
346
+ }
347
+ /**
348
+ * The trace id of a turn the caller started with a `traceparent` (its `command.message`), or
349
+ * undefined: the Runtime's own then (`traceIdOf`).
350
+ */
351
+ export function callerTraceId(events) {
352
+ for (const event of events) {
353
+ if (event.type !== "command.message")
354
+ continue;
355
+ const traceparent = event.payload?.trace?.traceparent;
356
+ const match = typeof traceparent === "string" ? /^[0-9a-f]{2}-([0-9a-f]{32})-/.exec(traceparent) : null;
357
+ if (match)
358
+ return match[1];
359
+ }
360
+ return undefined;
361
+ }
124
362
  /**
125
363
  * The task for one turn, or undefined when the session never had that turn. An active turn
126
364
  * takes its state from the session row; an ended one from its terminal event, or from the row
127
- * while that event has not reached the stream yet.
365
+ * while that event has not reached the stream yet. Its metadata holds the latest plan
366
+ * (`progress/v1`), the uncertain effect (`effects/v1`) and, once it ended, `telemetry/v1`; its
367
+ * artifacts are the output and the files the turn wrote (`files/v1`).
128
368
  */
129
369
  export function buildTask(input) {
130
370
  const { contextId, turnId, session, events } = input;
131
- const taskId = taskIdOf(contextId, turnId);
371
+ const taskId = input.taskId ?? taskIdOf(contextId, turnId);
132
372
  const active = session.activeTurnId === turnId;
133
373
  const terminal = [...events].reverse().find((event) => ENDED.has(event.type));
134
374
  const latest = session.lastTurnId === turnId && !active;
@@ -136,22 +376,27 @@ export function buildTask(input) {
136
376
  return undefined;
137
377
  let state;
138
378
  let message;
139
- let artifacts;
140
- const complete = (output) => {
379
+ let output;
380
+ const metadata = {};
381
+ const complete = (value) => {
141
382
  state = "TASK_STATE_COMPLETED";
142
- if (output !== undefined && output !== null)
143
- artifacts = [{ artifactId: "output", name: "output", parts: [outputPart(output)] }];
383
+ output = outputArtifact(value);
144
384
  };
145
385
  if (active) {
146
386
  const pending = session.status === "paused" ? pendingInteraction(session) : undefined;
147
387
  if (pending) {
148
- state = "TASK_STATE_INPUT_REQUIRED";
149
- message = agentMessage(pending.id, pending.prompt, contextId, taskId);
388
+ state = interruptedState(pending);
389
+ message = interactionMessage(pending, contextId, taskId);
150
390
  }
151
391
  else {
152
392
  state = "TASK_STATE_WORKING";
153
- if (session.status === "uncertain")
154
- message = agentMessage(`${turnId}:uncertain`, "An action's outcome is uncertain; the agent's operator must resolve it before the task continues.", contextId, taskId);
393
+ if (session.status === "uncertain") {
394
+ message = agentMessage(`${turnId}:uncertain`, UNCERTAIN_TEXT, contextId, taskId);
395
+ const uncertain = [...events].reverse().find((event) => event.type === "effect.uncertain");
396
+ const effectId = uncertain?.payload?.effectId;
397
+ if (typeof effectId === "string")
398
+ metadata[A2A_EXTENSIONS.effects] = { uncertain: effectId };
399
+ }
155
400
  }
156
401
  }
157
402
  else if (terminal) {
@@ -164,6 +409,9 @@ export function buildTask(input) {
164
409
  state = "TASK_STATE_FAILED";
165
410
  message = agentMessage(`${turnId}:failed`, failureText(payload.error?.code), contextId, taskId);
166
411
  }
412
+ const telemetry = telemetryOf(payload, input.traceId);
413
+ if (telemetry)
414
+ metadata[A2A_EXTENSIONS.telemetry] = telemetry;
167
415
  }
168
416
  else if (latest && session.status === "completed")
169
417
  complete(session.lastOutput);
@@ -175,6 +423,23 @@ export function buildTask(input) {
175
423
  }
176
424
  else
177
425
  state = "TASK_STATE_WORKING";
426
+ // The latest plan, and the files the turn wrote (the latest version of each, none deleted).
427
+ const files = new Map();
428
+ for (const event of events) {
429
+ if (event.type === "plan.updated") {
430
+ const progress = progressOf(event.payload);
431
+ if (progress)
432
+ metadata[A2A_EXTENSIONS.progress] = progress;
433
+ }
434
+ else if (FILE_EVENTS.has(event.type)) {
435
+ const file = fileArtifact(event.payload, input.baseUrl);
436
+ if (file)
437
+ files.set(String(event.payload.artifactId), file);
438
+ }
439
+ else if (event.type === "artifact.deleted")
440
+ files.delete(String(event.payload.artifactId));
441
+ }
442
+ const artifacts = [...(output ? [output] : []), ...files.values()];
178
443
  const timestamp = events.at(-1)?.time;
179
444
  const history = historyOf(events, contextId, taskId);
180
445
  const limit = input.historyLength;
@@ -186,7 +451,20 @@ export function buildTask(input) {
186
451
  ...(message ? { message } : {}),
187
452
  ...(timestamp ? { timestamp } : {}),
188
453
  },
189
- ...(artifacts ? { artifacts } : {}),
454
+ ...(artifacts.length > 0 ? { artifacts } : {}),
190
455
  ...(limit === 0 ? {} : { history: limit === undefined ? history : history.slice(-limit) }),
456
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
457
+ };
458
+ }
459
+ /**
460
+ * A queued message's task while it waits (profile v2 §1): `SUBMITTED`, from its `message.queued`
461
+ * event, until a turn takes it.
462
+ */
463
+ export function submittedTask(contextId, taskId, queued) {
464
+ return {
465
+ id: taskId,
466
+ contextId,
467
+ status: { state: "TASK_STATE_SUBMITTED", timestamp: queued.time },
468
+ history: [],
191
469
  };
192
470
  }