@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,4 +1,5 @@
1
1
  import type { ArtifactKind, ArtifactPage, ModelCallExportPage, ModelCallsPage, ToolCallsPage, SessionManifestView, SessionPage, SessionListItem, SessionUsageTotals, LiveEvent } from "@nylorun/core/contracts";
2
+ import type { TurnRow } from "../store/types.js";
2
3
  export type ReadAccess = {
3
4
  owner?: string;
4
5
  agents?: readonly string[];
@@ -14,6 +15,12 @@ export type PageOptions = {
14
15
  limit: number;
15
16
  cursor?: string;
16
17
  };
18
+ /** A page of the turn change feed: rows in the order they changed, and where to read on. */
19
+ export type TurnChangeRows = {
20
+ turns: TurnRow[];
21
+ cursor: string;
22
+ hasMore: boolean;
23
+ };
17
24
  export type ArtifactFilters = {
18
25
  sessionId?: string;
19
26
  kind?: ArtifactKind;
@@ -34,5 +41,17 @@ export interface ReadStore {
34
41
  * for a turn the session never had.
35
42
  */
36
43
  turnEvents(id: string, turnId: string, access: ReadAccess): Promise<LiveEvent[]>;
44
+ /**
45
+ * The turn index's change feed (Host feature `turns`): the turns `access` reaches (of one
46
+ * session with `sessionId`), in the order their transactions changed them, after `changedAfter`
47
+ * (a cursor it returned). Only changes below every transaction still running are read, so a
48
+ * change committed later never lands behind the cursor.
49
+ */
50
+ turnChanges(filters: {
51
+ sessionId?: string;
52
+ }, page: {
53
+ limit: number;
54
+ changedAfter?: string;
55
+ }, access: ReadAccess): Promise<TurnChangeRows>;
37
56
  close(): Promise<void>;
38
57
  }
@@ -0,0 +1,32 @@
1
+ CREATE TABLE "nylorun"."a2a_push_configs" (
2
+ "session_id" text COLLATE "C" NOT NULL,
3
+ "task_id" text COLLATE "C" NOT NULL,
4
+ "config_id" text COLLATE "C" NOT NULL,
5
+ "context_id" text NOT NULL,
6
+ "turn_id" text COLLATE "C" NOT NULL,
7
+ "url" text NOT NULL,
8
+ "token" text,
9
+ "authentication" json,
10
+ "base_url" text NOT NULL,
11
+ "extensions" json NOT NULL,
12
+ "trace_id" text,
13
+ "next_seq" bigint NOT NULL,
14
+ "created_at" timestamp with time zone DEFAULT clock_timestamp() NOT NULL,
15
+ "done_at" timestamp with time zone,
16
+ CONSTRAINT "a2a_push_configs_pkey" PRIMARY KEY("session_id","task_id","config_id")
17
+ );
18
+ --> statement-breakpoint
19
+ CREATE TABLE "nylorun"."a2a_pushes" (
20
+ "id" text COLLATE "C" PRIMARY KEY NOT NULL,
21
+ "session_id" text COLLATE "C" NOT NULL,
22
+ "task_id" text COLLATE "C" NOT NULL,
23
+ "config_id" text COLLATE "C" NOT NULL,
24
+ "body" text NOT NULL,
25
+ "created_at" timestamp with time zone DEFAULT clock_timestamp() NOT NULL,
26
+ "attempts" integer DEFAULT 0 NOT NULL,
27
+ "retry_at" timestamp with time zone,
28
+ "parked_at" timestamp with time zone
29
+ );
30
+ --> statement-breakpoint
31
+ CREATE INDEX "a2a_push_configs_done_at" ON "nylorun"."a2a_push_configs" USING btree ("done_at");--> statement-breakpoint
32
+ CREATE INDEX "a2a_pushes_created_at" ON "nylorun"."a2a_pushes" USING btree ("created_at");
@@ -0,0 +1,23 @@
1
+ CREATE TABLE "nylorun"."turns" (
2
+ "session_id" text COLLATE "C" NOT NULL,
3
+ "turn_id" text COLLATE "C" NOT NULL,
4
+ "agent_id" text COLLATE "C" NOT NULL,
5
+ "owner_user_id" text COLLATE "C",
6
+ "a2a_context_id" text COLLATE "C",
7
+ "status" text NOT NULL,
8
+ "started_at" text COLLATE "C" NOT NULL,
9
+ "ended_at" text COLLATE "C",
10
+ "changed_at" text COLLATE "C" NOT NULL,
11
+ "totals" json,
12
+ "waiting_for" text,
13
+ "queued_message_ids" text[] DEFAULT '{}' NOT NULL,
14
+ "first_seq" bigint NOT NULL,
15
+ "last_seq" bigint NOT NULL,
16
+ "txid" "xid8" DEFAULT pg_current_xact_id() NOT NULL,
17
+ CONSTRAINT "turns_pkey" PRIMARY KEY("session_id","turn_id")
18
+ );
19
+ --> statement-breakpoint
20
+ CREATE INDEX "turns_turn" ON "nylorun"."turns" USING btree ("turn_id");--> statement-breakpoint
21
+ CREATE INDEX "turns_changes" ON "nylorun"."turns" USING btree ("txid","session_id","turn_id");--> statement-breakpoint
22
+ CREATE INDEX "turns_owner_changes" ON "nylorun"."turns" USING btree ("owner_user_id","txid");--> statement-breakpoint
23
+ CREATE INDEX "turns_owner_agent" ON "nylorun"."turns" USING btree ("owner_user_id","agent_id","changed_at");
@@ -0,0 +1,88 @@
1
+ -- Custom SQL (a data change): the turn index (0040) for the turns the record already holds. Each
2
+ -- turn's row is what `Tx.event` would have written for its lifecycle events (`command.message`,
3
+ -- `command.approve`, `command.respond`, `turn.paused`, `turn.completed`, `turn.failed`,
4
+ -- `turn.cancelled`): its status from the last of them (and, paused, what its first open
5
+ -- interaction asks for), its start from the first, and the queued messages it took. Its session's
6
+ -- Agent, owner and A2A context come from the session row as it is now. Rows written from here on
7
+ -- come from `Tx.event`, in the transaction of the event.
8
+ WITH lifecycle AS (
9
+ SELECT "session_id", "seq", "type", nylorun.doc("body") AS doc
10
+ FROM "nylorun_streams"."session_events"
11
+ WHERE "type" IN (
12
+ 'command.message', 'command.approve', 'command.respond',
13
+ 'turn.paused', 'turn.completed', 'turn.failed', 'turn.cancelled'
14
+ )
15
+ ), turned AS (
16
+ SELECT "session_id", doc->>'turnId' AS turn_id, "seq", "type", doc
17
+ FROM lifecycle
18
+ WHERE doc->>'turnId' IS NOT NULL
19
+ ), bounds AS (
20
+ SELECT "session_id", turn_id, min("seq") AS first_seq,
21
+ min(doc->>'time') AS started_at,
22
+ coalesce(
23
+ array_remove(
24
+ array_agg(doc->'payload'->>'queuedMessageId' ORDER BY "seq")
25
+ FILTER (WHERE "type" = 'command.message'),
26
+ NULL
27
+ ),
28
+ '{}'
29
+ ) AS queued_message_ids
30
+ FROM turned
31
+ GROUP BY "session_id", turn_id
32
+ ), latest AS (
33
+ -- A turn's further messages (a queued batch) change nothing but its queued messages.
34
+ SELECT DISTINCT ON (t."session_id", t.turn_id) t."session_id", t.turn_id, t."seq", t."type", t.doc
35
+ FROM turned t
36
+ JOIN bounds b ON b."session_id" = t."session_id" AND b.turn_id = t.turn_id
37
+ WHERE NOT (t."type" = 'command.message' AND t."seq" > b.first_seq)
38
+ ORDER BY t."session_id", t.turn_id, t."seq" DESC
39
+ )
40
+ INSERT INTO "nylorun"."turns" (
41
+ "session_id", "turn_id", "agent_id", "owner_user_id", "a2a_context_id", "status",
42
+ "started_at", "ended_at", "changed_at", "totals", "waiting_for", "queued_message_ids",
43
+ "first_seq", "last_seq"
44
+ )
45
+ SELECT
46
+ b."session_id",
47
+ b.turn_id,
48
+ coalesce(s."agent_id", ''),
49
+ s."owner_user_id",
50
+ nylorun.doc(s."body")->'info'->'a2a'->>'contextId',
51
+ CASE l."type"
52
+ WHEN 'turn.completed' THEN 'completed'
53
+ WHEN 'turn.failed' THEN 'failed'
54
+ WHEN 'turn.cancelled' THEN 'cancelled'
55
+ WHEN 'turn.paused' THEN 'paused'
56
+ ELSE 'running'
57
+ END,
58
+ b.started_at,
59
+ CASE WHEN l."type" IN ('turn.completed', 'turn.failed', 'turn.cancelled') THEN l.doc->>'time' END,
60
+ l.doc->>'time',
61
+ CASE WHEN l."type" IN ('turn.completed', 'turn.failed', 'turn.cancelled')
62
+ AND jsonb_typeof(l.doc->'payload'->'totals'->'steps') = 'number'
63
+ AND jsonb_typeof(l.doc->'payload'->'totals'->'tokens') = 'number'
64
+ AND jsonb_typeof(l.doc->'payload'->'totals'->'costUsd') = 'number'
65
+ THEN json_build_object(
66
+ 'steps', l.doc->'payload'->'totals'->'steps',
67
+ 'tokens', l.doc->'payload'->'totals'->'tokens',
68
+ 'costUsd', l.doc->'payload'->'totals'->'costUsd'
69
+ ) END,
70
+ CASE WHEN l."type" = 'turn.paused' AND jsonb_typeof(l.doc->'payload'->'interactions') = 'array' THEN (
71
+ SELECT CASE
72
+ WHEN x.e->'interaction'->>'kind' = 'connection'
73
+ OR x.e->'interaction'->'metadata'->>'kind' = 'connection' THEN 'connection'
74
+ WHEN x.e->'interaction'->>'kind' = 'approval' THEN 'approval'
75
+ ELSE 'question'
76
+ END
77
+ FROM jsonb_array_elements(l.doc->'payload'->'interactions') WITH ORDINALITY AS x(e, i)
78
+ WHERE (x.e->>'status' IS NULL OR x.e->>'status' = 'interaction')
79
+ AND jsonb_typeof(x.e->'interaction'->'id') = 'string'
80
+ ORDER BY x.i
81
+ LIMIT 1
82
+ ) END,
83
+ b.queued_message_ids,
84
+ b.first_seq,
85
+ l."seq"
86
+ FROM bounds b
87
+ JOIN latest l ON l."session_id" = b."session_id" AND l.turn_id = b.turn_id
88
+ LEFT JOIN "nylorun"."sessions" s ON s."id" = b."session_id";
@@ -274,6 +274,27 @@
274
274
  "when": 1792484344984,
275
275
  "tag": "0038_t6_agent_manifest_source",
276
276
  "breakpoints": true
277
+ },
278
+ {
279
+ "idx": 39,
280
+ "version": "7",
281
+ "when": 1793445550433,
282
+ "tag": "0039_a2a_push",
283
+ "breakpoints": true
284
+ },
285
+ {
286
+ "idx": 40,
287
+ "version": "7",
288
+ "when": 1793448351698,
289
+ "tag": "0040_turn_index",
290
+ "breakpoints": true
291
+ },
292
+ {
293
+ "idx": 41,
294
+ "version": "7",
295
+ "when": 1793448353003,
296
+ "tag": "0041_turn_index_backfill",
297
+ "breakpoints": true
277
298
  }
