@vertesia/tools-sdk 0.81.1 → 1.0.0-dev.20260203.130115Z

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.
Files changed (101) hide show
  1. package/package.json +9 -8
  2. package/src/ContentTypesCollection.ts +51 -0
  3. package/src/InteractionCollection.ts +1 -94
  4. package/src/SkillCollection.ts +88 -15
  5. package/src/ToolCollection.ts +65 -17
  6. package/src/ToolRegistry.ts +101 -9
  7. package/src/auth.ts +37 -6
  8. package/src/index.ts +7 -3
  9. package/src/server/app-package.ts +102 -0
  10. package/src/server/conyent-types.ts +71 -0
  11. package/src/server/interactions.ts +100 -0
  12. package/src/server/mcp.ts +51 -0
  13. package/src/server/site.ts +53 -0
  14. package/src/server/skills.ts +133 -0
  15. package/src/server/tools.ts +128 -0
  16. package/src/server/types.ts +87 -0
  17. package/src/server/widgets.ts +38 -0
  18. package/src/server.ts +80 -359
  19. package/src/site/styles.ts +71 -0
  20. package/src/site/templates.ts +215 -119
  21. package/src/types.ts +22 -18
  22. package/src/utils.ts +20 -0
  23. package/lib/cjs/InteractionCollection.js +0 -164
  24. package/lib/cjs/InteractionCollection.js.map +0 -1
  25. package/lib/cjs/SkillCollection.js +0 -318
  26. package/lib/cjs/SkillCollection.js.map +0 -1
  27. package/lib/cjs/ToolCollection.js +0 -192
  28. package/lib/cjs/ToolCollection.js.map +0 -1
  29. package/lib/cjs/ToolRegistry.js +0 -44
  30. package/lib/cjs/ToolRegistry.js.map +0 -1
  31. package/lib/cjs/auth.js +0 -89
  32. package/lib/cjs/auth.js.map +0 -1
  33. package/lib/cjs/build/validate.js +0 -7
  34. package/lib/cjs/build/validate.js.map +0 -1
  35. package/lib/cjs/copy-assets.js +0 -84
  36. package/lib/cjs/copy-assets.js.map +0 -1
  37. package/lib/cjs/index.js +0 -30
  38. package/lib/cjs/index.js.map +0 -1
  39. package/lib/cjs/package.json +0 -3
  40. package/lib/cjs/server.js +0 -327
  41. package/lib/cjs/server.js.map +0 -1
  42. package/lib/cjs/site/styles.js +0 -621
  43. package/lib/cjs/site/styles.js.map +0 -1
  44. package/lib/cjs/site/templates.js +0 -932
  45. package/lib/cjs/site/templates.js.map +0 -1
  46. package/lib/cjs/types.js +0 -3
  47. package/lib/cjs/types.js.map +0 -1
  48. package/lib/cjs/utils.js +0 -7
  49. package/lib/cjs/utils.js.map +0 -1
  50. package/lib/esm/InteractionCollection.js +0 -125
  51. package/lib/esm/InteractionCollection.js.map +0 -1
  52. package/lib/esm/SkillCollection.js +0 -311
  53. package/lib/esm/SkillCollection.js.map +0 -1
  54. package/lib/esm/ToolCollection.js +0 -154
  55. package/lib/esm/ToolCollection.js.map +0 -1
  56. package/lib/esm/ToolRegistry.js +0 -39
  57. package/lib/esm/ToolRegistry.js.map +0 -1
  58. package/lib/esm/auth.js +0 -82
  59. package/lib/esm/auth.js.map +0 -1
  60. package/lib/esm/build/validate.js +0 -4
  61. package/lib/esm/build/validate.js.map +0 -1
  62. package/lib/esm/copy-assets.js +0 -81
  63. package/lib/esm/copy-assets.js.map +0 -1
  64. package/lib/esm/index.js +0 -10
  65. package/lib/esm/index.js.map +0 -1
  66. package/lib/esm/server.js +0 -323
  67. package/lib/esm/server.js.map +0 -1
  68. package/lib/esm/site/styles.js +0 -618
  69. package/lib/esm/site/styles.js.map +0 -1
  70. package/lib/esm/site/templates.js +0 -920
  71. package/lib/esm/site/templates.js.map +0 -1
  72. package/lib/esm/types.js +0 -2
  73. package/lib/esm/types.js.map +0 -1
  74. package/lib/esm/utils.js +0 -4
  75. package/lib/esm/utils.js.map +0 -1
  76. package/lib/types/InteractionCollection.d.ts +0 -48
  77. package/lib/types/InteractionCollection.d.ts.map +0 -1
  78. package/lib/types/SkillCollection.d.ts +0 -111
  79. package/lib/types/SkillCollection.d.ts.map +0 -1
  80. package/lib/types/ToolCollection.d.ts +0 -61
  81. package/lib/types/ToolCollection.d.ts.map +0 -1
  82. package/lib/types/ToolRegistry.d.ts +0 -15
  83. package/lib/types/ToolRegistry.d.ts.map +0 -1
  84. package/lib/types/auth.d.ts +0 -20
  85. package/lib/types/auth.d.ts.map +0 -1
  86. package/lib/types/build/validate.d.ts +0 -2
  87. package/lib/types/build/validate.d.ts.map +0 -1
  88. package/lib/types/copy-assets.d.ts +0 -14
  89. package/lib/types/copy-assets.d.ts.map +0 -1
  90. package/lib/types/index.d.ts +0 -10
  91. package/lib/types/index.d.ts.map +0 -1
  92. package/lib/types/server.d.ts +0 -72
  93. package/lib/types/server.d.ts.map +0 -1
  94. package/lib/types/site/styles.d.ts +0 -5
  95. package/lib/types/site/styles.d.ts.map +0 -1
  96. package/lib/types/site/templates.d.ts +0 -54
  97. package/lib/types/site/templates.d.ts.map +0 -1
  98. package/lib/types/types.d.ts +0 -262
  99. package/lib/types/types.d.ts.map +0 -1
  100. package/lib/types/utils.d.ts +0 -2
  101. package/lib/types/utils.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertesia/tools-sdk",
