@nylorun/runtime 0.40.0-beta → 0.41.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 (41) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/adapters/streams/s2.js +9 -2
  3. package/dist/api/components.d.ts +131 -0
  4. package/dist/api/components.js +3 -1
  5. package/dist/api/http/app.js +2 -0
  6. package/dist/api/http/routes/runs.d.ts +8 -0
  7. package/dist/api/http/routes/runs.js +30 -0
  8. package/dist/api/http/routes/triggers.d.ts +2 -1
  9. package/dist/api/http/routes/triggers.js +32 -4
  10. package/dist/api/openapi.js +3 -1
  11. package/dist/gates/policy.d.ts +6 -4
  12. package/dist/gates/policy.js +10 -2
  13. package/dist/harness-api/record.js +51 -6
  14. package/dist/harness-api/start.js +15 -9
  15. package/dist/harness-api/tool-journal.js +3 -0
  16. package/dist/management-openapi.json +11 -6
  17. package/dist/openapi.json +506 -12
  18. package/dist/store/postgres/drizzle/0043_agent_triggers_origin.sql +11 -0
  19. package/dist/store/postgres/drizzle/0044_agent_triggers_origin_backfill.sql +8 -0
  20. package/dist/store/postgres/drizzle/meta/_journal.json +14 -0
  21. package/dist/store/postgres/schema.d.ts +151 -4
  22. package/dist/store/postgres/schema.js +25 -4
  23. package/dist/store/postgres/store.js +20 -2
  24. package/dist/store/types.d.ts +10 -0
  25. package/dist/streams/types.d.ts +16 -1
  26. package/dist/streams/types.js +8 -1
  27. package/dist/tenant/advance.js +7 -0
  28. package/dist/tenant/agent-triggers.d.ts +60 -0
  29. package/dist/tenant/agent-triggers.js +179 -0
  30. package/dist/tenant/agents.d.ts +1 -1
  31. package/dist/tenant/agents.js +5 -1
  32. package/dist/tenant/limits.d.ts +26 -7
  33. package/dist/tenant/limits.js +39 -6
  34. package/dist/tenant/runs-stream.d.ts +51 -0
  35. package/dist/tenant/runs-stream.js +164 -0
  36. package/dist/tenant/sweep.js +3 -0
  37. package/dist/tenant/triggers.d.ts +31 -5
  38. package/dist/tenant/triggers.js +168 -48
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/package.json +3 -3
@@ -0,0 +1,11 @@
1
+ ALTER TABLE "nylorun"."agent_triggers" ADD COLUMN "origin" text DEFAULT 'manifest' NOT NULL;--> statement-breakpoint
2
+ ALTER TABLE "nylorun"."agent_triggers" ADD COLUMN "title" text;--> statement-breakpoint
3
+ ALTER TABLE "nylorun"."agent_triggers" ADD COLUMN "definition" json;--> statement-breakpoint
4
+ ALTER TABLE "nylorun"."agent_triggers" ADD COLUMN "created_session_id" text COLLATE "C";--> statement-breakpoint
5
+ ALTER TABLE "nylorun"."agent_triggers" ADD COLUMN "created_turn_id" text COLLATE "C";--> statement-breakpoint
6
+ ALTER TABLE "nylorun"."trigger_runs" ADD COLUMN "origin" text DEFAULT 'manifest' NOT NULL;--> statement-breakpoint
7
+ ALTER TABLE "nylorun"."trigger_runs" ADD COLUMN "title" text;--> statement-breakpoint
8
+ ALTER TABLE "nylorun"."trigger_runs" ADD COLUMN "streamed_at" text COLLATE "C";--> statement-breakpoint
9
+ CREATE INDEX "agent_triggers_created_session" ON "nylorun"."agent_triggers" USING btree ("agent_id","created_session_id") WHERE created_session_id IS NOT NULL;--> statement-breakpoint
10
+ CREATE INDEX "trigger_runs_unstreamed" ON "nylorun"."trigger_runs" USING btree ("fired_at") WHERE streamed_at IS NULL;--> statement-breakpoint
11
+ ALTER TABLE "nylorun"."agent_triggers" ADD CONSTRAINT "agent_triggers_origin_check" CHECK ("nylorun"."agent_triggers"."origin" IN ('manifest', 'agent', 'wake'));
@@ -0,0 +1,8 @@
1
+ -- Custom SQL (a data change): the origin of the trigger rows and runs written before 0043 (D83). A
2
+ -- booked wake's is `wake`; every other came from its manifest (`manifest`, the columns' default).
3
+ -- Runs recorded before the Tenant runs stream are marked streamed, so the sweep does not append
4
+ -- the history of the last 30 days to the new `runs` stream: it starts with the runs made from here
5
+ -- on.
6
+ UPDATE "nylorun"."agent_triggers" SET "origin" = 'wake' WHERE "kind" = 'wake';--> statement-breakpoint
7
+ UPDATE "nylorun"."trigger_runs" SET "origin" = 'wake' WHERE "kind" = 'wake';--> statement-breakpoint
8
+ UPDATE "nylorun"."trigger_runs" SET "streamed_at" = "fired_at" WHERE "streamed_at" IS NULL;
@@ -302,6 +302,20 @@
302
302
  "when": 1793509192153,
