@nylorun/runtime 0.27.0-beta → 0.29.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 (160) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +1 -1
  3. package/dist/api/a2a/card.d.ts +4 -3
  4. package/dist/api/a2a/card.js +3 -3
  5. package/dist/api/a2a/routes.js +18 -7
  6. package/dist/api/ag-ui/routes.js +4 -7
  7. package/dist/api/components.d.ts +5854 -2718
  8. package/dist/api/components.js +39 -30
  9. package/dist/api/http/app.js +9 -4
  10. package/dist/api/http/body.d.ts +2 -0
  11. package/dist/api/http/body.js +20 -0
  12. package/dist/api/http/define.d.ts +4 -0
  13. package/dist/api/http/define.js +20 -9
  14. package/dist/api/http/routes/agent-manifests.d.ts +10 -0
  15. package/dist/api/http/routes/agent-manifests.js +135 -0
  16. package/dist/api/http/routes/agents.d.ts +11 -0
  17. package/dist/api/http/routes/agents.js +418 -0
  18. package/dist/api/http/routes/artifacts.js +2 -2
  19. package/dist/api/http/routes/coverage.d.ts +4 -4
  20. package/dist/api/http/routes/coverage.js +12 -6
  21. package/dist/api/http/routes/files.js +1 -1
  22. package/dist/api/http/routes/mcp.js +2 -2
  23. package/dist/api/http/routes/me.d.ts +1 -1
  24. package/dist/api/http/routes/me.js +1 -2
  25. package/dist/api/http/routes/memories.d.ts +9 -0
  26. package/dist/api/http/routes/memories.js +152 -0
  27. package/dist/api/http/routes/reads.d.ts +1 -137
  28. package/dist/api/http/routes/reads.js +31 -12
  29. package/dist/api/http/routes/sessions.js +21 -50
  30. package/dist/api/http/routes/tenant.js +13 -10
  31. package/dist/api/http/routes/triggers.d.ts +10 -0
  32. package/dist/api/http/routes/triggers.js +183 -0
  33. package/dist/api/openapi.js +69 -47
  34. package/dist/artifacts/disk.d.ts +10 -10
  35. package/dist/artifacts/disk.js +27 -27
  36. package/dist/artifacts/quota.d.ts +1 -1
  37. package/dist/artifacts/quota.js +2 -2
  38. package/dist/artifacts/service.js +2 -2
  39. package/dist/core/flow-host.d.ts +0 -1
  40. package/dist/gates/egress-credentials.js +1 -1
  41. package/dist/gates/egress.d.ts +5 -5
  42. package/dist/gates/egress.js +6 -6
  43. package/dist/gates/http-tool.js +2 -2
  44. package/dist/gates/meter.js +22 -5
  45. package/dist/gates/policy.d.ts +8 -3
  46. package/dist/gates/policy.js +7 -2
  47. package/dist/gates/tenant-vaults.d.ts +1 -1
  48. package/dist/harness/pod.d.ts +2 -0
  49. package/dist/harness/setup.js +17 -6
  50. package/dist/harness-api/record.js +11 -0
  51. package/dist/harness-api/server.js +4 -0
  52. package/dist/harness-api/start.d.ts +11 -7
  53. package/dist/harness-api/start.js +49 -5
  54. package/dist/harness-api/workspace.js +2 -2
  55. package/dist/host/config-for.d.ts +2 -0
  56. package/dist/host/config-for.js +1 -0
  57. package/dist/host/http.d.ts +2 -2
  58. package/dist/host/http.js +5 -2
  59. package/dist/host/main.js +1 -0
  60. package/dist/keys/keys.d.ts +15 -4
  61. package/dist/keys/keys.js +16 -0
  62. package/dist/keys/webhooks.d.ts +25 -0
  63. package/dist/keys/webhooks.js +62 -0
  64. package/dist/management-openapi.json +313 -133
  65. package/dist/mcp/connect.d.ts +2 -2
  66. package/dist/mcp/connect.js +3 -3
  67. package/dist/mcp/preview.js +7 -7
  68. package/dist/memory/service.d.ts +97 -0
  69. package/dist/memory/service.js +335 -0
  70. package/dist/memory/tool.d.ts +36 -0
  71. package/dist/memory/tool.js +84 -0
  72. package/dist/model/pi-model.js +7 -0
  73. package/dist/openapi.json +13120 -9034
  74. package/dist/reads/types.d.ts +3 -3
  75. package/dist/record/agent.d.ts +27 -0
  76. package/dist/record/agent.js +46 -0
  77. package/dist/record/index.d.ts +1 -1
  78. package/dist/record/index.js +1 -1
  79. package/dist/sandbox/join.js +5 -5
  80. package/dist/sandbox/placement.d.ts +1 -1
  81. package/dist/sandbox/placement.js +3 -3
  82. package/dist/sandbox/pods/lifecycle.d.ts +6 -6
  83. package/dist/sandbox/pods/lifecycle.js +6 -6
  84. package/dist/sandbox/pods/reconcile.js +10 -2
  85. package/dist/sandbox/resolve.d.ts +3 -3
  86. package/dist/sandbox/resolve.js +7 -7
  87. package/dist/sandbox/setup.d.ts +3 -1
  88. package/dist/sandbox/setup.js +14 -4
  89. package/dist/store/postgres/drizzle/0032_agents.sql +54 -0
  90. package/dist/store/postgres/drizzle/0033_agent_versions.sql +7 -0
  91. package/dist/store/postgres/drizzle/0034_protocol_17_session_queue.sql +13 -0
  92. package/dist/store/postgres/drizzle/0035_t4_triggers.sql +41 -0
  93. package/dist/store/postgres/drizzle/0036_t5_memories.sql +30 -0
  94. package/dist/store/postgres/drizzle/0037_model_usage_cost_split.sql +4 -0
  95. package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
  96. package/dist/store/postgres/reads.js +44 -87
  97. package/dist/store/postgres/record-writer.d.ts +4 -4
  98. package/dist/store/postgres/record-writer.js +9 -9
  99. package/dist/store/postgres/schema.d.ts +1408 -123
  100. package/dist/store/postgres/schema.js +194 -38
  101. package/dist/store/postgres/store.js +329 -79
  102. package/dist/store/postgres/tenant.js +1 -2
  103. package/dist/store/types.d.ts +155 -50
  104. package/dist/store/types.js +0 -1
  105. package/dist/tenant/advance.js +20 -8
  106. package/dist/tenant/agent-manifests.d.ts +72 -0
  107. package/dist/tenant/agent-manifests.js +359 -0
  108. package/dist/tenant/agent-versions.d.ts +126 -0
  109. package/dist/tenant/agent-versions.js +389 -0
  110. package/dist/tenant/agents.d.ts +230 -0
  111. package/dist/tenant/{environments.js → agents.js} +464 -320
  112. package/dist/tenant/auth.d.ts +1 -1
  113. package/dist/tenant/auth.js +3 -3
  114. package/dist/tenant/commands.d.ts +63 -2
  115. package/dist/tenant/commands.js +240 -83
  116. package/dist/tenant/context.d.ts +46 -19
  117. package/dist/tenant/context.js +6 -2
  118. package/dist/tenant/effects.js +7 -16
  119. package/dist/tenant/identity-file.d.ts +6 -6
  120. package/dist/tenant/identity-file.js +12 -10
  121. package/dist/tenant/issuers.js +3 -3
  122. package/dist/tenant/limits.d.ts +0 -3
  123. package/dist/tenant/limits.js +0 -3
  124. package/dist/tenant/memories.d.ts +38 -0
  125. package/dist/tenant/memories.js +119 -0
  126. package/dist/tenant/reset.js +2 -2
  127. package/dist/tenant/run-grants.d.ts +6 -0
  128. package/dist/tenant/run-grants.js +4 -0
  129. package/dist/tenant/runtime.d.ts +1 -1
  130. package/dist/tenant/runtime.js +8 -0
  131. package/dist/tenant/sessions.d.ts +28 -45
  132. package/dist/tenant/sessions.js +85 -208
  133. package/dist/tenant/status.js +6 -7
  134. package/dist/tenant/sweep.js +12 -3
  135. package/dist/tenant/transcript.d.ts +16 -16
  136. package/dist/tenant/triggers.d.ts +143 -0
  137. package/dist/tenant/triggers.js +715 -0
  138. package/dist/tenant/turn-timing.d.ts +11 -1
  139. package/dist/tenant/turn-timing.js +3 -1
  140. package/dist/tenant/types.d.ts +5 -0
  141. package/dist/tenant/worker.d.ts +2 -0
  142. package/dist/tenant/worker.js +5 -0
  143. package/dist/traces/map.d.ts +1 -1
  144. package/dist/traces/map.js +7 -1
  145. package/dist/tuning.d.ts +7 -0
  146. package/dist/tuning.js +19 -0
  147. package/dist/vault/service.d.ts +13 -8
  148. package/dist/vault/service.js +61 -17
  149. package/dist/version.d.ts +1 -1
  150. package/dist/version.js +1 -1
  151. package/package.json +13 -12
  152. package/dist/api/http/routes/environment-manifests.d.ts +0 -9
  153. package/dist/api/http/routes/environment-manifests.js +0 -118
  154. package/dist/api/http/routes/environments.d.ts +0 -11
  155. package/dist/api/http/routes/environments.js +0 -332
  156. package/dist/record/environment.d.ts +0 -27
  157. package/dist/record/environment.js +0 -46
  158. package/dist/tenant/environment-manifests.d.ts +0 -57
  159. package/dist/tenant/environment-manifests.js +0 -208
  160. package/dist/tenant/environments.d.ts +0 -172
