@nylorun/runtime 0.20.0-beta → 0.21.1-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 (109) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +19 -12
  3. package/dist/adapters/execution/restate.js +26 -1
  4. package/dist/api/components.d.ts +155 -36
  5. package/dist/api/components.js +3 -3
  6. package/dist/api/http/app.js +2 -0
  7. package/dist/api/http/routes/mcp.d.ts +9 -0
  8. package/dist/api/http/routes/mcp.js +32 -0
  9. package/dist/api/http/routes/vaults.d.ts +1 -9
  10. package/dist/api/http/routes/vaults.js +3 -103
  11. package/dist/api/openapi.js +6 -3
  12. package/dist/artifacts/media-types.d.ts +2 -0
  13. package/dist/artifacts/media-types.js +5 -0
  14. package/dist/core/flow-host.d.ts +2 -3
  15. package/dist/core/flow-host.js +10 -7
  16. package/dist/execution/types.d.ts +13 -5
  17. package/dist/execution/types.js +13 -5
  18. package/dist/gates/http-tool.d.ts +7 -0
  19. package/dist/gates/http-tool.js +34 -3
  20. package/dist/gates/mcp-handler.d.ts +4 -1
  21. package/dist/gates/mcp-handler.js +64 -8
  22. package/dist/gates/tenant-vaults.d.ts +9 -15
  23. package/dist/gates/tenant-vaults.js +9 -17
  24. package/dist/gates/tool-client.js +36 -17
  25. package/dist/gates/tool-contract.d.ts +19 -3
  26. package/dist/gates/tool-contract.js +9 -3
  27. package/dist/gates/tool-gate.d.ts +2 -2
  28. package/dist/harness/calls.d.ts +2 -0
  29. package/dist/harness/calls.js +13 -3
  30. package/dist/harness-api/record.d.ts +7 -6
  31. package/dist/harness-api/record.js +99 -17
  32. package/dist/harness-api/server.js +35 -27
  33. package/dist/harness-api/start.js +2 -1
  34. package/dist/host/config-for.d.ts +0 -2
  35. package/dist/host/config-for.js +0 -1
  36. package/dist/host/create-host.d.ts +1 -1
  37. package/dist/host/create-host.js +2 -1
  38. package/dist/host/gates.js +0 -1
  39. package/dist/host/http.d.ts +0 -1
  40. package/dist/host/http.js +0 -6
  41. package/dist/host/main.js +13 -3
  42. package/dist/host/stack-config.d.ts +7 -8
  43. package/dist/host/stack-config.js +50 -92
  44. package/dist/index.d.ts +0 -1
  45. package/dist/keys/client.js +1 -9
  46. package/dist/keys/keys.d.ts +11 -13
  47. package/dist/keys/keys.js +5 -7
  48. package/dist/management-openapi.json +315 -345
  49. package/dist/mcp/connect.d.ts +159 -13
  50. package/dist/mcp/connect.js +538 -55
  51. package/dist/mcp/pool.d.ts +11 -2
  52. package/dist/mcp/pool.js +74 -27
  53. package/dist/mcp/preview.d.ts +35 -0
  54. package/dist/mcp/preview.js +215 -0
  55. package/dist/mcp/snapshot.d.ts +79 -5
  56. package/dist/mcp/snapshot.js +145 -27
  57. package/dist/model/catalog.d.ts +14 -0
  58. package/dist/model/catalog.js +14 -1
  59. package/dist/model/pi-model.js +36 -2
  60. package/dist/openapi.json +362 -123
  61. package/dist/redact.d.ts +19 -1
  62. package/dist/redact.js +42 -13
  63. package/dist/sandbox/session-sandbox.d.ts +7 -0
  64. package/dist/sandbox/session-sandbox.js +89 -28
  65. package/dist/store/postgres/drizzle/0016_mcp_oauth_removed.sql +5 -0
  66. package/dist/store/postgres/drizzle/0017_wake_outbox.sql +9 -0
  67. package/dist/store/postgres/drizzle/meta/_journal.json +14 -0
  68. package/dist/store/postgres/schema.d.ts +105 -261
  69. package/dist/store/postgres/schema.js +18 -36
  70. package/dist/store/postgres/store.js +54 -21
  71. package/dist/store/postgres/tenant.d.ts +14 -0
  72. package/dist/store/postgres/tenant.js +22 -2
  73. package/dist/store/types.d.ts +48 -13
  74. package/dist/tenant/advance.js +1 -4
  75. package/dist/tenant/artifact-tool.d.ts +9 -1
  76. package/dist/tenant/artifact-tool.js +113 -9
  77. package/dist/tenant/commands.js +5 -5
  78. package/dist/tenant/context.d.ts +15 -13
  79. package/dist/tenant/effects.d.ts +2 -2
  80. package/dist/tenant/effects.js +20 -5
  81. package/dist/tenant/ephemeral.d.ts +0 -6
  82. package/dist/tenant/ephemeral.js +0 -1
  83. package/dist/tenant/outbound.d.ts +14 -5
  84. package/dist/tenant/outbound.js +21 -9
  85. package/dist/tenant/runtime.js +11 -15
  86. package/dist/tenant/sessions.js +20 -3
  87. package/dist/tenant/store-pg.js +9 -0
  88. package/dist/tenant/sweep.d.ts +14 -0
  89. package/dist/tenant/sweep.js +69 -10
  90. package/dist/tenant/tool-results.d.ts +57 -0
  91. package/dist/tenant/tool-results.js +298 -0
  92. package/dist/tenant/tool-search.d.ts +43 -0
  93. package/dist/tenant/tool-search.js +100 -0
  94. package/dist/tenant/transcript.d.ts +108 -0
  95. package/dist/tenant/transcript.js +13 -0
  96. package/dist/tenant/types.d.ts +0 -14
  97. package/dist/vault/crypto.d.ts +0 -7
  98. package/dist/vault/crypto.js +0 -16
  99. package/dist/vault/headers.d.ts +17 -0
  100. package/dist/vault/headers.js +61 -0
  101. package/dist/vault/service.d.ts +11 -68
  102. package/dist/vault/service.js +83 -438
  103. package/dist/vault/sources.d.ts +20 -48
  104. package/dist/vault/sources.js +25 -189
  105. package/dist/version.d.ts +1 -1
  106. package/dist/version.js +1 -1
  107. package/package.json +3 -3
  108. package/dist/vault/oauth.d.ts +0 -55
  109. package/dist/vault/oauth.js +0 -186
