@noimosai/platform-mcp 0.1.3 → 0.1.5

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 (93) hide show
  1. package/README.md +17 -48
  2. package/bin/noimos-platform-mcp.mjs +4 -5
  3. package/dist/cli/cli.js +565 -0
  4. package/dist/cli/command-help.js +395 -0
  5. package/dist/cli/download.js +79 -0
  6. package/dist/cli/index.js +3 -0
  7. package/dist/cli/input-error.js +48 -0
  8. package/dist/cli/local-agents.js +199 -0
  9. package/dist/cli/mcp-supervisor.js +136 -0
  10. package/dist/cli/noimos-cli.js +72 -0
  11. package/dist/cli/project-archive.js +27 -0
  12. package/dist/cli/project-files.js +25 -0
  13. package/dist/cli/project-sync.js +265 -0
  14. package/dist/cli/setup-lock.js +1 -0
  15. package/dist/cli/setup.js +347 -0
  16. package/dist/cli/skills.js +142 -0
  17. package/dist/cli/team-harness-targets.js +337 -0
  18. package/dist/cli/team-harness.js +1562 -0
  19. package/dist/cli/team-resources.js +222 -0
  20. package/dist/cli/team-skills.js +457 -0
  21. package/dist/cli/update-bootstrap.js +437 -0
  22. package/dist/cli/update-release.js +212 -0
  23. package/dist/cli/version.js +1 -0
  24. package/dist/cli/wallet.js +85 -0
  25. package/dist/client/agent-run.js +1425 -0
  26. package/dist/client/browser.js +911 -0
  27. package/dist/client/distribution.js +2 -0
  28. package/dist/client/evm-wallet.js +214 -0
  29. package/dist/client/execution-context.js +14 -0
  30. package/dist/client/failure-recovery.js +128 -0
  31. package/dist/client/index.js +4 -225
  32. package/dist/client/local-input.js +33 -12
  33. package/dist/client/local-profile-lock.js +52 -0
  34. package/dist/client/local-profile.js +181 -0
  35. package/dist/client/local-projects.js +131 -0
  36. package/dist/client/local-setup.js +93 -0
  37. package/dist/client/realtime-voice.js +184 -0
  38. package/dist/client/tool-name-hints.js +48 -0
  39. package/dist/client/wallet-access.js +214 -0
  40. package/dist/mcp/agent-server.js +345 -0
  41. package/dist/mcp/context.js +86 -0
  42. package/dist/mcp/host-schema.js +76 -0
  43. package/dist/mcp/icon.js +1 -0
  44. package/dist/mcp/instructions.js +26 -0
  45. package/dist/mcp/local-projects.js +42 -0
  46. package/dist/mcp/local-upload.js +7 -13
  47. package/dist/mcp/noimos-runtime.js +18 -0
  48. package/dist/mcp/noimos-server.js +38 -0
  49. package/dist/mcp/operations.js +41 -0
  50. package/dist/mcp/parallel-result.js +17 -0
  51. package/dist/mcp/project-sync.js +105 -0
  52. package/dist/mcp/results-reply.js +15 -0
  53. package/dist/mcp/runtime.js +65 -11
  54. package/dist/mcp/server.js +382 -40
  55. package/dist/mcp/team-skill.js +63 -0
  56. package/dist/mcp/update-notice.js +75 -0
  57. package/dist/mcp/version.js +1 -0
  58. package/dist/schema/accountRequestMethods.js +1 -0
  59. package/dist/schema/boundedBody.js +30 -0
  60. package/dist/schema/cronValidator.js +150 -0
  61. package/dist/schema/platformApiKey.js +14 -0
  62. package/dist/schema/platformAppAuth.js +11 -0
  63. package/dist/schema/platformBillingConstants.js +4 -0
  64. package/dist/schema/platformCapabilityFamilies.js +293 -0
  65. package/dist/schema/platformConnectionCatalog.js +39 -0
  66. package/dist/schema/platformConnectionPolicy.js +33 -0
  67. package/dist/schema/platformCustomTools.js +148 -0
  68. package/dist/schema/platformEndUserApi.js +62 -0
  69. package/dist/schema/platformFunding.js +10 -0
  70. package/dist/schema/platformHttpConstraints.js +12 -0
  71. package/dist/schema/platformLocalSetup.js +25 -0
  72. package/dist/schema/platformMcpTools.js +7 -0
  73. package/dist/schema/platformMetaTools.js +63 -0
  74. package/dist/schema/platformMonitorSchedule.js +123 -0
  75. package/dist/schema/platformMonitors.js +159 -0
  76. package/dist/schema/platformOAuth.js +13 -0
  77. package/dist/schema/platformProjectPermissions.js +33 -0
  78. package/dist/schema/platformPublicExecution.js +202 -21
  79. package/dist/schema/platformRoutes.js +122 -0
  80. package/dist/schema/platformSkills.js +11 -0
  81. package/dist/schema/platformTeamSecretName.js +3 -0
  82. package/dist/schema/platformTeamSkillSlug.js +1 -0
  83. package/dist/schema/platformTeamSkills.js +302 -0
  84. package/dist/schema/platformToolAccess.js +62 -0
  85. package/dist/schema/platformTriggers.js +153 -0
  86. package/dist/schema/platformWorkflows.js +100 -0
  87. package/dist/schema/scheduleTimeZone.js +15 -0
  88. package/dist/schema/toolDiscovery.js +71 -0
  89. package/dist/schema/toolInputRepair.js +122 -0
  90. package/dist/schema/toolResultContext.js +51 -0
  91. package/dist/schema/untrustedContent.js +35 -0
  92. package/npm-shrinkwrap.json +73 -0
  93. package/package.json +5 -3
