luca 1.1.2 → 3.0.0
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/.github/workflows/release.yaml +169 -0
- package/AGENTS.md +99 -0
- package/CLAUDE.md +115 -0
- package/CNAME +1 -0
- package/README.md +257 -8
- package/RUNME.md +56 -0
- package/assistants/codingAssistant/ABOUT.md +5 -0
- package/assistants/codingAssistant/CORE.md +28 -0
- package/assistants/codingAssistant/hooks.ts +21 -0
- package/assistants/codingAssistant/tools.ts +12 -0
- package/assistants/inkbot/ABOUT.md +16 -0
- package/assistants/inkbot/CORE.md +330 -0
- package/assistants/inkbot/hooks.ts +6 -0
- package/assistants/inkbot/tools.ts +53 -0
- package/assistants/researcher/ABOUT.md +5 -0
- package/assistants/researcher/CORE.md +46 -0
- package/assistants/researcher/hooks.ts +16 -0
- package/assistants/researcher/tools.ts +237 -0
- package/bun.lock +2769 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-bootstrap.ts +118 -0
- package/commands/build-python-bridge.ts +43 -0
- package/commands/build-scaffolds.ts +176 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/inkbot.ts +874 -0
- package/commands/release.ts +80 -0
- package/commands/try-all-challenges.ts +543 -0
- package/commands/try-challenge.ts +100 -0
- package/dist/agi/container.server.d.ts +63 -0
- package/dist/agi/container.server.d.ts.map +1 -0
- package/dist/agi/endpoints/ask.d.ts +20 -0
- package/dist/agi/endpoints/ask.d.ts.map +1 -0
- package/dist/agi/endpoints/conversations/[id].d.ts +27 -0
- package/dist/agi/endpoints/conversations/[id].d.ts.map +1 -0
- package/dist/agi/endpoints/conversations.d.ts +18 -0
- package/dist/agi/endpoints/conversations.d.ts.map +1 -0
- package/dist/agi/endpoints/experts.d.ts +8 -0
- package/dist/agi/endpoints/experts.d.ts.map +1 -0
- package/dist/agi/feature.d.ts +9 -0
- package/dist/agi/feature.d.ts.map +1 -0
- package/dist/agi/features/assistant.d.ts +509 -0
- package/dist/agi/features/assistant.d.ts.map +1 -0
- package/dist/agi/features/assistants-manager.d.ts +236 -0
- package/dist/agi/features/assistants-manager.d.ts.map +1 -0
- package/dist/agi/features/autonomous-assistant.d.ts +281 -0
- package/dist/agi/features/autonomous-assistant.d.ts.map +1 -0
- package/dist/agi/features/browser-use.d.ts +479 -0
- package/dist/agi/features/browser-use.d.ts.map +1 -0
- package/dist/agi/features/claude-code.d.ts +824 -0
- package/dist/agi/features/claude-code.d.ts.map +1 -0
- package/dist/agi/features/conversation-history.d.ts +245 -0
- package/dist/agi/features/conversation-history.d.ts.map +1 -0
- package/dist/agi/features/conversation.d.ts +464 -0
- package/dist/agi/features/conversation.d.ts.map +1 -0
- package/dist/agi/features/docs-reader.d.ts +72 -0
- package/dist/agi/features/docs-reader.d.ts.map +1 -0
- package/dist/agi/features/file-tools.d.ts +110 -0
- package/dist/agi/features/file-tools.d.ts.map +1 -0
- package/dist/agi/features/luca-coder.d.ts +323 -0
- package/dist/agi/features/luca-coder.d.ts.map +1 -0
- package/dist/agi/features/openai-codex.d.ts +381 -0
- package/dist/agi/features/openai-codex.d.ts.map +1 -0
- package/dist/agi/features/openapi.d.ts +200 -0
- package/dist/agi/features/openapi.d.ts.map +1 -0
- package/dist/agi/features/skills-library.d.ts +167 -0
- package/dist/agi/features/skills-library.d.ts.map +1 -0
- package/dist/agi/index.d.ts +5 -0
- package/dist/agi/index.d.ts.map +1 -0
- package/dist/agi/lib/interceptor-chain.d.ts +44 -0
- package/dist/agi/lib/interceptor-chain.d.ts.map +1 -0
- package/dist/agi/lib/token-counter.d.ts +13 -0
- package/dist/agi/lib/token-counter.d.ts.map +1 -0
- package/dist/bootstrap/generated.d.ts +5 -0
- package/dist/bootstrap/generated.d.ts.map +1 -0
- package/dist/browser.d.ts +12 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/bus.d.ts +29 -0
- package/dist/bus.d.ts.map +1 -0
- package/dist/cli/build-info.d.ts +4 -0
- package/dist/cli/build-info.d.ts.map +1 -0
- package/dist/cli/cli.d.ts +3 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/client.d.ts +60 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/clients/civitai/index.d.ts +472 -0
- package/dist/clients/civitai/index.d.ts.map +1 -0
- package/dist/clients/client-template.d.ts +30 -0
- package/dist/clients/client-template.d.ts.map +1 -0
- package/dist/clients/comfyui/index.d.ts +281 -0
- package/dist/clients/comfyui/index.d.ts.map +1 -0
- package/dist/clients/elevenlabs/index.d.ts +197 -0
- package/dist/clients/elevenlabs/index.d.ts.map +1 -0
- package/dist/clients/graph.d.ts +64 -0
- package/dist/clients/graph.d.ts.map +1 -0
- package/dist/clients/openai/index.d.ts +247 -0
- package/dist/clients/openai/index.d.ts.map +1 -0
- package/dist/clients/rest.d.ts +92 -0
- package/dist/clients/rest.d.ts.map +1 -0
- package/dist/clients/supabase/index.d.ts +176 -0
- package/dist/clients/supabase/index.d.ts.map +1 -0
- package/dist/clients/websocket.d.ts +127 -0
- package/dist/clients/websocket.d.ts.map +1 -0
- package/dist/command.d.ts +163 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/commands/bootstrap.d.ts +20 -0
- package/dist/commands/bootstrap.d.ts.map +1 -0
- package/dist/commands/chat.d.ts +37 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/code.d.ts +28 -0
- package/dist/commands/code.d.ts.map +1 -0
- package/dist/commands/console.d.ts +22 -0
- package/dist/commands/console.d.ts.map +1 -0
- package/dist/commands/describe.d.ts +50 -0
- package/dist/commands/describe.d.ts.map +1 -0
- package/dist/commands/eval.d.ts +23 -0
- package/dist/commands/eval.d.ts.map +1 -0
- package/dist/commands/help.d.ts +25 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/index.d.ts +18 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/introspect.d.ts +24 -0
- package/dist/commands/introspect.d.ts.map +1 -0
- package/dist/commands/mcp.d.ts +35 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/prompt.d.ts +38 -0
- package/dist/commands/prompt.d.ts.map +1 -0
- package/dist/commands/run.d.ts +24 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/sandbox-mcp.d.ts +34 -0
- package/dist/commands/sandbox-mcp.d.ts.map +1 -0
- package/dist/commands/save-api-docs.d.ts +21 -0
- package/dist/commands/save-api-docs.d.ts.map +1 -0
- package/dist/commands/scaffold.d.ts +24 -0
- package/dist/commands/scaffold.d.ts.map +1 -0
- package/dist/commands/select.d.ts +22 -0
- package/dist/commands/select.d.ts.map +1 -0
- package/dist/commands/serve.d.ts +29 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/container-describer.d.ts +144 -0
- package/dist/container-describer.d.ts.map +1 -0
- package/dist/container.d.ts +451 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/endpoint.d.ts +113 -0
- package/dist/endpoint.d.ts.map +1 -0
- package/dist/feature.d.ts +47 -0
- package/dist/feature.d.ts.map +1 -0
- package/dist/graft.d.ts +29 -0
- package/dist/graft.d.ts.map +1 -0
- package/dist/hash-object.d.ts +8 -0
- package/dist/hash-object.d.ts.map +1 -0
- package/dist/helper.d.ts +209 -0
- package/dist/helper.d.ts.map +1 -0
- package/dist/introspection/generated.node.d.ts +44623 -0
- package/dist/introspection/generated.node.d.ts.map +1 -0
- package/dist/introspection/generated.web.d.ts +1412 -0
- package/dist/introspection/generated.web.d.ts.map +1 -0
- package/dist/introspection/index.d.ts +156 -0
- package/dist/introspection/index.d.ts.map +1 -0
- package/dist/introspection/scan.d.ts +147 -0
- package/dist/introspection/scan.d.ts.map +1 -0
- package/dist/node/container.d.ts +256 -0
- package/dist/node/container.d.ts.map +1 -0
- package/dist/node/feature.d.ts +9 -0
- package/dist/node/feature.d.ts.map +1 -0
- package/dist/node/features/container-link.d.ts +213 -0
- package/dist/node/features/container-link.d.ts.map +1 -0
- package/dist/node/features/content-db.d.ts +354 -0
- package/dist/node/features/content-db.d.ts.map +1 -0
- package/dist/node/features/disk-cache.d.ts +236 -0
- package/dist/node/features/disk-cache.d.ts.map +1 -0
- package/dist/node/features/dns.d.ts +511 -0
- package/dist/node/features/dns.d.ts.map +1 -0
- package/dist/node/features/docker.d.ts +485 -0
- package/dist/node/features/docker.d.ts.map +1 -0
- package/dist/node/features/downloader.d.ts +73 -0
- package/dist/node/features/downloader.d.ts.map +1 -0
- package/dist/node/features/figlet-fonts.d.ts +4 -0
- package/dist/node/features/figlet-fonts.d.ts.map +1 -0
- package/dist/node/features/file-manager.d.ts +177 -0
- package/dist/node/features/file-manager.d.ts.map +1 -0
- package/dist/node/features/fs.d.ts +635 -0
- package/dist/node/features/fs.d.ts.map +1 -0
- package/dist/node/features/git.d.ts +329 -0
- package/dist/node/features/git.d.ts.map +1 -0
- package/dist/node/features/google-auth.d.ts +200 -0
- package/dist/node/features/google-auth.d.ts.map +1 -0
- package/dist/node/features/google-calendar.d.ts +194 -0
- package/dist/node/features/google-calendar.d.ts.map +1 -0
- package/dist/node/features/google-docs.d.ts +138 -0
- package/dist/node/features/google-docs.d.ts.map +1 -0
- package/dist/node/features/google-drive.d.ts +202 -0
- package/dist/node/features/google-drive.d.ts.map +1 -0
- package/dist/node/features/google-mail.d.ts +221 -0
- package/dist/node/features/google-mail.d.ts.map +1 -0
- package/dist/node/features/google-sheets.d.ts +157 -0
- package/dist/node/features/google-sheets.d.ts.map +1 -0
- package/dist/node/features/grep.d.ts +207 -0
- package/dist/node/features/grep.d.ts.map +1 -0
- package/dist/node/features/helpers.d.ts +236 -0
- package/dist/node/features/helpers.d.ts.map +1 -0
- package/dist/node/features/ink.d.ts +332 -0
- package/dist/node/features/ink.d.ts.map +1 -0
- package/dist/node/features/ipc-socket.d.ts +298 -0
- package/dist/node/features/ipc-socket.d.ts.map +1 -0
- package/dist/node/features/json-tree.d.ts +140 -0
- package/dist/node/features/json-tree.d.ts.map +1 -0
- package/dist/node/features/networking.d.ts +373 -0
- package/dist/node/features/networking.d.ts.map +1 -0
- package/dist/node/features/nlp.d.ts +125 -0
- package/dist/node/features/nlp.d.ts.map +1 -0
- package/dist/node/features/opener.d.ts +93 -0
- package/dist/node/features/opener.d.ts.map +1 -0
- package/dist/node/features/os.d.ts +168 -0
- package/dist/node/features/os.d.ts.map +1 -0
- package/dist/node/features/package-finder.d.ts +419 -0
- package/dist/node/features/package-finder.d.ts.map +1 -0
- package/dist/node/features/postgres.d.ts +173 -0
- package/dist/node/features/postgres.d.ts.map +1 -0
- package/dist/node/features/proc.d.ts +285 -0
- package/dist/node/features/proc.d.ts.map +1 -0
- package/dist/node/features/process-manager.d.ts +427 -0
- package/dist/node/features/process-manager.d.ts.map +1 -0
- package/dist/node/features/python.d.ts +477 -0
- package/dist/node/features/python.d.ts.map +1 -0
- package/dist/node/features/redis.d.ts +247 -0
- package/dist/node/features/redis.d.ts.map +1 -0
- package/dist/node/features/repl.d.ts +84 -0
- package/dist/node/features/repl.d.ts.map +1 -0
- package/dist/node/features/runpod.d.ts +527 -0
- package/dist/node/features/runpod.d.ts.map +1 -0
- package/dist/node/features/secure-shell.d.ts +145 -0
- package/dist/node/features/secure-shell.d.ts.map +1 -0
- package/dist/node/features/semantic-search.d.ts +207 -0
- package/dist/node/features/semantic-search.d.ts.map +1 -0
- package/dist/node/features/sqlite.d.ts +180 -0
- package/dist/node/features/sqlite.d.ts.map +1 -0
- package/dist/node/features/telegram.d.ts +173 -0
- package/dist/node/features/telegram.d.ts.map +1 -0
- package/dist/node/features/transpiler.d.ts +51 -0
- package/dist/node/features/transpiler.d.ts.map +1 -0
- package/dist/node/features/tts.d.ts +108 -0
- package/dist/node/features/tts.d.ts.map +1 -0
- package/dist/node/features/ui.d.ts +562 -0
- package/dist/node/features/ui.d.ts.map +1 -0
- package/dist/node/features/vault.d.ts +90 -0
- package/dist/node/features/vault.d.ts.map +1 -0
- package/dist/node/features/vm.d.ts +285 -0
- package/dist/node/features/vm.d.ts.map +1 -0
- package/dist/node/features/yaml-tree.d.ts +118 -0
- package/dist/node/features/yaml-tree.d.ts.map +1 -0
- package/dist/node/features/yaml.d.ts +127 -0
- package/dist/node/features/yaml.d.ts.map +1 -0
- package/dist/node.d.ts +67 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/python/generated.d.ts +2 -0
- package/dist/python/generated.d.ts.map +1 -0
- package/dist/react/index.d.ts +36 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/registry.d.ts +97 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/scaffolds/generated.d.ts +13 -0
- package/dist/scaffolds/generated.d.ts.map +1 -0
- package/dist/scaffolds/template.d.ts +11 -0
- package/dist/scaffolds/template.d.ts.map +1 -0
- package/dist/schemas/base.d.ts +254 -0
- package/dist/schemas/base.d.ts.map +1 -0
- package/dist/selector.d.ts +130 -0
- package/dist/selector.d.ts.map +1 -0
- package/dist/server.d.ts +89 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/servers/express.d.ts +104 -0
- package/dist/servers/express.d.ts.map +1 -0
- package/dist/servers/mcp.d.ts +201 -0
- package/dist/servers/mcp.d.ts.map +1 -0
- package/dist/servers/socket.d.ts +121 -0
- package/dist/servers/socket.d.ts.map +1 -0
- package/dist/state.d.ts +24 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/web/clients/socket.d.ts +37 -0
- package/dist/web/clients/socket.d.ts.map +1 -0
- package/dist/web/container.d.ts +55 -0
- package/dist/web/container.d.ts.map +1 -0
- package/dist/web/extension.d.ts +4 -0
- package/dist/web/extension.d.ts.map +1 -0
- package/dist/web/feature.d.ts +8 -0
- package/dist/web/feature.d.ts.map +1 -0
- package/dist/web/features/asset-loader.d.ts +35 -0
- package/dist/web/features/asset-loader.d.ts.map +1 -0
- package/dist/web/features/container-link.d.ts +167 -0
- package/dist/web/features/container-link.d.ts.map +1 -0
- package/dist/web/features/esbuild.d.ts +51 -0
- package/dist/web/features/esbuild.d.ts.map +1 -0
- package/dist/web/features/helpers.d.ts +140 -0
- package/dist/web/features/helpers.d.ts.map +1 -0
- package/dist/web/features/network.d.ts +69 -0
- package/dist/web/features/network.d.ts.map +1 -0
- package/dist/web/features/speech.d.ts +71 -0
- package/dist/web/features/speech.d.ts.map +1 -0
- package/dist/web/features/vault.d.ts +62 -0
- package/dist/web/features/vault.d.ts.map +1 -0
- package/dist/web/features/vm.d.ts +48 -0
- package/dist/web/features/vm.d.ts.map +1 -0
- package/dist/web/features/voice-recognition.d.ts +96 -0
- package/dist/web/features/voice-recognition.d.ts.map +1 -0
- package/dist/web/shims/isomorphic-vm.d.ts +22 -0
- package/dist/web/shims/isomorphic-vm.d.ts.map +1 -0
- package/docs/CLI.md +335 -0
- package/docs/CNAME +1 -0
- package/docs/README.md +60 -0
- package/docs/TABLE-OF-CONTENTS.md +183 -0
- package/docs/apis/clients/elevenlabs.md +308 -0
- package/docs/apis/clients/graph.md +107 -0
- package/docs/apis/clients/openai.md +429 -0
- package/docs/apis/clients/rest.md +161 -0
- package/docs/apis/clients/websocket.md +174 -0
- package/docs/apis/features/agi/assistant.md +625 -0
- package/docs/apis/features/agi/assistants-manager.md +282 -0
- package/docs/apis/features/agi/auto-assistant.md +279 -0
- package/docs/apis/features/agi/browser-use.md +802 -0
- package/docs/apis/features/agi/claude-code.md +884 -0
- package/docs/apis/features/agi/conversation-history.md +364 -0
- package/docs/apis/features/agi/conversation.md +548 -0
- package/docs/apis/features/agi/docs-reader.md +99 -0
- package/docs/apis/features/agi/file-tools.md +163 -0
- package/docs/apis/features/agi/luca-coder.md +407 -0
- package/docs/apis/features/agi/openai-codex.md +396 -0
- package/docs/apis/features/agi/openapi.md +138 -0
- package/docs/apis/features/agi/semantic-search.md +387 -0
- package/docs/apis/features/agi/skills-library.md +239 -0
- package/docs/apis/features/node/container-link.md +192 -0
- package/docs/apis/features/node/content-db.md +450 -0
- package/docs/apis/features/node/disk-cache.md +379 -0
- package/docs/apis/features/node/dns.md +652 -0
- package/docs/apis/features/node/docker.md +706 -0
- package/docs/apis/features/node/downloader.md +81 -0
- package/docs/apis/features/node/esbuild.md +60 -0
- package/docs/apis/features/node/file-manager.md +191 -0
- package/docs/apis/features/node/fs.md +1217 -0
- package/docs/apis/features/node/git.md +371 -0
- package/docs/apis/features/node/google-auth.md +193 -0
- package/docs/apis/features/node/google-calendar.md +202 -0
- package/docs/apis/features/node/google-docs.md +173 -0
- package/docs/apis/features/node/google-drive.md +246 -0
- package/docs/apis/features/node/google-mail.md +214 -0
- package/docs/apis/features/node/google-sheets.md +194 -0
- package/docs/apis/features/node/grep.md +292 -0
- package/docs/apis/features/node/helpers.md +164 -0
- package/docs/apis/features/node/ink.md +334 -0
- package/docs/apis/features/node/ipc-socket.md +249 -0
- package/docs/apis/features/node/json-tree.md +86 -0
- package/docs/apis/features/node/networking.md +316 -0
- package/docs/apis/features/node/nlp.md +133 -0
- package/docs/apis/features/node/opener.md +97 -0
- package/docs/apis/features/node/os.md +146 -0
- package/docs/apis/features/node/package-finder.md +392 -0
- package/docs/apis/features/node/postgres.md +234 -0
- package/docs/apis/features/node/proc.md +399 -0
- package/docs/apis/features/node/process-manager.md +305 -0
- package/docs/apis/features/node/python.md +604 -0
- package/docs/apis/features/node/redis.md +380 -0
- package/docs/apis/features/node/repl.md +88 -0
- package/docs/apis/features/node/runpod.md +674 -0
- package/docs/apis/features/node/secure-shell.md +176 -0
- package/docs/apis/features/node/semantic-search.md +408 -0
- package/docs/apis/features/node/sqlite.md +233 -0
- package/docs/apis/features/node/telegram.md +279 -0
- package/docs/apis/features/node/transpiler.md +74 -0
- package/docs/apis/features/node/tts.md +133 -0
- package/docs/apis/features/node/ui.md +701 -0
- package/docs/apis/features/node/vault.md +59 -0
- package/docs/apis/features/node/vm.md +75 -0
- package/docs/apis/features/node/yaml-tree.md +85 -0
- package/docs/apis/features/node/yaml.md +176 -0
- package/docs/apis/features/web/asset-loader.md +59 -0
- package/docs/apis/features/web/container-link.md +192 -0
- package/docs/apis/features/web/esbuild.md +54 -0
- package/docs/apis/features/web/helpers.md +164 -0
- package/docs/apis/features/web/network.md +44 -0
- package/docs/apis/features/web/speech.md +69 -0
- package/docs/apis/features/web/vault.md +59 -0
- package/docs/apis/features/web/vm.md +75 -0
- package/docs/apis/features/web/voice.md +84 -0
- package/docs/apis/servers/express.md +171 -0
- package/docs/apis/servers/mcp.md +238 -0
- package/docs/apis/servers/websocket.md +170 -0
- package/docs/bootstrap/CLAUDE.md +101 -0
- package/docs/bootstrap/SKILL.md +341 -0
- package/docs/bootstrap/templates/about-command.ts +41 -0
- package/docs/bootstrap/templates/docs-models.ts +22 -0
- package/docs/bootstrap/templates/docs-readme.md +43 -0
- package/docs/bootstrap/templates/example-feature.ts +53 -0
- package/docs/bootstrap/templates/health-endpoint.ts +15 -0
- package/docs/bootstrap/templates/luca-cli.ts +30 -0
- package/docs/bootstrap/templates/runme.md +54 -0
- package/docs/challenges/caching-proxy.md +16 -0
- package/docs/challenges/content-db-round-trip.md +14 -0
- package/docs/challenges/custom-command.md +9 -0
- package/docs/challenges/file-watcher-pipeline.md +11 -0
- package/docs/challenges/grep-audit-report.md +15 -0
- package/docs/challenges/multi-feature-dashboard.md +14 -0
- package/docs/challenges/process-orchestrator.md +17 -0
- package/docs/challenges/rest-api-server-with-client.md +12 -0
- package/docs/challenges/script-runner-with-vm.md +11 -0
- package/docs/challenges/simple-rest-api.md +15 -0
- package/docs/challenges/websocket-serve-and-client.md +11 -0
- package/docs/challenges/yaml-config-system.md +14 -0
- package/docs/command-system-overhaul.md +94 -0
- package/docs/documentation-audit.md +134 -0
- package/docs/examples/assistant/CORE.md +18 -0
- package/docs/examples/assistant/hooks.ts +3 -0
- package/docs/examples/assistant/tools.ts +10 -0
- package/docs/examples/assistant-hooks-reference.ts +171 -0
- package/docs/examples/assistant-with-process-manager.md +84 -0
- package/docs/examples/content-db.md +77 -0
- package/docs/examples/disk-cache.md +83 -0
- package/docs/examples/docker.md +101 -0
- package/docs/examples/downloader.md +70 -0
- package/docs/examples/entity.md +124 -0
- package/docs/examples/esbuild.md +80 -0
- package/docs/examples/feature-as-tool-provider.md +143 -0
- package/docs/examples/file-manager.md +82 -0
- package/docs/examples/fs.md +83 -0
- package/docs/examples/git.md +85 -0
- package/docs/examples/google-auth.md +88 -0
- package/docs/examples/google-calendar.md +94 -0
- package/docs/examples/google-docs.md +82 -0
- package/docs/examples/google-drive.md +96 -0
- package/docs/examples/google-sheets.md +95 -0
- package/docs/examples/grep.md +85 -0
- package/docs/examples/ink-blocks.md +75 -0
- package/docs/examples/ink-renderer.md +41 -0
- package/docs/examples/ink.md +103 -0
- package/docs/examples/ipc-socket.md +103 -0
- package/docs/examples/json-tree.md +91 -0
- package/docs/examples/networking.md +58 -0
- package/docs/examples/nlp.md +91 -0
- package/docs/examples/opener.md +78 -0
- package/docs/examples/os.md +72 -0
- package/docs/examples/package-finder.md +89 -0
- package/docs/examples/postgres.md +91 -0
- package/docs/examples/proc.md +81 -0
- package/docs/examples/process-manager.md +79 -0
- package/docs/examples/python.md +132 -0
- package/docs/examples/repl.md +93 -0
- package/docs/examples/runpod.md +119 -0
- package/docs/examples/secure-shell.md +92 -0
- package/docs/examples/sqlite.md +86 -0
- package/docs/examples/structured-output-with-assistants.md +144 -0
- package/docs/examples/telegram.md +77 -0
- package/docs/examples/tts.md +86 -0
- package/docs/examples/ui.md +80 -0
- package/docs/examples/vault.md +70 -0
- package/docs/examples/vm.md +86 -0
- package/docs/examples/websocket-ask-and-reply-example.md +128 -0
- package/docs/examples/yaml-tree.md +93 -0
- package/docs/examples/yaml.md +104 -0
- package/docs/ideas/assistant-factory-pattern.md +142 -0
- package/docs/in-memory-fs.md +4 -0
- package/docs/introspection-audit.md +49 -0
- package/docs/introspection.md +164 -0
- package/docs/mcp/readme.md +162 -0
- package/docs/models.ts +41 -0
- package/docs/philosophy.md +86 -0
- package/docs/principles.md +7 -0
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
- package/docs/prompts/check-for-undocumented-features.md +27 -0
- package/docs/prompts/mcp-test-easy-command.md +27 -0
- package/docs/scaffolds/client.md +149 -0
- package/docs/scaffolds/command.md +120 -0
- package/docs/scaffolds/endpoint.md +171 -0
- package/docs/scaffolds/feature.md +158 -0
- package/docs/scaffolds/selector.md +91 -0
- package/docs/scaffolds/server.md +196 -0
- package/docs/selectors.md +115 -0
- package/docs/sessions/custom-command/attempt-log-2.md +195 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
- package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
- package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
- package/docs/tutorials/00-bootstrap.md +166 -0
- package/docs/tutorials/01-getting-started.md +106 -0
- package/docs/tutorials/02-container.md +210 -0
- package/docs/tutorials/03-scripts.md +194 -0
- package/docs/tutorials/04-features-overview.md +196 -0
- package/docs/tutorials/05-state-and-events.md +171 -0
- package/docs/tutorials/06-servers.md +157 -0
- package/docs/tutorials/07-endpoints.md +198 -0
- package/docs/tutorials/08-commands.md +252 -0
- package/docs/tutorials/09-clients.md +162 -0
- package/docs/tutorials/10-creating-features.md +203 -0
- package/docs/tutorials/11-contentbase.md +191 -0
- package/docs/tutorials/12-assistants.md +215 -0
- package/docs/tutorials/13-introspection.md +157 -0
- package/docs/tutorials/14-type-system.md +174 -0
- package/docs/tutorials/15-project-patterns.md +222 -0
- package/docs/tutorials/16-google-features.md +534 -0
- package/docs/tutorials/17-tui-blocks.md +530 -0
- package/docs/tutorials/18-semantic-search.md +334 -0
- package/docs/tutorials/19-python-sessions.md +401 -0
- package/docs/tutorials/20-browser-esm.md +234 -0
- package/index.html +1430 -0
- package/index.ts +1 -0
- package/install.sh +84 -0
- package/luca.cli.ts +16 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +219 -66
- package/public/index.html +1430 -0
- package/public/slides-ai-native.html +902 -0
- package/public/slides-intro.html +974 -0
- package/pyproject.toml +7 -0
- package/scripts/build-web.ts +28 -0
- package/scripts/examples/ask-luca-expert.ts +42 -0
- package/scripts/examples/assistant-questions.ts +12 -0
- package/scripts/examples/excalidraw-expert.ts +75 -0
- package/scripts/examples/expert-chat.ts +0 -0
- package/scripts/examples/file-manager.ts +14 -0
- package/scripts/examples/ideas.ts +12 -0
- package/scripts/examples/interactive-chat.ts +20 -0
- package/scripts/examples/openai-tool-calls.ts +113 -0
- package/scripts/examples/opening-a-web-browser.ts +5 -0
- package/scripts/examples/telegram-bot.ts +79 -0
- package/scripts/examples/using-assistant-with-mcp.ts +555 -0
- package/scripts/examples/using-claude-code.ts +10 -0
- package/scripts/examples/using-contentdb.ts +35 -0
- package/scripts/examples/using-conversations.ts +35 -0
- package/scripts/examples/using-disk-cache.ts +10 -0
- package/scripts/examples/using-docker-shell.ts +75 -0
- package/scripts/examples/using-elevenlabs.ts +25 -0
- package/scripts/examples/using-google-calendar.ts +57 -0
- package/scripts/examples/using-google-docs.ts +74 -0
- package/scripts/examples/using-google-drive.ts +74 -0
- package/scripts/examples/using-google-sheets.ts +89 -0
- package/scripts/examples/using-nlp.ts +55 -0
- package/scripts/examples/using-ollama.ts +11 -0
- package/scripts/examples/using-postgres.ts +55 -0
- package/scripts/examples/using-runpod.ts +32 -0
- package/scripts/examples/using-tts.ts +40 -0
- package/scripts/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/stamp-build.sh +12 -0
- package/scripts/test-assistant-hooks.ts +13 -0
- package/scripts/test-docs-reader.ts +10 -0
- package/scripts/test-linux-binary.sh +80 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +152 -0
- package/src/agi/endpoints/ask.ts +60 -0
- package/src/agi/endpoints/conversations/[id].ts +45 -0
- package/src/agi/endpoints/conversations.ts +31 -0
- package/src/agi/endpoints/experts.ts +37 -0
- package/src/agi/feature.ts +13 -0
- package/src/agi/features/agent-memory.ts +694 -0
- package/src/agi/features/assistant.ts +1624 -0
- package/src/agi/features/assistants-manager.ts +418 -0
- package/src/agi/features/autonomous-assistant.ts +431 -0
- package/src/agi/features/browser-use.ts +653 -0
- package/src/agi/features/claude-code.ts +1538 -0
- package/src/agi/features/coding-tools.ts +175 -0
- package/src/agi/features/conversation-history.ts +495 -0
- package/src/agi/features/conversation.ts +1323 -0
- package/src/agi/features/docs-reader.ts +167 -0
- package/src/agi/features/file-tools.ts +293 -0
- package/src/agi/features/luca-coder.ts +639 -0
- package/src/agi/features/openai-codex.ts +651 -0
- package/src/agi/features/openapi.ts +445 -0
- package/src/agi/features/skills-library.ts +478 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/interceptor-chain.ts +89 -0
- package/src/agi/lib/token-counter.ts +122 -0
- package/src/bootstrap/generated.ts +9792 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +122 -0
- package/src/cli/build-info.ts +4 -0
- package/src/cli/cli.ts +355 -0
- package/src/client.ts +170 -0
- package/src/clients/civitai/index.ts +537 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +604 -0
- package/src/clients/elevenlabs/index.ts +317 -0
- package/src/clients/graph.ts +87 -0
- package/src/clients/openai/index.ts +456 -0
- package/src/clients/rest.ts +207 -0
- package/src/clients/supabase/index.ts +357 -0
- package/src/clients/voicebox/index.ts +300 -0
- package/src/clients/websocket.ts +251 -0
- package/src/command.ts +505 -0
- package/src/commands/bootstrap.ts +244 -0
- package/src/commands/chat.ts +308 -0
- package/src/commands/code.ts +371 -0
- package/src/commands/console.ts +189 -0
- package/src/commands/describe.ts +243 -0
- package/src/commands/eval.ts +121 -0
- package/src/commands/help.ts +240 -0
- package/src/commands/index.ts +19 -0
- package/src/commands/introspect.ts +218 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +982 -0
- package/src/commands/run.ts +278 -0
- package/src/commands/sandbox-mcp.ts +343 -0
- package/src/commands/save-api-docs.ts +51 -0
- package/src/commands/scaffold.ts +225 -0
- package/src/commands/select.ts +99 -0
- package/src/commands/serve.ts +208 -0
- package/src/container-describer.ts +1084 -0
- package/src/container.ts +1186 -0
- package/src/endpoint.ts +365 -0
- package/src/entity.ts +173 -0
- package/src/feature.ts +118 -0
- package/src/graft.ts +181 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +849 -0
- package/src/introspection/generated.agi.ts +40208 -0
- package/src/introspection/generated.node.ts +28686 -0
- package/src/introspection/generated.web.ts +2251 -0
- package/src/introspection/index.ts +296 -0
- package/src/introspection/scan.ts +1131 -0
- package/src/node/container.ts +409 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +559 -0
- package/src/node/features/content-db.ts +812 -0
- package/src/node/features/disk-cache.ts +388 -0
- package/src/node/features/dns.ts +669 -0
- package/src/node/features/docker.ts +921 -0
- package/src/node/features/downloader.ts +79 -0
- package/src/node/features/figlet-fonts.ts +600 -0
- package/src/node/features/file-manager.ts +535 -0
- package/src/node/features/fs.ts +1050 -0
- package/src/node/features/git.ts +592 -0
- package/src/node/features/google-auth.ts +504 -0
- package/src/node/features/google-calendar.ts +306 -0
- package/src/node/features/google-docs.ts +412 -0
- package/src/node/features/google-drive.ts +346 -0
- package/src/node/features/google-mail.ts +540 -0
- package/src/node/features/google-sheets.ts +286 -0
- package/src/node/features/grep.ts +427 -0
- package/src/node/features/helpers.ts +735 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +649 -0
- package/src/node/features/json-tree.ts +170 -0
- package/src/node/features/networking.ts +961 -0
- package/src/node/features/nlp.ts +212 -0
- package/src/node/features/opener.ts +180 -0
- package/src/node/features/os.ts +403 -0
- package/src/node/features/package-finder.ts +540 -0
- package/src/node/features/postgres.ts +289 -0
- package/src/node/features/proc.ts +503 -0
- package/src/node/features/process-manager.ts +844 -0
- package/src/node/features/python.ts +906 -0
- package/src/node/features/redis.ts +446 -0
- package/src/node/features/repl.ts +212 -0
- package/src/node/features/runpod.ts +811 -0
- package/src/node/features/secure-shell.ts +267 -0
- package/src/node/features/semantic-search.ts +935 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +343 -0
- package/src/node/features/transpiler.ts +161 -0
- package/src/node/features/tts.ts +185 -0
- package/src/node/features/ui.ts +786 -0
- package/src/node/features/vault.ts +153 -0
- package/src/node/features/vm.ts +462 -0
- package/src/node/features/yaml-tree.ts +148 -0
- package/src/node/features/yaml.ts +133 -0
- package/src/node.ts +76 -0
- package/src/python/bridge.py +220 -0
- package/src/python/generated.ts +227 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +210 -0
- package/src/scaffolds/generated.ts +1815 -0
- package/src/scaffolds/template.ts +46 -0
- package/src/schemas/base.ts +296 -0
- package/src/selector.ts +352 -0
- package/src/server.ts +229 -0
- package/src/servers/express.ts +283 -0
- package/src/servers/mcp.ts +802 -0
- package/src/servers/socket.ts +258 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +99 -0
- package/src/web/container.ts +75 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +72 -0
- package/src/web/features/container-link.ts +382 -0
- package/src/web/features/esbuild.ts +93 -0
- package/src/web/features/helpers.ts +269 -0
- package/src/web/features/network.ts +85 -0
- package/src/web/features/speech.ts +104 -0
- package/src/web/features/vault.ts +207 -0
- package/src/web/features/vm.ts +85 -0
- package/src/web/features/voice-recognition.ts +161 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/test/assistant-hooks.test.ts +306 -0
- package/test/assistant.test.ts +81 -0
- package/test/bus.test.ts +134 -0
- package/test/clients-servers.test.ts +217 -0
- package/test/command.test.ts +267 -0
- package/test/container-link.test.ts +274 -0
- package/test/conversation.test.ts +220 -0
- package/test/features.test.ts +160 -0
- package/test/fork-and-research.test.ts +450 -0
- package/test/integration.test.ts +787 -0
- package/test/interceptor-chain.test.ts +61 -0
- package/test/node-container.test.ts +121 -0
- package/test/python-session.test.ts +105 -0
- package/test/rate-limit.test.ts +272 -0
- package/test/semantic-search.test.ts +550 -0
- package/test/state.test.ts +121 -0
- package/test/vm-context.test.ts +146 -0
- package/test/vm-loadmodule.test.ts +213 -0
- package/test/websocket-ask.test.ts +101 -0
- package/test-integration/assistant.test.ts +138 -0
- package/test-integration/assistants-manager.test.ts +113 -0
- package/test-integration/claude-code.test.ts +98 -0
- package/test-integration/conversation-history.test.ts +205 -0
- package/test-integration/conversation.test.ts +137 -0
- package/test-integration/elevenlabs.test.ts +55 -0
- package/test-integration/google-services.test.ts +80 -0
- package/test-integration/helpers.ts +89 -0
- package/test-integration/memory.test.ts +204 -0
- package/test-integration/openai-codex.test.ts +93 -0
- package/test-integration/runpod.test.ts +58 -0
- package/test-integration/server-endpoints.test.ts +97 -0
- package/test-integration/telegram.test.ts +46 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +58 -0
- package/uv.lock +8 -0
- package/LICENSE +0 -21
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -5
- package/dist/cli/run.d.ts +0 -12
- package/dist/cli/run.js +0 -42
- package/dist/config/consts.d.ts +0 -2
- package/dist/config/consts.js +0 -29
- package/dist/config/default.d.ts +0 -8
- package/dist/config/default.js +0 -15
- package/dist/config/initConfig.d.ts +0 -1
- package/dist/config/initConfig.js +0 -52
- package/dist/config/openConfig.d.ts +0 -2
- package/dist/config/openConfig.js +0 -24
- package/dist/config/runConfig.d.ts +0 -3
- package/dist/config/runConfig.js +0 -117
- package/dist/config/types.d.ts +0 -13
- package/dist/config/types.js +0 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -5
- package/dist/utils/common.d.ts +0 -2
- package/dist/utils/common.js +0 -52
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.js +0 -17
|
@@ -0,0 +1,1624 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
+
import { type AvailableFeatures } from '@soederpop/luca/feature'
|
|
4
|
+
import { Feature } from '../feature.js'
|
|
5
|
+
import type { Conversation, ConversationTool, ContentPart, AskOptions, ForkOptions, Message } from './conversation'
|
|
6
|
+
import type { ContentDb } from '@soederpop/luca/node'
|
|
7
|
+
import type { ConversationHistory, ConversationMeta } from './conversation-history'
|
|
8
|
+
import hashObject from '../../hash-object.js'
|
|
9
|
+
import { InterceptorChain, type InterceptorFn, type InterceptorPoints, type InterceptorPoint } from '../lib/interceptor-chain.js'
|
|
10
|
+
import type { Entity } from '../../entity.js'
|
|
11
|
+
import { State } from '../../state.js'
|
|
12
|
+
|
|
13
|
+
declare module '@soederpop/luca/feature' {
|
|
14
|
+
interface AvailableFeatures {
|
|
15
|
+
assistant: typeof Assistant
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const AssistantEventsSchema = FeatureEventsSchema.extend({
|
|
20
|
+
created: z.tuple([]).describe('Emitted immediately after the assistant loads its prompt, tools, and hooks.'),
|
|
21
|
+
started: z.tuple([]).describe('Emitted when the assistant has been initialized'),
|
|
22
|
+
turnStart: z.tuple([z.object({ turn: z.number(), isFollowUp: z.boolean() })]).describe('Emitted when a new completion turn begins. isFollowUp is true when resuming after tool calls'),
|
|
23
|
+
turnEnd: z.tuple([z.object({ turn: z.number(), hasToolCalls: z.boolean() })]).describe('Emitted when a completion turn ends. hasToolCalls indicates whether tool calls will follow'),
|
|
24
|
+
chunk: z.tuple([z.string().describe('A chunk of streamed text')]).describe('Emitted as tokens stream in'),
|
|
25
|
+
preview: z.tuple([z.string().describe('The accumulated response so far')]).describe('Emitted with the full response text accumulated across all turns'),
|
|
26
|
+
response: z.tuple([z.string().describe('The final response text')]).describe('Emitted when a complete response is produced (accumulated across all turns)'),
|
|
27
|
+
rawEvent: z.tuple([z.any().describe('A raw streaming event from the active model API')]).describe('Emitted for each raw streaming event from the underlying conversation transport'),
|
|
28
|
+
mcpEvent: z.tuple([z.any().describe('A raw MCP-related streaming event')]).describe('Emitted for MCP-specific streaming and output-item events when using Responses API MCP tools'),
|
|
29
|
+
toolCall: z.tuple([z.string().describe('Tool name'), z.any().describe('Tool arguments')]).describe('Emitted when a tool is called'),
|
|
30
|
+
toolResult: z.tuple([z.string().describe('Tool name'), z.any().describe('Result value')]).describe('Emitted when a tool returns a result'),
|
|
31
|
+
toolError: z.tuple([z.string().describe('Tool name'), z.any().describe('Error')]).describe('Emitted when a tool call fails'),
|
|
32
|
+
hookFired: z.tuple([z.string().describe('Hook/event name')]).describe('Emitted when a hook function is called'),
|
|
33
|
+
reloaded: z.tuple([]).describe('Emitted after tools, hooks, and system prompt are reloaded from disk'),
|
|
34
|
+
systemPromptExtensionsChanged: z.tuple([]).describe('Emitted when system prompt extensions are added or removed'),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const AssistantStateSchema = FeatureStateSchema.extend({
|
|
38
|
+
started: z.boolean().describe('Whether the assistant has been initialized'),
|
|
39
|
+
conversationCount: z.number().describe('Number of ask() calls made'),
|
|
40
|
+
lastResponse: z.string().describe('The most recent response text'),
|
|
41
|
+
folder: z.string().describe('The resolved assistant folder path'),
|
|
42
|
+
docsFolder: z.string().describe('The resolved docs folder'),
|
|
43
|
+
conversationId: z.string().optional().describe('The active conversation persistence ID'),
|
|
44
|
+
threadId: z.string().optional().describe('The active thread ID'),
|
|
45
|
+
systemPrompt: z.string().describe('The loaded system prompt text'),
|
|
46
|
+
systemPromptExtensions: z.record(z.string(), z.string()).describe('Named extensions appended to the system prompt'),
|
|
47
|
+
meta: z.record(z.string(), z.any()).describe('Parsed YAML frontmatter from CORE.md'),
|
|
48
|
+
tools: z.record(z.string(), z.any()).describe('Registered tool implementations'),
|
|
49
|
+
hooks: z.record(z.string(), z.any()).describe('Loaded event hook functions'),
|
|
50
|
+
resumeThreadId: z.string().optional().describe('Thread ID override for resume'),
|
|
51
|
+
pendingPlugins: z.array(z.any()).describe('Pending async plugin promises'),
|
|
52
|
+
conversation: z.any().nullable().describe('The active Conversation feature instance'),
|
|
53
|
+
subagents: z.record(z.string(), z.any()).describe('Cached subagent instances'),
|
|
54
|
+
forkDepth: z.number().describe('How many times this assistant has been forked from an ancestor. 0 = original.'),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
export const AssistantOptionsSchema = FeatureOptionsSchema.extend({
|
|
58
|
+
/** The folder containing the assistant definition (CORE.md, tools.ts, hooks.ts). Optional for runtime-created assistants. */
|
|
59
|
+
folder: z.string().default('.').describe('The folder containing the assistant definition. Defaults to cwd for runtime-created assistants.'),
|
|
60
|
+
|
|
61
|
+
/** If the docs folder is different from folder/docs */
|
|
62
|
+
docsFolder: z.string().optional().describe('The folder containing the assistant documentation'),
|
|
63
|
+
|
|
64
|
+
/** Provide a complete system prompt directly, bypassing CORE.md. Useful for runtime-created assistants. */
|
|
65
|
+
systemPrompt: z.string().optional().describe('Provide a complete system prompt directly, bypassing CORE.md'),
|
|
66
|
+
|
|
67
|
+
/** Text to prepend to the system prompt from CORE.md */
|
|
68
|
+
prependPrompt: z.string().optional().describe('Text to prepend to the system prompt'),
|
|
69
|
+
|
|
70
|
+
/** Text to append to the system prompt from CORE.md */
|
|
71
|
+
appendPrompt: z.string().optional().describe('Text to append to the system prompt'),
|
|
72
|
+
/** Override or extend the tools loaded from tools.ts */
|
|
73
|
+
|
|
74
|
+
tools: z.record(z.string(), z.any()).optional().describe('Override or extend the tools loaded from tools.ts'),
|
|
75
|
+
/** Override or extend the schemas loaded from tools.ts */
|
|
76
|
+
|
|
77
|
+
schemas: z.record(z.string(), z.any()).optional().describe('Override or extend schemas whose keys match tool names'),
|
|
78
|
+
/** OpenAI model to use for the conversation */
|
|
79
|
+
|
|
80
|
+
model: z.string().optional().describe('OpenAI model to use'),
|
|
81
|
+
/** Maximum number of output tokens per completion */
|
|
82
|
+
|
|
83
|
+
maxTokens: z.number().optional().describe('Maximum number of output tokens per completion'),
|
|
84
|
+
/** Sampling temperature (0-2). Higher = more random, lower = more deterministic. */
|
|
85
|
+
temperature: z.number().min(0).max(2).optional().describe('Sampling temperature (0-2)'),
|
|
86
|
+
/** Nucleus sampling cutoff (0-1). */
|
|
87
|
+
topP: z.number().min(0).max(1).optional().describe('Nucleus sampling cutoff (0-1)'),
|
|
88
|
+
/** Top-K sampling. Only supported by local/Anthropic models. */
|
|
89
|
+
topK: z.number().optional().describe('Top-K sampling. Only supported by local/Anthropic models'),
|
|
90
|
+
/** Frequency penalty (-2 to 2). */
|
|
91
|
+
frequencyPenalty: z.number().min(-2).max(2).optional().describe('Frequency penalty (-2 to 2)'),
|
|
92
|
+
/** Presence penalty (-2 to 2). */
|
|
93
|
+
presencePenalty: z.number().min(-2).max(2).optional().describe('Presence penalty (-2 to 2)'),
|
|
94
|
+
/** Stop sequences. */
|
|
95
|
+
stop: z.array(z.string()).optional().describe('Stop sequences'),
|
|
96
|
+
|
|
97
|
+
local: z.boolean().default(false).describe('Whether to use our local models for this'),
|
|
98
|
+
|
|
99
|
+
/** History persistence mode: lifecycle (ephemeral), daily (auto-resume per day), persistent (single long-running thread), session (unique per run, resumable) */
|
|
100
|
+
historyMode: z.enum(['lifecycle', 'daily', 'persistent', 'session']).optional().describe('Conversation history persistence mode'),
|
|
101
|
+
|
|
102
|
+
/** When true, prepend a timestamp to each user message so the assistant can track the passage of time across sessions */
|
|
103
|
+
injectTimestamps: z.boolean().default(false).describe('Prepend timestamps to user messages so the assistant can perceive time passing between sessions'),
|
|
104
|
+
|
|
105
|
+
/** Strict allowlist of tool names to include. Only these tools will be available. Supports "*" glob matching. */
|
|
106
|
+
allowTools: z.array(z.string()).optional().describe('Strict allowlist of tool name patterns. Only matching tools are available. Supports * glob matching.'),
|
|
107
|
+
|
|
108
|
+
/** Denylist of tool names to exclude. Matching tools will be removed. Supports "*" glob matching. */
|
|
109
|
+
forbidTools: z.array(z.string()).optional().describe('Denylist of tool name patterns to exclude. Supports * glob matching.'),
|
|
110
|
+
|
|
111
|
+
/** Convenience alias for allowTools — an explicit list of tool names (exact matches only). */
|
|
112
|
+
toolNames: z.array(z.string()).optional().describe('Explicit list of tool names to include (exact match). Shorthand for allowTools without glob patterns.'),
|
|
113
|
+
|
|
114
|
+
/** Options passed through to the underlying OpenAI client (e.g. baseURL, apiKey). */
|
|
115
|
+
clientOptions: z.record(z.string(), z.any()).optional().describe('Options for the OpenAI client, passed through to the conversation'),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
export type AssistantState = z.infer<typeof AssistantStateSchema>
|
|
119
|
+
export type AssistantOptions = z.infer<typeof AssistantOptionsSchema>
|
|
120
|
+
|
|
121
|
+
/** Fork options extended with assistant-specific tool filtering and lifecycle hooks. */
|
|
122
|
+
export type AssistantForkOptions = ForkOptions & {
|
|
123
|
+
/** Denylist of tool name patterns to exclude from the fork. Supports "*" glob matching. */
|
|
124
|
+
forbidTools?: string[]
|
|
125
|
+
/** Strict allowlist of tool name patterns for the fork. Supports "*" glob matching. */
|
|
126
|
+
allowTools?: string[]
|
|
127
|
+
/** Explicit list of tool names to include in the fork (exact match). */
|
|
128
|
+
toolNames?: string[]
|
|
129
|
+
/**
|
|
130
|
+
* Called with the forked assistant after it has been fully initialized (started, interceptors cloned,
|
|
131
|
+
* system prompt extensions copied, forkDepth set). Use this to add/remove tools, tweak state,
|
|
132
|
+
* inject system prompt extensions, or anything else before the fork is used.
|
|
133
|
+
*/
|
|
134
|
+
onFork?: (fork: Assistant, parent: Assistant) => void | Promise<void>
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ResearchJobState {
|
|
138
|
+
status: 'running' | 'completed' | 'failed'
|
|
139
|
+
prompt: string
|
|
140
|
+
questions: string[]
|
|
141
|
+
results: (string | null)[]
|
|
142
|
+
errors: (string | null)[]
|
|
143
|
+
completed: number
|
|
144
|
+
total: number
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ResearchJobOptions {
|
|
148
|
+
prompt: string
|
|
149
|
+
questions: string[]
|
|
150
|
+
forkOptions: AssistantForkOptions
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type ResearchJobEvents = {
|
|
154
|
+
forkCompleted: [number, string]
|
|
155
|
+
forkError: [number, string]
|
|
156
|
+
completed: [string[]]
|
|
157
|
+
failed: [(string | null)[]]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type ResearchJob = Entity<ResearchJobState, ResearchJobOptions, ResearchJobEvents>
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* An Assistant is a combination of a system prompt and tool calls that has a
|
|
164
|
+
* conversation with an LLM. You define an assistant by creating a folder with
|
|
165
|
+
* CORE.md (system prompt), tools.ts (tool implementations), and hooks.ts (event handlers).
|
|
166
|
+
*
|
|
167
|
+
* @extends Feature
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```typescript
|
|
171
|
+
* const assistant = container.feature('assistant', {
|
|
172
|
+
* folder: 'assistants/my-helper'
|
|
173
|
+
* })
|
|
174
|
+
* const answer = await assistant.ask('What capabilities do you have?')
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
178
|
+
static override stateSchema = AssistantStateSchema
|
|
179
|
+
static override optionsSchema = AssistantOptionsSchema
|
|
180
|
+
static override eventsSchema = AssistantEventsSchema
|
|
181
|
+
static override shortcut = 'features.assistant' as const
|
|
182
|
+
|
|
183
|
+
static { Feature.register(this, 'assistant') }
|
|
184
|
+
|
|
185
|
+
readonly interceptors = {
|
|
186
|
+
beforeAsk: new InterceptorChain<InterceptorPoints['beforeAsk']>(),
|
|
187
|
+
beforeTurn: new InterceptorChain<InterceptorPoints['beforeTurn']>(),
|
|
188
|
+
beforeToolCall: new InterceptorChain<InterceptorPoints['beforeToolCall']>(),
|
|
189
|
+
afterToolCall: new InterceptorChain<InterceptorPoints['afterToolCall']>(),
|
|
190
|
+
beforeResponse: new InterceptorChain<InterceptorPoints['beforeResponse']>(),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Extension point for plugins, setupToolsConsumer, and hooks to attach
|
|
195
|
+
* arbitrary methods to the assistant instance (e.g. voice-mode adding
|
|
196
|
+
* mute/unmute). Access via `assistant.ext.myMethod()`.
|
|
197
|
+
*/
|
|
198
|
+
readonly ext: Record<string, (...args: any[]) => any> = {}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Observable runtime state that the assistant can manipulate freely via
|
|
202
|
+
* tool calls, hooks, or extensions. Unlike the feature's own `state`
|
|
203
|
+
* (which tracks internal lifecycle), mentalState is a blank slate for
|
|
204
|
+
* the assistant's own use — tracking mood, goals, context, preferences,
|
|
205
|
+
* or anything else. Fully observable so UI or other systems can react.
|
|
206
|
+
*/
|
|
207
|
+
readonly mentalState = new State<Record<string, any>>()
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Register an interceptor at a given point in the pipeline.
|
|
211
|
+
*
|
|
212
|
+
* @param point - The interception point
|
|
213
|
+
* @param fn - Middleware function receiving (ctx, next)
|
|
214
|
+
* @returns this, for chaining
|
|
215
|
+
*/
|
|
216
|
+
intercept<K extends InterceptorPoint>(point: K, fn: InterceptorFn<InterceptorPoints[K]>): this {
|
|
217
|
+
if (!(point in this.interceptors)) {
|
|
218
|
+
const available = Object.keys(this.interceptors).join(', ')
|
|
219
|
+
throw new Error(`Unknown intercept point "${point}". Available points: ${available}`)
|
|
220
|
+
}
|
|
221
|
+
this.interceptors[point].add(fn as any)
|
|
222
|
+
return this
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Trigger a named hook and await its completion. The hook function receives
|
|
227
|
+
* `(assistant, ...args)` and its return value is passed back to the caller.
|
|
228
|
+
* This ensures hooks run to completion BEFORE any subsequent logic executes,
|
|
229
|
+
* unlike the old bus-based approach where async hooks were fire-and-forget.
|
|
230
|
+
*
|
|
231
|
+
* Hooks that don't exist are silently skipped (returns undefined).
|
|
232
|
+
*
|
|
233
|
+
* @param hookName - The hook to trigger (matches an export name from hooks.ts)
|
|
234
|
+
* @param args - Arguments passed to the hook after the assistant instance
|
|
235
|
+
* @returns The hook's return value, or undefined if no hook exists
|
|
236
|
+
*/
|
|
237
|
+
async triggerHook(hookName: string, ...args: any[]): Promise<any> {
|
|
238
|
+
const hooks = (this.state.get('hooks') || {}) as Record<string, (...args: any[]) => any>
|
|
239
|
+
const hookFn = hooks[hookName]
|
|
240
|
+
if (!hookFn) return undefined
|
|
241
|
+
this.emit('hookFired', hookName)
|
|
242
|
+
return await hookFn(this, ...args)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** @returns Default state with the assistant not started, zero conversations, and the resolved folder path. */
|
|
246
|
+
override get initialState(): AssistantState {
|
|
247
|
+
return {
|
|
248
|
+
...super.initialState,
|
|
249
|
+
started: false,
|
|
250
|
+
conversationCount: 0,
|
|
251
|
+
lastResponse: '',
|
|
252
|
+
folder: this.resolvedFolder,
|
|
253
|
+
systemPrompt: '',
|
|
254
|
+
systemPromptExtensions: {},
|
|
255
|
+
meta: {},
|
|
256
|
+
tools: {},
|
|
257
|
+
hooks: {},
|
|
258
|
+
resumeThreadId: undefined,
|
|
259
|
+
pendingPlugins: [],
|
|
260
|
+
conversation: null,
|
|
261
|
+
subagents: {},
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
get name() {
|
|
267
|
+
return this.options.name || this.resolvedFolder.split('/').pop()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The absolute resolved path to the assistant folder. */
|
|
271
|
+
get resolvedFolder(): string {
|
|
272
|
+
return this.container.paths.resolve(this.options.folder)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The path to CORE.md which provides the system prompt. */
|
|
276
|
+
get corePromptPath(): string {
|
|
277
|
+
return this.paths.resolve('CORE.md')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The path to tools.ts which provides tool implementations and schemas. */
|
|
281
|
+
get toolsModulePath(): string {
|
|
282
|
+
return this.paths.resolve('tools.ts')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The path to hooks.ts which provides event handler functions. */
|
|
286
|
+
get hooksModulePath(): string {
|
|
287
|
+
return this.paths.resolve('hooks.ts')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Whether this assistant has a voice.yaml configuration file. */
|
|
291
|
+
get hasVoice(): boolean {
|
|
292
|
+
return this.container.fs.exists(this.paths.resolve('voice.yaml'))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Parsed voice configuration from voice.yaml, or undefined if not present. */
|
|
296
|
+
get voiceConfig(): Record<string, any> | undefined {
|
|
297
|
+
if (!this.hasVoice) return undefined
|
|
298
|
+
const yaml = this.container.feature('yaml')
|
|
299
|
+
return yaml.parse(String(this.container.fs.readFile(this.paths.resolve('voice.yaml'))))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
get resolvedDocsFolder() {
|
|
303
|
+
const { docsFolder = this.effectiveOptions.docsFolder || 'docs' } = this.state.current
|
|
304
|
+
|
|
305
|
+
if (this.container.fs.exists(docsFolder)) {
|
|
306
|
+
return this.container.paths.resolve(docsFolder)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const findUp = this.container.fs.findUp('docs', {
|
|
310
|
+
cwd: this.resolvedFolder
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
if (typeof findUp === 'string' && this.container.fs.exists(findUp!)) {
|
|
314
|
+
this.state.set('docsFolder', findUp!)
|
|
315
|
+
return this.container.paths.resolve(findUp!)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return this.paths.resolve('docs')
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Returns an instance of a ContentDb feature for the resolved docs folder
|
|
323
|
+
*/
|
|
324
|
+
get contentDb() : ContentDb {
|
|
325
|
+
return this.container.feature('contentDb', { rootPath: this.resolvedDocsFolder })
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Called immediately after the assistant is constructed. Synchronously loads
|
|
331
|
+
* the system prompt, tools, and hooks. Hooks are invoked via triggerHook()
|
|
332
|
+
* at each emit site, ensuring async hooks are properly awaited.
|
|
333
|
+
*/
|
|
334
|
+
override afterInitialize() {
|
|
335
|
+
this.state.set('pendingPlugins', [])
|
|
336
|
+
|
|
337
|
+
// Load system prompt synchronously
|
|
338
|
+
this.state.set('systemPrompt', this.loadSystemPrompt())
|
|
339
|
+
|
|
340
|
+
// Load tools and hooks synchronously via vm.performSync
|
|
341
|
+
this.state.set('tools', this.loadTools())
|
|
342
|
+
this.state.set('hooks', this.loadHooks())
|
|
343
|
+
|
|
344
|
+
// Defer created hook+event so external listeners can register first
|
|
345
|
+
setTimeout(async () => {
|
|
346
|
+
await this.triggerHook('created')
|
|
347
|
+
this.emit('created')
|
|
348
|
+
}, 1)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
get conversation(): Conversation {
|
|
352
|
+
let conv = this.state.get('conversation') as Conversation | null
|
|
353
|
+
if (!conv) {
|
|
354
|
+
conv = this.container.feature('conversation', {
|
|
355
|
+
model: this.effectiveOptions.model || 'gpt-5.4',
|
|
356
|
+
local: !!this.effectiveOptions.local,
|
|
357
|
+
tools: this.tools,
|
|
358
|
+
api: 'chat',
|
|
359
|
+
...(this.effectiveOptions.maxTokens ? { maxTokens: this.effectiveOptions.maxTokens } : {}),
|
|
360
|
+
...(this.effectiveOptions.temperature != null ? { temperature: this.effectiveOptions.temperature } : {}),
|
|
361
|
+
...(this.effectiveOptions.topP != null ? { topP: this.effectiveOptions.topP } : {}),
|
|
362
|
+
...(this.effectiveOptions.topK != null ? { topK: this.effectiveOptions.topK } : {}),
|
|
363
|
+
...(this.effectiveOptions.frequencyPenalty != null ? { frequencyPenalty: this.effectiveOptions.frequencyPenalty } : {}),
|
|
364
|
+
...(this.effectiveOptions.presencePenalty != null ? { presencePenalty: this.effectiveOptions.presencePenalty } : {}),
|
|
365
|
+
...(this.effectiveOptions.stop ? { stop: this.effectiveOptions.stop } : {}),
|
|
366
|
+
...(this.effectiveOptions.clientOptions ? { clientOptions: this.effectiveOptions.clientOptions } : {}),
|
|
367
|
+
history: [
|
|
368
|
+
{ role: 'system', content: this.effectiveSystemPrompt },
|
|
369
|
+
],
|
|
370
|
+
})
|
|
371
|
+
this.state.set('conversation', conv)
|
|
372
|
+
}
|
|
373
|
+
return conv
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
get availableTools() {
|
|
377
|
+
return Object.keys(this.tools)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
get messages() {
|
|
381
|
+
return this.conversation.messages
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Whether the assistant has been started and is ready to receive questions. */
|
|
385
|
+
get isStarted(): boolean {
|
|
386
|
+
return !!this.state.get('started')
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Whether this assistant was created via fork(). */
|
|
390
|
+
get isFork(): boolean {
|
|
391
|
+
return (this.state.get('forkDepth') ?? 0) > 0
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** How many levels deep this fork is. 0 = original, 1 = direct fork, 2 = fork of a fork, etc. */
|
|
395
|
+
get forkDepth(): number {
|
|
396
|
+
return (this.state.get('forkDepth') as number) ?? 0
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** The current system prompt text. */
|
|
400
|
+
get systemPrompt(): string {
|
|
401
|
+
return this.state.get('systemPrompt') || ''
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The named extensions appended to the system prompt. */
|
|
405
|
+
get systemPromptExtensions(): Record<string, string> {
|
|
406
|
+
return (this.state.get('systemPromptExtensions') || {}) as Record<string, string>
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** The system prompt with all extensions appended. This is the value passed to the conversation. */
|
|
410
|
+
get effectiveSystemPrompt(): string {
|
|
411
|
+
const base = this.systemPrompt
|
|
412
|
+
const extensions = Object.values(this.systemPromptExtensions)
|
|
413
|
+
if (!extensions.length) return base
|
|
414
|
+
return [base, ...extensions].join('\n\n')
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Add or update a named system prompt extension. The value is appended
|
|
419
|
+
* to the base system prompt when passed to the conversation.
|
|
420
|
+
*
|
|
421
|
+
* @param key - A unique identifier for this extension
|
|
422
|
+
* @param value - The text to append
|
|
423
|
+
* @returns this, for chaining
|
|
424
|
+
*/
|
|
425
|
+
addSystemPromptExtension(key: string, value: string): this {
|
|
426
|
+
this.state.set('systemPromptExtensions', { ...this.systemPromptExtensions, [key]: value })
|
|
427
|
+
this.syncSystemPromptToConversation()
|
|
428
|
+
this.emit('systemPromptExtensionsChanged')
|
|
429
|
+
return this
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Remove a named system prompt extension.
|
|
434
|
+
*
|
|
435
|
+
* @param key - The identifier of the extension to remove
|
|
436
|
+
* @returns this, for chaining
|
|
437
|
+
*/
|
|
438
|
+
removeSystemPromptExtension(key: string): this {
|
|
439
|
+
const current = { ...this.systemPromptExtensions }
|
|
440
|
+
delete current[key]
|
|
441
|
+
this.state.set('systemPromptExtensions', current)
|
|
442
|
+
this.syncSystemPromptToConversation()
|
|
443
|
+
this.emit('systemPromptExtensionsChanged')
|
|
444
|
+
return this
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Update the conversation's system message to reflect the current effective prompt. */
|
|
448
|
+
private syncSystemPromptToConversation() {
|
|
449
|
+
const conv = this.state.get('conversation') as Conversation | null
|
|
450
|
+
if (!conv) return
|
|
451
|
+
const messages = [...conv.messages]
|
|
452
|
+
if (messages.length > 0 && (messages[0]!.role === 'system' || messages[0]!.role === 'developer')) {
|
|
453
|
+
messages[0] = { ...messages[0]!, content: this.effectiveSystemPrompt }
|
|
454
|
+
conv.state.set('messages', messages)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** The tools registered with this assistant. */
|
|
459
|
+
get tools(): Record<string, ConversationTool> {
|
|
460
|
+
const all = (this.state.get('tools') || {}) as Record<string, ConversationTool>
|
|
461
|
+
return this.applyToolFilters(all)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Apply allowTools, forbidTools, and toolNames filters from options.
|
|
466
|
+
* toolNames is treated as an exact-match allowlist. allowTools/forbidTools support "*" glob patterns.
|
|
467
|
+
* allowTools is applied first (strict allowlist), then forbidTools removes from whatever remains.
|
|
468
|
+
*/
|
|
469
|
+
private applyToolFilters(tools: Record<string, ConversationTool>): Record<string, ConversationTool> {
|
|
470
|
+
const { allowTools, forbidTools, toolNames } = this.effectiveOptions
|
|
471
|
+
if (!allowTools && !forbidTools && !toolNames) return tools
|
|
472
|
+
|
|
473
|
+
let names = Object.keys(tools)
|
|
474
|
+
|
|
475
|
+
// toolNames is a strict exact-match allowlist
|
|
476
|
+
if (toolNames) {
|
|
477
|
+
const allowed = new Set(toolNames)
|
|
478
|
+
names = names.filter(n => allowed.has(n))
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// allowTools: only keep names matching at least one pattern
|
|
482
|
+
if (allowTools) {
|
|
483
|
+
names = names.filter(n => allowTools.some(pattern => this.matchToolPattern(pattern, n)))
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// forbidTools: remove names matching any pattern
|
|
487
|
+
if (forbidTools) {
|
|
488
|
+
names = names.filter(n => !forbidTools.some(pattern => this.matchToolPattern(pattern, n)))
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const result: Record<string, ConversationTool> = {}
|
|
492
|
+
for (const n of names) {
|
|
493
|
+
const tool = tools[n]
|
|
494
|
+
if (tool) result[n] = tool
|
|
495
|
+
}
|
|
496
|
+
return result
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Match a tool name against a pattern that supports "*" as a wildcard.
|
|
501
|
+
* - "*" matches everything
|
|
502
|
+
* - "prefix*" matches names starting with prefix
|
|
503
|
+
* - "*suffix" matches names ending with suffix
|
|
504
|
+
* - "pre*suf" matches names starting with pre and ending with suf
|
|
505
|
+
* - exact string matches exactly
|
|
506
|
+
*/
|
|
507
|
+
private matchToolPattern(pattern: string, name: string): boolean {
|
|
508
|
+
if (pattern === '*') return true
|
|
509
|
+
if (!pattern.includes('*')) return pattern === name
|
|
510
|
+
|
|
511
|
+
// Convert glob pattern to regex: escape regex chars, replace * with .*
|
|
512
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')
|
|
513
|
+
return new RegExp(`^${escaped}$`).test(name)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Apply a setup function or a Helper instance to this assistant.
|
|
518
|
+
*
|
|
519
|
+
* When passed a function, it receives the assistant and can configure
|
|
520
|
+
* tools, hooks, event listeners, etc.
|
|
521
|
+
*
|
|
522
|
+
* When passed a Helper instance that exposes tools via toTools(),
|
|
523
|
+
* those tools are automatically added to this assistant.
|
|
524
|
+
*
|
|
525
|
+
* @param fnOrHelper - Setup function or Helper instance
|
|
526
|
+
* @returns this, for chaining
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```typescript
|
|
530
|
+
* assistant
|
|
531
|
+
* .use(setupLogging)
|
|
532
|
+
* .use(container.feature('git'))
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
use(fnOrHelper: ((assistant: this) => void | Promise<void>) | { toTools: () => { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> } } | { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> }): this {
|
|
536
|
+
if (typeof fnOrHelper === 'function') {
|
|
537
|
+
const result = fnOrHelper(this)
|
|
538
|
+
if (result && typeof (result as any).then === 'function') {
|
|
539
|
+
const pending = this.state.get('pendingPlugins') as Promise<void>[]
|
|
540
|
+
this.state.set('pendingPlugins', [...pending, result as Promise<void>])
|
|
541
|
+
}
|
|
542
|
+
} else if (fnOrHelper && typeof (fnOrHelper as any).toTools === 'function') {
|
|
543
|
+
this._registerTools((fnOrHelper as any).toTools())
|
|
544
|
+
if (typeof (fnOrHelper as any).setupToolsConsumer === 'function') {
|
|
545
|
+
(fnOrHelper as any).setupToolsConsumer(this)
|
|
546
|
+
}
|
|
547
|
+
} else if (fnOrHelper && 'schemas' in fnOrHelper && 'handlers' in fnOrHelper) {
|
|
548
|
+
this._registerTools(fnOrHelper as { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> })
|
|
549
|
+
if (typeof (fnOrHelper as any).setup === 'function') {
|
|
550
|
+
(fnOrHelper as any).setup(this)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return this
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Register tools from a `{ schemas, handlers }` object. */
|
|
557
|
+
private _registerTools({ schemas, handlers }: { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> }) {
|
|
558
|
+
for (const name of Object.keys(schemas)) {
|
|
559
|
+
if (typeof handlers[name] === 'function') {
|
|
560
|
+
this.addTool(name, handlers[name] as any, schemas[name])
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Add a tool to this assistant. The tool name is derived from the
|
|
567
|
+
* handler's function name.
|
|
568
|
+
*
|
|
569
|
+
* @param handler - A named function that implements the tool
|
|
570
|
+
* @param schema - Optional Zod schema describing the tool's parameters
|
|
571
|
+
* @returns this, for chaining
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* ```typescript
|
|
575
|
+
* assistant.addTool(function getWeather(args) {
|
|
576
|
+
* return { temp: 72 }
|
|
577
|
+
* }, z.object({ city: z.string() }).describe('Get weather for a city'))
|
|
578
|
+
* ```
|
|
579
|
+
*/
|
|
580
|
+
addTool(name: string, handler: (...args: any[]) => any, schema?: z.ZodType): this {
|
|
581
|
+
if (!name) throw new Error('addTool handler must be a named function')
|
|
582
|
+
if (!this._runtimeToolNames) this._runtimeToolNames = new Set()
|
|
583
|
+
this._runtimeToolNames.add(name)
|
|
584
|
+
|
|
585
|
+
const current = { ...this.tools }
|
|
586
|
+
|
|
587
|
+
if (schema) {
|
|
588
|
+
const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
|
|
589
|
+
// OpenAI requires `required` to list ALL property keys — optional params
|
|
590
|
+
// must still appear in `required` but use a default value in the schema.
|
|
591
|
+
const properties = jsonSchema.properties || {}
|
|
592
|
+
const required = Object.keys(properties)
|
|
593
|
+
current[name] = {
|
|
594
|
+
handler: handler as ConversationTool['handler'],
|
|
595
|
+
description: jsonSchema.description || name,
|
|
596
|
+
parameters: {
|
|
597
|
+
type: jsonSchema.type || 'object',
|
|
598
|
+
properties,
|
|
599
|
+
required,
|
|
600
|
+
},
|
|
601
|
+
}
|
|
602
|
+
} else {
|
|
603
|
+
current[name] = {
|
|
604
|
+
handler: handler as ConversationTool['handler'],
|
|
605
|
+
description: name,
|
|
606
|
+
parameters: { type: 'object', properties: {} },
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
this.state.set('tools', current)
|
|
611
|
+
this.emit('toolsChanged')
|
|
612
|
+
|
|
613
|
+
return this
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Remove a tool by name or handler function reference.
|
|
618
|
+
*
|
|
619
|
+
* @param nameOrHandler - The tool name string, or the handler function to match
|
|
620
|
+
* @returns this, for chaining
|
|
621
|
+
*/
|
|
622
|
+
removeTool(nameOrHandler: string | ((...args: any[]) => any)): this {
|
|
623
|
+
const current = { ...this.tools }
|
|
624
|
+
|
|
625
|
+
if (typeof nameOrHandler === 'string') {
|
|
626
|
+
delete current[nameOrHandler]
|
|
627
|
+
this._runtimeToolNames?.delete(nameOrHandler)
|
|
628
|
+
} else {
|
|
629
|
+
for (const [name, tool] of Object.entries(current)) {
|
|
630
|
+
if (tool.handler === nameOrHandler) {
|
|
631
|
+
delete current[name]
|
|
632
|
+
this._runtimeToolNames?.delete(name)
|
|
633
|
+
break
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
this.state.set('tools', current)
|
|
639
|
+
this.emit('toolsChanged')
|
|
640
|
+
|
|
641
|
+
return this
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Simulate a tool call and its result by appending the appropriate
|
|
646
|
+
* messages to the conversation history. Useful for injecting context
|
|
647
|
+
* that looks like the assistant performed a tool call.
|
|
648
|
+
*
|
|
649
|
+
* @param toolCallName - The name of the tool
|
|
650
|
+
* @param args - The arguments that were "passed" to the tool
|
|
651
|
+
* @param result - The result the tool "returned"
|
|
652
|
+
* @returns this, for chaining
|
|
653
|
+
*/
|
|
654
|
+
simulateToolCallWithResult(toolCallName: string, args: Record<string, any>, result: any): this {
|
|
655
|
+
if (!this.conversation) {
|
|
656
|
+
throw new Error('Cannot simulate: assistant has no active conversation. Call start() first.')
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const callId = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
660
|
+
|
|
661
|
+
this.conversation.pushMessage({
|
|
662
|
+
role: 'assistant',
|
|
663
|
+
content: null,
|
|
664
|
+
tool_calls: [{
|
|
665
|
+
id: callId,
|
|
666
|
+
type: 'function',
|
|
667
|
+
function: {
|
|
668
|
+
name: toolCallName,
|
|
669
|
+
arguments: JSON.stringify(args),
|
|
670
|
+
},
|
|
671
|
+
}],
|
|
672
|
+
} as Message)
|
|
673
|
+
|
|
674
|
+
this.conversation.pushMessage({
|
|
675
|
+
role: 'tool',
|
|
676
|
+
tool_call_id: callId,
|
|
677
|
+
content: typeof result === 'string' ? result : JSON.stringify(result),
|
|
678
|
+
} as Message)
|
|
679
|
+
|
|
680
|
+
return this
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Simulate a user question and assistant response by appending both
|
|
685
|
+
* messages to the conversation history.
|
|
686
|
+
*
|
|
687
|
+
* @param question - The user's question
|
|
688
|
+
* @param response - The assistant's response
|
|
689
|
+
* @returns this, for chaining
|
|
690
|
+
*/
|
|
691
|
+
simulateQuestionAndResponse(question: string, response: string): this {
|
|
692
|
+
if (!this.conversation) {
|
|
693
|
+
throw new Error('Cannot simulate: assistant has no active conversation. Call start() first.')
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
this.conversation.pushMessage({ role: 'user', content: question })
|
|
697
|
+
this.conversation.pushMessage({ role: 'assistant', content: response })
|
|
698
|
+
|
|
699
|
+
return this
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Parsed YAML frontmatter from CORE.md, or empty object if none.
|
|
704
|
+
*/
|
|
705
|
+
get meta(): Record<string, any> {
|
|
706
|
+
return (this.state.get('meta') || {}) as Record<string, any>
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Merged options where CORE.md frontmatter provides defaults and
|
|
711
|
+
* constructor options take precedence. Prefer this over `this.options`
|
|
712
|
+
* anywhere model parameters or runtime config is consumed.
|
|
713
|
+
*/
|
|
714
|
+
get effectiveOptions(): AssistantOptions & Record<string, any> {
|
|
715
|
+
return { ...this.meta, ...this.options }
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Load the system prompt from CORE.md, applying any prepend/append options.
|
|
720
|
+
* YAML frontmatter (between --- fences) is stripped from the prompt and
|
|
721
|
+
* stored in `_meta`.
|
|
722
|
+
*
|
|
723
|
+
* @returns {string} The assembled system prompt
|
|
724
|
+
*/
|
|
725
|
+
loadSystemPrompt(): string {
|
|
726
|
+
const { fs } = this.container
|
|
727
|
+
let prompt = ''
|
|
728
|
+
this.state.set('meta', {})
|
|
729
|
+
|
|
730
|
+
if (this.options.systemPrompt) {
|
|
731
|
+
prompt = this.options.systemPrompt
|
|
732
|
+
} else if (fs.exists(this.corePromptPath)) {
|
|
733
|
+
const raw = fs.readFile(this.corePromptPath).toString()
|
|
734
|
+
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
|
|
735
|
+
|
|
736
|
+
if (fmMatch) {
|
|
737
|
+
const yaml = this.container.feature('yaml')
|
|
738
|
+
this.state.set('meta', yaml.parse(fmMatch[1]!) ?? {})
|
|
739
|
+
prompt = raw.slice(fmMatch[0].length)
|
|
740
|
+
} else {
|
|
741
|
+
prompt = raw
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (this.options.prependPrompt) {
|
|
746
|
+
prompt = this.options.prependPrompt + '\n\n' + prompt
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (this.options.appendPrompt) {
|
|
750
|
+
prompt = prompt + '\n\n' + this.options.appendPrompt
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (this.options.injectTimestamps) {
|
|
754
|
+
prompt = prompt + '\n\n' + [
|
|
755
|
+
'## Timestamps',
|
|
756
|
+
'Each user message is prefixed with a timestamp in [YYYY-MM-DD HH:MM] format.',
|
|
757
|
+
'Use these to understand the passage of time between interactions.',
|
|
758
|
+
'The user may return hours or days later within the same conversation — acknowledge the time gap naturally when relevant, and use timestamps to contextualize when topics were previously discussed.',
|
|
759
|
+
].join('\n')
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return prompt.trim()
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Load tools from tools.ts using the container's VM feature, injecting
|
|
767
|
+
* the container and assistant as globals. Merges with any tools
|
|
768
|
+
* provided in the constructor options. Runs synchronously via vm.loadModule.
|
|
769
|
+
*
|
|
770
|
+
* @returns {Record<string, ConversationTool>} The assembled tool map
|
|
771
|
+
*/
|
|
772
|
+
loadTools(): Record<string, ConversationTool> {
|
|
773
|
+
const tools: Record<string, ConversationTool> = {}
|
|
774
|
+
|
|
775
|
+
// Skip loading if no tools file exists (runtime-created assistants)
|
|
776
|
+
if (!this.container.fs.exists(this.toolsModulePath)) {
|
|
777
|
+
return this.mergeOptionTools(tools)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Ensure virtual modules (zod, @soederpop/luca, etc.) are seeded so tools
|
|
781
|
+
// files outside the project tree can resolve them through the VM
|
|
782
|
+
if (this.container.features.has('helpers')) {
|
|
783
|
+
const helpers = this.container.feature('helpers') as any
|
|
784
|
+
helpers.seedVirtualModules()
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const vm = this.container.feature('vm')
|
|
788
|
+
|
|
789
|
+
let moduleExports: Record<string, any>
|
|
790
|
+
try {
|
|
791
|
+
moduleExports = vm.loadModule(this.toolsModulePath, {
|
|
792
|
+
container: this.container,
|
|
793
|
+
me: this,
|
|
794
|
+
my: this,
|
|
795
|
+
assistant: this,
|
|
796
|
+
console: console,
|
|
797
|
+
})
|
|
798
|
+
} catch (err: any) {
|
|
799
|
+
console.error(`Failed to load tools from ${this.toolsModulePath}`)
|
|
800
|
+
console.error(`There may be a syntax error in this file. Please check it.`)
|
|
801
|
+
console.error(err.message || err)
|
|
802
|
+
return this.mergeOptionTools(tools)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Stash `export const use = [...]` for deferred processing during start(),
|
|
806
|
+
// since the assistant isn't fully constructed yet when loadTools() runs
|
|
807
|
+
if (Array.isArray(moduleExports.use)) {
|
|
808
|
+
this.state.set('deferredUse', moduleExports.use)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (Object.keys(moduleExports).length) {
|
|
812
|
+
const schemas: Record<string, z.ZodType> = moduleExports.schemas || {}
|
|
813
|
+
|
|
814
|
+
for (const [name, fn] of Object.entries(moduleExports)) {
|
|
815
|
+
if (name === 'schemas' || name === 'default' || name === 'use' || typeof fn !== 'function') continue
|
|
816
|
+
|
|
817
|
+
const schema = schemas[name]
|
|
818
|
+
if (schema) {
|
|
819
|
+
const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
|
|
820
|
+
tools[name] = {
|
|
821
|
+
handler: fn as ConversationTool['handler'],
|
|
822
|
+
description: jsonSchema.description || name,
|
|
823
|
+
parameters: {
|
|
824
|
+
type: jsonSchema.type || 'object',
|
|
825
|
+
properties: jsonSchema.properties || {},
|
|
826
|
+
...(jsonSchema.required ? { required: jsonSchema.required } : {}),
|
|
827
|
+
},
|
|
828
|
+
}
|
|
829
|
+
} else {
|
|
830
|
+
tools[name] = {
|
|
831
|
+
handler: fn as ConversationTool['handler'],
|
|
832
|
+
description: name,
|
|
833
|
+
parameters: { type: 'object', properties: {} },
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return this.mergeOptionTools(tools)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Merge tools provided via constructor options into the tool map.
|
|
844
|
+
* This allows runtime-created assistants to define tools entirely via options.
|
|
845
|
+
*/
|
|
846
|
+
private mergeOptionTools(tools: Record<string, ConversationTool>): Record<string, ConversationTool> {
|
|
847
|
+
if (this.options.tools) {
|
|
848
|
+
const optionSchemas = this.options.schemas || {}
|
|
849
|
+
|
|
850
|
+
for (const [name, fn] of Object.entries(this.options.tools)) {
|
|
851
|
+
if (typeof fn !== 'function') continue
|
|
852
|
+
|
|
853
|
+
const schema = optionSchemas[name]
|
|
854
|
+
if (schema) {
|
|
855
|
+
const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
|
|
856
|
+
tools[name] = {
|
|
857
|
+
handler: fn as ConversationTool['handler'],
|
|
858
|
+
description: jsonSchema.description || name,
|
|
859
|
+
parameters: {
|
|
860
|
+
type: jsonSchema.type || 'object',
|
|
861
|
+
properties: jsonSchema.properties || {},
|
|
862
|
+
...(jsonSchema.required ? { required: jsonSchema.required } : {}),
|
|
863
|
+
},
|
|
864
|
+
}
|
|
865
|
+
} else {
|
|
866
|
+
tools[name] = {
|
|
867
|
+
handler: fn as ConversationTool['handler'],
|
|
868
|
+
description: name,
|
|
869
|
+
parameters: { type: 'object', properties: {} },
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return tools
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Load event hooks from hooks.ts. Each exported function name should
|
|
880
|
+
* match an event the assistant emits. When that event fires, the
|
|
881
|
+
* corresponding hook function is called. Runs synchronously via vm.loadModule.
|
|
882
|
+
*
|
|
883
|
+
* @returns {Record<string, Function>} The hook function map
|
|
884
|
+
*/
|
|
885
|
+
loadHooks(): Record<string, (...args: any[]) => any> {
|
|
886
|
+
const hooks: Record<string, (...args: any[]) => any> = {}
|
|
887
|
+
|
|
888
|
+
// Skip loading if no hooks file exists (runtime-created assistants)
|
|
889
|
+
if (!this.container.fs.exists(this.hooksModulePath)) {
|
|
890
|
+
return hooks
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const vm = this.container.feature('vm')
|
|
894
|
+
|
|
895
|
+
let moduleExports: Record<string, any>
|
|
896
|
+
try {
|
|
897
|
+
moduleExports = vm.loadModule(this.hooksModulePath, {
|
|
898
|
+
container: this.container,
|
|
899
|
+
me: this,
|
|
900
|
+
my: this,
|
|
901
|
+
assistant: this,
|
|
902
|
+
console: console,
|
|
903
|
+
})
|
|
904
|
+
} catch (err: any) {
|
|
905
|
+
console.error(`Failed to load hooks from ${this.hooksModulePath}`)
|
|
906
|
+
console.error(`There may be a syntax error in this file. Please check it.`)
|
|
907
|
+
console.error(err.message || err)
|
|
908
|
+
return hooks
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
for (const [name, fn] of Object.entries(moduleExports)) {
|
|
912
|
+
if (name === 'default' || typeof fn !== 'function') continue
|
|
913
|
+
hooks[name] = fn as (...args: any[]) => any
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return hooks
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Provides a helper for creating paths off of the assistant's base folder
|
|
921
|
+
*/
|
|
922
|
+
get paths() {
|
|
923
|
+
const { container } = this
|
|
924
|
+
const base = this.resolvedFolder
|
|
925
|
+
|
|
926
|
+
return {
|
|
927
|
+
resolve(...args: any[]) {
|
|
928
|
+
return container.paths.resolve(base, ...args)
|
|
929
|
+
},
|
|
930
|
+
join(...args: any[]) {
|
|
931
|
+
return container.paths.resolve(base, ...args)
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Prepend a [YYYY-MM-DD HH:MM] timestamp to user message content.
|
|
938
|
+
*/
|
|
939
|
+
private prependTimestamp(content: string | ContentPart[]): string | ContentPart[] {
|
|
940
|
+
const now = new Date()
|
|
941
|
+
const pad = (n: number) => String(n).padStart(2, '0')
|
|
942
|
+
const stamp = `[${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}]`
|
|
943
|
+
|
|
944
|
+
if (typeof content === 'string') {
|
|
945
|
+
return `${stamp} ${content}`
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const firstPart = content[0]
|
|
949
|
+
if (firstPart && firstPart.type === 'text') {
|
|
950
|
+
return [{ type: 'text' as const, text: `${stamp} ${firstPart.text}` }, ...content.slice(1)]
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
return [{ type: 'text' as const, text: stamp }, ...content]
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// -- History mode helpers --
|
|
957
|
+
|
|
958
|
+
/** The assistant name derived from the folder basename. */
|
|
959
|
+
get assistantName(): string {
|
|
960
|
+
return this.resolvedFolder.split('/').pop() || 'assistant'
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** An 8-char hash of the container cwd for per-project thread isolation. */
|
|
964
|
+
get cwdHash(): string {
|
|
965
|
+
return hashObject(this.container.cwd).slice(0, 8)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/** The thread prefix for this assistant+project combination. */
|
|
969
|
+
get threadPrefix(): string {
|
|
970
|
+
return `${this.assistantName}:${this.cwdHash}:`
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/** Build a thread ID based on the history mode. */
|
|
974
|
+
private buildThreadId(mode: string): string {
|
|
975
|
+
const prefix = this.threadPrefix
|
|
976
|
+
switch (mode) {
|
|
977
|
+
case 'daily': {
|
|
978
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
979
|
+
return `${prefix}${today}`
|
|
980
|
+
}
|
|
981
|
+
case 'persistent':
|
|
982
|
+
return `${prefix}persistent`
|
|
983
|
+
case 'session':
|
|
984
|
+
return `${prefix}${this.uuid}`
|
|
985
|
+
default:
|
|
986
|
+
return `${prefix}${this.uuid}`
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** The conversationHistory feature instance. */
|
|
991
|
+
get conversationHistory(): ConversationHistory {
|
|
992
|
+
return this.container.feature('conversationHistory') as ConversationHistory
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** The active thread ID (undefined in lifecycle mode). */
|
|
996
|
+
get currentThreadId(): string | undefined {
|
|
997
|
+
return this.state.get('threadId')
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Override thread for resume. Call before start().
|
|
1002
|
+
*
|
|
1003
|
+
* @param threadId - The thread ID to resume
|
|
1004
|
+
* @returns this, for chaining
|
|
1005
|
+
*/
|
|
1006
|
+
resumeThread(threadId: string): this {
|
|
1007
|
+
this.state.set('resumeThreadId', threadId)
|
|
1008
|
+
return this
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* List saved conversations for this assistant+project.
|
|
1013
|
+
*
|
|
1014
|
+
* @param opts - Optional limit
|
|
1015
|
+
* @returns Conversation metadata records
|
|
1016
|
+
*/
|
|
1017
|
+
async listHistory(opts?: { limit?: number }): Promise<ConversationMeta[]> {
|
|
1018
|
+
const metas = await this.conversationHistory.findByThreadPrefix(this.threadPrefix)
|
|
1019
|
+
if (opts?.limit) return metas.slice(0, opts.limit)
|
|
1020
|
+
return metas
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Delete all history for this assistant+project.
|
|
1025
|
+
*
|
|
1026
|
+
* @returns Number of conversations deleted
|
|
1027
|
+
*/
|
|
1028
|
+
async clearHistory(): Promise<number> {
|
|
1029
|
+
return this.conversationHistory.deleteByThreadPrefix(this.threadPrefix)
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Load history into the conversation after it's been created.
|
|
1034
|
+
* Called from start() for non-lifecycle modes.
|
|
1035
|
+
*/
|
|
1036
|
+
private async loadConversationHistory(): Promise<void> {
|
|
1037
|
+
const mode = this.effectiveOptions.historyMode || 'lifecycle'
|
|
1038
|
+
if (mode === 'lifecycle') return
|
|
1039
|
+
|
|
1040
|
+
const threadId = (this.state.get('resumeThreadId') as string | undefined) || this.buildThreadId(mode)
|
|
1041
|
+
this.state.set('threadId', threadId)
|
|
1042
|
+
|
|
1043
|
+
const existing = await this.conversationHistory.findByThread(threadId)
|
|
1044
|
+
|
|
1045
|
+
if (existing) {
|
|
1046
|
+
// Replace conversation messages with loaded history
|
|
1047
|
+
const messages = [...existing.messages]
|
|
1048
|
+
|
|
1049
|
+
// Swap in fresh system prompt if it changed
|
|
1050
|
+
if (messages.length > 0 && (messages[0]!.role === 'system' || messages[0]!.role === 'developer')) {
|
|
1051
|
+
messages[0] = { role: messages[0]!.role, content: this.effectiveSystemPrompt }
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
this.conversation.state.set('id', existing.id)
|
|
1055
|
+
this.conversation.state.set('thread', threadId)
|
|
1056
|
+
this.conversation.state.set('messages', messages)
|
|
1057
|
+
this.state.set('conversationId', existing.id)
|
|
1058
|
+
|
|
1059
|
+
// Restore lastResponseId so the Responses API can continue the chain
|
|
1060
|
+
if (existing.metadata?.lastResponseId) {
|
|
1061
|
+
this.conversation.state.set('lastResponseId', existing.metadata.lastResponseId)
|
|
1062
|
+
}
|
|
1063
|
+
} else {
|
|
1064
|
+
// Fresh conversation — just set thread
|
|
1065
|
+
this.conversation.state.set('thread', threadId)
|
|
1066
|
+
this.state.set('conversationId', this.conversation.state.get('id'))
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/** Tool names added at runtime via addTool()/use(), so reload() can preserve them. */
|
|
1071
|
+
private _runtimeToolNames!: Set<string>
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Reload tools, hooks, and system prompt from disk. Useful during development
|
|
1075
|
+
* or when tool/hook files have been modified and you want the assistant to
|
|
1076
|
+
* pick up changes without restarting.
|
|
1077
|
+
*
|
|
1078
|
+
* @returns this, for chaining
|
|
1079
|
+
*/
|
|
1080
|
+
reload(): this {
|
|
1081
|
+
// Snapshot runtime-added tools before reloading from disk
|
|
1082
|
+
const runtimeTools: Record<string, ConversationTool> = {}
|
|
1083
|
+
if (this._runtimeToolNames?.size) {
|
|
1084
|
+
const current = this.tools
|
|
1085
|
+
for (const name of this._runtimeToolNames) {
|
|
1086
|
+
if (current[name]) runtimeTools[name] = current[name]
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Reload system prompt from disk
|
|
1091
|
+
this.state.set('systemPrompt', this.loadSystemPrompt())
|
|
1092
|
+
|
|
1093
|
+
// Reload tools from disk (merges with option tools), then restore runtime tools
|
|
1094
|
+
const diskTools = this.loadTools()
|
|
1095
|
+
this.state.set('tools', { ...diskTools, ...runtimeTools })
|
|
1096
|
+
this.emit('toolsChanged')
|
|
1097
|
+
|
|
1098
|
+
// Reload hooks from disk — triggerHook reads from state so new hooks are active immediately
|
|
1099
|
+
this.state.set('hooks', this.loadHooks())
|
|
1100
|
+
|
|
1101
|
+
this.emit('reloaded')
|
|
1102
|
+
|
|
1103
|
+
return this
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Start the assistant by creating the conversation and wiring up events.
|
|
1108
|
+
* The system prompt, tools, and hooks are already loaded synchronously
|
|
1109
|
+
* during initialization.
|
|
1110
|
+
*
|
|
1111
|
+
* @returns {Promise<this>} The initialized assistant
|
|
1112
|
+
*/
|
|
1113
|
+
async start(): Promise<this> {
|
|
1114
|
+
// Prevent duplicate listener registration if already started
|
|
1115
|
+
if (this.isStarted) return this
|
|
1116
|
+
|
|
1117
|
+
// Process deferred `use` entries from tools.ts (stashed during loadTools
|
|
1118
|
+
// because the assistant isn't fully constructed at that point)
|
|
1119
|
+
const deferredUse = this.state.get('deferredUse') as any[] | undefined
|
|
1120
|
+
if (deferredUse?.length) {
|
|
1121
|
+
for (const entry of deferredUse) {
|
|
1122
|
+
this.use(entry)
|
|
1123
|
+
}
|
|
1124
|
+
this.state.set('deferredUse', undefined)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Allow hooks to run before the assistant starts (blocks until complete)
|
|
1128
|
+
await this.triggerHook('beforeStart')
|
|
1129
|
+
|
|
1130
|
+
// Wait for any async .use() plugins to finish before starting
|
|
1131
|
+
const pending = this.state.get('pendingPlugins') as Promise<void>[]
|
|
1132
|
+
if (pending.length) {
|
|
1133
|
+
await Promise.all(pending)
|
|
1134
|
+
this.state.set('pendingPlugins', [])
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Allow hooks.ts to export a formatSystemPrompt(assistant, prompt) => string
|
|
1138
|
+
// that transforms the system prompt before the conversation is created.
|
|
1139
|
+
const formatted = await this.triggerHook('formatSystemPrompt', this.systemPrompt)
|
|
1140
|
+
if (typeof formatted === 'string') {
|
|
1141
|
+
this.state.set('systemPrompt', formatted)
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Wire up event forwarding from conversation to assistant.
|
|
1145
|
+
// Each forwarded event triggers its hook (awaited) before emitting on the assistant bus.
|
|
1146
|
+
this.conversation.on('turnStart', async (info: any) => {
|
|
1147
|
+
await this.triggerHook('turnStart', info)
|
|
1148
|
+
this.emit('turnStart', info)
|
|
1149
|
+
})
|
|
1150
|
+
this.conversation.on('turnEnd', async (info: any) => {
|
|
1151
|
+
await this.triggerHook('turnEnd', info)
|
|
1152
|
+
this.emit('turnEnd', info)
|
|
1153
|
+
})
|
|
1154
|
+
this.conversation.on('chunk', async (chunk: string) => {
|
|
1155
|
+
await this.triggerHook('chunk', chunk)
|
|
1156
|
+
this.emit('chunk', chunk)
|
|
1157
|
+
})
|
|
1158
|
+
this.conversation.on('preview', async (text: string) => {
|
|
1159
|
+
await this.triggerHook('preview', text)
|
|
1160
|
+
this.emit('preview', text)
|
|
1161
|
+
})
|
|
1162
|
+
this.conversation.on('response', async (text: string) => {
|
|
1163
|
+
await this.triggerHook('response', text)
|
|
1164
|
+
this.emit('response', text)
|
|
1165
|
+
this.state.set('lastResponse', text)
|
|
1166
|
+
})
|
|
1167
|
+
this.conversation.on('rawEvent', async (event: any) => {
|
|
1168
|
+
await this.triggerHook('rawEvent', event)
|
|
1169
|
+
this.emit('rawEvent', event)
|
|
1170
|
+
})
|
|
1171
|
+
this.conversation.on('mcpEvent', async (event: any) => {
|
|
1172
|
+
await this.triggerHook('mcpEvent', event)
|
|
1173
|
+
this.emit('mcpEvent', event)
|
|
1174
|
+
})
|
|
1175
|
+
this.conversation.on('toolCall', async (name: string, args: any) => {
|
|
1176
|
+
await this.triggerHook('toolCall', name, args)
|
|
1177
|
+
this.emit('toolCall', name, args)
|
|
1178
|
+
})
|
|
1179
|
+
this.conversation.on('toolResult', async (name: string, result: any) => {
|
|
1180
|
+
await this.triggerHook('toolResult', name, result)
|
|
1181
|
+
this.emit('toolResult', name, result)
|
|
1182
|
+
})
|
|
1183
|
+
this.conversation.on('toolError', async (name: string, error: any) => {
|
|
1184
|
+
await this.triggerHook('toolError', name, error)
|
|
1185
|
+
this.emit('toolError', name, error)
|
|
1186
|
+
})
|
|
1187
|
+
|
|
1188
|
+
// Install interceptor-aware tool executor on the conversation
|
|
1189
|
+
this.conversation.toolExecutor = async (name: string, args: Record<string, any>, handler: (...a: any[]) => Promise<any>) => {
|
|
1190
|
+
const ctx = { name, args, result: undefined as string | undefined, error: undefined, skip: false }
|
|
1191
|
+
|
|
1192
|
+
// Hook runs first (awaited), then interceptor chain
|
|
1193
|
+
await this.triggerHook('beforeToolCall', ctx)
|
|
1194
|
+
await this.interceptors.beforeToolCall.run(ctx, async () => {})
|
|
1195
|
+
|
|
1196
|
+
if (ctx.skip) {
|
|
1197
|
+
const result = ctx.result ?? JSON.stringify({ skipped: true })
|
|
1198
|
+
await this.triggerHook('toolResult', ctx.name, result)
|
|
1199
|
+
this.emit('toolResult', ctx.name, result)
|
|
1200
|
+
return result
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
try {
|
|
1204
|
+
await this.triggerHook('toolCall', ctx.name, ctx.args)
|
|
1205
|
+
this.emit('toolCall', ctx.name, ctx.args)
|
|
1206
|
+
const output = await handler(ctx.args)
|
|
1207
|
+
ctx.result = typeof output === 'string' ? output : JSON.stringify(output)
|
|
1208
|
+
} catch (err: any) {
|
|
1209
|
+
ctx.error = err
|
|
1210
|
+
ctx.result = JSON.stringify({ error: err.message || String(err) })
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Hook runs first (awaited), then interceptor chain
|
|
1214
|
+
await this.triggerHook('afterToolCall', ctx)
|
|
1215
|
+
await this.interceptors.afterToolCall.run(ctx, async () => {})
|
|
1216
|
+
|
|
1217
|
+
if (ctx.error && !ctx.result?.includes('"error"')) {
|
|
1218
|
+
await this.triggerHook('toolError', ctx.name, ctx.error)
|
|
1219
|
+
this.emit('toolError', ctx.name, ctx.error)
|
|
1220
|
+
} else {
|
|
1221
|
+
await this.triggerHook('toolResult', ctx.name, ctx.result!)
|
|
1222
|
+
this.emit('toolResult', ctx.name, ctx.result!)
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
return ctx.result!
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Load conversation history for non-lifecycle modes
|
|
1229
|
+
await this.loadConversationHistory()
|
|
1230
|
+
|
|
1231
|
+
// Enable autoCompact for modes that accumulate history
|
|
1232
|
+
const mode = this.effectiveOptions.historyMode || 'lifecycle'
|
|
1233
|
+
if (mode === 'daily' || mode === 'persistent') {
|
|
1234
|
+
(this.conversation.options as any).autoCompact = true
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
this.on('toolsChanged', () => {
|
|
1238
|
+
const conv = this.state.get('conversation') as Conversation | null
|
|
1239
|
+
if (conv) {
|
|
1240
|
+
conv.updateTools(this.tools)
|
|
1241
|
+
}
|
|
1242
|
+
})
|
|
1243
|
+
|
|
1244
|
+
this.state.set('started', true)
|
|
1245
|
+
await this.triggerHook('started')
|
|
1246
|
+
this.emit('started')
|
|
1247
|
+
|
|
1248
|
+
// afterStart blocks until complete — use for setup that needs the full assistant ready
|
|
1249
|
+
await this.triggerHook('afterStart')
|
|
1250
|
+
|
|
1251
|
+
return this
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Ask the assistant a question. It will use its tools to produce
|
|
1256
|
+
* a streamed response. The assistant auto-starts if needed.
|
|
1257
|
+
*
|
|
1258
|
+
* @param {string | ContentPart[]} question - The question to ask
|
|
1259
|
+
* @returns {Promise<string>} The assistant's response
|
|
1260
|
+
*
|
|
1261
|
+
* @example
|
|
1262
|
+
* ```typescript
|
|
1263
|
+
* const answer = await assistant.ask('What capabilities do you have?')
|
|
1264
|
+
* ```
|
|
1265
|
+
*/
|
|
1266
|
+
async ask(question: string | ContentPart[], options?: AskOptions): Promise<string> {
|
|
1267
|
+
if (!this.isStarted) {
|
|
1268
|
+
await this.start()
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if (!this.conversation) {
|
|
1272
|
+
return 'Assistant is not started'
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const count = (this.state.get('conversationCount') || 0) + 1
|
|
1276
|
+
this.state.set('conversationCount', count)
|
|
1277
|
+
|
|
1278
|
+
if (this.effectiveOptions.injectTimestamps) {
|
|
1279
|
+
question = this.prependTimestamp(question)
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Trigger beforeInitialAsk only on the first ask() call
|
|
1283
|
+
if (count === 1) {
|
|
1284
|
+
await this.triggerHook('beforeInitialAsk', question, options)
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Trigger beforeAsk hook on every ask() call — can modify question via return value
|
|
1288
|
+
const hookResult = await this.triggerHook('beforeAsk', question, options)
|
|
1289
|
+
if (typeof hookResult === 'string') {
|
|
1290
|
+
question = hookResult
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Run beforeAsk interceptors — they can rewrite the question or short-circuit
|
|
1294
|
+
if (this.interceptors.beforeAsk.hasInterceptors) {
|
|
1295
|
+
const ctx = { question, options } as InterceptorPoints['beforeAsk']
|
|
1296
|
+
await this.interceptors.beforeAsk.run(ctx, async () => {})
|
|
1297
|
+
if (ctx.result !== undefined) return ctx.result
|
|
1298
|
+
question = ctx.question
|
|
1299
|
+
options = ctx.options
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
let result = await this.conversation.ask(question, options)
|
|
1303
|
+
|
|
1304
|
+
// Run beforeResponse interceptors — they can rewrite the final text
|
|
1305
|
+
if (this.interceptors.beforeResponse.hasInterceptors) {
|
|
1306
|
+
const ctx = { text: result }
|
|
1307
|
+
await this.interceptors.beforeResponse.run(ctx, async () => {})
|
|
1308
|
+
result = ctx.text
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Auto-save for non-lifecycle modes
|
|
1312
|
+
if (this.effectiveOptions.historyMode !== 'lifecycle' && this.state.get('threadId')) {
|
|
1313
|
+
await this.conversation.save({ thread: this.state.get('threadId') })
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
await this.triggerHook('answered', result)
|
|
1317
|
+
this.emit('answered', result)
|
|
1318
|
+
|
|
1319
|
+
return result
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Save the conversation to disk via conversationHistory.
|
|
1324
|
+
*
|
|
1325
|
+
* @param opts - Optional overrides for title, tags, thread, or metadata
|
|
1326
|
+
* @returns The saved conversation record
|
|
1327
|
+
*/
|
|
1328
|
+
async save(opts?: { title?: string; tags?: string[]; thread?: string; metadata?: Record<string, any> }) {
|
|
1329
|
+
if (!this.conversation) {
|
|
1330
|
+
throw new Error('Cannot save: assistant has no active conversation')
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
return this.conversation.save(opts)
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// -- Fork & Research API --
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Fork the assistant into a new independent instance. The fork gets its own
|
|
1340
|
+
* conversation (with configurable history truncation) but preserves the
|
|
1341
|
+
* assistant's full identity: interceptors, tools, hooks, system prompt extensions.
|
|
1342
|
+
*
|
|
1343
|
+
* @param options - Fork options including history truncation and conversation overrides
|
|
1344
|
+
* - `history: 'full'` (default) — deep copy all messages
|
|
1345
|
+
* - `history: 'none'` — system prompt only
|
|
1346
|
+
* - `history: number` — keep last N exchanges + system prompt
|
|
1347
|
+
* - Plus any conversation creation overrides (model, maxTokens, temperature, etc.)
|
|
1348
|
+
*
|
|
1349
|
+
* When called with an array, creates multiple independent forks.
|
|
1350
|
+
*
|
|
1351
|
+
* @example
|
|
1352
|
+
* ```typescript
|
|
1353
|
+
* // Single fork with no history, cheap model
|
|
1354
|
+
* const fork = await assistant.fork({ history: 'none', model: 'gpt-4o-mini' })
|
|
1355
|
+
* const answer = await fork.ask('Quick factual question')
|
|
1356
|
+
*
|
|
1357
|
+
* // Multiple forks
|
|
1358
|
+
* const [a, b] = await assistant.fork([
|
|
1359
|
+
* { history: 'none' },
|
|
1360
|
+
* { history: 3 },
|
|
1361
|
+
* ])
|
|
1362
|
+
* ```
|
|
1363
|
+
*/
|
|
1364
|
+
async fork(options?: AssistantForkOptions): Promise<Assistant>
|
|
1365
|
+
async fork(options?: AssistantForkOptions[]): Promise<Assistant[]>
|
|
1366
|
+
async fork(options: AssistantForkOptions | AssistantForkOptions[] = {}): Promise<Assistant | Assistant[]> {
|
|
1367
|
+
if (Array.isArray(options)) {
|
|
1368
|
+
return Promise.all(options.map(o => this.fork(o)))
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (!this.isStarted) {
|
|
1372
|
+
await this.start()
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Separate assistant-level options from conversation-level options
|
|
1376
|
+
const { history: historyMode, forbidTools, allowTools, toolNames, onFork, ...convOverrides } = options
|
|
1377
|
+
|
|
1378
|
+
// Fork the conversation with history truncation
|
|
1379
|
+
const forkedConv = this.conversation.fork({ history: historyMode ?? 'full', ...convOverrides })
|
|
1380
|
+
|
|
1381
|
+
// Create a new assistant that reuses the forked conversation
|
|
1382
|
+
const forkedAssistant = this.container.feature('assistant', {
|
|
1383
|
+
...this.options,
|
|
1384
|
+
// Pass through conversation overrides that map to assistant options
|
|
1385
|
+
...(convOverrides.model ? { model: convOverrides.model } : {}),
|
|
1386
|
+
...(convOverrides.maxTokens ? { maxTokens: convOverrides.maxTokens } : {}),
|
|
1387
|
+
...(convOverrides.temperature != null ? { temperature: convOverrides.temperature } : {}),
|
|
1388
|
+
...(convOverrides.topP != null ? { topP: convOverrides.topP } : {}),
|
|
1389
|
+
...(convOverrides.topK != null ? { topK: convOverrides.topK } : {}),
|
|
1390
|
+
...(convOverrides.frequencyPenalty != null ? { frequencyPenalty: convOverrides.frequencyPenalty } : {}),
|
|
1391
|
+
...(convOverrides.presencePenalty != null ? { presencePenalty: convOverrides.presencePenalty } : {}),
|
|
1392
|
+
...(convOverrides.stop ? { stop: convOverrides.stop } : {}),
|
|
1393
|
+
// Pass through tool filtering options
|
|
1394
|
+
...(forbidTools ? { forbidTools } : {}),
|
|
1395
|
+
...(allowTools ? { allowTools } : {}),
|
|
1396
|
+
...(toolNames ? { toolNames } : {}),
|
|
1397
|
+
}) as Assistant
|
|
1398
|
+
|
|
1399
|
+
// Inject the forked conversation directly, bypassing the lazy getter
|
|
1400
|
+
forkedAssistant.state.set('conversation', forkedConv)
|
|
1401
|
+
|
|
1402
|
+
// Track fork depth so forks know they are forks
|
|
1403
|
+
forkedAssistant.state.set('forkDepth', this.forkDepth + 1)
|
|
1404
|
+
|
|
1405
|
+
// Clone interceptors so the fork behaves like the original
|
|
1406
|
+
forkedAssistant.interceptors.beforeAsk = this.interceptors.beforeAsk.clone()
|
|
1407
|
+
forkedAssistant.interceptors.beforeTurn = this.interceptors.beforeTurn.clone()
|
|
1408
|
+
forkedAssistant.interceptors.beforeToolCall = this.interceptors.beforeToolCall.clone()
|
|
1409
|
+
forkedAssistant.interceptors.afterToolCall = this.interceptors.afterToolCall.clone()
|
|
1410
|
+
forkedAssistant.interceptors.beforeResponse = this.interceptors.beforeResponse.clone()
|
|
1411
|
+
|
|
1412
|
+
// Copy system prompt extensions
|
|
1413
|
+
forkedAssistant.state.set('systemPromptExtensions', { ...this.systemPromptExtensions })
|
|
1414
|
+
|
|
1415
|
+
// Start wires up event forwarding and the interceptor-aware tool executor
|
|
1416
|
+
await forkedAssistant.start()
|
|
1417
|
+
|
|
1418
|
+
// Call the onFork hook if provided — lets callers customize the fork before use
|
|
1419
|
+
if (onFork) {
|
|
1420
|
+
await onFork(forkedAssistant, this)
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
return forkedAssistant
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/** Active and completed research jobs, keyed by job entity ID. */
|
|
1427
|
+
readonly researchJobs = new Map<string, ResearchJob>()
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Create a non-blocking research job that fans out questions across forked assistants.
|
|
1431
|
+
* The forks fire immediately and the returned entity tracks progress via observable
|
|
1432
|
+
* state and events. Each fork preserves the full assistant identity (interceptors,
|
|
1433
|
+
* tools, hooks).
|
|
1434
|
+
*
|
|
1435
|
+
* @param prompt - Shared context/framing prompt prepended to each fork's system prompt
|
|
1436
|
+
* @param questions - Array of questions (strings) or objects with question + per-fork overrides
|
|
1437
|
+
* @param defaults - Default fork options applied to all forks
|
|
1438
|
+
* @returns A research job entity with observable state and events
|
|
1439
|
+
*
|
|
1440
|
+
* @example
|
|
1441
|
+
* ```typescript
|
|
1442
|
+
* // Fire and forget — check later
|
|
1443
|
+
* const job = await assistant.createResearchJob(
|
|
1444
|
+
* "Analyze this codebase for security issues",
|
|
1445
|
+
* ["Look for SQL injection", "Look for XSS", "Look for auth bypass"],
|
|
1446
|
+
* { history: 'none', model: 'gpt-4o-mini' }
|
|
1447
|
+
* )
|
|
1448
|
+
*
|
|
1449
|
+
* // Check progress
|
|
1450
|
+
* job.state.get('completed') // 2 of 3
|
|
1451
|
+
* job.state.get('results') // [answer1, answer2, null]
|
|
1452
|
+
*
|
|
1453
|
+
* // React to events
|
|
1454
|
+
* job.on('forkCompleted', (index, result) => console.log(`Fork ${index} done`))
|
|
1455
|
+
*
|
|
1456
|
+
* // Or just wait
|
|
1457
|
+
* await job.waitFor('completed')
|
|
1458
|
+
* ```
|
|
1459
|
+
*/
|
|
1460
|
+
async createResearchJob(
|
|
1461
|
+
prompt: string,
|
|
1462
|
+
questions: (string | { question: string; forkOptions?: AssistantForkOptions })[],
|
|
1463
|
+
defaults: AssistantForkOptions = {}
|
|
1464
|
+
): Promise<ResearchJob> {
|
|
1465
|
+
if (!this.isStarted) {
|
|
1466
|
+
await this.start()
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
const jobId = `research:${this.container.utils.uuid()}`
|
|
1470
|
+
const total = questions.length
|
|
1471
|
+
|
|
1472
|
+
const job = this.container.entity<ResearchJobState, ResearchJobOptions, ResearchJobEvents>(
|
|
1473
|
+
jobId,
|
|
1474
|
+
{ prompt, questions: questions.map(q => typeof q === 'string' ? q : q.question), forkOptions: defaults },
|
|
1475
|
+
) as ResearchJob
|
|
1476
|
+
|
|
1477
|
+
job.setState({
|
|
1478
|
+
status: 'running',
|
|
1479
|
+
prompt,
|
|
1480
|
+
questions: questions.map(q => typeof q === 'string' ? q : q.question),
|
|
1481
|
+
results: new Array(total).fill(null),
|
|
1482
|
+
errors: new Array(total).fill(null),
|
|
1483
|
+
completed: 0,
|
|
1484
|
+
total,
|
|
1485
|
+
})
|
|
1486
|
+
|
|
1487
|
+
this.researchJobs.set(jobId, job)
|
|
1488
|
+
|
|
1489
|
+
// Build fork configs and create forks
|
|
1490
|
+
const forkConfigs = questions.map(q => ({
|
|
1491
|
+
...defaults,
|
|
1492
|
+
...(typeof q === 'string' ? {} : q.forkOptions),
|
|
1493
|
+
}))
|
|
1494
|
+
|
|
1495
|
+
const forks = await this.fork(forkConfigs)
|
|
1496
|
+
|
|
1497
|
+
// Apply shared prompt as a system prompt extension on each fork
|
|
1498
|
+
if (prompt) {
|
|
1499
|
+
for (const fork of forks) {
|
|
1500
|
+
fork.addSystemPromptExtension('researchPrompt', prompt)
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// Fire all forks — don't await the batch, let them resolve individually
|
|
1505
|
+
for (let i = 0; i < forks.length; i++) {
|
|
1506
|
+
const fork = forks[i]!
|
|
1507
|
+
const q = questions[i]!
|
|
1508
|
+
const question = typeof q === 'string' ? q : q.question
|
|
1509
|
+
|
|
1510
|
+
fork.ask(question).then(
|
|
1511
|
+
(result) => {
|
|
1512
|
+
const results = [...job.state.get('results')!]
|
|
1513
|
+
results[i] = result
|
|
1514
|
+
const completed = job.state.get('completed')! + 1
|
|
1515
|
+
|
|
1516
|
+
job.setState({ results, completed })
|
|
1517
|
+
job.emit('forkCompleted', i, result)
|
|
1518
|
+
|
|
1519
|
+
if (completed === total) {
|
|
1520
|
+
job.setState({ status: 'completed' })
|
|
1521
|
+
job.emit('completed', results as string[])
|
|
1522
|
+
}
|
|
1523
|
+
},
|
|
1524
|
+
(err) => {
|
|
1525
|
+
const errors = [...job.state.get('errors')!]
|
|
1526
|
+
errors[i] = err?.message || String(err)
|
|
1527
|
+
const completed = job.state.get('completed')! + 1
|
|
1528
|
+
|
|
1529
|
+
job.setState({ errors, completed })
|
|
1530
|
+
job.emit('forkError', i, errors[i]!)
|
|
1531
|
+
|
|
1532
|
+
if (completed === total) {
|
|
1533
|
+
const results = job.state.get('results')!
|
|
1534
|
+
const hasAnyResult = results.some(r => r !== null)
|
|
1535
|
+
job.setState({ status: hasAnyResult ? 'completed' : 'failed' })
|
|
1536
|
+
|
|
1537
|
+
if (hasAnyResult) {
|
|
1538
|
+
job.emit('completed', results as string[])
|
|
1539
|
+
} else {
|
|
1540
|
+
job.emit('failed', errors)
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
)
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
return job
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Fan out N questions in parallel using forked assistants, return the results.
|
|
1552
|
+
* Sugar over createResearchJob — blocks until all forks complete.
|
|
1553
|
+
*
|
|
1554
|
+
* @param questions - Array of questions (strings) or objects with question + per-fork overrides
|
|
1555
|
+
* @param defaults - Default fork options applied to all forks
|
|
1556
|
+
* @returns Array of response strings, one per question
|
|
1557
|
+
*
|
|
1558
|
+
* @example
|
|
1559
|
+
* ```typescript
|
|
1560
|
+
* const results = await assistant.research([
|
|
1561
|
+
* "What are best practices for X?",
|
|
1562
|
+
* "What are common pitfalls of X?",
|
|
1563
|
+
* ], { history: 'none', model: 'gpt-4o-mini' })
|
|
1564
|
+
* ```
|
|
1565
|
+
*/
|
|
1566
|
+
async research(
|
|
1567
|
+
questions: (string | { question: string; forkOptions?: AssistantForkOptions })[],
|
|
1568
|
+
defaults: AssistantForkOptions & { prompt?: string } = {}
|
|
1569
|
+
): Promise<(string | null)[]> {
|
|
1570
|
+
const { prompt = '', ...forkDefaults } = defaults
|
|
1571
|
+
const job = await this.createResearchJob(prompt, questions, forkDefaults)
|
|
1572
|
+
await job.waitFor('completed')
|
|
1573
|
+
return job.state.get('results')!
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// -- Subagent API --
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Names of assistants available as subagents, discovered via the assistantsManager.
|
|
1580
|
+
*
|
|
1581
|
+
* @returns {string[]} Available assistant names
|
|
1582
|
+
*/
|
|
1583
|
+
get availableSubagents(): string[] {
|
|
1584
|
+
try {
|
|
1585
|
+
const manager = this.container.feature('assistantsManager')
|
|
1586
|
+
return manager.available
|
|
1587
|
+
} catch {
|
|
1588
|
+
return []
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* Get or create a subagent assistant. Uses the assistantsManager to discover
|
|
1594
|
+
* and create the assistant, then caches the instance for reuse across tool calls.
|
|
1595
|
+
*
|
|
1596
|
+
* @param id - The assistant name (e.g. 'codingAssistant')
|
|
1597
|
+
* @param options - Additional options to pass to the assistant constructor
|
|
1598
|
+
* @returns {Promise<Assistant>} The subagent assistant instance, started and ready
|
|
1599
|
+
*
|
|
1600
|
+
* @example
|
|
1601
|
+
* ```typescript
|
|
1602
|
+
* const researcher = await assistant.subagent('codingAssistant')
|
|
1603
|
+
* const answer = await researcher.ask('Find all usages of container.feature("fs")')
|
|
1604
|
+
* ```
|
|
1605
|
+
*/
|
|
1606
|
+
async subagent(id: string, options: Record<string, any> = {}): Promise<Assistant> {
|
|
1607
|
+
const subagents = (this.state.get('subagents') || {}) as Record<string, Assistant>
|
|
1608
|
+
if (subagents[id]) return subagents[id]
|
|
1609
|
+
|
|
1610
|
+
const manager = this.container.feature('assistantsManager')
|
|
1611
|
+
|
|
1612
|
+
if (!manager.state.get('discovered')) {
|
|
1613
|
+
await manager.discover()
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const instance = manager.create(id, options)
|
|
1617
|
+
await instance.start()
|
|
1618
|
+
|
|
1619
|
+
this.state.set('subagents', { ...subagents, [id]: instance })
|
|
1620
|
+
return instance
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
export default Assistant
|