@nylorun/runtime 0.40.0-beta → 0.42.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 +30 -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 +550 -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 +196 -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 +36 -5
  38. package/dist/tenant/triggers.js +183 -48
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/package.json +3 -3
@@ -28,7 +28,22 @@ export declare function nextFireAt(agentId: string, triggerId: string, trigger:
28
28
  * refuses, two runs closer than `triggerMinIntervalMs` over the next `triggers.intervalCheckFires`
29
29
  * fires, an `at` that is not in the future, a prompt past `triggerPromptChars`.
30
30
  */
31
- export declare function triggerIssues(triggers: Readonly<Record<string, Trigger>> | undefined, limits: RuntimeLimits, now?: Date): string[];
31
+ export declare function triggerIssues(triggers: Readonly<Record<string, Trigger>> | undefined, limits: RuntimeLimits, now?: Date,
32
+ /** How an issue names trigger `id`: its place in the manifest by default. */
33
+ label?: (id: string) => string): string[];
34
+ /**
35
+ * The line an agent-made trigger's run starts with under the `nylorun-triggers` plugin (ADR-106):
36
+ * which trigger woke the turn, and that nobody is watching.
37
+ */
38
+ export declare function triggerFrame(triggerId: string, title: string | null | undefined): string;
39
+ /** Where a row's definition comes from (D83): its origin, or `wake` for a booked wake written before origins. */
40
+ export declare function originOf(row: Pick<AgentTriggerRow, "origin" | "kind">): AgentTriggerRow["origin"];
41
+ /**
42
+ * The definition trigger row `row` fires with (D83): a booked wake's, from its session and note;
43
+ * an agent-made trigger's own, from the row; a manifest trigger's, the entry of the version the
44
+ * Agent runs (`manifest`, read once by the caller). Undefined when it has none.
45
+ */
46
+ export declare function definitionOf(row: AgentTriggerRow, manifest: Readonly<Record<string, Trigger>>): Trigger | undefined;
32
47
  /**
33
48
  * Writes the state of a new Agent's triggers (D74): every trigger of the version it is made from
34
49
  * starts active, with its next time. Arm them after the commit (`armAgentTriggers`).
@@ -38,8 +53,11 @@ export declare function createAgentTriggers(t: Tx, agent: Environment, now?: Dat
38
53
  * After Agent `agent` (locked) moved from manifest version `from` to the version it now runs (track
39
54
  * T3, D73), in the move's transaction: its triggers merge by id. One the new version adds arrives
40
55
  * paused (`new`), one it drops loses its state (its runs stay), and one whose timing changed gets a
41
- * new generation and next time; a changed prompt or session applies at the next fire. The sweep
42
- * arms what changed (`reconcileTriggers`).
56
+ * new generation and next time; a changed prompt or session applies at the next fire. Only rows of
57
+ * origin `manifest` merge (D83): an agent's and a booked wake stay as they are, but for an agent's
58
+ * paused `undeclared`, which resumes on a version that declares `harness.engine.triggers`. A
59
+ * manifest trigger whose id an agent's trigger holds is not added. The sweep arms what changed
60
+ * (`reconcileTriggers`).
43
61
  */
44
62
  export declare function mergeAgentTriggers(t: Tx, agent: Environment, from: number, now?: Date): Promise<void>;
45
63
  /** Arms one trigger's timer for its next time, or the next step of a wait past `TIMER_MAX_MS`. */
@@ -53,6 +71,8 @@ export declare function armAgentTriggers(ctx: TenantContext, agentId: string): P
53
71
  export declare function reconcileTriggers(ctx: TenantContext, first: boolean): Promise<void>;
54
72
  /** `DurableExecution.timer` fired for key `key` (`TenantWorker.fire`). */
55
73
  export declare function fireTimer(ctx: TenantContext, key: string): Promise<void>;
74
+ /** Why an agent's trigger is paused: the version its Agent runs lacks `harness.engine.triggers` (D83). */
75
+ export declare const UNDECLARED = "undeclared";
56
76
  /** What makes a run: its cause's key, when it was due, whether late. */
57
77
  interface RunCause {
58
78
  runKey: string;
@@ -71,10 +91,15 @@ export interface WebhookPayload {
71
91
  * Runs trigger `triggerId` of `agent` (locked, with the trigger's row) in `t`: see the module
72
92
  * comment. Returns the run; a cause already run returns that run.
73
93
  */
74
- export declare function deliverRun(ctx: TenantContext, t: Tx, agent: Environment, triggerId: string, trigger: Trigger, row: Pick<AgentTriggerRow, "kind">, cause: RunCause): Promise<TriggerRunRow>;
94
+ export declare function deliverRun(ctx: TenantContext, t: Tx, agent: Environment, triggerId: string, trigger: Trigger, row: Pick<AgentTriggerRow, "kind"> & Partial<Pick<AgentTriggerRow, "origin" | "title" | "createdSessionId">>, cause: RunCause): Promise<TriggerRunRow>;
75
95
  /** A run as the API shows it. */
76
96
  export declare function runView(row: TriggerRunRow): TriggerRun;
77
- /** `GET /v1/agents/{id}/triggers`. */
97
+ /** One trigger as the API shows it: its definition and the Agent's state of it. */
98
+ export declare function triggerView(row: AgentTriggerRow, trigger: Trigger, origin?: string): TriggerView;
99
+ /**
100
+ * `GET /v1/agents/{id}/triggers`: the manifest's triggers the Agent holds, and the triggers its
101
+ * agents made (D83); never a session's booked wake.
102
+ */
78
103
  export declare function listTriggers(ctx: TenantContext, agentId: string, origin?: string): Promise<{
79
104
  triggers: TriggerView[];
80
105
  }>;
@@ -82,6 +107,12 @@ export declare function listTriggers(ctx: TenantContext, agentId: string, origin
82
107
  export declare function setTriggerStatus(ctx: TenantContext, agentId: string, triggerId: string, action: "pause" | "resume", origin?: string): Promise<{
83
108
  triggers: TriggerView[];
84
109
  }>;
110
+ /**
111
+ * `DELETE /v1/agents/{id}/triggers/{triggerId}` (D83): deletes a trigger one of the Agent's agents
112
+ * made (origin `agent`), with `agent.trigger_deleted`. A manifest's is the manifest's to drop
113
+ * (`409 trigger_from_manifest`); its runs stay.
114
+ */
115
+ export declare function deleteTrigger(ctx: TenantContext, agentId: string, triggerId: string, principal?: string): Promise<void>;
85
116
  /**
86
117
  * `POST .../triggers/{triggerId}/run`: runs it now, whatever its status, keyed by the request's
87
118
  * `Idempotency-Key`, so a retried request makes one run.
@@ -25,15 +25,24 @@
25
25
  *
26
26
  * A schedule's runs are moved by a fixed offset (jitter), from a hash of the Agent and trigger id:
27
27
  * up to `triggerJitterPercent` of its interval and `triggerJitterMaxMs`.
28
+ *
29
+ * **Origins** (D83). A row's definition comes from where it was made (`origin`, `definitionOf`):
30
+ * `manifest`, the entry of the version the Agent runs; `agent`, the row's own (a trigger an agent
31
+ * made with the `triggers` tool, `agent-triggers.ts`); `wake`, a session's booked wake. Every
32
+ * origin fires through `fireTimer` and `deliverRun`; only where its definition comes from differs.
33
+ * An upgrade merges `manifest` rows only, and an `agent` row does not fire while the version lacks
34
+ * `harness.engine.triggers`: it is paused (`undeclared`) until a version declares it again.
28
35
  */
29
36
  import { createHash, randomBytes } from "node:crypto";
30
37
  import { Cron } from "croner";
31
38
  import { TIMER_MAX_MS, triggerKindOf, } from "@nylorun/core/contracts";
39
+ import { appendRuns } from "./runs-stream.js";
32
40
  import { hostTuning } from "../tuning.js";
33
41
  import { checkAgentTurn, liveEnvironment } from "./agents.js";
34
42
  import { acceptMessage, QUEUE_SCOPE, takesMessages } from "./commands.js";
35
43
  import { fail, HttpError } from "./http.js";
36
44
  import { readRuntimeLimits } from "./runtime-limits.js";
45
+ import { triggerLimitsInForce } from "./limits.js";
37
46
  import { openSessionIn } from "./sessions.js";
38
47
  const TIMER_PREFIX = "trigger:";
39
48
  const HOUR_MS = 3_600_000;
@@ -97,7 +106,9 @@ export function nextFireAt(agentId, triggerId, trigger, after, limits) {
97
106
  * refuses, two runs closer than `triggerMinIntervalMs` over the next `triggers.intervalCheckFires`
98
107
  * fires, an `at` that is not in the future, a prompt past `triggerPromptChars`.
99
108
  */
100
- export function triggerIssues(triggers, limits, now = new Date()) {
109
+ export function triggerIssues(triggers, limits, now = new Date(),
110
+ /** How an issue names trigger `id`: its place in the manifest by default. */
111
+ label = (id) => `runtime.triggers.${id}`) {
101
112
  if (!triggers)
102
113
  return [];
103
114
  const issues = [];
@@ -106,7 +117,7 @@ export function triggerIssues(triggers, limits, now = new Date()) {
106
117
  issues.push(`runtime.triggers declares ${ids.length} triggers, over the Runtime limit triggersPerAgent (${limits.triggersPerAgent}; PUT /v1/tenant/limits).`);
107
118
  const fires = hostTuning().triggers.intervalCheckFires;
108
119
  for (const [id, trigger] of Object.entries(triggers)) {
109
- const at = `runtime.triggers.${id}`;
120
+ const at = label(id);
110
121
  if (trigger.prompt.length > limits.triggerPromptChars)
111
122
  issues.push(`${at}.prompt is ${trigger.prompt.length} characters, over the Runtime limit triggerPromptChars (${limits.triggerPromptChars}).`);
112
123
  if (trigger.at !== undefined && Date.parse(trigger.at) <= now.getTime())
@@ -135,9 +146,48 @@ export function triggerIssues(triggers, limits, now = new Date()) {
135
146
  }
136
147
  /** The triggers of the manifest version an Agent runs. */
137
148
  async function triggersOf(t, agent) {
138
- const version = await t.agentManifest(agent.manifestName, agent.manifestVersion);
139
- return (version?.body?.runtime?.triggers ?? {});
149
+ return (await pinnedOf(t, agent)).triggers;
150
+ }
151
+ /** What the manifest version an Agent runs says of triggers: its own, and whether it declares the capability. */
152
+ async function pinnedOf(t, agent) {
153
+ const body = (await t.agentManifest(agent.manifestName, agent.manifestVersion))?.body;
154
+ const harness = body?.harness;
155
+ const plugins = harness?.engine?.plugins;
156
+ return {
157
+ triggers: (body?.runtime?.triggers ?? {}),
158
+ declared: harness?.engine?.triggers === true,
159
+ // The version declares the Runtime's `nylorun-triggers` plugin (ADR-106).
160
+ plugin: Array.isArray(plugins) && plugins.some((item) => item?.name === "nylorun-triggers"),
161
+ };
162
+ }
163
+ /**
164
+ * The line an agent-made trigger's run starts with under the `nylorun-triggers` plugin (ADR-106):
165
+ * which trigger woke the turn, and that nobody is watching.
166
+ */
167
+ export function triggerFrame(triggerId, title) {
168
+ return `[trigger ${triggerId}${title ? ` · ${title}` : ""} · nobody is watching]`;
169
+ }
170
+ /** Where a row's definition comes from (D83): its origin, or `wake` for a booked wake written before origins. */
171
+ export function originOf(row) {
172
+ return row.kind === "wake" ? "wake" : row.origin;
173
+ }
174
+ /**
175
+ * The definition trigger row `row` fires with (D83): a booked wake's, from its session and note;
176
+ * an agent-made trigger's own, from the row; a manifest trigger's, the entry of the version the
177
+ * Agent runs (`manifest`, read once by the caller). Undefined when it has none.
178
+ */
179
+ export function definitionOf(row, manifest) {
180
+ switch (originOf(row)) {
181
+ case "wake":
182
+ return wakeTriggerOf(row);
183
+ case "agent":
184
+ return (row.definition ?? undefined);
185
+ default:
186
+ return manifest[row.triggerId];
187
+ }
140
188
  }
189
+ /** The columns a row of origin `manifest` or `wake` leaves empty. */
190
+ const NO_AGENT_FIELDS = { title: null, definition: null, createdSessionId: null, createdTurnId: null };
141
191
  /**
142
192
  * Writes the state of a new Agent's triggers (D74): every trigger of the version it is made from
143
193
  * starts active, with its next time. Arm them after the commit (`armAgentTriggers`).
@@ -161,6 +211,8 @@ export async function createAgentTriggers(t, agent, now = new Date()) {
161
211
  sessionId: null,
162
212
  note: null,
163
213
  effectId: null,
214
+ origin: "manifest",
215
+ ...NO_AGENT_FIELDS,
164
216
  createdAt: stamp,
165
217
  updatedAt: stamp,
166
218
  });
@@ -170,21 +222,41 @@ export async function createAgentTriggers(t, agent, now = new Date()) {
170
222
  * After Agent `agent` (locked) moved from manifest version `from` to the version it now runs (track
171
223
  * T3, D73), in the move's transaction: its triggers merge by id. One the new version adds arrives
172
224
  * paused (`new`), one it drops loses its state (its runs stay), and one whose timing changed gets a
173
- * new generation and next time; a changed prompt or session applies at the next fire. The sweep
174
- * arms what changed (`reconcileTriggers`).
225
+ * new generation and next time; a changed prompt or session applies at the next fire. Only rows of
226
+ * origin `manifest` merge (D83): an agent's and a booked wake stay as they are, but for an agent's
227
+ * paused `undeclared`, which resumes on a version that declares `harness.engine.triggers`. A
228
+ * manifest trigger whose id an agent's trigger holds is not added. The sweep arms what changed
229
+ * (`reconcileTriggers`).
175
230
  */
176
231
  export async function mergeAgentTriggers(t, agent, from, now = new Date()) {
177
232
  const before = ((await t.agentManifest(agent.manifestName, from))?.body?.runtime?.triggers ??
178
233
  {});
179
- const after = await triggersOf(t, agent);
234
+ const { triggers: after, declared } = await pinnedOf(t, agent);
180
235
  const limits = await readRuntimeLimits(t);
181
236
  const stamp = now.toISOString();
182
- const rows = new Map((await t.agentTriggers(agent.id)).map((row) => [row.triggerId, row]));
183
- for (const [triggerId, row] of rows)
184
- if (row.kind !== "wake" && after[triggerId] === undefined)
237
+ const all = await t.agentTriggers(agent.id);
238
+ const rows = new Map(all.filter((row) => originOf(row) === "manifest").map((row) => [row.triggerId, row]));
239
+ const taken = new Set(all.filter((row) => originOf(row) !== "manifest").map((row) => row.triggerId));
240
+ for (const [triggerId] of rows)
241
+ if (after[triggerId] === undefined)
185
242
  await t.deleteAgentTrigger(agent.id, triggerId);
243
+ if (declared)
244
+ for (const row of all)
245
+ if (originOf(row) === "agent" && row.status === "paused" && row.pausedReason === UNDECLARED) {
246
+ const next = row.definition ? nextFireAt(agent.id, row.triggerId, row.definition, now, limits) : null;
247
+ await t.putAgentTrigger({
248
+ ...row,
249
+ status: next === null ? "completed" : "active",
250
+ pausedReason: null,
251
+ nextFireAt: next?.toISOString() ?? null,
252
+ generation: row.generation + 1,
253
+ updatedAt: stamp,
254
+ });
255
+ }
186
256
  const timing = (trigger) => trigger === undefined ? undefined : JSON.stringify([trigger.schedule, trigger.timezone, trigger.at, trigger.webhook]);
187
257
  for (const [triggerId, trigger] of Object.entries(after)) {
258
+ if (taken.has(triggerId))
259
+ continue;
188
260
  const row = rows.get(triggerId);
189
261
  if (!row) {
190
262
  await t.putAgentTrigger({
@@ -201,6 +273,8 @@ export async function mergeAgentTriggers(t, agent, from, now = new Date()) {
201
273
  sessionId: null,
202
274
  note: null,
203
275
  effectId: null,
276
+ origin: "manifest",
277
+ ...NO_AGENT_FIELDS,
204
278
  createdAt: stamp,
205
279
  updatedAt: stamp,
206
280
  });
@@ -272,7 +346,9 @@ export async function fireTimer(ctx, key) {
272
346
  if (!target)
273
347
  return;
274
348
  const { agentId, triggerId } = target;
349
+ const runs = [];
275
350
  const armed = await ctx.store.tx(async (t) => {
351
+ runs.length = 0;
276
352
  const row = await t.agentTrigger(agentId, triggerId, { lock: true });
277
353
  if (!row || row.status !== "active" || row.nextFireAt === null)
278
354
  return undefined;
@@ -284,17 +360,31 @@ export async function fireTimer(ctx, key) {
284
360
  const agent = await liveEnvironment(t, agentId, { lock: true });
285
361
  if (!agent)
286
362
  return undefined;
287
- // A booked wake (D77) is its own trigger: its session, with its note as the message.
288
- const trigger = row.kind === "wake" ? wakeTriggerOf(row) : (await triggersOf(t, agent))[triggerId];
363
+ // A booked wake (D77) is its own trigger: its session, with its note as the message; an
364
+ // agent's (D83) is the row's own definition.
365
+ const pinned = await pinnedOf(t, agent);
366
+ const origin = originOf(row);
367
+ const trigger = definitionOf(row, pinned.triggers);
289
368
  if (!trigger) {
290
- await t.deleteAgentTrigger(agentId, triggerId);
369
+ // A manifest's that the version no longer has, or a wake with nothing to say. An agent's
370
+ // row always holds its definition.
371
+ if (origin !== "agent")
372
+ await t.deleteAgentTrigger(agentId, triggerId);
291
373
  return undefined;
292
374
  }
293
- await deliverRun(ctx, t, agent, triggerId, trigger, row, {
375
+ // An agent's trigger runs only while the version declares the capability: until one does, it
376
+ // waits paused and is not deleted.
377
+ if (origin === "agent" && !pinned.declared) {
378
+ const stamp = new Date(now).toISOString();
379
+ await t.putAgentTrigger({ ...row, status: "paused", pausedReason: UNDECLARED, nextFireAt: null, generation: row.generation + 1, updatedAt: stamp });
380
+ await t.agentEvent(agentId, "agent.trigger_paused", { triggerId, reason: UNDECLARED });
381
+ return undefined;
382
+ }
383
+ runs.push(await deliverRun(ctx, t, agent, triggerId, trigger, row, {
294
384
  runKey: `fire:${triggerId}:${due}`,
295
385
  dueAt: row.nextFireAt,
296
386
  late: now - due > hostTuning().triggers.lateGraceMs,
297
- });
387
+ }));
298
388
  const limits = await readRuntimeLimits(t);
299
389
  // A booked wake runs once and goes: the session may book the next.
300
390
  if (row.kind === "wake") {
@@ -314,9 +404,12 @@ export async function fireTimer(ctx, key) {
314
404
  await t.putAgentTrigger(updated);
315
405
  return updated;
316
406
  });
407
+ await appendRuns(ctx, runs);
317
408
  if (armed)
318
409
  await armTrigger(ctx, armed);
319
410
  }
411
+ /** Why an agent's trigger is paused: the version its Agent runs lacks `harness.engine.triggers` (D83). */
412
+ export const UNDECLARED = "undeclared";
320
413
  /** Who writes a triggered message: core, for the trigger (R4.9). */
321
414
  const triggerWriter = (triggerId) => ({
322
415
  source: { kind: "core", id: "runtime" },
@@ -330,6 +423,7 @@ export async function deliverRun(ctx, t, agent, triggerId, trigger, row, cause)
330
423
  const existing = await t.triggerRunByKey(agent.id, cause.runKey);
331
424
  if (existing)
332
425
  return existing;
426
+ const origin = row.kind === "wake" ? "wake" : (row.origin ?? "manifest");
333
427
  const firedAt = new Date().toISOString();
334
428
  const id = newRunId();
335
429
  const run = {
@@ -346,8 +440,13 @@ export async function deliverRun(ctx, t, agent, triggerId, trigger, row, cause)
346
440
  sessionId: null,
347
441
  turnId: null,
348
442
  queuedMessageId: null,
443
+ origin,
444
+ title: row.title ?? null,
445
+ streamedAt: null,
349
446
  };
350
- const outcome = await runOutcome(ctx, t, agent, triggerId, trigger, row.kind, id, firedAt, cause.payload);
447
+ // Under the `nylorun-triggers` plugin, an agent-made trigger's run is framed (ADR-106).
448
+ const framed = origin === "agent" && (await pinnedOf(t, agent)).plugin ? { ...trigger, prompt: `${triggerFrame(triggerId, row.title)}\n${trigger.prompt}` } : trigger;
449
+ const outcome = await runOutcome(ctx, t, agent, triggerId, framed, row.kind, id, firedAt, cause.payload, row.createdSessionId ?? null);
351
450
  Object.assign(run, outcome);
352
451
  await t.insertTriggerRun(run);
353
452
  await t.agentEvent(agent.id, "agent.run", {
@@ -361,14 +460,17 @@ export async function deliverRun(ctx, t, agent, triggerId, trigger, row, cause)
361
460
  });
362
461
  return run;
363
462
  }
364
- async function runOutcome(ctx, t, agent, triggerId, trigger, kind, runId, firedAt, payload) {
463
+ async function runOutcome(ctx, t, agent, triggerId, trigger, kind, runId, firedAt, payload,
464
+ /** The session whose agent made the trigger (D83): a session it opens is that session's owner's. */
465
+ createdBy) {
365
466
  const skipped = (reason, sessionId = null) => ({ status: "skipped", reason, sessionId });
366
467
  const failed = (reason, sessionId = null) => ({ status: "failed", reason, sessionId });
367
468
  if (agent.desiredState === "off")
368
469
  return skipped("paused");
369
- const limits = await readRuntimeLimits(t);
470
+ // The hourly cap in force: the manifest's `runtime.limits.triggers.runsPerHour`, never above the Tenant's.
471
+ const { runsPerHour } = triggerLimitsInForce(await readRuntimeLimits(t), agent.applied?.limits);
370
472
  const hourAgo = new Date(Date.parse(firedAt) - HOUR_MS).toISOString();
371
- if ((await t.countTriggerRuns(agent.id, hourAgo, ["started", "queued"])) >= limits.triggerRunsPerHour)
473
+ if ((await t.countTriggerRuns(agent.id, hourAgo, ["started", "queued"])) >= runsPerHour)
372
474
  return skipped("rate_limited");
373
475
  // `new`: a session of its own, named for the run, so a repeated cause never opens a second.
374
476
  const sessionId = trigger.session === "new" ? `run-${runId}` : trigger.session;
@@ -377,10 +479,11 @@ async function runOutcome(ctx, t, agent, triggerId, trigger, kind, runId, firedA
377
479
  return failed("session_conflict", sessionId);
378
480
  try {
379
481
  if (!session) {
482
+ const maker = createdBy === null ? undefined : await t.get("sessions", createdBy);
380
483
  await openSessionIn(ctx, t, sessionId, {
381
484
  requestId: `trigger-${runId}`,
382
485
  agent: agent.id,
383
- ownerUserId: agent.ownerUserId ?? "installation",
486
+ ownerUserId: maker?.ownerUserId ?? agent.ownerUserId ?? "installation",
384
487
  });
385
488
  session = (await t.lockSession(sessionId));
386
489
  }
@@ -462,7 +565,32 @@ export function runView(row) {
462
565
  queuedMessageId: row.queuedMessageId,
463
566
  };
464
567
  }
465
- /** `GET /v1/agents/{id}/triggers`. */
568
+ /** One trigger as the API shows it: its definition and the Agent's state of it. */
569
+ export function triggerView(row, trigger, origin = "") {
570
+ const from = originOf(row);
571
+ return {
572
+ id: row.triggerId,
573
+ kind: row.kind,
574
+ origin: from,
575
+ title: row.title,
576
+ createdSessionId: row.createdSessionId,
577
+ status: row.status,
578
+ pausedReason: row.pausedReason,
579
+ ...(trigger.schedule === undefined ? {} : { schedule: trigger.schedule, timezone: trigger.timezone ?? "UTC" }),
580
+ ...(trigger.at === undefined ? {} : { at: trigger.at }),
581
+ session: trigger.session,
582
+ nextRunAt: row.status === "active" ? row.nextFireAt : null,
583
+ lastRunAt: row.lastFireAt,
584
+ ...(row.endpointId === null
585
+ ? {}
586
+ : { webhookUrl: `${origin}${webhookPath(row.endpointId)}`, secretSet: row.sealedSecret !== null }),
587
+ ...(from === "wake" ? {} : { prompt: trigger.prompt }),
588
+ };
589
+ }
590
+ /**
591
+ * `GET /v1/agents/{id}/triggers`: the manifest's triggers the Agent holds, and the triggers its
592
+ * agents made (D83); never a session's booked wake.
593
+ */
466
594
  export async function listTriggers(ctx, agentId, origin = "") {
467
595
  return ctx.store.tx(async (t) => {
468
596
  const agent = (await liveEnvironment(t, agentId)) ?? fail(404, `Agent ${agentId} not found`, { code: "not_found" });
@@ -470,34 +598,19 @@ export async function listTriggers(ctx, agentId, origin = "") {
470
598
  const rows = await t.agentTriggers(agentId);
471
599
  return {
472
600
  triggers: rows.flatMap((row) => {
473
- const trigger = triggers[row.triggerId];
474
- if (!trigger)
601
+ if (originOf(row) === "wake")
475
602
  return [];
476
- return [
477
- {
478
- id: row.triggerId,
479
- kind: row.kind,
480
- status: row.status,
481
- pausedReason: row.pausedReason,
482
- ...(trigger.schedule === undefined ? {} : { schedule: trigger.schedule, timezone: trigger.timezone ?? "UTC" }),
483
- ...(trigger.at === undefined ? {} : { at: trigger.at }),
484
- session: trigger.session,
485
- nextRunAt: row.status === "active" ? row.nextFireAt : null,
486
- lastRunAt: row.lastFireAt,
487
- ...(row.endpointId === null
488
- ? {}
489
- : { webhookUrl: `${origin}${webhookPath(row.endpointId)}`, secretSet: row.sealedSecret !== null }),
490
- },
491
- ];
603
+ const trigger = definitionOf(row, triggers);
604
+ return trigger ? [triggerView(row, trigger, origin)] : [];
492
605
  }),
493
606
  };
494
607
  });
495
608
  }
496
- /** The trigger `triggerId` of Agent `agentId`, locked with its Agent, or a 404. */
609
+ /** The trigger `triggerId` of Agent `agentId`, locked with its Agent, or a 404. A booked wake is not one. */
497
610
  async function lockedTrigger(t, agentId, triggerId) {
498
611
  const agent = (await liveEnvironment(t, agentId, { lock: true })) ?? fail(404, `Agent ${agentId} not found`, { code: "not_found" });
499
612
  const row = await t.agentTrigger(agentId, triggerId, { lock: true });
500
- const trigger = (await triggersOf(t, agent))[triggerId];
613
+ const trigger = row && originOf(row) !== "wake" ? definitionOf(row, await triggersOf(t, agent)) : undefined;
501
614
  if (!row || !trigger)
502
615
  return fail(404, `Agent ${agentId} has no trigger ${triggerId}`, { code: "not_found" });
503
616
  return { agent, row, trigger };
@@ -520,9 +633,7 @@ export async function setTriggerStatus(ctx, agentId, triggerId, action, origin =
520
633
  const next = nextFireAt(agentId, triggerId, trigger, now, await readRuntimeLimits(t));
521
634
  // An `at` runs once: one that ran, or whose time passed, does not run again.
522
635
  if (trigger.at !== undefined && (next === null || row.status === "completed"))
523
- fail(409, `Trigger ${triggerId} was due at ${trigger.at} and does not run again: save a manifest version with a new time.`, {
524
- code: "trigger_expired",
525
- });
636
+ fail(409, `Trigger ${triggerId} was due at ${trigger.at} and does not run again: ${originOf(row) === "agent" ? "the agent creates it again with a new time" : "save a manifest version with a new time"}.`, { code: "trigger_expired" });
526
637
  const updated = {
527
638
  ...row,
528
639
  status: "active",
@@ -539,20 +650,37 @@ export async function setTriggerStatus(ctx, agentId, triggerId, action, origin =
539
650
  await armTrigger(ctx, changed);
540
651
  return listTriggers(ctx, agentId, origin);
541
652
  }
653
+ /**
654
+ * `DELETE /v1/agents/{id}/triggers/{triggerId}` (D83): deletes a trigger one of the Agent's agents
655
+ * made (origin `agent`), with `agent.trigger_deleted`. A manifest's is the manifest's to drop
656
+ * (`409 trigger_from_manifest`); its runs stay.
657
+ */
658
+ export async function deleteTrigger(ctx, agentId, triggerId, principal) {
659
+ await ctx.store.tx(async (t) => {
660
+ const { row } = await lockedTrigger(t, agentId, triggerId);
661
+ if (originOf(row) !== "agent")
662
+ fail(409, `Trigger ${triggerId} is declared by the Agent's manifest: save a version without it, or pause it.`, {
663
+ code: "trigger_from_manifest",
664
+ });
665
+ await t.deleteAgentTrigger(agentId, triggerId);
666
+ await t.agentEvent(agentId, "agent.trigger_deleted", { triggerId, ...(principal === undefined ? {} : { principal }) });
667
+ });
668
+ }
542
669
  /**
543
670
  * `POST .../triggers/{triggerId}/run`: runs it now, whatever its status, keyed by the request's
544
671
  * `Idempotency-Key`, so a retried request makes one run.
545
672
  */
546
673
  export async function runTriggerNow(ctx, agentId, triggerId, idempotencyKey) {
547
- return ctx.store.tx(async (t) => {
674
+ const run = await ctx.store.tx(async (t) => {
548
675
  const { agent, row, trigger } = await lockedTrigger(t, agentId, triggerId);
549
- const run = await deliverRun(ctx, t, agent, triggerId, trigger, row, {
676
+ return deliverRun(ctx, t, agent, triggerId, trigger, row, {
550
677
  runKey: `manual:${triggerId}:${idempotencyKey}`,
551
678
  dueAt: new Date().toISOString(),
552
679
  late: false,
553
680
  });
554
- return runView(run);
555
681
  });
682
+ await appendRuns(ctx, [run]);
683
+ return runView(run);
556
684
  }
557
685
  /** `GET /v1/agents/{id}/runs`: newest first, `limit` per page, after cursor `before`. */
558
686
  export async function listRuns(ctx, agentId, options) {
@@ -615,7 +743,9 @@ export async function receiveWebhook(ctx, endpointId, delivery) {
615
743
  });
616
744
  if (!verified)
617
745
  return unsigned();
618
- return ctx.store.tx(async (t) => {
746
+ const runs = [];
747
+ const answer = await ctx.store.tx(async (t) => {
748
+ runs.length = 0;
619
749
  const { agent, row, trigger } = await lockedTrigger(t, found.agentId, found.triggerId);
620
750
  const runKey = `webhook:${row.triggerId}:${id}`;
621
751
  const repeated = await t.triggerRunByKey(agent.id, runKey);
@@ -630,8 +760,11 @@ export async function receiveWebhook(ctx, endpointId, delivery) {
630
760
  });
631
761
  if (!repeated)
632
762
  await t.putAgentTrigger({ ...row, lastFireAt: receivedAt, updatedAt: receivedAt });
763
+ runs.push(run);
633
764
  return { runId: run.id, status: run.status, reason: run.reason };
634
765
  });
766
+ await appendRuns(ctx, runs);
767
+ return answer;
635
768
  }
636
769
  // ---------------------------------------------------------------------------
637
770
  // Booked wakes (D77)
@@ -707,6 +840,8 @@ export async function serveRuntimeTool(t, s, request) {
707
840
  sessionId: s.id,
708
841
  note: input.note,
709
842
  effectId: request.effectId,
843
+ origin: "wake",
844
+ ...NO_AGENT_FIELDS,
710
845
  createdAt: existing?.createdAt ?? stamp,
711
846
  updatedAt: stamp,
712
847
  });
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const RUNTIME_VERSION = "0.40.0-beta";
1
+ export declare const RUNTIME_VERSION = "0.42.0-beta";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Kept in step with package.json by runtime/server/scripts/check-package.mjs, which runs in npm run check.
2
2
  // release:prepare also rewrites this when it bumps @nylorun/runtime.
3
- export const RUNTIME_VERSION = "0.40.0-beta";
3
+ export const RUNTIME_VERSION = "0.42.0-beta";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/runtime",
3
- "version": "0.40.0-beta",
3
+ "version": "0.42.0-beta",
4
4
  "description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -65,8 +65,8 @@
65
65
  "@hono/node-server": "^2.1.5",
66
66
  "@hono/zod-openapi": "^1.6.3",
67
67
  "@modelcontextprotocol/sdk": "^1.32.1",
68
- "@nylorun/core": "0.34.0-beta",
69
- "@nylorun/harness": "0.31.0-beta",
68
+ "@nylorun/core": "0.36.0-beta",
69
+ "@nylorun/harness": "0.33.0-beta",
70
70
  "@restatedev/restate-sdk": "1.17.3",
71
71
  "@restatedev/restate-sdk-clients": "1.17.3",
72
72
  "@s2-dev/streamstore": "0.28.0",