History

clean up

Rhys Sullivan committed 7 months ago

Commit 7832dec1 parent 7fda56016 files · +375 −3138Browse files

Unified · Split

16 files
  1. packages/kernel/core/src/discovery.ts +0 −486
  2. packages/kernel/core/src/http-request-placements.ts +0 −164
  3. packages/kernel/core/src/index.test.ts +0 −684
  4. packages/kernel/core/src/index.ts +2 −6
  5. packages/kernel/core/src/isomorphic-hash.ts +0 −132
  6. packages/kernel/core/src/schema-types.test.ts +0 −98
  7. packages/kernel/core/src/schema-types.ts +0 −249
  8. packages/kernel/core/src/system-tools.ts +0 −247
  9. packages/kernel/core/src/tool-map.ts +0 −549
  10. packages/kernel/core/src/types.ts +31 −261
  11. packages/kernel/core/src/validation.ts +89 −0
  12. packages/kernel/ir/package.json +1 −5
  13. packages/kernel/ir/src/index.ts +8 −0
  14. packages/kernel/ir/src/registry.ts +34 −0
  15. packages/kernel/ir/src/serialize.ts +87 −0
  16. packages/kernel/runtime-quickjs/src/index.test.ts +123 −257
packages/kernel/core/src/discovery.ts+0−486
@@ -1,486 +0,0 @@
1−import * as Effect from "effect/Effect";
2−
3−import { toolDescriptorsFromTools } from "./tool-map";
4−import type {
5− CatalogPrimitive,
6− DescribePrimitive,
7− DiscoverPrimitive,
8− DiscoveryPrimitives,
9− SearchHit,
10− ToolCatalog,
11− ToolCatalogEntry,
12− ToolDescriptor,
13− ToolMap,
14− ToolNamespace,
15− ToolPath,
16−} from "./types";
17−
18−const tokenize = (value: string): string[] =>
19− value
20− .toLowerCase()
21− .split(/\W+/)
22− .map((token) => token.trim())
23− .filter(Boolean);
24−
25−const searchableTextForTool = (tool: ToolDescriptor): string =>
26− [
27− tool.path,
28− tool.sourceKey,
29− tool.description ?? "",
30− tool.contract?.inputTypePreview ?? "",
31− tool.contract?.outputTypePreview ?? "",
32− ]
33− .join(" ")
34− .toLowerCase();
35−
36−const descriptorContract = (
37− descriptor: ToolDescriptor,
38− includeSchemas: boolean,
39−): ToolDescriptor["contract"] | undefined => {
40− const contract = descriptor.contract;
41− if (!contract) {
42− return undefined;
43− }
44−
45− if (includeSchemas) {
46− return contract;
47− }
48−
49− return {
50− ...(contract.inputTypePreview !== undefined
51− ? { inputTypePreview: contract.inputTypePreview }
52− : {}),
53− ...(contract.outputTypePreview !== undefined
54− ? { outputTypePreview: contract.outputTypePreview }
55− : {}),
56− ...(contract.exampleInput !== undefined
57− ? { exampleInput: contract.exampleInput }
58− : {}),
59− ...(contract.exampleOutput !== undefined
60− ? { exampleOutput: contract.exampleOutput }
61− : {}),
62− };
63−};
64−
65−const projectDescriptor = (input: {
66− descriptor: ToolDescriptor;
67− includeSchemas: boolean;
68−}): ToolDescriptor => {
69− const { descriptor, includeSchemas } = input;
70−
71− if (includeSchemas) {
72− return descriptor;
73− }
74−
75− return {
76− ...descriptor,
77− ...(descriptorContract(descriptor, false)
78− ? { contract: descriptorContract(descriptor, false) }
79− : {}),
80− };
81−};
82−
83−const formatToolLine = (tool: ToolDescriptor): string =>
84− tool.description && tool.description.trim().length > 0
85− ? `- ${tool.path}: ${tool.description.trim()}`
86− : `- ${tool.path}`;
87−
88−const namespaceFromPath = (path: string): string => {
89− const [first, second] = path.split(".");
90− return second ? `${first}.${second}` : first;
91−};
92−
93−const namespaceFromCatalogEntry = (entry: ToolCatalogEntry): string =>
94− entry.namespace ?? namespaceFromPath(entry.descriptor.path);
95−
96−const searchableTextForEntry = (entry: ToolCatalogEntry): string =>
97− entry.searchText?.trim().toLowerCase() || searchableTextForTool(entry.descriptor);
98−
99−const scoreCatalogEntry = (
100− queryTokens: readonly string[],
101− entry: ToolCatalogEntry,
102−): number => {
103− if (entry.score) {
104− return entry.score(queryTokens);
105− }
106−
107− const haystack = searchableTextForEntry(entry);
108− return queryTokens.reduce(
109− (total, token) => total + (haystack.includes(token) ? 1 : 0),
110− 0,
111− );
112−};
113−
114−const mergeToolNamespaces = (
115− groups: ReadonlyArray<readonly ToolNamespace[]>,
116−): ToolNamespace[] => {
117− const merged = new Map<string, ToolNamespace>();
118−
119− for (const group of groups) {
120− for (const namespace of group) {
121− const existing = merged.get(namespace.namespace);
122− merged.set(namespace.namespace, {
123− namespace: namespace.namespace,
124− displayName: existing?.displayName ?? namespace.displayName,
125− ...(existing?.toolCount !== undefined || namespace.toolCount !== undefined
126− ? {
127− toolCount:
128− (existing?.toolCount ?? 0) + (namespace.toolCount ?? 0),
129− }
130− : {}),
131− });
132− }
133− }
134−
135− return [...merged.values()].sort((left, right) =>
136− left.namespace.localeCompare(right.namespace),
137− );
138−};
139−
140−const dedupeToolDescriptors = (
141− groups: ReadonlyArray<readonly ToolDescriptor[]>,
142−): ToolDescriptor[] => {
143− const merged = new Map<string, ToolDescriptor>();
144−
145− for (const group of groups) {
146− for (const descriptor of group) {
147− if (!merged.has(descriptor.path)) {
148− merged.set(descriptor.path, descriptor);
149− }
150− }
151− }
152−
153− return [...merged.values()].sort((left, right) =>
154− left.path.localeCompare(right.path),
155− );
156−};
157−
158−const dedupeSearchHits = (
159− groups: ReadonlyArray<readonly SearchHit[]>,
160−): SearchHit[] => {
161− const merged = new Map<string, SearchHit>();
162−
163− for (const group of groups) {
164− for (const hit of group) {
165− if (!merged.has(hit.path)) {
166− merged.set(hit.path, hit);
167− }
168− }
169− }
170−
171− return [...merged.values()].sort(
172− (left, right) => right.score - left.score || left.path.localeCompare(right.path),
173− );
174−};
175−
176−export function createToolCatalogFromTools(input: {
177− tools: ToolMap;
178− defaultNamespace?: string;
179−}): ToolCatalog {
180− return createToolCatalogFromEntries({
181− entries: toolDescriptorsFromTools({
182− tools: input.tools,
183− }).map((descriptor) => ({
184− descriptor,
185− ...(input.defaultNamespace !== undefined
186− ? { namespace: input.defaultNamespace }
187− : {}),
188− })),
189− });
190−}
191−
192−export function createToolCatalogFromEntries(input: {
193− entries: ReadonlyArray<ToolCatalogEntry>;
194−}): ToolCatalog {
195− const entries = [...input.entries];
196− const byPath = new Map(
197− entries.map((entry) => [entry.descriptor.path as string, entry]),
198− );
199− const namespaceCounts = new Map<string, number>();
200−
201− for (const entry of entries) {
202− const namespace = namespaceFromCatalogEntry(entry);
203− namespaceCounts.set(namespace, (namespaceCounts.get(namespace) ?? 0) + 1);
204− }
205−
206− return {
207− listNamespaces: ({ limit }) =>
208− Effect.succeed(
209− [...namespaceCounts.entries()]
210− .map(([namespace, toolCount]) => ({ namespace, toolCount }))
211− .slice(0, limit),
212− ),
213− listTools: ({ namespace, query, limit, includeSchemas = false }) =>
214− Effect.succeed(
215− entries
216− .filter((entry) =>
217− !namespace || namespaceFromCatalogEntry(entry) === namespace,
218− )
219− .filter((entry) => {
220− if (!query) {
221− return true;
222− }
223−
224− const haystack = searchableTextForEntry(entry);
225− return tokenize(query).every((token) => haystack.includes(token));
226− })
227− .slice(0, limit)
228− .map((entry) =>
229− projectDescriptor({
230− descriptor: entry.descriptor,
231− includeSchemas,
232− })
233− ),
234− ),
235− getToolByPath: ({ path, includeSchemas }) =>
236− Effect.succeed(
237− byPath.get(path)
238− ? projectDescriptor({
239− descriptor: byPath.get(path)!.descriptor,
240− includeSchemas,
241− })
242− : null,
243− ),
244− searchTools: ({ query, namespace, limit }) => {
245− const queryTokens = tokenize(query);
246−
247− return Effect.succeed(
248− entries
249− .filter((entry) =>
250− !namespace || namespaceFromCatalogEntry(entry) === namespace,
251− )
252− .map((entry) => ({
253− path: entry.descriptor.path,
254− score: scoreCatalogEntry(queryTokens, entry),
255− }))
256− .filter((hit) => hit.score > 0)
257− .sort((left, right) => right.score - left.score)
258− .slice(0, limit),
259− );
260− },
261− } satisfies ToolCatalog;
262−}
263−
264−export function mergeToolCatalogs(input: {
265− catalogs: ReadonlyArray<ToolCatalog>;
266−}): ToolCatalog {
267− const catalogs = [...input.catalogs];
268−
269− return {
270− listNamespaces: ({ limit }) =>
271− Effect.gen(function* () {
272− const groups = yield* Effect.forEach(
273− catalogs,
274− (catalog) => catalog.listNamespaces({ limit: Math.max(limit, limit * catalogs.length) }),
275− { concurrency: "unbounded" },
276− );
277−
278− return mergeToolNamespaces(groups).slice(0, limit);
279− }),
280−
281− listTools: ({ namespace, query, limit, includeSchemas = false }) =>
282− Effect.gen(function* () {
283− const groups = yield* Effect.forEach(
284− catalogs,
285− (catalog) =>
286− catalog.listTools({
287− ...(namespace !== undefined ? { namespace } : {}),
288− ...(query !== undefined ? { query } : {}),
289− limit: Math.max(limit, limit * catalogs.length),
290− includeSchemas,
291− }),
292− { concurrency: "unbounded" },
293− );
294−
295− return dedupeToolDescriptors(groups).slice(0, limit);
296− }),
297−
298− getToolByPath: ({ path, includeSchemas }) =>
299− Effect.gen(function* () {
300− for (const catalog of catalogs) {
301− const descriptor = yield* catalog.getToolByPath({ path, includeSchemas });
302− if (descriptor) {
303− return descriptor;
304− }
305− }
306−
307− return null;
308− }),
309−
310− searchTools: ({ query, namespace, limit }) =>
311− Effect.gen(function* () {
312− const groups = yield* Effect.forEach(
313− catalogs,
314− (catalog) =>
315− catalog.searchTools({
316− query,
317− ...(namespace !== undefined ? { namespace } : {}),
318− limit: Math.max(limit, limit * catalogs.length),
319− }),
320− { concurrency: "unbounded" },
321− );
322−
323− return dedupeSearchHits(groups).slice(0, limit);
324− }),
325− } satisfies ToolCatalog;
326−}
327−
328−export function createStaticDiscoveryFromTools(input: {
329− tools: ToolMap;
330− sourceKey?: string;
331−}): {
332− tools: ToolDescriptor[];
333− executeDescription: Effect.Effect<string, never>;
334−} {
335− const tools = toolDescriptorsFromTools({
336− tools: input.tools,
337− sourceKey: input.sourceKey,
338− });
339−
340− return {
341− tools,
342− executeDescription: Effect.succeed(buildStaticExecuteDescription({ tools })),
343− };
344−}
345−
346−export function createToolCatalogDiscovery(input: {
347− catalog: ToolCatalog;
348−}): {
349− primitives: DiscoveryPrimitives;
350− executeDescription: Effect.Effect<string, unknown>;
351−} {
352− const primitives = createDiscoveryPrimitivesFromToolCatalog({
353− catalog: input.catalog,
354− });
355−
356− return {
357− primitives,
358− executeDescription: buildDynamicExecuteDescription({
359− catalog: input.catalog,
360− }),
361− };
362−}
363−
364−export function createDiscoveryPrimitivesFromToolCatalog(input: {
365− catalog: ToolCatalog;
366−}): DiscoveryPrimitives {
367− const { catalog } = input;
368−
369− const describe: DescribePrimitive = {
370− tool: ({ path, includeSchemas = false }) =>
371− catalog.getToolByPath({ path, includeSchemas }),
372− };
373−
374− const discover: DiscoverPrimitive = ({
375− query,
376− sourceKey: _sourceKey,
377− limit = 12,
378− includeSchemas = false,
379− }) =>
380− Effect.gen(function* () {
381− const hits = yield* catalog.searchTools({
382− query,
383− limit,
384− });
385−
386− if (hits.length === 0) {
387− return {
388− bestPath: null,
389− results: [],
390− total: 0,
391− };
392− }
393−
394− const descriptors = yield* Effect.forEach(
395− hits,
396− (hit) =>
397− catalog.getToolByPath({
398− path: hit.path,
399− includeSchemas,
400− }),
401− { concurrency: "unbounded" },
402− );
403−
404− const hydrated = hits
405− .map((hit, index) => {
406− const descriptor = descriptors[index];
407− if (!descriptor) {
408− return null;
409− }
410−
411− return {
412− path: descriptor.path,
413− score: hit.score,
414− description: descriptor.description,
415− interaction: descriptor.interaction ?? "auto",
416− ...(descriptorContract(descriptor, includeSchemas)
417− ? { contract: descriptorContract(descriptor, includeSchemas) }
418− : {}),
419− };
420− })
421− .filter(Boolean) as Array<
422− Record<string, unknown> & { path: ToolPath; score: number }
423− >;
424−
425− return {
426− bestPath: hydrated[0]?.path ?? null,
427− results: hydrated,
428− total: hydrated.length,
429− };
430− });
431−
432− const catalogPrimitive: CatalogPrimitive = {
433− namespaces: ({ limit = 200 }) =>
434− catalog.listNamespaces({ limit }).pipe(
435− Effect.map((namespaces) => ({ namespaces })),
436− ),
437− tools: ({ namespace, query, limit = 200, includeSchemas = false }) =>
438− catalog.listTools({
439− ...(namespace !== undefined ? { namespace } : {}),
440− ...(query !== undefined ? { query } : {}),
441− limit,
442− includeSchemas,
443− }).pipe(
444− Effect.map((results) => ({ results })),
445− ),
446− };
447−
448− return {
449− catalog: catalogPrimitive,
450− describe,
451− discover,
452− };
453−}
454−
455−export function buildStaticExecuteDescription(input: {
456− tools: readonly ToolDescriptor[];
457−}): string {
458− return [
459− "Execute TypeScript in sandbox; call tools directly.",
460− "Available tools:",
461− ...input.tools.map(formatToolLine),
462− "Do not use fetch; use tools.* only.",
463− ].join("\n");
464−}
465−
466−export function buildDynamicExecuteDescription(input: {
467− catalog: ToolCatalog;
468−}): Effect.Effect<string, unknown> {
469− return Effect.gen(function* () {
470− const namespaces = yield* input.catalog.listNamespaces({ limit: 200 });
471−
472− return [
473− "Execute TypeScript in sandbox; call tools via discovery workflow.",
474− "Available namespaces:",
475− ...namespaces.map((namespace) => `- ${namespace.displayName ?? namespace.namespace}`),
476− "Workflow:",
477− '1) const { results, bestPath } = await tools.discover({ query: "<intent>", limit: 12 });',
478− '2) const path = bestPath ?? results[0]?.path; if (!path) return "No matching tools found.";',
479− "3) const details = await tools.describe.tool({ path, includeSchemas: true });",
480− "4) Read details.contract?.inputSchema/details.contract?.outputSchema when you need the projected shape.",
481− "5) Call selected tools.<path>(input).",
482− "The tools object is a lazy proxy, so Object.keys(tools) is not a useful way to discover capabilities.",
483− "Do not use fetch; use tools.* only.",
484− ].join("\n");
485− });
486−}
packages/kernel/core/src/http-request-placements.ts+0−164
@@ -1,164 +0,0 @@
1−export type HttpRequestPlacements = {
2− headers?: Readonly<Record<string, string>>;
3− queryParams?: Readonly<Record<string, string>>;
4− cookies?: Readonly<Record<string, string>>;
5− bodyValues?: Readonly<Record<string, string>>;
6−};
7−
8−const isRecord = (value: unknown): value is Record<string, unknown> =>
9− typeof value === "object" && value !== null && !Array.isArray(value);
10−
11−const decodeJsonPointerSegment = (value: string): string =>
12− value.replaceAll("~1", "/").replaceAll("~0", "~");
13−
14−const pathSegmentsFromPlacementPath = (value: string): ReadonlyArray<string> => {
15− const trimmed = value.trim();
16− if (trimmed.length === 0) {
17− return [];
18− }
19−
20− if (trimmed.startsWith("/")) {
21− return trimmed
22− .split("/")
23− .slice(1)
24− .map(decodeJsonPointerSegment)
25− .filter((segment) => segment.length > 0);
26− }
27−
28− return trimmed.split(".").filter((segment) => segment.length > 0);
29−};
30−
31−const findHeaderKey = (
32− headers: Readonly<Record<string, string>>,
33− name: string,
34−): string | null => {
35− const lowered = name.toLowerCase();
36− for (const key of Object.keys(headers)) {
37− if (key.toLowerCase() === lowered) {
38− return key;
39− }
40− }
41−
42− return null;
43−};
44−
45−const parseCookieHeader = (value: string): Record<string, string> => {
46− const parsed: Record<string, string> = {};
47−
48− for (const part of value.split(";")) {
49− const trimmed = part.trim();
50− if (trimmed.length === 0) {
51− continue;
52− }
53−
54− const separatorIndex = trimmed.indexOf("=");
55− if (separatorIndex === -1) {
56− parsed[trimmed] = "";
57− continue;
58− }
59−
60− const name = trimmed.slice(0, separatorIndex).trim();
61− const cookieValue = trimmed.slice(separatorIndex + 1).trim();
62− if (name.length > 0) {
63− parsed[name] = cookieValue;
64− }
65− }
66−
67− return parsed;
68−};
69−
70−const setNestedRecordValue = (
71− target: Record<string, unknown>,
72− path: ReadonlyArray<string>,
73− value: string,
74−) => {
75− let current = target;
76−
77− for (let index = 0; index < path.length - 1; index += 1) {
78− const segment = path[index]!;
79− const next = current[segment];
80− if (isRecord(next)) {
81− current = next;
82− continue;
83− }
84−
85− const replacement: Record<string, unknown> = {};
86− current[segment] = replacement;
87− current = replacement;
88− }
89−
90− current[path[path.length - 1]!] = value;
91−};
92−
93−export const applyHttpQueryPlacementsToUrl = (input: {
94− url: string | URL;
95− queryParams?: Readonly<Record<string, string>>;
96−}): URL => {
97− const url = input.url instanceof URL ? new URL(input.url.toString()) : new URL(input.url);
98−
99− for (const [key, value] of Object.entries(input.queryParams ?? {})) {
100− url.searchParams.set(key, value);
101− }
102−
103− return url;
104−};
105−
106−export const applyCookiePlacementsToHeaders = (input: {
107− headers: Readonly<Record<string, string>>;
108− cookies?: Readonly<Record<string, string>>;
109−}): Record<string, string> => {
110− const cookies = input.cookies ?? {};
111− if (Object.keys(cookies).length === 0) {
112− return { ...input.headers };
113− }
114−
115− const cookieHeaderKey = findHeaderKey(input.headers, "cookie");
116− const cookieHeaderValue = cookieHeaderKey ? input.headers[cookieHeaderKey] : null;
117− const mergedCookies = {
118− ...(cookieHeaderValue ? parseCookieHeader(cookieHeaderValue) : {}),
119− ...cookies,
120− };
121− const headers = { ...input.headers };
122−
123− headers[cookieHeaderKey ?? "cookie"] = Object.entries(mergedCookies)
124− .map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
125− .join("; ");
126−
127− return headers;
128−};
129−
130−export const applyJsonBodyPlacements = (input: {
131− body: unknown;
132− bodyValues?: Readonly<Record<string, string>>;
133− label?: string;
134−}): unknown => {
135− const bodyValues = input.bodyValues ?? {};
136− if (Object.keys(bodyValues).length === 0) {
137− return input.body;
138− }
139−
140− const root =
141− input.body == null
142− ? {}
143− : isRecord(input.body)
144− ? structuredClone(input.body)
145− : null;
146−
147− if (root === null) {
148− throw new Error(
149− `${input.label ?? "HTTP request"} auth body placements require an object JSON body`,
150− );
151− }
152−
153− for (const [rawPath, value] of Object.entries(bodyValues)) {
154− const path = pathSegmentsFromPlacementPath(rawPath);
155− if (path.length === 0) {
156− throw new Error(
157− `${input.label ?? "HTTP request"} auth body placement path cannot be empty`,
158− );
159− }
160− setNestedRecordValue(root, path, value);
161− }
162−
163− return root;
164−};
packages/kernel/core/src/index.test.ts+0−684
@@ -1,684 +0,0 @@
1−import { describe, expect, it } from "@effect/vitest";
2−import { assertInstanceOf, assertNone, assertTrue } from "@effect/vitest/utils";
3−import * as Either from "effect/Either";
4−import * as Effect from "effect/Effect";
5−import * as Fiber from "effect/Fiber";
6−
7−import * as Schema from "effect/Schema";
8−import * as TestClock from "effect/TestClock";
9−
10−import {
11− allowAllToolInteractions,
12− createToolCatalogDiscovery,
13− createToolCatalogFromEntries,
14− createToolCatalogFromTools,
15− createStaticDiscoveryFromTools,
16− createSystemToolMap,
17− makeToolInvokerFromTools,
18− mergeToolMaps,
19− standardSchemaFromJsonSchema,
20− ToolInteractionDeniedError,
21− ToolInteractionPendingError,
22− toTool,
23− type ToolCatalog,
24− type CodeExecutor,
25− type ToolDescriptor,
26− type ToolMap,
27− type ToolPath,
28−} from "./index";
29−
30−const asToolPath = (value: string): ToolPath => value as ToolPath;
31−
32−const numberPairInputSchema = Schema.standardSchemaV1(
33− Schema.Struct({
34− a: Schema.Number,
35− b: Schema.Number,
36− }),
37−);
38−
39−const titleInputSchema = Schema.standardSchemaV1(
40− Schema.Struct({
41− title: Schema.String,
42− }),
43−);
44−
45−const messageInputSchema = Schema.standardSchemaV1(
46− Schema.Struct({
47− message: Schema.String,
48− }),
49−);
50−
51−
52−describe("codemode-core", () => {
53− it.effect("builds static discovery from tool map keys", () =>
54− Effect.gen(function* () {
55− const tools = {
56− "math.add": {
57− description: "Add two numbers",
58− inputSchema: numberPairInputSchema,
59− execute: async ({ a, b }: { a: number; b: number }) => ({ sum: a + b }),
60− },
61− "issues.create": toTool({
62− tool: {
63− description: "Create issue",
64− inputSchema: titleInputSchema,
65− execute: async ({ title }: { title: string }) => ({ id: "issue_1", title }),
66− },
67− metadata: {
68− interaction: "required",
69− },
70− }),
71− } satisfies ToolMap;
72−
73− const staticDiscovery = createStaticDiscoveryFromTools({
74− tools,
75− sourceKey: "api.demo",
76− });
77−
78− expect(staticDiscovery.tools.map((tool) => tool.path)).toEqual([
79− "issues.create",
80− "math.add",
81− ]);
82−
83− const createIssueDescriptor = staticDiscovery.tools.find(
84− (tool) => tool.path === "issues.create",
85− );
86− expect(createIssueDescriptor?.interaction).toBe("required");
87− expect(createIssueDescriptor?.sourceKey).toBe("api.demo");
88− expect(yield* staticDiscovery.executeDescription).toBe(
89− [
90− "Execute TypeScript in sandbox; call tools directly.",
91− "Available tools:",
92− "- issues.create: Create issue",
93− "- math.add: Add two numbers",
94− "Do not use fetch; use tools.* only.",
95− ].join("\n"),
96− );
97− }),
98− );
99−
100−
101− it.effect("returns pending interaction error for required tools", () =>
102− Effect.gen(function* () {
103− const invoker = makeToolInvokerFromTools({
104− tools: {
105− "issues.create": toTool({
106− tool: {
107− inputSchema: titleInputSchema,
108− execute: ({ title }: { title: string }) => ({ id: "issue_1", title }),
109− },
110− metadata: {
111− interaction: "required",
112− },
113− }),
114− },
115− });
116−
117− const pendingError = yield* Effect.flip(
118− invoker.invoke({
119− path: "issues.create",
120− args: { title: "hello" },
121− }),
122− );
123−
124− assertInstanceOf(pendingError, ToolInteractionPendingError);
125− expect(pendingError.path).toBe("issues.create");
126− expect(pendingError.elicitation.mode).toBe("form");
127− }),
128− );
129−
130− it.effect("waits for elicitation response and executes when accepted", () =>
131− Effect.gen(function* () {
132− const invoker = makeToolInvokerFromTools({
133− tools: {
134− "issues.create": toTool({
135− tool: {
136− inputSchema: titleInputSchema,
137− execute: ({ title }: { title: string }) => ({ id: "issue_1", title }),
138− },
139− metadata: {
140− interaction: "required",
141− },
142− }),
143− },
144− onElicitation: () =>
145− Effect.succeed({
146− action: "accept" as const,
147− }),
148− });
149−
150− const output = yield* invoker.invoke({
151− path: "issues.create",
152− args: { title: "hello" },
153− context: { runId: "run_1", callId: "call_1" },
154− });
155−
156− expect(output).toEqual({ id: "issue_1", title: "hello" });
157− }),
158− );
159−
160− it.effect("waits through polling-style elicitation callback and then executes", () =>
161− Effect.gen(function* () {
162− let pollingSleeps = 0;
163−
164− const invoker = makeToolInvokerFromTools({
165− tools: {
166− "issues.create": toTool({
167− tool: {
168− inputSchema: titleInputSchema,
169− execute: ({ title }: { title: string }) => ({ id: "issue_1", title }),
170− },
171− metadata: {
172− interaction: "required",
173− },
174− }),
175− },
176− onElicitation: () =>
177− Effect.gen(function* () {
178− for (let attempt = 0; attempt < 3; attempt += 1) {
179− pollingSleeps += 1;
180− yield* Effect.sleep("200 seconds");
181− }
182−
183− return {
184− action: "accept" as const,
185− };
186− }),
187− });
188−
189− const fiber = yield* invoker.invoke({
190− path: "issues.create",
191− args: { title: "hello" },
192− context: { runId: "run_poll", callId: "call_poll" },
193− }).pipe(Effect.fork);
194−
195− yield* TestClock.adjust("200 seconds");
196− const firstPoll = yield* Fiber.poll(fiber);
197− assertNone(firstPoll);
198−
199− yield* TestClock.adjust("200 seconds");
200− const secondPoll = yield* Fiber.poll(fiber);
201− assertNone(secondPoll);
202−
203− yield* TestClock.adjust("200 seconds");
204− const output = yield* Fiber.join(fiber);
205−
206− expect(output).toEqual({ id: "issue_1", title: "hello" });
207− expect(pollingSleeps).toBe(3);
208− }),
209− );
210−
211− it.effect("fails when elicitation is declined", () =>
212− Effect.gen(function* () {
213− const invoker = makeToolInvokerFromTools({
214− tools: {
215− "issues.create": toTool({
216− tool: {
217− inputSchema: titleInputSchema,
218− execute: ({ title }: { title: string }) => ({ id: "issue_1", title }),
219− },
220− metadata: {
221− interaction: "required",
222− },
223− }),
224− },
225− onElicitation: () =>
226− Effect.succeed({
227− action: "decline" as const,
228− content: {
229− reason: "User declined tool execution",
230− },
231− }),
232− });
233−
234− const declinedError = yield* Effect.flip(
235− invoker.invoke({
236− path: "issues.create",
237− args: { title: "hello" },
238− }),
239− );
240−
241− assertInstanceOf(declinedError, ToolInteractionDeniedError);
242− expect(declinedError.reason).toContain("User declined");
243− }),
244− );
245−
246− it.effect("builds Standard Schema validators from JSON Schema", () =>
247− Effect.gen(function* () {
248− const tools = {
249− "math.add": {
250− description: "Add two numbers",
251− inputSchema: standardSchemaFromJsonSchema({
252− type: "object",
253− required: ["a", "b"],
254− properties: {
255− a: { type: "number" },
256− b: { type: "number" },
257− },
258− additionalProperties: false,
259− }),
260− execute: ({ a, b }: { a: number; b: number }) => ({ sum: a + b }),
261− },
262− } satisfies ToolMap;
263−
264− const invoker = makeToolInvokerFromTools({ tools });
265−
266− const success = yield* invoker.invoke({
267− path: "math.add",
268− args: { a: 1, b: 2 },
269− });
270− expect(success).toEqual({ sum: 3 });
271−
272− const failure = yield* Effect.either(
273− invoker.invoke({
274− path: "math.add",
275− args: { a: "1", b: 2 },
276− }),
277− );
278−
279− assertTrue(Either.isLeft(failure));
280− assertInstanceOf(failure.left, Error);
281− expect(failure.left.message).toContain("Input validation failed");
282− }),
283− );
284−
285− it.effect("enforces standard JSON Schema string formats", () =>
286− Effect.gen(function* () {
287− const tools = {
288− "events.create": {
289− inputSchema: standardSchemaFromJsonSchema({
290− type: "object",
291− required: ["dateFrom", "issueId", "email", "documentationUrl"],
292− properties: {
293− dateFrom: { type: "string", format: "date-time" },
294− issueId: { type: "string", format: "uuid" },
295− email: { type: "string", format: "email" },
296− documentationUrl: { type: "string", format: "uri" },
297− },
298− additionalProperties: false,
299− }),
300− execute: (input: unknown) => input,
301− },
302− } satisfies ToolMap;
303−
304− const invoker = makeToolInvokerFromTools({ tools });
305−
306− const success = yield* invoker.invoke({
307− path: "events.create",
308− args: {
309− dateFrom: "2026-03-30T12:00:00Z",
310− issueId: "2c981f4a-7318-47a3-a3d3-8c145ee8ca1f",
311− email: "rhys@example.com",
312− documentationUrl: "https://example.com/docs",
313− },
314− });
315−
316− expect(success).toEqual({
317− dateFrom: "2026-03-30T12:00:00Z",
318− issueId: "2c981f4a-7318-47a3-a3d3-8c145ee8ca1f",
319− email: "rhys@example.com",
320− documentationUrl: "https://example.com/docs",
321− });
322−
323− const failure = yield* Effect.either(
324− invoker.invoke({
325− path: "events.create",
326− args: {
327− dateFrom: "not-a-date",
328− issueId: "not-a-uuid",
329− email: "not-an-email",
330− documentationUrl: "not-a-uri",
331− },
332− }),
333− );
334−
335− assertTrue(Either.isLeft(failure));
336− assertInstanceOf(failure.left, Error);
337− expect(failure.left.message).toContain('dateFrom: format: must match format "date-time"');
338− expect(failure.left.message).toContain('issueId: format: must match format "uuid"');
339− expect(failure.left.message).toContain('email: format: must match format "email"');
340− expect(failure.left.message).toContain(
341− 'documentationUrl: format: must match format "uri"',
342− );
343− }),
344− );
345−
346− it.effect("hydrates dynamic discover results via tool catalog", () =>
347− Effect.gen(function* () {
348− const descriptors: Record<string, ToolDescriptor> = {
349− "source.docs.search": {
350− path: asToolPath("source.docs.search"),
351− sourceKey: "source.docs",
352− description: "Search docs",
353− contract: {
354− inputTypePreview: "object",
355− outputTypePreview: "object",
356− },
357− },
358− "source.issues.create": {
359− path: asToolPath("source.issues.create"),
360− sourceKey: "source.issues",
361− description: "Create issue",
362− interaction: "required",
363− contract: {
364− inputTypePreview: "object",
365− outputTypePreview: "object",
366− },
367− },
368− };
369−
370− const catalog: ToolCatalog = {
371− listNamespaces: () =>
372− Effect.succeed([
373− {
374− namespace: "docs",
375− displayName: "Docs",
376− toolCount: 1,
377− },
378− {
379− namespace: "issues",
380− displayName: "Issues",
381− toolCount: 1,
382− },
383− ]),
384− listTools: ({ namespace, limit, includeSchemas = false }) =>
385− Effect.succeed(
386− Object.values(descriptors)
387− .filter((descriptor) =>
388− !namespace
389− || descriptor.path.startsWith(`${namespace}.`)
390− || descriptor.path.startsWith(`source.${namespace}.`)
391− )
392− .slice(0, limit)
393− .map((descriptor) => ({
394− ...descriptor,
395− ...(descriptor.contract
396− ? {
397− contract: {
398− ...(descriptor.contract.inputTypePreview !== undefined
399− ? { inputTypePreview: descriptor.contract.inputTypePreview }
400− : {}),
401− ...(descriptor.contract.outputTypePreview !== undefined
402− ? { outputTypePreview: descriptor.contract.outputTypePreview }
403− : {}),
404− ...(includeSchemas && descriptor.contract.inputSchema !== undefined
405− ? { inputSchema: descriptor.contract.inputSchema }
406− : {}),
407− ...(includeSchemas && descriptor.contract.outputSchema !== undefined
408− ? { outputSchema: descriptor.contract.outputSchema }
409− : {}),
410− },
411− }
412− : {}),
413− })),
414− ),
415− getToolByPath: ({ path }) =>
416− Effect.succeed(descriptors[path] ?? null),
417− searchTools: () =>
418− Effect.succeed([
419− { path: asToolPath("source.issues.create"), score: 0.93 },
420− { path: asToolPath("source.docs.search"), score: 0.72 },
421− ]),
422− };
423−
424− const dynamic = createToolCatalogDiscovery({ catalog });
425−
426− const namespaces = yield* dynamic.primitives.catalog!.namespaces({ limit: 10 });
427− expect(namespaces.namespaces).toHaveLength(2);
428−
429− const discovered = yield* dynamic.primitives.discover!({
430− query: "create issue",
431− limit: 5,
432− });
433−
434− expect(discovered.bestPath).toBe("source.issues.create");
435− expect(discovered.results[0]?.path).toBe("source.issues.create");
436− expect(discovered.results[0]?.interaction).toBe("required");
437−
438− const executeDescription = yield* dynamic.executeDescription;
439− expect(executeDescription).toContain(
440− 'const { results, bestPath } = await tools.discover({ query: "<intent>", limit: 12 });',
441− );
442− expect(executeDescription).toContain(
443− 'const path = bestPath ?? results[0]?.path; if (!path) return "No matching tools found.";',
444− );
445− expect(executeDescription).toContain(
446− "Object.keys(tools) is not a useful way to discover capabilities.",
447− );
448− }),
449− );
450−
451− it.effect("builds a tool catalog from descriptor entries", () =>
452− Effect.gen(function* () {
453− const catalog = createToolCatalogFromEntries({
454− entries: [
455− {
456− namespace: "github.repos",
457− searchText: "github repository details get repo",
458− descriptor: {
459− path: asToolPath("github.repos.getRepo"),
460− sourceKey: "source.github",
461− description: "Get repository details",
462− contract: {
463− inputTypePreview: "{ owner: string; repo: string }",
464− outputTypePreview: "Repository",
465− inputSchema: { type: "object" },
466− },
467− },
468− },
469− {
470− namespace: "github.issues",
471− searchText: "github issues list repository issues",
472− score: (queryTokens) =>
473− queryTokens.includes("issues") && queryTokens.includes("repository")
474− ? 50
475− : 0,
476− descriptor: {
477− path: asToolPath("github.issues.list"),
478− sourceKey: "source.github",
479− description: "List repository issues",
480− contract: {
481− inputTypePreview: "{ owner: string; repo: string }",
482− outputTypePreview: "Issue[]",
483− },
484− },
485− },
486− ],
487− });
488−
489− const namespaces = yield* catalog.listNamespaces({ limit: 10 });
490− expect(namespaces).toEqual([
491− { namespace: "github.repos", toolCount: 1 },
492− { namespace: "github.issues", toolCount: 1 },
493− ]);
494−
495− const listed = yield* catalog.listTools({
496− namespace: "github.repos",
497− query: "repository details",
498− limit: 10,
499− includeSchemas: false,
500− });
501− expect(listed[0]?.path).toBe("github.repos.getRepo");
502− expect(listed[0]?.contract?.inputSchema).toBeUndefined();
503−
504− const discovered = yield* catalog.searchTools({
505− query: "repository issues",
506− limit: 10,
507− });
508− expect(discovered[0]?.path).toBe("github.issues.list");
509−
510− const described = yield* catalog.getToolByPath({
511− path: asToolPath("github.repos.getRepo"),
512− includeSchemas: true,
513− });
514− expect(described?.contract?.inputSchema).toEqual({ type: "object" });
515− }),
516− );
517−
518− it.effect("system tools can be composed as normal tools", () =>
519− Effect.gen(function* () {
520− const catalog = createToolCatalogFromTools({
521− tools: {
522− "source.issues.create": toTool({
523− tool: {
524− description: "Create issue",
525− inputSchema: titleInputSchema,
526− execute: ({ title }: { title: string }) => ({ id: "issue_1", title }),
527− },
528− metadata: {
529− interaction: "required",
530− sourceKey: "source.issues",
531− },
532− }),
533− },
534− defaultNamespace: "issues",
535− });
536−
537− const systemTools = createSystemToolMap({ catalog });
538− const allTools = mergeToolMaps([
539− {
540− "math.add": {
541− inputSchema: numberPairInputSchema,
542− execute: ({ a, b }: { a: number; b: number }) => ({ sum: a + b }),
543− },
544− },
545− systemTools,
546− ]);
547−
548− const invoker = makeToolInvokerFromTools({ tools: allTools });
549− const discovered = yield* invoker.invoke({
550− path: "discover",
551− args: { query: "create issue", limit: 5 },
552− });
553−
554− expect(discovered).toMatchObject({
555− bestPath: "source.issues.create",
556− total: 1,
557− });
558−
559− const namespaces = yield* invoker.invoke({
560− path: "catalog.namespaces",
561− args: { limit: 10 },
562− });
563−
564− expect(namespaces).toEqual({
565− namespaces: [
566− {
567− namespace: "issues",
568− toolCount: 1,
569− },
570− ],
571− });
572− }),
573− );
574−
575− it.effect("executes code against tool map via executor contract", () =>
576− Effect.gen(function* () {
577− const tools = {
578− "math.add": {
579− inputSchema: numberPairInputSchema,
580− execute: async ({ a, b }: { a: number; b: number }) => ({ sum: a + b }),
581− },
582− "notifications.send": toTool({
583− tool: {
584− inputSchema: messageInputSchema,
585− execute: async ({ message }: { message: string }) => ({ delivered: true, message }),
586− },
587− metadata: { interaction: "required" },
588− }),
589− } satisfies ToolMap;
590−
591− const executor: CodeExecutor = {
592− execute: (code, toolInvoker) =>
593− Effect.gen(function* () {
594− const math = yield* toolInvoker.invoke({
595− path: "math.add",
596− args: { a: 2, b: 3 },
597− });
598− const notification = yield* toolInvoker.invoke({
599− path: "notifications.send",
600− args: { message: "sum is 5" },
601− });
602− return {
603− result: { code, math, notification },
604− logs: ["executed"],
605− };
606− }),
607− };
608−
609− const code = "return await tools.math.add({ a: 2, b: 3 });";
610− const output = yield* executor.execute(
611− code,
612− makeToolInvokerFromTools({
613− tools,
614− onToolInteraction: allowAllToolInteractions,
615− }),
616− );
617−
618− expect(output.result).toEqual({
619− code,
620− math: { sum: 5 },
621− notification: { delivered: true, message: "sum is 5" },
622− });
623− expect(output.logs).toEqual(["executed"]);
624− }),
625− );
626−
627− it.effect("supports lazy tool invoker without passing tools", () =>
628− Effect.gen(function* () {
629− let mathCalls = 0;
630−
631− const toolInvoker = makeToolInvokerFromTools({
632− tools: {
633− "math.add": {
634− inputSchema: numberPairInputSchema,
635− execute: ({ a, b }: { a: number; b: number }) => {
636− mathCalls += 1;
637− return { sum: a + b };
638− },
639− },
640− },
641− });
642−
643− const executor: CodeExecutor = {
644− execute: (code, invoker) =>
645− Effect.gen(function* () {
646− const math = yield* invoker.invoke({
647− path: "math.add",
648− args: { a: 20, b: 22 },
649− });
650− return {
651− result: { code, math },
652− logs: ["lazy"],
653− };
654− }),
655− };
656−
657− const code = "return await tools.math.add({ a: 20, b: 22 });";
658− const output = yield* executor.execute(code, toolInvoker);
659−
660− expect(mathCalls).toBe(1);
661− expect(output.result).toEqual({
662− code,
663− math: { sum: 42 },
664− });
665− expect(output.logs).toEqual(["lazy"]);
666− }),
667− );
668−
669− it.effect("surfaces executor-reported errors", () =>
670− Effect.gen(function* () {
671− const executor: CodeExecutor = {
672− execute: (_code, _toolInvoker) =>
673− Effect.succeed({ result: null, error: "boom" }),
674− };
675−
676− const output = yield* executor.execute(
677− "return 1",
678− makeToolInvokerFromTools({ tools: {} }),
679− );
680−
681− expect(output.error).toBe("boom");
682− }),
683− );
684−});
packages/kernel/core/src/index.ts+2−6
@@ -1,8 +1,4 @@
11 export * from "./types";
2+export * from "./validation";
23 export * from "./json-schema";
3−export * from "./schema-types";
4−export * from "./tool-map";
5−export * from "./discovery";
6−export * from "./system-tools";
7−export * from "./http-request-placements";
8−export * from "./isomorphic-hash";
4+export * from "./effect-errors";
packages/kernel/core/src/isomorphic-hash.ts+0−132
@@ -1,132 +0,0 @@
1−/**
2− * Isomorphic SHA-256 implementation that works in both Node.js and the browser.
3− *
4− * Avoids importing `node:crypto`, which Vite externalizes for browser builds,
5− * causing runtime errors when this module is pulled into client bundles.
6− *
7− * Uses the same SHA-256 algorithm as `node:crypto`, producing identical hex output.
8− */
9−
10−// SHA-256 constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes
11−const K = new Uint32Array([
12− 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
13− 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
14− 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
15− 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
16− 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
17− 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
18− 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
19− 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
20−]);
21−
22−const rotr = (x: number, n: number): number => (x >>> n) | (x << (32 - n));
23−
24−const sha256Compress = (state: Uint32Array, block: Uint32Array): void => {
25− const w = new Uint32Array(64);
26− for (let i = 0; i < 16; i++) w[i] = block[i];
27− for (let i = 16; i < 64; i++) {
28− const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
29− const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
30− w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
31− }
32−
33− let a = state[0], b = state[1], c = state[2], d = state[3];
34− let e = state[4], f = state[5], g = state[6], h = state[7];
35−
36− for (let i = 0; i < 64; i++) {
37− const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
38− const ch = (e & f) ^ (~e & g);
39− const temp1 = (h + S1 + ch + K[i] + w[i]) | 0;
40− const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
41− const maj = (a & b) ^ (a & c) ^ (b & c);
42− const temp2 = (S0 + maj) | 0;
43−
44− h = g; g = f; f = e; e = (d + temp1) | 0;
45− d = c; c = b; b = a; a = (temp1 + temp2) | 0;
46− }
47−
48− state[0] = (state[0] + a) | 0;
49− state[1] = (state[1] + b) | 0;
50− state[2] = (state[2] + c) | 0;
51− state[3] = (state[3] + d) | 0;
52− state[4] = (state[4] + e) | 0;
53− state[5] = (state[5] + f) | 0;
54− state[6] = (state[6] + g) | 0;
55− state[7] = (state[7] + h) | 0;
56−};
57−
58−const utf8Encode = (str: string): Uint8Array => {
59− if (typeof TextEncoder !== "undefined") {
60− return new TextEncoder().encode(str);
61− }
62−
63− // Fallback for environments without TextEncoder
64− const bytes: number[] = [];
65− for (let i = 0; i < str.length; i++) {
66− let c = str.charCodeAt(i);
67− if (c < 0x80) {
68− bytes.push(c);
69− } else if (c < 0x800) {
70− bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
71− } else if (c >= 0xd800 && c < 0xdc00 && i + 1 < str.length) {
72− const next = str.charCodeAt(++i);
73− c = 0x10000 + ((c - 0xd800) << 10) + (next - 0xdc00);
74− bytes.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
75− } else {
76− bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
77− }
78− }
79− return new Uint8Array(bytes);
80−};
81−
82−const sha256 = (data: Uint8Array): Uint8Array => {
83− const state = new Uint32Array([
84− 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
85− 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
86− ]);
87−
88− const bitLen = data.length * 8;
89−
90− // Padding: append 1 bit, then zeros, then 64-bit big-endian length
91− const padded = new Uint8Array(Math.ceil((data.length + 9) / 64) * 64);
92− padded.set(data);
93− padded[data.length] = 0x80;
94− // Write the 64-bit big-endian bit length at the end
95− const view = new DataView(padded.buffer);
96− // For strings up to ~256 MB, the high 32 bits of the bit length are zero
97− view.setUint32(padded.length - 4, bitLen, false);
98− if (bitLen > 0xffffffff) {
99− view.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000), false);
100− }
101−
102− // Process each 64-byte block
103− const block = new Uint32Array(16);
104− for (let offset = 0; offset < padded.length; offset += 64) {
105− for (let i = 0; i < 16; i++) {
106− block[i] = view.getUint32(offset + i * 4, false);
107− }
108− sha256Compress(state, block);
109− }
110−
111− const result = new Uint8Array(32);
112− const resultView = new DataView(result.buffer);
113− for (let i = 0; i < 8; i++) {
114− resultView.setUint32(i * 4, state[i], false);
115− }
116− return result;
117−};
118−
119−const hexFromBytes = (bytes: Uint8Array): string => {
120− let hex = "";
121− for (let i = 0; i < bytes.length; i++) {
122− hex += bytes[i].toString(16).padStart(2, "0");
123− }
124− return hex;
125−};
126−
127−/**
128− * Compute a SHA-256 hex digest of a UTF-8 string.
129− * Drop-in replacement for `createHash("sha256").update(value).digest("hex")`.
130− */
131−export const sha256Hex = (value: string): string =>
132− hexFromBytes(sha256(utf8Encode(value)));
packages/kernel/core/src/schema-types.test.ts+0−98
@@ -1,98 +0,0 @@
1−import { describe, expect, it } from "@effect/vitest";
2−
3−import { typeSignatureFromSchema } from "./schema-types";
4−
5−describe("schema-types", () => {
6− it("expands local $defs refs in type signatures", () => {
7− const schema = {
8− type: "object",
9− properties: {
10− args: {
11− type: "object",
12− properties: {
13− input: {
14− $ref: "#/$defs/input",
15− },
16− },
17− required: ["input"],
18− additionalProperties: false,
19− },
20− },
21− required: ["args"],
22− additionalProperties: false,
23− $defs: {
24− input: {
25− type: "object",
26− properties: {
27− issueId: {
28− type: "string",
29− },
30− externalLink: {
31− type: "string",
32− },
33− },
34− required: ["issueId"],
35− additionalProperties: false,
36− },
37− },
38− };
39−
40− expect(typeSignatureFromSchema(schema, "unknown", Infinity)).toBe(
41− "{ args: { input: { issueId: string, externalLink?: string } } }",
42− );
43− });
44−
45− it("renders GraphQL error arrays with object item shapes", () => {
46− const schema = {
47− type: "object",
48− properties: {
49− data: {
50− type: "object",
51− properties: {
52− success: {
53− type: "boolean",
54− },
55− },
56− required: ["success"],
57− additionalProperties: false,
58− },
59− errors: {
60− type: "array",
61− items: {
62− type: "object",
63− properties: {
64− message: {
65− type: "string",
66− },
67− },
68− required: ["message"],
69− additionalProperties: true,
70− },
71− },
72− },
73− required: ["data", "errors"],
74− additionalProperties: false,
75− };
76−
77− expect(typeSignatureFromSchema(schema, "unknown", Infinity)).toContain(
78− "errors: { message: string }[]",
79− );
80− });
81−
82− it("quotes invalid object property names", () => {
83− const schema = {
84− type: "object",
85− properties: {
86− "x-request-id": {
87− type: "string",
88− },
89− },
90− required: ["x-request-id"],
91− additionalProperties: false,
92− };
93−
94− expect(typeSignatureFromSchema(schema, "unknown", Infinity)).toBe(
95− '{ "x-request-id": string }',
96− );
97− });
98−});
packages/kernel/core/src/schema-types.ts+0−249
@@ -1,249 +0,0 @@
1−type JsonSchemaRecord = Record<string, unknown>;
2−
3−export type TypeSignatureRenderOptions = {
4− maxLength?: number;
5− maxDepth?: number;
6− maxProperties?: number;
7−};
8−
9−const asRecord = (value: unknown): JsonSchemaRecord =>
10− typeof value === "object" && value !== null && !Array.isArray(value)
11− ? (value as JsonSchemaRecord)
12− : {};
13−
14−const asStringArray = (value: unknown): Array<string> =>
15− Array.isArray(value)
16− ? value.filter((item): item is string => typeof item === "string")
17− : [];
18−
19−const truncate = (value: string, maxLength: number): string =>
20− value.length <= maxLength
21− ? value
22− : `${value.slice(0, Math.max(0, maxLength - 4))} ...`;
23−
24−const VALID_IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
25−
26−const formatPropertyKey = (value: string): string =>
27− VALID_IDENTIFIER_PATTERN.test(value) ? value : JSON.stringify(value);
28−
29−const compositeLabel = (
30− key: "oneOf" | "anyOf" | "allOf",
31− schema: JsonSchemaRecord,
32− render: (input: unknown) => string,
33−): string | null => {
34− const items = Array.isArray(schema[key]) ? schema[key].map(asRecord) : [];
35− if (items.length === 0) {
36− return null;
37− }
38−
39− const labels = items
40− .map((item) => render(item))
41− .filter((label) => label.length > 0);
42−
43− if (labels.length === 0) {
44− return null;
45− }
46−
47− return labels.join(key === "allOf" ? " & " : " | ");
48−};
49−
50−const refSegments = (ref: string): Array<string> | null => {
51− if (!ref.startsWith("#/")) {
52− return null;
53− }
54−
55− return ref
56− .slice(2)
57− .split("/")
58− .map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
59−};
60−
61−const resolveLocalRef = (
62− root: JsonSchemaRecord,
63− ref: string,
64−): JsonSchemaRecord | null => {
65− const segments = refSegments(ref);
66− if (!segments || segments.length === 0) {
67− return null;
68− }
69−
70− let current: unknown = root;
71− for (const segment of segments) {
72− const record = asRecord(current);
73− if (!(segment in record)) {
74− return null;
75− }
76− current = record[segment];
77− }
78−
79− const resolved = asRecord(current);
80− return Object.keys(resolved).length > 0 ? resolved : null;
81−};
82−
83−const refFallbackLabel = (ref: string): string => {
84− const segments = refSegments(ref);
85− return segments?.at(-1) ?? ref;
86−};
87−
88−export const schemaToTypeSignatureWithOptions = (
89− input: unknown,
90− options: TypeSignatureRenderOptions = {},
91−): string => {
92− const root = asRecord(input);
93− const maxLength = options.maxLength ?? 220;
94− const boundedSummary = Number.isFinite(maxLength);
95− const maxDepth = options.maxDepth ?? (boundedSummary ? 4 : Number.POSITIVE_INFINITY);
96− const maxProperties = options.maxProperties ?? (boundedSummary ? 6 : Number.POSITIVE_INFINITY);
97−
98− const render = (
99− currentInput: unknown,
100− seenRefs: ReadonlySet<string>,
101− depthRemaining: number,
102− ): string => {
103− const schema = asRecord(currentInput);
104−
105− if (depthRemaining <= 0) {
106− if (typeof schema.title === "string" && schema.title.length > 0) {
107− return schema.title;
108− }
109−
110− if (schema.type === "array") {
111− return "unknown[]";
112− }
113−
114− if (schema.type === "object" || schema.properties) {
115− return schema.additionalProperties ? "Record<string, unknown>" : "object";
116− }
117− }
118−
119− if (typeof schema.$ref === "string") {
120− const ref = schema.$ref.trim();
121− if (ref.length === 0) {
122− return "unknown";
123− }
124−
125− if (seenRefs.has(ref)) {
126− return refFallbackLabel(ref);
127− }
128−
129− const target = resolveLocalRef(root, ref);
130− if (!target) {
131− return refFallbackLabel(ref);
132− }
133−
134− return render(target, new Set([...seenRefs, ref]), depthRemaining - 1);
135− }
136−
137− if ("const" in schema) {
138− return JSON.stringify(schema.const);
139− }
140−
141− const enumValues = Array.isArray(schema.enum) ? schema.enum : [];
142− if (enumValues.length > 0) {
143− return truncate(
144− enumValues.map((value) => JSON.stringify(value)).join(" | "),
145− maxLength,
146− );
147− }
148−
149− const composite =
150− compositeLabel("oneOf", schema, (value) => render(value, seenRefs, depthRemaining - 1))
151− ?? compositeLabel("anyOf", schema, (value) => render(value, seenRefs, depthRemaining - 1))
152− ?? compositeLabel("allOf", schema, (value) => render(value, seenRefs, depthRemaining - 1));
153− if (composite) {
154− return truncate(composite, maxLength);
155− }
156−
157− if (schema.type === "array") {
158− const itemLabel = schema.items
159− ? render(schema.items, seenRefs, depthRemaining - 1)
160− : "unknown";
161− return truncate(`${itemLabel}[]`, maxLength);
162− }
163−
164− if (schema.type === "object" || schema.properties) {
165− const properties = asRecord(schema.properties);
166− const keys = Object.keys(properties);
167− if (keys.length === 0) {
168− return schema.additionalProperties ? "Record<string, unknown>" : "object";
169− }
170−
171− const required = new Set(asStringArray(schema.required));
172− const visibleKeys = keys.slice(0, maxProperties);
173− const parts = visibleKeys.map((key) =>
174− `${formatPropertyKey(key)}${required.has(key) ? "" : "?"}: ${render(properties[key], seenRefs, depthRemaining - 1)}`
175− );
176− if (visibleKeys.length < keys.length) {
177− parts.push("...");
178− }
179−
180− return truncate(`{ ${parts.join(", ")} }`, maxLength);
181− }
182−
183− if (Array.isArray(schema.type)) {
184− return truncate(schema.type.join(" | "), maxLength);
185− }
186−
187− if (typeof schema.type === "string") {
188− return schema.type;
189− }
190−
191− return "unknown";
192− };
193−
194− return render(root, new Set(), maxDepth);
195−};
196−
197−export const schemaToTypeSignature = (
198− input: unknown,
199− maxLength: number = 220,
200−): string =>
201− schemaToTypeSignatureWithOptions(input, { maxLength });
202−
203−export const typeSignatureFromSchemaJson = (
204− schemaJson: string | undefined,
205− fallback: string,
206− maxLength: number = 220,
207−): string => {
208− if (!schemaJson) {
209− return fallback;
210− }
211−
212− try {
213− return schemaToTypeSignature(JSON.parse(schemaJson) as unknown, maxLength);
214− } catch {
215− return fallback;
216− }
217−};
218−
219−export const typeSignatureFromSchema = (
220− schema: unknown,
221− fallback: string,
222− maxLength: number = 220,
223−): string => {
224− if (schema === undefined) {
225− return fallback;
226− }
227−
228− try {
229− return schemaToTypeSignature(schema, maxLength);
230− } catch {
231− return fallback;
232− }
233−};
234−
235−export const typeSignatureFromSchemaWithOptions = (
236− schema: unknown,
237− fallback: string,
238− options: TypeSignatureRenderOptions = {},
239−): string => {
240− if (schema === undefined) {
241− return fallback;
242− }
243−
244− try {
245− return schemaToTypeSignatureWithOptions(schema, options);
246− } catch {
247− return fallback;
248− }
249−};
packages/kernel/core/src/system-tools.ts+0−247
@@ -1,247 +0,0 @@
1−import * as Effect from "effect/Effect";
2−import * as Schema from "effect/Schema";
3−
4−import { createDiscoveryPrimitivesFromToolCatalog } from "./discovery";
5−import { toTool } from "./tool-map";
6−import {
7− ToolDescriptorSchema,
8− ToolNamespaceSchema,
9− type ToolCatalog,
10− type ToolMap,
11− type ToolPath,
12−} from "./types";
13−
14−const asToolPath = (value: string): ToolPath => value as ToolPath;
15−
16−const catalogNamespacesInputSchema = Schema.standardSchemaV1(
17− Schema.Struct({
18− limit: Schema.optional(Schema.Number),
19− }),
20−);
21−
22−const catalogNamespacesOutputSchema = Schema.standardSchemaV1(
23− Schema.Struct({
24− namespaces: Schema.Array(ToolNamespaceSchema),
25− }),
26−);
27−
28−const catalogToolsInputSchema = Schema.standardSchemaV1(
29− Schema.Struct({
30− namespace: Schema.optional(Schema.String),
31− query: Schema.optional(Schema.String),
32− limit: Schema.optional(Schema.Number),
33− includeSchemas: Schema.optional(Schema.Boolean),
34− }),
35−);
36−
37−const catalogToolsOutputSchema = Schema.standardSchemaV1(
38− Schema.Struct({
39− results: Schema.Array(ToolDescriptorSchema),
40− }),
41−);
42−
43−const describeToolInputSchema = Schema.standardSchemaV1(
44− Schema.Struct({
45− path: Schema.String,
46− includeSchemas: Schema.optional(Schema.Boolean),
47− }),
48−);
49−
50−const describeToolOutputSchema = Schema.standardSchemaV1(
51− Schema.NullOr(ToolDescriptorSchema),
52−);
53−
54−const discoverInputSchema = Schema.standardSchemaV1(
55− Schema.Struct({
56− query: Schema.String,
57− limit: Schema.optional(Schema.Number),
58− includeSchemas: Schema.optional(Schema.Boolean),
59− }),
60−);
61−
62−const discoverResultItemSchema = Schema.extend(
63− ToolDescriptorSchema,
64− Schema.Struct({
65− score: Schema.Number,
66− }),
67−);
68−
69−const discoverOutputSchema = Schema.standardSchemaV1(
70− Schema.Struct({
71− bestPath: Schema.NullOr(Schema.String),
72− results: Schema.Array(discoverResultItemSchema),
73− total: Schema.Number,
74− }),
75−);
76−
77−export type DiscoverResultItem = Record<string, unknown> & {
78− path: ToolPath;
79− score: number;
80−};
81−
82−export type CreateSystemToolMapInput = {
83− catalog?: ToolCatalog;
84− getCatalog?: () => ToolCatalog;
85− sourceKey?: string;
86− filterDiscoverResults?: (
87− results: readonly DiscoverResultItem[],
88− ) => Promise<readonly DiscoverResultItem[]> | readonly DiscoverResultItem[];
89−};
90−
91−export const createSystemToolMap = (
92− input: CreateSystemToolMapInput,
93−): ToolMap => {
94− const sourceKey = input.sourceKey ?? "system";
95− const getCatalog = (): ToolCatalog => {
96− if (input.catalog) {
97− return input.catalog;
98− }
99− if (input.getCatalog) {
100− return input.getCatalog();
101− }
102−
103− throw new Error("createSystemToolMap requires a catalog or getCatalog");
104− };
105− const getPrimitives = () =>
106− createDiscoveryPrimitivesFromToolCatalog({
107− catalog: getCatalog(),
108− }) as Required<ReturnType<typeof createDiscoveryPrimitivesFromToolCatalog>>;
109−
110− const tools: ToolMap = {};
111−
112− tools["catalog.namespaces"] = toTool({
113− tool: {
114− description: "List available namespaces with display names and tool counts",
115− inputSchema: catalogNamespacesInputSchema,
116− outputSchema: catalogNamespacesOutputSchema,
117− execute: ({ limit }: { limit?: number }) =>
118− Effect.runPromise(
119− getPrimitives().catalog.namespaces((limit !== undefined ? { limit } : {})),
120− ),
121− },
122− metadata: {
123− sourceKey,
124− interaction: "auto",
125− },
126− });
127−
128− tools["catalog.tools"] = toTool({
129− tool: {
130− description: "List tools with optional namespace and query filters",
131− inputSchema: catalogToolsInputSchema,
132− outputSchema: catalogToolsOutputSchema,
133− execute: (
134− toolInput: {
135− namespace?: string;
136− query?: string;
137− limit?: number;
138− includeSchemas?: boolean;
139− },
140− ) =>
141− Effect.runPromise(
142− getPrimitives().catalog.tools({
143− ...(toolInput.namespace !== undefined
144− ? { namespace: toolInput.namespace }
145− : {}),
146− ...(toolInput.query !== undefined ? { query: toolInput.query } : {}),
147− ...(toolInput.limit !== undefined ? { limit: toolInput.limit } : {}),
148− ...(toolInput.includeSchemas !== undefined
149− ? { includeSchemas: toolInput.includeSchemas }
150− : {}),
151− }),
152− ),
153− },
154− metadata: {
155− sourceKey,
156− interaction: "auto",
157− },
158− });
159−
160− tools["describe.tool"] = toTool({
161− tool: {
162− description: "Get metadata and optional schemas for a tool path",
163− inputSchema: describeToolInputSchema,
164− outputSchema: describeToolOutputSchema,
165− execute: ({ path, includeSchemas }: { path: string; includeSchemas?: boolean }) =>
166− Effect.runPromise(
167− getPrimitives().describe.tool({
168− path: asToolPath(path),
169− ...(includeSchemas !== undefined ? { includeSchemas } : {}),
170− }),
171− ),
172− },
173− metadata: {
174− sourceKey,
175− interaction: "auto",
176− },
177− });
178−
179− tools.discover = toTool({
180− tool: {
181− description: "Search tools by intent and return ranked matches",
182− inputSchema: discoverInputSchema,
183− outputSchema: discoverOutputSchema,
184− execute: async (
185− toolInput: {
186− query: string;
187− limit?: number;
188− includeSchemas?: boolean;
189− },
190− ) => {
191− const result = await Effect.runPromise(
192− getPrimitives().discover({
193− query: toolInput.query,
194− ...(toolInput.limit !== undefined ? { limit: toolInput.limit } : {}),
195− ...(toolInput.includeSchemas !== undefined
196− ? { includeSchemas: toolInput.includeSchemas }
197− : {}),
198− }),
199− );
200−
201− if (!input.filterDiscoverResults) {
202− return result;
203− }
204−
205− const filtered = await input.filterDiscoverResults(
206− result.results as DiscoverResultItem[],
207− );
208−
209− return {
210− ...result,
211− results: filtered,
212− bestPath: filtered[0]?.path ?? null,
213− total: filtered.length,
214− };
215− },
216− },
217− metadata: {
218− sourceKey,
219− interaction: "auto",
220− },
221− });
222−
223− return tools;
224−};
225−
226−export type MergeToolMapsOptions = {
227− conflictMode?: "throw" | "override";
228−};
229−
230−export const mergeToolMaps = (
231− maps: ReadonlyArray<ToolMap>,
232− options: MergeToolMapsOptions = {},
233−): ToolMap => {
234− const conflictMode = options.conflictMode ?? "throw";
235− const merged: ToolMap = {};
236−
237− for (const map of maps) {
238− for (const [path, tool] of Object.entries(map)) {
239− if (conflictMode === "throw" && path in merged) {
240− throw new Error(`Tool path conflict: ${path}`);
241− }
242− merged[path] = tool;
243− }
244− }
245−
246− return merged;
247−};
packages/kernel/core/src/tool-map.ts+0−549
@@ -1,549 +0,0 @@
1−import type { StandardSchemaV1 } from "@standard-schema/spec";
2−import * as Data from "effect/Data";
3−import * as Effect from "effect/Effect";
4−import * as JSONSchema from "effect/JSONSchema";
5−import { typeSignatureFromSchema } from "./schema-types";
6−
7−import type {
8− ElicitationRequest,
9− ElicitationResponse,
10− ExecutableTool,
11− OnElicitation,
12− OnToolInteraction,
13− ToolDefinition,
14− ToolDescriptor,
15− ToolElicitationRequest,
16− ToolInput,
17− ToolInteractionDecision,
18− ToolInteractionRequest,
19− ToolInvocationContext,
20− ToolInvoker,
21− ToolMap,
22− ToolMetadata,
23− ToolPath,
24−} from "./types";
25−import { kernelCoreEffectError } from "./effect-errors";
26−
27−type ResolvedTool = {
28− path: ToolPath;
29− tool: ExecutableTool;
30− metadata?: ToolMetadata;
31−};
32−
33−const asToolPath = (value: string): ToolPath => value as ToolPath;
34−
35−const toError = (cause: unknown): Error =>
36− cause instanceof Error ? cause : new Error(String(cause));
37−
38−const getSchemaValidator = (schema: unknown):
39− | ((value: unknown, options?: StandardSchemaV1.Options) =>
40− | StandardSchemaV1.Result<unknown>
41− | Promise<StandardSchemaV1.Result<unknown>>)
42− | null => {
43− if (!schema || (typeof schema !== "object" && typeof schema !== "function")) {
44− return null;
45− }
46−
47− const standard = (schema as { "~standard"?: unknown })["~standard"];
48− if (!standard || typeof standard !== "object") {
49− return null;
50− }
51−
52− const validate = (standard as { validate?: unknown }).validate;
53− return typeof validate === "function"
54− ? (validate as (
55− value: unknown,
56− options?: StandardSchemaV1.Options,
57− ) =>
58− | StandardSchemaV1.Result<unknown>
59− | Promise<StandardSchemaV1.Result<unknown>>)
60− : null;
61−};
62−
63−const formatIssuePath = (
64− path: ReadonlyArray<PropertyKey | StandardSchemaV1.PathSegment> | undefined,
65−): string => {
66− if (!path || path.length === 0) {
67− return "$";
68− }
69−
70− return path
71− .map((segment) =>
72− typeof segment === "object" && segment !== null && "key" in segment
73− ? String(segment.key)
74− : String(segment),
75− )
76− .join(".");
77−};
78−
79−const formatIssues = (issues: ReadonlyArray<StandardSchemaV1.Issue>): string =>
80− issues
81− .map((issue) => `${formatIssuePath(issue.path)}: ${issue.message}`)
82− .join("; ");
83−
84−const parseInput = (input: {
85− schema: unknown;
86− value: unknown;
87− path: string;
88−}): Effect.Effect<unknown, Error> => {
89− const validate = getSchemaValidator(input.schema);
90− if (!validate) {
91− return Effect.fail(
92− kernelCoreEffectError("tool-map", `Tool ${input.path} has no Standard Schema validator on inputSchema`),
93− );
94− }
95−
96− return Effect.tryPromise({
97− try: () => Promise.resolve(validate(input.value)),
98− catch: (cause) => kernelCoreEffectError("tool-map", `Validation error for ${input.path}: ${String(cause)}`),
99− }).pipe(
100− Effect.flatMap((result) => {
101− if ("issues" in result && result.issues) {
102− return Effect.fail(
103− kernelCoreEffectError("tool-map",
104− `Input validation failed for ${input.path}: ${formatIssues(result.issues)}`,
105− ),
106− );
107− }
108− return Effect.succeed(result.value);
109− }),
110− );
111−};
112−
113−const defaultRequiredElicitation = (path: string): ElicitationRequest => ({
114− mode: "form",
115− message: `Approval required before invoking ${path}`,
116− requestedSchema: {
117− type: "object",
118− properties: {},
119− additionalProperties: false,
120− },
121−});
122−
123−const executeDecision: ToolInteractionDecision = { kind: "execute" };
124−
125−const toElicitationDecision = (input: {
126− metadata?: ToolMetadata;
127− path: string;
128−}): ToolInteractionDecision | null => {
129− if (input.metadata?.elicitation) {
130− return {
131− kind: "elicit",
132− elicitation: input.metadata.elicitation,
133− };
134− }
135−
136− if (input.metadata?.interaction === "required") {
137− return {
138− kind: "elicit",
139− elicitation: defaultRequiredElicitation(input.path),
140− };
141− }
142−
143− return null;
144−};
145−
146−const defaultInteractionDecision = (input: {
147− metadata?: ToolMetadata;
148− path: string;
149−}): ToolInteractionDecision =>
150− toElicitationDecision(input) ?? executeDecision;
151−
152−export class ToolInteractionPendingError extends Data.TaggedError(
153− "ToolInteractionPendingError",
154−)<{
155− readonly path: string;
156− readonly elicitation: ElicitationRequest;
157− readonly interactionId?: string;
158−}> {}
159−
160−export class ToolInteractionDeniedError extends Data.TaggedError(
161− "ToolInteractionDeniedError",
162−)<{
163− readonly path: string;
164− readonly reason: string;
165−}> {}
166−
167−export const allowAllToolInteractions: OnToolInteraction = () =>
168− Effect.succeed(executeDecision);
169−
170−const evaluateInteractionDecision = (input: {
171− path: ToolPath;
172− args: unknown;
173− metadata?: ToolMetadata;
174− sourceKey: string;
175− context?: ToolInvocationContext;
176− onToolInteraction?: OnToolInteraction;
177−}): Effect.Effect<ToolInteractionDecision, unknown> => {
178− const defaultDecision = defaultInteractionDecision({
179− metadata: input.metadata,
180− path: input.path,
181− });
182−
183− if (!input.onToolInteraction) {
184− return Effect.succeed(defaultDecision);
185− }
186−
187− const request: ToolInteractionRequest = {
188− path: input.path,
189− sourceKey: input.sourceKey,
190− args: input.args,
191− metadata: input.metadata,
192− context: input.context,
193− defaultElicitation:
194− defaultDecision.kind === "elicit"
195− ? defaultDecision.elicitation
196− : null,
197− };
198−
199− return input.onToolInteraction(request);
200−};
201−
202−const isRecord = (value: unknown): value is Record<string, unknown> =>
203− typeof value === "object" && value !== null && !Array.isArray(value);
204−
205−const mergeAcceptedElicitationContent = (input: {
206− path: ToolPath;
207− args: unknown;
208− response: ElicitationResponse;
209− inputSchema: unknown;
210−}): Effect.Effect<unknown, Error> => {
211− if (!input.response.content) {
212− return Effect.succeed(input.args);
213− }
214−
215− if (!isRecord(input.args)) {
216− return Effect.fail(
217− kernelCoreEffectError("tool-map",
218− `Tool ${input.path} cannot merge elicitation content into non-object arguments`,
219− ),
220− );
221− }
222−
223− const merged = {
224− ...input.args,
225− ...input.response.content,
226− };
227−
228− return parseInput({
229− schema: input.inputSchema,
230− value: merged,
231− path: input.path,
232− });
233−};
234−
235−const declineReasonFromResponse = (input: {
236− path: ToolPath;
237− response: ElicitationResponse;
238−}): string => {
239− const reason =
240− input.response.content
241− && typeof input.response.content.reason === "string"
242− && input.response.content.reason.trim().length > 0
243− ? input.response.content.reason.trim()
244− : null;
245−
246− if (reason) {
247− return reason;
248− }
249−
250− return input.response.action === "cancel"
251− ? `Interaction cancelled for ${input.path}`
252− : `Interaction declined for ${input.path}`;
253−};
254−
255−const resolveInteractionDecision = (input: {
256− path: ToolPath;
257− args: unknown;
258− inputSchema: unknown;
259− metadata?: ToolMetadata;
260− sourceKey: string;
261− context?: ToolInvocationContext;
262− decision: ToolInteractionDecision;
263− interactionId: string;
264− onElicitation?: OnElicitation;
265−}): Effect.Effect<unknown, ToolInteractionPendingError | ToolInteractionDeniedError | Error> => {
266− if (input.decision.kind === "execute") {
267− return Effect.succeed(input.args);
268− }
269−
270− if (input.decision.kind === "decline") {
271− return Effect.fail(
272− new ToolInteractionDeniedError({
273− path: input.path,
274− reason: input.decision.reason,
275− }),
276− );
277− }
278−
279− if (!input.onElicitation) {
280− return Effect.fail(
281− new ToolInteractionPendingError({
282− path: input.path,
283− elicitation: input.decision.elicitation,
284− interactionId: input.interactionId,
285− }),
286− );
287− }
288−
289− const elicitationRequest: ToolElicitationRequest = {
290− interactionId: input.interactionId,
291− path: input.path,
292− sourceKey: input.sourceKey,
293− args: input.args,
294− metadata: input.metadata,
295− context: input.context,
296− elicitation: input.decision.elicitation,
297− };
298−
299− return input.onElicitation(elicitationRequest).pipe(
300− Effect.mapError(toError),
301− Effect.flatMap((response) => {
302− if (response.action !== "accept") {
303− return Effect.fail(
304− new ToolInteractionDeniedError({
305− path: input.path,
306− reason: declineReasonFromResponse({
307− path: input.path,
308− response,
309− }),
310− }),
311− );
312− }
313−
314− return mergeAcceptedElicitationContent({
315− path: input.path,
316− args: input.args,
317− response,
318− inputSchema: input.inputSchema,
319− });
320− }),
321− );
322−};
323−
324−export function wrapTool(input: {
325− tool: ExecutableTool;
326− metadata?: ToolMetadata;
327−}): ToolDefinition {
328− return {
329− tool: input.tool,
330− metadata: input.metadata,
331− };
332−}
333−
334−export const toTool = wrapTool;
335−export const toExecutorTool = wrapTool;
336−
337−const isToolDefinition = (value: ToolInput): value is ToolDefinition =>
338− typeof value === "object" && value !== null && "tool" in value;
339−
340−const deriveSchemaValue = (value: unknown): unknown | undefined => {
341− if (value === undefined || value === null) {
342− return undefined;
343− }
344−
345− try {
346− if (
347− (typeof value === "object" || typeof value === "function")
348− && value !== null
349− && "~standard" in value
350− ) {
351− return JSONSchema.make(value as any);
352− }
353−
354− return value;
355− } catch {
356− return undefined;
357− }
358−};
359−
360−const inferTypeFromSchemaValue = (
361− schema: unknown,
362− fallback: string,
363− maxLength: number = 240,
364−): string => typeSignatureFromSchema(schema, fallback, maxLength);
365−
366−export function createToolsFromRecord(input: {
367− tools: Record<string, ExecutableTool>;
368− sourceKey?: string;
369−}): ToolMap {
370− const { tools, sourceKey = "in_memory.tools" } = input;
371−
372− return Object.fromEntries(
373− Object.entries(tools)
374− .sort(([left], [right]) => left.localeCompare(right))
375− .map(([path, tool]) => [
376− path,
377− wrapTool({
378− tool,
379− metadata: { sourceKey },
380− }),
381− ]),
382− ) as ToolMap;
383−}
384−
385−const resolveToolsFromMap = (input: {
386− tools: ToolMap;
387− sourceKey?: string;
388−}): ResolvedTool[] => {
389− const defaultSourceKey = input.sourceKey ?? "in_memory.tools";
390−
391− return Object.entries(input.tools)
392− .sort(([left], [right]) => left.localeCompare(right))
393− .map(([path, value]) => {
394− const entry = isToolDefinition(value) ? value : { tool: value };
395− const metadata = entry.metadata
396− ? {
397− sourceKey: defaultSourceKey,
398− ...entry.metadata,
399− }
400− : { sourceKey: defaultSourceKey };
401−
402− return {
403− path: asToolPath(path),
404− tool: entry.tool,
405− metadata,
406− } satisfies ResolvedTool;
407− });
408−};
409−
410−export function toolDescriptorsFromTools(input: {
411− tools: ToolMap;
412− sourceKey?: string;
413−}): ToolDescriptor[] {
414− const resolvedTools = resolveToolsFromMap({
415− tools: input.tools,
416− sourceKey: input.sourceKey,
417− });
418−
419− return resolvedTools.map((entry) => {
420− const metadata = entry.metadata;
421− const definition = entry.tool;
422− const inputSchema =
423− metadata?.contract?.inputSchema
424− ?? deriveSchemaValue(definition.inputSchema)
425− ?? deriveSchemaValue(definition.parameters);
426− const outputSchema =
427− metadata?.contract?.outputSchema
428− ?? deriveSchemaValue(definition.outputSchema);
429− const contract = {
430− inputTypePreview:
431− metadata?.contract?.inputTypePreview ?? inferTypeFromSchemaValue(inputSchema, "unknown"),
432− outputTypePreview:
433− metadata?.contract?.outputTypePreview ?? inferTypeFromSchemaValue(outputSchema, "unknown"),
434− ...(inputSchema !== undefined ? { inputSchema } : {}),
435− ...(outputSchema !== undefined ? { outputSchema } : {}),
436− ...(metadata?.contract?.exampleInput !== undefined
437− ? { exampleInput: metadata.contract.exampleInput }
438− : {}),
439− ...(metadata?.contract?.exampleOutput !== undefined
440− ? { exampleOutput: metadata.contract.exampleOutput }
441− : {}),
442− } satisfies NonNullable<ToolDescriptor["contract"]>;
443−
444− return {
445− path: entry.path,
446− sourceKey: metadata?.sourceKey ?? "in_memory.tools",
447− description: definition.description,
448− interaction: metadata?.interaction,
449− elicitation: metadata?.elicitation,
450− contract,
451− ...(metadata?.pluginKind ? { pluginKind: metadata.pluginKind } : {}),
452− ...(metadata?.pluginData !== undefined
453− ? { pluginData: metadata.pluginData }
454− : {}),
455− } satisfies ToolDescriptor;
456− });
457−}
458−
459−const asInteractionIdPart = (value: string): string =>
460− value.replace(/[^a-zA-Z0-9._-]/g, "_");
461−
462−const createInteractionIdFactory = () => {
463− let sequence = 0;
464−
465− return (input: { path: ToolPath; context?: ToolInvocationContext }): string => {
466− sequence += 1;
467−
468− const runId =
469− typeof input.context?.runId === "string" && input.context.runId.length > 0
470− ? input.context.runId
471− : "run";
472− const callId =
473− typeof input.context?.callId === "string" && input.context.callId.length > 0
474− ? input.context.callId
475− : `call_${String(sequence)}`;
476−
477− return `${asInteractionIdPart(runId)}:${asInteractionIdPart(callId)}:${asInteractionIdPart(String(input.path))}:${String(sequence)}`;
478− };
479−};
480−
481−export const makeToolInvokerFromTools = (input: {
482− tools: ToolMap;
483− sourceKey?: string;
484− onToolInteraction?: OnToolInteraction;
485− onElicitation?: OnElicitation;
486−}): ToolInvoker => {
487− const resolvedTools = resolveToolsFromMap({
488− tools: input.tools,
489− sourceKey: input.sourceKey,
490− });
491− const byPath = new Map(resolvedTools.map((entry) => [entry.path as string, entry]));
492− const nextInteractionId = createInteractionIdFactory();
493−
494− return {
495− invoke: ({ path, args, context }) =>
496− Effect.gen(function* () {
497− const entry = byPath.get(path);
498− if (!entry) {
499− return yield* kernelCoreEffectError("tool-map", `Unknown tool path: ${path}`);
500− }
501−
502− const parsedInput = yield* parseInput({
503− schema: entry.tool.inputSchema,
504− value: args,
505− path,
506− });
507−
508− const decision = yield* evaluateInteractionDecision({
509− path: entry.path,
510− args: parsedInput,
511− metadata: entry.metadata,
512− sourceKey: entry.metadata?.sourceKey ?? "in_memory.tools",
513− context,
514− onToolInteraction: input.onToolInteraction,
515− });
516−
517− const interactionId =
518− decision.kind === "elicit" && decision.interactionId
519− ? decision.interactionId
520− : nextInteractionId({ path: entry.path, context });
521−
522− const executableInput = yield* resolveInteractionDecision({
523− path: entry.path,
524− args: parsedInput,
525− inputSchema: entry.tool.inputSchema,
526− metadata: entry.metadata,
527− sourceKey: entry.metadata?.sourceKey ?? "in_memory.tools",
528− context,
529− decision,
530− interactionId,
531− onElicitation: input.onElicitation,
532− });
533−
534− return yield* Effect.tryPromise({
535− try: () =>
536− Promise.resolve(
537− entry.tool.execute(executableInput, {
538− path: entry.path,
539− sourceKey: entry.metadata?.sourceKey ?? "in_memory.tools",
540− metadata: entry.metadata,
541− invocation: context,
542− onElicitation: input.onElicitation,
543− }),
544− ),
545− catch: (cause) => kernelCoreEffectError("tool-map", `Tool execution error for ${entry.path}: ${String(cause)}`),
546− });
547− }),
548− };
549−};
packages/kernel/core/src/types.ts+31−261
@@ -1,277 +1,42 @@
11 import type { StandardSchemaV1 } from "@standard-schema/spec";
22 import type * as Effect from "effect/Effect";
3−import * as Schema from "effect/Schema";
43
4+/** Branded tool path */
55 export type ToolPath = string & { readonly __toolPath: unique symbol };
66
7+export const asToolPath = (value: string): ToolPath => value as ToolPath;
8+
9+/** Standard Schema alias */
710 export type StandardSchema<Input = unknown, Output = unknown> =
811 StandardSchemaV1<Input, Output>;
912
10−export type ElicitationFormRequest = {
11− mode?: "form";
12− message: string;
13− requestedSchema: Record<string, unknown>;
14−};
15−
16−export type ElicitationUrlRequest = {
17− mode: "url";
18− message: string;
19− url: string;
20− elicitationId: string;
21−};
22−
23−export type ElicitationRequest =
24− | ElicitationFormRequest
25− | ElicitationUrlRequest;
26−
27−export type ElicitationAction = "accept" | "decline" | "cancel";
28−
29−export type ElicitationResponse = {
30− action: ElicitationAction;
31− content?: Record<string, unknown>;
32−};
33−
34−export type ToolInvocationContext = {
35− runId?: string;
36− callId?: string;
37− scope?: string;
38− actor?: string;
39− [key: string]: unknown;
40−};
41−
42−export type ToolInteractionRequest = {
43− path: ToolPath;
44− sourceKey: string;
45− args: unknown;
46− metadata?: ToolMetadata;
47− context?: ToolInvocationContext;
48− defaultElicitation: ElicitationRequest | null;
49−};
50−
51−export type ToolInteractionDecision =
52− | { kind: "execute" }
53− | { kind: "decline"; reason: string }
54− | {
55− kind: "elicit";
56− elicitation: ElicitationRequest;
57− interactionId?: string;
58− };
59−
60−export type ToolElicitationRequest = {
61− interactionId: string;
62− path: ToolPath;
63− sourceKey: string;
64− args: unknown;
65− metadata?: ToolMetadata;
66− context?: ToolInvocationContext;
67− elicitation: ElicitationRequest;
68−};
69−
70−export type OnToolInteraction = (
71− input: ToolInteractionRequest,
72−) => Effect.Effect<ToolInteractionDecision, unknown>;
73−
74−export type OnElicitation = (
75− input: ToolElicitationRequest,
76−) => Effect.Effect<ElicitationResponse, unknown>;
77−
78−export type ToolContract = {
79− inputTypePreview?: string;
80− outputTypePreview?: string;
81− inputSchema?: unknown;
82− outputSchema?: unknown;
83− exampleInput?: unknown;
84− exampleOutput?: unknown;
85−};
86−
87−export const ToolContractSchema = Schema.Struct({
88− inputTypePreview: Schema.optional(Schema.String),
89− outputTypePreview: Schema.optional(Schema.String),
90− inputSchema: Schema.optional(Schema.Unknown),
91− outputSchema: Schema.optional(Schema.Unknown),
92− exampleInput: Schema.optional(Schema.Unknown),
93− exampleOutput: Schema.optional(Schema.Unknown),
94−});
95−
96−export type ToolMetadata = {
97− interaction?: "auto" | "required";
98− elicitation?: ElicitationRequest;
99− contract?: ToolContract;
100− sourceKey?: string;
101− pluginKind?: string;
102− pluginData?: unknown;
103−};
104−
105−export type ToolExecutionContext = {
106− path: ToolPath;
107− sourceKey: string;
108− metadata?: ToolMetadata;
109− invocation?: ToolInvocationContext;
110− onElicitation?: OnElicitation;
111−};
112−
113−export type ExecutableTool = {
114− description?: string;
115− inputSchema: StandardSchema;
116− outputSchema?: StandardSchema;
117− parameters?: StandardSchema;
118− execute: (...args: any[]) => unknown;
119−};
120−
121−export const unknownInputSchema: StandardSchema = {
122− "~standard": {
123− version: 1,
124− vendor: "@executor/codemode-core",
125− validate: (value: unknown) => ({
126− value,
127− }),
128− },
129−};
130−
131−
132−export type ToolDefinition = {
133− tool: ExecutableTool;
134− metadata?: ToolMetadata;
135−};
136−
137−export type ToolInput = ExecutableTool | ToolDefinition;
138−
139−export type ToolMap = Record<string, ToolInput>;
140−
141−export type ToolDescriptor = {
142− path: ToolPath;
143− sourceKey: string;
144− description?: string;
145− interaction?: "auto" | "required";
146− elicitation?: ElicitationRequest;
147− contract?: ToolContract;
148− pluginKind?: string;
149− pluginData?: unknown;
150−};
151−
152−export const ToolDescriptorSchema = Schema.Struct({
153− path: Schema.String,
154− sourceKey: Schema.String,
155− description: Schema.optional(Schema.String),
156− interaction: Schema.optional(
157− Schema.Union(Schema.Literal("auto"), Schema.Literal("required")),
158− ),
159− elicitation: Schema.optional(Schema.Unknown),
160− contract: Schema.optional(ToolContractSchema),
161− pluginKind: Schema.optional(Schema.String),
162− pluginData: Schema.optional(Schema.Unknown),
163−});
164−
165−export type ToolNamespace = {
166− namespace: string;
167− displayName?: string;
168− toolCount?: number;
169−};
170−
171−export const ToolNamespaceSchema = Schema.Struct({
172− namespace: Schema.String,
173− displayName: Schema.optional(Schema.String),
174− toolCount: Schema.optional(Schema.Number),
175−});
176−
177−export type SearchHit = {
178− path: ToolPath;
179− score: number;
180−};
181−
182−export type ToolCatalogEntry = {
183− descriptor: ToolDescriptor;
184− namespace?: string;
185− searchText?: string;
186− score?: (queryTokens: readonly string[]) => number;
187−};
188−
189−export interface ToolCatalog {
190− listNamespaces(input: {
191− limit: number;
192− }): Effect.Effect<readonly ToolNamespace[], unknown>;
193−
194− listTools(input: {
195− namespace?: string;
196− query?: string;
197− limit: number;
198− includeSchemas?: boolean;
199− }): Effect.Effect<readonly ToolDescriptor[], unknown>;
200−
201− getToolByPath(input: {
202− path: ToolPath;
203− includeSchemas: boolean;
204− }): Effect.Effect<ToolDescriptor | null, unknown>;
205−
206− searchTools(input: {
207− query: string;
208− namespace?: string;
209− limit: number;
210− }): Effect.Effect<readonly SearchHit[], unknown>;
13+/** A tool that can be invoked */
14+export interface Tool {
15+ readonly path: ToolPath;
16+ readonly description?: string;
17+ readonly inputSchema: StandardSchema;
18+ readonly outputSchema?: StandardSchema;
19+ readonly execute: (
20+ input: unknown,
21+ ) => unknown | Promise<unknown>;
21122 }
21223
213−export type CatalogPrimitive = {
214− namespaces(input: {
215− limit?: number;
216− }): Effect.Effect<
217− { namespaces: readonly ToolNamespace[] },
218− unknown
219− >;
220−
221− tools(input: {
222− namespace?: string;
223− query?: string;
224− limit?: number;
225− includeSchemas?: boolean;
226− }): Effect.Effect<{ results: readonly ToolDescriptor[] }, unknown>;
227−};
228−
229−export type DescribePrimitive = {
230− tool(input: {
231− path: ToolPath;
232− includeSchemas?: boolean;
233− }): Effect.Effect<ToolDescriptor | null, unknown>;
234−};
235−
236−export type DiscoverPrimitive = (input: {
237− query: string;
238− sourceKey?: string;
239− limit?: number;
240− includeSchemas?: boolean;
241−}) => Effect.Effect<
242− {
243− bestPath: ToolPath | null;
244− results: readonly (Record<string, unknown> & {
245− path: ToolPath;
246− score: number;
247− })[];
248− total: number;
249− },
250− unknown
251−>;
252−
253−export type DiscoveryPrimitives = {
254− catalog?: CatalogPrimitive;
255− describe?: DescribePrimitive;
256− discover?: DiscoverPrimitive;
257−};
24+/** Invoke a tool by path */
25+export interface ToolInvoker {
26+ invoke(input: {
27+ path: string;
28+ args: unknown;
29+ }): Effect.Effect<unknown, unknown>;
30+}
25831
32+/** Result of executing code in a sandbox */
25933 export type ExecuteResult = {
26034 result: unknown;
26135 error?: string;
26236 logs?: string[];
26337 };
26438
265−export type ToolInvocationInput = {
266− path: string;
267− args: unknown;
268− context?: ToolInvocationContext;
269−};
270−
271−export interface ToolInvoker {
272− invoke(input: ToolInvocationInput): Effect.Effect<unknown, unknown>;
273−}
274−
39+/** Executes code in a sandboxed runtime with tool access */
27540 export interface CodeExecutor {
27641 execute(
27742 code: string,
@@ -279,8 +44,13 @@ export interface CodeExecutor { · 1 unmodified line
27944 ): Effect.Effect<ExecuteResult, unknown>;
28045 }
28146
282−export type CodeToolOutput = {
283− code: string;
284− result: unknown;
285− logs?: string[];
47+/** Accept-anything schema for tools with no input validation */
48+export const unknownInputSchema: StandardSchema = {
49+ "~standard": {
50+ version: 1,
51+ vendor: "@executor/codemode-core",
52+ validate: (value: unknown) => ({
53+ value,
54+ }),
55+ },
28656 };
packages/kernel/core/src/validation.ts+89−0
@@ -0,0 +1,89 @@
1+import type { StandardSchemaV1 } from "@standard-schema/spec";
2+import * as Effect from "effect/Effect";
3+
4+import { kernelCoreEffectError } from "./effect-errors";
5+
6+const getSchemaValidator = (
7+ schema: unknown,
8+): ((
9+ value: unknown,
10+) =>
11+ | StandardSchemaV1.Result<unknown>
12+ | Promise<StandardSchemaV1.Result<unknown>>) | null => {
13+ if (!schema || (typeof schema !== "object" && typeof schema !== "function")) {
14+ return null;
15+ }
16+
17+ const standard = (schema as { "~standard"?: unknown })["~standard"];
18+ if (!standard || typeof standard !== "object") {
19+ return null;
20+ }
21+
22+ const validate = (standard as { validate?: unknown }).validate;
23+ return typeof validate === "function"
24+ ? (validate as (
25+ value: unknown,
26+ ) =>
27+ | StandardSchemaV1.Result<unknown>
28+ | Promise<StandardSchemaV1.Result<unknown>>)
29+ : null;
30+};
31+
32+const formatIssuePath = (
33+ path: ReadonlyArray<PropertyKey | StandardSchemaV1.PathSegment> | undefined,
34+): string => {
35+ if (!path || path.length === 0) {
36+ return "$";
37+ }
38+
39+ return path
40+ .map((segment) =>
41+ typeof segment === "object" && segment !== null && "key" in segment
42+ ? String(segment.key)
43+ : String(segment),
44+ )
45+ .join(".");
46+};
47+
48+const formatIssues = (issues: ReadonlyArray<StandardSchemaV1.Issue>): string =>
49+ issues
50+ .map((issue) => `${formatIssuePath(issue.path)}: ${issue.message}`)
51+ .join("; ");
52+
53+/** Validate a value against a Standard Schema */
54+export const validateInput = (input: {
55+ schema: unknown;
56+ value: unknown;
57+ path: string;
58+}): Effect.Effect<unknown, Error> => {
59+ const validate = getSchemaValidator(input.schema);
60+ if (!validate) {
61+ return Effect.fail(
62+ kernelCoreEffectError(
63+ "validation",
64+ `Tool ${input.path} has no Standard Schema validator on inputSchema`,
65+ ),
66+ );
67+ }
68+
69+ return Effect.tryPromise({
70+ try: () => Promise.resolve(validate(input.value)),
71+ catch: (cause) =>
72+ kernelCoreEffectError(
73+ "validation",
74+ `Validation error for ${input.path}: ${String(cause)}`,
75+ ),
76+ }).pipe(
77+ Effect.flatMap((result) => {
78+ if ("issues" in result && result.issues) {
79+ return Effect.fail(
80+ kernelCoreEffectError(
81+ "validation",
82+ `Input validation failed for ${input.path}: ${formatIssues(result.issues)}`,
83+ ),
84+ );
85+ }
86+ return Effect.succeed(result.value);
87+ }),
88+ );
89+};
packages/kernel/ir/package.json+1−5
@@ -4,10 +4,7 @@ · 3 unmodified lines
44 "type": "module",
55 "version": "0.0.0",
66 "exports": {
7− ".": "./src/index.ts",
8− "./catalog": "./src/catalog.ts",
9− "./ids": "./src/ids.ts",
10− "./model": "./src/model.ts"
7+ ".": "./src/index.ts"
118 },
129 "scripts": {
1310 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
@@ -15,7 +12,6 @@ · 1 unmodified line
1512 "test:watch": "vitest"
1613 },
1714 "dependencies": {
18− "@executor/codemode-core": "workspace:*",
1915 "effect": "catalog:"
2016 },
2117 "devDependencies": {
packages/kernel/ir/src/index.ts+8−0
@@ -0,0 +1,8 @@
1+export {
2+ ToolRegistration,
3+ SerializedCatalog,
4+ type LiveToolRegistration,
5+ type LiveCatalog,
6+} from "./registry";
7+
8+export { serialize, deserializeToJsonSchema } from "./serialize";
packages/kernel/ir/src/registry.ts+34−0
@@ -0,0 +1,34 @@
1+import { Schema } from "effect";
2+import type { JsonSchema7Root } from "effect/JSONSchema";
3+
4+export class ToolRegistration extends Schema.Class<ToolRegistration>("ToolRegistration")({
5+ path: Schema.String,
6+ description: Schema.optional(Schema.String),
7+ tags: Schema.optional(Schema.Array(Schema.String)),
8+ namespace: Schema.optional(Schema.String),
9+ input: Schema.optional(Schema.String),
10+ output: Schema.optional(Schema.String),
11+ error: Schema.optional(Schema.String),
12+}) {}
13+
14+export class SerializedCatalog extends Schema.Class<SerializedCatalog>("SerializedCatalog")({
15+ version: Schema.Literal("v4.1"),
16+ types: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
17+ tools: Schema.Array(ToolRegistration),
18+}) {}
19+
20+export interface LiveToolRegistration {
21+ readonly path: string;
22+ readonly description?: string;
23+ readonly tags?: readonly string[];
24+ readonly namespace?: string;
25+ readonly input?: Schema.Schema.AnyNoContext;
26+ readonly output?: Schema.Schema.AnyNoContext;
27+ readonly error?: Schema.Schema.AnyNoContext;
28+}
29+
30+export interface LiveCatalog {
31+ readonly version: "v4.1";
32+ readonly types: Record<string, JsonSchema7Root>;
33+ readonly tools: readonly LiveToolRegistration[];
34+}
packages/kernel/ir/src/serialize.ts+87−0
@@ -0,0 +1,87 @@
1+import { JSONSchema, SchemaAST } from "effect";
2+import { Option } from "effect";
3+import type { JsonSchema7, JsonSchema7Root } from "effect/JSONSchema";
4+import type { LiveCatalog, SerializedCatalog } from "./registry";
5+
6+function getSchemaIdentifier(schema: { ast: SchemaAST.AST }): string | undefined {
7+ return Option.getOrUndefined(SchemaAST.getJSONIdentifier(schema.ast));
8+}
9+
10+export function serialize(catalog: LiveCatalog): typeof SerializedCatalog.Type {
11+ const defs: Record<string, JsonSchema7> = {};
12+
13+ function schemaToRef(schema: { ast: SchemaAST.AST } | undefined): string | undefined {
14+ if (!schema) return undefined;
15+
16+ const identifier = getSchemaIdentifier(schema);
17+ const jsonSchema = JSONSchema.fromAST(schema.ast, {
18+ definitions: defs,
19+ target: "jsonSchema2020-12",
20+ });
21+
22+ if (identifier) {
23+ return identifier;
24+ }
25+
26+ // Inline schema without an identifier — store under a generated key
27+ const key = `__inline_${Object.keys(defs).length}`;
28+ defs[key] = jsonSchema;
29+ return key;
30+ }
31+
32+ const tools = catalog.tools.map((tool) => ({
33+ path: tool.path,
34+ description: tool.description,
35+ tags: tool.tags ? [...tool.tags] : undefined,
36+ namespace: tool.namespace,
37+ input: schemaToRef(tool.input),
38+ output: schemaToRef(tool.output),
39+ error: schemaToRef(tool.error),
40+ }));
41+
42+ return {
43+ version: "v4.1" as const,
44+ types: defs as Record<string, unknown>,
45+ tools,
46+ };
47+}
48+
49+export function deserializeToJsonSchema(
50+ serialized: typeof SerializedCatalog.Type,
51+): {
52+ tools: ReadonlyArray<{
53+ path: string;
54+ description?: string;
55+ tags?: ReadonlyArray<string>;
56+ namespace?: string;
57+ input?: JsonSchema7Root;
58+ output?: JsonSchema7Root;
59+ error?: JsonSchema7Root;
60+ }>;
61+ types: Record<string, unknown>;
62+} {
63+ const types = serialized.types;
64+
65+ function resolveRef(ref: string | undefined): JsonSchema7Root | undefined {
66+ if (!ref) return undefined;
67+ const schema = types[ref];
68+ if (!schema) return undefined;
69+ return {
70+ ...(schema as JsonSchema7Root),
71+ $defs: types as Record<string, JsonSchema7>,
72+ };
73+ }
74+
75+ return {
76+ tools: serialized.tools.map((tool) => ({
77+ path: tool.path,
78+ description: tool.description,
79+ tags: tool.tags,
80+ namespace: tool.namespace,
81+ input: resolveRef(tool.input),
82+ output: resolveRef(tool.output),
83+ error: resolveRef(tool.error),
84+ })),
85+ types,
86+ };
87+}
packages/kernel/runtime-quickjs/src/index.test.ts+123−257
@@ -1,314 +1,180 @@
11 import { describe, expect, it } from "@effect/vitest";
22 import * as Effect from "effect/Effect";
3−import * as Schema from "effect/Schema";
4−
5−import {
6− allowAllToolInteractions,
7− makeToolInvokerFromTools,
8− toExecutorTool,
9−} from "@executor/codemode-core";
103
4+import type { ToolInvoker } from "@executor/codemode-core";
115 import { makeQuickJsExecutor } from "./index";
126
13−const numberPairInputSchema = Schema.standardSchemaV1(
14− Schema.Struct({
15− a: Schema.Number,
16− b: Schema.Number,
17− }),
18−);
19−
20−const messageInputSchema = Schema.standardSchemaV1(
21− Schema.Struct({
22− message: Schema.String,
23− }),
24−);
25−
26−const discoverInputSchema = Schema.standardSchemaV1(
27− Schema.Struct({
28− query: Schema.String,
29− limit: Schema.optional(Schema.Number),
30− }),
31−);
32−
33−const describeToolInputSchema = Schema.standardSchemaV1(
34− Schema.Struct({
35− path: Schema.String,
36− includeSchemas: Schema.optional(Schema.Boolean),
37− }),
38−);
39−
40−const repoInputSchema = Schema.standardSchemaV1(
41− Schema.Struct({
42− owner: Schema.String,
43− repo: Schema.String,
44− }),
45−);
46−
47−const tools = {
48− "math.add": {
49− description: "Add two numbers",
50− inputSchema: numberPairInputSchema,
51− execute: ({ a, b }: { a: number; b: number }) => ({ sum: a + b }),
7+const makeTestInvoker = (
8+ handlers: Record<string, (args: unknown) => unknown>,
9+): ToolInvoker => ({
10+ invoke: ({ path, args }) => {
11+ const handler = handlers[path];
12+ if (!handler) {
13+ return Effect.fail(new Error(`Unknown tool: ${path}`));
14+ }
15+ return Effect.try(() => handler(args));
5216 },
53− "notifications.send": toExecutorTool({
54− tool: {
55− description: "Send a message",
56− inputSchema: messageInputSchema,
57− execute: ({ message }: { message: string }) => ({
58− delivered: true,
59− message,
60− }),
61− },
62− metadata: {
63− interaction: "required",
64− },
65− }),
66−};
67−
68−describe("runtime-quickjs", () => {
69− it.effect("executes simple code and returns result", () =>
70− Effect.gen(function* () {
71− const executor = makeQuickJsExecutor();
72− const toolInvoker = makeToolInvokerFromTools({ tools });
73−
74− const output = yield* executor.execute(
75− "return 1 + 2;",
76− toolInvoker,
77− );
78−
79− expect(output.result).toBe(3);
80− expect(output.error).toBeUndefined();
81− }),
82− );
83−
84− it.effect("executes code with tool calls", () =>
85− Effect.gen(function* () {
86− const executor = makeQuickJsExecutor();
87− const toolInvoker = makeToolInvokerFromTools({
88− tools,
89− onToolInteraction: allowAllToolInteractions,
90− });
91−
92− const output = yield* executor.execute(
93− [
94− "const math = await tools.math.add({ a: 19, b: 23 });",
95− "await tools.notifications.send({ message: `sum is ${math.sum}` });",
96− "return math;",
97− ].join("\n"),
98− toolInvoker,
99− );
100−
101− expect(output.result).toEqual({ sum: 42 });
102− expect(output.error).toBeUndefined();
103− }),
104− );
105−
106− it.effect("captures console output in logs", () =>
107− Effect.gen(function* () {
108− const executor = makeQuickJsExecutor();
109− const toolInvoker = makeToolInvokerFromTools({ tools });
110−
111− const output = yield* executor.execute(
112− [
113− 'console.log("hello from quickjs");',
114− 'console.warn("a warning");',
115− 'console.error("an error");',
116− "return 42;",
117− ].join("\n"),
118− toolInvoker,
119− );
17+});
12018
121− expect(output.result).toBe(42);
122− expect(output.logs).toContain("[log] hello from quickjs");
123− expect(output.logs).toContain("[warn] a warning");
124− expect(output.logs).toContain("[error] an error");
125− }),
126− );
19+const executor = makeQuickJsExecutor({ timeoutMs: 5_000 });
12720
128− it.effect("reports execution errors without crashing", () =>
21+describe("quickjs executor", () => {
22+ it.effect("runs plain code", () =>
12923 Effect.gen(function* () {
130− const executor = makeQuickJsExecutor();
131− const toolInvoker = makeToolInvokerFromTools({ tools });
132−
133− const output = yield* executor.execute(
134− 'throw new Error("boom");',
135− toolInvoker,
24+ const result = yield* executor.execute(
25+ `return 1 + 2`,
26+ makeTestInvoker({}),
13627 );
137−
138− expect(output.result).toBeNull();
139− expect(output.error).toContain("boom");
28+ expect(result.result).toBe(3);
29+ expect(result.error).toBeUndefined();
14030 }),
14131 );
14232
143− it.effect("handles tool call errors gracefully", () =>
33+ it.effect("invokes a tool and returns its result", () =>
14434 Effect.gen(function* () {
145− const failingTools = {
146− "broken.thing": {
147− description: "Always fails",
148− inputSchema: Schema.standardSchemaV1(Schema.Struct({})),
149− execute: () => {
150− throw new Error("tool is broken");
151− },
152− },
153− };
154−
155− const executor = makeQuickJsExecutor();
156− const toolInvoker = makeToolInvokerFromTools({
157− tools: failingTools,
35+ const invoker = makeTestInvoker({
36+ "math.add": (args: any) => ({
37+ sum: args.a + args.b,
38+ }),
15839 });
15940
160− const output = yield* executor.execute(
161− "return await tools.broken.thing({});",
162− toolInvoker,
41+ const result = yield* executor.execute(
42+ `
43+ const res = await tools.math.add({ a: 5, b: 3 });
44+ return res.sum;
45+ `,
46+ invoker,
16347 );
16448
165− expect(output.result).toBeNull();
166− expect(output.error).toContain("tool is broken");
49+ expect(result.error).toBeUndefined();
50+ expect(result.result).toBe(8);
16751 }),
16852 );
16953
170− it.effect("respects timeout", () =>
54+ it.effect("invokes multiple tools in sequence", () =>
17155 Effect.gen(function* () {
172− const executor = makeQuickJsExecutor({
173− timeoutMs: 250,
56+ const invoker = makeTestInvoker({
57+ "users.get": (args: any) => ({
58+ id: args.id,
59+ name: `User ${args.id}`,
60+ }),
61+ "users.greet": (args: any) => ({
62+ message: `Hello, ${args.name}!`,
63+ }),
17464 });
175− const toolInvoker = makeToolInvokerFromTools({ tools });
17665
177− const output = yield* executor.execute(
178− "while (true) {}",
179− toolInvoker,
66+ const result = yield* executor.execute(
67+ `
68+ const user = await tools.users.get({ id: 42 });
69+ const greeting = await tools.users.greet({ name: user.name });
70+ return greeting.message;
71+ `,
72+ invoker,
18073 );
18174
182− expect(output.result).toBeNull();
183− expect(output.error).toContain("timed out");
75+ expect(result.error).toBeUndefined();
76+ expect(result.result).toBe("Hello, User 42!");
18477 }),
18578 );
18679
187− it.effect("network access is denied by default", () =>
80+ it.effect("handles tool errors", () =>
18881 Effect.gen(function* () {
189− const executor = makeQuickJsExecutor();
190− const toolInvoker = makeToolInvokerFromTools({ tools });
82+ const invoker = makeTestInvoker({
83+ "db.query": () => {
84+ throw new Error("connection refused");
85+ },
86+ });
19187
192− const output = yield* executor.execute(
193− 'await fetch("https://example.com"); return 1;',
194− toolInvoker,
88+ const result = yield* executor.execute(
89+ `
90+ try {
91+ await tools.db.query({ sql: "SELECT 1" });
92+ return "should not reach";
93+ } catch (e) {
94+ return "caught: " + e.message;
95+ }
96+ `,
97+ invoker,
19598 );
19699
197− expect(output.result).toBeNull();
198− expect(output.error).toContain("fetch is disabled in QuickJS executor");
100+ expect(result.error).toBeUndefined();
101+ expect(result.result).toContain("caught:");
199102 }),
200103 );
201104
202− it.effect("multiple sequential tool calls work correctly", () =>
105+ it.effect("handles unknown tool path", () =>
203106 Effect.gen(function* () {
204− const executor = makeQuickJsExecutor();
205− const toolInvoker = makeToolInvokerFromTools({ tools });
206−
207− const output = yield* executor.execute(
208− [
209− "const r1 = await tools.math.add({ a: 1, b: 2 });",
210− "const r2 = await tools.math.add({ a: r1.sum, b: 10 });",
211− "const r3 = await tools.math.add({ a: r2.sum, b: 100 });",
212− "return r3;",
213− ].join("\n"),
214− toolInvoker,
107+ const invoker = makeTestInvoker({});
108+
109+ const result = yield* executor.execute(
110+ `
111+ try {
112+ await tools.nonexistent.thing({ x: 1 });
113+ return "should not reach";
114+ } catch (e) {
115+ return "caught: " + e.message;
116+ }
117+ `,
118+ invoker,
215119 );
216120
217− expect(output.result).toEqual({ sum: 113 });
218− expect(output.error).toBeUndefined();
121+ expect(result.error).toBeUndefined();
122+ expect(result.result).toContain("caught:");
219123 }),
220124 );
221125
222− it.effect("does not expose internal executor bridge globals to user code", () =>
126+ it.effect("captures console.log output", () =>
223127 Effect.gen(function* () {
224− const executor = makeQuickJsExecutor();
225− const toolInvoker = makeToolInvokerFromTools({ tools });
226−
227− const output = yield* executor.execute(
228− [
229− "return {",
230− " hasInvokeTool: typeof globalThis.__executor_invokeTool !== 'undefined',",
231− " hasLogBridge: typeof globalThis.__executor_log !== 'undefined',",
232− " globalKeys: Object.keys(globalThis).sort(),",
233− "};",
234− ].join("\n"),
235− toolInvoker,
128+ const result = yield* executor.execute(
129+ `
130+ console.log("hello from sandbox");
131+ console.warn("a warning");
132+ return "done";
133+ `,
134+ makeTestInvoker({}),
236135 );
237136
238− expect(output.result).toEqual({
239− hasInvokeTool: false,
240− hasLogBridge: false,
241− globalKeys: [],
242− });
243− expect(output.error).toBeUndefined();
137+ expect(result.error).toBeUndefined();
138+ expect(result.result).toBe("done");
139+ expect(result.logs).toContainEqual("[log] hello from sandbox");
140+ expect(result.logs).toContainEqual("[warn] a warning");
244141 }),
245142 );
246143
247− it.effect("supports the documented discovery workflow shape", () =>
144+ it.effect("passes tool result into next tool call", () =>
248145 Effect.gen(function* () {
249− const executor = makeQuickJsExecutor();
250− const toolInvoker = makeToolInvokerFromTools({
251− tools: {
252− discover: {
253− inputSchema: discoverInputSchema,
254− execute: () => ({
255− bestPath: "github.issues.list",
256− results: [
257− {
258− path: "github.issues.list",
259− score: 0.99,
260− },
261− ],
262− total: 1,
263− }),
264− },
265− "describe.tool": {
266− inputSchema: describeToolInputSchema,
267− execute: ({ path }: { path: string; includeSchemas?: boolean }) => ({
268− path,
269− contract: {
270− inputTypePreview: "{ owner: string; repo: string }",
271− },
272− }),
273− },
274− "github.issues.list": {
275− inputSchema: repoInputSchema,
276− execute: ({ owner, repo }: { owner: string; repo: string }) => ({
277− owner,
278− repo,
279− issues: [{ id: "issue_1" }],
280− }),
281− },
282− },
146+ const invoker = makeTestInvoker({
147+ "stripe.customers.list": () => ({
148+ data: [
149+ { id: "cus_1", email: "alice@example.com" },
150+ { id: "cus_2", email: "bob@example.com" },
151+ ],
152+ }),
153+ "stripe.invoices.create": (args: any) => ({
154+ id: "inv_1",
155+ customer: args.customer,
156+ amount: args.amount,
157+ }),
283158 });
284159
285− const output = yield* executor.execute(
286− [
287− 'const { results, bestPath } = await tools.discover({ query: "github issues", limit: 5 });',
288− "const path = bestPath ?? results[0]?.path;",
289− 'if (!path) throw new Error("No matching tools found.");',
290− "const detail = await tools.describe.tool({ path, includeSchemas: true });",
291− 'const issues = await tools.github.issues.list({ owner: "openai", repo: "codex" });',
292− "return { path, detail, issues };",
293− ].join("\n"),
294− toolInvoker,
160+ const result = yield* executor.execute(
161+ `
162+ const customers = await tools.stripe.customers.list();
163+ const invoice = await tools.stripe.invoices.create({
164+ customer: customers.data[0].id,
165+ amount: 5000,
166+ });
167+ return invoice;
168+ `,
169+ invoker,
295170 );
296171
297− expect(output.result).toEqual({
298− path: "github.issues.list",
299− detail: {
300− path: "github.issues.list",
301− contract: {
302− inputTypePreview: "{ owner: string; repo: string }",
303− },
304− },
305− issues: {
306− owner: "openai",
307− repo: "codex",
308− issues: [{ id: "issue_1" }],
309− },
172+ expect(result.error).toBeUndefined();
173+ expect(result.result).toEqual({
174+ id: "inv_1",
175+ customer: "cus_1",
176+ amount: 5000,
310177 });
311− expect(output.error).toBeUndefined();
312178 }),
313179 );
314180 });