nanocodex 0.6.4 → 0.6.6
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 +37 -11
- package/browser/Agent.d.mts +1 -1
- package/browser/ElevenLabsPlayback.mjs +119 -0
- package/browser/InlineAgent.mjs +22 -17
- package/browser/Transport.d.mts +2 -0
- package/browser/Transport.mjs +3 -2
- package/browser/Voice.d.mts +4 -0
- package/browser/Voice.mjs +38 -6
- package/browser/VoiceSession.mjs +37 -7
- package/browser/host.d.mts +20 -4
- package/browser/host.mjs +156 -18
- package/cloud/types.d.mts +1 -0
- package/cloudflare/Agent.d.mts +14 -9
- package/cloudflare/Agent.mjs +354 -96
- package/cloudflare/agent-settings.d.mts +15 -0
- package/cloudflare/agent-settings.mjs +135 -0
- package/cloudflare/chat-reasoning.mjs +20 -0
- package/cloudflare/durable-placement.d.mts +12 -0
- package/cloudflare/durable-placement.mjs +82 -0
- package/cloudflare/egress.d.mts +31 -0
- package/cloudflare/gateway-binding-responses.mjs +84 -0
- package/cloudflare/gateway-responses.d.mts +48 -0
- package/cloudflare/gateway-responses.mjs +191 -0
- package/cloudflare/index.d.mts +6 -0
- package/cloudflare/index.mjs +3 -0
- package/cloudflare/managed-auth.d.mts +15 -0
- package/cloudflare/managed-auth.mjs +119 -0
- package/cloudflare/managed-live.d.mts +7 -0
- package/cloudflare/managed-live.mjs +56 -0
- package/cloudflare/prepared-connection.mjs +49 -0
- package/cloudflare/provider-stream.mjs +353 -0
- package/cloudflare/rpc.d.mts +9 -0
- package/cloudflare/rpc.mjs +14 -0
- package/cloudflare/tool-alias-diagnostic.mjs +21 -0
- package/cloudflare/tracing.d.mts +16 -0
- package/cloudflare/tracing.mjs +47 -0
- package/cloudflare/workers-ai-responses.d.mts +28 -0
- package/cloudflare/workers-ai-responses.mjs +399 -0
- package/host/Agent.d.mts +1 -1
- package/index.d.mts +2 -0
- package/internal.mjs +53 -14
- package/managed/Agent.d.mts +25 -69
- package/managed/Agent.mjs +82 -196
- package/managed/README.md +9 -38
- package/managed/Voice.mjs +5 -2
- package/managed/index.d.mts +0 -3
- package/managed/internal.mjs +2 -1
- package/node/Agent.d.mts +1 -1
- package/node/Agent.mjs +9 -3
- package/node/host.mjs +5 -0
- package/package.json +34 -2
- package/pkg-node/nanocodex.d.ts +22 -18
- package/pkg-node/nanocodex.js +118 -92
- package/pkg-node/nanocodex_bg.wasm.d.ts +6 -5
- package/pkg-web/.nanocodex-bindgen-stamp +7 -1
- package/pkg-web/nanocodex-build.json +1 -1
- package/pkg-web/nanocodex.d.ts +28 -23
- package/pkg-web/nanocodex.js +117 -91
- package/pkg-web/nanocodex_bg.js +117 -91
- package/pkg-web/nanocodex_bg.wasm +0 -0
- package/pkg-web/nanocodex_bg.wasm.d.ts +6 -5
- package/pkg-web/nanocodex_worker.js +1 -1
- package/runtime/before-compaction.mjs +49 -0
- package/runtime/code-evaluator.worker.mjs +2 -9
- package/runtime/managed-transport.mjs +10 -2
- package/runtime/mcp-runtime.mjs +20 -4
- package/runtime/quickjs-evaluator.mjs +11 -5
- package/runtime/response-controls.mjs +67 -4
- package/runtime/steer-receipt.mjs +13 -0
- package/runtime/subagent-routing.d.mts +38 -0
- package/runtime/subagent-routing.mjs +81 -0
- package/runtime/subagents.d.mts +1 -1
- package/runtime/tempo-provider.d.mts +1 -1
- package/runtime/tempo-provider.mjs +1 -1
- package/tools/environment.d.mts +17 -2
- package/tools/environment.mjs +30 -3
- package/tools/index.d.mts +2 -0
- package/types.d.mts +30 -2
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const USER_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
2
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
3
|
+
export const API_KEY = /^ncx_live_([A-Za-z0-9_-]{12})_([A-Za-z0-9_-]{43})$/;
|
|
4
|
+
const CONNECT_USER_HEADER = "x-nanocodex-connect-user";
|
|
5
|
+
const CONNECT_GRANT_ID_HEADER = "x-nanocodex-connect-grant-id";
|
|
6
|
+
const CONNECT_CAPABILITIES_HEADER = "x-nanocodex-connect-capabilities";
|
|
7
|
+
const CONNECT_CONNECTORS_HEADER = "x-nanocodex-connect-connectors";
|
|
8
|
+
const CONNECT_CONNECTOR_CONNECTIONS_HEADER = "x-nanocodex-connect-connector-connections";
|
|
9
|
+
const CONNECT_MCP_IDS_HEADER = "x-nanocodex-connect-mcp-ids";
|
|
10
|
+
const CONNECT_APP_TOOL_CATALOG_DIGEST_HEADER = "x-nanocodex-connect-app-tool-catalog-digest";
|
|
11
|
+
const SESSION_OWNER_ASSERTION = "x-nanocodex-owner-id";
|
|
12
|
+
const SESSION_ORGANIZATION_ASSERTION = "x-nanocodex-session-organization-id";
|
|
13
|
+
const SESSION_TEAM_ASSERTION = "x-nanocodex-session-team-id";
|
|
14
|
+
const SESSION_AUTHORIZATION_EPOCH_ASSERTION = "x-nanocodex-authorization-epoch";
|
|
15
|
+
const SESSION_CAPABILITIES_ASSERTION = "x-nanocodex-capabilities";
|
|
16
|
+
export function isUserId(value) {
|
|
17
|
+
return typeof value === "string" && USER_ID.test(value);
|
|
18
|
+
}
|
|
19
|
+
function isUuid(value) {
|
|
20
|
+
return typeof value === "string" && UUID.test(value);
|
|
21
|
+
}
|
|
22
|
+
function isOrganizationRole(value) {
|
|
23
|
+
return value === "owner" || value === "writer" || value === "reader";
|
|
24
|
+
}
|
|
25
|
+
export function isOrganizationCapabilities(value) {
|
|
26
|
+
if (!Array.isArray(value) || new Set(value).size !== value.length)
|
|
27
|
+
return false;
|
|
28
|
+
return value.every((capability) => capability === "agents:read"
|
|
29
|
+
|| capability === "agents:portability"
|
|
30
|
+
|| capability === "agents:write"
|
|
31
|
+
|| capability === "api_keys:read"
|
|
32
|
+
|| capability === "api_keys:write"
|
|
33
|
+
|| capability === "history:read"
|
|
34
|
+
|| capability === "memory:read"
|
|
35
|
+
|| capability === "memory:write"
|
|
36
|
+
|| capability === "tools:use"
|
|
37
|
+
|| capability === "organization:read"
|
|
38
|
+
|| capability === "organization:write");
|
|
39
|
+
}
|
|
40
|
+
export function isApiKeyBase(value) {
|
|
41
|
+
if (typeof value !== "object" || value === null)
|
|
42
|
+
return false;
|
|
43
|
+
const record = value;
|
|
44
|
+
return typeof record.id === "string"
|
|
45
|
+
&& /^[A-Za-z0-9_-]{12}$/.test(record.id)
|
|
46
|
+
&& typeof record.label === "string"
|
|
47
|
+
&& record.label.length <= 120
|
|
48
|
+
&& record.prefix === `ncx_live_${record.id}`
|
|
49
|
+
&& Number.isFinite(record.createdAt)
|
|
50
|
+
&& typeof record.digest === "string"
|
|
51
|
+
&& /^[A-Za-z0-9_-]{43}$/.test(record.digest)
|
|
52
|
+
&& isUserId(record.userId);
|
|
53
|
+
}
|
|
54
|
+
export function isStoredApiKey(value) {
|
|
55
|
+
if (!isApiKeyBase(value))
|
|
56
|
+
return false;
|
|
57
|
+
const record = value;
|
|
58
|
+
return isUuid(record.organizationId)
|
|
59
|
+
&& isUuid(record.teamId)
|
|
60
|
+
&& isOrganizationRole(record.role)
|
|
61
|
+
&& isOrganizationCapabilities(record.capabilities)
|
|
62
|
+
&& Number.isSafeInteger(record.authorizationEpoch)
|
|
63
|
+
&& Number(record.authorizationEpoch) >= 1;
|
|
64
|
+
}
|
|
65
|
+
export function forwardPrincipalAssertions(headers, principal) {
|
|
66
|
+
headers.set("x-nanocodex-request-principal", JSON.stringify({ kind: principal.kind, user_id: principal.userId }));
|
|
67
|
+
headers.set(SESSION_OWNER_ASSERTION, principal.userId);
|
|
68
|
+
headers.set(SESSION_ORGANIZATION_ASSERTION, principal.organizationId);
|
|
69
|
+
headers.set(SESSION_TEAM_ASSERTION, principal.teamId);
|
|
70
|
+
headers.set(SESSION_AUTHORIZATION_EPOCH_ASSERTION, String(principal.authorizationEpoch));
|
|
71
|
+
headers.set(SESSION_CAPABILITIES_ASSERTION, JSON.stringify(principal.capabilities));
|
|
72
|
+
for (const name of [
|
|
73
|
+
CONNECT_USER_HEADER,
|
|
74
|
+
CONNECT_GRANT_ID_HEADER,
|
|
75
|
+
CONNECT_CAPABILITIES_HEADER,
|
|
76
|
+
CONNECT_CONNECTORS_HEADER,
|
|
77
|
+
CONNECT_CONNECTOR_CONNECTIONS_HEADER,
|
|
78
|
+
CONNECT_MCP_IDS_HEADER,
|
|
79
|
+
CONNECT_APP_TOOL_CATALOG_DIGEST_HEADER,
|
|
80
|
+
"x-nanocodex-connect-sandbox-execution",
|
|
81
|
+
"x-nanocodex-connect-output-checkpoints",
|
|
82
|
+
]) {
|
|
83
|
+
headers.delete(name);
|
|
84
|
+
}
|
|
85
|
+
if (principal.connectGrant) {
|
|
86
|
+
if (principal.connectGrant.outputCheckpoints === true) headers.set("x-nanocodex-connect-output-checkpoints", "true");
|
|
87
|
+
if (principal.connectGrant.sandboxExecution === true) {
|
|
88
|
+
headers.set("x-nanocodex-connect-sandbox-execution", "true");
|
|
89
|
+
}
|
|
90
|
+
headers.set(CONNECT_GRANT_ID_HEADER, principal.connectGrant.grantId);
|
|
91
|
+
headers.set(CONNECT_CONNECTORS_HEADER, JSON.stringify(principal.connectGrant.connectors));
|
|
92
|
+
if (principal.connectGrant.connectorConnections !== undefined) {
|
|
93
|
+
headers.set(CONNECT_CONNECTOR_CONNECTIONS_HEADER, JSON.stringify(principal.connectGrant.connectorConnections));
|
|
94
|
+
}
|
|
95
|
+
headers.set(CONNECT_MCP_IDS_HEADER, JSON.stringify(principal.connectGrant.mcpIds));
|
|
96
|
+
if (principal.connectGrant.appToolCatalogDigest !== undefined) {
|
|
97
|
+
headers.set(CONNECT_APP_TOOL_CATALOG_DIGEST_HEADER, principal.connectGrant.appToolCatalogDigest);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function sha256(value) {
|
|
102
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)));
|
|
103
|
+
let binary = "";
|
|
104
|
+
for (const byte of digest)
|
|
105
|
+
binary += String.fromCharCode(byte);
|
|
106
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
107
|
+
}
|
|
108
|
+
export async function apiKeyDigest(request) {
|
|
109
|
+
const authorization = request.headers.get("authorization");
|
|
110
|
+
if (!authorization?.startsWith("Bearer "))
|
|
111
|
+
return;
|
|
112
|
+
const token = authorization.slice("Bearer ".length);
|
|
113
|
+
return API_KEY.test(token) ? sha256(token) : undefined;
|
|
114
|
+
}
|
|
115
|
+
export function apiKeyPrincipal(record, digest) {
|
|
116
|
+
if (!isStoredApiKey(record) || record.digest !== digest)
|
|
117
|
+
return;
|
|
118
|
+
return { kind: "api_key", userId: record.userId, organizationId: record.organizationId, teamId: record.teamId, role: record.role, subjectId: `api_key:${record.id}`, credentialId: record.id, authorizationEpoch: record.authorizationEpoch, capabilities: record.capabilities };
|
|
119
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ManagedAgentSettings } from "./agent-settings.mjs";
|
|
2
|
+
import type { AdmissionPrincipal } from "./managed-auth.mjs";
|
|
3
|
+
export function nativeLiveRequest(request: Request): boolean;
|
|
4
|
+
export function liveAgentSettings(request: Request): ManagedAgentSettings | Response;
|
|
5
|
+
export function liveAgentFailure(request: Request, principal: AdmissionPrincipal | undefined): Response | undefined;
|
|
6
|
+
export function liveAgentRequest(request: Request, principal: AdmissionPrincipal, settings: ManagedAgentSettings, agentId: string, clientIngressColo: string | null): Request;
|
|
7
|
+
export function newManagedAgentId(): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseAgentSettingsQuery, agentSettingsQuery } from "./agent-settings.mjs";
|
|
2
|
+
import { API_KEY, forwardPrincipalAssertions } from "./managed-auth.mjs";
|
|
3
|
+
import { ingressColo } from "./durable-placement.mjs";
|
|
4
|
+
|
|
5
|
+
/** Narrow profile only; cookies and Connect retain the full authenticator's precedence. */
|
|
6
|
+
export function nativeLiveRequest(request) {
|
|
7
|
+
return request.method === "GET" && new URL(request.url).pathname === "/v1/agents/live"
|
|
8
|
+
&& !request.headers.has("cookie")
|
|
9
|
+
&& ![...request.headers.keys()].some(name => name.startsWith("x-nanocodex-connect-"))
|
|
10
|
+
&& request.headers.get("authorization")?.startsWith("Bearer ") === true
|
|
11
|
+
&& API_KEY.test(request.headers.get("authorization").slice("Bearer ".length));
|
|
12
|
+
}
|
|
13
|
+
function failure(error, status) {
|
|
14
|
+
return Response.json({ error }, { status, headers: { "cache-control": "no-store" } });
|
|
15
|
+
}
|
|
16
|
+
/** Preserve managed admission's query-before-upgrade-before-auth ordering. */
|
|
17
|
+
export function liveAgentSettings(request) {
|
|
18
|
+
let settings;
|
|
19
|
+
try { settings = parseAgentSettingsQuery(new URL(request.url).searchParams); }
|
|
20
|
+
catch { return failure("invalid_request", 400); }
|
|
21
|
+
if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") {
|
|
22
|
+
return new Response("Expected WebSocket upgrade", { status: 426 });
|
|
23
|
+
}
|
|
24
|
+
return settings;
|
|
25
|
+
}
|
|
26
|
+
export function liveAgentFailure(request, principal) {
|
|
27
|
+
if (!principal) return failure("unauthorized", 401);
|
|
28
|
+
if (!["agents:read", "agents:write", "tools:use"].every(c => principal.capabilities.includes(c))) return failure("forbidden", 403);
|
|
29
|
+
if (principal.connectGrant && !principal.connectGrant.connectors.includes("chatgpt")) return failure("connector_forbidden", 403);
|
|
30
|
+
if (principal.kind !== "api_key" && request.headers.get("origin") !== new URL(request.url).origin) return failure("forbidden_origin", 403);
|
|
31
|
+
}
|
|
32
|
+
/** Internal request only, after liveAgentFailure. Never accepts HTTP-supplied authority. */
|
|
33
|
+
export function liveAgentRequest(request, principal, settings, agentId, clientIngressColo) {
|
|
34
|
+
const headers = new Headers(request.headers);
|
|
35
|
+
forwardPrincipalAssertions(headers, principal);
|
|
36
|
+
headers.delete("x-nanocodex-client-ingress-colo");
|
|
37
|
+
headers.delete("x-nanocodex-worker-colo");
|
|
38
|
+
const colo = ingressColo(clientIngressColo);
|
|
39
|
+
if (colo) headers.set("x-nanocodex-client-ingress-colo", colo);
|
|
40
|
+
headers.set("x-nanocodex-create-session-id", agentId);
|
|
41
|
+
const query = agentSettingsQuery(settings);
|
|
42
|
+
query.set("public_origin", new URL(request.url).origin);
|
|
43
|
+
return new Request(`https://session.internal/create-live?${query}`, new Request(request, { headers }));
|
|
44
|
+
}
|
|
45
|
+
export function newManagedAgentId() {
|
|
46
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
47
|
+
let timestamp = BigInt(Date.now());
|
|
48
|
+
for (let index = 5; index >= 0; index -= 1) {
|
|
49
|
+
bytes[index] = Number(timestamp & 0xffn);
|
|
50
|
+
timestamp >>= 8n;
|
|
51
|
+
}
|
|
52
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
|
53
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
54
|
+
const hex = [...bytes].map(byte => byte.toString(16).padStart(2, "0")).join("");
|
|
55
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** One construction-owned socket, transferred only to the matching root host. */
|
|
2
|
+
export function prepareConnection(endpoint, sessionId, signal) {
|
|
3
|
+
signal?.throwIfAborted();
|
|
4
|
+
let transferred = false;
|
|
5
|
+
let disposed = false;
|
|
6
|
+
let opened;
|
|
7
|
+
let closing;
|
|
8
|
+
let rejectDisposed;
|
|
9
|
+
const disposedPromise = new Promise((_, reject) => { rejectDisposed = reject; });
|
|
10
|
+
const opening = Promise.resolve().then(() => {
|
|
11
|
+
signal?.throwIfAborted();
|
|
12
|
+
return endpoint.createWebSocket(endpoint.websocketUrl, sessionId, { authorization: "preconnect" });
|
|
13
|
+
}).then(value => {
|
|
14
|
+
opened = value;
|
|
15
|
+
if (disposed) {
|
|
16
|
+
void close().catch(() => {});
|
|
17
|
+
throw new Error("Cloudflare Agent preparation was disposed");
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
});
|
|
21
|
+
const promise = Promise.race([opening, disposedPromise]);
|
|
22
|
+
void promise.catch(() => {});
|
|
23
|
+
const abort = () => { void dispose().catch(() => {}); };
|
|
24
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
25
|
+
return { take, dispose };
|
|
26
|
+
|
|
27
|
+
function take(url, id, request) {
|
|
28
|
+
if (disposed || transferred || url !== endpoint.websocketUrl || id !== sessionId
|
|
29
|
+
|| (request.threadId ?? id) !== sessionId || request.authorization !== "preconnect") {
|
|
30
|
+
throw new Error("Cloudflare Agent preparation does not match the root transport");
|
|
31
|
+
}
|
|
32
|
+
transferred = true;
|
|
33
|
+
signal?.removeEventListener("abort", abort);
|
|
34
|
+
return promise;
|
|
35
|
+
}
|
|
36
|
+
function close() {
|
|
37
|
+
if (opened !== undefined) closing ??= Promise.resolve().then(() => opened.socket.close());
|
|
38
|
+
return closing ?? Promise.resolve();
|
|
39
|
+
}
|
|
40
|
+
function dispose() {
|
|
41
|
+
if (transferred) return Promise.resolve();
|
|
42
|
+
if (!disposed) {
|
|
43
|
+
disposed = true;
|
|
44
|
+
signal?.removeEventListener("abort", abort);
|
|
45
|
+
rejectDisposed(new Error("Cloudflare Agent preparation was disposed"));
|
|
46
|
+
}
|
|
47
|
+
return close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { chatReasoningText } from "./chat-reasoning.mjs";
|
|
2
|
+
import { fromBindingResponsesResult } from "./gateway-binding-responses.mjs";
|
|
3
|
+
|
|
4
|
+
// Codes are local constants only. Never surface an upstream exception/message.
|
|
5
|
+
class StreamProtocolError extends Error {
|
|
6
|
+
constructor(code) { super(`Responses: invalid provider stream\nProtocol invariant: ${code}`); this.code = code; }
|
|
7
|
+
}
|
|
8
|
+
const invalid = code => { throw new StreamProtocolError(code); };
|
|
9
|
+
// The portable normalizer has static failures but can also throw arbitrary
|
|
10
|
+
// exceptions. Only exact known messages map to public diagnostics.
|
|
11
|
+
const normalizationCodes = new Map([
|
|
12
|
+
["invalid chat completion or unsupported finish reason", "normalize_completion"],
|
|
13
|
+
["unsupported completion content", "normalize_content"],
|
|
14
|
+
["invalid completion tool calls", "normalize_tool_calls"],
|
|
15
|
+
["provider refused completion", "normalize_refusal"],
|
|
16
|
+
["incomplete completion cannot dispatch tool calls", "normalize_incomplete_tools"],
|
|
17
|
+
["invalid reasoning details", "normalize_reasoning_details"],
|
|
18
|
+
["reasoning details exceed limit", "normalize_reasoning_size"],
|
|
19
|
+
["tool choice forbids calls", "normalize_tool_forbidden"],
|
|
20
|
+
["required tool call missing", "normalize_tool_required"],
|
|
21
|
+
["unsupported completion tool call", "normalize_tool_type"],
|
|
22
|
+
["model returned an unknown tool alias", "normalize_tool_alias"],
|
|
23
|
+
["model returned ambiguous original tool alias", "normalize_tool_alias_ambiguous"],
|
|
24
|
+
["model returned namespaced wire tool alias", "normalize_tool_alias_namespace_wire"],
|
|
25
|
+
["model returned flattened namespace tool alias", "normalize_tool_alias_namespace_flat"],
|
|
26
|
+
["model returned repeated tool alias", "normalize_tool_alias_repeated"],
|
|
27
|
+
["model returned unregistered wire tool alias", "normalize_tool_alias_unregistered_wire"],
|
|
28
|
+
["model returned parallel wrapper tool alias", "normalize_tool_alias_parallel_wrapper"],
|
|
29
|
+
["model returned a different forced tool", "normalize_forced_tool"],
|
|
30
|
+
["model returned invalid tool JSON", "normalize_tool_json"],
|
|
31
|
+
["model returned unwrapped custom tool input", "normalize_custom_raw_input"],
|
|
32
|
+
["invalid tool call ID", "normalize_tool_id"],
|
|
33
|
+
["tool arguments must be a JSON object", "normalize_tool_arguments"],
|
|
34
|
+
["model returned duplicate tool call IDs", "normalize_tool_duplicate_id"],
|
|
35
|
+
["custom tool arguments must contain a string input", "normalize_custom_input"],
|
|
36
|
+
["tool_calls finish reason omitted tool calls", "normalize_missing_tools"],
|
|
37
|
+
].map(([message, code]) => [`Workers AI Responses: ${message}`, code]));
|
|
38
|
+
for (const [message, code] of [
|
|
39
|
+
["invalid reasoning text", "normalize_reasoning_text"],
|
|
40
|
+
["invalid reasoning details", "normalize_reasoning_details"],
|
|
41
|
+
["invalid reasoning detail text", "normalize_reasoning_detail_text"],
|
|
42
|
+
]) normalizationCodes.set(`Responses: ${message}`, code);
|
|
43
|
+
class StreamReadError extends Error {}
|
|
44
|
+
const encoder = new TextEncoder();
|
|
45
|
+
const frame = event => encoder.encode(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
46
|
+
export const providerStream = (body, format = "chat", hooks = {}) => ({ providerStream: true, body, format, ...hooks });
|
|
47
|
+
|
|
48
|
+
// Streaming UTF-8/SSE framing, including split CRLF, multiline data and comments.
|
|
49
|
+
// Bound each frame and the full wire stream, including endless keepalives.
|
|
50
|
+
async function* records(reader) {
|
|
51
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
52
|
+
let buffer = "", data = [], event = "", size = 0, total = 0;
|
|
53
|
+
const line = value => {
|
|
54
|
+
if (!value) {
|
|
55
|
+
const record = data.length ? { data: data.join("\n"), event } : null;
|
|
56
|
+
data = []; event = ""; size = 0;
|
|
57
|
+
return record;
|
|
58
|
+
}
|
|
59
|
+
size += value.length;
|
|
60
|
+
if (size > 4 * 1024 * 1024) invalid("frame_size");
|
|
61
|
+
const colon = value.indexOf(":");
|
|
62
|
+
const field = colon < 0 ? value : value.slice(0, colon);
|
|
63
|
+
let valueText = colon < 0 ? "" : value.slice(colon + 1);
|
|
64
|
+
if (valueText.startsWith(" ")) valueText = valueText.slice(1);
|
|
65
|
+
if (field === "data") data.push(valueText);
|
|
66
|
+
else if (field === "event") event = valueText;
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
while (true) {
|
|
70
|
+
let read;
|
|
71
|
+
try { read = await reader.read(); } catch { throw new StreamReadError(); }
|
|
72
|
+
const { value, done } = read;
|
|
73
|
+
total += value?.byteLength ?? 0;
|
|
74
|
+
if (total > 32 * 1024 * 1024) invalid("wire_size");
|
|
75
|
+
try { buffer += done ? decoder.decode() : decoder.decode(value, { stream: true }); }
|
|
76
|
+
catch { invalid("frame_utf8"); }
|
|
77
|
+
if (buffer.length > 4 * 1024 * 1024) invalid("frame_buffer_size");
|
|
78
|
+
let match;
|
|
79
|
+
while ((match = /\r\n|\r|\n/.exec(buffer))) {
|
|
80
|
+
if (!done && match[0] === "\r" && match.index === buffer.length - 1) break;
|
|
81
|
+
const record = line(buffer.slice(0, match.index));
|
|
82
|
+
buffer = buffer.slice(match.index + match[0].length);
|
|
83
|
+
if (record) yield record;
|
|
84
|
+
}
|
|
85
|
+
if (done) {
|
|
86
|
+
// A terminal event must be fully framed; never accept a truncated last frame.
|
|
87
|
+
if (buffer || data.length) invalid("frame_truncated");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function streamResponse(source, normalize, responseEvents, signal) {
|
|
94
|
+
if (!(source.body instanceof ReadableStream)) invalid("body_type");
|
|
95
|
+
const checkedNormalize = (result, prologue) => {
|
|
96
|
+
try { return normalize(result, prologue); }
|
|
97
|
+
catch (error) { invalid(normalizationCodes.get(error instanceof Error ? error.message : undefined) ?? "normalize_unknown"); }
|
|
98
|
+
};
|
|
99
|
+
const reader = source.body.getReader();
|
|
100
|
+
const iterator = records(reader);
|
|
101
|
+
let controller, settled = false, first = false, sequence = 0, finalSeen = false;
|
|
102
|
+
const id = `resp_${crypto.randomUUID()}`;
|
|
103
|
+
const reasoningDetails = [];
|
|
104
|
+
let retainedSize = 0;
|
|
105
|
+
const retain = text => {
|
|
106
|
+
retainedSize += encoder.encode(text).byteLength;
|
|
107
|
+
if (retainedSize > 8 * 1024 * 1024) invalid("retained_size");
|
|
108
|
+
};
|
|
109
|
+
const live = new Map();
|
|
110
|
+
const calls = new Map();
|
|
111
|
+
const nativeItems = new Map();
|
|
112
|
+
const nativeParts = new Map();
|
|
113
|
+
let usage, finishReason, started = false, bindingUsageSeen = false;
|
|
114
|
+
const emit = (type, fields) => controller.enqueue(frame({ type, sequence_number: sequence++, ...fields }));
|
|
115
|
+
const firstToken = () => { if (!first) { first = true; try { source.firstToken?.(); } catch { /* best effort */ } } };
|
|
116
|
+
const finish = async outcome => {
|
|
117
|
+
if (settled) return;
|
|
118
|
+
settled = true;
|
|
119
|
+
signal?.removeEventListener("abort", abort);
|
|
120
|
+
try { await source.finish?.(outcome); } catch { /* best effort */ }
|
|
121
|
+
};
|
|
122
|
+
let readerCancelled = false;
|
|
123
|
+
const cancelReader = () => {
|
|
124
|
+
if (readerCancelled) return;
|
|
125
|
+
readerCancelled = true;
|
|
126
|
+
void reader.cancel().catch(() => {}).finally(() => { reader.releaseLock(); });
|
|
127
|
+
};
|
|
128
|
+
const abort = () => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
cancelReader();
|
|
131
|
+
controller.error(new DOMException("Provider stream cancelled", signal?.reason?.name === "TimeoutError" ? "TimeoutError" : "AbortError"));
|
|
132
|
+
void finish(signal?.reason?.name === "TimeoutError" ? "timeout" : "cancelled");
|
|
133
|
+
};
|
|
134
|
+
const delta = (kind, text) => {
|
|
135
|
+
if (typeof text !== "string") invalid("text_type");
|
|
136
|
+
if (!text) return;
|
|
137
|
+
retain(text);
|
|
138
|
+
if (kind === "message") firstToken();
|
|
139
|
+
let entry = live.get(kind);
|
|
140
|
+
if (!entry) {
|
|
141
|
+
const item = kind === "message"
|
|
142
|
+
? { type: kind, id: `msg_${crypto.randomUUID()}`, role: "assistant", status: "in_progress", content: [] }
|
|
143
|
+
: { type: kind, id: `rs_${crypto.randomUUID()}`, status: "in_progress", summary: [], content: [] };
|
|
144
|
+
entry = { item, index: live.size, text: "" };
|
|
145
|
+
live.set(kind, entry);
|
|
146
|
+
emit("response.output_item.added", { output_index: entry.index, item });
|
|
147
|
+
emit("response.content_part.added", { output_index: entry.index, item_id: item.id, content_index: 0,
|
|
148
|
+
part: kind === "message" ? { type: "output_text", text: "", annotations: [] } : { type: "reasoning_text", text: "" } });
|
|
149
|
+
}
|
|
150
|
+
entry.text += text;
|
|
151
|
+
emit(kind === "message" ? "response.output_text.delta" : "response.reasoning_text.delta",
|
|
152
|
+
{ output_index: entry.index, item_id: entry.item.id, content_index: 0, delta: text });
|
|
153
|
+
};
|
|
154
|
+
// Tool declarations/arguments stay private until the existing normalizer has
|
|
155
|
+
// checked aliases, JSON, IDs and completeness. The parallel request bit is a
|
|
156
|
+
// generation preference; the host scheduler owns execution concurrency.
|
|
157
|
+
const complete = async result => {
|
|
158
|
+
const response = checkedNormalize(result);
|
|
159
|
+
response.id = id;
|
|
160
|
+
const output = [];
|
|
161
|
+
for (const [kind, entry] of live) {
|
|
162
|
+
const item = response.output.find(value => value.type === kind);
|
|
163
|
+
if (item?.content?.[0]?.text !== entry.text) invalid("output_text_mismatch");
|
|
164
|
+
item.id = entry.item.id;
|
|
165
|
+
output.push(item);
|
|
166
|
+
}
|
|
167
|
+
output.push(...response.output.filter(item => !live.has(item.type)));
|
|
168
|
+
response.output = output;
|
|
169
|
+
for (const event of responseEvents(response)) {
|
|
170
|
+
if (event.type === "response.created") continue;
|
|
171
|
+
const entry = [...live.values()].find(value => value.index === event.output_index);
|
|
172
|
+
if (entry && (event.type.endsWith(".added") || event.type.endsWith(".delta"))) continue;
|
|
173
|
+
const { type, sequence_number: _sequence, ...fields } = event;
|
|
174
|
+
if ((["response.output_text.delta", "response.function_call_arguments.delta", "response.custom_tool_call_input.delta"].includes(type) && fields.delta)
|
|
175
|
+
|| (type === "response.output_item.added" && ["function_call", "custom_tool_call", "tool_search_call"].includes(fields.item?.type))) firstToken();
|
|
176
|
+
emit(type, fields);
|
|
177
|
+
}
|
|
178
|
+
finalSeen = true;
|
|
179
|
+
cancelReader();
|
|
180
|
+
await finish("success");
|
|
181
|
+
controller.close();
|
|
182
|
+
};
|
|
183
|
+
const chat = async value => {
|
|
184
|
+
// Workers AI appends its aggregate usage as a binding-specific envelope,
|
|
185
|
+
// after the Chat finish chunk and before [DONE]. It is metadata, not output.
|
|
186
|
+
if (source.format === "workers_ai_chat" && value && Object.hasOwn(value, "response")) {
|
|
187
|
+
if (!finishReason || bindingUsageSeen || value.response !== "" || !value.usage
|
|
188
|
+
|| typeof value.usage !== "object" || Array.isArray(value.usage)
|
|
189
|
+
|| Object.keys(value).some(key => !["response", "usage"].includes(key))) invalid("binding_usage_trailer");
|
|
190
|
+
bindingUsageSeen = true;
|
|
191
|
+
usage = value.usage;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (bindingUsageSeen || !value || value.error || !Array.isArray(value.choices) || value.choices.length > 1) invalid("chat_envelope");
|
|
195
|
+
if (value.usage != null) usage = value.usage;
|
|
196
|
+
const choice = value.choices[0];
|
|
197
|
+
if (!choice) { if (!finishReason || value.usage == null) invalid("chat_empty_choice"); return; }
|
|
198
|
+
if (choice.index !== undefined && choice.index !== 0) invalid("chat_choice_index");
|
|
199
|
+
const part = choice.delta;
|
|
200
|
+
if (!part || typeof part !== "object" || Array.isArray(part) || part.refusal
|
|
201
|
+
|| (part.role != null && part.role !== "assistant")) invalid("chat_delta");
|
|
202
|
+
if (finishReason) {
|
|
203
|
+
// OpenRouter repeats its finish choice with an empty delta on the usage
|
|
204
|
+
// trailer before [DONE]. Admit metadata only, never additional output or
|
|
205
|
+
// a changed terminal reason after the first finish chunk.
|
|
206
|
+
if (value.usage == null || choice.finish_reason !== finishReason
|
|
207
|
+
|| Object.entries(part).some(([field, fragment]) => field === "role" ? fragment != null && fragment !== "assistant"
|
|
208
|
+
: !["content", "reasoning_content", "reasoning"].includes(field) || (fragment !== null && fragment !== ""))) invalid("chat_usage_trailer");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (part.content != null) delta("message", part.content);
|
|
212
|
+
let reasoning;
|
|
213
|
+
try { reasoning = chatReasoningText(part); } catch { invalid("chat_reasoning"); }
|
|
214
|
+
delta("reasoning", reasoning);
|
|
215
|
+
if (part.reasoning_details != null) {
|
|
216
|
+
if (!Array.isArray(part.reasoning_details) || part.reasoning_details.some(d => !d || typeof d !== "object" || Array.isArray(d))) invalid("chat_reasoning_details");
|
|
217
|
+
retain(JSON.stringify(part.reasoning_details));
|
|
218
|
+
reasoningDetails.push(...part.reasoning_details);
|
|
219
|
+
}
|
|
220
|
+
if (part.tool_calls != null) {
|
|
221
|
+
if (!Array.isArray(part.tool_calls)) invalid("tool_calls_type");
|
|
222
|
+
for (const fragment of part.tool_calls) {
|
|
223
|
+
if (!Number.isSafeInteger(fragment?.index) || fragment.index < 0 || fragment.index >= 1024) invalid("tool_fragment_index");
|
|
224
|
+
if (fragment.type != null && fragment.type !== "function") invalid("tool_fragment_type");
|
|
225
|
+
let call = calls.get(fragment.index);
|
|
226
|
+
if (!call) { call = { type: "function", function: { name: "", arguments: "" } }; calls.set(fragment.index, call); }
|
|
227
|
+
if (fragment.id != null) {
|
|
228
|
+
if (typeof fragment.id !== "string" || !fragment.id) invalid("tool_fragment_id");
|
|
229
|
+
if (call.id && call.id !== fragment.id) invalid("tool_fragment_id_changed");
|
|
230
|
+
call.id = fragment.id;
|
|
231
|
+
}
|
|
232
|
+
for (const field of ["name", "arguments"]) if (fragment.function?.[field] != null) {
|
|
233
|
+
if (typeof fragment.function[field] !== "string") invalid("tool_fragment_field_type");
|
|
234
|
+
retain(fragment.function[field]);
|
|
235
|
+
call.function[field] += fragment.function[field];
|
|
236
|
+
if (call.function[field].length > 4 * 1024 * 1024) invalid("tool_fragment_size");
|
|
237
|
+
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (choice.finish_reason != null) {
|
|
242
|
+
if (!["stop", "tool_calls", "length", "content_filter"].includes(choice.finish_reason)) invalid("chat_finish_reason");
|
|
243
|
+
finishReason = choice.finish_reason;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const native = async value => {
|
|
247
|
+
if (!value || typeof value.type !== "string" || value.error) invalid("native_envelope");
|
|
248
|
+
if (value.type === "response.output_item.added") {
|
|
249
|
+
if (!Number.isSafeInteger(value.output_index) || value.output_index < 0 || value.output_index >= 1024 || nativeItems.has(value.output_index)
|
|
250
|
+
|| !value.item || typeof value.item.id !== "string" || !value.item.id || !["message", "reasoning", "function_call"].includes(value.item.type)
|
|
251
|
+
|| [...nativeItems.values()].some(item => item.id === value.item.id)
|
|
252
|
+
|| (value.item.type === "message" && value.item.role !== "assistant")) invalid("native_item");
|
|
253
|
+
nativeItems.set(value.output_index, { ...value.item, streamedText: "", streamedArguments: "" });
|
|
254
|
+
} else if (["response.output_text.delta", "response.reasoning_text.delta", "response.reasoning_summary_text.delta", "response.function_call_arguments.delta"].includes(value.type)) {
|
|
255
|
+
const item = nativeItems.get(value.output_index);
|
|
256
|
+
if (!item || value.item_id !== item.id || typeof value.delta !== "string") invalid("native_delta");
|
|
257
|
+
if (value.type === "response.function_call_arguments.delta") {
|
|
258
|
+
if (item.type !== "function_call") invalid("native_tool_type");
|
|
259
|
+
retain(value.delta);
|
|
260
|
+
item.streamedArguments += value.delta;
|
|
261
|
+
if (item.streamedArguments.length > 4 * 1024 * 1024) invalid("native_arguments_size");
|
|
262
|
+
|
|
263
|
+
} else {
|
|
264
|
+
const kind = value.type === "response.output_text.delta" ? "message" : "reasoning";
|
|
265
|
+
if (item.type !== kind || (value.content_index !== undefined && (!Number.isSafeInteger(value.content_index) || value.content_index < 0))
|
|
266
|
+
|| (value.summary_index !== undefined && (!Number.isSafeInteger(value.summary_index) || value.summary_index < 0))) invalid("native_text_part");
|
|
267
|
+
if (value.delta) {
|
|
268
|
+
const part = `${value.output_index}:${value.type}:${value.content_index ?? value.summary_index ?? 0}`;
|
|
269
|
+
if (nativeParts.get(kind) !== part) {
|
|
270
|
+
if (nativeParts.has(kind)) delta(kind, "\n");
|
|
271
|
+
if (item.streamedText) item.streamedText += "\n";
|
|
272
|
+
nativeParts.set(kind, part);
|
|
273
|
+
}
|
|
274
|
+
item.streamedText += value.delta;
|
|
275
|
+
delta(kind, value.delta);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} else if (["response.completed", "response.incomplete"].includes(value.type)) {
|
|
279
|
+
if (value.response?.status !== value.type.slice("response.".length)) invalid("native_terminal_status");
|
|
280
|
+
for (const [index, item] of nativeItems) {
|
|
281
|
+
const final = value.response?.output?.[index];
|
|
282
|
+
if (!final || item.id !== final.id || item.type !== final.type) invalid("native_terminal_item");
|
|
283
|
+
if (item.streamedText) {
|
|
284
|
+
const parts = item.type === "message" ? final.content : [...(final.summary ?? []), ...(final.content ?? [])];
|
|
285
|
+
if (!Array.isArray(parts) || parts.map(part => part?.text).join("\n") !== item.streamedText) invalid("native_terminal_text");
|
|
286
|
+
}
|
|
287
|
+
if (item.type === "function_call" && (item.streamedArguments !== final.arguments
|
|
288
|
+
|| item.call_id !== final.call_id || item.name !== final.name)) invalid("native_terminal_tool");
|
|
289
|
+
}
|
|
290
|
+
let result;
|
|
291
|
+
try { result = fromBindingResponsesResult(value.response); }
|
|
292
|
+
catch { invalid("native_normalization"); }
|
|
293
|
+
await complete(result);
|
|
294
|
+
} else if (!["response.created", "response.in_progress", "response.queued", "response.output_item.done",
|
|
295
|
+
"response.content_part.added", "response.content_part.done", "response.output_text.done", "response.reasoning_text.done",
|
|
296
|
+
"response.reasoning_summary_part.added", "response.reasoning_summary_part.done", "response.reasoning_summary_text.done",
|
|
297
|
+
"response.function_call_arguments.done"].includes(value.type)) invalid("native_event_type");
|
|
298
|
+
};
|
|
299
|
+
const body = new ReadableStream({
|
|
300
|
+
start(value) {
|
|
301
|
+
controller = value;
|
|
302
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
303
|
+
if (signal?.aborted) abort();
|
|
304
|
+
},
|
|
305
|
+
async pull() {
|
|
306
|
+
if (settled) return;
|
|
307
|
+
try {
|
|
308
|
+
if (!started) {
|
|
309
|
+
started = true;
|
|
310
|
+
// Obtain canonical model identity without admitting provider data.
|
|
311
|
+
const base = checkedNormalize({ choices: [{ message: { content: "" }, finish_reason: "stop" }] }, true);
|
|
312
|
+
emit("response.created", { response: { ...base, id, status: "in_progress", output: [], usage: null, end_turn: false } });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const before = sequence;
|
|
316
|
+
do {
|
|
317
|
+
const record = await iterator.next();
|
|
318
|
+
if (settled) return;
|
|
319
|
+
if (record.done) invalid("terminal_missing");
|
|
320
|
+
if (record.value.data === "[DONE]") {
|
|
321
|
+
if (!["chat", "workers_ai_chat"].includes(source.format) || !finishReason) invalid("terminal_done");
|
|
322
|
+
const tool_calls = [...calls].sort(([a], [b]) => a - b).map(([index, call], position) => {
|
|
323
|
+
if (index !== position) invalid("tool_terminal_index");
|
|
324
|
+
if (!call.id) invalid("tool_terminal_id_missing");
|
|
325
|
+
return call;
|
|
326
|
+
});
|
|
327
|
+
await complete({ choices: [{ message: { content: live.get("message")?.text ?? "",
|
|
328
|
+
reasoning_content: live.get("reasoning")?.text ?? "", ...(reasoningDetails.length ? { reasoning_details: reasoningDetails } : {}), tool_calls }, finish_reason: finishReason }], usage });
|
|
329
|
+
} else {
|
|
330
|
+
let value;
|
|
331
|
+
try { value = JSON.parse(record.value.data); } catch { invalid("frame_json"); }
|
|
332
|
+
if (source.format === "responses") {
|
|
333
|
+
if (record.value.event && record.value.event !== value.type) invalid("native_event_mismatch");
|
|
334
|
+
await native(value);
|
|
335
|
+
} else await chat(value);
|
|
336
|
+
}
|
|
337
|
+
} while (!settled && sequence === before);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (settled) return;
|
|
340
|
+
cancelReader();
|
|
341
|
+
controller.error(new Error(error instanceof StreamReadError
|
|
342
|
+
? "Responses: provider stream read failed"
|
|
343
|
+
: `Responses: invalid provider stream\nProtocol invariant: ${error instanceof StreamProtocolError ? error.code : "unknown"}`));
|
|
344
|
+
await finish(error instanceof StreamReadError ? "network_error" : "protocol_error");
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
async cancel() {
|
|
348
|
+
cancelReader();
|
|
349
|
+
if (!finalSeen) await finish("cancelled");
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
return new Response(body, { headers: { "content-type": "text/event-stream", "cache-control": "no-cache", "x-nanocodex-inference-buffering": "streaming" } });
|
|
353
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/// <reference lib="esnext.disposable" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Copy a data-only RPC result and dispose its owner, including on clone failure.
|
|
5
|
+
* Returns detached structured data; rejects RPC stubs/functions. Do not use for
|
|
6
|
+
* streams or capabilities that must survive the result (use dup/dispose there).
|
|
7
|
+
* Plain local adapters and primitive/undefined results are also accepted.
|
|
8
|
+
*/
|
|
9
|
+
export declare function consumeRpcData<T>(value: T): T extends object ? Omit<T, typeof Symbol.dispose> : T;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consume a data-only RPC result before returning or caching its detached data.
|
|
3
|
+
* Each receiving hop owns its result, even when the payload has no stubs.
|
|
4
|
+
* structuredClone rejects RPC capabilities; callers retaining capabilities must
|
|
5
|
+
* instead manage their lifetime explicitly (dup/dispose).
|
|
6
|
+
*/
|
|
7
|
+
export function consumeRpcData(value) {
|
|
8
|
+
try {
|
|
9
|
+
return structuredClone(value);
|
|
10
|
+
} finally {
|
|
11
|
+
// Plain local adapters and primitive/undefined RPC results have no disposer.
|
|
12
|
+
value?.[Symbol.dispose]?.();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Diagnostic classification only: never resolve or dispatch a new tool identity.
|
|
2
|
+
// Return local constants, never provider names, arguments or registry contents.
|
|
3
|
+
export function toolAliasFailure(returnedName, registry) {
|
|
4
|
+
if (typeof returnedName !== "string" || returnedName.length > 512) return "unknown tool alias";
|
|
5
|
+
const entries = [...registry.values()];
|
|
6
|
+
const original = entry => entry.namespace ? `${entry.namespace}.${entry.name}` : entry.name;
|
|
7
|
+
if (entries.filter(entry => returnedName === entry.name || returnedName === original(entry)).length > 1) {
|
|
8
|
+
return "ambiguous original tool alias";
|
|
9
|
+
}
|
|
10
|
+
if (entries.some(entry => entry.namespace && returnedName === `${entry.namespace}.${entry.alias}`)) return "namespaced wire tool alias";
|
|
11
|
+
if (entries.some(entry => entry.namespace && ["_", "__"].some(separator => returnedName === `${entry.namespace}${separator}${entry.name}`))) {
|
|
12
|
+
return "flattened namespace tool alias";
|
|
13
|
+
}
|
|
14
|
+
if (entries.some(entry => [entry.alias, entry.name, original(entry)].some(name => name && returnedName.length > name.length
|
|
15
|
+
&& returnedName.length % name.length === 0 && returnedName === name.repeat(returnedName.length / name.length)))) {
|
|
16
|
+
return "repeated tool alias";
|
|
17
|
+
}
|
|
18
|
+
if (/^tool_[0-9]+$/.test(returnedName)) return "unregistered wire tool alias";
|
|
19
|
+
if (returnedName === "multi_tool_use.parallel") return "parallel wrapper tool alias";
|
|
20
|
+
return "unknown tool alias";
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SpanAttributes = Record<string, string | number | boolean | undefined>;
|
|
2
|
+
export interface TraceSpan {
|
|
3
|
+
setAttribute(key: string, value: string | number | boolean): unknown;
|
|
4
|
+
setAttributes?(attributes: SpanAttributes): unknown;
|
|
5
|
+
recordException?(exception: { code: string }): void;
|
|
6
|
+
}
|
|
7
|
+
/** Only content-free metadata may be supplied. Uses individual attributes on older workerd. */
|
|
8
|
+
export declare function setSpanAttributes(span: TraceSpan, attributes: SpanAttributes): void;
|
|
9
|
+
/** Annotates the active native span when supported; does not create a span. */
|
|
10
|
+
export declare function annotateActiveSpan(attributes: SpanAttributes): void;
|
|
11
|
+
/** code must be a static operation label, never derived from a caught error. */
|
|
12
|
+
export declare function recordSpanException(span: TraceSpan, code: string): void;
|
|
13
|
+
/** Native nested spans that record sanitized failures and rethrow the original error. */
|
|
14
|
+
export declare const tracing: {
|
|
15
|
+
enterSpan<T, A extends unknown[]>(name: string, callback: (span: TraceSpan, ...args: A) => T, ...args: A): T;
|
|
16
|
+
};
|