@@ -48,6 +48,6 @@ export declare function accessOf(scope: AuthScope): SessionAccess | undefined;
48
48
  export declare function ownerOf(scope: AuthScope): string | undefined;
49
49
  /** The application principal behind the request, acting for a subject or not. */
50
50
  export declare function requirePrincipal(scope: AuthScope): string;
51
- /** True when a token caller may reach `agentId` (its issuer's allowlist). Other callers always may. */
51
+ /** True when a token caller may reach Agent `agentId` (its grants). Other callers always may. */
52
52
  export declare function mayUseAgent(scope: AuthScope, agentId: string): boolean;
53
53
  export declare function requireApplication(scope: AuthScope): string;
@@ -1,5 +1,5 @@
1
1
  import { SCOPES_HEADER, SUBJECT_HEADER, } from "@nylorun/core/compatibility";
2
- import { parseSubjectHeaders, TOKEN_SCOPES } from "@nylorun/core/contracts";
2
+ import { agentGranted, parseSubjectHeaders, TOKEN_SCOPES } from "@nylorun/core/contracts";
3
3
  import { hashToken } from "../core/bearer.js";
4
4
  import { fail } from "./http.js";
5
5
  import { verifyIssuerToken } from "./issuers.js";
@@ -168,9 +168,9 @@ export function requirePrincipal(scope) {
168
168
  return scope.principalId;
169
169
  return fail(403, "Application credential required");
170
170
  }