3
- "version": "0.81.1",
3
+ "version": "1.0.0-dev.20260203.130115Z",
4
4
  "description": "Tools SDK - utilities for building remote tools",
5
5
  "type": "module",
6
6
  "types": "./lib/types/index.d.ts",
@@ -18,22 +18,23 @@
18
18
  "tools-sdk-copy-assets": "./lib/esm/copy-assets.js"
19
19
  },
20
20
  "devDependencies": {
21
- "@types/node": "^24.10.1",
21
+ "@types/node": "^22.19.2",
22
22
  "ts-dual-module": "^0.6.3",
23
- "typescript": "^5.0.2",
24
- "vitest": "^3.0.9"
23
+ "typescript": "^5.9.3",
24
+ "vitest": "^4.0.16"
25
25
  },
26
26
  "ts_dual_module": {
27
27
  "outDir": "lib"
28
28
  },
29
29
  "peerDependencies": {
30
- "hono": "^4.10.0"
30
+ "hono": "^4.11.7"
31
31
  },
32
32
  "dependencies": {
33
33
  "jose": "^6.0.11",
34
- "@llumiverse/common": "0.24.0",
35
- "@vertesia/common": "0.81.1",
36
- "@vertesia/client": "0.81.1"
34
+ "zod": "^4.3.5",
35
+ "@llumiverse/common": "1.0.0-dev.20260202.145450Z",
36
+ "@vertesia/common": "1.0.0-dev.20260203.130115Z",
37
+ "@vertesia/client": "1.0.0-dev.20260203.130115Z"
37
38
  },
