@opengeni/codemode 0.2.2
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/LICENSE +190 -0
- package/README.md +71 -0
- package/dist/artifacts.d.ts +99 -0
- package/dist/declarations.d.ts +12 -0
- package/dist/environment.d.ts +28 -0
- package/dist/index.d.ts +161 -0
- package/dist/index.js +1543 -0
- package/dist/index.js.map +1 -0
- package/dist/interaction.d.ts +1529 -0
- package/dist/structured.d.ts +11 -0
- package/package.json +43 -0
- package/src/artifacts.ts +313 -0
- package/src/declarations.ts +345 -0
- package/src/environment.ts +101 -0
- package/src/index.ts +736 -0
- package/src/interaction.ts +726 -0
- package/src/structured.ts +52 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import Ajv, { type ValidateFunction } from "ajv";
|
|
3
|
+
import Ajv2019 from "ajv/dist/2019.js";
|
|
4
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
5
|
+
import {
|
|
6
|
+
ATTEMPT_TOOL_CATALOG_VERSION,
|
|
7
|
+
ATTEMPT_TOOL_CATALOG_MAX_BYTES,
|
|
8
|
+
AttemptToolCall,
|
|
9
|
+
AttemptToolCatalog,
|
|
10
|
+
AttemptToolCatalogEntry,
|
|
11
|
+
AttemptToolResult,
|
|
12
|
+
CodemodeCallSubmission,
|
|
13
|
+
CodemodeOperation,
|
|
14
|
+
CodemodeDispatchAck,
|
|
15
|
+
CodemodeDispatchRequest,
|
|
16
|
+
type AttemptToolCall as AttemptToolCallValue,
|
|
17
|
+
type AttemptToolCaller,
|
|
18
|
+
type AttemptToolCatalog as AttemptToolCatalogValue,
|
|
19
|
+
type AttemptToolCatalogEntry as AttemptToolCatalogEntryValue,
|
|
20
|
+
type AttemptToolIdentity,
|
|
21
|
+
type AttemptToolResult as AttemptToolResultValue,
|
|
22
|
+
type CodemodeDispatchAck as CodemodeDispatchAckValue,
|
|
23
|
+
type CodemodeDispatchRequest as CodemodeDispatchRequestValue,
|
|
24
|
+
type CodemodeOperation as CodemodeOperationValue,
|
|
25
|
+
} from "@opengeni/contracts";
|
|
26
|
+
|
|
27
|
+
export type { AttemptToolCatalog, AttemptToolCatalogEntry } from "@opengeni/contracts";
|
|
28
|
+
|
|
29
|
+
export type AttemptToolScope = Pick<
|
|
30
|
+
AttemptToolCatalogValue,
|
|
31
|
+
"accountId" | "workspaceId" | "sessionId" | "turnId" | "attemptId" | "executionGeneration"
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
export type AttemptToolExecutionContext = {
|
|
35
|
+
operationId: string;
|
|
36
|
+
caller: AttemptToolCaller;
|
|
37
|
+
/** In-process transport metadata; never part of catalog identity or digest. */
|
|
38
|
+
transportMeta?: Record<string, unknown> | null;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type AttemptToolDefinition = Omit<AttemptToolCatalogEntryValue, "codemodePath"> & {
|
|
43
|
+
/** Optional human-readable path. Unsafe/colliding segments are normalized. */
|
|
44
|
+
codemodePath?: readonly string[];
|
|
45
|
+
execute: (
|
|
46
|
+
args: Record<string, unknown>,
|
|
47
|
+
context: AttemptToolExecutionContext,
|
|
48
|
+
) => Promise<AttemptToolResultValue> | AttemptToolResultValue;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type AttemptToolAuthorization = (input: {
|
|
52
|
+
call: AttemptToolCallValue;
|
|
53
|
+
entry: AttemptToolCatalogEntryValue;
|
|
54
|
+
}) => Promise<void> | void;
|
|
55
|
+
|
|
56
|
+
export type CreateAttemptToolEnvironmentInput = {
|
|
57
|
+
scope: AttemptToolScope;
|
|
58
|
+
generation: number;
|
|
59
|
+
definitions: readonly AttemptToolDefinition[];
|
|
60
|
+
createdAt?: Date;
|
|
61
|
+
authorize?: AttemptToolAuthorization;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type ModelAttemptToolCall = {
|
|
65
|
+
operationId?: string;
|
|
66
|
+
modelName: string;
|
|
67
|
+
arguments: Record<string, unknown>;
|
|
68
|
+
subjectId: string;
|
|
69
|
+
transportMeta?: Record<string, unknown> | null;
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export class AttemptToolCatalogStaleError extends Error {
|
|
74
|
+
readonly code = "catalog_stale";
|
|
75
|
+
|
|
76
|
+
constructor() {
|
|
77
|
+
super("Codemode catalog is stale for the active execution attempt");
|
|
78
|
+
this.name = "AttemptToolCatalogStaleError";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class AttemptToolNotFoundError extends Error {
|
|
83
|
+
readonly code = "tool_not_found";
|
|
84
|
+
|
|
85
|
+
constructor() {
|
|
86
|
+
super("Tool is not present in the active execution attempt catalog");
|
|
87
|
+
this.name = "AttemptToolNotFoundError";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class AttemptToolApprovalRequiredError extends Error {
|
|
92
|
+
readonly code = "approval_required";
|
|
93
|
+
|
|
94
|
+
constructor() {
|
|
95
|
+
super("Tool requires human approval and must be invoked through the agent");
|
|
96
|
+
this.name = "AttemptToolApprovalRequiredError";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class AttemptToolCatalogIntegrityError extends Error {
|
|
101
|
+
readonly code = "catalog_integrity_failed";
|
|
102
|
+
|
|
103
|
+
constructor() {
|
|
104
|
+
super("Attempt tool catalog digest does not match its authoritative content");
|
|
105
|
+
this.name = "AttemptToolCatalogIntegrityError";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class AttemptToolCatalogTooLargeError extends Error {
|
|
110
|
+
readonly code = "catalog_too_large";
|
|
111
|
+
|
|
112
|
+
constructor() {
|
|
113
|
+
super("Attempt tool catalog exceeds the maximum serialized size");
|
|
114
|
+
this.name = "AttemptToolCatalogTooLargeError";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export class AttemptToolInputValidationError extends Error {
|
|
119
|
+
readonly code = "invalid_tool_arguments";
|
|
120
|
+
|
|
121
|
+
constructor() {
|
|
122
|
+
super("Tool arguments do not match the attempt catalog input schema");
|
|
123
|
+
this.name = "AttemptToolInputValidationError";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class AttemptToolOutputValidationError extends Error {
|
|
128
|
+
readonly code = "invalid_tool_result";
|
|
129
|
+
|
|
130
|
+
constructor() {
|
|
131
|
+
super("Tool result does not match the attempt catalog output schema");
|
|
132
|
+
this.name = "AttemptToolOutputValidationError";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class CodemodeTransportError extends Error {
|
|
137
|
+
readonly code = "codemode_transport_error";
|
|
138
|
+
|
|
139
|
+
constructor(
|
|
140
|
+
message: string,
|
|
141
|
+
readonly status: number | null = null,
|
|
142
|
+
) {
|
|
143
|
+
super(message);
|
|
144
|
+
this.name = "CodemodeTransportError";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class CodemodeOperationError extends Error {
|
|
149
|
+
constructor(
|
|
150
|
+
readonly operation: CodemodeOperationValue,
|
|
151
|
+
readonly code: string,
|
|
152
|
+
) {
|
|
153
|
+
super(operation.errorMessage ?? `Codemode operation ${operation.state}`);
|
|
154
|
+
this.name = "CodemodeOperationError";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type CodemodeTokenProvider = string | (() => string | Promise<string>);
|
|
159
|
+
|
|
160
|
+
export type CodemodeClientOptions = {
|
|
161
|
+
/** `/v1/workspaces/:workspaceId/codemode` base URL. */
|
|
162
|
+
baseUrl: string;
|
|
163
|
+
token: CodemodeTokenProvider;
|
|
164
|
+
fetch?: typeof globalThis.fetch;
|
|
165
|
+
pollIntervalMs?: number;
|
|
166
|
+
timeoutMs?: number;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export type CodemodeCallOptions = {
|
|
170
|
+
operationId?: string;
|
|
171
|
+
signal?: AbortSignal;
|
|
172
|
+
timeoutMs?: number;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export type CodemodeToolFunction = (
|
|
176
|
+
argumentsValue?: Record<string, unknown>,
|
|
177
|
+
options?: CodemodeCallOptions,
|
|
178
|
+
) => Promise<unknown>;
|
|
179
|
+
|
|
180
|
+
export type CodemodeToolResult = AttemptToolResultValue;
|
|
181
|
+
|
|
182
|
+
export class CodemodeToolCallError extends Error {
|
|
183
|
+
readonly code: string;
|
|
184
|
+
readonly retryable: boolean;
|
|
185
|
+
|
|
186
|
+
constructor(readonly result: AttemptToolResultValue) {
|
|
187
|
+
const error = structuredToolError(result);
|
|
188
|
+
super(error.message);
|
|
189
|
+
this.name = "CodemodeToolCallError";
|
|
190
|
+
this.code = error.code;
|
|
191
|
+
this.retryable = error.retryable;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export class CodemodeToolContractError extends Error {
|
|
196
|
+
readonly code = "invalid_tool_result";
|
|
197
|
+
|
|
198
|
+
constructor(message: string) {
|
|
199
|
+
super(message);
|
|
200
|
+
this.name = "CodemodeToolContractError";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface CodemodeTools {
|
|
205
|
+
[key: string]: CodemodeTools | CodemodeToolFunction;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Persistent, idempotent client for one exact sandbox attempt bearer. */
|
|
209
|
+
export class CodemodeClient {
|
|
210
|
+
private readonly baseUrl: string;
|
|
211
|
+
private readonly fetchImpl: typeof globalThis.fetch;
|
|
212
|
+
private readonly pollIntervalMs: number;
|
|
213
|
+
private readonly timeoutMs: number;
|
|
214
|
+
private catalogSnapshot: AttemptToolCatalogValue | null = null;
|
|
215
|
+
|
|
216
|
+
constructor(private readonly options: CodemodeClientOptions) {
|
|
217
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/u, "");
|
|
218
|
+
if (!/^https?:\/\//u.test(this.baseUrl)) {
|
|
219
|
+
throw new Error("Codemode baseUrl must be an absolute HTTP(S) URL");
|
|
220
|
+
}
|
|
221
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
222
|
+
this.pollIntervalMs = boundedPositiveInteger(options.pollIntervalMs ?? 500, 50, 30_000);
|
|
223
|
+
this.timeoutMs = boundedPositiveInteger(options.timeoutMs ?? 10 * 60_000, 1_000, 60 * 60_000);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async catalog(
|
|
227
|
+
options: { refresh?: boolean; signal?: AbortSignal } = {},
|
|
228
|
+
): Promise<AttemptToolCatalogValue> {
|
|
229
|
+
if (this.catalogSnapshot && !options.refresh) return this.catalogSnapshot;
|
|
230
|
+
const response = await this.request("/catalog", {
|
|
231
|
+
method: "GET",
|
|
232
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
233
|
+
});
|
|
234
|
+
const catalog = parseVerifiedAttemptToolCatalog(await response.json());
|
|
235
|
+
this.catalogSnapshot = catalog;
|
|
236
|
+
return catalog;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async tools(options: { refresh?: boolean; signal?: AbortSignal } = {}): Promise<CodemodeTools> {
|
|
240
|
+
return compileCodemodeTools(await this.catalog(options), this);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async call(
|
|
244
|
+
identity: AttemptToolIdentity,
|
|
245
|
+
argumentsValue: Record<string, unknown> = {},
|
|
246
|
+
options: CodemodeCallOptions = {},
|
|
247
|
+
): Promise<AttemptToolResultValue> {
|
|
248
|
+
const catalog = await this.catalog(options.signal ? { signal: options.signal } : {});
|
|
249
|
+
if (
|
|
250
|
+
!catalog.entries.some(
|
|
251
|
+
(entry) =>
|
|
252
|
+
entry.identity.serverId === identity.serverId &&
|
|
253
|
+
entry.identity.toolName === identity.toolName,
|
|
254
|
+
)
|
|
255
|
+
) {
|
|
256
|
+
throw new AttemptToolNotFoundError();
|
|
257
|
+
}
|
|
258
|
+
const operationId = options.operationId ?? randomUUID();
|
|
259
|
+
const deadline =
|
|
260
|
+
Date.now() + boundedPositiveInteger(options.timeoutMs ?? this.timeoutMs, 1_000, 60 * 60_000);
|
|
261
|
+
let submitted = false;
|
|
262
|
+
let operation: CodemodeOperationValue | null = null;
|
|
263
|
+
let nextNotifyAt = 0;
|
|
264
|
+
while (true) {
|
|
265
|
+
throwIfAborted(options.signal);
|
|
266
|
+
if (Date.now() >= deadline) {
|
|
267
|
+
throw new CodemodeTransportError(
|
|
268
|
+
`Codemode operation ${operationId} did not settle before the client deadline`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const shouldNotify =
|
|
272
|
+
!submitted || (operation?.state === "queued" && Date.now() >= nextNotifyAt);
|
|
273
|
+
if (shouldNotify) {
|
|
274
|
+
submitted = true;
|
|
275
|
+
nextNotifyAt = Date.now() + 2_000;
|
|
276
|
+
try {
|
|
277
|
+
operation = await this.submit(
|
|
278
|
+
operationId,
|
|
279
|
+
catalog.digest,
|
|
280
|
+
identity,
|
|
281
|
+
argumentsValue,
|
|
282
|
+
options.signal,
|
|
283
|
+
);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
// The POST may have committed before its response was lost, or an
|
|
286
|
+
// attempt may have closed between submission and a wake retry. The
|
|
287
|
+
// caller-owned id is the recovery handle: read before deciding that
|
|
288
|
+
// another side effect is necessary.
|
|
289
|
+
try {
|
|
290
|
+
operation = await this.read(operationId, options.signal);
|
|
291
|
+
} catch {
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
operation = await this.read(operationId, options.signal);
|
|
297
|
+
}
|
|
298
|
+
if (operation.state === "completed") return AttemptToolResult.parse(operation.result);
|
|
299
|
+
if (["failed", "outcome_unknown", "cancelled"].includes(operation.state)) {
|
|
300
|
+
throw new CodemodeOperationError(
|
|
301
|
+
operation,
|
|
302
|
+
operation.errorCode ?? `codemode_${operation.state}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
await abortableDelay(this.pollIntervalMs, options.signal);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Resolve and call one exact generated namespace path without parsing a wire name. */
|
|
310
|
+
async callPath(
|
|
311
|
+
path: readonly string[],
|
|
312
|
+
argumentsValue: Record<string, unknown> = {},
|
|
313
|
+
options: CodemodeCallOptions = {},
|
|
314
|
+
): Promise<AttemptToolResultValue> {
|
|
315
|
+
if (path.length < 2 || path.some((segment) => segment.length === 0)) {
|
|
316
|
+
throw new AttemptToolNotFoundError();
|
|
317
|
+
}
|
|
318
|
+
const catalog = await this.catalog(options.signal ? { signal: options.signal } : {});
|
|
319
|
+
const matches = catalog.entries.filter(
|
|
320
|
+
(entry) =>
|
|
321
|
+
entry.codemodePath.length === path.length &&
|
|
322
|
+
entry.codemodePath.every((segment, index) => segment === path[index]),
|
|
323
|
+
);
|
|
324
|
+
if (matches.length !== 1) throw new AttemptToolNotFoundError();
|
|
325
|
+
return await this.call(matches[0]!.identity, argumentsValue, options);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Return structured content when the catalog declares it; otherwise retain the full MCP result. */
|
|
329
|
+
async callPathValue(
|
|
330
|
+
path: readonly string[],
|
|
331
|
+
argumentsValue: Record<string, unknown> = {},
|
|
332
|
+
options: CodemodeCallOptions = {},
|
|
333
|
+
): Promise<unknown> {
|
|
334
|
+
if (path.length < 2 || path.some((segment) => segment.length === 0)) {
|
|
335
|
+
throw new AttemptToolNotFoundError();
|
|
336
|
+
}
|
|
337
|
+
const catalog = await this.catalog(options.signal ? { signal: options.signal } : {});
|
|
338
|
+
const matches = catalog.entries.filter(
|
|
339
|
+
(entry) =>
|
|
340
|
+
entry.codemodePath.length === path.length &&
|
|
341
|
+
entry.codemodePath.every((segment, index) => segment === path[index]),
|
|
342
|
+
);
|
|
343
|
+
if (matches.length !== 1) throw new AttemptToolNotFoundError();
|
|
344
|
+
const entry = matches[0]!;
|
|
345
|
+
const result = await this.call(entry.identity, argumentsValue, options);
|
|
346
|
+
if (!entry.outputSchema) return result;
|
|
347
|
+
if (result.isError) throw new CodemodeToolCallError(result);
|
|
348
|
+
if (!result.structuredContent) {
|
|
349
|
+
throw new CodemodeToolContractError(
|
|
350
|
+
`Codemode tool ${path.join(".")} declared outputSchema but returned no structured content`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return result.structuredContent;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private async submit(
|
|
357
|
+
operationId: string,
|
|
358
|
+
catalogDigest: string,
|
|
359
|
+
identity: AttemptToolIdentity,
|
|
360
|
+
argumentsValue: Record<string, unknown>,
|
|
361
|
+
signal?: AbortSignal,
|
|
362
|
+
): Promise<CodemodeOperationValue> {
|
|
363
|
+
const response = await this.request("/calls", {
|
|
364
|
+
method: "POST",
|
|
365
|
+
...(signal ? { signal } : {}),
|
|
366
|
+
headers: { "content-type": "application/json" },
|
|
367
|
+
body: JSON.stringify({ operationId, catalogDigest, identity, arguments: argumentsValue }),
|
|
368
|
+
});
|
|
369
|
+
return CodemodeCallSubmission.parse(await response.json()).operation;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private async read(operationId: string, signal?: AbortSignal): Promise<CodemodeOperationValue> {
|
|
373
|
+
const response = await this.request(`/calls/${operationId}`, {
|
|
374
|
+
method: "GET",
|
|
375
|
+
...(signal ? { signal } : {}),
|
|
376
|
+
});
|
|
377
|
+
return CodemodeOperation.parse(await response.json());
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private async request(path: string, init: RequestInit): Promise<Response> {
|
|
381
|
+
const token =
|
|
382
|
+
typeof this.options.token === "function" ? await this.options.token() : this.options.token;
|
|
383
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
384
|
+
...init,
|
|
385
|
+
headers: {
|
|
386
|
+
...Object.fromEntries(new Headers(init.headers).entries()),
|
|
387
|
+
authorization: `Bearer ${token}`,
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
if (!response.ok) {
|
|
391
|
+
let message = `Codemode request failed with HTTP ${response.status}`;
|
|
392
|
+
try {
|
|
393
|
+
const payload = (await response.json()) as { error?: { message?: unknown } };
|
|
394
|
+
if (typeof payload.error?.message === "string") message = payload.error.message;
|
|
395
|
+
} catch {
|
|
396
|
+
// The status is sufficient; never echo an unbounded provider body.
|
|
397
|
+
}
|
|
398
|
+
throw new CodemodeTransportError(message, response.status);
|
|
399
|
+
}
|
|
400
|
+
return response;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function compileCodemodeTools(
|
|
405
|
+
catalog: AttemptToolCatalogValue,
|
|
406
|
+
client: CodemodeClient,
|
|
407
|
+
): CodemodeTools {
|
|
408
|
+
const verified = parseVerifiedAttemptToolCatalog(catalog);
|
|
409
|
+
const root: CodemodeTools = Object.create(null) as CodemodeTools;
|
|
410
|
+
for (const entry of verified.entries) {
|
|
411
|
+
let cursor = root;
|
|
412
|
+
for (const segment of entry.codemodePath.slice(0, -1)) {
|
|
413
|
+
let existing = cursor[segment];
|
|
414
|
+
if (typeof existing === "function") throw new Error("Codemode path collides with a tool");
|
|
415
|
+
if (!existing) {
|
|
416
|
+
existing = Object.create(null) as CodemodeTools;
|
|
417
|
+
cursor[segment] = existing;
|
|
418
|
+
}
|
|
419
|
+
cursor = existing;
|
|
420
|
+
}
|
|
421
|
+
const leaf = entry.codemodePath.at(-1)!;
|
|
422
|
+
const invoke: CodemodeToolFunction = async (args = {}, options = {}) =>
|
|
423
|
+
await client.callPathValue(entry.codemodePath, args, options);
|
|
424
|
+
Object.defineProperty(invoke, "entry", { value: entry, enumerable: false });
|
|
425
|
+
cursor[leaf] = invoke;
|
|
426
|
+
}
|
|
427
|
+
return root;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
type CompiledDefinition = {
|
|
431
|
+
entry: AttemptToolCatalogEntryValue;
|
|
432
|
+
execute: AttemptToolDefinition["execute"];
|
|
433
|
+
validateInput: ValidateFunction<unknown>;
|
|
434
|
+
validateOutput: ValidateFunction<unknown> | null;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
export class AttemptToolEnvironment {
|
|
438
|
+
readonly catalog: AttemptToolCatalogValue;
|
|
439
|
+
private readonly byIdentity = new Map<string, CompiledDefinition>();
|
|
440
|
+
private readonly byModelName = new Map<string, CompiledDefinition>();
|
|
441
|
+
|
|
442
|
+
constructor(
|
|
443
|
+
catalog: AttemptToolCatalogValue,
|
|
444
|
+
definitions: readonly CompiledDefinition[],
|
|
445
|
+
private readonly authorize: AttemptToolAuthorization | undefined,
|
|
446
|
+
) {
|
|
447
|
+
this.catalog = catalog;
|
|
448
|
+
for (const definition of definitions) {
|
|
449
|
+
this.byIdentity.set(identityKey(definition.entry.identity), definition);
|
|
450
|
+
this.byModelName.set(definition.entry.modelName, definition);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async call(
|
|
455
|
+
input: AttemptToolCallValue,
|
|
456
|
+
context: { transportMeta?: Record<string, unknown> | null; signal?: AbortSignal } = {},
|
|
457
|
+
): Promise<AttemptToolResultValue> {
|
|
458
|
+
const call = AttemptToolCall.parse(input);
|
|
459
|
+
if (call.catalogDigest !== this.catalog.digest) {
|
|
460
|
+
throw new AttemptToolCatalogStaleError();
|
|
461
|
+
}
|
|
462
|
+
const definition = this.byIdentity.get(identityKey(call.identity));
|
|
463
|
+
if (!definition) {
|
|
464
|
+
throw new AttemptToolNotFoundError();
|
|
465
|
+
}
|
|
466
|
+
if (call.caller.kind === "codemode" && definition.entry.approval === "human") {
|
|
467
|
+
throw new AttemptToolApprovalRequiredError();
|
|
468
|
+
}
|
|
469
|
+
if (!definition.validateInput(call.arguments)) {
|
|
470
|
+
throw new AttemptToolInputValidationError();
|
|
471
|
+
}
|
|
472
|
+
await this.authorize?.({ call, entry: definition.entry });
|
|
473
|
+
const result = AttemptToolResult.parse(
|
|
474
|
+
await definition.execute(call.arguments, {
|
|
475
|
+
operationId: call.operationId,
|
|
476
|
+
caller: call.caller,
|
|
477
|
+
...(context.transportMeta === undefined ? {} : { transportMeta: context.transportMeta }),
|
|
478
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
479
|
+
}),
|
|
480
|
+
);
|
|
481
|
+
if (!result.isError && definition.validateOutput) {
|
|
482
|
+
if (
|
|
483
|
+
result.structuredContent === undefined ||
|
|
484
|
+
!definition.validateOutput(result.structuredContent)
|
|
485
|
+
) {
|
|
486
|
+
throw new AttemptToolOutputValidationError();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async callModel(input: ModelAttemptToolCall): Promise<AttemptToolResultValue> {
|
|
493
|
+
const definition = this.byModelName.get(input.modelName);
|
|
494
|
+
if (!definition) {
|
|
495
|
+
throw new AttemptToolNotFoundError();
|
|
496
|
+
}
|
|
497
|
+
const call = AttemptToolCall.parse({
|
|
498
|
+
operationId: input.operationId ?? randomUUID(),
|
|
499
|
+
catalogDigest: this.catalog.digest,
|
|
500
|
+
identity: definition.entry.identity,
|
|
501
|
+
arguments: input.arguments,
|
|
502
|
+
caller: { kind: "model", subjectId: input.subjectId },
|
|
503
|
+
});
|
|
504
|
+
return await this.call(call, {
|
|
505
|
+
...(input.transportMeta === undefined ? {} : { transportMeta: input.transportMeta }),
|
|
506
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function createAttemptToolEnvironment(
|
|
512
|
+
input: CreateAttemptToolEnvironmentInput,
|
|
513
|
+
): AttemptToolEnvironment {
|
|
514
|
+
const createdAt = (input.createdAt ?? new Date()).toISOString();
|
|
515
|
+
const paths = allocateCodemodePaths(input.definitions);
|
|
516
|
+
const schemaValidators = createSchemaValidators();
|
|
517
|
+
const compiled = input.definitions.map((definition, index): CompiledDefinition => {
|
|
518
|
+
const { execute, codemodePath: _path, ...entryInput } = definition;
|
|
519
|
+
const entry = AttemptToolCatalogEntry.parse({
|
|
520
|
+
...entryInput,
|
|
521
|
+
codemodePath: paths[index],
|
|
522
|
+
});
|
|
523
|
+
return {
|
|
524
|
+
entry,
|
|
525
|
+
execute,
|
|
526
|
+
validateInput: compileCatalogSchema(schemaValidators, entry.inputSchema),
|
|
527
|
+
validateOutput: entry.outputSchema
|
|
528
|
+
? compileCatalogSchema(schemaValidators, entry.outputSchema)
|
|
529
|
+
: null,
|
|
530
|
+
};
|
|
531
|
+
});
|
|
532
|
+
const unsigned = {
|
|
533
|
+
version: ATTEMPT_TOOL_CATALOG_VERSION,
|
|
534
|
+
...input.scope,
|
|
535
|
+
generation: input.generation,
|
|
536
|
+
createdAt,
|
|
537
|
+
entries: compiled.map(({ entry }) => entry),
|
|
538
|
+
};
|
|
539
|
+
const catalog = AttemptToolCatalog.parse({
|
|
540
|
+
...unsigned,
|
|
541
|
+
digest: digestAttemptToolCatalog(unsigned),
|
|
542
|
+
});
|
|
543
|
+
assertCatalogSize(catalog);
|
|
544
|
+
return new AttemptToolEnvironment(catalog, compiled, input.authorize);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function digestAttemptToolCatalog(catalog: Omit<AttemptToolCatalogValue, "digest">): string {
|
|
548
|
+
const { createdAt: _createdAt, ...authoritative } = catalog;
|
|
549
|
+
return digestCanonicalJson(authoritative);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function digestCodemodeOperationRequest(
|
|
553
|
+
input: Pick<AttemptToolCallValue, "catalogDigest" | "identity" | "arguments" | "caller">,
|
|
554
|
+
): string {
|
|
555
|
+
return digestCanonicalJson(input);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function codemodeDispatchSubject(workspaceId: string, attemptId: string): string {
|
|
559
|
+
if (!UUID_PATTERN.test(workspaceId) || !UUID_PATTERN.test(attemptId)) {
|
|
560
|
+
throw new Error("Codemode dispatch subject requires UUID workspace and attempt ids");
|
|
561
|
+
}
|
|
562
|
+
return `codemode.${workspaceId}.${attemptId}.dispatch`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function encodeCodemodeDispatchRequest(input: CodemodeDispatchRequestValue): Uint8Array {
|
|
566
|
+
return new TextEncoder().encode(JSON.stringify(CodemodeDispatchRequest.parse(input)));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function decodeCodemodeDispatchRequest(input: Uint8Array): CodemodeDispatchRequestValue {
|
|
570
|
+
return CodemodeDispatchRequest.parse(JSON.parse(new TextDecoder().decode(input)));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export function encodeCodemodeDispatchAck(input: CodemodeDispatchAckValue): Uint8Array {
|
|
574
|
+
return new TextEncoder().encode(JSON.stringify(CodemodeDispatchAck.parse(input)));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function decodeCodemodeDispatchAck(input: Uint8Array): CodemodeDispatchAckValue {
|
|
578
|
+
return CodemodeDispatchAck.parse(JSON.parse(new TextDecoder().decode(input)));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function parseVerifiedAttemptToolCatalog(input: unknown): AttemptToolCatalogValue {
|
|
582
|
+
const catalog = AttemptToolCatalog.parse(input);
|
|
583
|
+
assertCatalogSize(catalog);
|
|
584
|
+
const { digest, ...unsigned } = catalog;
|
|
585
|
+
if (digestAttemptToolCatalog(unsigned) !== digest) {
|
|
586
|
+
throw new AttemptToolCatalogIntegrityError();
|
|
587
|
+
}
|
|
588
|
+
return catalog;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function assertCatalogSize(catalog: AttemptToolCatalogValue): void {
|
|
592
|
+
if (
|
|
593
|
+
new TextEncoder().encode(JSON.stringify(catalog)).byteLength > ATTEMPT_TOOL_CATALOG_MAX_BYTES
|
|
594
|
+
) {
|
|
595
|
+
throw new AttemptToolCatalogTooLargeError();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
type SchemaCompiler = { compile(schema: object): ValidateFunction<unknown> };
|
|
600
|
+
|
|
601
|
+
function createSchemaValidators(): {
|
|
602
|
+
draft7: SchemaCompiler;
|
|
603
|
+
draft2019: SchemaCompiler;
|
|
604
|
+
draft2020: SchemaCompiler;
|
|
605
|
+
} {
|
|
606
|
+
const options = {
|
|
607
|
+
allErrors: false,
|
|
608
|
+
coerceTypes: false,
|
|
609
|
+
strict: false,
|
|
610
|
+
useDefaults: false,
|
|
611
|
+
validateFormats: false,
|
|
612
|
+
} as const;
|
|
613
|
+
return {
|
|
614
|
+
draft7: new Ajv(options),
|
|
615
|
+
draft2019: new Ajv2019(options),
|
|
616
|
+
draft2020: new Ajv2020(options),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function compileCatalogSchema(
|
|
621
|
+
validators: ReturnType<typeof createSchemaValidators>,
|
|
622
|
+
schema: AttemptToolCatalogEntryValue["inputSchema"],
|
|
623
|
+
): ValidateFunction<unknown> {
|
|
624
|
+
const dialect = typeof schema.$schema === "string" ? schema.$schema : "";
|
|
625
|
+
if (dialect.includes("2020-12")) return validators.draft2020.compile(schema);
|
|
626
|
+
if (dialect.includes("2019-09")) return validators.draft2019.compile(schema);
|
|
627
|
+
return validators.draft7.compile(schema);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function allocateCodemodePaths(definitions: readonly AttemptToolDefinition[]): string[][] {
|
|
631
|
+
const bases = definitions.map((definition) =>
|
|
632
|
+
(definition.codemodePath?.length
|
|
633
|
+
? definition.codemodePath
|
|
634
|
+
: [definition.identity.serverId, definition.identity.toolName]
|
|
635
|
+
).map(safeNamespaceSegment),
|
|
636
|
+
);
|
|
637
|
+
const counts = new Map<string, number>();
|
|
638
|
+
for (const path of bases) {
|
|
639
|
+
const key = path.join("\u0000");
|
|
640
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
641
|
+
}
|
|
642
|
+
return bases.map((base, index) => {
|
|
643
|
+
const key = base.join("\u0000");
|
|
644
|
+
if (counts.get(key) === 1) return base;
|
|
645
|
+
const suffix = `_${shortIdentityDigest(definitions[index]!.identity)}`;
|
|
646
|
+
const last = base.at(-1)!;
|
|
647
|
+
return [...base.slice(0, -1), `${last.slice(0, 128 - suffix.length)}${suffix}`];
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function safeNamespaceSegment(value: string): string {
|
|
652
|
+
let normalized = value.replace(/[^A-Za-z0-9_$]/gu, "_");
|
|
653
|
+
if (!/^[A-Za-z_$]/u.test(normalized)) normalized = `_${normalized}`;
|
|
654
|
+
if (["__proto__", "prototype", "constructor"].includes(normalized)) {
|
|
655
|
+
normalized = `_${normalized}`;
|
|
656
|
+
}
|
|
657
|
+
return normalized.slice(0, 128) || "_";
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function shortIdentityDigest(identity: AttemptToolIdentity): string {
|
|
661
|
+
return createHash("sha256").update(identityKey(identity), "utf8").digest("hex").slice(0, 10);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function identityKey(identity: AttemptToolIdentity): string {
|
|
665
|
+
return `${identity.serverId}\u0000${identity.toolName}`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function digestCanonicalJson(value: unknown): string {
|
|
669
|
+
return createHash("sha256")
|
|
670
|
+
.update(JSON.stringify(canonicalJsonValue(value)), "utf8")
|
|
671
|
+
.digest("hex");
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function canonicalJsonValue(value: unknown): unknown {
|
|
675
|
+
if (Array.isArray(value)) return value.map(canonicalJsonValue);
|
|
676
|
+
if (value !== null && typeof value === "object") {
|
|
677
|
+
return Object.fromEntries(
|
|
678
|
+
Object.entries(value as Record<string, unknown>)
|
|
679
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
680
|
+
.map(([key, entry]) => [key, canonicalJsonValue(entry)]),
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
return value;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
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;
|
|
687
|
+
|
|
688
|
+
function boundedPositiveInteger(value: number, minimum: number, maximum: number): number {
|
|
689
|
+
if (!Number.isFinite(value)) return minimum;
|
|
690
|
+
return Math.max(minimum, Math.min(maximum, Math.floor(value)));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
694
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function structuredToolError(result: AttemptToolResultValue): {
|
|
698
|
+
code: string;
|
|
699
|
+
message: string;
|
|
700
|
+
retryable: boolean;
|
|
701
|
+
} {
|
|
702
|
+
const structured = result.structuredContent as
|
|
703
|
+
| { error?: { code?: unknown; message?: unknown; retryable?: unknown } }
|
|
704
|
+
| undefined;
|
|
705
|
+
const error = structured?.error;
|
|
706
|
+
return {
|
|
707
|
+
code: typeof error?.code === "string" ? error.code : "tool_error",
|
|
708
|
+
message: typeof error?.message === "string" ? error.message : "Codemode tool failed",
|
|
709
|
+
retryable: error?.retryable === true,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function abortableDelay(delayMs: number, signal?: AbortSignal): Promise<void> {
|
|
714
|
+
if (!signal) {
|
|
715
|
+
await new Promise<void>((resolve) => setTimeout(resolve, delayMs));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
throwIfAborted(signal);
|
|
719
|
+
await new Promise<void>((resolve, reject) => {
|
|
720
|
+
const timer = setTimeout(() => {
|
|
721
|
+
signal.removeEventListener("abort", onAbort);
|
|
722
|
+
resolve();
|
|
723
|
+
}, delayMs);
|
|
724
|
+
const onAbort = () => {
|
|
725
|
+
clearTimeout(timer);
|
|
726
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
727
|
+
};
|
|
728
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export * from "./environment";
|
|
733
|
+
export * from "./interaction";
|
|
734
|
+
export * from "./artifacts";
|
|
735
|
+
export * from "./structured";
|
|
736
|
+
export * from "./declarations";
|