171
- /** True when a token caller may reach `agentId` (its issuer's allowlist). Other callers always may. */
171
+ /** True when a token caller may reach Agent `agentId` (its grants). Other callers always may. */
172
172
  export function mayUseAgent(scope, agentId) {
173
- return scope.kind !== "token" || scope.agents === "*" || scope.agents.has(agentId);
173
+ return scope.kind !== "token" || scope.agents === "*" || agentGranted(scope.agents, agentId);
174
174
  }
175
175
  export function requireApplication(scope) {
176
176
  if (scope.kind === "application")
@@ -1,5 +1,6 @@
1
- import type { EventWriter, SessionCommand } from "@nylorun/core/contracts";
2
- import { type AuthScope, type TenantContext } from "./context.js";
1
+ import type { EventWriter, MessageDelivery, MessageEventBody, SessionCommand } from "@nylorun/core/contracts";
2
+ import type { Tx } from "../store/types.js";
3
+ import { type AuthScope, type Session, type TenantContext } from "./context.js";
3
4
  import type { TraceContext } from "../traces/ids.js";
4
5
  export declare function command(ctx: TenantContext, id: string, input: SessionCommand, scope: AuthScope,
5
6
  /**
@@ -8,7 +9,67 @@ export declare function command(ctx: TenantContext, id: string, input: SessionCo
8
9
  */
9
10
  options?: {
10
11
  trace?: TraceContext;
12
+ /**
13
+ * What a message to a busy session does when it says nothing (`delivery`): the route's
14
+ * answer to the caller's protocol (`queue` from 17). Core's own messages and the AG-UI and
15
+ * A2A adapters, which stream the run they start, take `reject`.
16
+ */
17
+ delivery?: MessageDelivery;
11
18
  }): Promise<unknown>;
19
+ export declare const takesMessages: (s: Session) => boolean;
20
+ /** A message ready to start a turn: as sent, its engine input and who sent it. */
21
+ export interface TurnMessage {
22
+ command: MessageEventBody;
23
+ /** An agent's message input, or a workflow's input value. */
24
+ input: unknown;
25
+ /** The artifacts its file parts named. */
26
+ fileArtifacts: string[];
27
+ writer: EventWriter;
28
+ /** Set for a message that waited in the session's queue. */
29
+ queuedMessageId?: string;
30
+ trace?: TraceContext;
31
+ /** What sent it when not a client (track T4): a trigger's run, or the session's own wake. */
32
+ cause?: MessageCause;
33
+ }
34
+ /** What sent a message when not a client (`command.message`'s `cause`). */
35
+ export interface MessageCause {
36
+ kind: "trigger" | "self";
37
+ triggerId: string;
38
+ runId: string;
39
+ }
40
+ /** How a message was taken: it started a turn, or waits in the session's queue. */
41
+ export type MessageAnswer = {
42
+ status: "accepted";
43
+ turnId: string;
44
+ cursor: string;
45
+ } | {
46
+ status: "queued";
47
+ queuedMessageId: string;
48
+ position: number;
49
+ cursor: string;
50
+ };
51
+ /**
52
+ * Takes a resolved message on session `s` (locked) in transaction `t`: it starts a turn when the
53
+ * session takes messages and none wait, else it waits in the queue behind them (protocol 17), and
54
+ * an idle session whose queue was left waiting (its Agent was unavailable when the last turn
55
+ * ended) starts a turn with all of them, this one last. Past `sessionQueuedMessages` it is
56
+ * `429 queue_full`. `waiting` is the queue's length, read under the session's lock.
57
+ */
58
+ export declare function acceptMessage(ctx: TenantContext, t: Tx, s: Session, id: string, message: TurnMessage, waiting?: number): Promise<MessageAnswer>;
59
+ /** Who checks a queued message's turn start: core, since the sender's grants were checked when it was queued. */
60
+ export declare const QUEUE_SCOPE: AuthScope;
61
+ /**
62
+ * Starts the next turn of session `s` (locked) from its queue, when it takes messages and messages
63
+ * wait (protocol 17, D76): in the transaction that ended the last turn, or a cancel's. Our engine
64
+ * takes every queued message that carries the same turn manifest as the first, a workflow one.
65
+ * Writes `queue.taken`. Returns nothing when nothing started: an empty queue, or an Agent that
66
+ * cannot take a turn now, whose messages wait for the next message or turn end.
67
+ */
68
+ export declare function startQueuedTurn(ctx: TenantContext, t: Tx, s: Session, id: string): Promise<{
69
+ turnId: string;
70
+ taken: string[];
71
+ cursor: string;
72
+ } | undefined>;
12
73
  /**
13
74
  * Who sent a command (R4.9), from its credential and never its body: a client over the Runtime
14
75
  * API (an application, or a person through it or a token), or core itself.
@@ -14,6 +14,12 @@
14
14
  * same transaction; the engine host sees it before its next effect and before settlement on
15
15
  * any Worker. It then aborts an advance running on this process (`ctx.abortLocal`); every
16
16
  * other process aborts its own on the signal (`control.ts`).
17
+ *
18
+ * A message to a session whose turn runs or waits (protocol 17, D76) is queued
19
+ * (`session_queue`, `message.queued`) unless it asks `delivery: "reject"`, which is
20
+ * `409 session_busy`. A turn that ends takes every queued message in the same transaction
21
+ * (`startQueuedTurn`): client messages and queued ones start a turn through one door,
22
+ * `beginMessageTurn`, which writes each one's `command.message`.
17
23
  */
18
24
  import { randomUUID } from "node:crypto";
19
25
  import { createDurableCheckpoint, createFlowCheckpoint, resumeFlowCheckpoint, } from "@nylorun/harness/run";
@@ -21,10 +27,11 @@ import { commandKey, cancelQueuedEffects, flowInteractionOf, foreignInteractionC
21
27
  import { resolveMessageManifest } from "../core/turn-manifest.js";
22
28
  import { canonical } from "../store/canonical.js";
23
29
  import { lockedSession, } from "./context.js";
24
- import { fail } from "./http.js";
30
+ import { fail, HttpError } from "./http.js";
31
+ import { readRuntimeLimits } from "./runtime-limits.js";
25
32
  import { settleToolRow } from "../harness-api/tool-journal.js";
26
33
  import { accessOf } from "./auth.js";
27
- import { checkEnvironmentTurn } from "./environments.js";
34
+ import { checkAgentTurn } from "./agents.js";
28
35
  import { turnManifestOf, variantStore } from "./session.js";
29
36
  import { slimModelEffects } from "./slim.js";
30
37
  import { resolveMessageParts } from "../artifacts/parts.js";
@@ -42,6 +49,7 @@ export async function command(ctx, id, input, scope,
42
49
  options = {}) {
43
50
  const { store } = ctx;
44
51
  const cascadeCancelIds = [];
52
+ let cancelledTurn = null;
45
53
  const response = await store.tx(async (t) => {
46
54
  const s = await lockedSession(t, id, accessOf(scope));
47
55
  const command = input;
@@ -64,8 +72,11 @@ options = {}) {
64
72
  let held;
65
73
  // The turn a `cancel` ended, if one was active.
66
74
  let cancelledTurnId = null;
75
+ /** A message's answer: accepted with its turn, or queued (protocol 17). */
76
+ let answer;
67
77
  if (command.type === "cancel") {
68
78
  cancelledTurnId = s.activeTurnId;
79
+ cancelledTurn = cancelledTurnId;
69
80
  const workflowCancel = isWorkflowManifest(s.manifest);
70
81
  // Linked sessions this one started: a workflow's agents, or flow agents an agent
71
82
  // uses as tools.
@@ -128,84 +139,25 @@ options = {}) {
128
139
  }
129
140
  }
130
141
  else {
131
- // A session opened before protocol 11 (migration 0020), 13 (migration 0024) or 15
132
- // (migrations 0029, 0030) keeps its history but takes no turn.
142
+ // A session opened before protocol 11 (migration 0020), 13 (migration 0024), 15
143
+ // (migrations 0029, 0030) or 16 (migration 0032) keeps its history but takes no turn.
133
144
  if (s.closedReason !== undefined)
134
- fail(409, s.closedReason === "protocol_11"
135
- ? "This session was opened before protocol 11: open a new session that names an Environment."
136
- : s.closedReason === "protocol_15"
137
- ? "This session was opened before protocol 15, and its pinned manifest holds a runtime block: our harness now writes the model's instructions and defines its tools itself. Open a new session."
138
- : "This session was opened before protocol 13, on a manifest the Runtime no longer runs: register the agent again (manifest 7) and open a new session.", { code: "session_closed" });
145
+ fail(409, s.closedReason === "protocol_16"
146
+ ? "This session was opened before protocol 16, from an agent definition: save the agent as an agent manifest (nylorun migrate v8, then nylorun apply), make an Agent from it and open a new session on it."
147
+ : s.closedReason === "protocol_11"
148
+ ? "This session was opened before protocol 11: open a new session on an Agent."
149
+ : s.closedReason === "protocol_15"
150
+ ? "This session was opened before protocol 15, and its pinned manifest holds a runtime block: our harness now writes the model's instructions and defines its tools itself. Open a new session."
151
+ : "This session was opened before protocol 13, on a manifest the Runtime no longer runs: register the agent again (manifest 7) and open a new session.", { code: "session_closed" });
139
152
  if (command.type === "message") {
140
- if (!["idle", "completed", "failed", "cancelled"].includes(s.status))
141
- fail(409, "Session has active or unresolved work");
153
+ const delivery = command.delivery ?? options.delivery ?? "reject";
154
+ const waiting = await t.countQueuedMessages(id);
155
+ if (!takesMessages(s) && delivery === "reject")
156
+ fail(409, "Session has active or unresolved work", { code: "session_busy" });
142
157
  // Every turn start on a sandbox: it exists, the token's grants reach it, and no other
143
- // session's turn holds it.
144
- await checkEnvironmentTurn(t, s, scope, ctx);
145
- s.turnStartState = s.state;
146
- // A new turn folds from the latest snapshot: a cancel or failure only reverts this turn.
147
- if (s.history?.snapshot !== undefined)
148
- s.history = { ...s.history, from: s.history.snapshot };
149
- s.activeTurnId = randomUUID();
150
- startTurn(s);
151
- if (options.trace)
152
- s.turnTrace = options.trace;
153
- else
154
- delete s.turnTrace;
155
- // File parts name artifacts the caller may read; each is pinned to a version now. Any
156
- // file goes (protocol 15): the harness decides how the model sees it, and a workflow
157
- // passes it on as data.
158
- const parts = "parts" in command
159
- ? await resolveMessageParts(t, command.parts, id, accessOf(scope))
160
- : undefined;
161
- // The files the session's messages named: `read_artifact` reads them again (protocol 12).
162
- if (parts) {
163
- const named = parts.pinned.flatMap((part) => (part.type === "file" ? [part.artifactId] : []));
164
- if (named.length > 0)
165
- s.fileArtifacts = [...new Set([...(s.fileArtifacts ?? []), ...named])];
166
- }
167
- const data = "data" in command ? command.data : undefined;
168
- if (isWorkflowManifest(s.manifest)) {
169
- const input = "content" in command
170
- ? command.content
171
- : parts
172
- ? { parts: parts.pinned }
173
- : data;
174
- s.checkpoint = createFlowCheckpoint({
175
- manifest: s.manifest,
176
- sessionId: id,
177
- turnId: s.activeTurnId,
178
- input,
179
- });
180
- }
181
- else {
182
- // Optional message.manifest: validate as variant, pin by hash, apply this turn only.
183
- const resolved = resolveMessageManifest({
184
- pinned: s.manifest,
185
- pinnedHash: s.manifestHash,
186
- messageManifest: command.manifest,
187
- store: variantStore(s),
188
- });
189
- if (!resolved.ok) {
190
- fail(400, resolved.message);
191
- }
192
- else {
193
- s.checkpoint = createDurableCheckpoint({
194
- manifest: resolved.manifest,
195
- sessionId: id,
196
- turnId: s.activeTurnId,
197
- input: "content" in command
198
- ? command.content
199
- : parts
200
- ? { content: parts.engine }
201
- : typeof data === "string"
202
- ? data
203
- : JSON.stringify(data),
204
- state: s.state,
205
- info: s.info,
206
- });
207
- }
208
- }
158
+ // session's turn holds it. A queued message is checked when it is queued, too.
159
+ await checkAgentTurn(t, s, scope, ctx);
160
+ answer = await acceptMessage(ctx, t, s, id, await turnMessage(t, s, id, command, scope, options.trace), waiting);
209
161
  }
210
162
  else if (command.type === "approve" &&
211
163
  (held = await heldCall(t, s, command.interactionId)) !== undefined) {
@@ -278,13 +230,15 @@ options = {}) {
278
230
  segment: (s.checkpoint?.segment ?? 0) + 1,
279
231
  });
280
232
  }
233
+ }
234
+ if (command.type !== "cancel" && command.type !== "message") {
281
235
  // A paused turn goes on: its wait for a person is not its running time (R7.1).
282
236
  resumeTurn(s);
283
237
  s.status = "runnable";
284
238
  s.waits = undefined;
285
239
  // A flow answers each interaction in the same segment, as a held tool call's approval is
286
240
  // answered (R5 step 2): the interaction names the cause.
287
- const cause = command.type !== "message" && (isWorkflowManifest(s.manifest) || held !== undefined)
241
+ const cause = isWorkflowManifest(s.manifest) || held !== undefined
288
242
  ? command.interactionId
289
243
  : s.checkpoint.segment;
290
244
  const commandWake = {
@@ -292,8 +246,11 @@ options = {}) {
292
246
  dedupeKey: `${command.type}:${s.activeTurnId}:${cause}`,
293
247
  };
294
248
  await t.wake(id, commandWake);
295
- event = await t.event(id, s.activeTurnId, `command.${command.type}`, command.type === "message" && options.trace ? { ...command, trace: options.trace } : command, commandWriter(scope));
249
+ event = await t.event(id, s.activeTurnId, `command.${command.type}`, command, commandWriter(scope));
296
250
  }
251
+ // A cancel ends the turn, not the queue: the messages waiting start the next one.
252
+ if (command.type === "cancel")
253
+ await startQueuedTurn(ctx, t, s, id);
297
254
  await t.put("sessions", id, s);
298
255
  // Direct cancel of a linked agent fails that node with agent.cancelled (WF-R54). The agent
299
256
  // session is locked above, before wakeLinkedWorkflow locks its workflow.
@@ -306,16 +263,18 @@ options = {}) {
306
263
  error: "Agent turn was cancelled",
307
264
  });