@@ -1,10 +1,15 @@
1
- import { delegateManifest } from "@nylorun/core/define";
1
+ import { createHash } from "node:crypto";
2
+ import { TOOLS_CAPABILITY_ID, deferredToolsInstructions, deferredToolsTools, delegateManifest, mcpToolSettings, } from "@nylorun/core/define";
2
3
  /** The `mcp.discovered` payload: each server's outcome, with the tools it added to the snapshot. */
3
4
  export function mcpDiscovered(snapshot, diagnostics) {
4
5
  const key = (item) => JSON.stringify([item.agentId ?? null, item.capabilityId, item.serverName]);
5
6
  const tools = new Map();
6
- for (const tool of snapshot.mcpTools)
7
+ const deferred = new Map();
8
+ for (const tool of snapshot.mcpTools) {
7
9
  tools.set(key(tool), (tools.get(key(tool)) ?? 0) + 1);
10
+ if (tool.deferred)
11
+ deferred.set(key(tool), (deferred.get(key(tool)) ?? 0) + 1);
12
+ }
8
13
  return {
9
14
  servers: diagnostics.map((item) => ({
10
15
  ...(item.agentId === undefined ? {} : { agentId: item.agentId }),
@@ -13,12 +18,32 @@ export function mcpDiscovered(snapshot, diagnostics) {
13
18
  outcome: item.outcome,
14
19
  message: item.message,
15
20
  tools: tools.get(key(item)) ?? 0,
21
+ ...(deferred.has(key(item)) ? { deferred: deferred.get(key(item)) } : {}),
22
+ ...(item.disabled ? { disabled: item.disabled } : {}),
23
+ ...(item.unknownTools?.length ? { unknownTools: [...item.unknownTools] } : {}),
16
24
  ...(item.credentialIds === undefined ? {} : { credentialIds: [...item.credentialIds] }),
25
+ ...(item.renamed?.length ? { renamed: item.renamed.map((tool) => ({ ...tool })) } : {}),
17
26
  })),
18
27
  };
19
28
  }
20
- export function modelToolName(serverName, serverToolName) {
21
- return `${serverName}__${serverToolName}`;
29
+ /** The longest tool name every model provider accepts (R2b C6, Q14). */
30
+ export const MODEL_TOOL_NAME_MAX = 64;
31
+ /**
32
+ * The name the model knows an MCP tool by (R2b C6, Q13): `server__tool`, with each character
33
+ * outside `[A-Za-z0-9_-]` replaced by `_`, since MCP allows `.` and 128 characters and OpenAI,
34
+ * Bedrock and Gemini on Vertex do not. A name over 64 characters, or one that collides
35
+ * (`suffixed`), keeps 55 and adds `_` and 8 hex characters of the SHA-256 of the raw
36
+ * `server/tool`.
37
+ */
38
+ export function normalizeToolName(serverName, serverToolName, options = {}) {
39
+ const name = `${serverName}__${serverToolName}`.replace(/[^A-Za-z0-9_-]/gu, "_");
40
+ if (name.length <= MODEL_TOOL_NAME_MAX && !options.suffixed)
41
+ return name;
42
+ const hash = createHash("sha256")
43
+ .update(`${serverName}/${serverToolName}`)
44
+ .digest("hex")
45
+ .slice(0, 8);
46
+ return `${name.slice(0, MODEL_TOOL_NAME_MAX - 9)}_${hash}`;
22
47
  }
23
48
  export function declaredToolNames(manifest) {
24
49
  const names = new Set();
@@ -27,37 +52,130 @@ export function declaredToolNames(manifest) {
27
52
  names.add(tool.name);
28
53
  return names;
29
54
  }
55
+ /** The declaration of the server a tool of the snapshot comes from, in `manifest`. */
56
+ function serverOf(manifest, tool) {
57
+ const agent = tool.agentId === undefined ? manifest : delegateManifest(manifest, tool.agentId);
58
+ return agent?.capabilities.find((item) => item.id === tool.capabilityId)?.mcpServers?.[tool.serverName];
59
+ }
60
+ /** The tool's settings in `manifest` (R2b C9); the defaults when its server is not there. */
61
+ function settingsOf(manifest, tool) {
62
+ const server = serverOf(manifest, tool);
63
+ return server ? mcpToolSettings(server, tool.serverToolName) : { enabled: true, approval: "never" };
64
+ }
65
+ /** True when the agent the tool belongs to has `tool_search` and `tool_call` (`nylorun.tools`). */
66
+ function reachesDeferred(manifest, agentId) {
67
+ const agent = agentId === undefined ? manifest : delegateManifest(manifest, agentId);
68
+ return agent?.capabilities.some((capability) => capability.id === TOOLS_CAPABILITY_ID) === true;
69
+ }
30
70
  /**
31
- * The tools of a snapshot, as the engine advertises them. A tool of a server whose `approval` is
32
- * `always` in the session's `manifest` waits for approval on each call.
71
+ * The tools of a snapshot, as the engine gets them for a turn whose manifest is `manifest` (the
72
+ * session's pin, or a turn variant that tightened its MCP servers' tools):
73
+ *
74
+ * - a tool its settings disable is left out (R2b C9); one whose approval resolves to `always`
75
+ * waits for approval on each call;
76
+ * - a deferred tool (R2b C10) is marked so, out of the model's list, and its agent gets
77
+ * `tool_search`, with the note on the servers it holds deferred tools of, and `tool_call` in
78
+ * its `nylorun.tools` capability. An agent whose turn has no such capability keeps its tools in
79
+ * its list.
80
+ *
81
+ * Nothing here changes between the steps of a turn, so neither does the model's tool list.
33
82
  */
34
83
  export function sessionToolsOf(snapshot, manifest) {
35
84
  if (!snapshot?.mcpTools.length)
36
85
  return undefined;
37
- const approved = (tool) => {
38
- const agent = tool.agentId === undefined ? manifest : delegateManifest(manifest, tool.agentId);
39
- const server = agent?.capabilities.find((item) => item.id === tool.capabilityId)?.mcpServers?.[tool.serverName];
40
- return server !== undefined && "approval" in server && server.approval === "always";
41
- };
42
- return snapshot.mcpTools.map((tool) => ({
43
- ...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
44
- capabilityId: tool.capabilityId,
45
- name: tool.name,
46
- ...(tool.description === undefined
47
- ? {}
48
- : { description: tool.description }),
49
- inputSchema: tool.inputSchema,
50
- ...(tool.outputSchema === undefined
51
- ? {}
52
- : { outputSchema: tool.outputSchema }),
53
- ...(approved(tool) ? { approval: "always" } : {}),
54
- }));
86
+ const inline = [];
87
+ const deferred = new Map();
88
+ const notes = new Map();
89
+ for (const tool of snapshot.mcpTools) {
90
+ const settings = settingsOf(manifest, tool);
91
+ if (!settings.enabled)
92
+ continue;
93
+ const defer = tool.deferred === true && reachesDeferred(manifest, tool.agentId);
94
+ const entry = {
95
+ ...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
96
+ capabilityId: tool.capabilityId,
97
+ name: tool.name,
98
+ ...(tool.description === undefined ? {} : { description: tool.description }),
99
+ inputSchema: tool.inputSchema,
100
+ ...(tool.outputSchema === undefined ? {} : { outputSchema: tool.outputSchema }),
101
+ ...(settings.approval === "always" ? { approval: "always" } : {}),
102
+ ...(defer ? { deferred: true } : {}),
103
+ };
104
+ if (!defer) {
105
+ inline.push(entry);
106
+ continue;
107
+ }
108
+ deferred.set(tool.agentId, [...(deferred.get(tool.agentId) ?? []), entry]);
109
+ const servers = notes.get(tool.agentId) ?? new Map();
110
+ servers.set(tool.serverName, (servers.get(tool.serverName) ?? 0) + 1);
111
+ notes.set(tool.agentId, servers);
112
+ }
113
+ const platform = [];
114
+ for (const [agentId, tools] of deferred) {
115
+ const servers = [...notes.get(agentId)].map(([serverName, count]) => {
116
+ const instructions = snapshot.servers?.find((item) => item.agentId === agentId && item.serverName === serverName)?.instructions;
117
+ return { serverName, tools: count, ...(instructions ? { instructions } : {}) };
118
+ });
119
+ const [search, call] = deferredToolsTools();
120
+ for (const tool of [
121
+ { ...search, instructions: [deferredToolsInstructions(servers)] },
122
+ call,
123
+ ])
124
+ platform.push({
125
+ ...(agentId === undefined ? {} : { agentId }),
126
+ capabilityId: TOOLS_CAPABILITY_ID,
127
+ ...tool,
128
+ });
129
+ platform.push(...tools);
130
+ }
131
+ return [...inline, ...platform];
55
132
  }
56
- /** The snapshot's record of an MCP tool call, or undefined when the call is not an MCP tool. */
57
- export function mcpToolOf(snapshot, request) {
58
- return snapshot?.mcpTools.find((tool) => tool.agentId === request.agent?.id &&
133
+ /** The share of the model's context window past which an agent's MCP tools are deferred (Q21). */
134
+ export const DEFER_CONTEXT_SHARE = 0.1;
135
+ /** Characters per token, to estimate what tool definitions take of a context window (Q21). */
136
+ export const CHARS_PER_TOKEN = 4;
137
+ /**
138
+ * The snapshot with its deferred tools marked (R2b C10, Q21), decided once for the session when
139
+ * core records it. A tool's own setting, then `"*"`'s, then its server's, decides; a tool none
140
+ * sets is deferred when the JSON of its agent's MCP tools that would be listed passes a tenth of
141
+ * the model's context window (`contextWindow` tokens, at 4 characters a token). An agent without
142
+ * the `nylorun.tools` capability (its definition names a tool `tool_search` or `tool_call`, or the
143
+ * session was opened before it existed) defers nothing.
144
+ */
145
+ export function pinDeferral(snapshot, manifest, contextWindow) {
146
+ const limit = contextWindow * CHARS_PER_TOKEN * DEFER_CONTEXT_SHARE;
147
+ const listed = new Map();
148
+ const set = snapshot.mcpTools.map((tool) => settingsOf(manifest, tool).deferred);
149
+ snapshot.mcpTools.forEach((tool, i) => {
150
+ if (set[i] === true)
151
+ return;
152
+ const size = JSON.stringify({ name: tool.name, description: tool.description ?? "", inputSchema: tool.inputSchema }).length;
153
+ listed.set(tool.agentId, (listed.get(tool.agentId) ?? 0) + size);
154
+ });
155
+ const mcpTools = snapshot.mcpTools.map((tool, i) => {
156
+ const { deferred: _, ...rest } = tool;
157
+ if (!reachesDeferred(manifest, tool.agentId))
158
+ return rest;
159
+ const defer = set[i] ?? (listed.get(tool.agentId) ?? 0) > limit;
160
+ return defer ? { ...rest, deferred: true } : rest;
161
+ });
162
+ return { ...snapshot, mcpTools };
163
+ }
164
+ /**
165
+ * The snapshot's record of an MCP tool call, or undefined when the call is not an MCP tool. With
166
+ * the turn's `manifest`, a tool its settings disable is not one either (R2b C9).
167
+ */
168
+ export function mcpToolOf(snapshot, request, manifest) {
169
+ const tool = snapshot?.mcpTools.find((tool) => tool.agentId === request.agent?.id &&
59
170
  tool.capabilityId === request.capabilityId &&
60
171
  tool.name === request.toolName);
172
+ if (tool && manifest && !isEnabledIn(manifest, tool))
173
+ return undefined;
174
+ return tool;
175
+ }
176
+ /** True when the tool's settings in `manifest` leave it enabled (R2b C9). */
177
+ export function isEnabledIn(manifest, tool) {
178
+ return settingsOf(manifest, tool).enabled;
61
179
  }
62
180
  /** The manifest of the agent an effect belongs to: the root, or an agent it uses as a tool. */
63
181
  export function manifestFor(manifest, agent) {
@@ -1,4 +1,18 @@
1
1
  import type { HostModelCatalog } from "@nylorun/core/contracts";
2
2
  export type { HostModelCatalog };
3
+ /**
4
+ * The context window, in tokens, of the Tenant's model (`GET /v1/tenant/model`'s view): the
5
+ * custom endpoint's setting, else the provider catalog's. A model the catalog does not know, or
6
+ * none configured, is assumed small (`CUSTOM_CONTEXT_WINDOW`), as for a custom endpoint.
7
+ */
8
+ export declare function contextWindowOf(view: {
9
+ configured: boolean;
10
+ provider?: string;
11
+ model?: string;
12
+ baseUrl?: string;
13
+ settings?: {
14
+ contextWindow?: number;
15
+ };
16
+ }): number;
3
17
  /** Public provider and model names. No credentials. */
4
18
  export declare function hostModelCatalog(): HostModelCatalog;
@@ -1,4 +1,4 @@
1
- import { modelsFor } from "./models.js";
1
+ import { CUSTOM_CONTEXT_WINDOW, modelsFor } from "./models.js";
2
2
  const emptyStore = {
3
3
  async read() {
4
4
  return undefined;
@@ -11,6 +11,19 @@ const emptyStore = {
11
11
  },
12
12
  async delete() { },
13
13
  };
14
+ /**
15
+ * The context window, in tokens, of the Tenant's model (`GET /v1/tenant/model`'s view): the
16
+ * custom endpoint's setting, else the provider catalog's. A model the catalog does not know, or
17
+ * none configured, is assumed small (`CUSTOM_CONTEXT_WINDOW`), as for a custom endpoint.
18
+ */
19
+ export function contextWindowOf(view) {
20
+ if (!view.configured || !view.provider || !view.model)
21
+ return CUSTOM_CONTEXT_WINDOW;
22
+ if (view.baseUrl)
23
+ return view.settings?.contextWindow ?? CUSTOM_CONTEXT_WINDOW;
24
+ const models = modelsFor({ provider: "", model: "" }, emptyStore, { environment: false });
25
+ return models.getModel(view.provider, view.model)?.contextWindow ?? CUSTOM_CONTEXT_WINDOW;
26
+ }
14
27
  /** Public provider and model names. No credentials. */
15
28
  export function hostModelCatalog() {
16
29
  const models = modelsFor({ provider: "", model: "" }, emptyStore, {
@@ -226,11 +226,45 @@ export function piModel(options = {}) {
226
226
  async function buildContext(call, selected, options) {
227
227
  const messages = [];
228
228
  const instructions = [];
229
- const content = async (parts) => {
229
+ const content = async (parts, toolResult = false) => {
230
230
  const result = [];
231
231
  for (const part of parts) {
232
232
  if (part.type === "text")
233
233
  result.push({ type: "text", text: part.text });
234
+ else if (part.type === "media" && toolResult) {
235
+ // An image a tool returned, stored as an artifact (R2b C11): shown to a model that
236
+ // reads images. Otherwise, or when it cannot be read, a note: the call still goes.
237
+ const note = (why) => ({
238
+ type: "text",
239
+ text: `(The tool returned an image, ${essence(part.mediaType)}, that is not shown: ${why}.)`,
240
+ });
241
+ if (!selected.input.includes("image") || !isModelImage(part.mediaType)) {
242
+ result.push(note(selected.input.includes("image")
243
+ ? "the model reads PNG, JPEG, GIF and WebP images only"
244
+ : "this model does not accept images"));
245
+ continue;
246
+ }
247
+ if (!options.files) {
248
+ result.push(note("files are unavailable"));
249
+ continue;
250
+ }
251
+ try {
252
+ const file = await options.files(part.reference);
253
+ if (!isModelImage(file.mediaType))
254
+ result.push(note(`it is ${essence(file.mediaType)}`));
255
+ else
256
+ result.push({
257
+ type: "image",
258
+ data: Buffer.from(file.bytes).toString("base64"),
259
+ mimeType: essence(file.mediaType),
260
+ });
261
+ }
262
+ catch (error) {
263
+ if (!(error instanceof FileUnavailableError))
264
+ throw error;
265
+ result.push(note(error.message.replace(/\.$/u, "")));
266
+ }
267
+ }
234
268
  else if (part.type === "media") {
235
269
  // A file the message named by artifact (protocol 6): its bytes go only into the
236
270
  // provider request built here, never into the transcript or the journal.
@@ -284,7 +318,7 @@ async function buildContext(call, selected, options) {
284
318
  toolName: item.toolName,
285
319
  isError: item.status !== "completed",
286
320
  timestamp: Date.now(),
287
- content: await content(item.content),
321
+ content: await content(item.content, true),
288
322
  });
289
323
  }
290
324
  else if (item.kind === "message" && item.role === "assistant") {