@@ -0,0 +1,1425 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, lstat, open, readFile, realpath, link, rename, unlink, writeFile, } from "node:fs/promises";
3
+ import { join, relative, sep } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { z } from "zod";
6
+ import { platformToolInputSchema, platformCreateExecutionSchema, platformQuoteSchema, platformExecutionIdSchema, platformJsonObjectSchema, platformArtifactSchema, validatePlatformArgumentIdentifiers, } from "../schema/platformPublicExecution.js";
7
+ import { fitToolResultPage, toolResultSelectionSchema, TOOL_RESULT_PAGE_MAX_CHARS, TOOL_RESULT_PAGE_MAX_ITEMS, } from "../schema/toolResultContext.js";
8
+ import { platformFundingShortfallSchema } from "../schema/platformFunding.js";
9
+ import { AUTO_CAPABILITY_VARIANTS } from "../schema/platformCapabilityFamilies.js";
10
+ import { platformExecutionHasPartialOutput } from "../schema/platformPublicExecution.js";
11
+ import { PLATFORM_META_LIMITS } from "../schema/platformMetaTools.js";
12
+ import { PlatformApiError } from "./browser.js";
13
+ import { platformExecutionGuidance, platformUpstreamRefusalNext, } from "./failure-recovery.js";
14
+ const keySchema = z.string().regex(/^[A-Za-z0-9._:-]{8,128}$/);
15
+ export const RETAINED_READ_MS = 5 * 60 * 1000;
16
+ function lastFinishedExecution() {
17
+ let kept;
18
+ return {
19
+ read: (id) => kept?.execution.id === id && Date.now() < kept.until ? kept.execution : undefined,
20
+ keep: (execution) => {
21
+ kept = { until: Date.now() + RETAINED_READ_MS, execution };
22
+ },
23
+ };
24
+ }
25
+ const digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
26
+ const recoveryReferencePattern = /^run:([a-f0-9]{64}):([A-Za-z0-9._:-]{8,128})$/;
27
+ const recoveryReferenceSchema = z.string().regex(recoveryReferencePattern);
28
+ function pathText(text) {
29
+ const parts = text.startsWith("/")
30
+ ? text
31
+ .split("/")
32
+ .slice(1)
33
+ .map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"))
34
+ : (text.replaceAll("[]", "[*]").match(/[^.[\]]+/g) ?? []);
35
+ return parts
36
+ .map((part) => part.replace(/^(['"])(.*)\1$/, "$2"))
37
+ .filter((part, index) => part !== "" && !(index === 0 && part === "$"));
38
+ }
39
+ const looseResultPath = z.union([
40
+ z.array(z.union([z.string().max(256), z.number().int().nonnegative().safe().transform(String)])),
41
+ z.string().max(4096).transform(pathText),
42
+ ]);
43
+ const pathSchema = looseResultPath.pipe(z.array(z.string().max(256)).max(24));
44
+ const TEXT_PAGE_MAX_CHARS = 20_000;
45
+ const selectionSchema = toolResultSelectionSchema.extend({
46
+ path: looseResultPath.optional().pipe(toolResultSelectionSchema.shape.path),
47
+ limit: z.number().int().min(1).optional(),
48
+ maxChars: z.number().int().min(1).optional(),
49
+ find: z
50
+ .array(z.string().min(1).max(50))
51
+ .max(32)
52
+ .refine((terms) => terms.join("").length <= 400, "find takes at most 400 characters of terms in all.")
53
+ .optional(),
54
+ });
55
+ const pickRecordSchema = z
56
+ .record(z.string().min(1).max(80), pathSchema)
57
+ .refine((value) => Object.keys(value).length <= 24);
58
+ const picksSchema = z.preprocess((value) => Array.isArray(value)
59
+ ? Object.fromEntries(value.map((path) => [Array.isArray(path) ? path.join(".") : String(path), path]))
60
+ : value, pickRecordSchema);
61
+ export const agentRunSchema = platformToolInputSchema
62
+ .extend({
63
+ maximumChargeMicros: z
64
+ .number()
65
+ .int()
66
+ .nonnegative()
67
+ .safe()
68
+ .nullable()
69
+ .optional()
70
+ .describe("Optional user-requested Relvo spending ceiling. Omit or use null when none was requested; do not invent a limit or ask for one. 0 permits only zero Relvo charges. 1 USD = 1000000 micros. Does not cap external account charges."),
71
+ externalBillingAccepted: z
72
+ .boolean()
73
+ .default(true)
74
+ .describe("Connected-account and BYOK usage runs by default; false stops before submitting when the user asked to avoid that account's or key's charges or quota use. The Relvo ceiling still applies; this never authorizes sending, publishing or unrelated account changes."),
75
+ idempotencyKey: keySchema.describe("A new random key, such as a UUID, for every new request, even one repeating an earlier conversation's: a key that already ran returns that run's stored result, not new data. Save before calling. Reuse unchanged after lost replies; never replace unknown work."),
76
+ waitSeconds: z.number().int().min(1).max(60).default(25),
77
+ selection: selectionSchema
78
+ .optional()
79
+ .describe("Optional result path/page/fields. fields works on objects and rows. Full output remains stored."),
80
+ pick: picksSchema
81
+ .optional()
82
+ .describe('Optional named exact result paths, e.g. {title:["data","title"]}; with a path, may be relative to it (each row of its page). Missing paths are reported; null/false/0 are preserved.'),
83
+ file: z
84
+ .boolean()
85
+ .optional()
86
+ .describe("Write the whole selected path (fields applied) to a private local file instead of returning it: JSON Lines, one row per line, for an array. Returns the path for the host's file tools."),
87
+ })
88
+ .strict();
89
+ export const agentParallelSchema = z
90
+ .object({
91
+ calls: z
92
+ .array(z
93
+ .object(agentRunSchema.shape)
94
+ .omit({ waitSeconds: true, selection: true, pick: true, file: true })
95
+ .strict()
96
+ .superRefine((value, context) => validatePlatformArgumentIdentifiers(value.tool, value.arguments, context)))
97
+ .min(1)
98
+ .max(PLATFORM_META_LIMITS.batch)
99
+ .describe("Independent catalog tool calls. Each needs its own saved key; dependent work belongs in a later call. Returns admission receipts without waiting for completion."),
100
+ maximumChargeMicros: agentRunSchema.shape.maximumChargeMicros.describe("Optional total Relvo reservation ceiling. When supplied, every call must also have a ceiling and their sum must fit. Omit or use null when none was requested. External charges are separate."),
101
+ })
102
+ .strict()
103
+ .superRefine((value, context) => {
104
+ if (new Set(value.calls.map((call) => call.idempotencyKey)).size !== value.calls.length)
105
+ context.addIssue({
106
+ code: "custom",
107
+ path: ["calls"],
108
+ message: "Each call needs its own retained idempotency key.",
109
+ });
110
+ if (value.maximumChargeMicros != null) {
111
+ if (value.calls.some((call) => call.maximumChargeMicros == null))
112
+ context.addIssue({
113
+ code: "custom",
114
+ path: ["calls"],
115
+ message: "A total ceiling requires an explicit ceiling on every call.",
116
+ });
117
+ else if (value.calls.reduce((sum, call) => sum + BigInt(call.maximumChargeMicros ?? 0), 0n) >
118
+ BigInt(value.maximumChargeMicros))
119
+ context.addIssue({
120
+ code: "custom",
121
+ path: ["maximumChargeMicros"],
122
+ message: "Call ceilings exceed the total reservation ceiling.",
123
+ });
124
+ }
125
+ if (Buffer.byteLength(JSON.stringify(value)) > PLATFORM_META_LIMITS.requestBytes)
126
+ context.addIssue({ code: "custom", message: "Parallel input exceeds its byte limit." });
127
+ });
128
+ export const agentResumeSchema = z
129
+ .object({
130
+ runId: z.union([recoveryReferenceSchema, digestSchema, platformExecutionIdSchema]),
131
+ waitSeconds: agentRunSchema.shape.waitSeconds,
132
+ selection: agentRunSchema.shape.selection,
133
+ pick: agentRunSchema.shape.pick,
134
+ file: agentRunSchema.shape.file,
135
+ })
136
+ .strict();
137
+ export const agentResultSchema = z
138
+ .object({
139
+ id: platformExecutionIdSchema,
140
+ ...selectionSchema.shape,
141
+ pick: agentRunSchema.shape.pick,
142
+ file: agentRunSchema.shape.file,
143
+ waitSeconds: agentRunSchema.shape.waitSeconds,
144
+ })
145
+ .strict();
146
+ export const agentResultsSchema = z
147
+ .object({
148
+ ids: z
149
+ .array(platformExecutionIdSchema)
150
+ .min(1)
151
+ .max(PLATFORM_META_LIMITS.batch)
152
+ .describe("Execution IDs to read together, such as every executionId a parallel call returned."),
153
+ ...selectionSchema.shape,
154
+ pick: agentRunSchema.shape.pick,
155
+ waitSeconds: agentRunSchema.shape.waitSeconds,
156
+ })
157
+ .strict();
158
+ const PENDING = ["queued", "running", "waiting"];
159
+ const RESULT_SELECTION_KEYS = [...Object.keys(selectionSchema.shape), "pick"];
160
+ const savedSchema = z
161
+ .object({
162
+ version: z.literal(1),
163
+ requestHash: digestSchema,
164
+ idempotencyKey: keySchema,
165
+ quote: platformQuoteSchema,
166
+ purchase: platformCreateExecutionSchema,
167
+ })
168
+ .strict();
169
+ const EARLIER_RUN_MS = 10 * 60_000;
170
+ const NO_COMPATIBLE_ALTERNATIVES_NEXT = "No source this project runs could apply these inputs together; sourceRefusals names the inputs each could not apply and the only ones it applies. Run this tool again with just those inputs, dropping one only if the user did not ask for it; an input outside every source's applies is refused the same way. alternatives are tools of the sources it does run, not equivalents: run one, with its provider pinned, only if its own inputs express the request. Otherwise tell the user which condition could not be applied, and offer connecting a provider the tool lists as own_key only if they want it.";
171
+ const DOWNLOAD_NOTE = "Generated files are private and are not in result. Get each one's download URL (valid 5 minutes) with platform_artifact_url {id, artifactId}: platform operation:call on the compact MCP, or relvo artifact-url / relvo download on the CLI. Give the URL only to the user who asked.";
172
+ const EARLIER_RUN_NOTE = "This idempotency key already ran this request at createdAt, so this is that run's stored result, not new data. If the user wants current results, run again with a new idempotency key.";
173
+ export function outputArtifacts(value, depth = 0) {
174
+ if (depth > 3 || value === null || typeof value !== "object")
175
+ return [];
176
+ if (Array.isArray(value))
177
+ return value.flatMap((item) => outputArtifacts(item, depth + 1));
178
+ const own = Array.isArray(value.artifacts)
179
+ ? value.artifacts.flatMap((item) => platformArtifactSchema.safeParse(item).data ?? [])
180
+ : [];
181
+ const found = [
182
+ ...own,
183
+ ...Object.entries(value).flatMap(([key, child]) => key === "artifacts" ? [] : outputArtifacts(child, depth + 1)),
184
+ ];
185
+ return [...new Map(found.map((artifact) => [artifact.id, artifact])).values()];
186
+ }
187
+ const hash = (value) => createHash("sha256").update(value).digest("hex");
188
+ const isApiError = (error, code) => error instanceof Error &&
189
+ error.name === "PlatformApiError" &&
190
+ "code" in error &&
191
+ error.code === code;
192
+ const RUN_FIELDS = new Set(["provider", "relevanceCheck"]);
193
+ const invalidFields = z.object({
194
+ name: z.literal("PlatformApiError"),
195
+ code: z.literal("platform_invalid_tool_arguments"),
196
+ fields: z.array(z.string()).min(1),
197
+ });
198
+ function withRunFieldsMoved(error, request) {
199
+ const refused = invalidFields.safeParse(error);
200
+ if (!refused.success ||
201
+ !refused.data.fields.every((field) => RUN_FIELDS.has(field) &&
202
+ Object.hasOwn(request.arguments, field) &&
203
+ (field === "provider" ? request.provider : request.relevanceCheck) === undefined))
204
+ return undefined;
205
+ const fields = refused.data.fields, moved = platformToolInputSchema.safeParse({
206
+ ...request,
207
+ ...Object.fromEntries(fields.map((field) => [field, request.arguments[field]])),
208
+ arguments: Object.fromEntries(Object.entries(request.arguments).filter(([key]) => !fields.includes(key))),
209
+ });
210
+ return moved.success ? moved.data : undefined;
211
+ }
212
+ function canonical(value) {
213
+ if (Array.isArray(value))
214
+ return `[${value.map(canonical).join(",")}]`;
215
+ if (value !== null && typeof value === "object")
216
+ return `{${Object.keys(value)
217
+ .sort()
218
+ .map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
219
+ .join(",")}}`;
220
+ return JSON.stringify(value);
221
+ }
222
+ const keyForm = (key) => key.toLowerCase().replace(/[_-]/g, "");
223
+ function ownKey(object, key) {
224
+ if (Object.hasOwn(object, key))
225
+ return key;
226
+ const matches = Object.keys(object).filter((name) => keyForm(name) === keyForm(key));
227
+ return matches.length === 1 ? matches[0] : undefined;
228
+ }
229
+ function childAt(value, part) {
230
+ if (value === undefined || value === null || typeof value !== "object" || Array.isArray(value))
231
+ return {};
232
+ const name = ownKey(value, part);
233
+ return name === undefined ? {} : { name, value: value[name] };
234
+ }
235
+ function at(value, path) {
236
+ let current = value;
237
+ for (const key of path) {
238
+ if (current === null || typeof current !== "object")
239
+ return { found: false };
240
+ if (Array.isArray(current)) {
241
+ if (key === "length") {
242
+ current = current.length;
243
+ continue;
244
+ }
245
+ const index = /^(?:0|-?[1-9][0-9]*)$/.test(key)
246
+ ? Number(key) < 0
247
+ ? current.length + Number(key)
248
+ : Number(key)
249
+ : -1;
250
+ if (index < 0 || index >= current.length)
251
+ return { found: false };
252
+ current = current[index];
253
+ }
254
+ else {
255
+ const own = ownKey(current, key);
256
+ if (own === undefined)
257
+ return { found: false };
258
+ current = current[own];
259
+ }
260
+ }
261
+ return { found: true, value: current };
262
+ }
263
+ function nestedPath(value, path) {
264
+ const found = [];
265
+ let visits = 0;
266
+ const visit = (node, prefix) => {
267
+ if (found.length > 1 ||
268
+ ++visits > 2000 ||
269
+ prefix.length > 3 ||
270
+ node === null ||
271
+ typeof node !== "object")
272
+ return;
273
+ if (prefix.length && at(node, path).found) {
274
+ found.push(prefix);
275
+ return;
276
+ }
277
+ const children = Array.isArray(node)
278
+ ? node.slice(0, 20).map((child, index) => [String(index), child])
279
+ : Object.entries(node).slice(0, 64);
280
+ for (const [key, child] of children)
281
+ visit(child, [...prefix, key]);
282
+ };
283
+ if (path.length)
284
+ visit(value, []);
285
+ return found.length === 1 ? [...found[0], ...path] : undefined;
286
+ }
287
+ function shape(value, depth = 0) {
288
+ if (Array.isArray(value))
289
+ return {
290
+ kind: "array",
291
+ length: value.length,
292
+ ...(value.length && depth < 2 ? { firstItem: shape(value[0], depth + 1) } : {}),
293
+ };
294
+ if (value !== null && typeof value === "object")
295
+ return {
296
+ kind: "object",
297
+ totalKeys: Object.keys(value).length,
298
+ keys: Object.keys(value)
299
+ .filter((key) => key.length <= 256)
300
+ .slice(0, 64),
301
+ ...(depth < 1
302
+ ? {
303
+ children: Object.fromEntries(Object.entries(value)
304
+ .filter(([key, child]) => key.length <= 256 && child !== null && typeof child === "object")
305
+ .slice(0, 2)
306
+ .map(([key, child]) => [key, shape(child, depth + 1)])),
307
+ }
308
+ : {}),
309
+ };
310
+ return {
311
+ kind: value === null ? "null" : typeof value,
312
+ ...(typeof value === "string" ? { length: value.length } : {}),
313
+ };
314
+ }
315
+ function absentFields(rows, fields = []) {
316
+ const objects = rows.flatMap((row) => row !== null && typeof row === "object" && !Array.isArray(row) ? [row] : []);
317
+ if (!objects.length || objects.length !== rows.length)
318
+ return [];
319
+ return fields.filter((field) => !objects.some((row) => Object.keys(objectFields(row, [field])).length));
320
+ }
321
+ function rowFields(row, fields) {
322
+ if (!fields?.length)
323
+ return row;
324
+ if (row === null || typeof row !== "object" || Array.isArray(row))
325
+ throw new Error("Select row fields only on objects; inspect the retained result shape.");
326
+ return objectFields(row, fields);
327
+ }
328
+ function objectFields(row, fields) {
329
+ const picked = {};
330
+ for (const key of fields) {
331
+ const own = ownKey(row, key);
332
+ if (own !== undefined) {
333
+ picked[own] = row[own];
334
+ continue;
335
+ }
336
+ const parts = [];
337
+ let value = row;
338
+ for (const part of key.split(".")) {
339
+ const child = childAt(value, part);
340
+ value = child.value;
341
+ parts.push(child.name ?? part);
342
+ }
343
+ if (parts.length < 2 || value === undefined)
344
+ continue;
345
+ let target = picked;
346
+ for (const part of parts.slice(0, -1)) {
347
+ const existing = target[part];
348
+ const copy = existing !== null && typeof existing === "object" && !Array.isArray(existing)
349
+ ? { ...existing }
350
+ : {};
351
+ target[part] = copy;
352
+ target = copy;
353
+ }
354
+ target[parts[parts.length - 1]] = value;
355
+ }
356
+ return picked;
357
+ }
358
+ function cell(picked, field) {
359
+ const own = childAt(picked, field);
360
+ if (own.name !== undefined)
361
+ return own.value ?? null;
362
+ let value = picked;
363
+ for (const part of field.split("."))
364
+ value = childAt(value, part).value;
365
+ return value ?? null;
366
+ }
367
+ const TABLE_AFTER_ROWS = 20;
368
+ function rowWindow(row, path, index) {
369
+ const fields = {}, fragments = [], references = [];
370
+ for (const [key, value] of Object.entries(row)) {
371
+ const target = [...path, String(index), key];
372
+ if (typeof value === "string" && value.length > 512) {
373
+ let end = 512;
374
+ if (/[\uD800-\uDBFF]/.test(value[end - 1] ?? "") && /[\uDC00-\uDFFF]/.test(value[end] ?? ""))
375
+ end++;
376
+ fragments.push({
377
+ path: target,
378
+ text: value.slice(0, end),
379
+ offset: 0,
380
+ totalChars: value.length,
381
+ nextOffset: end,
382
+ });
383
+ }
384
+ else if (value !== null &&
385
+ typeof value === "object" &&
386
+ Buffer.byteLength(JSON.stringify(value)) > 512) {
387
+ references.push({ path: target, shape: shape(value) });
388
+ }
389
+ else
390
+ fields[key] = value;
391
+ }
392
+ return {
393
+ index,
394
+ fields,
395
+ ...(fragments.length ? { fragments } : {}),
396
+ ...(references.length ? { references } : {}),
397
+ };
398
+ }
399
+ function findInText(text, terms, offset, pageBytes) {
400
+ const pattern = new RegExp(terms.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "giu");
401
+ const matches = [];
402
+ let used = 1, position = offset;
403
+ while (position < text.length) {
404
+ const newline = text.indexOf("\n", position), end = newline < 0 ? text.length : newline;
405
+ const line = text.slice(position, end);
406
+ for (let cursor = 0; cursor < line.length;) {
407
+ pattern.lastIndex = cursor;
408
+ const hit = pattern.exec(line);
409
+ if (!hit)
410
+ break;
411
+ let from = line.length > 600 ? Math.max(cursor, hit.index - 200) : 0, to = Math.min(line.length, from + 600);
412
+ if (/[\uDC00-\uDFFF]/.test(line[from] ?? ""))
413
+ from--;
414
+ if (/[\uD800-\uDBFF]/.test(line[to - 1] ?? ""))
415
+ to++;
416
+ const match = { offset: position + from, text: line.slice(from, to) }, size = Buffer.byteLength(JSON.stringify(match)) + 1;
417
+ if (used + size > pageBytes && matches.length)
418
+ return { matches, total: text.length, nextOffset: position + from };
419
+ matches.push(match);
420
+ used += size;
421
+ cursor = to;
422
+ }
423
+ position = end + 1;
424
+ }
425
+ return { matches, total: text.length, nextOffset: null };
426
+ }
427
+ function pathSlice(value, requested) {
428
+ const path = requested.map((segment, index) => segment === "*" && Array.isArray(at(value, requested.slice(0, index)).value) ? ":" : segment);
429
+ const isSlice = (segment) => /^(-?\d+)?:(-?\d+)?$/.test(segment);
430
+ const sliceAt = path.findIndex((segment, index) => isSlice(segment) &&
431
+ path.slice(index + 1).every((part) => !isSlice(part) && !part.includes(".")));
432
+ const slice = sliceAt < 0 ? null : /^(-?\d+)?:(-?\d+)?$/.exec(path[sliceAt]);
433
+ if (!slice)
434
+ return undefined;
435
+ const sliced = at(value, path.slice(0, sliceAt)).value, length = Array.isArray(sliced) ? sliced.length : 0;
436
+ const bound = (text) => text === undefined
437
+ ? undefined
438
+ : Number(text) < 0
439
+ ? Math.max(0, length + Number(text))
440
+ : Number(text);
441
+ return {
442
+ path: path.slice(0, sliceAt),
443
+ start: bound(slice[1]) ?? 0,
444
+ end: bound(slice[2]),
445
+ ...(sliceAt < path.length - 1 ? { field: path.slice(sliceAt + 1).join(".") } : {}),
446
+ };
447
+ }
448
+ function rowPicks(value, picks) {
449
+ let path;
450
+ const relative = {};
451
+ for (const [label, pick] of Object.entries(picks)) {
452
+ const index = pick.findIndex((segment, position) => (segment === "*" || /^(-?\d+)?:(-?\d+)?$/.test(segment)) &&
453
+ Array.isArray(at(value, pick.slice(0, position)).value));
454
+ const prefix = index < 0 ? undefined : [...pick.slice(0, index), pick[index] === "*" ? ":" : pick[index]];
455
+ if (!prefix || (path && JSON.stringify(prefix) !== JSON.stringify(path)))
456
+ return undefined;
457
+ path = prefix;
458
+ relative[label] = pick.slice(index + 1);
459
+ }
460
+ return path && { path, picks: relative };
461
+ }
462
+ const FIND_NEEDS_TEXT = "find searches one text and this selection is not one, so this view is not filtered by it. Select a text with path, such as items[0].body, to search it.";
463
+ export function agentResultView(value, raw, picks) {
464
+ const view = selectedView(value, raw, picks);
465
+ return selectionSchema.parse(raw ?? {}).find?.length &&
466
+ !("matches" in view) &&
467
+ view.found !== false
468
+ ? { ...view, findNotApplied: FIND_NEEDS_TEXT }
469
+ : view;
470
+ }
471
+ function selectedView(value, raw, picks, rowsOnly = false) {
472
+ const rows = picks && !rowsOnly && !raw?.path?.length ? rowPicks(value, picks) : undefined;
473
+ if (rows)
474
+ return selectedView(value, { ...raw, path: rows.path }, rows.picks, true);
475
+ const requested = selectionSchema.parse(raw ?? {});
476
+ const parsed = {
477
+ ...requested,
478
+ limit: Math.min(requested.limit ?? 20, TOOL_RESULT_PAGE_MAX_ITEMS),
479
+ };
480
+ const textChars = Math.min(requested.maxChars ?? requested.limit ?? TOOL_RESULT_PAGE_MAX_CHARS, TEXT_PAGE_MAX_CHARS);
481
+ const slice = pathSlice(value, parsed.path);
482
+ const input = slice
483
+ ? {
484
+ ...parsed,
485
+ path: slice.path,
486
+ offset: slice.start,
487
+ ...(slice.field ? { fields: [slice.field] } : {}),
488
+ limit: slice.end !== undefined
489
+ ? Math.min(Math.max(0, slice.end - slice.start), TOOL_RESULT_PAGE_MAX_ITEMS)
490
+ : parsed.limit,
491
+ }
492
+ : parsed;
493
+ const located = at(value, input.path);
494
+ if (!located.found) {
495
+ const nested = slice ? undefined : nestedPath(value, input.path);
496
+ return nested
497
+ ? {
498
+ ...selectedView(value, { ...input, limit: requested.limit, path: nested }, picks, rowsOnly),
499
+ autoSelected: true,
500
+ requestedPath: input.path,
501
+ }
502
+ : { found: false, path: input.path, shape: shape(value), retained: true };
503
+ }
504
+ const source = located.value ?? null;
505
+ if (slice &&
506
+ Array.isArray(source) &&
507
+ slice.start >= Math.min(slice.end ?? source.length, source.length))
508
+ return { value: [], path: input.path, total: source.length, nextOffset: null, retained: true };
509
+ const bytes = (item) => Buffer.byteLength(JSON.stringify(item));
510
+ if (!picks &&
511
+ !input.fields?.length &&
512
+ input.offset === 0 &&
513
+ input.path.length < 16 &&
514
+ source !== null &&
515
+ typeof source === "object" &&
516
+ !Array.isArray(source) &&
517
+ bytes(source) > 8000) {
518
+ const collections = Object.entries(source).filter(([, child]) => child !== null && typeof child === "object");
519
+ const metadata = Object.fromEntries(Object.entries(source).filter(([, child]) => child === null || typeof child !== "object"));
520
+ if (collections.length === 1 && bytes(metadata) <= 512)
521
+ return {
522
+ ...selectedView(value, {
523
+ ...input,
524
+ limit: requested.limit,
525
+ path: [...input.path, collections[0][0]],
526
+ }),
527
+ autoSelected: true,
528
+ envelopeShape: shape(source),
529
+ };
530
+ const texts = Object.entries(source).filter(([, child]) => typeof child === "string" && bytes(child) > 512);
531
+ const fields = Object.fromEntries(Object.entries(source).filter(([key]) => key !== texts[0]?.[0]));
532
+ if (!collections.length && texts.length === 1 && bytes(fields) <= 512)
533
+ return {
534
+ ...selectedView(value, { ...input, limit: undefined, path: [...input.path, texts[0][0]] }),
535
+ autoSelected: true,
536
+ fields,
537
+ };
538
+ }
539
+ if (!picks && typeof source === "string" && requested.find?.length)
540
+ return {
541
+ ...findInText(source, requested.find, input.offset, Math.max(8000, textChars)),
542
+ path: input.path,
543
+ retained: true,
544
+ instruction: "Each match is a line containing a term; offset is where its text starts. Read around a match with offset and maxChars, or search on from nextOffset.",
545
+ };
546
+ const absent = !picks && Array.isArray(source)
547
+ ? absentFields(source, input.fields)
548
+ : [];
549
+ if (absent.length && absent.length === input.fields?.length)
550
+ return {
551
+ found: false,
552
+ path: input.path,
553
+ missing: absent,
554
+ shape: shape(source),
555
+ retained: true,
556
+ };
557
+ if (!picks && Array.isArray(source) && input.fields?.length && input.limit > TABLE_AFTER_ROWS) {
558
+ const columns = input.fields, rows = source.slice(input.offset, input.offset + input.limit).map((row) => {
559
+ const picked = rowFields(row, columns);
560
+ return columns.map((field) => cell(picked, field));
561
+ });
562
+ let lower = 1, upper = rows.length, count = 0;
563
+ while (lower <= upper) {
564
+ const middle = Math.floor((lower + upper) / 2);
565
+ if (bytes({ columns, rows: rows.slice(0, middle) }) <= TEXT_PAGE_MAX_CHARS) {
566
+ count = middle;
567
+ lower = middle + 1;
568
+ }
569
+ else
570
+ upper = middle - 1;
571
+ }
572
+ if (!count)
573
+ return {
574
+ offloaded: true,
575
+ path: input.path,
576
+ shape: shape(source),
577
+ retained: true,
578
+ instruction: "Read narrower paths or fewer rows from the same execution; do not regenerate.",
579
+ };
580
+ return {
581
+ table: { columns, rows: rows.slice(0, count) },
582
+ ...(absent.length ? { missing: absent } : {}),
583
+ path: input.path,
584
+ total: source.length,
585
+ returned: count,
586
+ nextOffset: input.offset + count < source.length ? input.offset + count : null,
587
+ retained: true,
588
+ };
589
+ }
590
+ let output, total = 1, returned = 1, paged = !picks;
591
+ const missing = [];
592
+ if (picks) {
593
+ const choose = (base, found) => Object.fromEntries(Object.entries(picks).flatMap(([label, path]) => {
594
+ const item = at(base, path);
595
+ if (!item.found)
596
+ return [];
597
+ found.add(label);
598
+ return [[label, item.value ?? null]];
599
+ }));
600
+ let found = new Set();
601
+ output = rowsOnly ? {} : choose(value, found);
602
+ if (!found.size && (rowsOnly || input.path.length)) {
603
+ const relative = new Set(), rows = Array.isArray(source)
604
+ ? source.slice(input.offset, input.offset + input.limit)
605
+ : undefined;
606
+ const chosen = rows ? rows.map((row) => choose(row, relative)) : choose(source, relative);
607
+ if (relative.size) {
608
+ output = chosen;
609
+ found = relative;
610
+ if (rows && Array.isArray(source)) {
611
+ total = source.length;
612
+ returned = rows.length;
613
+ paged = true;
614
+ }
615
+ }
616
+ }
617
+ missing.push(...Object.keys(picks).filter((label) => !found.has(label)));
618
+ }
619
+ else if (Array.isArray(source)) {
620
+ total = source.length;
621
+ const rows = source.slice(input.offset, input.offset + input.limit);
622
+ output = rows.map((row) => rowFields(row, input.fields));
623
+ returned = rows.length;
624
+ missing.push(...absent);
625
+ }
626
+ else if (source !== null && typeof source === "object") {
627
+ const fields = input.fields?.length ? input.fields : undefined, keys = fields ?? Object.keys(source);
628
+ const present = fields
629
+ ? fields.filter((field) => Object.keys(objectFields(source, [field])).length)
630
+ : keys;
631
+ missing.push(...(fields ?? []).filter((field) => !present.includes(field)));
632
+ total = present.length;
633
+ const page = present.slice(input.offset, input.offset + input.limit);
634
+ output = fields
635
+ ? objectFields(source, page)
636
+ : Object.fromEntries(page.map((key) => [key, source[key]]));
637
+ returned = page.length;
638
+ }
639
+ else if (typeof source === "string") {
640
+ total = source.length;
641
+ let end = Math.min(source.length, input.offset + textChars);
642
+ if (end < source.length &&
643
+ /[\uD800-\uDBFF]/.test(source[end - 1] ?? "") &&
644
+ /[\uDC00-\uDFFF]/.test(source[end] ?? ""))
645
+ end++;
646
+ output = source.slice(input.offset, end);
647
+ returned = output.length;
648
+ }
649
+ else
650
+ output = input.offset ? null : source;
651
+ if (!picks && (source === null || typeof source !== "object"))
652
+ missing.push(...(input.fields ?? []));
653
+ if (!picks &&
654
+ Array.isArray(output) &&
655
+ bytes(output) > 8000 &&
656
+ output.some((row) => bytes(row) > 4000)) {
657
+ const windows = [];
658
+ for (const [index, row] of output.entries()) {
659
+ if (row === null || typeof row !== "object" || Array.isArray(row))
660
+ break;
661
+ windows.push(rowWindow(row, input.path, input.offset + index));
662
+ }
663
+ if (windows.length === output.length) {
664
+ const page = fitToolResultPage({
665
+ path: input.path,
666
+ kind: "array",
667
+ offset: input.offset,
668
+ total,
669
+ nextOffset: input.offset + windows.length < total ? input.offset + windows.length : null,
670
+ context: { offloaded: false, preview: windows, totalBytes: bytes(windows) },
671
+ }, (candidate) => bytes(candidate.context.preview) <= 8000);
672
+ if (bytes(page.context.preview) <= 8000)
673
+ return {
674
+ rowWindows: page.context.preview,
675
+ path: input.path,
676
+ total,
677
+ nextOffset: page.nextOffset,
678
+ retained: true,
679
+ instruction: "fields are exact complete values; fragments are incomplete text excerpts with exact source paths/cursors. Read needed missing details from the same execution; never treat a fragment as a complete URL or identifier.",
680
+ };
681
+ }
682
+ }
683
+ const pageBytes = typeof output === "string"
684
+ ? Math.max(8000, textChars)
685
+ : requested.limit !== undefined || slice?.end !== undefined
686
+ ? TEXT_PAGE_MAX_CHARS
687
+ : 8000;
688
+ if (paged && (Array.isArray(output) || typeof output === "string") && bytes(output) > pageBytes) {
689
+ const page = fitToolResultPage({
690
+ path: input.path,
691
+ kind: Array.isArray(output) ? "array" : "string",
692
+ offset: input.offset,
693
+ total,
694
+ nextOffset: null,
695
+ context: { offloaded: false, preview: output, totalBytes: bytes(output) },
696
+ }, (candidate) => bytes(candidate.context.preview) <= pageBytes);
697
+ output = page.context.preview;
698
+ if (typeof output === "string" || Array.isArray(output))
699
+ returned = output.length;
700
+ }
701
+ if (bytes(output) > pageBytes)
702
+ return {
703
+ offloaded: true,
704
+ path: input.path,
705
+ shape: shape(source),
706
+ retained: true,
707
+ instruction: "Read narrower paths or fewer rows from the same execution; do not regenerate.",
708
+ };
709
+ const short = paged && Array.isArray(output) && returned < Math.min(input.limit, total - input.offset);
710
+ return {
711
+ value: output,
712
+ ...(missing.length ? { missing } : {}),
713
+ path: input.path,
714
+ total,
715
+ ...(short ? { returned } : {}),
716
+ nextOffset: !paged || input.offset + returned >= total ? null : input.offset + returned,
717
+ retained: true,
718
+ };
719
+ }
720
+ export function resultFilesFor(env, cwd = process.cwd()) {
721
+ return env.RELVO_RESULT_DIRECTORY
722
+ ? { directory: env.RELVO_RESULT_DIRECTORY }
723
+ : { directory: join(cwd, ".relvo", "results"), within: cwd };
724
+ }
725
+ const readableByOthers = (mode) => process.platform !== "win32" && (mode & 0o077) !== 0;
726
+ const failureText = (error) => error instanceof Error ? error.message : "The result could not be read.";
727
+ export class AgentRun {
728
+ client;
729
+ directory;
730
+ resultFiles;
731
+ topUp;
732
+ retained;
733
+ constructor(client, directory = join(tmpdir(), `relvo-runs-${process.getuid?.() ?? "user"}`), resultFiles, topUp, retained = lastFinishedExecution()) {
734
+ this.client = client;
735
+ this.directory = directory;
736
+ this.resultFiles = resultFiles;
737
+ this.topUp = topUp;
738
+ this.retained = retained;
739
+ }
740
+ async resultFolder() {
741
+ if (!this.resultFiles) {
742
+ const path = join(await this.folder(), "results");
743
+ await mkdir(path, { recursive: true, mode: 0o700 });
744
+ return path;
745
+ }
746
+ const { directory, within } = this.resultFiles;
747
+ const escape = new Error("The result directory must not lead outside the working directory.");
748
+ let current = within;
749
+ for (const part of within ? relative(within, directory).split(sep) : []) {
750
+ current = join(current ?? "", part);
751
+ const info = await lstat(current).catch((error) => {
752
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
753
+ return null;
754
+ throw error;
755
+ });
756
+ if (!info)
757
+ break;
758
+ if (info.isSymbolicLink())
759
+ throw escape;
760
+ }
761
+ await mkdir(directory, { recursive: true, mode: 0o700 });
762
+ const info = await lstat(directory);
763
+ if (!info.isDirectory() ||
764
+ info.isSymbolicLink() ||
765
+ (process.getuid && info.uid !== process.getuid()))
766
+ throw new Error("The result directory must be a directory that belongs to this user.");
767
+ if (within &&
768
+ (await realpath(directory)) !== join(await realpath(within), relative(within, directory)))
769
+ throw escape;
770
+ await writeFile(join(directory, ".gitignore"), "*\n", { flag: "wx", mode: 0o600 }).catch((error) => {
771
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
772
+ throw error;
773
+ });
774
+ return directory;
775
+ }
776
+ async alternatives(tool, maximumChargeMicros) {
777
+ const results = [];
778
+ try {
779
+ const definition = (await this.client.tool(tool)).data;
780
+ const providers = (definition.providers ?? []).filter((item) => item.available && item.id !== "auto"), plans = [];
781
+ const family = AUTO_CAPABILITY_VARIANTS[tool];
782
+ if (family) {
783
+ for (const name of family) {
784
+ const provider = providers.find((item) => name.startsWith(item.id + "_"));
785
+ if (provider)
786
+ plans.push({ name, provider: provider.id });
787
+ }
788
+ }
789
+ for (const provider of providers
790
+ .filter((item) => !plans.some((plan) => plan.provider === item.id))
791
+ .slice(0, 4)) {
792
+ const candidates = (await this.client.searchTools({
793
+ query: definition.title ?? definition.name.replaceAll("_", " "),
794
+ provider: provider.id,
795
+ limit: 12,
796
+ summary: true,
797
+ })).data.tools;
798
+ plans.push(...candidates
799
+ .filter((item) => item.name !== tool && item.name.startsWith(provider.id + "_"))
800
+ .map((item) => ({ name: item.name, provider: provider.id })));
801
+ }
802
+ for (const plan of plans.slice(0, 12)) {
803
+ if (results.some((item) => item.tool === plan.name))
804
+ continue;
805
+ try {
806
+ const candidate = (await this.client.tool(plan.name)).data;
807
+ if (!candidate.available)
808
+ continue;
809
+ const quote = (await this.client.quote({
810
+ tool: candidate.name,
811
+ arguments: candidate.example,
812
+ provider: plan.provider,
813
+ })).data;
814
+ if (quote.maximumChargeMicros > maximumChargeMicros ||
815
+ quote.funding === "provider" ||
816
+ quote.funding === "mixed" ||
817
+ quote.externalBillingProviders?.length)
818
+ continue;
819
+ const core = {
820
+ tool: candidate.name,
821
+ provider: plan.provider,
822
+ description: candidate.description.slice(0, 400),
823
+ example: candidate.example,
824
+ quotedExampleCeilingMicros: quote.maximumChargeMicros,
825
+ };
826
+ const inline = candidate.inputSchema !== undefined &&
827
+ Buffer.byteLength(JSON.stringify([...results, { ...core, inputSchema: candidate.inputSchema }])) <= 7500;
828
+ results.push({
829
+ ...core,
830
+ quoteBasis: "example",
831
+ schemaComplete: inline,
832
+ ...(inline ? { inputSchema: candidate.inputSchema } : { inspectWith: "tool" }),
833
+ });
834
+ if (results.length === 3)
835
+ return results;
836
+ }
837
+ catch {
838
+ }
839
+ }
840
+ }
841
+ catch {
842
+ }
843
+ return results;
844
+ }
845
+ async folder() {
846
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
847
+ const root = await lstat(this.directory);
848
+ if (!root.isDirectory() ||
849
+ root.isSymbolicLink() ||
850
+ readableByOthers(root.mode) ||
851
+ (process.getuid && root.uid !== process.getuid()))
852
+ throw new Error("The private run directory must belong to this user and allow owner access only.");
853
+ const path = join(this.directory, await this.client.requestJournalScope());
854
+ await mkdir(path, { recursive: true, mode: 0o700 });
855
+ const scoped = await lstat(path);
856
+ if (!scoped.isDirectory() ||
857
+ scoped.isSymbolicLink() ||
858
+ readableByOthers(scoped.mode) ||
859
+ (process.getuid && scoped.uid !== process.getuid()))
860
+ throw new Error("Invalid private run directory.");
861
+ return path;
862
+ }
863
+ async read(runId) {
864
+ const path = join(await this.folder(), digestSchema.parse(runId) + ".json");
865
+ try {
866
+ const info = await lstat(path);
867
+ if (!info.isFile() || info.isSymbolicLink() || readableByOthers(info.mode))
868
+ throw new Error("Invalid saved run file.");
869
+ return savedSchema.parse(JSON.parse(await readFile(path, "utf8")));
870
+ }
871
+ catch (error) {
872
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
873
+ return null;
874
+ throw error;
875
+ }
876
+ }
877
+ async persist(runId, plan, replace = false) {
878
+ const directory = await this.folder(), destination = join(directory, runId + ".json"), temporary = join(directory, randomUUID() + ".tmp");
879
+ const file = await open(temporary, "wx", 0o600);
880
+ try {
881
+ await file.writeFile(JSON.stringify(plan));
882
+ await file.sync();
883
+ }
884
+ finally {
885
+ await file.close();
886
+ }
887
+ if (replace)
888
+ await rename(temporary, destination);
889
+ else {
890
+ try {
891
+ await link(temporary, destination);
892
+ }
893
+ catch (error) {
894
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
895
+ throw error;
896
+ }
897
+ finally {
898
+ await unlink(temporary);
899
+ }
900
+ }
901
+ const stored = await this.read(runId);
902
+ if (!stored || stored.requestHash !== plan.requestHash)
903
+ throw new Error("This run key is bound to another request. Retain the original purchase.");
904
+ return stored;
905
+ }
906
+ async parallel(raw, signal) {
907
+ const input = agentParallelSchema.parse(raw), results = new Array(input.calls.length);
908
+ let next = 0;
909
+ await Promise.all(Array.from({ length: Math.min(input.calls.length, PLATFORM_META_LIMITS.concurrency) }, async () => {
910
+ for (let index = next++; index < input.calls.length; index = next++) {
911
+ const call = input.calls[index], receipt = { index, tool: call.tool, idempotencyKey: call.idempotencyKey };
912
+ try {
913
+ const result = await this.run(call, signal, { waitForCompletion: false });
914
+ results[index] =
915
+ "id" in result
916
+ ? {
917
+ ...receipt,
918
+ admission: "accepted",
919
+ runId: result.runId,
920
+ executionId: result.id,
921
+ status: result.status,
922
+ reservedMicros: result.reservedMicros,
923
+ chargedMicros: result.chargedMicros,
924
+ ...(result.errorCode ? { errorCode: result.errorCode } : {}),
925
+ ...(result.reconnectUrl ? { reconnectUrl: result.reconnectUrl } : {}),
926
+ ...("earlierRun" in result ? { earlierRun: result.earlierRun } : {}),
927
+ ...platformExecutionGuidance(result),
928
+ }
929
+ : {
930
+ ...receipt,
931
+ admission: "rejected",
932
+ runId: result.runId,
933
+ error: result.reason,
934
+ };
935
+ }
936
+ catch (error) {
937
+ const api = error instanceof PlatformApiError ? error : undefined;
938
+ results[index] = {
939
+ ...receipt,
940
+ admission: api && api.status >= 400 && api.status < 500 && api.status !== 408
941
+ ? "rejected"
942
+ : "unknown",
943
+ error: (api?.guidance ? api.summary : failureText(error)).slice(0, 400),
944
+ ...(api?.code ? { errorCode: api.code } : {}),
945
+ ...(api?.shortfall ? { shortfall: api.shortfall } : {}),
946
+ ...(api?.reconnectUrl ? { reconnectUrl: api.reconnectUrl } : {}),
947
+ ...api?.guidance,
948
+ ...(api?.billing ? { billing: api.billing } : {}),
949
+ ...(api?.billingUrl
950
+ ? {
951
+ fundingRecovery: {
952
+ billingUrl: api.billingUrl,
953
+ ...(api.shortfall ? { shortfall: api.shortfall } : {}),
954
+ },
955
+ }
956
+ : {}),
957
+ };
958
+ }
959
+ }
960
+ }));
961
+ const recoveryInstructions = {};
962
+ const receipts = results.map(({ recovery, billing, ...receipt }) => {
963
+ const compact = { ...receipt };
964
+ if (recovery) {
965
+ const { instruction, ...action } = recovery;
966
+ if (instruction)
967
+ recoveryInstructions[recovery.action] = instruction;
968
+ compact.recovery = action;
969
+ }
970
+ if (billing) {
971
+ const { note: _note, ...charge } = billing;
972
+ compact.billing = charge;
973
+ }
974
+ return compact;
975
+ });
976
+ return {
977
+ allAccepted: results.every((result) => result.admission === "accepted"),
978
+ results: receipts,
979
+ ...(Object.keys(recoveryInstructions).length ? { recoveryInstructions } : {}),
980
+ instruction: "Admission is not completion. Read the accepted executionIds together with one result {ids:[...]}; needs_review still requires approval. For unknown admission repeat the identical call and key, never replace accepted or uncertain work. Dependent calls must wait for their inputs." +
981
+ (results.some((result) => result.billing)
982
+ ? " Report Relvo billing per call; zero is only Relvo's charge. Earlier or external-provider charges remain separate."
983
+ : ""),
984
+ };
985
+ }
986
+ async run(raw, signal, options = {}) {
987
+ const input = agentRunSchema.parse(raw), { waitSeconds, selection, pick, file, idempotencyKey, maximumChargeMicros: requestedCeiling, externalBillingAccepted, intent: _intent, ...sent } = input;
988
+ const maximumChargeMicros = requestedCeiling ?? undefined;
989
+ const view = {
990
+ ...input,
991
+ signal,
992
+ waitSeconds: options.waitForCompletion === false ? 0 : waitSeconds,
993
+ startedAt: Date.now(),
994
+ };
995
+ signal?.throwIfAborted();
996
+ if (file && pick)
997
+ throw new Error("file writes one selected path; use selection, not pick.");
998
+ const requestHash = hash(canonical(platformJsonObjectSchema.parse({
999
+ tool: sent.tool,
1000
+ arguments: sent.arguments,
1001
+ ...(maximumChargeMicros === undefined ? {} : { maximumChargeMicros }),
1002
+ ...(sent.provider === undefined ? {} : { provider: sent.provider }),
1003
+ ...(sent.relevanceCheck === undefined ? {} : { relevanceCheck: sent.relevanceCheck }),
1004
+ ...(sent.sessionId === undefined ? {} : { sessionId: sent.sessionId }),
1005
+ }))), runId = hash(idempotencyKey);
1006
+ let saved = await this.read(runId);
1007
+ if (saved && saved.requestHash !== requestHash)
1008
+ throw new Error("This run key is bound to another request. Recover the original run instead.");
1009
+ const purchase = async () => {
1010
+ const quoted = (body) => {
1011
+ const quoteInput = { ...body, ...(input.intent ? { intent: input.intent } : {}) };
1012
+ return this.client
1013
+ .quote({
1014
+ ...quoteInput,
1015
+ ...(maximumChargeMicros === undefined ? {} : { maximumChargeMicros }),
1016
+ }, signal)
1017
+ .catch((error) => {
1018
+ if (maximumChargeMicros !== undefined &&
1019
+ isApiError(error, "platform_invalid_tool_request"))
1020
+ return this.client.quote(quoteInput, signal);
1021
+ throw error;
1022
+ });
1023
+ };
1024
+ let request = sent;
1025
+ const quotedRequest = await quoted(request)
1026
+ .catch((error) => {
1027
+ const moved = withRunFieldsMoved(error, request);
1028
+ if (!moved)
1029
+ throw error;
1030
+ request = moved;
1031
+ return quoted(moved);
1032
+ })
1033
+ .catch(async (error) => {
1034
+ signal?.throwIfAborted();
1035
+ const recovered = await this.recover(idempotencyKey, view, requestHash).catch((lookup) => {
1036
+ if (isApiError(lookup, "platform_idempotency_key_used_for_another_request"))
1037
+ throw lookup;
1038
+ return null;
1039
+ });
1040
+ if (recovered)
1041
+ return recovered;
1042
+ throw error;
1043
+ });
1044
+ if ("admitted" in quotedRequest)
1045
+ return quotedRequest;
1046
+ const quote = quotedRequest.data;
1047
+ const external = quote.funding === "provider" ||
1048
+ quote.funding === "mixed" ||
1049
+ Boolean(quote.externalBillingProviders?.length);
1050
+ const overBudget = maximumChargeMicros !== undefined && quote.maximumChargeMicros > maximumChargeMicros;
1051
+ if (overBudget || (external && !externalBillingAccepted)) {
1052
+ const recovered = await this.recover(idempotencyKey, view, requestHash);
1053
+ if (recovered)
1054
+ return recovered;
1055
+ const alternatives = !request.provider && overBudget && maximumChargeMicros !== undefined
1056
+ ? await this.alternatives(request.tool, maximumChargeMicros)
1057
+ : [];
1058
+ return {
1059
+ runId: await this.reference(idempotencyKey),
1060
+ admitted: false,
1061
+ quote,
1062
+ reason: overBudget
1063
+ ? "The quote exceeds the authorized Relvo ceiling. No execution was submitted."
1064
+ : "This run declined external account usage (externalBillingAccepted:false). No execution was submitted.",
1065
+ ...(external && !externalBillingAccepted
1066
+ ? {
1067
+ next: "This tool is billed by the connected account or the project's own key. Use a tool Relvo bills instead, or run it with externalBillingAccepted omitted once the user allows that usage; retain the same request and purchase key.",
1068
+ }
1069
+ : {}),
1070
+ ...(alternatives.length
1071
+ ? {
1072
+ alternatives,
1073
+ next: "These are candidate tools, not guaranteed equivalents. Reuse included complete schemas; inspect only missing constraints. Choose only a matching capability and quote/run actual inputs with the listed provider pinned. Example prices never authorize a purchase or a larger task budget.",
1074
+ }
1075
+ : {}),
1076
+ };
1077
+ }
1078
+ return savedSchema.parse({
1079
+ version: 1,
1080
+ requestHash,
1081
+ idempotencyKey,
1082
+ quote,
1083
+ purchase: {
1084
+ ...request,
1085
+ ...quote.plan,
1086
+ ...(input.intent ? { intent: input.intent } : {}),
1087
+ maximumChargeMicros: maximumChargeMicros ?? quote.maximumChargeMicros,
1088
+ priceVersion: quote.priceVersion,
1089
+ runRequestHash: requestHash,
1090
+ },
1091
+ });
1092
+ };
1093
+ if (!saved) {
1094
+ const planned = await purchase();
1095
+ if ("admitted" in planned)
1096
+ return planned;
1097
+ saved = await this.persist(runId, planned);
1098
+ }
1099
+ try {
1100
+ return await this.submit(saved, view);
1101
+ }
1102
+ catch (error) {
1103
+ if (!isApiError(error, "platform_quote_changed"))
1104
+ throw error;
1105
+ const planned = await purchase();
1106
+ if ("admitted" in planned)
1107
+ return planned;
1108
+ return this.submit(await this.persist(runId, planned, true), view);
1109
+ }
1110
+ }
1111
+ async resume(raw, signal) {
1112
+ const input = agentResumeSchema.parse(raw);
1113
+ const view = { ...input, signal };
1114
+ signal?.throwIfAborted();
1115
+ if (input.file && input.pick)
1116
+ throw new Error("file writes one selected path; use selection, not pick.");
1117
+ const reference = recoveryReferencePattern.exec(input.runId), idempotencyKey = reference?.[2];
1118
+ if (reference && reference[1] !== (await this.client.requestJournalScope()))
1119
+ throw new Error("Saved purchase unavailable in this credential scope. Inspect project history; do not replace unknown work.");
1120
+ if (!reference && !digestSchema.safeParse(input.runId).success)
1121
+ return this.result({
1122
+ id: input.runId,
1123
+ waitSeconds: input.waitSeconds,
1124
+ ...input.selection,
1125
+ ...(input.pick ? { pick: input.pick } : {}),
1126
+ ...(input.file ? { file: input.file } : {}),
1127
+ }, signal);
1128
+ const saved = await this.read(idempotencyKey ? hash(idempotencyKey) : input.runId);
1129
+ if (!saved && idempotencyKey) {
1130
+ const recovered = await this.recover(idempotencyKey, view);
1131
+ if (recovered)
1132
+ return recovered;
1133
+ }
1134
+ if (!saved)
1135
+ throw new Error("Saved purchase unavailable in this credential scope. Inspect project history; do not replace unknown work.");
1136
+ try {
1137
+ return await this.submit(saved, view);
1138
+ }
1139
+ catch (error) {
1140
+ if (!isApiError(error, "platform_quote_changed"))
1141
+ throw error;
1142
+ throw new Error("The price changed before this purchase was admitted, so nothing was bought. Call run again with the same request and idempotency key: it quotes the new price and retains any supplied ceiling.");
1143
+ }
1144
+ }
1145
+ async reference(idempotencyKey) {
1146
+ return `run:${await this.client.requestJournalScope()}:${idempotencyKey}`;
1147
+ }
1148
+ async recover(idempotencyKey, view, requestHash) {
1149
+ const execution = (await this.client.recoverExecution({ idempotencyKey, requestHash }, view.signal)).data.execution;
1150
+ return execution ? this.finish(execution.id, execution, null, view) : null;
1151
+ }
1152
+ async submit(saved, view) {
1153
+ let execution, toppedUp = null;
1154
+ try {
1155
+ execution = (await this.client.createExecution(saved.purchase, saved.idempotencyKey, view.signal)).data;
1156
+ }
1157
+ catch (error) {
1158
+ if (isApiError(error, "platform_idempotency_key_used_for_another_request")) {
1159
+ const recovered = await this.recover(saved.idempotencyKey, view, saved.requestHash);
1160
+ if (recovered)
1161
+ return recovered;
1162
+ throw error;
1163
+ }
1164
+ const shortfall = error instanceof Error && error.name === "PlatformApiError" && "shortfall" in error
1165
+ ? platformFundingShortfallSchema.safeParse(error.shortfall).data
1166
+ : undefined;
1167
+ toppedUp = shortfall && this.topUp ? await this.topUp(shortfall) : null;
1168
+ if (!toppedUp)
1169
+ throw error;
1170
+ execution = (await this.client.createExecution(saved.purchase, saved.idempotencyKey, view.signal)).data;
1171
+ }
1172
+ return this.finish(execution.id, execution, saved.quote, view, toppedUp);
1173
+ }
1174
+ async finish(runId, execution, quote, { waitSeconds, selection, pick, file, signal, startedAt }, toppedUp = null) {
1175
+ let timedOut = false;
1176
+ if (waitSeconds > 0 && PENDING.includes(execution.status)) {
1177
+ try {
1178
+ const waited = await this.client.waitExecution(execution.id, waitSeconds * 1000, signal);
1179
+ timedOut = waited.timedOut;
1180
+ execution = waited.data ?? execution;
1181
+ }
1182
+ catch {
1183
+ timedOut = true;
1184
+ }
1185
+ }
1186
+ this.keep(execution);
1187
+ const earlier = startedAt !== undefined && Date.parse(execution.createdAt) < startedAt - EARLIER_RUN_MS;
1188
+ const alternatives = execution.status === "failed" && execution.errorCode === "platform_no_compatible_provider"
1189
+ ? await this.alternatives(execution.tool, quote?.maximumChargeMicros ?? execution.reservedMicros ?? 0)
1190
+ : [];
1191
+ const view = {
1192
+ ...this.view(runId, execution, quote, timedOut, selection, pick),
1193
+ ...(toppedUp ? { toppedUp } : {}),
1194
+ ...(earlier
1195
+ ? {
1196
+ earlierRun: {
1197
+ createdAt: execution.createdAt,
1198
+ note: EARLIER_RUN_NOTE,
1199
+ },
1200
+ }
1201
+ : {}),
1202
+ ...(alternatives.length ? { alternatives, next: NO_COMPATIBLE_ALTERNATIVES_NEXT } : {}),
1203
+ };
1204
+ if (!file || (execution.status !== "succeeded" && !this.hasPartial(execution)))
1205
+ return view;
1206
+ try {
1207
+ return {
1208
+ ...view,
1209
+ file: await this.exportFile(execution.id, execution.output, selection?.path ?? [], selection?.fields),
1210
+ };
1211
+ }
1212
+ catch (error) {
1213
+ return {
1214
+ ...view,
1215
+ fileError: `${failureText(error)} The saved output is retained: result {id} reads it.`,
1216
+ };
1217
+ }
1218
+ }
1219
+ keep(execution) {
1220
+ if (execution.status === "succeeded" || this.hasPartial(execution)) {
1221
+ try {
1222
+ this.retained.keep(execution);
1223
+ }
1224
+ catch {
1225
+ }
1226
+ }
1227
+ }
1228
+ hasPartial(execution) {
1229
+ return platformExecutionHasPartialOutput(execution);
1230
+ }
1231
+ readableResult(execution, selection, pick, partial = false) {
1232
+ try {
1233
+ const result = agentResultView(execution.output, selection, pick);
1234
+ return partial ? { partialResult: result } : { result };
1235
+ }
1236
+ catch (error) {
1237
+ return {
1238
+ resultError: `${failureText(error)} The saved output is retained: result {id} reads it with another selection.`,
1239
+ };
1240
+ }
1241
+ }
1242
+ view(runId, execution, quote, timedOut, selection, pick) {
1243
+ return {
1244
+ runId,
1245
+ admitted: true,
1246
+ ...(quote?.plan ? { quote: { plan: quote.plan } } : {}),
1247
+ id: execution.id,
1248
+ status: execution.status,
1249
+ timedOut,
1250
+ reservedMicros: execution.reservedMicros,
1251
+ chargedMicros: ["succeeded", "failed", "cancelled"].includes(execution.status)
1252
+ ? execution.chargedMicros
1253
+ : null,
1254
+ errorCode: execution.errorCode,
1255
+ ...(execution.sourceRefusals?.length ? { sourceRefusals: execution.sourceRefusals } : {}),
1256
+ ...(execution.reconnectUrl
1257
+ ? {
1258
+ reconnectUrl: execution.reconnectUrl,
1259
+ next: "Show the user reconnectUrl to reconnect the account, then run this again.",
1260
+ }
1261
+ : {}),
1262
+ ...platformExecutionGuidance(execution),
1263
+ ...(execution.status === "succeeded" ? this.readableResult(execution, selection, pick) : {}),
1264
+ ...(this.hasPartial(execution)
1265
+ ? {
1266
+ ...this.readableResult(execution, selection, pick, true),
1267
+ next: "The task did not finish. Report partialResult as incomplete saved output, with the failure and charge; do not repeat completed work.",
1268
+ }
1269
+ : {}),
1270
+ ...this.pendingNext(execution),
1271
+ ...this.refusal(execution),
1272
+ ...this.downloads(execution),
1273
+ retained: {
1274
+ readWith: "result",
1275
+ input: { id: execution.id },
1276
+ optionalInput: RESULT_SELECTION_KEYS,
1277
+ },
1278
+ };
1279
+ }
1280
+ pendingNext(execution) {
1281
+ if (!PENDING.includes(execution.status))
1282
+ return {};
1283
+ return execution.rateLimitedUntil
1284
+ ? {
1285
+ rateLimitedUntil: execution.rateLimitedUntil,
1286
+ next: `The app's rate limit holds this call until rateLimitedUntil; it is sent again by itself. Read it with result {id} after that time and do not repurchase. Sending more calls to this app meanwhile only adds refusals.`,
1287
+ }
1288
+ : {
1289
+ next: "Still pending: result {id} (on the CLI, relvo result <id> --wait 60) waits for the same execution. Do not repurchase; answer only after it succeeds or fails.",
1290
+ };
1291
+ }
1292
+ refusal({ upstreamRefusal, reconnectUrl }) {
1293
+ return upstreamRefusal
1294
+ ? {
1295
+ upstreamRefusal,
1296
+ ...(reconnectUrl ? { reconnectUrl } : {}),
1297
+ next: platformUpstreamRefusalNext(upstreamRefusal, reconnectUrl),
1298
+ }
1299
+ : {};
1300
+ }
1301
+ downloads(execution) {
1302
+ const ids = execution.status === "succeeded" || this.hasPartial(execution)
1303
+ ? outputArtifacts(execution.output ?? null).map((artifact) => artifact.id)
1304
+ : [];
1305
+ return ids.length
1306
+ ? {
1307
+ downloads: {
1308
+ with: "platform_artifact_url",
1309
+ inputs: ids.map((artifactId) => ({ id: execution.id, artifactId })),
1310
+ note: DOWNLOAD_NOTE,
1311
+ },
1312
+ }
1313
+ : {};
1314
+ }
1315
+ async exportFile(id, output, requested, requestedFields) {
1316
+ const slice = pathSlice(output, requested), direct = slice?.path ?? requested, fields = slice?.field ? [slice.field] : requestedFields;
1317
+ const first = at(output, direct), nested = first.found || slice ? undefined : nestedPath(output, direct);
1318
+ const selected = nested ?? direct, located = nested ? at(output, nested) : first;
1319
+ if (!located.found)
1320
+ return { found: false, path: selected, shape: shape(output) };
1321
+ const found = located.value ?? null;
1322
+ let value = slice && Array.isArray(found) ? found.slice(slice.start, slice.end) : found, path = selected;
1323
+ if (found !== null &&
1324
+ typeof found === "object" &&
1325
+ !Array.isArray(found) &&
1326
+ !fields?.some((key) => Object.hasOwn(found, key))) {
1327
+ const arrays = Object.entries(found).filter(([, child]) => Array.isArray(child));
1328
+ if (arrays.length === 1) {
1329
+ path = [...(nested ?? requested), arrays[0][0]];
1330
+ value = arrays[0][1];
1331
+ }
1332
+ }
1333
+ const rows = Array.isArray(value) ? value.map((row) => rowFields(row, fields)) : null;
1334
+ const text = rows
1335
+ ? rows.map((row) => JSON.stringify(row) + "\n").join("")
1336
+ : JSON.stringify(rowFields(value, fields)) + "\n";
1337
+ const directory = await this.resultFolder();
1338
+ const rowsOf = slice &&
1339
+ Array.isArray(found) &&
1340
+ (slice.start > 0 || (slice.end !== undefined && slice.end < found.length))
1341
+ ? { rows: [slice.start, slice.end ?? null] }
1342
+ : {};
1343
+ const destination = join(directory, `${hash(id)}-${hash(JSON.stringify({ path, fields: fields ?? [], ...rowsOf })).slice(0, 16)}.${rows ? "jsonl" : "json"}`);
1344
+ const temporary = join(directory, randomUUID() + ".tmp"), file = await open(temporary, "wx", 0o600);
1345
+ try {
1346
+ await file.writeFile(text);
1347
+ await file.sync();
1348
+ }
1349
+ finally {
1350
+ await file.close();
1351
+ }
1352
+ await rename(temporary, destination);
1353
+ return {
1354
+ path: destination,
1355
+ format: rows ? "jsonl" : "json",
1356
+ ...(rows ? { rows: rows.length } : {}),
1357
+ bytes: Buffer.byteLength(text),
1358
+ selectedPath: path,
1359
+ use: rows
1360
+ ? "One JSON object per line: filter with jq -c 'select(...)', search with grep, or page with Read."
1361
+ : "Filter with jq or read it with Read.",
1362
+ };
1363
+ }
1364
+ async result(raw, signal) {
1365
+ const { id, pick, file, waitSeconds, ...selection } = agentResultSchema.parse(raw);
1366
+ if (file && pick)
1367
+ throw new Error("file writes one selected path; use path and fields, not pick.");
1368
+ return this.readExecution(id, selection, pick, file, waitSeconds * 1000, signal);
1369
+ }
1370
+ async results(raw, signal) {
1371
+ const { ids, pick, waitSeconds, ...selection } = agentResultsSchema.parse(raw);
1372
+ const unique = [...new Set(ids)], until = Date.now() + waitSeconds * 1000, results = new Array(unique.length);
1373
+ let next = 0;
1374
+ await Promise.all(Array.from({ length: Math.min(unique.length, PLATFORM_META_LIMITS.concurrency) }, async () => {
1375
+ for (let index = next++; index < unique.length; index = next++) {
1376
+ const id = unique[index];
1377
+ try {
1378
+ results[index] = await this.readExecution(id, selection, pick, undefined, Math.max(0, until - Date.now()), signal);
1379
+ }
1380
+ catch (error) {
1381
+ results[index] = { id, error: failureText(error) };
1382
+ }
1383
+ }
1384
+ }));
1385
+ return { results };
1386
+ }
1387
+ async readExecution(id, selection, pick, file, waitMs, signal) {
1388
+ const retained = this.retained.read(id);
1389
+ if (retained)
1390
+ await this.retained.authorize?.(id);
1391
+ let execution = retained ?? (await this.client.execution(id, signal)).data, timedOut = false;
1392
+ if (waitMs >= 1000 && PENDING.includes(execution.status)) {
1393
+ const waited = await this.client.waitExecution(id, waitMs, signal);
1394
+ timedOut = waited.timedOut;
1395
+ execution = waited.data ?? execution;
1396
+ }
1397
+ if (!retained)
1398
+ this.keep(execution);
1399
+ if (execution.status === "succeeded" && file)
1400
+ return {
1401
+ id: execution.id,
1402
+ status: execution.status,
1403
+ chargedMicros: execution.chargedMicros,
1404
+ file: await this.exportFile(execution.id, execution.output, selection.path, selection.fields),
1405
+ ...this.refusal(execution),
1406
+ ...this.downloads(execution),
1407
+ };
1408
+ if (execution.status === "succeeded")
1409
+ return {
1410
+ id: execution.id,
1411
+ status: execution.status,
1412
+ chargedMicros: execution.chargedMicros,
1413
+ result: agentResultView(execution.output, selection, pick),
1414
+ ...this.refusal(execution),
1415
+ ...this.downloads(execution),
1416
+ };
1417
+ const { runId: _runId, admitted: _admitted, timedOut: _timedOut, ...view } = this.view("", execution, null, false, selection, pick);
1418
+ if (this.hasPartial(execution) && file)
1419
+ return {
1420
+ ...view,
1421
+ file: await this.exportFile(execution.id, execution.output, selection.path, selection.fields),
1422
+ };
1423
+ return timedOut ? { ...view, timedOut } : view;
1424
+ }
1425
+ }