303
303
  "tag": "0042_worker_engine_image",
304
304
  "breakpoints": true
305
+ },
306
+ {
307
+ "idx": 43,
308
+ "version": "7",
309
+ "when": 1793548856412,
310
+ "tag": "0043_agent_triggers_origin",
311
+ "breakpoints": true
312
+ },
313
+ {
314
+ "idx": 44,
315
+ "version": "7",
316
+ "when": 1793548858191,
317
+ "tag": "0044_agent_triggers_origin_backfill",
318
+ "breakpoints": true
305
319
  }
306
320
  ]
307
321
  }
@@ -1526,9 +1526,11 @@ export declare const agents: import("drizzle-orm/pg-core").PgTableWithColumns<{
1526
1526
  /**
1527
1527
  * An Agent's triggers (track T4, D74): what its manifest's `runtime.triggers` declares is the
1528
1528
  * manifest's; whether each one is on and when it fires next is the Agent's, here. A booked wake
1529
- * (D77) is a row of kind `wake` with id `wake:<sessionId>`. Postgres holds the truth: one
1530
- * durable timer per active trigger is armed from `nextFireAt`, and the sweep re-arms an overdue
1531
- * one, so the execution's timers can be rebuilt from these rows.
1529
+ * (D77) is a row of kind `wake` with id `wake:<sessionId>`. A trigger an agent made with the
1530
+ * `triggers` tool (D83) is a row of origin `agent`, its definition (a `Trigger`, as a manifest
1531
+ * entry holds it) and title on the row, with the session and turn that made it. Postgres holds
1532
+ * the truth: one durable timer per active trigger is armed from `nextFireAt`, and the sweep
1533
+ * re-arms an overdue one, so the execution's timers can be rebuilt from these rows.
1532
1534
  */
1533
1535
  export declare const agentTriggers: import("drizzle-orm/pg-core").PgTableWithColumns<{
1534
1536
  name: "agent_triggers";
@@ -1769,6 +1771,97 @@ export declare const agentTriggers: import("drizzle-orm/pg-core").PgTableWithCol
1769
1771
  identity: undefined;
1770
1772
  generated: undefined;
1771
1773
  }, {}, {}>;
1774
+ origin: import("drizzle-orm/pg-core").PgColumn<{
1775
+ name: "origin";
1776
+ tableName: "agent_triggers";
1777
+ dataType: "string";
1778
+ columnType: "PgText";
1779
+ data: "agent" | "manifest" | "wake";
1780
+ driverParam: string;
1781
+ notNull: true;
1782
+ hasDefault: true;
1783
+ isPrimaryKey: false;
1784
+ isAutoincrement: false;
1785
+ hasRuntimeDefault: false;
1786
+ enumValues: ["manifest", "agent", "wake"];
1787
+ baseColumn: never;
1788
+ identity: undefined;
1789
+ generated: undefined;
1790
+ }, {}, {}>;
1791
+ title: import("drizzle-orm/pg-core").PgColumn<{
1792
+ name: "title";
1793
+ tableName: "agent_triggers";
1794
+ dataType: "string";
1795
+ columnType: "PgText";
1796
+ data: string;
1797
+ driverParam: string;
1798
+ notNull: false;
1799
+ hasDefault: false;
1800
+ isPrimaryKey: false;
1801
+ isAutoincrement: false;
1802
+ hasRuntimeDefault: false;
1803
+ enumValues: [string, ...string[]];
1804
+ baseColumn: never;
1805
+ identity: undefined;
1806
+ generated: undefined;
1807
+ }, {}, {}>;
1808
+ definition: import("drizzle-orm/pg-core").PgColumn<{
1809
+ name: "definition";
1810
+ tableName: "agent_triggers";
1811
+ dataType: "custom";
1812
+ columnType: "PgCustomColumn";
1813
+ data: unknown;
1814
+ driverParam: unknown;
1815
+ notNull: false;
1816
+ hasDefault: false;
1817
+ isPrimaryKey: false;
1818
+ isAutoincrement: false;
1819
+ hasRuntimeDefault: false;
1820
+ enumValues: undefined;
1821
+ baseColumn: never;
1822
+ identity: undefined;
1823
+ generated: undefined;
1824
+ }, {}, {
1825
+ pgColumnBuilderBrand: 'PgCustomColumnBuilderBrand';
1826
+ }>;
1827
+ createdSessionId: import("drizzle-orm/pg-core").PgColumn<{
1828
+ name: "createdSessionId";
1829
+ tableName: "agent_triggers";
1830
+ dataType: "custom";
1831
+ columnType: "PgCustomColumn";
1832
+ data: string;
1833
+ driverParam: string;
1834
+ notNull: false;
1835
+ hasDefault: false;
1836
+ isPrimaryKey: false;
1837
+ isAutoincrement: false;
1838
+ hasRuntimeDefault: false;
1839
+ enumValues: undefined;
1840
+ baseColumn: never;
1841
+ identity: undefined;
1842
+ generated: undefined;
1843
+ }, {}, {
1844
+ pgColumnBuilderBrand: 'PgCustomColumnBuilderBrand';
1845
+ }>;
1846
+ createdTurnId: import("drizzle-orm/pg-core").PgColumn<{
1847
+ name: "createdTurnId";
1848
+ tableName: "agent_triggers";
1849
+ dataType: "custom";
1850
+ columnType: "PgCustomColumn";
1851
+ data: string;
1852
+ driverParam: string;
1853
+ notNull: false;
1854
+ hasDefault: false;
1855
+ isPrimaryKey: false;
1856
+ isAutoincrement: false;
1857
+ hasRuntimeDefault: false;
1858
+ enumValues: undefined;
1859
+ baseColumn: never;
1860
+ identity: undefined;
1861
+ generated: undefined;
1862
+ }, {}, {
1863
+ pgColumnBuilderBrand: 'PgCustomColumnBuilderBrand';
1864
+ }>;
1772
1865
  createdAt: import("drizzle-orm/pg-core").PgColumn<{
1773
1866
  name: "createdAt";
1774
1867
  tableName: "agent_triggers";
@@ -2219,7 +2312,8 @@ export declare const memoryVersions: import("drizzle-orm/pg-core").PgTableWithCo
2219
2312
  * Each run of an Agent's triggers (D74): one per fire, delivery or run by hand, keyed by what
2220
2313
  * caused it (`runKey`: `fire:<trigger>:<due ms>`, `webhook:<trigger>:<delivery id>`,
2221
2314
  * `manual:<trigger>:<idempotency key>`), so a repeated cause makes no second run. Kept for the
2222
- * Runtime limit `triggerRunRetentionMs`.
2315
+ * Runtime limit `triggerRunRetentionMs`. `streamedAt` is the Tenant runs stream's outbox (D83):
2316
+ * set once the run is in the `runs` stream; the sweep appends a run still unset.
2223
2317
  */
2224
2318
  export declare const triggerRuns: import("drizzle-orm/pg-core").PgTableWithColumns<{
2225
2319
  name: "trigger_runs";
@@ -2464,6 +2558,59 @@ export declare const triggerRuns: import("drizzle-orm/pg-core").PgTableWithColum
2464
2558
  }, {}, {
2465
2559
  pgColumnBuilderBrand: 'PgCustomColumnBuilderBrand';
2466
2560
  }>;
2561
+ origin: import("drizzle-orm/pg-core").PgColumn<{
2562
+ name: "origin";
2563
+ tableName: "trigger_runs";
2564
+ dataType: "string";
2565
+ columnType: "PgText";
2566
+ data: "agent" | "manifest" | "wake";
2567
+ driverParam: string;
2568
+ notNull: true;
2569
+ hasDefault: true;
2570
+ isPrimaryKey: false;
2571
+ isAutoincrement: false;
2572
+ hasRuntimeDefault: false;
2573
+ enumValues: ["manifest", "agent", "wake"];
2574
+ baseColumn: never;
2575
+ identity: undefined;
2576
+ generated: undefined;
2577
+ }, {}, {}>;
2578
+ title: import("drizzle-orm/pg-core").PgColumn<{
2579
+ name: "title";
2580
+ tableName: "trigger_runs";
2581
+ dataType: "string";
2582
+ columnType: "PgText";
2583
+ data: string;
2584
+ driverParam: string;
2585
+ notNull: false;
2586
+ hasDefault: false;
2587
+ isPrimaryKey: false;
2588
+ isAutoincrement: false;
2589
+ hasRuntimeDefault: false;
2590
+ enumValues: [string, ...string[]];
2591
+ baseColumn: never;
2592
+ identity: undefined;
2593
+ generated: undefined;
2594
+ }, {}, {}>;
2595
+ streamedAt: import("drizzle-orm/pg-core").PgColumn<{
2596
+ name: "streamedAt";
2597
+ tableName: "trigger_runs";
2598
+ dataType: "custom";
2599
+ columnType: "PgCustomColumn";
2600
+ data: string;
2601
+ driverParam: string;
2602
+ notNull: false;
2603
+ hasDefault: false;
2604
+ isPrimaryKey: false;
2605
+ isAutoincrement: false;
2606
+ hasRuntimeDefault: false;
2607
+ enumValues: undefined;
2608
+ baseColumn: never;
2609
+ identity: undefined;
2610
+ generated: undefined;
2611
+ }, {}, {
2612
+ pgColumnBuilderBrand: 'PgCustomColumnBuilderBrand';
2613
+ }>;
2467
2614
  };
2468
2615
  dialect: 'pg';
2469
2616
  }>;
