/** * Self-modification tools: agent creates/lists/deletes its own tools, * updates its own system prompt. Inspired by agi-diy `create_tool`. * * Custom tools are stored in IndexedDB as JS source code. * On each agent build, they're eval'd back into tool() instances. */ import { tool, type Tool } from '@strands-agents/sdk' import { z } from 'zod' import { get, set, del } from 'idb-keyval' const CUSTOM_TOOLS_KEY = 'careless-custom-tools' const SYSTEM_PROMPT_ADDITIONS_KEY = 'careless-system-prompt-additions' interface StoredCustomTool { name: string description: string /** JSON schema-ish; keys w/ {type, description} */ params: Record /** JS source for the callback; async function body. Has `input` in scope. */ source: string createdAt: number } export async function loadCustomTools(): Promise { try { const stored = await get(CUSTOM_TOOLS_KEY) return Array.isArray(stored) ? stored : [] } catch { return [] } } interface ParamSpec { type?: 'string' | 'number' | 'boolean' | 'array' | 'object' description?: string required?: boolean } async function saveCustomTools(tools: StoredCustomTool[]) { await set(CUSTOM_TOOLS_KEY, tools) } function paramsToZod(params: Record) { const shape: Record = {} for (const [k, v] of Object.entries(params || {})) { let t: z.ZodTypeAny = z.string() if (v.type === 'number') t = z.number() else if (v.type === 'boolean') t = z.boolean() else if (v.type === 'array') t = z.array(z.any()) else if (v.type === 'object') t = z.record(z.string(), z.any()) if (v.description) t = t.describe(v.description) if (v.required === false) t = t.optional() shape[k] = t } return z.object(shape) } /** Build actual tool() instances from stored custom tools. */ export async function buildCustomTools() { const stored = await loadCustomTools() const result: Tool[] = [] for (const s of stored) { try { const inputSchema = paramsToZod(s.params as Record) // Build callback from source. `input` is in scope. // eslint-disable-next-line @typescript-eslint/no-implied-eval const fn = new Function('input', `return (async () => { ${s.source} })()`) const t = tool({ name: s.name, description: s.description + ' [custom user tool]', inputSchema, callback: async (input: unknown) => { try { const r = await fn(input) return typeof r === 'string' ? r : JSON.stringify(r) } catch (err: unknown) { const msg = err instanceof Error ? (err as Error).message : String(err) return JSON.stringify({ status: 'error', error: msg }) } }, }) result.push(t) } catch (e) { console.warn('[custom-tool] failed to build', s.name, e) } } return result } export const createToolTool = tool({ name: 'create_tool', description: 'Define a new tool for yourself at runtime. Takes a name, description, JSON-style params schema, and JS source (async, `input` in scope, return a string or JSON-serializable value). Persists across sessions.', inputSchema: z.object({ name: z.string().describe('Snake_case tool name (must be unique)'), description: z.string().describe('What this tool does; agent sees this'), params: z.string().describe('JSON string: {paramName: {type: "string"|"number"|"boolean", description?: "...", required?: true}}'), source: z.string().describe('JS code. `input` object in scope. Return or await any value.'), }), callback: async (input) => { try { const existing = await loadCustomTools() if (existing.find(t => t.name === input.name)) { return JSON.stringify({ status: 'error', error: `Tool ${input.name} already exists. Delete it first.` }) } let params: any = {} try { params = JSON.parse(input.params || '{}') } catch (e: unknown) { return JSON.stringify({ status: 'error', error: `params JSON parse: ${(e as Error).message}` }) } // Validate source compiles try { // eslint-disable-next-line @typescript-eslint/no-implied-eval new Function('input', `return (async () => { ${input.source} })()`) } catch (e: unknown) { return JSON.stringify({ status: 'error', error: `source compile: ${(e as Error).message}` }) } const newTool: StoredCustomTool = { name: input.name, description: input.description, params, source: input.source, createdAt: Date.now(), } await saveCustomTools([...existing, newTool]) window.dispatchEvent(new CustomEvent('careless:custom-tools-changed')) return JSON.stringify({ status: 'created', name: input.name, total: existing.length + 1, note: 'Tool available on next agent turn.' }) } catch (err: unknown) { return JSON.stringify({ status: 'error', error: (err as Error).message }) } }, }) export const listCustomToolsTool = tool({ name: 'list_custom_tools', description: 'List all user-created custom tools (created via create_tool).', inputSchema: z.object({}), callback: async () => { const tools = await loadCustomTools() return JSON.stringify({ status: 'success', count: tools.length, tools: tools.map(t => ({ name: t.name, description: t.description, created_at: t.createdAt })), }) }, }) export const deleteCustomToolTool = tool({ name: 'delete_custom_tool', description: 'Delete a user-created custom tool by name.', inputSchema: z.object({ name: z.string() }), callback: async (input) => { const existing = await loadCustomTools() const filtered = existing.filter(t => t.name !== input.name) if (filtered.length === existing.length) { return JSON.stringify({ status: 'error', error: `Tool ${input.name} not found` }) } await saveCustomTools(filtered) window.dispatchEvent(new CustomEvent('careless:custom-tools-changed')) return JSON.stringify({ status: 'deleted', name: input.name, remaining: filtered.length }) }, }) export const updateSelfPromptTool = tool({ name: 'update_self_prompt', description: 'Append a learning/insight to your own system prompt. Persists across sessions. Use this to remember things about the user or evolve your behavior.', inputSchema: z.object({ addition: z.string().describe('Text to append to system prompt'), reason: z.string().optional(), }), callback: async (input) => { try { const existing = (await get(SYSTEM_PROMPT_ADDITIONS_KEY)) || [] existing.push(input.addition) await set(SYSTEM_PROMPT_ADDITIONS_KEY, existing) window.dispatchEvent(new CustomEvent('careless:system-prompt-changed')) return JSON.stringify({ status: 'added', total_additions: existing.length, reason: input.reason }) } catch (err: unknown) { return JSON.stringify({ status: 'error', error: (err as Error).message }) } }, }) export const listSelfPromptAdditionsTool = tool({ name: 'list_self_prompt_additions', description: 'List all self-added system prompt additions.', inputSchema: z.object({}), callback: async () => { const additions = (await get(SYSTEM_PROMPT_ADDITIONS_KEY)) || [] return JSON.stringify({ status: 'success', count: additions.length, additions }) }, }) export const clearSelfPromptAdditionsTool = tool({ name: 'clear_self_prompt_additions', description: 'Clear all self-added system prompt additions.', inputSchema: z.object({}), callback: async () => { await del(SYSTEM_PROMPT_ADDITIONS_KEY) window.dispatchEvent(new CustomEvent('careless:system-prompt-changed')) return JSON.stringify({ status: 'cleared' }) }, }) export async function getSelfPromptAdditions(): Promise { const additions = (await get(SYSTEM_PROMPT_ADDITIONS_KEY)) || [] if (!additions.length) return '' return `\n\n## 📝 Self-Learned Context\n${additions.map((a, i) => `${i + 1}. ${a}`).join('\n')}` } export const SELF_MODIFY_TOOLS = [ createToolTool, listCustomToolsTool, deleteCustomToolTool, updateSelfPromptTool, listSelfPromptAdditionsTool, clearSelfPromptAdditionsTool, ] /** Helper: get just the names of stored custom tools. */ export async function getCustomToolNames(): Promise { const tools = await loadCustomTools() return tools.map(t => t.name) }