278
299
  ]
279
300
  }
@@ -11,7 +11,7 @@ import { database, driverError } from "./db.js";
11
11
  import { createPostgresReadClient } from "./connect.js";
12
12
  import { hostTuning } from "../../tuning.js";
13
13
  import { toolCallDuration, toolCallStatus } from "../../harness-api/tool-status.js";
14
- import { effects, artifacts, modelUsage, sandboxes, sessions, sessionEvents, tenantSettings, } from "./schema.js";
14
+ import { effects, artifacts, modelUsage, sandboxes, sessions, sessionEvents, tenantSettings, turns, } from "./schema.js";
15
15
  const sessionFields = {
16
16
  id: sessions.id,
17
17
  agentId: sessions.agentId,
@@ -101,6 +101,20 @@ function publicToolCall(body) {
101
101
  durationMs: toolCallDuration(body) ?? null,
102
102
  };
103
103
  }
104
+ /** The change feed's cursor key: the last row's `[txid, sessionId, turnId]`. */
105
+ function turnChangeKey(key) {
106
+ if (key === undefined)
107
+ return undefined;
108
+ const [txid, sessionId, turnId] = key;
109
+ if (key.length !== 3 ||
110
+ typeof txid !== "string" ||
111
+ typeof sessionId !== "string" ||
112
+ typeof turnId !== "string" ||
113
+ !/^\d{1,20}$/.test(txid) ||
114
+ BigInt(txid) > 18446744073709551615n)
115
+ return fail(400, "Invalid cursor key", { code: "cursor_invalid" });
116
+ return [txid, sessionId, turnId];
117
+ }
104
118
  function keyOf(key, length, timestamp = false, nullable = false) {
105
119
  if (key &&
106
120
  (key.length !== length ||
@@ -316,6 +330,32 @@ export function createPostgresReadStore(source, tenantId) {
316
330
  .orderBy(asc(sessionEvents.seq));
317
331
  return rows.map((row) => row.body);
318
332
  }),
333
+ turnChanges: (filters, page, access) => read(async (tx) => {
334
+ if (filters.sessionId !== undefined)
335
+ await requireSession(tx, filters.sessionId, access);
336
+ const cursor = readCursor(tenantId, "turns", { sessionId: filters.sessionId ?? null });
337
+ const key = turnChangeKey(cursor.decode(page.changedAfter));
338
+ const rows = await tx
339
+ .select()
340
+ .from(turns)
341
+ .where(and(
342
+ // Below every transaction still running: one that commits later sorts after.
343
+ sql `${turns.txid} < pg_snapshot_xmin(pg_current_snapshot())`, key
344
+ ? sql `(${turns.txid}, ${turns.sessionId}, ${turns.turnId}) > (${key[0]}::xid8, ${key[1]}, ${key[2]})`
345
+ : undefined, filters.sessionId === undefined ? undefined : eq(turns.sessionId, filters.sessionId), access.owner === undefined ? undefined : eq(turns.ownerUserId, access.owner), access.agents === undefined ? undefined : grantsWhere(turns.agentId, access.agents)))
346
+ .orderBy(asc(turns.txid), asc(turns.sessionId), asc(turns.turnId))
347
+ .limit(page.limit + 1);
348
+ const items = rows.slice(0, page.limit);
349
+ const last = items.at(-1);
350
+ return {
351
+ turns: items,
352
+ // Echoed when nothing changed; a first read starts before every row.
353
+ cursor: last
354
+ ? cursor.encode([last.txid, last.sessionId, last.turnId])
355
+ : (page.changedAfter ?? cursor.encode(["0", "", ""])),
356
+ hasMore: rows.length > page.limit,
357
+ };
358
+ }),
319
359
  exportModel: (after, limit) => read(async (tx) => {
320
360
  const cursor = readCursor(tenantId, "model-export", {});
321
361
  const key = keyOf(cursor.decode(after), 2);