@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.
- package/CHANGELOG.md +30 -0
- package/dist/adapters/streams/s2.js +9 -2
- package/dist/api/components.d.ts +131 -0
- package/dist/api/components.js +3 -1
- package/dist/api/http/app.js +2 -0
- package/dist/api/http/routes/runs.d.ts +8 -0
- package/dist/api/http/routes/runs.js +30 -0
- package/dist/api/http/routes/triggers.d.ts +2 -1
- package/dist/api/http/routes/triggers.js +32 -4
- package/dist/api/openapi.js +3 -1
- package/dist/gates/policy.d.ts +6 -4
- package/dist/gates/policy.js +10 -2
- package/dist/harness-api/record.js +51 -6
- package/dist/harness-api/start.js +15 -9
- package/dist/harness-api/tool-journal.js +3 -0
- package/dist/management-openapi.json +11 -6
- package/dist/openapi.json +550 -12
- package/dist/store/postgres/drizzle/0043_agent_triggers_origin.sql +11 -0
- package/dist/store/postgres/drizzle/0044_agent_triggers_origin_backfill.sql +8 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +14 -0
- package/dist/store/postgres/schema.d.ts +151 -4
- package/dist/store/postgres/schema.js +25 -4
- package/dist/store/postgres/store.js +20 -2
- package/dist/store/types.d.ts +10 -0
- package/dist/streams/types.d.ts +16 -1
- package/dist/streams/types.js +8 -1
- package/dist/tenant/advance.js +7 -0
- package/dist/tenant/agent-triggers.d.ts +60 -0
- package/dist/tenant/agent-triggers.js +196 -0
- package/dist/tenant/agents.d.ts +1 -1
- package/dist/tenant/agents.js +5 -1
- package/dist/tenant/limits.d.ts +26 -7
- package/dist/tenant/limits.js +39 -6
- package/dist/tenant/runs-stream.d.ts +51 -0
- package/dist/tenant/runs-stream.js +164 -0
- package/dist/tenant/sweep.js +3 -0
- package/dist/tenant/triggers.d.ts +36 -5
- package/dist/tenant/triggers.js +183 -48
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
|
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.
|
|
42
|
-
*
|
|
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"
|
|
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
|
-
/**
|
|
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.
|
package/dist/tenant/triggers.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
139
|
-
|
|
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.
|
|
174
|
-
*
|
|
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
|
|
234
|
+
const { triggers: after, declared } = await pinnedOf(t, agent);
|
|
180
235
|
const limits = await readRuntimeLimits(t);
|
|
181
236
|
const stamp = now.toISOString();
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"])) >=
|
|
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
|
-
/**
|
|
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
|
-
|
|
474
|
-
if (!trigger)
|
|
601
|
+
if (originOf(row) === "wake")
|
|
475
602
|
return [];
|
|
476
|
-
|
|
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))
|
|
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
|
-
|
|
674
|
+
const run = await ctx.store.tx(async (t) => {
|
|
548
675
|
const { agent, row, trigger } = await lockedTrigger(t, agentId, triggerId);
|
|
549
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
69
|
-
"@nylorun/harness": "0.
|
|
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",
|