308
265
  const response = {
309
- status: "accepted",
310
- turnId: s.activeTurnId,
311
- cursor: event.cursor,
266
+ ...(answer ?? {
267
+ status: "accepted",
268
+ turnId: command.type === "cancel" ? null : s.activeTurnId,
269
+ cursor: event.cursor,
270
+ }),
312
271
  requestId: command.requestId,
313
272
  };
314
273
  await t.put("commands", key, { command, response });
315
274
  return response;
316
275
  });
317
276
  if (input.type === "cancel")
318
- ctx.abortLocal(id);
277
+ ctx.abortLocal(id, cancelledTurn ?? undefined);
319
278
  // Cascade cancel linked agents deepest-first under existing fencing (WF-R53).
320
279
  for (const agentId of cascadeCancelIds) {
321
280
  try {
@@ -332,6 +291,204 @@ options = {}) {
332
291
  }
333
292
  return response;
334
293
  }
294
+ /** Statuses in which a session takes a message as a new turn; in the others a turn runs or waits. */
295
+ const TAKES_MESSAGES = ["idle", "completed", "failed", "cancelled"];
296
+ export const takesMessages = (s) => TAKES_MESSAGES.includes(s.status);
297
+ /**
298
+ * Takes a resolved message on session `s` (locked) in transaction `t`: it starts a turn when the
299
+ * session takes messages and none wait, else it waits in the queue behind them (protocol 17), and
300
+ * an idle session whose queue was left waiting (its Agent was unavailable when the last turn
301
+ * ended) starts a turn with all of them, this one last. Past `sessionQueuedMessages` it is
302
+ * `429 queue_full`. `waiting` is the queue's length, read under the session's lock.
303
+ */
304
+ export async function acceptMessage(ctx, t, s, id, message, waiting) {
305
+ const queued = waiting ?? (await t.countQueuedMessages(id));
306
+ const takes = takesMessages(s);
307
+ if (takes && queued === 0) {
308
+ const event = await beginMessageTurn(t, s, id, [message]);
309
+ return { status: "accepted", turnId: s.activeTurnId, cursor: event.cursor };
310
+ }
311
+ // The turn runs or waits, or earlier messages still wait: this one waits behind them.
312
+ const { sessionQueuedMessages } = await readRuntimeLimits(t);
313
+ if (queued >= sessionQueuedMessages)
314
+ fail(429, `Session ${id} already holds ${queued} queued messages, the Runtime limit sessionQueuedMessages (PUT /v1/tenant/limits); they start a turn when the running one ends.`, { code: "queue_full" });
315
+ const queuedMessageId = randomUUID();
316
+ await t.enqueueMessage({
317
+ id: queuedMessageId,
318
+ sessionId: id,
319
+ command: message.command,
320
+ input: message.input,
321
+ fileArtifacts: message.fileArtifacts,
322
+ writer: message.writer,
323
+ trace: message.trace ?? null,
324
+ cause: message.cause ?? null,
325
+ createdAt: new Date().toISOString(),
326
+ });
327
+ const position = queued + 1;
328
+ const event = await t.event(id, s.activeTurnId, "message.queued", { queuedMessageId, position, requestId: message.command.requestId }, message.writer);
329
+ const started = takes ? await startQueuedTurn(ctx, t, s, id) : undefined;
330
+ if (started?.taken.includes(queuedMessageId))
331
+ return { status: "accepted", turnId: started.turnId, cursor: started.cursor };
332
+ return { status: "queued", queuedMessageId, position, cursor: event.cursor };
333
+ }
334
+ /**
335
+ * Resolves a message for a turn, now or when it leaves the queue: file parts name artifacts the
336
+ * caller may read, each pinned to a version (any file goes since protocol 15: the harness decides
337
+ * how the model sees it, and a workflow passes it on as data), and a turn manifest must be a
338
+ * variant of the pinned agent.
339
+ */
340
+ async function turnMessage(t, s, id, command, scope, trace) {
341
+ const parts = "parts" in command ? await resolveMessageParts(t, command.parts, id, accessOf(scope)) : undefined;
342
+ const fileArtifacts = (parts?.pinned ?? []).flatMap((part) => (part.type === "file" ? [part.artifactId] : []));
343
+ const data = "data" in command ? command.data : undefined;
344
+ let input;
345
+ if (isWorkflowManifest(s.manifest))
346
+ input =
347
+ "content" in command
348
+ ? command.content
349
+ : parts
350
+ ? { parts: parts.pinned }
351
+ : data;
352
+ else {
353
+ const resolved = resolveMessageManifest({
354
+ pinned: s.manifest,
355
+ pinnedHash: s.manifestHash,
356
+ messageManifest: command.manifest,
357
+ store: variantStore(s),
358
+ });
359
+ if (!resolved.ok)
360
+ fail(400, resolved.message);
361
+ input =
362
+ "content" in command
363
+ ? command.content
364
+ : parts
365
+ ? { content: parts.engine }
366
+ : typeof data === "string"
367
+ ? data
368
+ : JSON.stringify(data);
369
+ }
370
+ return { command, input, fileArtifacts, writer: commandWriter(scope), ...(trace ? { trace } : {}) };
371
+ }
372
+ /**
373
+ * Starts a turn on session `s` (locked, taking messages) with `messages`, in order: the one door
374
+ * for a client's message and queued ones (protocol 17). Our engine reads more than one as
375
+ * `{kind: "messages", messages}`, each its own user message; a workflow takes one. The checkpoint
376
+ * is made before the session changes, so a manifest that no longer resolves changes nothing.
377
+ * Writes each message's `command.message` and returns the last.
378
+ */
379
+ async function beginMessageTurn(t, s, id, messages) {
380
+ const turnId = randomUUID();
381
+ const first = messages[0];
382
+ let checkpoint;
383
+ if (isWorkflowManifest(s.manifest))
384
+ checkpoint = createFlowCheckpoint({
385
+ manifest: s.manifest,
386
+ sessionId: id,
387
+ turnId,
388
+ input: first.input,
389
+ });
390
+ else {
391
+ const resolved = resolveMessageManifest({
392
+ pinned: s.manifest,
393
+ pinnedHash: s.manifestHash,
394
+ messageManifest: first.command.manifest,
395
+ store: variantStore(s),
396
+ });
397
+ if (!resolved.ok)
398
+ return fail(400, resolved.message);
399
+ checkpoint = createDurableCheckpoint({
400
+ manifest: resolved.manifest,
401
+ sessionId: id,
402
+ turnId,
403
+ input: (messages.length === 1
404
+ ? first.input
405
+ : { kind: "messages", messages: messages.map((message) => message.input) }),
406
+ state: s.state,
407
+ info: s.info,
408
+ });
409
+ }
410
+ s.turnStartState = s.state;
411
+ // A new turn folds from the latest snapshot: a cancel or failure only reverts this turn.
412
+ if (s.history?.snapshot !== undefined)
413
+ s.history = { ...s.history, from: s.history.snapshot };
414
+ s.activeTurnId = turnId;
415
+ startTurn(s);
416
+ if (first.trace)
417
+ s.turnTrace = first.trace;
418
+ else
419
+ delete s.turnTrace;
420
+ // A trigger's run or a booked wake among the messages started it (track T4).
421
+ const cause = messages.find((message) => message.cause)?.cause;
422
+ if (cause)
423
+ s.turnCause = { kind: cause.kind, triggerId: cause.triggerId, runId: cause.runId };
424
+ else
425
+ delete s.turnCause;
426
+ // The files the session's messages named: `read_artifact` reads them again (protocol 12).
427
+ const named = messages.flatMap((message) => message.fileArtifacts);
428
+ if (named.length > 0)
429
+ s.fileArtifacts = [...new Set([...(s.fileArtifacts ?? []), ...named])];
430
+ s.checkpoint = checkpoint;
431
+ s.status = "runnable";
432
+ s.waits = undefined;
433
+ await t.wake(id, { reason: "message", dedupeKey: `message:${turnId}:${checkpoint.segment}` });
434
+ let event;
435
+ for (const message of messages)
436
+ event = await t.event(id, turnId, "command.message", {
437
+ ...message.command,
438
+ ...(message.queuedMessageId === undefined ? {} : { queuedMessageId: message.queuedMessageId }),
439
+ ...(message.trace ? { trace: message.trace } : {}),
440
+ ...(message.cause ? { cause: message.cause } : {}),
441
+ }, message.writer);
442
+ return event;
443
+ }
444
+ /** Who checks a queued message's turn start: core, since the sender's grants were checked when it was queued. */
445
+ export const QUEUE_SCOPE = { kind: "application", principalId: "queue" };
446
+ /**
447
+ * Starts the next turn of session `s` (locked) from its queue, when it takes messages and messages
448
+ * wait (protocol 17, D76): in the transaction that ended the last turn, or a cancel's. Our engine
449
+ * takes every queued message that carries the same turn manifest as the first, a workflow one.
450
+ * Writes `queue.taken`. Returns nothing when nothing started: an empty queue, or an Agent that
451
+ * cannot take a turn now, whose messages wait for the next message or turn end.
452
+ */
453
+ export async function startQueuedTurn(ctx, t, s, id) {
454
+ if (s.closedReason !== undefined || !takesMessages(s) || s.activeTurnId !== null)
455
+ return undefined;
456
+ const queued = await t.queuedMessages(id);
457
+ if (queued.length === 0)
458
+ return undefined;
459
+ const manifestOf = (row) => canonical(row.command.manifest ?? null);
460
+ const batch = isWorkflowManifest(s.manifest) ? [queued[0]] : [];
461
+ if (batch.length === 0)
462
+ for (const row of queued) {
463
+ if (manifestOf(row) !== manifestOf(queued[0]))
464
+ break;
465
+ batch.push(row);
466
+ }
467
+ try {
468
+ await checkAgentTurn(t, s, QUEUE_SCOPE, ctx);
469
+ await beginMessageTurn(t, s, id, batch.map((row) => ({
470
+ command: row.command,
471
+ input: row.input,
472
+ fileArtifacts: row.fileArtifacts,
473
+ writer: row.writer,
474
+ queuedMessageId: row.id,
475
+ ...(row.trace ? { trace: row.trace } : {}),
476
+ ...(row.cause ? { cause: row.cause } : {}),
477
+ })));
478
+ }
479
+ catch (error) {
480
+ if (error instanceof HttpError)
481
+ return undefined;
482
+ throw error;
483
+ }
484
+ const taken = batch.map((row) => row.id);
485
+ await t.deleteQueuedMessages(taken);
486
+ const event = await t.event(id, s.activeTurnId, "queue.taken", {
487
+ turnId: s.activeTurnId,
488
+ queuedMessageIds: taken,
489
+ });
490
+ return { turnId: s.activeTurnId, taken, cursor: event.cursor };
491
+ }
335
492
  /** Core's own commands (R4.9): the flow host's, and a drain's cancels. */
