Spaces:
Running
Running
| import type { EngineChatTool, EngineChatToolCall } from '../engine'; | |
| import { deleteMemory, getMemory, listMemory, putMemory } from './idb'; | |
| import { runJavaScript } from './js-sandbox'; | |
| export interface ArtifactDocument { | |
| id: string; | |
| title: string; | |
| source: string; | |
| sandboxedSource: string; | |
| createdAt: number; | |
| } | |
| export interface ToolExecution { | |
| call: EngineChatToolCall; | |
| output: string; | |
| artifact?: ArtifactDocument; | |
| failed: boolean; | |
| } | |
| const MAX_ARTIFACT_BYTES = 256 * 1024; | |
| const MAX_TOOL_RESULT_CHARACTERS = 64 * 1024; | |
| const ARTIFACT_CSP = [ | |
| "default-src 'none'", | |
| "base-uri 'none'", | |
| "connect-src 'none'", | |
| "form-action 'none'", | |
| "frame-src 'none'", | |
| "img-src data: blob:", | |
| "font-src data:", | |
| "media-src data: blob:", | |
| "style-src 'unsafe-inline'", | |
| "script-src 'none'", | |
| ].join('; '); | |
| export const HTML_ARTIFACT_TOOL: EngineChatTool = { | |
| type: 'function', | |
| function: { | |
| name: 'html_artifact', | |
| description: 'Create a self-contained HTML/CSS artifact and open it in a network-isolated preview. Return complete HTML source, not a Markdown code block. JavaScript is never executed; use js_eval for deterministic computation.', | |
| parameters: { | |
| type: 'object', | |
| properties: { | |
| html: { type: 'string', description: 'Complete, self-contained HTML/CSS source. Script elements remain visible in the code view but are not executed.' }, | |
| title: { type: 'string', description: 'Short artifact title.' }, | |
| }, | |
| required: ['html'], | |
| additionalProperties: false, | |
| }, | |
| }, | |
| }; | |
| export const AGENT_TOOLS: EngineChatTool[] = [ | |
| HTML_ARTIFACT_TOOL, | |
| { | |
| type: 'function', | |
| function: { | |
| name: 'js_eval', | |
| description: 'Evaluate deterministic JavaScript in a disposable worker with no network or browser storage. Return the result explicitly.', | |
| parameters: { | |
| type: 'object', | |
| properties: { | |
| source: { type: 'string', description: 'JavaScript function body. Use return to emit a result.' }, | |
| }, | |
| required: ['source'], | |
| additionalProperties: false, | |
| }, | |
| }, | |
| }, | |
| { | |
| type: 'function', | |
| function: { | |
| name: 'memory', | |
| description: 'Store, retrieve, list, or delete small user-approved notes in this browser profile.', | |
| parameters: { | |
| type: 'object', | |
| properties: { | |
| action: { type: 'string', enum: ['put', 'get', 'list', 'delete'] }, | |
| key: { type: 'string', description: 'Memory key for put, get, or delete.' }, | |
| value: { type: 'string', description: 'Memory value for put.' }, | |
| }, | |
| required: ['action'], | |
| additionalProperties: false, | |
| }, | |
| }, | |
| }, | |
| ]; | |
| function isRecord(value: unknown): value is Record<string, unknown> { | |
| return typeof value === 'object' && value !== null && !Array.isArray(value); | |
| } | |
| function requiredString(value: unknown, field: string): string { | |
| if (typeof value !== 'string' || !value.trim()) throw new Error(`${field} must be a non-empty string`); | |
| return value; | |
| } | |
| function parseArguments(call: EngineChatToolCall): Record<string, unknown> { | |
| const parsed: unknown = JSON.parse(call.function.arguments); | |
| if (!isRecord(parsed)) throw new Error('tool arguments must be a JSON object'); | |
| return parsed; | |
| } | |
| function boundedJson(value: unknown): string { | |
| const serialized = JSON.stringify(value); | |
| if (serialized.length <= MAX_TOOL_RESULT_CHARACTERS) return serialized; | |
| return JSON.stringify({ | |
| ok: false, | |
| error: 'tool result exceeded 64 KiB and was truncated', | |
| preview: serialized.slice(0, MAX_TOOL_RESULT_CHARACTERS - 256), | |
| }); | |
| } | |
| function injectArtifactCsp(html: string): string { | |
| const meta = `<meta http-equiv="Content-Security-Policy" content="${ARTIFACT_CSP}">`; | |
| return `<!doctype html><html><head>${meta}</head><body>${html}</body></html>`; | |
| } | |
| export function prepareHtmlArtifact( | |
| html: string, | |
| title = 'Untitled artifact', | |
| id: string = crypto.randomUUID(), | |
| ): ArtifactDocument { | |
| const bytes = new TextEncoder().encode(html).byteLength; | |
| if (bytes > MAX_ARTIFACT_BYTES) throw new Error('html_artifact source exceeds 256 KiB'); | |
| return { | |
| id, | |
| title: title.trim().slice(0, 120) || 'Untitled artifact', | |
| source: html, | |
| sandboxedSource: injectArtifactCsp(html), | |
| createdAt: Date.now(), | |
| }; | |
| } | |
| async function executeMemory(args: Record<string, unknown>): Promise<unknown> { | |
| const action = requiredString(args.action, 'memory.action'); | |
| if (action === 'list') { | |
| return { | |
| action, | |
| entries: (await listMemory()).slice(0, 100).map((record) => ({ | |
| key: record.key, | |
| valuePreview: record.value.slice(0, 512), | |
| updatedAt: record.updatedAt, | |
| })), | |
| }; | |
| } | |
| const key = requiredString(args.key, 'memory.key'); | |
| if (action === 'put') { | |
| const value = requiredString(args.value, 'memory.value'); | |
| if (!window.confirm(`Allow Bonsai to store the local memory note “${key}”?`)) { | |
| throw new Error('operator declined memory write'); | |
| } | |
| return { action, record: await putMemory(key, value) }; | |
| } | |
| if (action === 'get') { | |
| return { action, record: await getMemory(key) ?? null }; | |
| } | |
| if (action === 'delete') { | |
| if (!window.confirm(`Allow Bonsai to delete the local memory note “${key}”?`)) { | |
| throw new Error('operator declined memory deletion'); | |
| } | |
| await deleteMemory(key); | |
| return { action, key, deleted: true }; | |
| } | |
| throw new Error(`unsupported memory action: ${action}`); | |
| } | |
| export async function executeAgentTool(call: EngineChatToolCall, signal?: AbortSignal): Promise<ToolExecution> { | |
| try { | |
| if (signal?.aborted) throw new DOMException('The tool operation was aborted.', 'AbortError'); | |
| const args = parseArguments(call); | |
| if (call.function.name === 'html_artifact') { | |
| const artifact = prepareHtmlArtifact( | |
| requiredString(args.html, 'html_artifact.html'), | |
| typeof args.title === 'string' ? args.title : undefined, | |
| call.id, | |
| ); | |
| return { | |
| call, | |
| artifact, | |
| output: boundedJson({ ok: true, artifactId: artifact.id, title: artifact.title, bytes: artifact.source.length }), | |
| failed: false, | |
| }; | |
| } | |
| if (call.function.name === 'js_eval') { | |
| const result = await runJavaScript(requiredString(args.source, 'js_eval.source'), 5000, signal); | |
| return { call, output: boundedJson({ ok: true, ...result }), failed: false }; | |
| } | |
| if (call.function.name === 'memory') { | |
| const result = await executeMemory(args); | |
| if (signal?.aborted) throw new DOMException('The tool operation was aborted.', 'AbortError'); | |
| return { call, output: boundedJson({ ok: true, ...(result as object) }), failed: false }; | |
| } | |
| throw new Error(`unsupported tool: ${call.function.name}`); | |
| } catch (error) { | |
| const message = error instanceof Error ? error.message : String(error); | |
| return { call, output: boundedJson({ ok: false, error: message }), failed: true }; | |
| } | |
| } | |