Skip to content

Commit d58f9fc

Browse files
committed
allow for an AI to write custom tools
1 parent 2a59723 commit d58f9fc

File tree

13 files changed

+359
-10
lines changed

13 files changed

+359
-10
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"pino-pretty": "^13.0.0",
4242
"prettier": "^3.7.4",
4343
"prismjs": "^1.30.0",
44-
"rawr": "^1.0.0",
44+
"rawr": "^1.0.1",
4545
"react": "^19.2.0",
4646
"react-dom": "^19.2.0",
4747
"react-simple-code-editor": "^0.14.1",

public/workers/mediapipe.worker.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/workers/mediapipe.worker.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/nodes/ai/custom-out.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Tool Out Node - Runtime implementation
3+
*
4+
* Returns the result of a custom tool execution back to the AI agent.
5+
*/
6+
export const toolOutRuntime = {
7+
type: 'tool-out',
8+
9+
onInit() {
10+
this.status({ text: 'Ready', fill: 'green' });
11+
},
12+
13+
onInput(msg) {
14+
const toolRequest = msg._toolRequest;
15+
16+
if (!toolRequest || !toolRequest.requestId) {
17+
this.status({ text: 'No request', fill: 'yellow' });
18+
this.warn('No _toolRequest context found. Wire this node to a tool-in node.');
19+
return;
20+
}
21+
22+
const { requestId } = toolRequest;
23+
const pending = globalThis._customToolRequests?.get(requestId);
24+
25+
if (!pending) {
26+
this.status({ text: 'Expired', fill: 'yellow' });
27+
this.warn(`Request ${requestId} not found or already completed (may have timed out)`);
28+
return;
29+
}
30+
31+
// Clear timeout and resolve the promise
32+
clearTimeout(pending.timeout);
33+
globalThis._customToolRequests.delete(requestId);
34+
35+
// Return the result
36+
pending.resolve(msg.payload);
37+
38+
this.status({ text: 'Sent', fill: 'green' });
39+
40+
// Reset status after a moment
41+
setTimeout(() => {
42+
this.status({ text: 'Ready', fill: 'green' });
43+
}, 1000);
44+
}
45+
};

src/nodes/ai/custom-out.jsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Tool Out Node - Returns result from a custom tool flow
3+
*/
4+
export const toolOutNode = {
5+
type: 'tool-out',
6+
category: 'ai',
7+
description: 'Returns the result of a custom tool to the AI',
8+
paletteLabel: 'tool out',
9+
label: (node) => node.name || 'tool out',
10+
color: '#a66bbf', // Purple for AI nodes
11+
fontColor: '#fff',
12+
icon: true,
13+
faChar: '\uf0ad', // wrench
14+
faColor: 'rgba(255,255,255,0.9)',
15+
16+
inputs: 1,
17+
outputs: 0,
18+
19+
defaults: {},
20+
21+
messageInterface: {
22+
reads: {
23+
payload: {
24+
type: 'any',
25+
description: 'Result to return to the AI'
26+
},
27+
_toolRequest: {
28+
type: 'object',
29+
description: 'Internal: request context from tool-in node'
30+
}
31+
}
32+
},
33+
34+
renderHelp() {
35+
return (
36+
<>
37+
<p>Returns the result of a custom tool execution back to the AI agent. Must receive a message from a <strong>tool-in</strong> node (which provides the internal request context).</p>
38+
39+
<h5>Input</h5>
40+
<ul>
41+
<li><code>msg.payload</code> - The result to return to the AI (any type)</li>
42+
<li><code>msg._toolRequest</code> - Internal context from tool-in node (passed through automatically)</li>
43+
</ul>
44+
45+
<h5>Usage</h5>
46+
<p>Wire this node at the end of your custom tool flow. The <code>msg.payload</code> will be returned as the tool's result.</p>
47+
48+
<h5>Example Flow</h5>
49+
<pre>{`[tool-in: get_weather]
50+
-> [http request: weather API]
51+
-> [function: format result]
52+
-> [tool-out]`}</pre>
53+
</>
54+
);
55+
}
56+
};

src/nodes/ai/custom-tool.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Tool In Node - Runtime implementation
3+
*
4+
* Registers a custom tool that AI agents can call via MCP.
5+
* When called, triggers the flow with the provided arguments.
6+
*/
7+
8+
// Global registry of custom tools (toolName -> node instance)
9+
// This is populated by onInit and cleared by onClose
10+
if (!globalThis._customTools) {
11+
globalThis._customTools = new Map();
12+
}
13+
14+
// Pending tool requests waiting for responses (requestId -> { resolve, reject, timeout })
15+
if (!globalThis._customToolRequests) {
16+
globalThis._customToolRequests = new Map();
17+
}
18+
19+
export const toolInRuntime = {
20+
type: 'tool-in',
21+
22+
onInit() {
23+
const toolName = this.config.toolName;
24+
25+
if (!toolName) {
26+
this.status({ text: 'No name', fill: 'red' });
27+
this.error('Tool name is required');
28+
return;
29+
}
30+
31+
// Validate tool name (no spaces, alphanumeric + underscore)
32+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(toolName)) {
33+
this.status({ text: 'Invalid name', fill: 'red' });
34+
this.error('Tool name must start with a letter or underscore and contain only letters, numbers, and underscores');
35+
return;
36+
}
37+
38+
// Check for duplicate tool names
39+
if (globalThis._customTools.has(toolName)) {
40+
this.status({ text: 'Duplicate', fill: 'yellow' });
41+
this.warn(`Tool "${toolName}" is already defined by another node`);
42+
}
43+
44+
// Register this tool
45+
globalThis._customTools.set(toolName, {
46+
nodeId: this.id,
47+
toolName,
48+
description: this.config.description || '',
49+
node: this
50+
});
51+
52+
this.status({ text: toolName, fill: 'green' });
53+
},
54+
55+
// Called by MCP handler when AI invokes this tool
56+
invoke(message, requestId) {
57+
// Merge incoming message with _toolRequest context
58+
const msg = {
59+
...(message || {}),
60+
_toolRequest: {
61+
requestId,
62+
toolName: this.config.toolName,
63+
nodeId: this.id
64+
}
65+
};
66+
this.send(msg);
67+
},
68+
69+
onClose() {
70+
const toolName = this.config.toolName;
71+
if (toolName && globalThis._customTools.get(toolName)?.nodeId === this.id) {
72+
globalThis._customTools.delete(toolName);
73+
}
74+
}
75+
};