336
493
  const CORE_PRINCIPALS = { "flow-host": "flow-host", drain: "core" };
337
494
  /**
@@ -40,19 +40,28 @@ import type { TraceOptions } from "../traces/options.js";
40
40
  import type { TraceContext } from "../traces/ids.js";
41
41
  export interface Session {
42
42
  id: string;
43
+ /** The Agent the session runs on (protocol 16; a definition id before). */
43
44
  agentId: string;
44
45
  ownerUserId: string;
45
- /** Session pin — the manifest the session was created with. */
46
+ /**
47
+ * Session pin: the loop manifest the session runs, its Agent's manifest version's harness
48
+ * section as the harness runs it (`loopManifestOf`), or a flow's leaf agent.
49
+ */
46
50
  manifest: any;
47
51
  manifestHash: string;
48
52
  /**
49
- * The hash of the definition the session was opened from (R2b C11); since protocol 15 it equals
50
- * `manifestHash`. Absent on older sessions.
53
+ * The agent manifest version the session runs (protocol 16): its Agent's when it opened, and
54
+ * again at a turn's first segment when its Agent's version changed since (T3, `repinTurn`).
55
+ * Absent on a session opened before protocol 16.
51
56
  */
52
- definitionHash?: string;
57
+ definition?: {
58
+ name: string;
59
+ version: number;
60
+ };
61
+ /** The pinned version's `harness.loop`: passed to the harness, never read by the Runtime. */
62
+ loop?: Record<string, unknown>;
53
63
  /** Validated turn-manifest variants, keyed by hash (loops.md §4.2). */