38
39
  "repository": {
39
40
  "type": "git",
@@ -0,0 +1,51 @@
1
+ import { InCodeTypeDefinition } from "@vertesia/common";
2
+ import { CollectionProperties, ICollection } from "./types.js";
3
+ import { kebabCaseToTitle } from "./utils.js";
4
+
5
+ export interface ContentTypesCollectionProps extends CollectionProperties {
6
+ types: InCodeTypeDefinition[];
7
+ }
8
+ export class ContentTypesCollection implements ICollection<InCodeTypeDefinition> {
9
+ types: InCodeTypeDefinition[];
10
+ name: string;
11
+ title?: string;
12
+ icon?: string;
13
+ description?: string;
14
+ constructor({
15
+ name, title, icon, description, types
16
+ }: ContentTypesCollectionProps) {
17
+ this.name = name;
18
+ this.title = title || kebabCaseToTitle(name);
19
+ this.icon = icon;
20
+ this.description = description;
21
+ this.types = types;
22
+ }
23
+
24
+ getContentTypes() {
25
+ return this.types;
26
+ }
27
+
28
+ [Symbol.iterator](): Iterator<InCodeTypeDefinition> {
29
+ let index = 0;
30
+ const types = this.types;
31
+
32
+ return {
33
+ next(): IteratorResult<InCodeTypeDefinition> {
34
+ if (index < types.length) {
35
+ return { value: types[index++], done: false };
36
+ } else {
37
+ return { done: true, value: undefined };
38
+ }
39
+ }
40
+ };
41
+ }
42
+
43
+ map<U>(callback: (type: InCodeTypeDefinition, index: number) => U): U[] {
44
+ return this.types.map(callback);
45
+ }
46
+
47
+ getTypeByName(name: string): InCodeTypeDefinition | undefined {
48
+ return this.types.find(type => type.name === name);
49
+ }
50
+
51
+ }
@@ -1,8 +1,5 @@
1
- import { readdirSync, statSync, existsSync, readFileSync } from "fs";
2
- import { join } from "path";
3
- import { pathToFileURL } from "url";
4
1
  import { InteractionSpec } from "@vertesia/common";
5
- import { ICollection, CollectionProperties } from "./types.js";
2
+ import { CollectionProperties, ICollection } from "./types.js";
6
3
  import { kebabCaseToTitle } from "./utils.js";
7
4
 
8
5
  export interface InteractionCollectionProps extends CollectionProperties {
@@ -23,9 +20,6 @@ export class InteractionCollection implements ICollection<InteractionSpec> {
23
20
  this.description = description;
24
21
  this.interactions = interactions;
25
22
  }
26
- addInteraction(interaction: any) {
27
- this.interactions.push(interaction);
28
- }
29
23
 
30
24
  getInteractions() {
31
25
  return this.interactions;
@@ -54,90 +48,3 @@ export class InteractionCollection implements ICollection<InteractionSpec> {
54
48
  return this.interactions.find(interaction => interaction.name === name);
55
49
  }
56
50
  }
57
-
58
- /**
59
- * Load all interactions from a directory.
60
- * Scans for subdirectories containing index.ts/index.js files.
61
- *
62
- * Directory structure:
63
- * ```
64
- * interactions/
65
- * nagare/
66
- * extract-fund-actuals/
67
- * index.ts # exports default InteractionSpec
68
- * prompt.jst # prompt template (read via readPromptFile helper)
69
- * parse-fund-document/
70
- * index.ts
71
- * prompt.md
72
- * ```
73
- *
74
- * @param interactionsDir - Path to the interactions collection directory
75
- * @returns Promise resolving to array of InteractionSpec objects
76
- */
77
- export async function loadInteractionsFromDirectory(interactionsDir: string): Promise<InteractionSpec[]> {
78
- const interactions: InteractionSpec[] = [];
79
-
80
- if (!existsSync(interactionsDir)) {
81
- console.warn(`Interactions directory not found: ${interactionsDir}`);
82
- return interactions;
83
- }
84
-
85
- let entries: string[];
86
- try {
87
- entries = readdirSync(interactionsDir);
88
- } catch {
89
- console.warn(`Could not read interactions directory: ${interactionsDir}`);
90
- return interactions;
91
- }
92
-
93
- for (const entry of entries) {
94
- // Skip hidden files and index files
95
- if (entry.startsWith('.')) continue;
96
- if (entry === 'index.ts' || entry === 'index.js') continue;
97
-
98
- const entryPath = join(interactionsDir, entry);
99
-
100
- try {
101
- const stat = statSync(entryPath);
102
- if (!stat.isDirectory()) continue;
103
-
104
- // Look for index.ts or index.js in the subdirectory
105
- const indexTs = join(entryPath, 'index.ts');
106
- const indexJs = join(entryPath, 'index.js');
107
- const indexPath = existsSync(indexTs) ? indexTs : existsSync(indexJs) ? indexJs : null;
108
-
109
- if (!indexPath) {
110
- continue; // No index file, skip
111
- }
112
-
113
- // Dynamic import
114
- const fileUrl = pathToFileURL(indexPath).href;
115
- const module = await import(fileUrl);
116
-
117
- const interaction = module.default || module.interaction;
118
-
119
- if (interaction && typeof interaction.name === 'string') {
120
- interactions.push(interaction);
121
- } else {
122
- console.warn(`No valid InteractionSpec export found in ${entry}/index`);
123
- }
124
- } catch (err) {
125
- console.warn(`Error loading interaction from ${entry}:`, err);
126
- }
127
- }
128
-
129
- return interactions;
130
- }
131
-
132
- /**
133
- * Helper to read a prompt file from the same directory as the interaction.
134
- * Use this in interaction index.ts files to load prompt templates.
135
- *
136
- * @param dirname - Pass __dirname or dirname(fileURLToPath(import.meta.url))
137
- * @param filename - Prompt filename (e.g., 'prompt.jst' or 'prompt.md')
138
- * @returns File contents as string
139
- */
140
- export function readPromptFile(dirname: string, filename: string): string {
141
- const filePath = join(dirname, filename);
142
- return readFileSync(filePath, 'utf-8');
143
- }
@@ -1,8 +1,9 @@
1
- import { readdirSync, statSync, existsSync, readFileSync } from "fs";
2
- import { join } from "path";
3
1
  import { ToolDefinition } from "@llumiverse/common";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
4
3
  import { Context } from "hono";
5
4
  import { HTTPException } from "hono/http-exception";
5
+ import { join } from "path";
6
+ import { ToolContext } from "./server/types.js";
6
7
  import type {
7
8
  CollectionProperties,
8
9
  ICollection,
@@ -14,6 +15,7 @@ import type {
14
15
  ToolExecutionResult,
15
16
  } from "./types.js";
16
17
  import { kebabCaseToTitle } from "./utils.js";
18
+ import { AgentToolDefinition } from "@vertesia/common";
17
19
 
18
20
  export interface SkillCollectionProperties extends CollectionProperties {
19
21
  /**
@@ -83,8 +85,9 @@ export class SkillCollection implements ICollection<SkillDefinition> {
83
85
  * Get skills exposed as tool definitions.
84
86
  * This allows skills to appear alongside regular tools.
85
87
  * When called, they return rendered instructions.
88
+ * Includes related_tools for dynamic tool discovery.
86
89
  */
87
- getToolDefinitions(): ToolDefinition[] {
90
+ getToolDefinitions(): AgentToolDefinition[] {
88
91
  const defaultSchema: ToolDefinition['input_schema'] = {
89
92
  type: 'object',
90
93
  properties: {
@@ -95,11 +98,21 @@ export class SkillCollection implements ICollection<SkillDefinition> {
95
98
  }
96
99
  };
97
100
 
98
- return Array.from(this.skills.values()).map(skill => ({
99
- name: `skill_${skill.name}`,
100
- description: `[Skill] ${skill.description}. Returns contextual instructions for this task.`,
101
- input_schema: skill.input_schema || defaultSchema
102
- }));
101
+ return Array.from(this.skills.values()).map(skill => {
102
+ // Build description with related tools info if available
103
+ let description = `[Skill] ${skill.description}. Returns contextual instructions for this task.`;
104
+ if (skill.related_tools && skill.related_tools.length > 0) {
105
+ description += ` Unlocks tools: ${skill.related_tools.join(', ')}.`;
106
+ }
107
+
108
+ return {
109
+ name: `learn_${skill.name}`,
110
+ description,
111
+ input_schema: skill.input_schema || defaultSchema,
112
+ related_tools: skill.related_tools,
113
+ category: this.name,
114
+ };
115
+ });
103
116
  }
104
117
 
105
118
  /**
@@ -114,24 +127,65 @@ export class SkillCollection implements ICollection<SkillDefinition> {
114
127
  };
115
128
  }
116
129
 
130
+ getWidgets(): {
131
+ name: string;
132
+ skill: string;
133
+ }[] {
134
+ const out: {
135
+ name: string;
136
+ skill: string;
137
+ }[] = [];
138
+ for (const skill of this.skills.values()) {
139
+ if (skill.widgets) {
140
+ for (const widget of skill.widgets) {
141
+ out.push({
142
+ name: widget,
143
+ skill: skill.name,
144
+ });
145
+ }
146
+ }
147
+ }
148
+ return Array.from(out);
149
+ }
150
+
117
151
  /**
118
152
  * Execute a skill - accepts standard tool execution payload.
119
153
  * Returns rendered instructions in tool result format.
154
+ *
155
+ * @param ctx - Hono context
156
+ * @param preParsedPayload - Optional pre-parsed payload (used when routing from root endpoint)
120
157
  */
121
- async execute(ctx: Context): Promise<Response> {
122
- let payload: ToolExecutionPayload<Record<string, any>> | undefined;
158
+ async execute(ctx: Context, preParsedPayload?: ToolExecutionPayload<Record<string, any>>): Promise<Response> {
159
+ const toolCtx = ctx as ToolContext;
160
+ let payload: ToolExecutionPayload<Record<string, any>> | undefined = preParsedPayload;
123
161
  try {
124
- payload = await ctx.req.json() as ToolExecutionPayload<Record<string, any>>;
162
+ if (!payload) {
163
+ // Check if body was already parsed and validated by middleware
164
+ if (toolCtx.payload) {
165
+ payload = toolCtx.payload;
166
+ } else {
167
+ throw new HTTPException(400, {
168
+ message: 'Invalid or missing skill execution payload. Expected { tool_use: { id, tool_name, tool_input? }, metadata? }'
169
+ });
170
+ }
171
+ }
172
+
125
173
  const toolName = payload.tool_use.tool_name;
126
174
 
127
- // Extract skill name from tool name (remove "skill_" prefix if present)
128
- const skillName = toolName.startsWith('skill_')
129
- ? toolName.slice(6)
175
+ // Extract skill name from tool name (remove "learn_" prefix if present)
176
+ const skillName = toolName.startsWith('learn_')
177
+ ? toolName.replace('learn_', '')
130
178
  : toolName;
131
179
 
132
180
  const skill = this.skills.get(skillName);
133
181
 
134
182
  if (!skill) {
183
+ console.warn("[SkillCollection] Skill not found", {
184
+ collection: this.name,
185
+ requestedSkill: skillName,
186
+ toolName,
187
+ availableSkills: Array.from(this.skills.keys()),
188
+ });
135
189
  throw new HTTPException(404, {
136
190
  message: `Skill not found: ${skillName}`
137
191
  });
@@ -155,8 +209,23 @@ export class SkillCollection implements ICollection<SkillDefinition> {
155
209
  } satisfies ToolExecutionResult & { tool_use_id: string });
156
210
  } catch (err: any) {
157
211
  const status = err.status || 500;
212
+ const toolName = payload?.tool_use?.tool_name;
213
+ const toolUseId = payload?.tool_use?.id;
214
+
215
+ if (status >= 500) {
216
+ console.error("[SkillCollection] Skill execution failed", {
217
+ collection: this.name,
218
+ skill: toolName,
219
+ toolUseId,
220
+ error: err.message,
221
+ status,
222
+ toolInput: payload?.tool_use?.tool_input,
223
+ stack: err.stack,
224
+ });
225
+ }
226
+
158
227
  return ctx.json({
159
- tool_use_id: payload?.tool_use?.id || "unknown",
228
+ tool_use_id: toolUseId || "unknown",
160
229
  is_error: true,
161
230
  content: err.message || "Error executing skill",
162
231
  }, status);
@@ -198,6 +267,8 @@ interface SkillFrontmatter {
198
267
  language?: string;
199
268
  packages?: string[];
200
269
  system_packages?: string[];
270
+ widgets?: string[];
271
+ scripts?: string[];
201
272
  }
202
273
 
203
274
  /**
@@ -247,6 +318,8 @@ export function parseSkillFile(
247
318
  description: frontmatter.description,
248
319
  instructions,
249
320
  content_type: contentType,
321
+ widgets: frontmatter.widgets || undefined,
322
+ scripts: frontmatter.scripts || undefined,
250
323
  };
251
324
 
252
325
  // Build context triggers
@@ -4,9 +4,11 @@ import { pathToFileURL } from "url";
4
4
  import { Context } from "hono";
5
5
  import { HTTPException } from "hono/http-exception";
6
6
  import { authorize } from "./auth.js";
7
- import { ToolRegistry } from "./ToolRegistry.js";
8
- import type { CollectionProperties, ICollection, Tool, ToolDefinition, ToolExecutionPayload, ToolExecutionResponse, ToolExecutionResponseError } from "./types.js";
7
+ import { ToolFilterOptions, ToolRegistry } from "./ToolRegistry.js";
8
+ import { ToolContext } from "./server/types.js";
9
+ import type { CollectionProperties, ICollection, Tool, ToolExecutionPayload, ToolExecutionResponse, ToolExecutionResponseError } from "./types.js";
9
10
  import { kebabCaseToTitle } from "./utils.js";
11
+ import { AgentToolDefinition } from "@vertesia/common";
10
12
 
11
13
  export interface ToolCollectionProperties extends CollectionProperties {
12
14
  /**
@@ -51,7 +53,8 @@ export class ToolCollection implements ICollection<Tool<any>> {
51
53
  this.title = title || kebabCaseToTitle(name);
52
54
  this.icon = icon;
53
55
  this.description = description;
54
- this.tools = new ToolRegistry(tools);
56
+ // we add the collection name info
57
+ this.tools = new ToolRegistry(name, tools);
55
58
  }
56
59
 
57
60
  [Symbol.iterator](): Iterator<Tool<any>> {
@@ -73,11 +76,26 @@ export class ToolCollection implements ICollection<Tool<any>> {
73
76
  return this.tools.getTools().map(callback);
74
77
  }
75
78
 
76
- async execute(ctx: Context): Promise<Response> {
77
- let payload: ToolExecutionPayload<any> | undefined;
79
+ async execute(ctx: Context, preParsedPayload?: ToolExecutionPayload<any>): Promise<Response> {
80
+ let payload: ToolExecutionPayload<any> | undefined = preParsedPayload;
78
81
  try {
79
- payload = await readPayload(ctx);
80
- const session = await authorize(ctx);
82
+ if (!payload) {
83
+ payload = await readPayload(ctx);
84
+ }
85
+ const toolName = payload.tool_use?.tool_name;
86
+ const toolUseId = payload.tool_use?.id;
87
+ const endpointOverrides = payload.metadata?.endpoints;
88
+
89
+ const runId = payload.metadata?.run_id;
90
+
91
+ console.log(`[ToolCollection] Tool call received: ${toolName}`, {
92
+ collection: this.name,
93
+ toolUseId,
94
+ runId,
95
+ hasEndpointOverrides: !!endpointOverrides,
96
+ });
97
+
98
+ const session = await authorize(ctx, endpointOverrides, { toolName, toolUseId, runId });
81
99
  const r = await this.tools.runTool(payload, session);
82
100
  return ctx.json({
83
101
  ...r,
@@ -85,29 +103,59 @@ export class ToolCollection implements ICollection<Tool<any>> {
85
103
  } satisfies ToolExecutionResponse);
86
104
  } catch (err: any) { // HTTPException ?
87
105
  const status = err.status || 500;
106
+ const toolName = payload?.tool_use?.tool_name;
107
+ const toolUseId = payload?.tool_use?.id;
108
+
109
+ console.error("[ToolCollection] Tool execution failed", {
110
+ collection: this.name,
111
+ tool: toolName,
112
+ toolUseId,
113
+ error: err.message,
114
+ status,
115
+ stack: err.stack,
116
+ });
117
+
88
118
  return ctx.json({
89
- tool_use_id: payload?.tool_use.id || "undefined",
119
+ tool_use_id: toolUseId || "undefined",
90
120
  error: err.message || "Error executing tool",
91
121
  status
92
122
  } satisfies ToolExecutionResponseError, status)
93
123
  }
94
124
  }
95
125
 
96
- getToolDefinitions(): ToolDefinition[] {
97
- return this.tools.getDefinitions();
126
+ /**
127
+ * Get tool definitions with optional filtering.
128
+ * @param options - Filtering options for default/unlocked tools
129
+ * @returns Filtered tool definitions
130
+ */
131
+ getToolDefinitions(options?: ToolFilterOptions): AgentToolDefinition[] {
132
+ return this.tools.getDefinitions(options);
133
+ }
134
+
135
+ /**
136
+ * Get tools that are in reserve (default: false and not unlocked).
137
+ * @param unlockedTools - List of tool names that are unlocked
138
+ * @returns Tool definitions for reserve tools
139
+ */
140
+ getReserveTools(unlockedTools: string[] = []): AgentToolDefinition[] {
141
+ return this.tools.getReserveTools(unlockedTools);
98
142
  }
99
143
 
100
144
  }
101
145
 
102
146
 
103
- async function readPayload(ctx: Context) {
104
- try {
105
- return await ctx.req.json() as ToolExecutionPayload<any>;
106
- } catch (err: any) {
107
- throw new HTTPException(500, {
108
- message: "Failed to load execution request payload: " + err.message
109
- });
147
+ function readPayload(ctx: Context): ToolExecutionPayload<any> {
148
+ const toolCtx = ctx as ToolContext;
149
+
150
+ // Check if body was already parsed and validated by middleware
151
+ if (toolCtx.payload) {
152
+ return toolCtx.payload;
110
153
  }
154
+
155
+ // If no payload, middleware couldn't parse/validate - return error
156
+ throw new HTTPException(400, {
157
+ message: 'Invalid or missing tool execution payload. Expected { tool_use: { id, tool_name, tool_input? }, metadata? }'
158
+ });
111
159
  }
112
160
 
113
161
  /**
@@ -1,21 +1,81 @@
1
+ import { AgentToolDefinition } from "@vertesia/common";
1
2
  import { HTTPException } from "hono/http-exception";
2
- import { Tool, ToolDefinition, ToolExecutionContext, ToolExecutionPayload, ToolExecutionResult } from "./types.js";
3
+ import { Tool, ToolExecutionContext, ToolExecutionPayload, ToolExecutionResult } from "./types.js";
4
+
5
+ /**
6
+ * Options for filtering tool definitions
7
+ */
8
+ export interface ToolFilterOptions {
9
+ /**
10
+ * If true, only return tools that are available by default (default !== false).
11
+ * If false or undefined, return all tools.
12
+ */
13
+ defaultOnly?: boolean;
14
+ /**
15
+ * List of tool names that are unlocked (available even if default: false).
16
+ * These tools will be included even when defaultOnly is true.
17
+ */
18
+ unlockedTools?: string[];
19
+ }
20
+
3
21
  export class ToolRegistry {
4
22
 
23
+ // The category name usinfg this registry
24
+ category: string;
5
25
  registry: Record<string, Tool<any>> = {};
6
26
 
7
- constructor(tools: Tool<any>[] = []) {
27
+ constructor(category: string, tools: Tool<any>[] = []) {
28
+ this.category = category;
8
29
  for (const tool of tools) {
9
30
  this.registry[tool.name] = tool;
10
31
  }
11
32
  }
12
33
 
13
- getDefinitions(): ToolDefinition[] {
14
- return Object.values(this.registry).map(tool => ({
15
- name: tool.name,
16
- description: tool.description,
17
- input_schema: tool.input_schema
18
- }));
34
+ /**
35
+ * Get tool definitions with optional filtering.
36
+ * @param options - Filtering options
37
+ * @returns Filtered tool definitions
38
+ */
39
+ getDefinitions(options?: ToolFilterOptions): AgentToolDefinition[] {
40
+ const { defaultOnly, unlockedTools = [] } = options || {};
41
+ const unlockedSet = new Set(unlockedTools);
42
+
43
+ return Object.values(this.registry)
44
+ .filter(tool => {
45
+ // If not filtering by default, include all tools
46
+ if (!defaultOnly) return true;
47
+
48
+ // Include if tool is default (default !== false) or is in unlocked list
49
+ const isDefault = tool.default !== false;
50
+ const isUnlocked = unlockedSet.has(tool.name);
51
+ return isDefault || isUnlocked;
52
+ })
53
+ .map(tool => ({
54
+ name: tool.name,
55
+ description: tool.description,
56
+ input_schema: tool.input_schema,
57
+ category: this.category,
58
+ default: tool.default,
59
+ }));
60
+ }
61
+
62
+ /**
63
+ * Get tools that are in reserve (default: false and not unlocked).
64
+ * @param unlockedTools - List of tool names that are unlocked
65
+ * @returns Tool definitions for reserve tools
66
+ */
67
+ getReserveTools(unlockedTools: string[] = []): AgentToolDefinition[] {
68
+ const unlockedSet = new Set(unlockedTools);
69
+
70
+ return Object.values(this.registry)
71
+ .filter(tool => tool.default === false && !unlockedSet.has(tool.name))
72
+ .map(tool => ({
73
+ name: tool.name,
74
+ description: tool.description,
75
+ input_schema: tool.input_schema,
76
+ category: this.category,
77
+ default: tool.default,
78
+ }));
19
79
  }
20
80
 
21
81
  getTool<ParamsT extends Record<string, any>>(name: string): Tool<ParamsT> {
@@ -35,7 +95,15 @@ export class ToolRegistry {
35
95
  }
36
96
 
37
97
  runTool<ParamsT extends Record<string, any>>(payload: ToolExecutionPayload<ParamsT>, context: ToolExecutionContext): Promise<ToolExecutionResult> {
38
- return this.getTool(payload.tool_use.tool_name).run(payload, context);
98
+ const toolName = payload.tool_use.tool_name;
99
+ const toolUseId = payload.tool_use.id;
100
+ const runId = payload.metadata?.run_id;
101
+ console.log(`[ToolRegistry] Executing tool: ${toolName}`, {
102
+ toolUseId,
103
+ runId,
104
+ input: sanitizeInput(payload.tool_use.tool_input),
105
+ });
106
+ return this.getTool(toolName).run(payload, context);
39
107
  }
40
108
 
41
109
  }
@@ -46,4 +114,28 @@ export class ToolNotFoundError extends HTTPException {
46
114
  super(404, { message: "Tool function not found: " + name });
47
115
  this.name = "ToolNotFoundError";
48
116
  }
117
+ }
118
+
119
+ const SENSITIVE_KEYS = new Set([
120
+ 'apikey', 'api_key', 'token', 'secret', 'password', 'credential', 'credentials',
121
+ 'authorization', 'auth', 'key', 'private_key', 'access_token', 'refresh_token'
122
+ ]);
123
+
124
+ function sanitizeInput(input: Record<string, any> | null | undefined): Record<string, any> | null {
125
+ if (!input) return null;
126
+
127
+ const sanitized: Record<string, any> = {};
128
+ for (const [key, value] of Object.entries(input)) {
129
+ const lowerKey = key.toLowerCase();
130
+ if (SENSITIVE_KEYS.has(lowerKey) || lowerKey.includes('key') || lowerKey.includes('token') || lowerKey.includes('secret')) {
131
+ sanitized[key] = '[REDACTED]';
132
+ } else if (typeof value === 'string' && value.length > 50) {
133
+ sanitized[key] = value.slice(0, 50) + '...';
134
+ } else if (typeof value === 'object' && value !== null) {
135
+ sanitized[key] = Array.isArray(value) ? `[Array(${value.length})]` : '[Object]';
136
+ } else {
137
+ sanitized[key] = value;
138
+ }
139
+ }
140
+ return sanitized;
49
141
  }