luca 2.0.0 → 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 -9
- 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 -12
- 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 -58
- 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/cli.js +0 -48
- package/dist/cli/common.d.ts +0 -2
- package/dist/cli/common.js +0 -6
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -5
- package/dist/cli/run.d.ts +0 -1
- package/dist/cli/run.js +0 -38
- package/dist/core/index.d.ts +0 -4
- package/dist/core/index.js +0 -32
- package/dist/core/read.d.ts +0 -2
- package/dist/core/read.js +0 -29
- package/dist/core/request.d.ts +0 -1
- package/dist/core/request.js +0 -2
- package/dist/core/write.d.ts +0 -2
- package/dist/core/write.js +0 -21
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -5
- package/dist/utils/common.d.ts +0 -9
- package/dist/utils/common.js +0 -57
- package/dist/utils/consts.d.ts +0 -3
- package/dist/utils/consts.js +0 -11
- package/dist/utils/dict.d.ts +0 -1
- package/dist/utils/dict.js +0 -7
- package/dist/utils/index.d.ts +0 -5
- package/dist/utils/index.js +0 -21
- package/dist/utils/log.d.ts +0 -1
- package/dist/utils/log.js +0 -5
- package/dist/utils/types.d.ts +0 -1
- package/dist/utils/types.js +0 -2
- package/dist/utils/utils.test.d.ts +0 -1
- package/dist/utils/utils.test.js +0 -7
|
@@ -0,0 +1,1323 @@
|
|
|
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 { OpenAIClient } from '../../clients/openai';
|
|
6
|
+
import type OpenAI from 'openai';
|
|
7
|
+
import type { ConversationHistory } from './conversation-history';
|
|
8
|
+
import { countMessageTokens, getContextWindow } from '../lib/token-counter.js';
|
|
9
|
+
|
|
10
|
+
declare module '@soederpop/luca/feature' {
|
|
11
|
+
interface AvailableFeatures {
|
|
12
|
+
conversation: typeof Conversation
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Message = OpenAI.Chat.Completions.ChatCompletionMessageParam
|
|
17
|
+
|
|
18
|
+
export type ContentPart =
|
|
19
|
+
| { type: 'text'; text: string }
|
|
20
|
+
| { type: 'image_url'; image_url: { url: string; detail?: 'low' | 'high' | 'auto' } }
|
|
21
|
+
| { type: 'input_audio'; data: string; format: 'mp3' | 'wav' }
|
|
22
|
+
| { type: 'input_file'; file_data: string; filename: string }
|
|
23
|
+
|
|
24
|
+
export interface ConversationTool {
|
|
25
|
+
handler: (...args: any[]) => Promise<any>
|
|
26
|
+
description: string
|
|
27
|
+
parameters: Record<string, any>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ConversationMCPServer {
|
|
31
|
+
url: string
|
|
32
|
+
headers?: Record<string, string>
|
|
33
|
+
allowedTools?: string[] | { tool_names?: string[] }
|
|
34
|
+
requireApproval?: 'always' | 'never' | {
|
|
35
|
+
always?: { tool_names?: string[] }
|
|
36
|
+
never?: { tool_names?: string[] }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const ConversationOptionsSchema = FeatureOptionsSchema.extend({
|
|
41
|
+
/** A unique identifier for the conversation */
|
|
42
|
+
id: z.string().optional().describe('A unique identifier for the conversation'),
|
|
43
|
+
/** A human-readable title for the conversation */
|
|
44
|
+
title: z.string().optional().describe('A human-readable title for the conversation'),
|
|
45
|
+
/** A unique identifier for threads, an arbitrary grouping mechanism */
|
|
46
|
+
thread: z.string().optional().describe('A unique identifier for threads, an arbitrary grouping mechanism'),
|
|
47
|
+
/** Any available OpenAI model */
|
|
48
|
+
model: z.string().optional().describe('Any available OpenAI model'),
|
|
49
|
+
/** Initial message history to seed the conversation */
|
|
50
|
+
history: z.array(z.any()).optional().describe('Initial message history to seed the conversation'),
|
|
51
|
+
/** Tools the model can call during conversation */
|
|
52
|
+
tools: z.record(z.string(), z.any()).optional().describe('Tools the model can call during conversation'),
|
|
53
|
+
/** Remote MCP servers to expose as tools when using the OpenAI Responses API */
|
|
54
|
+
mcpServers: z.record(z.string(), z.any()).optional().describe('Remote MCP servers keyed by server label'),
|
|
55
|
+
/** Which OpenAI API to use for completions */
|
|
56
|
+
api: z.enum(['auto', 'responses', 'chat']).optional().describe('Completion API mode. auto uses Responses unless local=true'),
|
|
57
|
+
/** Tags for categorizing and searching this conversation */
|
|
58
|
+
tags: z.array(z.string()).optional().describe('Tags for categorizing and searching this conversation'),
|
|
59
|
+
/** Arbitrary metadata to attach to this conversation */
|
|
60
|
+
metadata: z.record(z.string(), z.any()).optional().describe('Arbitrary metadata to attach to this conversation'),
|
|
61
|
+
|
|
62
|
+
clientOptions: z.record(z.string(), z.any()).optional().describe('Options for the OpenAI client'), // the type of options for OpenAI client
|
|
63
|
+
|
|
64
|
+
local: z.boolean().optional().describe('Whether to use the local ollama models instead of the remote OpenAI models'),
|
|
65
|
+
|
|
66
|
+
/** Maximum number of output tokens per completion */
|
|
67
|
+
maxTokens: z.number().optional().describe('Maximum number of output tokens per completion (default 512)'),
|
|
68
|
+
|
|
69
|
+
/** Sampling temperature (0-2). Higher = more random, lower = more deterministic. */
|
|
70
|
+
temperature: z.number().min(0).max(2).optional().describe('Sampling temperature (0-2). Higher = more random, lower = more deterministic'),
|
|
71
|
+
/** Nucleus sampling: only consider tokens with top_p cumulative probability (0-1). */
|
|
72
|
+
topP: z.number().min(0).max(1).optional().describe('Nucleus sampling cutoff (0-1). Lower = more focused'),
|
|
73
|
+
/** Top-K sampling: only consider the K most likely tokens. Not supported by OpenAI — used with local/Anthropic models. */
|
|
74
|
+
topK: z.number().optional().describe('Top-K sampling. Only supported by local/Anthropic models'),
|
|
75
|
+
/** Penalizes tokens based on how often they already appeared (-2 to 2). */
|
|
76
|
+
frequencyPenalty: z.number().min(-2).max(2).optional().describe('Frequency penalty (-2 to 2). Positive = discourage repetition'),
|
|
77
|
+
/** Penalizes tokens based on whether they appeared at all (-2 to 2). */
|
|
78
|
+
presencePenalty: z.number().min(-2).max(2).optional().describe('Presence penalty (-2 to 2). Positive = encourage new topics'),
|
|
79
|
+
/** Stop sequences — model stops generating when it encounters any of these strings. */
|
|
80
|
+
stop: z.array(z.string()).optional().describe('Stop sequences — generation halts when any of these strings is produced'),
|
|
81
|
+
|
|
82
|
+
/** Enable automatic compaction when estimated input tokens approach the context limit */
|
|
83
|
+
autoCompact: z.boolean().optional().describe('Enable automatic compaction when input tokens approach the context limit'),
|
|
84
|
+
/** Fraction of contextWindow at which auto-compact triggers (0.0–1.0, default 0.8) */
|
|
85
|
+
compactThreshold: z.number().min(0).max(1).optional().describe('Fraction of context window at which auto-compact triggers (default 0.8)'),
|
|
86
|
+
/** Override the inferred context window size for this model */
|
|
87
|
+
contextWindow: z.number().optional().describe('Override the inferred context window size for this model'),
|
|
88
|
+
/** Number of recent messages to preserve after compaction (default 4) */
|
|
89
|
+
compactKeepRecent: z.number().optional().describe('Number of recent messages to preserve after compaction (default 4)'),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
export const ConversationStateSchema = FeatureStateSchema.extend({
|
|
93
|
+
id: z.string().describe('Unique identifier for this conversation instance'),
|
|
94
|
+
thread: z.string().describe('Thread identifier for grouping conversations'),
|
|
95
|
+
model: z.string().describe('The OpenAI model being used'),
|
|
96
|
+
messages: z.array(z.any()).describe('Full message history of the conversation'),
|
|
97
|
+
streaming: z.boolean().describe('Whether a streaming response is currently in progress'),
|
|
98
|
+
lastResponse: z.string().describe('The last assistant response text'),
|
|
99
|
+
toolCalls: z.number().describe('Total number of tool calls made in this conversation'),
|
|
100
|
+
api: z.enum(['responses', 'chat']).describe('Which completion API is active for this conversation'),
|
|
101
|
+
lastResponseId: z.string().nullable().describe('Most recent OpenAI Responses API response ID for continuing conversation state'),
|
|
102
|
+
tokenUsage: z.object({
|
|
103
|
+
prompt: z.number().describe('Total prompt tokens consumed'),
|
|
104
|
+
completion: z.number().describe('Total completion tokens consumed'),
|
|
105
|
+
total: z.number().describe('Total tokens consumed'),
|
|
106
|
+
}).describe('Cumulative token usage statistics'),
|
|
107
|
+
estimatedInputTokens: z.number().describe('Estimated input token count for the current messages array'),
|
|
108
|
+
compactionCount: z.number().describe('Number of times compact() has been called'),
|
|
109
|
+
contextWindow: z.number().describe('The context window size for the current model'),
|
|
110
|
+
tools: z.record(z.string(), z.any()).describe('Active tools map including any runtime overrides'),
|
|
111
|
+
callMaxTokens: z.number().nullable().describe('Per-call max tokens override, cleared after each ask()'),
|
|
112
|
+
|
|
113
|
+
/** Sampling parameters — state is the runtime source of truth, seeded from options at construction. */
|
|
114
|
+
temperature: z.number().nullable().describe('Sampling temperature (0-2). Null means use model default'),
|
|
115
|
+
topP: z.number().nullable().describe('Nucleus sampling cutoff (0-1). Null means use model default'),
|
|
116
|
+
topK: z.number().nullable().describe('Top-K sampling. Null means use model default'),
|
|
117
|
+
frequencyPenalty: z.number().nullable().describe('Frequency penalty (-2 to 2). Null means use model default'),
|
|
118
|
+
presencePenalty: z.number().nullable().describe('Presence penalty (-2 to 2). Null means use model default'),
|
|
119
|
+
stop: z.array(z.string()).nullable().describe('Stop sequences. Null means none'),
|
|
120
|
+
maxTokens: z.number().nullable().describe('Maximum output tokens per completion. Null means use model default'),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
export class ConversationAbortError extends Error {
|
|
124
|
+
/** The partial text accumulated before the abort. */
|
|
125
|
+
readonly partial: string
|
|
126
|
+
|
|
127
|
+
constructor(partial: string) {
|
|
128
|
+
super('Conversation aborted')
|
|
129
|
+
this.name = 'ConversationAbortError'
|
|
130
|
+
this.partial = partial
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const ConversationEventsSchema = FeatureEventsSchema.extend({
|
|
135
|
+
userMessage: z.tuple([z.any().describe('The user message content (string or ContentPart[])')]).describe('Fired when a user message is added to the conversation'),
|
|
136
|
+
aborted: z.tuple([z.string().describe('Partial text accumulated before the abort')]).describe('Fired when the conversation is aborted mid-response'),
|
|
137
|
+
turnStart: z.tuple([z.object({ turn: z.number(), isFollowUp: z.boolean() })]).describe('Fired at the start of each completion turn'),
|
|
138
|
+
turnEnd: z.tuple([z.object({ turn: z.number(), hasToolCalls: z.boolean() })]).describe('Fired at the end of each completion turn'),
|
|
139
|
+
toolCallsStart: z.tuple([z.any().describe('Array of tool call objects from the model')]).describe('Fired when the model begins a batch of tool calls'),
|
|
140
|
+
toolCall: z.tuple([z.string().describe('Tool name'), z.any().describe('Parsed arguments object')]).describe('Fired before invoking a single tool handler'),
|
|
141
|
+
toolResult: z.tuple([z.string().describe('Tool name'), z.string().describe('Serialized result')]).describe('Fired after a tool handler returns successfully'),
|
|
142
|
+
toolError: z.tuple([z.string().describe('Tool name'), z.any().describe('Error object or message')]).describe('Fired when a tool handler throws or the tool is unknown'),
|
|
143
|
+
toolCallsEnd: z.tuple([]).describe('Fired after all tool calls in a turn have been executed'),
|
|
144
|
+
chunk: z.tuple([z.string().describe('Text delta from the stream')]).describe('Fired for each streaming text delta'),
|
|
145
|
+
preview: z.tuple([z.string().describe('Accumulated text so far')]).describe('Fired after each chunk with the full accumulated text'),
|
|
146
|
+
response: z.tuple([z.string().describe('Final accumulated response text')]).describe('Fired when the final text response is produced'),
|
|
147
|
+
responseCompleted: z.tuple([z.any().describe('The completed OpenAI Response object')]).describe('Fired when the Responses API stream completes'),
|
|
148
|
+
rawEvent: z.tuple([z.any().describe('Raw stream event from the API')]).describe('Fired for every raw event from the Responses API stream'),
|
|
149
|
+
mcpEvent: z.tuple([z.any().describe('MCP-related stream event')]).describe('Fired for MCP-related events from the Responses API'),
|
|
150
|
+
summarizeStart: z.tuple([]).describe('Fired before generating a conversation summary'),
|
|
151
|
+
summarizeEnd: z.tuple([z.string().describe('The generated summary text')]).describe('Fired after the summary is generated'),
|
|
152
|
+
compactStart: z.tuple([z.object({ messageCount: z.number(), keepRecent: z.number() })]).describe('Fired before compacting the conversation history'),
|
|
153
|
+
compactEnd: z.tuple([z.object({ summary: z.string(), removedCount: z.number(), estimatedTokens: z.number(), compactionCount: z.number() })]).describe('Fired after compaction completes'),
|
|
154
|
+
autoCompactTriggered: z.tuple([z.object({ estimated: z.number(), limit: z.number(), contextWindow: z.number() })]).describe('Fired when auto-compact kicks in because tokens exceeded the threshold'),
|
|
155
|
+
}).describe('Conversation events')
|
|
156
|
+
|
|
157
|
+
export type ConversationOptions = z.infer<typeof ConversationOptionsSchema>
|
|
158
|
+
export type ConversationState = z.infer<typeof ConversationStateSchema>
|
|
159
|
+
|
|
160
|
+
export type AskOptions = {
|
|
161
|
+
maxTokens?: number
|
|
162
|
+
/**
|
|
163
|
+
* When provided, enables OpenAI Structured Outputs. The model is constrained
|
|
164
|
+
* to return JSON matching this Zod schema. The return value of ask() will be
|
|
165
|
+
* the parsed object instead of a raw string.
|
|
166
|
+
*/
|
|
167
|
+
schema?: z.ZodType
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export type ForkOptions = Omit<Partial<ConversationOptions>, 'history'> & {
|
|
171
|
+
/**
|
|
172
|
+
* Controls how much message history carries over to the fork.
|
|
173
|
+
* - `'full'` (default) — deep copy all messages
|
|
174
|
+
* - `'none'` — system prompt only, no chat history
|
|
175
|
+
* - `number` — keep system prompt + last N user/assistant exchanges
|
|
176
|
+
*/
|
|
177
|
+
history?: 'full' | 'none' | number
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Recursively set `additionalProperties: false` on every object-type node
|
|
182
|
+
* in a JSON Schema tree. OpenAI strict mode requires this at every level.
|
|
183
|
+
* Also ensures every object has a `required` array listing all its property keys.
|
|
184
|
+
*/
|
|
185
|
+
function strictifySchema(schema: Record<string, any>): Record<string, any> {
|
|
186
|
+
const clone = { ...schema }
|
|
187
|
+
|
|
188
|
+
if (clone.type === 'object' && clone.properties) {
|
|
189
|
+
clone.additionalProperties = false
|
|
190
|
+
clone.required = Object.keys(clone.properties)
|
|
191
|
+
const props: Record<string, any> = {}
|
|
192
|
+
for (const [key, val] of Object.entries(clone.properties)) {
|
|
193
|
+
props[key] = strictifySchema(val as Record<string, any>)
|
|
194
|
+
}
|
|
195
|
+
clone.properties = props
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (clone.items) {
|
|
199
|
+
clone.items = strictifySchema(clone.items)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// anyOf / oneOf / allOf
|
|
203
|
+
for (const combiner of ['anyOf', 'oneOf', 'allOf'] as const) {
|
|
204
|
+
if (Array.isArray(clone[combiner])) {
|
|
205
|
+
clone[combiner] = clone[combiner].map((s: Record<string, any>) => strictifySchema(s))
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return clone
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A self-contained conversation with OpenAI that supports streaming,
|
|
214
|
+
* tool calling, and message state management.
|
|
215
|
+
*
|
|
216
|
+
* @extends Feature
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```typescript
|
|
220
|
+
* const conversation = container.feature('conversation', {
|
|
221
|
+
* model: 'gpt-4.1',
|
|
222
|
+
* tools: myToolMap,
|
|
223
|
+
* history: [{ role: 'system', content: 'You are a helpful assistant.' }]
|
|
224
|
+
* })
|
|
225
|
+
* const reply = await conversation.ask('What is the meaning of life?')
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
export class Conversation extends Feature<ConversationState, ConversationOptions> {
|
|
229
|
+
static override stateSchema = ConversationStateSchema
|
|
230
|
+
static override optionsSchema = ConversationOptionsSchema
|
|
231
|
+
static override eventsSchema = ConversationEventsSchema
|
|
232
|
+
static override shortcut = 'features.conversation' as const
|
|
233
|
+
|
|
234
|
+
static { Feature.register(this, 'conversation') }
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Pluggable tool executor. Called for each tool invocation with the tool
|
|
238
|
+
* name, parsed args, and the default handler. Return the serialized result string.
|
|
239
|
+
* The Assistant replaces this to wire in beforeToolCall/afterToolCall interceptors.
|
|
240
|
+
*/
|
|
241
|
+
toolExecutor: ((name: string, args: Record<string, any>, handler: (...args: any[]) => Promise<any>) => Promise<string>) | null = null
|
|
242
|
+
|
|
243
|
+
/** The active structured output schema for the current ask() call, if any. */
|
|
244
|
+
private _activeSchema: z.ZodType | null = null
|
|
245
|
+
|
|
246
|
+
/** AbortController for the current ask() call, if any. */
|
|
247
|
+
private _abortController: AbortController | null = null
|
|
248
|
+
|
|
249
|
+
/** Registered stubs: matched against user input to short-circuit the API with a canned response. */
|
|
250
|
+
private _stubs: Array<{ matcher: string | RegExp; response: string | (() => string) }> = []
|
|
251
|
+
|
|
252
|
+
/** Resolved max tokens: per-call override > state-level. Undefined means no limit (model default). */
|
|
253
|
+
private get maxTokens(): number | undefined {
|
|
254
|
+
return (this.state.get('callMaxTokens') as number | null) ?? (this.state.get('maxTokens') as number | null) ?? undefined
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** @returns Default state seeded from options: id, thread, model, initial history, and zero token usage. */
|
|
258
|
+
override get initialState(): ConversationState {
|
|
259
|
+
return {
|
|
260
|
+
...super.initialState,
|
|
261
|
+
id: this.options.id || this.uuid,
|
|
262
|
+
thread: this.options.thread || 'default',
|
|
263
|
+
model: this.options.model || 'gpt-5',
|
|
264
|
+
messages: this.options.history || [],
|
|
265
|
+
streaming: false,
|
|
266
|
+
lastResponse: '',
|
|
267
|
+
toolCalls: 0,
|
|
268
|
+
api: this.apiMode,
|
|
269
|
+
lastResponseId: null,
|
|
270
|
+
tokenUsage: { prompt: 0, completion: 0, total: 0 },
|
|
271
|
+
estimatedInputTokens: 0,
|
|
272
|
+
compactionCount: 0,
|
|
273
|
+
contextWindow: this.options.contextWindow || getContextWindow(this.options.model || 'gpt-5'),
|
|
274
|
+
tools: (this.options.tools || {}) as Record<string, ConversationTool>,
|
|
275
|
+
callMaxTokens: null,
|
|
276
|
+
temperature: this.options.temperature ?? null,
|
|
277
|
+
topP: this.options.topP ?? null,
|
|
278
|
+
topK: this.options.topK ?? null,
|
|
279
|
+
frequencyPenalty: this.options.frequencyPenalty ?? null,
|
|
280
|
+
presencePenalty: this.options.presencePenalty ?? null,
|
|
281
|
+
stop: this.options.stop ?? null,
|
|
282
|
+
maxTokens: this.options.maxTokens ?? null,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Returns the registered tools available for the model to call. */
|
|
287
|
+
get tools() : Record<string, ConversationTool> {
|
|
288
|
+
return (this.state.get('tools') || {}) as Record<string, ConversationTool>
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
get availableTools() {
|
|
292
|
+
return Object.keys(this.tools)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Add or replace a single tool by name.
|
|
297
|
+
* Uses the same format as tools passed at construction time.
|
|
298
|
+
*/
|
|
299
|
+
addTool(name: string, tool: ConversationTool): this {
|
|
300
|
+
this.state.set('tools', { ...this.tools, [name]: tool })
|
|
301
|
+
return this
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Remove a tool by name.
|
|
306
|
+
*/
|
|
307
|
+
removeTool(name: string): this {
|
|
308
|
+
const current = { ...this.tools }
|
|
309
|
+
delete current[name]
|
|
310
|
+
this.state.set('tools', current)
|
|
311
|
+
return this
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Merge new tools into the conversation, replacing any with the same name.
|
|
316
|
+
* Accepts the same Record<string, ConversationTool> format used at construction time.
|
|
317
|
+
*/
|
|
318
|
+
updateTools(tools: Record<string, ConversationTool>): this {
|
|
319
|
+
this.state.set('tools', { ...this.tools, ...tools })
|
|
320
|
+
return this
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Register a hardcoded stub response that bypasses the API when the user's message matches.
|
|
325
|
+
* Streaming is still simulated — chunk/preview events fire word-by-word.
|
|
326
|
+
*
|
|
327
|
+
* @param matcher - Exact string match, substring, or RegExp tested against user input
|
|
328
|
+
* @param response - The text to stream back, or a zero-arg function that returns it
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* conversation.stub('hello', 'Hi there!')
|
|
332
|
+
* conversation.stub(/weather/i, () => 'Sunny and 72°F.')
|
|
333
|
+
*/
|
|
334
|
+
stub(matcher: string | RegExp, response: string | (() => string)): this {
|
|
335
|
+
this._stubs.push({ matcher, response })
|
|
336
|
+
return this
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Returns configured remote MCP servers keyed by server label. */
|
|
340
|
+
get mcpServers(): Record<string, ConversationMCPServer> {
|
|
341
|
+
return (this.options.mcpServers || {}) as Record<string, ConversationMCPServer>
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Returns the full message history of the conversation. */
|
|
345
|
+
get messages(): Message[] {
|
|
346
|
+
return this.state.get('messages') || []
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Fork the conversation into a new independent instance.
|
|
351
|
+
* The fork inherits the same system prompt, tools, and message history,
|
|
352
|
+
* but has its own identity and state — changes in either direction do not affect the other.
|
|
353
|
+
*
|
|
354
|
+
* @param overrides - Option overrides for the forked conversation. Supports a `history` field
|
|
355
|
+
* that controls how much context carries over:
|
|
356
|
+
* - `'full'` (default) — deep copy all messages
|
|
357
|
+
* - `'none'` — system prompt only, no chat history
|
|
358
|
+
* - `number` — keep the system prompt plus the last N user/assistant exchanges
|
|
359
|
+
*
|
|
360
|
+
* When called with an array, creates multiple independent forks in one call.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* ```typescript
|
|
364
|
+
* // Full context fork
|
|
365
|
+
* const fork = conversation.fork()
|
|
366
|
+
*
|
|
367
|
+
* // System prompt only — cheapest
|
|
368
|
+
* const lean = conversation.fork({ history: 'none', model: 'gpt-4o-mini' })
|
|
369
|
+
*
|
|
370
|
+
* // Last 3 exchanges + system prompt
|
|
371
|
+
* const recent = conversation.fork({ history: 3 })
|
|
372
|
+
*
|
|
373
|
+
* // Multiple forks at once
|
|
374
|
+
* const [a, b, c] = conversation.fork([
|
|
375
|
+
* { history: 'none' },
|
|
376
|
+
* { history: 'none' },
|
|
377
|
+
* { history: 5 },
|
|
378
|
+
* ])
|
|
379
|
+
* ```
|
|
380
|
+
*/
|
|
381
|
+
fork(overrides?: ForkOptions): Conversation
|
|
382
|
+
fork(overrides?: ForkOptions[]): Conversation[]
|
|
383
|
+
fork(overrides: ForkOptions | ForkOptions[] = {}): Conversation | Conversation[] {
|
|
384
|
+
if (Array.isArray(overrides)) {
|
|
385
|
+
return overrides.map(o => this.fork(o))
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const { history: historyMode = 'full', ...convOverrides } = overrides
|
|
389
|
+
const allMessages = JSON.parse(JSON.stringify(this.messages)) as Message[]
|
|
390
|
+
|
|
391
|
+
let history: Message[]
|
|
392
|
+
if (historyMode === 'none') {
|
|
393
|
+
// System prompt only
|
|
394
|
+
const systemMsg = allMessages.find(m => m.role === 'system' || m.role === 'developer')
|
|
395
|
+
history = systemMsg ? [systemMsg] : []
|
|
396
|
+
} else if (historyMode === 'full') {
|
|
397
|
+
history = allMessages
|
|
398
|
+
} else {
|
|
399
|
+
// Keep last N exchanges (user + assistant pairs) plus system prompt
|
|
400
|
+
const systemMsg = allMessages.find(m => m.role === 'system' || m.role === 'developer')
|
|
401
|
+
const nonSystem = allMessages.filter(m => m.role !== 'system' && m.role !== 'developer')
|
|
402
|
+
|
|
403
|
+
// Walk backwards counting user messages as exchange boundaries.
|
|
404
|
+
// An exchange starts at a user message and includes everything after it
|
|
405
|
+
// until the next user message (assistant replies, tool calls, etc.).
|
|
406
|
+
let exchangeCount = 0
|
|
407
|
+
let cutoff = 0
|
|
408
|
+
for (let i = nonSystem.length - 1; i >= 0; i--) {
|
|
409
|
+
if (nonSystem[i]!.role === 'user') {
|
|
410
|
+
exchangeCount++
|
|
411
|
+
if (exchangeCount > historyMode) break
|
|
412
|
+
cutoff = i
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const kept = nonSystem.slice(cutoff)
|
|
417
|
+
history = systemMsg ? [systemMsg, ...kept] : kept
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const forked = this.container.feature('conversation', {
|
|
421
|
+
...this.options,
|
|
422
|
+
id: undefined,
|
|
423
|
+
history,
|
|
424
|
+
tools: { ...this.tools },
|
|
425
|
+
...convOverrides,
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// Copy stubs so forked conversations match the same patterns
|
|
429
|
+
;(forked as any)._stubs = [...this._stubs]
|
|
430
|
+
|
|
431
|
+
return forked
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Fan out N questions in parallel using forked conversations, return the results.
|
|
436
|
+
* Each fork is independent and ephemeral — no history is saved.
|
|
437
|
+
*
|
|
438
|
+
* @param questions - Array of questions (strings) or objects with question + per-fork overrides
|
|
439
|
+
* @param defaults - Default fork options applied to all forks (individual overrides take precedence)
|
|
440
|
+
* @returns Array of response strings, one per question
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```typescript
|
|
444
|
+
* const results = await conversation.research([
|
|
445
|
+
* "What are the pros of approach A?",
|
|
446
|
+
* "What are the pros of approach B?",
|
|
447
|
+
* ], { history: 'none', model: 'gpt-4o-mini' })
|
|
448
|
+
*
|
|
449
|
+
* // Per-fork overrides
|
|
450
|
+
* const results = await conversation.research([
|
|
451
|
+
* "Quick factual question",
|
|
452
|
+
* { question: "Needs recent context", forkOptions: { history: 5 } },
|
|
453
|
+
* ], { history: 'none' })
|
|
454
|
+
* ```
|
|
455
|
+
*/
|
|
456
|
+
async research(
|
|
457
|
+
questions: (string | { question: string; forkOptions?: ForkOptions })[],
|
|
458
|
+
defaults: ForkOptions = {}
|
|
459
|
+
): Promise<string[]> {
|
|
460
|
+
const forkConfigs = questions.map(q => ({
|
|
461
|
+
...defaults,
|
|
462
|
+
...(typeof q === 'string' ? {} : q.forkOptions),
|
|
463
|
+
}))
|
|
464
|
+
|
|
465
|
+
const forks = this.fork(forkConfigs)
|
|
466
|
+
|
|
467
|
+
return Promise.all(
|
|
468
|
+
forks.map((fork, i) => {
|
|
469
|
+
const q = questions[i]!
|
|
470
|
+
const question = typeof q === 'string' ? q : q.question
|
|
471
|
+
return fork.ask(question)
|
|
472
|
+
})
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Returns the OpenAI model name being used for completions. */
|
|
477
|
+
get model(): string {
|
|
478
|
+
return this.state.get('model')!
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Returns the active completion API mode after resolving auto/local behavior. */
|
|
482
|
+
get apiMode(): 'responses' | 'chat' {
|
|
483
|
+
const mode = this.options.api || 'auto'
|
|
484
|
+
if (mode === 'chat' || mode === 'responses') return mode
|
|
485
|
+
return this.options.local ? 'chat' : 'responses'
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Whether a streaming response is currently in progress. */
|
|
489
|
+
get isStreaming(): boolean {
|
|
490
|
+
return !!this.state.get('streaming')
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Abort the current ask() call. Cancels the in-flight network request and
|
|
495
|
+
* any pending tool executions. The ask() promise will reject with a
|
|
496
|
+
* ConversationAbortError whose `partial` property contains any text
|
|
497
|
+
* accumulated before the abort.
|
|
498
|
+
*/
|
|
499
|
+
abort(): void {
|
|
500
|
+
this._abortController?.abort()
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Returns the correct parameter name for limiting output tokens.
|
|
505
|
+
* Local models (LM Studio, Ollama) and legacy OpenAI models use max_tokens.
|
|
506
|
+
* Newer OpenAI models (gpt-4o+, gpt-4.1, gpt-5, o1, o3, o4) require max_completion_tokens.
|
|
507
|
+
*/
|
|
508
|
+
private get maxTokensParam(): 'max_tokens' | 'max_completion_tokens' {
|
|
509
|
+
if (this.options.local) return 'max_tokens'
|
|
510
|
+
|
|
511
|
+
const model = this.model
|
|
512
|
+
const needsCompletionTokens = [
|
|
513
|
+
'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4',
|
|
514
|
+
]
|
|
515
|
+
|
|
516
|
+
if (needsCompletionTokens.some((prefix) => model.startsWith(prefix))) {
|
|
517
|
+
return 'max_completion_tokens'
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return 'max_tokens'
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** The context window size for the current model (from options override or auto-detected). */
|
|
524
|
+
get contextWindow(): number {
|
|
525
|
+
return this.options.contextWindow || getContextWindow(this.model)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Whether the conversation is approaching the context limit. */
|
|
529
|
+
get isNearContextLimit(): boolean {
|
|
530
|
+
const threshold = this.options.compactThreshold ?? 0.8
|
|
531
|
+
return this.estimateTokens() >= this.contextWindow * threshold
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Estimate the input token count for the current messages array
|
|
536
|
+
* using the js-tiktoken tokenizer. Updates state.
|
|
537
|
+
*/
|
|
538
|
+
estimateTokens(): number {
|
|
539
|
+
const count = countMessageTokens(this.messages, this.model)
|
|
540
|
+
this.state.set('estimatedInputTokens', count)
|
|
541
|
+
return count
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Generate a summary of the conversation so far using the LLM.
|
|
546
|
+
* Read-only — does not modify messages.
|
|
547
|
+
*/
|
|
548
|
+
async summarize(): Promise<string> {
|
|
549
|
+
this.emit('summarizeStart')
|
|
550
|
+
|
|
551
|
+
const transcript = this.messages
|
|
552
|
+
.map(m => {
|
|
553
|
+
const role = m.role
|
|
554
|
+
const content = typeof m.content === 'string'
|
|
555
|
+
? m.content
|
|
556
|
+
: Array.isArray(m.content)
|
|
557
|
+
? (m.content as any[]).filter((p: any) => p.type === 'text').map((p: any) => p.text).join('\n')
|
|
558
|
+
: (m.content != null ? JSON.stringify(m.content) : '(no content)')
|
|
559
|
+
return `[${role}]: ${content || '(no text content)'}`
|
|
560
|
+
})
|
|
561
|
+
.join('\n\n')
|
|
562
|
+
|
|
563
|
+
const response = await this.openai.raw.chat.completions.create({
|
|
564
|
+
model: this.model,
|
|
565
|
+
messages: [
|
|
566
|
+
{
|
|
567
|
+
role: 'system',
|
|
568
|
+
content: 'You are a conversation summarizer. Produce a concise but comprehensive summary of the following conversation. Preserve all key facts, decisions, context, user preferences, and any important details needed to continue the conversation. Output only the summary.',
|
|
569
|
+
},
|
|
570
|
+
{ role: 'user', content: transcript },
|
|
571
|
+
],
|
|
572
|
+
stream: false,
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
const summary = (response as any).choices?.[0]?.message?.content || ''
|
|
576
|
+
this.emit('summarizeEnd', summary)
|
|
577
|
+
return summary
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Compact the conversation by summarizing old messages and replacing them
|
|
582
|
+
* with a summary message. Keeps the system message (if any) and the most
|
|
583
|
+
* recent N messages.
|
|
584
|
+
*/
|
|
585
|
+
async compact(options?: { keepRecent?: number }): Promise<{ summary: string; removedCount: number; estimatedTokens: number }> {
|
|
586
|
+
const keepRecent = options?.keepRecent ?? this.options.compactKeepRecent ?? 4
|
|
587
|
+
const messages = this.messages
|
|
588
|
+
|
|
589
|
+
if (messages.length <= keepRecent + 1) {
|
|
590
|
+
return { summary: '', removedCount: 0, estimatedTokens: this.estimateTokens() }
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
this.emit('compactStart', { messageCount: messages.length, keepRecent })
|
|
594
|
+
|
|
595
|
+
const summary = await this.summarize()
|
|
596
|
+
|
|
597
|
+
const systemMessage = (messages[0]?.role === 'system' || messages[0]?.role === 'developer')
|
|
598
|
+
? messages[0]
|
|
599
|
+
: null
|
|
600
|
+
|
|
601
|
+
const recentMessages = messages.slice(-keepRecent)
|
|
602
|
+
|
|
603
|
+
const newMessages: Message[] = []
|
|
604
|
+
if (systemMessage) newMessages.push(systemMessage)
|
|
605
|
+
|
|
606
|
+
newMessages.push({
|
|
607
|
+
role: 'developer',
|
|
608
|
+
content: `[Conversation Summary — the following is a summary of the earlier conversation that has been compacted to save context space]\n\n${summary}`,
|
|
609
|
+
} as Message)
|
|
610
|
+
|
|
611
|
+
newMessages.push(...recentMessages)
|
|
612
|
+
|
|
613
|
+
const removedCount = messages.length - newMessages.length
|
|
614
|
+
this.state.set('messages', newMessages)
|
|
615
|
+
this.state.set('compactionCount', (this.state.get('compactionCount') || 0) + 1)
|
|
616
|
+
|
|
617
|
+
// Responses API: clear continuation chain since message history changed
|
|
618
|
+
if (this.apiMode === 'responses') {
|
|
619
|
+
this.state.set('lastResponseId', null)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const estimatedTokens = this.estimateTokens()
|
|
623
|
+
|
|
624
|
+
this.emit('compactEnd', { summary, removedCount, estimatedTokens, compactionCount: this.state.get('compactionCount') })
|
|
625
|
+
|
|
626
|
+
return { summary, removedCount, estimatedTokens }
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Get the OpenAI-formatted tools array from the registered tools.
|
|
631
|
+
*
|
|
632
|
+
* @returns {OpenAI.Chat.Completions.ChatCompletionTool[]} The tools formatted for OpenAI
|
|
633
|
+
*/
|
|
634
|
+
private get openaiTools(): OpenAI.Chat.Completions.ChatCompletionTool[] {
|
|
635
|
+
return Object.entries(this.tools).map(([name, tool]) => ({
|
|
636
|
+
type: 'function' as const,
|
|
637
|
+
function: {
|
|
638
|
+
name,
|
|
639
|
+
description: tool.description,
|
|
640
|
+
parameters: tool.parameters
|
|
641
|
+
}
|
|
642
|
+
}))
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Get the OpenAI Responses-formatted tools array from local function tools
|
|
647
|
+
* plus configured remote MCP servers.
|
|
648
|
+
*/
|
|
649
|
+
private get responseTools(): OpenAI.Responses.Tool[] {
|
|
650
|
+
const functionTools = Object.entries(this.tools).map(([name, tool]) => ({
|
|
651
|
+
type: 'function' as const,
|
|
652
|
+
name,
|
|
653
|
+
description: tool.description,
|
|
654
|
+
parameters: { ...tool.parameters, additionalProperties: false },
|
|
655
|
+
strict: true,
|
|
656
|
+
}))
|
|
657
|
+
|
|
658
|
+
const mcpTools = Object.entries(this.mcpServers)
|
|
659
|
+
.filter(([, server]) => !!server?.url)
|
|
660
|
+
.map(([serverLabel, server]) => ({
|
|
661
|
+
type: 'mcp' as const,
|
|
662
|
+
server_label: serverLabel,
|
|
663
|
+
server_url: server.url,
|
|
664
|
+
...(server.headers ? { headers: server.headers } : {}),
|
|
665
|
+
...(server.allowedTools ? { allowed_tools: server.allowedTools } : {}),
|
|
666
|
+
...(server.requireApproval ? { require_approval: server.requireApproval } : {}),
|
|
667
|
+
}))
|
|
668
|
+
|
|
669
|
+
return [...functionTools, ...mcpTools]
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Returns the first system/developer text message to use as Responses instructions. */
|
|
673
|
+
private get responsesInstructions(): string | undefined {
|
|
674
|
+
for (const message of this.messages) {
|
|
675
|
+
if ((message.role === 'system' || message.role === 'developer') && typeof message.content === 'string') {
|
|
676
|
+
return message.content
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return undefined
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Send a message and get a streamed response. Automatically handles
|
|
684
|
+
* tool calls by invoking the registered handlers and feeding results
|
|
685
|
+
* back to the model until a final text response is produced.
|
|
686
|
+
*
|
|
687
|
+
* @param {string | ContentPart[]} content - The user message, either a string or array of content parts (text + images)
|
|
688
|
+
* @returns {Promise<string>} The assistant's final text response
|
|
689
|
+
*
|
|
690
|
+
* @example
|
|
691
|
+
* const reply = await conversation.ask("What's the weather in SF?")
|
|
692
|
+
* // With image:
|
|
693
|
+
* const reply = await conversation.ask([
|
|
694
|
+
* { type: 'text', text: 'What is in this diagram?' },
|
|
695
|
+
* { type: 'image_url', image_url: { url: 'data:image/png;base64,...' } }
|
|
696
|
+
* ])
|
|
697
|
+
*/
|
|
698
|
+
async ask(content: string | ContentPart[], options?: AskOptions): Promise<string> {
|
|
699
|
+
this.state.set('callMaxTokens', options?.maxTokens ?? null)
|
|
700
|
+
this._activeSchema = options?.schema ?? null
|
|
701
|
+
this._abortController = new AbortController()
|
|
702
|
+
|
|
703
|
+
// Auto-compact before adding the new message
|
|
704
|
+
if (this.options.autoCompact) {
|
|
705
|
+
const threshold = this.options.compactThreshold ?? 0.8
|
|
706
|
+
const estimated = this.estimateTokens()
|
|
707
|
+
const limit = this.contextWindow * threshold
|
|
708
|
+
if (estimated >= limit) {
|
|
709
|
+
this.emit('autoCompactTriggered', { estimated, limit, contextWindow: this.contextWindow })
|
|
710
|
+
await this.compact()
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const userMessage: Message = { role: 'user', content: content as any }
|
|
715
|
+
this.pushMessage(userMessage)
|
|
716
|
+
this.emit('userMessage', content)
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
const stubText = this._matchStub(typeof content === 'string' ? content : '')
|
|
720
|
+
if (stubText !== null) {
|
|
721
|
+
return await this._streamStub(stubText)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
let raw: string
|
|
725
|
+
|
|
726
|
+
if (this.apiMode === 'responses') {
|
|
727
|
+
const previousResponseId = this.state.get('lastResponseId') || undefined
|
|
728
|
+
let input: OpenAI.Responses.ResponseInput
|
|
729
|
+
|
|
730
|
+
if (previousResponseId) {
|
|
731
|
+
// Can chain via previous_response_id — only send the new user message
|
|
732
|
+
input = [this.toResponsesUserMessage(content)]
|
|
733
|
+
} else {
|
|
734
|
+
// No previous response ID (first call or resumed from disk).
|
|
735
|
+
// Convert full message history to Responses API input so the model has context.
|
|
736
|
+
input = this.messagesToResponsesInput()
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
raw = await this.runResponsesLoop({
|
|
740
|
+
turn: 1,
|
|
741
|
+
accumulated: '',
|
|
742
|
+
input,
|
|
743
|
+
previousResponseId,
|
|
744
|
+
})
|
|
745
|
+
} else {
|
|
746
|
+
raw = await this.runChatCompletionLoop({ turn: 1, accumulated: '' })
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// When a structured output schema is active, parse the JSON response
|
|
750
|
+
if (this._activeSchema) {
|
|
751
|
+
try {
|
|
752
|
+
const parsed = JSON.parse(raw)
|
|
753
|
+
return parsed
|
|
754
|
+
} catch {
|
|
755
|
+
// Model returned something that isn't valid JSON — return raw
|
|
756
|
+
return raw
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return raw
|
|
761
|
+
} catch (err: any) {
|
|
762
|
+
if (err instanceof ConversationAbortError) {
|
|
763
|
+
this.emit('aborted', err.partial)
|
|
764
|
+
throw err
|
|
765
|
+
}
|
|
766
|
+
// Re-throw abort errors from the OpenAI SDK / DOM AbortController
|
|
767
|
+
if (err.name === 'AbortError' || this._abortController?.signal.aborted) {
|
|
768
|
+
const partial = this.state.get('lastResponse') || ''
|
|
769
|
+
this.emit('aborted', partial)
|
|
770
|
+
throw new ConversationAbortError(partial)
|
|
771
|
+
}
|
|
772
|
+
throw err
|
|
773
|
+
} finally {
|
|
774
|
+
this.state.set('callMaxTokens', null)
|
|
775
|
+
this._activeSchema = null
|
|
776
|
+
this._abortController = null
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** Convert user content into a Responses API input message item. */
|
|
781
|
+
private toResponsesUserMessage(content: string | ContentPart[]): OpenAI.Responses.ResponseInputItem.Message {
|
|
782
|
+
if (typeof content === 'string') {
|
|
783
|
+
return {
|
|
784
|
+
type: 'message',
|
|
785
|
+
role: 'user',
|
|
786
|
+
content: [{ type: 'input_text', text: content }]
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const parts = content.map((part) => {
|
|
791
|
+
if (part.type === 'text') {
|
|
792
|
+
return { type: 'input_text' as const, text: part.text }
|
|
793
|
+
}
|
|
794
|
+
if (part.type === 'input_audio') {
|
|
795
|
+
return { type: 'input_audio' as const, data: part.data, format: part.format }
|
|
796
|
+
}
|
|
797
|
+
if (part.type === 'input_file') {
|
|
798
|
+
return { type: 'input_file' as const, file_data: part.file_data, filename: part.filename }
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
type: 'input_image' as const,
|
|
803
|
+
image_url: part.image_url.url,
|
|
804
|
+
detail: part.image_url.detail || 'auto',
|
|
805
|
+
}
|
|
806
|
+
}) as OpenAI.Responses.ResponseInputMessageContentList
|
|
807
|
+
|
|
808
|
+
return {
|
|
809
|
+
type: 'message',
|
|
810
|
+
role: 'user',
|
|
811
|
+
content: parts,
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Convert the full Chat Completions message history into Responses API input items.
|
|
817
|
+
* Used when resuming a conversation without a previous_response_id.
|
|
818
|
+
*/
|
|
819
|
+
private messagesToResponsesInput(): OpenAI.Responses.ResponseInput {
|
|
820
|
+
const input: OpenAI.Responses.ResponseInput = []
|
|
821
|
+
|
|
822
|
+
for (const msg of this.messages) {
|
|
823
|
+
if (msg.role === 'system' || msg.role === 'developer') {
|
|
824
|
+
// System/developer messages are handled via the instructions parameter
|
|
825
|
+
continue
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (msg.role === 'user') {
|
|
829
|
+
if (typeof msg.content === 'string') {
|
|
830
|
+
input.push({
|
|
831
|
+
type: 'message',
|
|
832
|
+
role: 'user',
|
|
833
|
+
content: [{ type: 'input_text', text: msg.content }],
|
|
834
|
+
})
|
|
835
|
+
} else if (Array.isArray(msg.content)) {
|
|
836
|
+
input.push(this.toResponsesUserMessage(msg.content as ContentPart[]))
|
|
837
|
+
}
|
|
838
|
+
continue
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (msg.role === 'assistant') {
|
|
842
|
+
const content = typeof msg.content === 'string' ? msg.content : (msg.content || []).map((p: any) => p.text || '').join('')
|
|
843
|
+
if (content) {
|
|
844
|
+
input.push({
|
|
845
|
+
type: 'message',
|
|
846
|
+
role: 'assistant',
|
|
847
|
+
content: [{ type: 'output_text', text: content, annotations: [] }],
|
|
848
|
+
id: `msg_replay-${input.length}`,
|
|
849
|
+
status: 'completed',
|
|
850
|
+
} as any)
|
|
851
|
+
}
|
|
852
|
+
continue
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Tool results — skip in the replay since the assistant's tool_calls won't have matching IDs
|
|
856
|
+
// The model will still understand context from the assistant messages that followed
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return input
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Build the OpenAI response_format / text.format config from the active Zod schema.
|
|
864
|
+
* Returns undefined when no schema is active.
|
|
865
|
+
*/
|
|
866
|
+
private get structuredOutputConfig(): { name: string; schema: Record<string, any>; strict: true } | undefined {
|
|
867
|
+
if (!this._activeSchema) return undefined
|
|
868
|
+
|
|
869
|
+
const raw = (this._activeSchema as any).toJSONSchema() as Record<string, any>
|
|
870
|
+
const strict = strictifySchema(raw)
|
|
871
|
+
|
|
872
|
+
// Derive a name from the schema description or fall back to a default.
|
|
873
|
+
// OpenAI requires [a-zA-Z0-9_-] max 64 chars.
|
|
874
|
+
const desc = raw.description || 'structured_output'
|
|
875
|
+
const name = desc.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64)
|
|
876
|
+
|
|
877
|
+
return {
|
|
878
|
+
name,
|
|
879
|
+
schema: { type: strict.type || 'object', properties: strict.properties, required: strict.required, additionalProperties: false },
|
|
880
|
+
strict: true,
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Returns the OpenAI client instance from the container. */
|
|
885
|
+
get openai() {
|
|
886
|
+
let baseURL = this.options.clientOptions?.baseURL ? this.options.clientOptions.baseURL : undefined
|
|
887
|
+
|
|
888
|
+
if (this.options.local) {
|
|
889
|
+
baseURL = "http://localhost:1234/v1"
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return (this.container as any).client('openai', {
|
|
893
|
+
defaultModel: this.model || (this.options.local ? this.model || "qwen/qwen3-coder-30b" : "gpt-5"),
|
|
894
|
+
...this.options.clientOptions,
|
|
895
|
+
...(baseURL ? { baseURL } : {}),
|
|
896
|
+
}) as OpenAIClient
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** Returns the conversationHistory feature for persistence. */
|
|
900
|
+
get history(): ConversationHistory {
|
|
901
|
+
return this.container.feature('conversationHistory') as ConversationHistory
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Persist this conversation to disk via conversationHistory.
|
|
906
|
+
* Creates a new record if this conversation hasn't been saved before,
|
|
907
|
+
* or updates the existing one.
|
|
908
|
+
*
|
|
909
|
+
* @param opts - Optional overrides for title, tags, thread, or metadata
|
|
910
|
+
* @returns The saved conversation record
|
|
911
|
+
*/
|
|
912
|
+
async save(opts?: { title?: string; tags?: string[]; thread?: string; metadata?: Record<string, any> }) {
|
|
913
|
+
const id = this.state.get('id')!
|
|
914
|
+
const existing = await this.history.load(id)
|
|
915
|
+
|
|
916
|
+
// Persist lastResponseId so the Responses API can continue the chain on resume
|
|
917
|
+
const lastResponseId = this.state.get('lastResponseId')
|
|
918
|
+
const responseMeta = lastResponseId ? { lastResponseId } : {}
|
|
919
|
+
|
|
920
|
+
if (existing) {
|
|
921
|
+
existing.messages = this.messages
|
|
922
|
+
existing.model = this.model
|
|
923
|
+
if (opts?.title) existing.title = opts.title
|
|
924
|
+
if (opts?.tags) existing.tags = opts.tags
|
|
925
|
+
if (opts?.thread) existing.thread = opts.thread
|
|
926
|
+
existing.metadata = { ...existing.metadata, ...responseMeta, ...(opts?.metadata || {}) }
|
|
927
|
+
await this.history.save(existing)
|
|
928
|
+
return existing
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return this.history.create({
|
|
932
|
+
id,
|
|
933
|
+
title: opts?.title || this.options.title || 'Untitled',
|
|
934
|
+
model: this.model,
|
|
935
|
+
messages: this.messages,
|
|
936
|
+
tags: opts?.tags || this.options.tags || [],
|
|
937
|
+
thread: opts?.thread || this.options.thread || this.state.get('thread'),
|
|
938
|
+
metadata: { ...responseMeta, ...(opts?.metadata || this.options.metadata || {}) },
|
|
939
|
+
})
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Execute a single tool call, routing through the pluggable toolExecutor
|
|
944
|
+
* if one is set (e.g. by the Assistant's interceptor chain).
|
|
945
|
+
*/
|
|
946
|
+
private async executeTool(toolName: string, rawArgs: string): Promise<string> {
|
|
947
|
+
const tool = this.tools[toolName]
|
|
948
|
+
const callCount = (this.state.get('toolCalls') || 0) + 1
|
|
949
|
+
this.state.set('toolCalls', callCount)
|
|
950
|
+
|
|
951
|
+
if (!tool) {
|
|
952
|
+
const result = JSON.stringify({ error: `Unknown tool: ${toolName}` })
|
|
953
|
+
this.emit('toolError', toolName, result)
|
|
954
|
+
return result
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
let args: Record<string, any>
|
|
958
|
+
try {
|
|
959
|
+
args = rawArgs ? JSON.parse(rawArgs) : {}
|
|
960
|
+
} catch (parseErr: any) {
|
|
961
|
+
const result = JSON.stringify({ error: `Failed to parse tool arguments: ${parseErr.message}`, rawArgs })
|
|
962
|
+
this.emit('toolError', toolName, parseErr)
|
|
963
|
+
return result
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (this.toolExecutor) {
|
|
967
|
+
return this.toolExecutor(toolName, args, tool.handler)
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
try {
|
|
971
|
+
this.emit('toolCall', toolName, args)
|
|
972
|
+
const output = await tool.handler(args)
|
|
973
|
+
const result = typeof output === 'string' ? output : JSON.stringify(output)
|
|
974
|
+
this.emit('toolResult', toolName, result)
|
|
975
|
+
return result
|
|
976
|
+
} catch (err: any) {
|
|
977
|
+
const result = JSON.stringify({ error: err.message || String(err) })
|
|
978
|
+
this.emit('toolError', toolName, err)
|
|
979
|
+
return result
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/** Check registered stubs against user input. Returns the response text, or null if no match. */
|
|
984
|
+
private _matchStub(input: string): string | null {
|
|
985
|
+
for (const { matcher, response } of this._stubs) {
|
|
986
|
+
const matched = typeof matcher === 'string'
|
|
987
|
+
? input === matcher || input.includes(matcher)
|
|
988
|
+
: matcher.test(input)
|
|
989
|
+
if (matched) {
|
|
990
|
+
return typeof response === 'function' ? response() : response
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return null
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Simulate a streaming response for a hardcoded stub text.
|
|
998
|
+
* Emits chunk/preview events word-by-word, yielding between each to keep the event loop alive.
|
|
999
|
+
*/
|
|
1000
|
+
private async _streamStub(text: string): Promise<string> {
|
|
1001
|
+
this.state.set('streaming', true)
|
|
1002
|
+
this.emit('turnStart', { turn: 1, isFollowUp: false })
|
|
1003
|
+
|
|
1004
|
+
let accumulated = ''
|
|
1005
|
+
const chunks = text.match(/\S+\s*/g) ?? [text]
|
|
1006
|
+
|
|
1007
|
+
try {
|
|
1008
|
+
for (const chunk of chunks) {
|
|
1009
|
+
accumulated += chunk
|
|
1010
|
+
this.emit('chunk', chunk)
|
|
1011
|
+
this.emit('preview', accumulated)
|
|
1012
|
+
await Promise.resolve()
|
|
1013
|
+
}
|
|
1014
|
+
} finally {
|
|
1015
|
+
this.state.set('streaming', false)
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const trimmed = text
|
|
1019
|
+
this.pushMessage({ role: 'assistant', content: trimmed })
|
|
1020
|
+
this.state.set('lastResponse', trimmed)
|
|
1021
|
+
this.emit('turnEnd', { turn: 1, hasToolCalls: false })
|
|
1022
|
+
this.emit('response', trimmed)
|
|
1023
|
+
|
|
1024
|
+
return trimmed
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Runs the streaming Responses API loop. Handles local function calls by
|
|
1029
|
+
* executing handlers and submitting `function_call_output` items until
|
|
1030
|
+
* the model produces a final text response.
|
|
1031
|
+
*/
|
|
1032
|
+
private async runResponsesLoop(context: {
|
|
1033
|
+
turn: number
|
|
1034
|
+
accumulated: string
|
|
1035
|
+
input: OpenAI.Responses.ResponseInput
|
|
1036
|
+
previousResponseId?: string
|
|
1037
|
+
}): Promise<string> {
|
|
1038
|
+
|
|
1039
|
+
const { turn } = context
|
|
1040
|
+
let accumulated = context.accumulated
|
|
1041
|
+
let turnContent = ''
|
|
1042
|
+
let finalResponse: OpenAI.Responses.Response | undefined
|
|
1043
|
+
|
|
1044
|
+
const toolsParam = this.responseTools.length > 0 ? this.responseTools : undefined
|
|
1045
|
+
|
|
1046
|
+
this.state.set('streaming', true)
|
|
1047
|
+
this.emit('turnStart', { turn, isFollowUp: turn > 1 })
|
|
1048
|
+
|
|
1049
|
+
const textFormat = this.structuredOutputConfig
|
|
1050
|
+
? { text: { format: { type: 'json_schema' as const, ...this.structuredOutputConfig } } }
|
|
1051
|
+
: {}
|
|
1052
|
+
|
|
1053
|
+
try {
|
|
1054
|
+
const stream = await this.openai.raw.responses.create({
|
|
1055
|
+
model: this.model as OpenAI.Responses.ResponseCreateParams['model'],
|
|
1056
|
+
input: context.input,
|
|
1057
|
+
stream: true,
|
|
1058
|
+
previous_response_id: context.previousResponseId,
|
|
1059
|
+
...(toolsParam ? { tools: toolsParam, tool_choice: 'auto', parallel_tool_calls: true } : {}),
|
|
1060
|
+
...(this.responsesInstructions ? { instructions: this.responsesInstructions } : {}),
|
|
1061
|
+
...(this.maxTokens ? { max_output_tokens: this.maxTokens } : {}),
|
|
1062
|
+
...(this.state.get('temperature') != null ? { temperature: this.state.get('temperature') } : {}),
|
|
1063
|
+
...(this.state.get('topP') != null ? { top_p: this.state.get('topP') } : {}),
|
|
1064
|
+
...(this.state.get('topK') != null ? { top_k: this.state.get('topK') } : {}),
|
|
1065
|
+
...(this.state.get('frequencyPenalty') != null ? { frequency_penalty: this.state.get('frequencyPenalty') } : {}),
|
|
1066
|
+
...(this.state.get('presencePenalty') != null ? { presence_penalty: this.state.get('presencePenalty') } : {}),
|
|
1067
|
+
...(this.state.get('stop') ? { stop: this.state.get('stop') } : {}),
|
|
1068
|
+
...textFormat,
|
|
1069
|
+
}, { signal: this._abortController?.signal })
|
|
1070
|
+
|
|
1071
|
+
for await (const event of stream) {
|
|
1072
|
+
this.emit('rawEvent', event)
|
|
1073
|
+
if ((event as any).type?.startsWith?.('response.mcp_')) {
|
|
1074
|
+
this.emit('mcpEvent', event)
|
|
1075
|
+
}
|
|
1076
|
+
if (((event as any).type === 'response.output_item.added' || (event as any).type === 'response.output_item.done')
|
|
1077
|
+
&& (event as any).item?.type?.startsWith?.('mcp_')) {
|
|
1078
|
+
this.emit('mcpEvent', event)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (event.type === 'response.output_text.delta') {
|
|
1082
|
+
const delta = event.delta || ''
|
|
1083
|
+
turnContent += delta
|
|
1084
|
+
accumulated += delta
|
|
1085
|
+
this.state.set('lastResponse', accumulated)
|
|
1086
|
+
this.emit('chunk', delta)
|
|
1087
|
+
this.emit('preview', accumulated)
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (event.type === 'response.completed') {
|
|
1091
|
+
finalResponse = event.response
|
|
1092
|
+
this.emit('responseCompleted', event.response)
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
} finally {
|
|
1096
|
+
this.state.set('streaming', false)
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (!finalResponse) {
|
|
1100
|
+
throw new Error('Responses stream ended without a completed response')
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
this.state.set('lastResponseId', finalResponse.id)
|
|
1104
|
+
this.applyResponsesUsage(finalResponse.usage || undefined)
|
|
1105
|
+
|
|
1106
|
+
const functionCalls = (finalResponse.output || []).filter((item) => item.type === 'function_call') as OpenAI.Responses.ResponseFunctionToolCall[]
|
|
1107
|
+
if (functionCalls.length > 0) {
|
|
1108
|
+
const assistantMessage: OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam = {
|
|
1109
|
+
role: 'assistant',
|
|
1110
|
+
content: turnContent || null,
|
|
1111
|
+
tool_calls: functionCalls.map((call) => ({
|
|
1112
|
+
id: call.call_id,
|
|
1113
|
+
type: 'function',
|
|
1114
|
+
function: {
|
|
1115
|
+
name: call.name,
|
|
1116
|
+
arguments: call.arguments || '{}',
|
|
1117
|
+
}
|
|
1118
|
+
}))
|
|
1119
|
+
}
|
|
1120
|
+
this.pushMessage(assistantMessage)
|
|
1121
|
+
|
|
1122
|
+
this.emit('toolCallsStart', functionCalls)
|
|
1123
|
+
|
|
1124
|
+
const functionOutputs: OpenAI.Responses.ResponseInputItem.FunctionCallOutput[] = []
|
|
1125
|
+
for (const call of functionCalls) {
|
|
1126
|
+
if (this._abortController?.signal.aborted) {
|
|
1127
|
+
throw new ConversationAbortError(accumulated)
|
|
1128
|
+
}
|
|
1129
|
+
const result = await this.executeTool(call.name, call.arguments || '{}')
|
|
1130
|
+
|
|
1131
|
+
this.pushMessage({
|
|
1132
|
+
role: 'tool',
|
|
1133
|
+
tool_call_id: call.call_id,
|
|
1134
|
+
content: result,
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
functionOutputs.push({
|
|
1138
|
+
type: 'function_call_output',
|
|
1139
|
+
call_id: call.call_id,
|
|
1140
|
+
output: result,
|
|
1141
|
+
})
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
this.emit('toolCallsEnd')
|
|
1145
|
+
this.emit('turnEnd', { turn, hasToolCalls: true })
|
|
1146
|
+
|
|
1147
|
+
return this.runResponsesLoop({
|
|
1148
|
+
turn: turn + 1,
|
|
1149
|
+
accumulated,
|
|
1150
|
+
input: functionOutputs,
|
|
1151
|
+
previousResponseId: finalResponse.id,
|
|
1152
|
+
})
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const finalText = turnContent || finalResponse.output_text || ''
|
|
1156
|
+
const assistantMessage: Message = { role: 'assistant', content: finalText }
|
|
1157
|
+
this.pushMessage(assistantMessage)
|
|
1158
|
+
this.state.set('lastResponse', accumulated || finalText)
|
|
1159
|
+
|
|
1160
|
+
this.emit('turnEnd', { turn, hasToolCalls: false })
|
|
1161
|
+
this.emit('response', accumulated || finalText)
|
|
1162
|
+
|
|
1163
|
+
return accumulated || finalText
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/** Apply Responses API usage stats to this conversation's token usage counters. */
|
|
1167
|
+
private applyResponsesUsage(usage?: OpenAI.Responses.ResponseUsage) {
|
|
1168
|
+
if (!usage) return
|
|
1169
|
+
const prev = this.state.get('tokenUsage')!
|
|
1170
|
+
this.state.set('tokenUsage', {
|
|
1171
|
+
prompt: prev.prompt + (usage.input_tokens || 0),
|
|
1172
|
+
completion: prev.completion + (usage.output_tokens || 0),
|
|
1173
|
+
total: prev.total + (usage.total_tokens || 0),
|
|
1174
|
+
})
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Runs the streaming completion loop. If the model requests tool calls,
|
|
1179
|
+
* executes them and loops again until a text response is produced.
|
|
1180
|
+
*
|
|
1181
|
+
* @returns {Promise<string>} The final assistant text response
|
|
1182
|
+
*/
|
|
1183
|
+
/**
|
|
1184
|
+
* Runs the streaming completion loop. If the model requests tool calls,
|
|
1185
|
+
* executes them and loops again until a text response is produced.
|
|
1186
|
+
*
|
|
1187
|
+
* @param context - Turn tracking: turn number and text accumulated across all turns
|
|
1188
|
+
* @returns {Promise<string>} The final assistant text response (accumulated across all turns)
|
|
1189
|
+
*/
|
|
1190
|
+
private async runChatCompletionLoop(context: { turn: number; accumulated: string } = { turn: 1, accumulated: '' }): Promise<string> {
|
|
1191
|
+
|
|
1192
|
+
const { turn } = context
|
|
1193
|
+
let accumulated = context.accumulated
|
|
1194
|
+
|
|
1195
|
+
const hasTools = Object.keys(this.tools).length > 0
|
|
1196
|
+
const toolsParam = hasTools ? this.openaiTools : undefined
|
|
1197
|
+
|
|
1198
|
+
this.state.set('streaming', true)
|
|
1199
|
+
this.emit('turnStart', { turn, isFollowUp: turn > 1 })
|
|
1200
|
+
|
|
1201
|
+
let turnContent = ''
|
|
1202
|
+
let toolCalls: Array<{ id: string; function: { name: string; arguments: string }; type: 'function' }> = []
|
|
1203
|
+
|
|
1204
|
+
const responseFormat = this.structuredOutputConfig
|
|
1205
|
+
? { response_format: { type: 'json_schema' as const, json_schema: this.structuredOutputConfig } }
|
|
1206
|
+
: {}
|
|
1207
|
+
|
|
1208
|
+
try {
|
|
1209
|
+
const stream = await this.openai.raw.chat.completions.create({
|
|
1210
|
+
model: this.model,
|
|
1211
|
+
messages: this.messages,
|
|
1212
|
+
stream: true,
|
|
1213
|
+
...(toolsParam ? { tools: toolsParam, tool_choice: 'auto' } : {}),
|
|
1214
|
+
...(this.maxTokens ? { [this.maxTokensParam]: this.maxTokens } : {}),
|
|
1215
|
+
...(this.state.get('temperature') != null ? { temperature: this.state.get('temperature') } : {}),
|
|
1216
|
+
...(this.state.get('topP') != null ? { top_p: this.state.get('topP') } : {}),
|
|
1217
|
+
...(this.state.get('topK') != null ? { top_k: this.state.get('topK') } : {}),
|
|
1218
|
+
...(this.state.get('frequencyPenalty') != null ? { frequency_penalty: this.state.get('frequencyPenalty') } : {}),
|
|
1219
|
+
...(this.state.get('presencePenalty') != null ? { presence_penalty: this.state.get('presencePenalty') } : {}),
|
|
1220
|
+
...(this.state.get('stop') ? { stop: this.state.get('stop') } : {}),
|
|
1221
|
+
...responseFormat,
|
|
1222
|
+
}, { signal: this._abortController?.signal })
|
|
1223
|
+
|
|
1224
|
+
for await (const chunk of stream) {
|
|
1225
|
+
const delta = chunk.choices[0]?.delta
|
|
1226
|
+
|
|
1227
|
+
if (delta?.content) {
|
|
1228
|
+
turnContent += delta.content
|
|
1229
|
+
accumulated += delta.content
|
|
1230
|
+
this.state.set('lastResponse', accumulated)
|
|
1231
|
+
this.emit('chunk', delta.content)
|
|
1232
|
+
this.emit('preview', accumulated)
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (delta?.tool_calls) {
|
|
1236
|
+
for (const tc of delta.tool_calls) {
|
|
1237
|
+
if (!toolCalls[tc.index]) {
|
|
1238
|
+
toolCalls[tc.index] = {
|
|
1239
|
+
id: tc.id || '',
|
|
1240
|
+
type: 'function',
|
|
1241
|
+
function: { name: '', arguments: '' }
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (tc.id) {
|
|
1245
|
+
toolCalls[tc.index]!.id = tc.id
|
|
1246
|
+
}
|
|
1247
|
+
if (tc.function?.name) {
|
|
1248
|
+
toolCalls[tc.index]!.function.name += tc.function.name
|
|
1249
|
+
}
|
|
1250
|
+
if (tc.function?.arguments) {
|
|
1251
|
+
toolCalls[tc.index]!.function.arguments += tc.function.arguments
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (chunk.usage) {
|
|
1257
|
+
const prev = this.state.get('tokenUsage')!
|
|
1258
|
+
this.state.set('tokenUsage', {
|
|
1259
|
+
prompt: prev.prompt + (chunk.usage.prompt_tokens || 0),
|
|
1260
|
+
completion: prev.completion + (chunk.usage.completion_tokens || 0),
|
|
1261
|
+
total: prev.total + (chunk.usage.total_tokens || 0)
|
|
1262
|
+
})
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
} finally {
|
|
1266
|
+
this.state.set('streaming', false)
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// If the model produced tool calls, execute them and loop
|
|
1270
|
+
if (toolCalls.length > 0) {
|
|
1271
|
+
const assistantMessage: OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam = {
|
|
1272
|
+
role: 'assistant',
|
|
1273
|
+
content: turnContent || null,
|
|
1274
|
+
tool_calls: toolCalls
|
|
1275
|
+
}
|
|
1276
|
+
this.pushMessage(assistantMessage)
|
|
1277
|
+
|
|
1278
|
+
this.emit('toolCallsStart', toolCalls)
|
|
1279
|
+
|
|
1280
|
+
for (const tc of toolCalls) {
|
|
1281
|
+
if (this._abortController?.signal.aborted) {
|
|
1282
|
+
throw new ConversationAbortError(accumulated)
|
|
1283
|
+
}
|
|
1284
|
+
const result = await this.executeTool(tc.function.name, tc.function.arguments)
|
|
1285
|
+
|
|
1286
|
+
const toolMessage: OpenAI.Chat.Completions.ChatCompletionToolMessageParam = {
|
|
1287
|
+
role: 'tool',
|
|
1288
|
+
tool_call_id: tc.id,
|
|
1289
|
+
content: result
|
|
1290
|
+
}
|
|
1291
|
+
this.pushMessage(toolMessage)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
this.emit('toolCallsEnd')
|
|
1295
|
+
this.emit('turnEnd', { turn, hasToolCalls: true })
|
|
1296
|
+
|
|
1297
|
+
// Loop: let the model respond to tool results
|
|
1298
|
+
return this.runChatCompletionLoop({ turn: turn + 1, accumulated })
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Final text response — use this turn's content for the message history,
|
|
1302
|
+
// but accumulated for the response event and return value
|
|
1303
|
+
const assistantMessage: Message = { role: 'assistant', content: turnContent }
|
|
1304
|
+
this.pushMessage(assistantMessage)
|
|
1305
|
+
this.state.set('lastResponse', accumulated)
|
|
1306
|
+
|
|
1307
|
+
this.emit('turnEnd', { turn, hasToolCalls: false })
|
|
1308
|
+
this.emit('response', accumulated)
|
|
1309
|
+
|
|
1310
|
+
return accumulated
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Append a message to the conversation state.
|
|
1315
|
+
*
|
|
1316
|
+
* @param {Message} message - The message to append
|
|
1317
|
+
*/
|
|
1318
|
+
pushMessage(message: Message) {
|
|
1319
|
+
this.state.set('messages', [...this.messages, message])
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
export default Conversation
|