src/nodes/ai/custom-tool.jsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Tool In Node - Defines an AI-callable tool backed by a flow
3+
*/
4+
export const toolInNode = {
5+
type: 'tool-in',
6+
category: 'ai',
7+
description: 'Defines a custom tool and receives calls to it from an AI agent',
8+
paletteLabel: 'tool in',
9+
label: (node) => node.name || node.toolName || 'tool in',
10+
color: '#a66bbf', // Purple for AI nodes
11+
fontColor: '#fff',
12+
icon: true,
13+
faChar: '\uf0ad', // wrench
14+
faColor: 'rgba(255,255,255,0.9)',
15+
16+
inputs: 0,
17+
outputs: 1,
18+
19+
defaults: {
20+
toolName: {
21+
type: 'string',
22+
default: '',
23+
label: 'Tool Name',
24+
placeholder: 'get_weather',
25+
required: true,
26+
description: 'Name for the tool (no spaces, like a function name)'
27+
},
28+
description: {
29+
type: 'string',
30+
default: '',
31+
label: 'Description',
32+
placeholder: 'Gets the current weather for a location',
33+
description: 'Description of what this tool does (helps AI know when to use it)'
34+
}
35+
},
36+
37+
messageInterface: {
38+
writes: {
39+
payload: {
40+
type: 'any',
41+
description: 'Payload from the message passed by the AI'
42+
},
43+
topic: {
44+
type: 'string',
45+
description: 'Topic from the message (if provided)'
46+
},
47+
_toolRequest: {
48+
type: 'object',
49+
description: 'Internal: request context for tool-out node'
50+
}
51+
}
52+
},
53+
54+
renderHelp() {
55+
return (
56+
<>
57+
<p>Defines a custom tool that AI agents can call via MCP. When an AI calls this tool, the flow is triggered with the provided arguments in <code>msg.payload</code>.</p>
58+
59+
<h5>Configuration</h5>
60+
<ul>
61+
<li><strong>Tool Name</strong> - Unique name for the tool (no spaces). This is how the AI will call it.</li>
62+
<li><strong>Description</strong> - Explains what the tool does. Good descriptions help the AI know when to use it.</li>
63+
</ul>
64+
65+
<h5>Output</h5>
66+
<ul>
67+
<li><code>msg.payload</code> - The payload from the message passed by the AI</li>
68+
<li><code>msg.topic</code> - Optional topic from the message</li>
69+
<li><code>msg._toolRequest</code> - Internal context (pass through to tool-out)</li>
70+
</ul>
71+
72+
<h5>Usage</h5>
73+
<p>Wire this node through your flow logic, then end with a <strong>tool-out</strong> node to return the result to the AI.</p>
74+
</>
75+
);
76+
}
77+
};

src/nodes/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ import { imageAiConfigNode, imageAiNode } from './ai/image-ai.jsx';
112112
import { llmConfigNode, llmNode } from './ai/llm.jsx';
113113
import { mcpOutputNode } from './ai/mcp-output.jsx';
114114
import { mcpInputNode } from './ai/mcp-input.jsx';
115+
import { toolInNode } from './ai/custom-tool.jsx';
116+
import { toolOutNode } from './ai/custom-out.jsx';
115117

116118
// Output nodes
117119
import { canvasConfigNode, canvasNode } from './output/canvas.jsx';
@@ -224,6 +226,8 @@ import { audioStemsNode } from './audio/stems.jsx';
224226
llmNode,
225227
mcpOutputNode,
226228
mcpInputNode,
229+
toolInNode,
230+
toolOutNode,
227231

228232
// Output
229233
canvasNode,

src/nodes/runtime.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ import { imageAiConfigRuntime, imageAiRuntime } from './ai/image-ai.js';
8686
import { llmConfigRuntime, llmRuntime } from './ai/llm.js';
8787
import { mcpOutputRuntime } from './ai/mcp-output.js';
8888
import { mcpInputRuntime } from './ai/mcp-input.js';
89+
import { toolInRuntime } from './ai/custom-tool.js';
90+
import { toolOutRuntime } from './ai/custom-out.js';
8991

9092
// Output nodes
9193
import { canvasConfigRuntime, canvasRuntime } from './output/canvas.js';
@@ -197,6 +199,8 @@ import { audioStemsRuntime } from './audio/stems.js';
197199
llmRuntime,
198200
mcpOutputRuntime,
199201
mcpInputRuntime,
202+
toolInRuntime,
203+
toolOutRuntime,
200204

201205
// Output
202206
canvasRuntime,

0 commit comments

Comments
 (0)