@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.
- package/package.json +9 -8
- package/src/ContentTypesCollection.ts +51 -0
- package/src/InteractionCollection.ts +1 -94
- package/src/SkillCollection.ts +88 -15
- package/src/ToolCollection.ts +65 -17
- package/src/ToolRegistry.ts +101 -9
- package/src/auth.ts +37 -6
- package/src/index.ts +7 -3
- package/src/server/app-package.ts +102 -0
- package/src/server/conyent-types.ts +71 -0
- package/src/server/interactions.ts +100 -0
- package/src/server/mcp.ts +51 -0
- package/src/server/site.ts +53 -0
- package/src/server/skills.ts +133 -0
- package/src/server/tools.ts +128 -0
- package/src/server/types.ts +87 -0
- package/src/server/widgets.ts +38 -0
- package/src/server.ts +80 -359
- package/src/site/styles.ts +71 -0
- package/src/site/templates.ts +215 -119
- package/src/types.ts +22 -18
- package/src/utils.ts +20 -0
- package/lib/cjs/InteractionCollection.js +0 -164
- package/lib/cjs/InteractionCollection.js.map +0 -1
- package/lib/cjs/SkillCollection.js +0 -318
- package/lib/cjs/SkillCollection.js.map +0 -1
- package/lib/cjs/ToolCollection.js +0 -192
- package/lib/cjs/ToolCollection.js.map +0 -1
- package/lib/cjs/ToolRegistry.js +0 -44
- package/lib/cjs/ToolRegistry.js.map +0 -1
- package/lib/cjs/auth.js +0 -89
- package/lib/cjs/auth.js.map +0 -1
- package/lib/cjs/build/validate.js +0 -7
- package/lib/cjs/build/validate.js.map +0 -1
- package/lib/cjs/copy-assets.js +0 -84
- package/lib/cjs/copy-assets.js.map +0 -1
- package/lib/cjs/index.js +0 -30
- package/lib/cjs/index.js.map +0 -1
- package/lib/cjs/package.json +0 -3
- package/lib/cjs/server.js +0 -327
- package/lib/cjs/server.js.map +0 -1
- package/lib/cjs/site/styles.js +0 -621
- package/lib/cjs/site/styles.js.map +0 -1
- package/lib/cjs/site/templates.js +0 -932
- package/lib/cjs/site/templates.js.map +0 -1
- package/lib/cjs/types.js +0 -3
- package/lib/cjs/types.js.map +0 -1
- package/lib/cjs/utils.js +0 -7
- package/lib/cjs/utils.js.map +0 -1
- package/lib/esm/InteractionCollection.js +0 -125
- package/lib/esm/InteractionCollection.js.map +0 -1
- package/lib/esm/SkillCollection.js +0 -311
- package/lib/esm/SkillCollection.js.map +0 -1
- package/lib/esm/ToolCollection.js +0 -154
- package/lib/esm/ToolCollection.js.map +0 -1
- package/lib/esm/ToolRegistry.js +0 -39
- package/lib/esm/ToolRegistry.js.map +0 -1
- package/lib/esm/auth.js +0 -82
- package/lib/esm/auth.js.map +0 -1
- package/lib/esm/build/validate.js +0 -4
- package/lib/esm/build/validate.js.map +0 -1
- package/lib/esm/copy-assets.js +0 -81
- package/lib/esm/copy-assets.js.map +0 -1
- package/lib/esm/index.js +0 -10
- package/lib/esm/index.js.map +0 -1
- package/lib/esm/server.js +0 -323
- package/lib/esm/server.js.map +0 -1
- package/lib/esm/site/styles.js +0 -618
- package/lib/esm/site/styles.js.map +0 -1
- package/lib/esm/site/templates.js +0 -920
- package/lib/esm/site/templates.js.map +0 -1
- package/lib/esm/types.js +0 -2
- package/lib/esm/types.js.map +0 -1
- package/lib/esm/utils.js +0 -4
- package/lib/esm/utils.js.map +0 -1
- package/lib/types/InteractionCollection.d.ts +0 -48
- package/lib/types/InteractionCollection.d.ts.map +0 -1
- package/lib/types/SkillCollection.d.ts +0 -111
- package/lib/types/SkillCollection.d.ts.map +0 -1
- package/lib/types/ToolCollection.d.ts +0 -61
- package/lib/types/ToolCollection.d.ts.map +0 -1
- package/lib/types/ToolRegistry.d.ts +0 -15
- package/lib/types/ToolRegistry.d.ts.map +0 -1
- package/lib/types/auth.d.ts +0 -20
- package/lib/types/auth.d.ts.map +0 -1
- package/lib/types/build/validate.d.ts +0 -2
- package/lib/types/build/validate.d.ts.map +0 -1
- package/lib/types/copy-assets.d.ts +0 -14
- package/lib/types/copy-assets.d.ts.map +0 -1
- package/lib/types/index.d.ts +0 -10
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/server.d.ts +0 -72
- package/lib/types/server.d.ts.map +0 -1
- package/lib/types/site/styles.d.ts +0 -5
- package/lib/types/site/styles.d.ts.map +0 -1
- package/lib/types/site/templates.d.ts +0 -54
- package/lib/types/site/templates.d.ts.map +0 -1
- package/lib/types/types.d.ts +0 -262
- package/lib/types/types.d.ts.map +0 -1
- package/lib/types/utils.d.ts +0 -2
- 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.
|
|
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": "^
|
|
21
|
+
"@types/node": "^22.19.2",
|
|
22
22
|
"ts-dual-module": "^0.6.3",
|
|
23
|
-
"typescript": "^5.
|
|
24
|
-
"vitest": "^
|
|
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.
|
|
30
|
+
"hono": "^4.11.7"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"jose": "^6.0.11",
|
|
34
|
-
"
|
|
35
|
-
"@
|
|
36
|
-
"@vertesia/
|
|
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 {
|
|
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
|
-
}
|
package/src/SkillCollection.ts
CHANGED
|
@@ -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():
|
|
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
|
-
|
|
100
|
-
description
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
128
|
-
const skillName = toolName.startsWith('
|
|
129
|
-
? toolName.
|
|
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:
|
|
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
|
package/src/ToolCollection.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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:
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
/**
|
package/src/ToolRegistry.ts
CHANGED
|
@@ -1,21 +1,81 @@
|
|
|
1
|
+
import { AgentToolDefinition } from "@vertesia/common";
|
|
1
2
|
import { HTTPException } from "hono/http-exception";
|
|
2
|
-
import { Tool,
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
}
|