@@ -217,9 +217,11 @@ export const agents = nylorun.table("agents", {
217
217
  /**
218
218
  * An Agent's triggers (track T4, D74): what its manifest's `runtime.triggers` declares is the
219
219
  * manifest's; whether each one is on and when it fires next is the Agent's, here. A booked wake
220
- * (D77) is a row of kind `wake` with id `wake:<sessionId>`. Postgres holds the truth: one
221
- * durable timer per active trigger is armed from `nextFireAt`, and the sweep re-arms an overdue
222
- * one, so the execution's timers can be rebuilt from these rows.
220
+ * (D77) is a row of kind `wake` with id `wake:<sessionId>`. A trigger an agent made with the
221
+ * `triggers` tool (D83) is a row of origin `agent`, its definition (a `Trigger`, as a manifest
222
+ * entry holds it) and title on the row, with the session and turn that made it. Postgres holds
223
+ * the truth: one durable timer per active trigger is armed from `nextFireAt`, and the sweep
224
+ * re-arms an overdue one, so the execution's timers can be rebuilt from these rows.
223
225
  */
224
226
  export const agentTriggers = nylorun.table("agent_triggers", {
225
227
  agentId: textC().notNull(),
@@ -244,10 +246,22 @@ export const agentTriggers = nylorun.table("agent_triggers", {
244
246
  sessionId: textC(),
245
247
  note: text(),
246
248
  effectId: text(),
249
+ /**
250
+ * Where its definition comes from (D83): `manifest` (the version's `runtime.triggers`),
251
+ * `agent` (`definition`, made with the `triggers` tool) or `wake` (a booked wake).
252
+ */
253
+ origin: text({ enum: ["manifest", "agent", "wake"] }).notNull().default("manifest"),
254
+ /** An agent-made trigger's title, its definition, and the session and turn that made it. */
255
+ title: text(),
256
+ definition: jsonText(),
257
+ createdSessionId: textC(),
258
+ createdTurnId: textC(),
247
259
  createdAt: textC().notNull(),
248
260
  updatedAt: textC().notNull(),
249
261
  }, (t) => [
262
+ check("agent_triggers_origin_check", sql `${t.origin} IN ('manifest', 'agent', 'wake')`),
250
263
  primaryKey({ name: "agent_triggers_pkey", columns: [t.agentId, t.triggerId] }),
264
+ index("agent_triggers_created_session").on(t.agentId, t.createdSessionId).where(sql `created_session_id IS NOT NULL`),
251
265
  index("agent_triggers_due").on(t.status, t.nextFireAt),
252
266
  uniqueIndex("agent_triggers_endpoint").on(t.endpointId).where(sql `endpoint_id IS NOT NULL`),
253
267
  ]);
@@ -293,7 +307,8 @@ export const memoryVersions = nylorun.table("memory_versions", {
293
307
  * Each run of an Agent's triggers (D74): one per fire, delivery or run by hand, keyed by what
294
308
  * caused it (`runKey`: `fire:<trigger>:<due ms>`, `webhook:<trigger>:<delivery id>`,
295
309
  * `manual:<trigger>:<idempotency key>`), so a repeated cause makes no second run. Kept for the
296
- * Runtime limit `triggerRunRetentionMs`.
310
+ * Runtime limit `triggerRunRetentionMs`. `streamedAt` is the Tenant runs stream's outbox (D83):
311
+ * set once the run is in the `runs` stream; the sweep appends a run still unset.
297
312
  */
298
313
  export const triggerRuns = nylorun.table("trigger_runs", {
299
314
  id: textC().primaryKey(),
@@ -309,10 +324,16 @@ export const triggerRuns = nylorun.table("trigger_runs", {
309
324
  sessionId: textC(),
310
325
  turnId: textC(),
311
326
  queuedMessageId: textC(),
327
+ /** Its trigger's origin and title when it ran (D83), for the runs stream after the trigger is gone. */
328
+ origin: text({ enum: ["manifest", "agent", "wake"] }).notNull().default("manifest"),
329
+ title: text(),
330
+ /** When it was appended to the Tenant's `runs` stream (ISO), or null until then. */
331
+ streamedAt: textC(),
312
332
  }, (t) => [
313
333
  uniqueIndex("trigger_runs_key").on(t.agentId, t.runKey),
314
334
  index("trigger_runs_agent").on(t.agentId, t.id),
315
335
  index("trigger_runs_fired").on(t.firedAt),
336
+ index("trigger_runs_unstreamed").on(t.firedAt).where(sql `streamed_at IS NULL`),
316
337
  ]);
317
338
  /**
318
339
  * An agent manifest's versions (protocol 16): a named, versioned document with a harness and a
@@ -1345,6 +1345,24 @@ class PostgresTx {
1345
1345
  .returning({ id: triggerRuns.id });
1346
1346
  return inserted.length === 1;
1347
1347
  }
1348
+ async unstreamedTriggerRuns(options) {
1349
+ this.check();
1350
+ return this.db
1351
+ .select()
1352
+ .from(triggerRuns)
1353
+ .where(and(isNull(triggerRuns.streamedAt), lt(triggerRuns.firedAt, options.firedBefore)))
1354
+ .orderBy(triggerRuns.firedAt, triggerRuns.id)
1355
+ .limit(options.limit);
1356
+ }
1357
+ async markTriggerRunsStreamed(ids, at) {
1358
+ this.check();
1359
+ if (ids.length === 0)
1360
+ return;
1361
+ await this.db
1362
+ .update(triggerRuns)
1363
+ .set({ streamedAt: at })
1364
+ .where(and(inArray(triggerRuns.id, [...ids]), isNull(triggerRuns.streamedAt)));
1365
+ }
1348
1366
  async triggerRunByKey(agentId, runKey) {
1349
1367
  this.check();
1350
1368
  const [row] = await this.db
@@ -1763,8 +1781,8 @@ class PostgresTx {
1763
1781
  if (scope === "sessions" || scope === "all") {
1764
1782
  for (const table of SESSION_TABLES)
1765
1783
  await db.delete(table);
1766
- // A session's booked wake (T4) goes with it.
1767
- await db.delete(agentTriggers).where(eq(agentTriggers.kind, "wake"));
1784
+ // A session's booked wake (T4) goes with it, and the triggers its agents made (D83).
1785
+ await db.delete(agentTriggers).where(or(eq(agentTriggers.kind, "wake"), inArray(agentTriggers.origin, ["wake", "agent"])));
1768
1786
  await db.delete(toolCrossings);
1769
1787
  // A session's artifacts go with it (their versions by cascade).
1770
1788
  await db.delete(artifacts).where(isNotNull(artifacts.sessionId));
@@ -776,6 +776,16 @@ export interface Tx {
776
776
  }): Promise<TriggerRunRow[]>;
777
777
  /** Deletes runs fired before `before`; returns how many. */
778
778
  pruneTriggerRuns(before: string): Promise<number>;
779
+ /**
780
+ * Runs of every Agent not yet on the Tenant's runs stream (D83), fired before `firedBefore`,
781
+ * oldest first.
782
+ */
783
+ unstreamedTriggerRuns(options: {
784
+ firedBefore: string;
785
+ limit: number;
786
+ }): Promise<TriggerRunRow[]>;
787
+ /** Marks runs as on the runs stream, at `at`; one already marked keeps its time. */
788
+ markTriggerRunsStreamed(ids: readonly string[], at: string): Promise<void>;
779
789
  /** One memory; with `lock`, its path is locked until the transaction ends, whether or not a row has it. */
780
790
  memory(agentId: string, path: string, options?: {
781
791
  lock?: boolean;
@@ -7,6 +7,7 @@
7
7
  * | Stream | Contents | Written by | Read by |
8
8
  * | --- | --- | --- | --- |
9
9
  * | `sessions/<sessionId>` | every event of the session, in sequence | the stream relay, from the record | history and session SSE |
10
+ | `runs` | every run of the Tenant's triggers (D83), kept 7 days | `tenant/runs-stream.ts`, after the commit that made the run, and the sweep | `GET /v1/runs/stream` |
10
11
  *
11
12
  * S2 serves API listeners only (history, SSE, AG-UI, A2A): nothing inside the Runtime signals
12
13
  * through it or waits on it (D48). Signals between processes go over the control bus on
@@ -15,7 +16,9 @@
15
16
  * **Basin generations.** A session id is unique within its Tenant's basin generation. A
16
17
  * Tenant reset, the only path that frees ids, moves the Tenant to the next generation, so
17
18
  * a session created again with the same id starts in an empty basin at sequence 0; the old
18
- * basin is deleted afterwards. The cursor is `base64url("<sessionId>:<seq>")`.
19
+ * basin is deleted afterwards. The cursor is `base64url("<sessionId>:<seq>")`. The `runs`
20
+ * stream starts at 0 in each generation too; its cursor names the generation
21
+ * (`base64url("runs:<generation>:<seq>")`), so one from an older basin is refused.
19
22
  *
20
23
  * S2 is the supported implementation (`adapters/streams/s2.ts`, the only file
21
24
  * importing the S2 SDK); `streams/memory.ts` is the in-memory fake.
@@ -100,6 +103,14 @@ export interface DurableStreams {
100
103
  }): Promise<void>;
101
104
  /** Deletes one stream. Idempotent. */
102
105
  deleteStream(tenantId: string, stream: string): Promise<void>;
106
+ /**
107
+ * Creates `stream` if it does not exist, or sets its configuration, keeping its records for
108
+ * `retentionSecs` (the basin's default keeps them forever). Absent for in-process
109
+ * implementations, which keep what they hold until they close.
110
+ */
111
+ ensureStream?(tenantId: string, stream: string, options: {
112
+ retentionSecs: number;
113
+ }): Promise<void>;
103
114
  /**
104
115
  * Names of the Tenant's streams starting with `prefix`, in name order,
105
116
  * without streams being deleted. Empty when the Tenant has no basin.
@@ -114,5 +125,9 @@ export interface DurableStreams {
114
125
  probe?(signal: AbortSignal): Promise<void>;
115
126
  }
116
127
  export declare const SESSION_STREAM_PREFIX = "sessions/";
128
+ /** The Tenant's runs stream (D83): every run of its triggers, beside the session streams. */
129
+ export declare const RUNS_STREAM = "runs";
130
+ /** How long the runs stream keeps a run: 7 days. */
131
+ export declare const RUNS_STREAM_RETENTION_SECS: number;
117
132
  /** The stream of a session: `sessions/<sessionId>`. */
118
133
  export declare function sessionStream(sessionId: string): string;
@@ -7,6 +7,7 @@
7
7
  * | Stream | Contents | Written by | Read by |
8
8
  * | --- | --- | --- | --- |
9
9
  * | `sessions/<sessionId>` | every event of the session, in sequence | the stream relay, from the record | history and session SSE |
10
+ | `runs` | every run of the Tenant's triggers (D83), kept 7 days | `tenant/runs-stream.ts`, after the commit that made the run, and the sweep | `GET /v1/runs/stream` |
10
11
  *
11
12
  * S2 serves API listeners only (history, SSE, AG-UI, A2A): nothing inside the Runtime signals
12
13
  * through it or waits on it (D48). Signals between processes go over the control bus on
@@ -15,7 +16,9 @@
15
16
  * **Basin generations.** A session id is unique within its Tenant's basin generation. A
16
17
  * Tenant reset, the only path that frees ids, moves the Tenant to the next generation, so
17
18
  * a session created again with the same id starts in an empty basin at sequence 0; the old
18
- * basin is deleted afterwards. The cursor is `base64url("<sessionId>:<seq>")`.
19
+ * basin is deleted afterwards. The cursor is `base64url("<sessionId>:<seq>")`. The `runs`
20
+ * stream starts at 0 in each generation too; its cursor names the generation
21
+ * (`base64url("runs:<generation>:<seq>")`), so one from an older basin is refused.
19
22
  *
20
23
  * S2 is the supported implementation (`adapters/streams/s2.ts`, the only file
21
24
  * importing the S2 SDK); `streams/memory.ts` is the in-memory fake.
@@ -44,6 +47,10 @@
44
47
  // ---------------------------------------------------------------------------
45
48
  // Stream names
46
49
  export const SESSION_STREAM_PREFIX = "sessions/";
50
+ /** The Tenant's runs stream (D83): every run of its triggers, beside the session streams. */
51
+ export const RUNS_STREAM = "runs";
52
+ /** How long the runs stream keeps a run: 7 days. */
53
+ export const RUNS_STREAM_RETENTION_SECS = 7 * 24 * 3600;
47
54
  /** The stream of a session: `sessions/<sessionId>`. */
48
55
  export function sessionStream(sessionId) {
49
56
  if (!sessionId)
@@ -621,6 +621,13 @@ async function heldCalls(t, effectIds) {
621
621
  ...(row.server === undefined ? {} : { server: row.server }),
622
622
  ...(row.tool === undefined ? {} : { tool: row.tool }),
623
623
  input: request.input ?? null,
624
+ // What a client renders the approval from (D83): the tool's identity and the call's input.
625
+ metadata: {
626
+ tool: row.tool ?? toolName,
627
+ ...(row.toolKind === undefined ? {} : { kind: row.toolKind }),
628
+ ...(row.server === undefined ? {} : { server: row.server }),
629
+ input: request.input ?? null,
630
+ },
624
631
  },
625
632
  status: "interaction",
626
633
  toolName,
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The `triggers` tool (D83): an agent whose version declares `harness.engine.triggers` lists,
3
+ * creates and deletes its own triggers. The Runtime serves each call as a `runtime` effect
4
+ * (operation `triggers`), in the call's own transaction, keyed by the effect id, so a replayed turn
5
+ * changes nothing twice (`harness-api/record.ts`). The Tenant's tool policy decides it first (a
6
+ * rule of `kind: "runtime"`, `tool: "triggers"`), so an `ask` rule can hold a create for a person.
7
+ *
8
+ * A trigger an agent makes is a row of `agent_triggers` with origin `agent`: its definition (a
9
+ * `Trigger`, as a manifest entry holds it) and title on the row, with the session and turn that
10
+ * made it. It fires through `fireTimer` and `deliverRun` like a manifest's (`triggers.ts`), and an
11
+ * upgrade never touches it.
12
+ *
13
+ * - **Where it runs.** `session: "this"` runs it in the calling session (a follow-up); `"own"` in
14
+ * a session named from the calling one, `<session>_t_<id>`, opened at its first run and kept.
15
+ * - **Scope.** A session lists and deletes the triggers made from it, and a trigger's own session
16
+ * may list and delete it too. Creating with an id the session's trigger has replaces it; an id
17
+ * another session's trigger, a manifest's or a wake holds is `tool.conflict`.
18
+ * - **Runs can't multiply.** A turn a trigger or a booked wake started may list and delete, never
19
+ * create.
20
+ * - **Checks.** Every create runs the checks a manifest save runs (`triggerIssues`): cron and
21
+ * zone, the minimum interval, `at` in the future, the prompt's length; and the count of the
22
+ * Agent's agent-made triggers. Each against the limits in force: the manifest's
23
+ * `runtime.limits.triggers` within the Tenant's (`triggerLimitsInForce`).
24
+ * - **Arming.** No I/O in the transaction: the call returns the rows to arm, which the record arms
25
+ * after its commit; the sweep's reconcile arms any it missed.
26
+ */
27
+ import { type TriggerToolOutput } from "@nylorun/core/contracts";
28
+ import type { AgentTriggerRow, Tx } from "../store/types.js";
29
+ import type { Session } from "./context.js";
30
+ /** What a served call settles as, and the rows to arm once its transaction commits. */
31
+ export interface ServedTriggers {
32
+ readonly outcome: {
33
+ kind: "completed";
34
+ output: TriggerToolOutput;
35
+ } | {
36
+ kind: "failed";
37
+ code: string;
38
+ message: string;
39
+ };
40
+ readonly arm: readonly AgentTriggerRow[];
41
+ }
42
+ /** The session an agent-made trigger runs in: the calling one, or its own, `<session>_t_<id>`. */
43
+ export declare function triggerSessionOf(callingSessionId: string, id: string, session: "this" | "own"): string;
44
+ /**
45
+ * Why session `s` cannot make `triggers` call `request` at all, before the Tenant's tool policy
46
+ * is asked (off means off, ADR-100): its version does not declare `engine.triggers`
47
+ * (`tool.unavailable`), or an agent used as a tool made it (`tool.forbidden`). Undefined when it can.
48
+ */
49
+ export declare function triggersRefusal(t: Tx, s: Session, request: {
50
+ agent?: unknown;
51
+ }): Promise<ServedTriggers["outcome"] | undefined>;
52
+ /**
53
+ * Serves one `triggers` call of session `s`'s active turn in `t` (see the module comment). The
54
+ * call's `agent` is the agent used as a tool that made it, absent for the root agent.
55
+ */
56
+ export declare function serveTriggersTool(t: Tx, s: Session, request: {
57
+ effectId: string;
58
+ input?: unknown;
59
+ agent?: unknown;
60
+ }): Promise<ServedTriggers>;