@opengeni/codemode 0.4.25-canary.0 → 0.4.27-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -4
- package/dist/declarations.d.ts +2 -2
- package/dist/index.d.ts +31 -41
- package/dist/index.js +314 -476
- package/dist/index.js.map +1 -1
- package/dist/site.d.ts +9 -0
- package/package.json +3 -3
- package/src/declarations.ts +13 -326
- package/src/index.ts +298 -319
- package/src/site.ts +158 -0
package/src/index.ts
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import Ajv, { type ValidateFunction } from "ajv";
|
|
3
|
-
import Ajv2019 from "ajv/dist/2019.js";
|
|
4
|
-
import Ajv2020 from "ajv/dist/2020.js";
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
5
2
|
import {
|
|
6
3
|
ATTEMPT_TOOL_CATALOG_VERSION,
|
|
7
4
|
ATTEMPT_TOOL_CATALOG_MAX_BYTES,
|
|
8
5
|
AttemptToolCall,
|
|
9
6
|
AttemptToolCatalog,
|
|
10
|
-
AttemptToolCatalogEntry,
|
|
11
7
|
AttemptToolResult,
|
|
12
8
|
CodemodeCallSubmission,
|
|
13
9
|
CodemodeOperation,
|
|
14
10
|
CodemodeDispatchAck,
|
|
15
11
|
CodemodeDispatchRequest,
|
|
16
|
-
OPENGENI_API_CONTRACT_HEADER,
|
|
17
|
-
OPENGENI_API_CONTRACT_REVISION,
|
|
18
|
-
isToolResultSpilledReceipt,
|
|
19
12
|
type AttemptToolCall as AttemptToolCallValue,
|
|
20
|
-
|
|
13
|
+
AttemptToolCaller,
|
|
21
14
|
type AttemptToolCatalog as AttemptToolCatalogValue,
|
|
22
15
|
type AttemptToolCatalogEntry as AttemptToolCatalogEntryValue,
|
|
23
16
|
type AttemptToolIdentity,
|
|
@@ -26,6 +19,22 @@ import {
|
|
|
26
19
|
type CodemodeDispatchRequest as CodemodeDispatchRequestValue,
|
|
27
20
|
type CodemodeOperation as CodemodeOperationValue,
|
|
28
21
|
} from "@opengeni/contracts";
|
|
22
|
+
import {
|
|
23
|
+
ToolGateway,
|
|
24
|
+
ToolGatewayApprovalRequiredError as AttemptToolApprovalRequiredError,
|
|
25
|
+
ToolGatewayCatalogIntegrityError as AttemptToolCatalogIntegrityError,
|
|
26
|
+
ToolGatewayCatalogStaleError as AttemptToolCatalogStaleError,
|
|
27
|
+
ToolGatewayCatalogTooLargeError as AttemptToolCatalogTooLargeError,
|
|
28
|
+
ToolGatewayInputValidationError as AttemptToolInputValidationError,
|
|
29
|
+
ToolGatewayOutputValidationError as AttemptToolOutputValidationError,
|
|
30
|
+
ToolGatewayPathCollisionError as AttemptToolPathCollisionError,
|
|
31
|
+
ToolGatewayToolNotFoundError as AttemptToolNotFoundError,
|
|
32
|
+
digestCanonicalJson,
|
|
33
|
+
prepareToolGatewayDefinitions,
|
|
34
|
+
type PreparedToolGatewayCall,
|
|
35
|
+
type ToolGatewayCallLifecycle,
|
|
36
|
+
type ToolGatewayDefinition,
|
|
37
|
+
} from "@opengeni/tool-gateway";
|
|
29
38
|
|
|
30
39
|
export type { AttemptToolCatalog, AttemptToolCatalogEntry } from "@opengeni/contracts";
|
|
31
40
|
|
|
@@ -45,6 +54,8 @@ export type AttemptToolExecutionContext = {
|
|
|
45
54
|
export type AttemptToolDefinition = Omit<AttemptToolCatalogEntryValue, "codemodePath"> & {
|
|
46
55
|
/** Optional human-readable path. Unsafe/colliding segments are normalized. */
|
|
47
56
|
codemodePath?: readonly string[];
|
|
57
|
+
/** In-process execution lifecycle shared by model MCP and Codemode. */
|
|
58
|
+
lifecycle?: ToolGatewayCallLifecycle;
|
|
48
59
|
execute: (
|
|
49
60
|
args: Record<string, unknown>,
|
|
50
61
|
context: AttemptToolExecutionContext,
|
|
@@ -62,6 +73,8 @@ export type CreateAttemptToolEnvironmentInput = {
|
|
|
62
73
|
definitions: readonly AttemptToolDefinition[];
|
|
63
74
|
createdAt?: Date;
|
|
64
75
|
authorize?: AttemptToolAuthorization;
|
|
76
|
+
/** Host-only approval projection for one exact model invocation. */
|
|
77
|
+
confirmModelApproval?: (input: { modelName: string; subjectId: string }) => boolean;
|
|
65
78
|
};
|
|
66
79
|
|
|
67
80
|
export type ModelAttemptToolCall = {
|
|
@@ -73,78 +86,40 @@ export type ModelAttemptToolCall = {
|
|
|
73
86
|
signal?: AbortSignal;
|
|
74
87
|
};
|
|
75
88
|
|
|
76
|
-
export
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
readonly code = "tool_not_found";
|
|
87
|
-
|
|
88
|
-
constructor() {
|
|
89
|
-
super("Tool is not present in the active execution attempt catalog");
|
|
90
|
-
this.name = "AttemptToolNotFoundError";
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export class AttemptToolApprovalRequiredError extends Error {
|
|
95
|
-
readonly code = "approval_required";
|
|
96
|
-
|
|
97
|
-
constructor() {
|
|
98
|
-
super("Tool requires human approval and must be invoked through the agent");
|
|
99
|
-
this.name = "AttemptToolApprovalRequiredError";
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export class AttemptToolCatalogIntegrityError extends Error {
|
|
104
|
-
readonly code = "catalog_integrity_failed";
|
|
105
|
-
|
|
106
|
-
constructor() {
|
|
107
|
-
super("Attempt tool catalog digest does not match its authoritative content");
|
|
108
|
-
this.name = "AttemptToolCatalogIntegrityError";
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export class AttemptToolCatalogTooLargeError extends Error {
|
|
113
|
-
readonly code = "catalog_too_large";
|
|
114
|
-
|
|
115
|
-
constructor() {
|
|
116
|
-
super("Attempt tool catalog exceeds the maximum serialized size");
|
|
117
|
-
this.name = "AttemptToolCatalogTooLargeError";
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export class AttemptToolInputValidationError extends Error {
|
|
122
|
-
readonly code = "invalid_tool_arguments";
|
|
123
|
-
|
|
124
|
-
constructor() {
|
|
125
|
-
super("Tool arguments do not match the attempt catalog input schema");
|
|
126
|
-
this.name = "AttemptToolInputValidationError";
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export class AttemptToolOutputValidationError extends Error {
|
|
131
|
-
readonly code = "invalid_tool_result";
|
|
132
|
-
|
|
133
|
-
constructor() {
|
|
134
|
-
super("Tool result does not match the attempt catalog output schema");
|
|
135
|
-
this.name = "AttemptToolOutputValidationError";
|
|
136
|
-
}
|
|
137
|
-
}
|
|
89
|
+
export {
|
|
90
|
+
AttemptToolCatalogStaleError,
|
|
91
|
+
AttemptToolNotFoundError,
|
|
92
|
+
AttemptToolApprovalRequiredError,
|
|
93
|
+
AttemptToolCatalogIntegrityError,
|
|
94
|
+
AttemptToolCatalogTooLargeError,
|
|
95
|
+
AttemptToolInputValidationError,
|
|
96
|
+
AttemptToolOutputValidationError,
|
|
97
|
+
AttemptToolPathCollisionError,
|
|
98
|
+
};
|
|
138
99
|
|
|
139
100
|
export class CodemodeTransportError extends Error {
|
|
140
101
|
readonly code = "codemode_transport_error";
|
|
102
|
+
readonly remoteCode: string | null;
|
|
103
|
+
readonly retryable: boolean | null;
|
|
104
|
+
readonly outcomeUnknown: boolean | null;
|
|
105
|
+
readonly details: Readonly<Record<string, unknown>> | null;
|
|
141
106
|
|
|
142
107
|
constructor(
|
|
143
108
|
message: string,
|
|
144
109
|
readonly status: number | null = null,
|
|
110
|
+
options: {
|
|
111
|
+
code?: string;
|
|
112
|
+
retryable?: boolean;
|
|
113
|
+
outcomeUnknown?: boolean;
|
|
114
|
+
details?: Readonly<Record<string, unknown>>;
|
|
115
|
+
} = {},
|
|
145
116
|
) {
|
|
146
117
|
super(message);
|
|
147
118
|
this.name = "CodemodeTransportError";
|
|
119
|
+
this.remoteCode = options.code ?? null;
|
|
120
|
+
this.retryable = options.retryable ?? null;
|
|
121
|
+
this.outcomeUnknown = options.outcomeUnknown ?? null;
|
|
122
|
+
this.details = options.details ?? null;
|
|
148
123
|
}
|
|
149
124
|
}
|
|
150
125
|
|
|
@@ -171,6 +146,7 @@ export type CodemodeClientOptions = {
|
|
|
171
146
|
|
|
172
147
|
export type CodemodeCallOptions = {
|
|
173
148
|
operationId?: string;
|
|
149
|
+
/** Stops client observation only; it never cancels an already-created server operation. */
|
|
174
150
|
signal?: AbortSignal;
|
|
175
151
|
timeoutMs?: number;
|
|
176
152
|
};
|
|
@@ -248,20 +224,33 @@ export class CodemodeClient {
|
|
|
248
224
|
argumentsValue: Record<string, unknown> = {},
|
|
249
225
|
options: CodemodeCallOptions = {},
|
|
250
226
|
): Promise<AttemptToolResultValue> {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
227
|
+
return (
|
|
228
|
+
await this.callResolved(
|
|
229
|
+
(catalog) =>
|
|
230
|
+
catalog.entries.find(
|
|
231
|
+
(entry) =>
|
|
232
|
+
entry.identity.serverId === identity.serverId &&
|
|
233
|
+
entry.identity.toolName === identity.toolName,
|
|
234
|
+
) ?? null,
|
|
235
|
+
argumentsValue,
|
|
236
|
+
options,
|
|
257
237
|
)
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
|
|
238
|
+
).result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async callResolved(
|
|
242
|
+
resolveEntry: (catalog: AttemptToolCatalogValue) => AttemptToolCatalogEntryValue | null,
|
|
243
|
+
argumentsValue: Record<string, unknown>,
|
|
244
|
+
options: CodemodeCallOptions,
|
|
245
|
+
): Promise<{ result: AttemptToolResultValue; entry: AttemptToolCatalogEntryValue }> {
|
|
246
|
+
let catalog = await this.catalog(options.signal ? { signal: options.signal } : {});
|
|
247
|
+
let entry = resolveEntry(catalog);
|
|
248
|
+
if (!entry) throw new AttemptToolNotFoundError();
|
|
261
249
|
const operationId = options.operationId ?? randomUUID();
|
|
262
250
|
const deadline =
|
|
263
251
|
Date.now() + boundedPositiveInteger(options.timeoutMs ?? this.timeoutMs, 1_000, 60 * 60_000);
|
|
264
252
|
let submitted = false;
|
|
253
|
+
let staleRefreshAttempted = false;
|
|
265
254
|
let operation: CodemodeOperationValue | null = null;
|
|
266
255
|
let nextNotifyAt = 0;
|
|
267
256
|
while (true) {
|
|
@@ -272,7 +261,9 @@ export class CodemodeClient {
|
|
|
272
261
|
);
|
|
273
262
|
}
|
|
274
263
|
const shouldNotify =
|
|
275
|
-
!submitted ||
|
|
264
|
+
!submitted ||
|
|
265
|
+
((operation?.state === "queued" || operation?.state === "running") &&
|
|
266
|
+
Date.now() >= nextNotifyAt);
|
|
276
267
|
if (shouldNotify) {
|
|
277
268
|
submitted = true;
|
|
278
269
|
nextNotifyAt = Date.now() + 2_000;
|
|
@@ -280,25 +271,65 @@ export class CodemodeClient {
|
|
|
280
271
|
operation = await this.submit(
|
|
281
272
|
operationId,
|
|
282
273
|
catalog.digest,
|
|
283
|
-
identity,
|
|
274
|
+
entry.identity,
|
|
284
275
|
argumentsValue,
|
|
285
276
|
options.signal,
|
|
286
277
|
);
|
|
287
278
|
} catch (error) {
|
|
279
|
+
if (
|
|
280
|
+
operation === null &&
|
|
281
|
+
!staleRefreshAttempted &&
|
|
282
|
+
error instanceof CodemodeTransportError &&
|
|
283
|
+
error.remoteCode === "codemode_catalog_stale"
|
|
284
|
+
) {
|
|
285
|
+
staleRefreshAttempted = true;
|
|
286
|
+
catalog = await this.catalog({
|
|
287
|
+
refresh: true,
|
|
288
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
289
|
+
});
|
|
290
|
+
entry = resolveEntry(catalog);
|
|
291
|
+
if (!entry) throw new AttemptToolNotFoundError();
|
|
292
|
+
submitted = false;
|
|
293
|
+
nextNotifyAt = 0;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (options.signal?.aborted) throw error;
|
|
297
|
+
if (operation === null && !canReconcileCodemodeSubmission(error)) throw error;
|
|
288
298
|
// The POST may have committed before its response was lost, or an
|
|
289
|
-
//
|
|
290
|
-
// caller-owned id is the
|
|
291
|
-
//
|
|
299
|
+
// already-bound operation may have settled while a later wake
|
|
300
|
+
// notification failed deterministically. The caller-owned id is the
|
|
301
|
+
// recovery handle: read and re-prove the exact binding before
|
|
302
|
+
// deciding that another side effect is necessary.
|
|
303
|
+
let recovered: CodemodeOperationValue;
|
|
292
304
|
try {
|
|
293
|
-
|
|
294
|
-
} catch {
|
|
295
|
-
throw
|
|
305
|
+
recovered = await this.read(operationId, options.signal);
|
|
306
|
+
} catch (recoveryError) {
|
|
307
|
+
if (options.signal?.aborted) throw recoveryError;
|
|
308
|
+
throw new CodemodeTransportError(
|
|
309
|
+
`Codemode operation ${operationId} could not be reconciled after its submission response failed`,
|
|
310
|
+
null,
|
|
311
|
+
{
|
|
312
|
+
code: "codemode_operation_recovery_unavailable",
|
|
313
|
+
retryable: true,
|
|
314
|
+
outcomeUnknown: true,
|
|
315
|
+
details: { operationId },
|
|
316
|
+
},
|
|
317
|
+
);
|
|
296
318
|
}
|
|
319
|
+
assertRecoveredCodemodeOperation(recovered, {
|
|
320
|
+
operationId,
|
|
321
|
+
catalog,
|
|
322
|
+
identity: entry.identity,
|
|
323
|
+
arguments: argumentsValue,
|
|
324
|
+
});
|
|
325
|
+
operation = recovered;
|
|
297
326
|
}
|
|
298
327
|
} else {
|
|
299
328
|
operation = await this.read(operationId, options.signal);
|
|
300
329
|
}
|
|
301
|
-
if (operation.state === "completed")
|
|
330
|
+
if (operation.state === "completed") {
|
|
331
|
+
return { result: AttemptToolResult.parse(operation.result), entry };
|
|
332
|
+
}
|
|
302
333
|
if (["failed", "outcome_unknown", "cancelled"].includes(operation.state)) {
|
|
303
334
|
throw new CodemodeOperationError(
|
|
304
335
|
operation,
|
|
@@ -318,14 +349,13 @@ export class CodemodeClient {
|
|
|
318
349
|
if (path.length < 2 || path.some((segment) => segment.length === 0)) {
|
|
319
350
|
throw new AttemptToolNotFoundError();
|
|
320
351
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return await this.call(matches[0]!.identity, argumentsValue, options);
|
|
352
|
+
return (
|
|
353
|
+
await this.callResolved(
|
|
354
|
+
(catalog) => catalogEntryForPath(catalog, path),
|
|
355
|
+
argumentsValue,
|
|
356
|
+
options,
|
|
357
|
+
)
|
|
358
|
+
).result;
|
|
329
359
|
}
|
|
330
360
|
|
|
331
361
|
/** Return structured content when the catalog declares it; otherwise retain the full MCP result. */
|
|
@@ -337,15 +367,11 @@ export class CodemodeClient {
|
|
|
337
367
|
if (path.length < 2 || path.some((segment) => segment.length === 0)) {
|
|
338
368
|
throw new AttemptToolNotFoundError();
|
|
339
369
|
}
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
entry.codemodePath.every((segment, index) => segment === path[index]),
|
|
370
|
+
const { result, entry } = await this.callResolved(
|
|
371
|
+
(catalog) => catalogEntryForPath(catalog, path),
|
|
372
|
+
argumentsValue,
|
|
373
|
+
options,
|
|
345
374
|
);
|
|
346
|
-
if (matches.length !== 1) throw new AttemptToolNotFoundError();
|
|
347
|
-
const entry = matches[0]!;
|
|
348
|
-
const result = await this.call(entry.identity, argumentsValue, options);
|
|
349
375
|
if (!entry.outputSchema) return result;
|
|
350
376
|
if (result.isError) throw new CodemodeToolCallError(result);
|
|
351
377
|
if (!result.structuredContent) {
|
|
@@ -385,33 +411,89 @@ export class CodemodeClient {
|
|
|
385
411
|
return CodemodeOperation.parse(await response.json());
|
|
386
412
|
}
|
|
387
413
|
|
|
388
|
-
|
|
414
|
+
/** Server-side Site preview forwarding. The attempt bearer never enters the page. */
|
|
415
|
+
async sessionRequest(path: string, init: RequestInit): Promise<Response> {
|
|
416
|
+
if (!path.startsWith("/v1/")) {
|
|
417
|
+
throw new Error("Unsupported Site session API path");
|
|
418
|
+
}
|
|
419
|
+
return this.request(`/sdk${path}`, init, false);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private async request(path: string, init: RequestInit, throwOnError = true): Promise<Response> {
|
|
389
423
|
const token =
|
|
390
424
|
typeof this.options.token === "function" ? await this.options.token() : this.options.token;
|
|
391
425
|
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
392
426
|
...init,
|
|
393
427
|
headers: {
|
|
394
428
|
...Object.fromEntries(new Headers(init.headers).entries()),
|
|
395
|
-
[OPENGENI_API_CONTRACT_HEADER]: OPENGENI_API_CONTRACT_REVISION,
|
|
396
429
|
authorization: `Bearer ${token}`,
|
|
397
430
|
},
|
|
398
431
|
});
|
|
399
|
-
if (!response.ok) {
|
|
432
|
+
if (!response.ok && throwOnError) {
|
|
400
433
|
let message = `Codemode request failed with HTTP ${response.status}`;
|
|
434
|
+
let errorOptions: {
|
|
435
|
+
code?: string;
|
|
436
|
+
retryable?: boolean;
|
|
437
|
+
outcomeUnknown?: boolean;
|
|
438
|
+
details?: Readonly<Record<string, unknown>>;
|
|
439
|
+
} = {};
|
|
401
440
|
try {
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
441
|
+
const error = parseCodemodeApiError(await response.json());
|
|
442
|
+
if (error?.message) message = error.message;
|
|
443
|
+
if (error) {
|
|
444
|
+
errorOptions = {
|
|
445
|
+
...(error.code ? { code: error.code } : {}),
|
|
446
|
+
...(error.retryable === undefined ? {} : { retryable: error.retryable }),
|
|
447
|
+
...(error.outcomeUnknown === undefined ? {} : { outcomeUnknown: error.outcomeUnknown }),
|
|
448
|
+
...(error.details ? { details: error.details } : {}),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
406
451
|
} catch {
|
|
407
452
|
// The status is sufficient; never echo an unbounded provider body.
|
|
408
453
|
}
|
|
409
|
-
throw new CodemodeTransportError(message, response.status);
|
|
454
|
+
throw new CodemodeTransportError(message, response.status, errorOptions);
|
|
410
455
|
}
|
|
411
456
|
return response;
|
|
412
457
|
}
|
|
413
458
|
}
|
|
414
459
|
|
|
460
|
+
function canReconcileCodemodeSubmission(error: unknown): boolean {
|
|
461
|
+
return !(error instanceof CodemodeTransportError) || error.outcomeUnknown === true;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function assertRecoveredCodemodeOperation(
|
|
465
|
+
operation: CodemodeOperationValue,
|
|
466
|
+
expected: {
|
|
467
|
+
operationId: string;
|
|
468
|
+
catalog: AttemptToolCatalogValue;
|
|
469
|
+
identity: AttemptToolIdentity;
|
|
470
|
+
arguments: Record<string, unknown>;
|
|
471
|
+
},
|
|
472
|
+
): void {
|
|
473
|
+
const matches =
|
|
474
|
+
operation.operationId === expected.operationId &&
|
|
475
|
+
operation.accountId === expected.catalog.accountId &&
|
|
476
|
+
operation.workspaceId === expected.catalog.workspaceId &&
|
|
477
|
+
operation.sessionId === expected.catalog.sessionId &&
|
|
478
|
+
operation.turnId === expected.catalog.turnId &&
|
|
479
|
+
operation.attemptId === expected.catalog.attemptId &&
|
|
480
|
+
operation.executionGeneration === expected.catalog.executionGeneration &&
|
|
481
|
+
operation.catalogDigest === expected.catalog.digest &&
|
|
482
|
+
operation.identity.serverId === expected.identity.serverId &&
|
|
483
|
+
operation.identity.toolName === expected.identity.toolName &&
|
|
484
|
+
digestCanonicalJson(operation.arguments) === digestCanonicalJson(expected.arguments);
|
|
485
|
+
if (matches) return;
|
|
486
|
+
throw new CodemodeTransportError(
|
|
487
|
+
"Codemode operation id is already bound to a different request",
|
|
488
|
+
409,
|
|
489
|
+
{
|
|
490
|
+
code: "codemode_operation_conflict",
|
|
491
|
+
retryable: false,
|
|
492
|
+
outcomeUnknown: false,
|
|
493
|
+
},
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
415
497
|
export function compileCodemodeTools(
|
|
416
498
|
catalog: AttemptToolCatalogValue,
|
|
417
499
|
client: CodemodeClient,
|
|
@@ -438,29 +520,11 @@ export function compileCodemodeTools(
|
|
|
438
520
|
return root;
|
|
439
521
|
}
|
|
440
522
|
|
|
441
|
-
type CompiledDefinition = {
|
|
442
|
-
entry: AttemptToolCatalogEntryValue;
|
|
443
|
-
execute: AttemptToolDefinition["execute"];
|
|
444
|
-
validateInput: ValidateFunction<unknown>;
|
|
445
|
-
validateOutput: ValidateFunction<unknown> | null;
|
|
446
|
-
};
|
|
447
|
-
|
|
448
523
|
export class AttemptToolEnvironment {
|
|
449
|
-
readonly catalog: AttemptToolCatalogValue;
|
|
450
|
-
private readonly byIdentity = new Map<string, CompiledDefinition>();
|
|
451
|
-
private readonly byModelName = new Map<string, CompiledDefinition>();
|
|
452
|
-
|
|
453
524
|
constructor(
|
|
454
|
-
catalog: AttemptToolCatalogValue,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
) {
|
|
458
|
-
this.catalog = catalog;
|
|
459
|
-
for (const definition of definitions) {
|
|
460
|
-
this.byIdentity.set(identityKey(definition.entry.identity), definition);
|
|
461
|
-
this.byModelName.set(definition.entry.modelName, definition);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
525
|
+
readonly catalog: AttemptToolCatalogValue,
|
|
526
|
+
private readonly gateway: ToolGateway,
|
|
527
|
+
) {}
|
|
464
528
|
|
|
465
529
|
async call(
|
|
466
530
|
input: AttemptToolCallValue,
|
|
@@ -469,58 +533,22 @@ export class AttemptToolEnvironment {
|
|
|
469
533
|
signal?: AbortSignal;
|
|
470
534
|
} = {},
|
|
471
535
|
): Promise<AttemptToolResultValue> {
|
|
536
|
+
return await (await this.prepareCall(input, context)).execute();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async prepareCall(
|
|
540
|
+
input: AttemptToolCallValue,
|
|
541
|
+
context: {
|
|
542
|
+
transportMeta?: Record<string, unknown> | null;
|
|
543
|
+
signal?: AbortSignal;
|
|
544
|
+
} = {},
|
|
545
|
+
): Promise<PreparedToolGatewayCall> {
|
|
472
546
|
const call = AttemptToolCall.parse(input);
|
|
473
|
-
|
|
474
|
-
throw new AttemptToolCatalogStaleError();
|
|
475
|
-
}
|
|
476
|
-
const definition = this.byIdentity.get(identityKey(call.identity));
|
|
477
|
-
if (!definition) {
|
|
478
|
-
throw new AttemptToolNotFoundError();
|
|
479
|
-
}
|
|
480
|
-
if (call.caller.kind === "codemode" && definition.entry.approval === "human") {
|
|
481
|
-
throw new AttemptToolApprovalRequiredError();
|
|
482
|
-
}
|
|
483
|
-
if (!definition.validateInput(call.arguments)) {
|
|
484
|
-
throw new AttemptToolInputValidationError();
|
|
485
|
-
}
|
|
486
|
-
await this.authorize?.({ call, entry: definition.entry });
|
|
487
|
-
const result = AttemptToolResult.parse(
|
|
488
|
-
await definition.execute(call.arguments, {
|
|
489
|
-
operationId: call.operationId,
|
|
490
|
-
caller: call.caller,
|
|
491
|
-
...(context.transportMeta === undefined ? {} : { transportMeta: context.transportMeta }),
|
|
492
|
-
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
493
|
-
}),
|
|
494
|
-
);
|
|
495
|
-
if (!result.isError && definition.validateOutput) {
|
|
496
|
-
const outputMatchesSchema =
|
|
497
|
-
result.structuredContent !== undefined &&
|
|
498
|
-
definition.validateOutput(result.structuredContent);
|
|
499
|
-
// Model overflow replaces the exact tool payload with a compact File
|
|
500
|
-
// receipt after execute. That handle is not the catalog output schema.
|
|
501
|
-
if (!outputMatchesSchema && !isToolResultSpilledReceipt(result.structuredContent)) {
|
|
502
|
-
throw new AttemptToolOutputValidationError();
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return result;
|
|
547
|
+
return await this.gateway.prepareCall(call, context);
|
|
506
548
|
}
|
|
507
549
|
|
|
508
550
|
async callModel(input: ModelAttemptToolCall): Promise<AttemptToolResultValue> {
|
|
509
|
-
|
|
510
|
-
if (!definition) {
|
|
511
|
-
throw new AttemptToolNotFoundError();
|
|
512
|
-
}
|
|
513
|
-
const call = AttemptToolCall.parse({
|
|
514
|
-
operationId: input.operationId ?? randomUUID(),
|
|
515
|
-
catalogDigest: this.catalog.digest,
|
|
516
|
-
identity: definition.entry.identity,
|
|
517
|
-
arguments: input.arguments,
|
|
518
|
-
caller: { kind: "model", subjectId: input.subjectId },
|
|
519
|
-
});
|
|
520
|
-
return await this.call(call, {
|
|
521
|
-
...(input.transportMeta === undefined ? {} : { transportMeta: input.transportMeta }),
|
|
522
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
523
|
-
});
|
|
551
|
+
return await this.gateway.callModel(input);
|
|
524
552
|
}
|
|
525
553
|
}
|
|
526
554
|
|
|
@@ -528,36 +556,55 @@ export function createAttemptToolEnvironment(
|
|
|
528
556
|
input: CreateAttemptToolEnvironmentInput,
|
|
529
557
|
): AttemptToolEnvironment {
|
|
530
558
|
const createdAt = (input.createdAt ?? new Date()).toISOString();
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
validateOutput: entry.outputSchema
|
|
544
|
-
? compileCatalogSchema(schemaValidators, entry.outputSchema)
|
|
545
|
-
: null,
|
|
546
|
-
};
|
|
547
|
-
});
|
|
559
|
+
const prepared = prepareToolGatewayDefinitions(
|
|
560
|
+
input.definitions.map(
|
|
561
|
+
(definition): ToolGatewayDefinition => ({
|
|
562
|
+
...definition,
|
|
563
|
+
execute: async (argumentsValue, context) =>
|
|
564
|
+
await definition.execute(argumentsValue, {
|
|
565
|
+
...context,
|
|
566
|
+
caller: AttemptToolCaller.parse(context.caller),
|
|
567
|
+
}),
|
|
568
|
+
}),
|
|
569
|
+
),
|
|
570
|
+
);
|
|
548
571
|
const unsigned = {
|
|
549
572
|
version: ATTEMPT_TOOL_CATALOG_VERSION,
|
|
550
573
|
...input.scope,
|
|
551
574
|
generation: input.generation,
|
|
552
575
|
createdAt,
|
|
553
|
-
entries:
|
|
576
|
+
entries: [...prepared.entries],
|
|
554
577
|
};
|
|
555
578
|
const catalog = AttemptToolCatalog.parse({
|
|
556
579
|
...unsigned,
|
|
557
580
|
digest: digestAttemptToolCatalog(unsigned),
|
|
558
581
|
});
|
|
559
582
|
assertCatalogSize(catalog);
|
|
560
|
-
return new AttemptToolEnvironment(
|
|
583
|
+
return new AttemptToolEnvironment(
|
|
584
|
+
catalog,
|
|
585
|
+
prepared.create({
|
|
586
|
+
catalogDigest: catalog.digest,
|
|
587
|
+
requireApproval: (entry, caller) => caller.kind === "codemode" && entry.approval === "human",
|
|
588
|
+
...(input.confirmModelApproval
|
|
589
|
+
? {
|
|
590
|
+
confirmModelApproval: ({ entry, subjectId }) =>
|
|
591
|
+
input.confirmModelApproval!({
|
|
592
|
+
modelName: entry.modelName,
|
|
593
|
+
subjectId,
|
|
594
|
+
}),
|
|
595
|
+
}
|
|
596
|
+
: {}),
|
|
597
|
+
...(input.authorize
|
|
598
|
+
? {
|
|
599
|
+
authorize: async ({ call, entry }) =>
|
|
600
|
+
await input.authorize!({
|
|
601
|
+
call: AttemptToolCall.parse(call),
|
|
602
|
+
entry,
|
|
603
|
+
}),
|
|
604
|
+
}
|
|
605
|
+
: {}),
|
|
606
|
+
}),
|
|
607
|
+
);
|
|
561
608
|
}
|
|
562
609
|
|
|
563
610
|
export function digestAttemptToolCatalog(catalog: Omit<AttemptToolCatalogValue, "digest">): string {
|
|
@@ -612,124 +659,6 @@ function assertCatalogSize(catalog: AttemptToolCatalogValue): void {
|
|
|
612
659
|
}
|
|
613
660
|
}
|
|
614
661
|
|
|
615
|
-
type SchemaCompiler = { compile(schema: object): ValidateFunction<unknown> };
|
|
616
|
-
|
|
617
|
-
// Validator compilation is structural and independent of attempt identity,
|
|
618
|
-
// executable closures, credentials, and authorization. Reuse only exact
|
|
619
|
-
// content-addressed validators; the attempt environment and catalog remain
|
|
620
|
-
// freshly bound and digested on every execution. The hard cap prevents an
|
|
621
|
-
// untrusted MCP schema stream from turning this process cache into a memory
|
|
622
|
-
// sink.
|
|
623
|
-
const COMPILED_CATALOG_SCHEMA_CACHE_MAX_ENTRIES = 512;
|
|
624
|
-
const compiledCatalogSchemaCache = new Map<string, ValidateFunction<unknown>>();
|
|
625
|
-
|
|
626
|
-
function createSchemaValidators(): {
|
|
627
|
-
draft7: SchemaCompiler;
|
|
628
|
-
draft2019: SchemaCompiler;
|
|
629
|
-
draft2020: SchemaCompiler;
|
|
630
|
-
} {
|
|
631
|
-
const options = {
|
|
632
|
-
allErrors: false,
|
|
633
|
-
coerceTypes: false,
|
|
634
|
-
strict: false,
|
|
635
|
-
useDefaults: false,
|
|
636
|
-
validateFormats: false,
|
|
637
|
-
} as const;
|
|
638
|
-
return {
|
|
639
|
-
draft7: new Ajv(options),
|
|
640
|
-
draft2019: new Ajv2019(options),
|
|
641
|
-
draft2020: new Ajv2020(options),
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function compileCatalogSchema(
|
|
646
|
-
validators: ReturnType<typeof createSchemaValidators>,
|
|
647
|
-
schema: AttemptToolCatalogEntryValue["inputSchema"],
|
|
648
|
-
): ValidateFunction<unknown> {
|
|
649
|
-
const dialect = typeof schema.$schema === "string" ? schema.$schema : "";
|
|
650
|
-
const family = dialect.includes("2020-12")
|
|
651
|
-
? "2020-12"
|
|
652
|
-
: dialect.includes("2019-09")
|
|
653
|
-
? "2019-09"
|
|
654
|
-
: "draft7";
|
|
655
|
-
const cacheKey = `${family}:${digestCanonicalJson(schema)}`;
|
|
656
|
-
const cached = compiledCatalogSchemaCache.get(cacheKey);
|
|
657
|
-
if (cached) {
|
|
658
|
-
compiledCatalogSchemaCache.delete(cacheKey);
|
|
659
|
-
compiledCatalogSchemaCache.set(cacheKey, cached);
|
|
660
|
-
return cached;
|
|
661
|
-
}
|
|
662
|
-
const compiled =
|
|
663
|
-
family === "2020-12"
|
|
664
|
-
? validators.draft2020.compile(schema)
|
|
665
|
-
: family === "2019-09"
|
|
666
|
-
? validators.draft2019.compile(schema)
|
|
667
|
-
: validators.draft7.compile(schema);
|
|
668
|
-
while (compiledCatalogSchemaCache.size >= COMPILED_CATALOG_SCHEMA_CACHE_MAX_ENTRIES) {
|
|
669
|
-
const oldest = compiledCatalogSchemaCache.keys().next().value;
|
|
670
|
-
if (oldest === undefined) break;
|
|
671
|
-
compiledCatalogSchemaCache.delete(oldest);
|
|
672
|
-
}
|
|
673
|
-
compiledCatalogSchemaCache.set(cacheKey, compiled);
|
|
674
|
-
return compiled;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function allocateCodemodePaths(definitions: readonly AttemptToolDefinition[]): string[][] {
|
|
678
|
-
const bases = definitions.map((definition) =>
|
|
679
|
-
(definition.codemodePath?.length
|
|
680
|
-
? definition.codemodePath
|
|
681
|
-
: [definition.identity.serverId, definition.identity.toolName]
|
|
682
|
-
).map(safeNamespaceSegment),
|
|
683
|
-
);
|
|
684
|
-
const counts = new Map<string, number>();
|
|
685
|
-
for (const path of bases) {
|
|
686
|
-
const key = path.join("\u0000");
|
|
687
|
-
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
688
|
-
}
|
|
689
|
-
return bases.map((base, index) => {
|
|
690
|
-
const key = base.join("\u0000");
|
|
691
|
-
if (counts.get(key) === 1) return base;
|
|
692
|
-
const suffix = `_${shortIdentityDigest(definitions[index]!.identity)}`;
|
|
693
|
-
const last = base.at(-1)!;
|
|
694
|
-
return [...base.slice(0, -1), `${last.slice(0, 128 - suffix.length)}${suffix}`];
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function safeNamespaceSegment(value: string): string {
|
|
699
|
-
let normalized = value.replace(/[^A-Za-z0-9_$]/gu, "_");
|
|
700
|
-
if (!/^[A-Za-z_$]/u.test(normalized)) normalized = `_${normalized}`;
|
|
701
|
-
if (["__proto__", "prototype", "constructor"].includes(normalized)) {
|
|
702
|
-
normalized = `_${normalized}`;
|
|
703
|
-
}
|
|
704
|
-
return normalized.slice(0, 128) || "_";
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function shortIdentityDigest(identity: AttemptToolIdentity): string {
|
|
708
|
-
return createHash("sha256").update(identityKey(identity), "utf8").digest("hex").slice(0, 10);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function identityKey(identity: AttemptToolIdentity): string {
|
|
712
|
-
return `${identity.serverId}\u0000${identity.toolName}`;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
function digestCanonicalJson(value: unknown): string {
|
|
716
|
-
return createHash("sha256")
|
|
717
|
-
.update(JSON.stringify(canonicalJsonValue(value)), "utf8")
|
|
718
|
-
.digest("hex");
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
function canonicalJsonValue(value: unknown): unknown {
|
|
722
|
-
if (Array.isArray(value)) return value.map(canonicalJsonValue);
|
|
723
|
-
if (value !== null && typeof value === "object") {
|
|
724
|
-
return Object.fromEntries(
|
|
725
|
-
Object.entries(value as Record<string, unknown>)
|
|
726
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
727
|
-
.map(([key, entry]) => [key, canonicalJsonValue(entry)]),
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
return value;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
662
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
734
663
|
|
|
735
664
|
function boundedPositiveInteger(value: number, minimum: number, maximum: number): number {
|
|
@@ -757,6 +686,55 @@ function structuredToolError(result: AttemptToolResultValue): {
|
|
|
757
686
|
};
|
|
758
687
|
}
|
|
759
688
|
|
|
689
|
+
function catalogEntryForPath(
|
|
690
|
+
catalog: AttemptToolCatalogValue,
|
|
691
|
+
path: readonly string[],
|
|
692
|
+
): AttemptToolCatalogEntryValue | null {
|
|
693
|
+
const matches = catalog.entries.filter(
|
|
694
|
+
(entry) =>
|
|
695
|
+
entry.codemodePath.length === path.length &&
|
|
696
|
+
entry.codemodePath.every((segment, index) => segment === path[index]),
|
|
697
|
+
);
|
|
698
|
+
return matches.length === 1 ? matches[0]! : null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function parseCodemodeApiError(input: unknown): {
|
|
702
|
+
message?: string;
|
|
703
|
+
code?: string;
|
|
704
|
+
retryable?: boolean;
|
|
705
|
+
outcomeUnknown?: boolean;
|
|
706
|
+
details?: Readonly<Record<string, unknown>>;
|
|
707
|
+
} | null {
|
|
708
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return null;
|
|
709
|
+
const root = input as Record<string, unknown>;
|
|
710
|
+
const nested =
|
|
711
|
+
root.error && typeof root.error === "object" && !Array.isArray(root.error)
|
|
712
|
+
? (root.error as Record<string, unknown>)
|
|
713
|
+
: root;
|
|
714
|
+
const details =
|
|
715
|
+
nested.details && typeof nested.details === "object" && !Array.isArray(nested.details)
|
|
716
|
+
? (nested.details as Readonly<Record<string, unknown>>)
|
|
717
|
+
: undefined;
|
|
718
|
+
const detailCode = details?.code;
|
|
719
|
+
const code =
|
|
720
|
+
typeof detailCode === "string" && /^[a-z0-9_]{1,128}$/u.test(detailCode)
|
|
721
|
+
? detailCode
|
|
722
|
+
: undefined;
|
|
723
|
+
const message = typeof nested.message === "string" ? nested.message : undefined;
|
|
724
|
+
const retryable = typeof nested.retryable === "boolean" ? nested.retryable : undefined;
|
|
725
|
+
const outcomeUnknown =
|
|
726
|
+
typeof nested.outcomeUnknown === "boolean" ? nested.outcomeUnknown : undefined;
|
|
727
|
+
return message || code || retryable !== undefined || outcomeUnknown !== undefined || details
|
|
728
|
+
? {
|
|
729
|
+
...(message ? { message } : {}),
|
|
730
|
+
...(code ? { code } : {}),
|
|
731
|
+
...(retryable === undefined ? {} : { retryable }),
|
|
732
|
+
...(outcomeUnknown === undefined ? {} : { outcomeUnknown }),
|
|
733
|
+
...(details ? { details } : {}),
|
|
734
|
+
}
|
|
735
|
+
: null;
|
|
736
|
+
}
|
|
737
|
+
|
|
760
738
|
async function abortableDelay(delayMs: number, signal?: AbortSignal): Promise<void> {
|
|
761
739
|
if (!signal) {
|
|
762
740
|
await new Promise<void>((resolve) => setTimeout(resolve, delayMs));
|
|
@@ -781,3 +759,4 @@ export * from "./interaction";
|
|
|
781
759
|
export * from "./artifacts";
|
|
782
760
|
export * from "./structured";
|
|
783
761
|
export * from "./declarations";
|
|
762
|
+
export * from "./site";
|