@kb-labs/shared-tool-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Tool factory for creating agent tools in a consistent way.
3
+ *
4
+ * Generic over TContext so it can be used with any tool context type —
5
+ * the consumer passes their own ToolContext without creating a cross-repo dependency.
6
+ *
7
+ * ---
8
+ *
9
+ * TODO: ToolShape/ToolResult type mismatch — migration to createTool is blocked
10
+ *
11
+ * Problem:
12
+ * `ToolShape.executor` returns `Promise<unknown>`, but agent-tools' `Tool.executor`
13
+ * expects `Promise<ToolResult>` (from @kb-labs/agent-contracts). This makes
14
+ * `registry.register(createSpawnAgentTool(context))` fail to type-check because
15
+ * `ToolShape` is not assignable to `Tool`.
16
+ *
17
+ * Root cause:
18
+ * `ToolResult` lives in `@kb-labs/agent-contracts` (agent-specific repo).
19
+ * `shared-tool-kit` (kb-labs-shared) cannot import from agent-contracts without
20
+ * creating a cross-repo dependency, which violates layering.
21
+ *
22
+ * Planned fix options (pick one):
23
+ * A) Add `TResult` type parameter to `ToolShape` and `ToolSpec`:
24
+ * `ToolShape<TContext, TResult = unknown>`
25
+ * `ToolSpec<TInput, TContext, TResult = unknown>`
26
+ * Then agent-tools can call `createTool<Input, Context, ToolResult>(...)` and
27
+ * get back a properly typed `ToolShape<Context, ToolResult>` that satisfies `Tool`.
28
+ * No new cross-repo dependency needed — ToolResult stays in agent-contracts.
29
+ *
30
+ * B) Move `ToolResult` to a platform-level package (e.g. core-platform or a new
31
+ * shared-contracts package) so shared-tool-kit can import it directly and
32
+ * `ToolShape.executor` returns `Promise<ToolResult>` out of the box.
33
+ * More "correct" architecturally but requires more refactoring.
34
+ *
35
+ * Current state:
36
+ * delegation.ts in agent-tools was reverted to manual factory pattern (not using
37
+ * createTool) until this is resolved. Migration is planned as a follow-up task.
38
+ */
39
+ /**
40
+ * OpenAI Function Calling compatible tool definition.
41
+ * Mirrors the structure expected by LLM APIs.
42
+ */
43
+ interface ToolDefinitionShape {
44
+ type: 'function';
45
+ function: {
46
+ name: string;
47
+ description: string;
48
+ parameters: {
49
+ type: 'object';
50
+ properties: Record<string, unknown>;
51
+ required?: string[];
52
+ };
53
+ };
54
+ }
55
+ /**
56
+ * A registered tool: definition for the LLM + executor function.
57
+ */
58
+ interface ToolShape<TContext = unknown> {
59
+ definition: ToolDefinitionShape;
60
+ executor: (input: Record<string, unknown>) => Promise<unknown>;
61
+ /** The context this tool was created with (for inspection/testing) */
62
+ _context?: TContext;
63
+ }
64
+ /**
65
+ * Specification for creating a tool via createTool().
66
+ */
67
+ interface ToolSpec<TInput extends Record<string, unknown> = Record<string, unknown>, TContext = unknown> {
68
+ /** Tool name (used in LLM function calling) */
69
+ name: string;
70
+ /** Human-readable description shown to the LLM */
71
+ description: string;
72
+ /** JSON Schema for the tool's input parameters */
73
+ parameters: {
74
+ type: 'object';
75
+ properties: Record<string, unknown>;
76
+ required?: string[];
77
+ };
78
+ /** Tool implementation — receives typed input and context */
79
+ execute: (input: TInput, context: TContext) => Promise<unknown>;
80
+ }
81
+ /**
82
+ * Create a tool factory function from a spec.
83
+ *
84
+ * Returns a factory `(context: TContext) => ToolShape` — matching the
85
+ * existing pattern in agent-tools where each `createXxxTool(context)` returns a Tool.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * const myToolFactory = createTool({
90
+ * name: 'my_tool',
91
+ * description: 'Does something useful',
92
+ * parameters: {
93
+ * type: 'object',
94
+ * properties: { value: { type: 'string' } },
95
+ * required: ['value'],
96
+ * },
97
+ * execute: async ({ value }, context) => {
98
+ * return { success: true, output: `Got: ${value}` };
99
+ * },
100
+ * });
101
+ *
102
+ * // In tool registry:
103
+ * const tool = myToolFactory(context);
104
+ * registry.register(tool);
105
+ * ```
106
+ */
107
+ declare function createTool<TInput extends Record<string, unknown> = Record<string, unknown>, TContext = unknown>(spec: ToolSpec<TInput, TContext>): (context: TContext) => ToolShape<TContext>;
108
+
109
+ export { type ToolDefinitionShape, type ToolShape, type ToolSpec, createTool };
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ // src/factory.ts
2
+ function createTool(spec) {
3
+ return (context) => ({
4
+ definition: {
5
+ type: "function",
6
+ function: {
7
+ name: spec.name,
8
+ description: spec.description,
9
+ parameters: spec.parameters
10
+ }
11
+ },
12
+ executor: (input) => spec.execute(input, context)
13
+ });
14
+ }
15
+
16
+ export { createTool };
17
+ //# sourceMappingURL=index.js.map
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/factory.ts"],"names":[],"mappings":";AA8GO,SAAS,WAGd,IAAA,EAA8E;AAC9E,EAAA,OAAO,CAAC,OAAA,MAA4C;AAAA,IAClD,UAAA,EAAY;AAAA,MACV,IAAA,EAAM,UAAA;AAAA,MACN,QAAA,EAAU;AAAA,QACR,MAAM,IAAA,CAAK,IAAA;AAAA,QACX,aAAa,IAAA,CAAK,WAAA;AAAA,QAClB,YAAY,IAAA,CAAK;AAAA;AACnB,KACF;AAAA,IACA,UAAU,CAAC,KAAA,KAAmC,IAAA,CAAK,OAAA,CAAQ,OAAiB,OAAO;AAAA,GACrF,CAAA;AACF","file":"index.js","sourcesContent":["/**\n * Tool factory for creating agent tools in a consistent way.\n *\n * Generic over TContext so it can be used with any tool context type —\n * the consumer passes their own ToolContext without creating a cross-repo dependency.\n *\n * ---\n *\n * TODO: ToolShape/ToolResult type mismatch — migration to createTool is blocked\n *\n * Problem:\n * `ToolShape.executor` returns `Promise<unknown>`, but agent-tools' `Tool.executor`\n * expects `Promise<ToolResult>` (from @kb-labs/agent-contracts). This makes\n * `registry.register(createSpawnAgentTool(context))` fail to type-check because\n * `ToolShape` is not assignable to `Tool`.\n *\n * Root cause:\n * `ToolResult` lives in `@kb-labs/agent-contracts` (agent-specific repo).\n * `shared-tool-kit` (kb-labs-shared) cannot import from agent-contracts without\n * creating a cross-repo dependency, which violates layering.\n *\n * Planned fix options (pick one):\n * A) Add `TResult` type parameter to `ToolShape` and `ToolSpec`:\n * `ToolShape<TContext, TResult = unknown>`\n * `ToolSpec<TInput, TContext, TResult = unknown>`\n * Then agent-tools can call `createTool<Input, Context, ToolResult>(...)` and\n * get back a properly typed `ToolShape<Context, ToolResult>` that satisfies `Tool`.\n * No new cross-repo dependency needed — ToolResult stays in agent-contracts.\n *\n * B) Move `ToolResult` to a platform-level package (e.g. core-platform or a new\n * shared-contracts package) so shared-tool-kit can import it directly and\n * `ToolShape.executor` returns `Promise<ToolResult>` out of the box.\n * More \"correct\" architecturally but requires more refactoring.\n *\n * Current state:\n * delegation.ts in agent-tools was reverted to manual factory pattern (not using\n * createTool) until this is resolved. Migration is planned as a follow-up task.\n */\n\n/**\n * OpenAI Function Calling compatible tool definition.\n * Mirrors the structure expected by LLM APIs.\n */\nexport interface ToolDefinitionShape {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: {\n type: 'object';\n properties: Record<string, unknown>;\n required?: string[];\n };\n };\n}\n\n/**\n * A registered tool: definition for the LLM + executor function.\n */\nexport interface ToolShape<TContext = unknown> {\n definition: ToolDefinitionShape;\n executor: (input: Record<string, unknown>) => Promise<unknown>;\n /** The context this tool was created with (for inspection/testing) */\n _context?: TContext;\n}\n\n/**\n * Specification for creating a tool via createTool().\n */\nexport interface ToolSpec<TInput extends Record<string, unknown> = Record<string, unknown>, TContext = unknown> {\n /** Tool name (used in LLM function calling) */\n name: string;\n /** Human-readable description shown to the LLM */\n description: string;\n /** JSON Schema for the tool's input parameters */\n parameters: {\n type: 'object';\n properties: Record<string, unknown>;\n required?: string[];\n };\n /** Tool implementation — receives typed input and context */\n execute: (input: TInput, context: TContext) => Promise<unknown>;\n}\n\n/**\n * Create a tool factory function from a spec.\n *\n * Returns a factory `(context: TContext) => ToolShape` — matching the\n * existing pattern in agent-tools where each `createXxxTool(context)` returns a Tool.\n *\n * @example\n * ```ts\n * const myToolFactory = createTool({\n * name: 'my_tool',\n * description: 'Does something useful',\n * parameters: {\n * type: 'object',\n * properties: { value: { type: 'string' } },\n * required: ['value'],\n * },\n * execute: async ({ value }, context) => {\n * return { success: true, output: `Got: ${value}` };\n * },\n * });\n *\n * // In tool registry:\n * const tool = myToolFactory(context);\n * registry.register(tool);\n * ```\n */\nexport function createTool<\n TInput extends Record<string, unknown> = Record<string, unknown>,\n TContext = unknown,\n>(spec: ToolSpec<TInput, TContext>): (context: TContext) => ToolShape<TContext> {\n return (context: TContext): ToolShape<TContext> => ({\n definition: {\n type: 'function',\n function: {\n name: spec.name,\n description: spec.description,\n parameters: spec.parameters,\n },\n },\n executor: (input: Record<string, unknown>) => spec.execute(input as TInput, context),\n });\n}\n"]}
@@ -0,0 +1,54 @@
1
+ import { ToolShape } from '../index.js';
2
+
3
+ /**
4
+ * @kb-labs/shared-tool-kit/testing
5
+ *
6
+ * Mock utilities for testing agent tools.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { mockTool } from '@kb-labs/shared-tool-kit/testing';
11
+ *
12
+ * const tool = mockTool('fs_read', { success: true, output: 'file content' });
13
+ * await tool.executor({ path: 'file.ts' });
14
+ * console.log(tool.getCalls()); // [{ path: 'file.ts' }]
15
+ * ```
16
+ */
17
+
18
+ /**
19
+ * A mock tool with built-in call tracking for tests.
20
+ */
21
+ interface MockToolInstance extends ToolShape {
22
+ /** All calls made to this tool's executor */
23
+ getCalls: () => readonly Record<string, unknown>[];
24
+ /** Last call arguments, or undefined if never called */
25
+ getLastCall: () => Record<string, unknown> | undefined;
26
+ /** True if executor was called at least once */
27
+ wasCalled: () => boolean;
28
+ /** Number of times executor was called */
29
+ callCount: () => number;
30
+ /** Replace the response returned by executor */
31
+ respondWith: (response: unknown) => MockToolInstance;
32
+ }
33
+ /**
34
+ * Create a mock tool for testing.
35
+ *
36
+ * The mock records all calls and returns a configurable response.
37
+ *
38
+ * @param name - Tool name (used in definition)
39
+ * @param response - Default response returned by executor (default: `{}`)
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const fsRead = mockTool('fs_read', { success: true, output: 'hello' });
44
+ *
45
+ * // Use in registry mock or pass directly
46
+ * await fsRead.executor({ path: 'foo.ts' });
47
+ *
48
+ * expect(fsRead.wasCalled()).toBe(true);
49
+ * expect(fsRead.getLastCall()).toEqual({ path: 'foo.ts' });
50
+ * ```
51
+ */
52
+ declare function mockTool(name: string, response?: unknown): MockToolInstance;
53
+
54
+ export { type MockToolInstance, mockTool };
@@ -0,0 +1,35 @@
1
+ // src/testing/index.ts
2
+ function mockTool(name, response = {}) {
3
+ const calls = [];
4
+ let currentResponse = response;
5
+ const instance = {
6
+ definition: {
7
+ type: "function",
8
+ function: {
9
+ name,
10
+ description: `Mock tool: ${name}`,
11
+ parameters: {
12
+ type: "object",
13
+ properties: {}
14
+ }
15
+ }
16
+ },
17
+ executor: async (input) => {
18
+ calls.push(input);
19
+ return currentResponse;
20
+ },
21
+ getCalls: () => calls,
22
+ getLastCall: () => calls[calls.length - 1],
23
+ wasCalled: () => calls.length > 0,
24
+ callCount: () => calls.length,
25
+ respondWith: (newResponse) => {
26
+ currentResponse = newResponse;
27
+ return instance;
28
+ }
29
+ };
30
+ return instance;
31
+ }
32
+
33
+ export { mockTool };
34
+ //# sourceMappingURL=index.js.map
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/testing/index.ts"],"names":[],"mappings":";AAoDO,SAAS,QAAA,CAAS,IAAA,EAAc,QAAA,GAAoB,EAAC,EAAqB;AAC/E,EAAA,MAAM,QAAmC,EAAC;AAC1C,EAAA,IAAI,eAAA,GAAkB,QAAA;AAEtB,EAAA,MAAM,QAAA,GAA6B;AAAA,IACjC,UAAA,EAAY;AAAA,MACV,IAAA,EAAM,UAAA;AAAA,MACN,QAAA,EAAU;AAAA,QACR,IAAA;AAAA,QACA,WAAA,EAAa,cAAc,IAAI,CAAA,CAAA;AAAA,QAC/B,UAAA,EAAY;AAAA,UACV,IAAA,EAAM,QAAA;AAAA,UACN,YAAY;AAAC;AACf;AACF,KACF;AAAA,IACA,QAAA,EAAU,OAAO,KAAA,KAAmC;AAClD,MAAA,KAAA,CAAM,KAAK,KAAK,CAAA;AAChB,MAAA,OAAO,eAAA;AAAA,IACT,CAAA;AAAA,IACA,UAAU,MAAM,KAAA;AAAA,IAChB,WAAA,EAAa,MAAM,KAAA,CAAM,KAAA,CAAM,SAAS,CAAC,CAAA;AAAA,IACzC,SAAA,EAAW,MAAM,KAAA,CAAM,MAAA,GAAS,CAAA;AAAA,IAChC,SAAA,EAAW,MAAM,KAAA,CAAM,MAAA;AAAA,IACvB,WAAA,EAAa,CAAC,WAAA,KAAyB;AACrC,MAAA,eAAA,GAAkB,WAAA;AAClB,MAAA,OAAO,QAAA;AAAA,IACT;AAAA,GACF;AAEA,EAAA,OAAO,QAAA;AACT","file":"index.js","sourcesContent":["/**\n * @kb-labs/shared-tool-kit/testing\n *\n * Mock utilities for testing agent tools.\n *\n * @example\n * ```ts\n * import { mockTool } from '@kb-labs/shared-tool-kit/testing';\n *\n * const tool = mockTool('fs_read', { success: true, output: 'file content' });\n * await tool.executor({ path: 'file.ts' });\n * console.log(tool.getCalls()); // [{ path: 'file.ts' }]\n * ```\n */\n\nimport type { ToolShape } from '../factory.js';\n\n/**\n * A mock tool with built-in call tracking for tests.\n */\nexport interface MockToolInstance extends ToolShape {\n /** All calls made to this tool's executor */\n getCalls: () => readonly Record<string, unknown>[];\n /** Last call arguments, or undefined if never called */\n getLastCall: () => Record<string, unknown> | undefined;\n /** True if executor was called at least once */\n wasCalled: () => boolean;\n /** Number of times executor was called */\n callCount: () => number;\n /** Replace the response returned by executor */\n respondWith: (response: unknown) => MockToolInstance;\n}\n\n/**\n * Create a mock tool for testing.\n *\n * The mock records all calls and returns a configurable response.\n *\n * @param name - Tool name (used in definition)\n * @param response - Default response returned by executor (default: `{}`)\n *\n * @example\n * ```ts\n * const fsRead = mockTool('fs_read', { success: true, output: 'hello' });\n *\n * // Use in registry mock or pass directly\n * await fsRead.executor({ path: 'foo.ts' });\n *\n * expect(fsRead.wasCalled()).toBe(true);\n * expect(fsRead.getLastCall()).toEqual({ path: 'foo.ts' });\n * ```\n */\nexport function mockTool(name: string, response: unknown = {}): MockToolInstance {\n const calls: Record<string, unknown>[] = [];\n let currentResponse = response;\n\n const instance: MockToolInstance = {\n definition: {\n type: 'function' as const,\n function: {\n name,\n description: `Mock tool: ${name}`,\n parameters: {\n type: 'object' as const,\n properties: {},\n },\n },\n },\n executor: async (input: Record<string, unknown>) => {\n calls.push(input);\n return currentResponse;\n },\n getCalls: () => calls,\n getLastCall: () => calls[calls.length - 1],\n wasCalled: () => calls.length > 0,\n callCount: () => calls.length,\n respondWith: (newResponse: unknown) => {\n currentResponse = newResponse;\n return instance;\n },\n };\n\n return instance;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@kb-labs/shared-tool-kit",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Tool factory and mock utilities for KB Labs agent tool development",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./testing": {
14
+ "import": "./dist/testing/index.js",
15
+ "types": "./dist/testing/index.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "sideEffects": false,
23
+ "devDependencies": {
24
+ "@kb-labs/devkit": "link:../../../kb-labs-devkit",
25
+ "@types/node": "^24.3.3",
26
+ "rimraf": "^6.0.1",
27
+ "tsup": "^8.5.0",
28
+ "typescript": "^5.6.3"
29
+ },
30
+ "engines": {
31
+ "node": ">=20.0.0",
32
+ "pnpm": ">=9.0.0"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "clean": "rimraf dist",
39
+ "build": "tsup --config tsup.config.ts",
40
+ "dev": "tsup --config tsup.config.ts --watch",
41
+ "type-check": "tsc --noEmit",
42
+ "lint": "eslint src",
43
+ "lint:fix": "eslint src --fix"
44
+ }
45
+ }