54
64
  variants?: Record<string, AgentManifest>;
55
- implementationVersion: string;
56
65
  info?: any;
57
66
  status: string;
58
67
  activeTurnId: string | null;
@@ -83,26 +92,35 @@ export interface Session {
83
92
  * and the context core hands on (to the harness, tools and sandbox commands) is in it.
84
93
  */
85
94
  turnTrace?: TraceContext;
95
+ /**
96
+ * What started the active turn when not a client (track T4): the trigger's run, or the session's
97
+ * own booked wake. A memory version written in the turn names its run (track T5).
98
+ */
99
+ turnCause?: {
100
+ kind: "trigger" | "self";
101
+ triggerId: string;
102
+ runId: string;
103
+ };
86
104
  creation: unknown;
87
105
  mcpSnapshot?: McpSnapshot;
88
106
  mcpDiagnostics?: readonly McpDiagnostic[];
89
107
  /**
90
- * The Environment the session names (`environment`, protocol 11): its sandbox is the
91
- * session's (the Environment's id keys the workspace) and its vault holds the session's
108
+ * The Agent's environment the session runs on: since protocol 16 always its Agent's id. Its
109
+ * sandbox is the session's (the id keys the workspace) and its vault holds the session's
92
110
  * credentials. It outlives the session.
93
111
  */
94
112
  environmentId?: string;
95
- /** The Environment's recipe version when the session opened (shown, never enforced). */
113
+ /** The Agent's recipe version when the session last pinned (shown, never enforced). */
96
114
  environmentVersion?: number;
97
115
  /**
98
- * The harness that runs the session's turns (R4.1), pinned when it opened: its Environment's
99
- * harness, else `nylorun`. Absent on a session opened before R4: ours (`harnessOf`).
116
+ * The harness that runs the session's turns (R4.1), pinned when it opened: its manifest's
117
+ * `harness.id`, else `nylorun`. Absent on a session opened before R4: ours (`harnessOf`).
100
118
  */
101
119
  harness?: string;
102
120
  /**
103
121
  * A flow agent's linked session: the session whose turn opened the flow tree. Its sessions
104
122
  * share the tree's Environment, and their turns nest in the root's, so they do not hold the
105
- * Environment against each other (`checkEnvironmentTurn`).
123
+ * Environment against each other (`checkAgentTurn`).
106
124
  */
107
125
  flowRootId?: string;
108
126
  /**
@@ -114,7 +132,8 @@ export interface Session {
114
132
  * Why the session takes no new turn: `protocol_11` for a session opened before protocol 11
115
133
  * (migration 0020), `protocol_13` for one opened before protocol 13 (migration 0024),
116
134
  * `protocol_15` for an agent session that pinned a `runtime` block before protocol 15
117
- * (migrations 0029, 0030). Its history stays readable.
135
+ * (migrations 0029, 0030), `protocol_16` for one opened from a definition before protocol 16
136
+ * (migration 0032). Its history stays readable.
118
137
  */
119
138
  closedReason?: string;
120
139
  /**
@@ -138,7 +157,7 @@ export type AuthScope = {
138
157
  }
139
158
  /**
140
159
  * A trusted issuer's token (Host feature `trusted-issuers`, F9-D12): one subject, with the
141
- * issuer's scopes, agents and Environment grants, until `expiresAt` (ms). Only its expiry ends it.
160
+ * issuer's scopes and Agent grants, until `expiresAt` (ms). Only its expiry ends it.
142
161
  */
143
162
  | {
144
163
  kind: "token";
@@ -147,9 +166,8 @@ export type AuthScope = {
147
166
  subject: string;
148
167
  /** The issuer's scopes the token holds, which may add `studio`. */
149
168
  scopes: ReadonlySet<IssuerScope>;
150
- agents: ReadonlySet<string> | "*";
151
- /** The Environments it reaches: exact ids or `p/*` prefixes. Absent or empty reaches none. */
152
- environments?: readonly string[];
169
+ /** The Agents it reaches: `*`, else exact ids or `p/*` prefixes; empty reaches none. */
170
+ agents: readonly string[] | "*";
153
171
  expiresAt: number;
154
172
  /** The token's `jti`, or a hash of it. */
155
173
  tokenId: string;
@@ -264,7 +282,7 @@ export interface TenantContext {
264
282
  abortLocal(sessionId: string, turnId?: string): void;
265
283
  /**
266
284
  * Sandbox pods (F7.2): the sandboxes service and the Runtime image pods copy the engine from.
267
- * Absent without a cluster (`nylorun local sandbox enable`): kind `pod` is `environment_unavailable`.
285
+ * Absent without a cluster (`nylorun local sandbox enable`): kind `pod` is `agent_unavailable`.
268
286
  */
269
287
  readonly pods?: TenantPods;
270
288
  /**
@@ -274,6 +292,12 @@ export interface TenantContext {
274
292
  * sent); `true` without sandbox pods, where there is nothing to send.
275
293
  */
276
294
  sandboxSignal(sandboxId: string, signal: SandboxSignal): Promise<boolean>;
295
+ /**
296
+ * Seam: arm a durable timer (`DurableExecution.timer`; track T4's triggers), outside any tx. The
297
+ * latest time for a key wins. `false` while the Tenant is closing (not armed): what it was for
298
+ * is in Postgres, and the sweep arms it again.
299
+ */
300
+ timer(key: string, at: Date): Promise<boolean>;
277
301
  /** This Tenant's execution invocations that need an operator, for Tenant status. */
278
302
  readonly stuckInvocations?: () => Promise<StuckInvocation[]>;
279
303
  /** Callbacks the Tenant sweep runs after its own steps (`sweep.ts`). */
@@ -283,12 +307,15 @@ export interface TenantContext {
283
307
  }
284
308
  /**
285
309
  * What a request acting for a person may reach: that person's sessions, and, for a token
286
- * caller, only sessions of the agents its issuer allows. Undefined means the whole Tenant.
310
+ * caller, only sessions on the Agents its grants reach (ids and `p/*` prefixes). Undefined means
311
+ * the whole Tenant.
287
312
  */
288
313
  export interface SessionAccess {
289
314
  readonly owner: string;
290
- readonly agents?: ReadonlySet<string>;
315
+ readonly agents?: readonly string[];
291
316
  }
317
+ /** Whether `access` reaches sessions on Agent `agentId`. */
318
+ export declare function reachesAgent(access: SessionAccess | undefined, agentId: string): boolean;
292
319
  /** The session, or a 404. Reads without locking. */
293
320
  export declare function sessionOf(t: Tx, id: string, access?: SessionAccess): Promise<Session>;
294
321
  /** Locks the session for the rest of the transaction and returns it, or a 404. */