luca 3.0.0 → 3.1.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 +1 -0
- package/CLAUDE.md +10 -2
- package/README.md +130 -112
- package/assistants/codingAssistant/CORE.md +6 -1
- package/assistants/codingAssistant/hooks.ts +1 -1
- package/assistants/inkbot/hooks.ts +1 -1
- package/assistants/inkbot/tools.ts +1 -1
- package/bun.lock +264 -321
- package/commands/audit-docs.ts +2 -2
- package/commands/build-bootstrap.ts +2 -3
- package/commands/build-python-bridge.ts +2 -3
- package/commands/build-scaffolds.ts +2 -3
- package/commands/bundle-consumer-project.ts +521 -0
- package/commands/generate-api-docs.ts +2 -2
- package/commands/inkbot.ts +2 -2
- package/commands/release.ts +2 -2
- package/commands/social.ts +137 -0
- package/commands/try-all-challenges.ts +3 -3
- package/commands/try-challenge.ts +3 -3
- package/datasets/lora/agentic-loop-session-candidates.jsonl +91 -0
- package/datasets/lora/agentic-loop-session-curation-summary.json +123 -0
- package/datasets/lora/luca-session-candidates.jsonl +29 -0
- package/datasets/lora/luca-session-curation-summary.json +121 -0
- package/datasets/lora/review-batch-1.jsonl +30 -0
- package/datasets/lora/review-manifest.json +41 -0
- package/datasets/lora/review-queue.jsonl +120 -0
- package/datasets/lora/review-schema.json +134 -0
- package/datasets/lora/review-template.jsonl +2 -0
- package/datasets/lora/review-ui.html +725 -0
- package/dist/agi/container.server.d.ts +2 -2
- package/dist/agi/features/assistant.d.ts +2 -2
- package/dist/agi/features/assistants-manager.d.ts +1 -1
- package/dist/agi/features/autonomous-assistant.d.ts +1 -1
- package/dist/agi/features/browser-use.d.ts +1 -1
- package/dist/agi/features/claude-code.d.ts +1 -1
- package/dist/agi/features/conversation-history.d.ts +2 -2
- package/dist/agi/features/conversation.d.ts +1 -1
- package/dist/agi/features/docs-reader.d.ts +1 -1
- package/dist/agi/features/file-tools.d.ts +1 -1
- package/dist/agi/features/luca-coder.d.ts +1 -1
- package/dist/agi/features/openai-codex.d.ts +1 -1
- package/dist/agi/features/skills-library.d.ts +1 -1
- package/dist/clients/civitai/index.d.ts +4 -4
- package/dist/clients/client-template.d.ts +4 -4
- package/dist/clients/comfyui/index.d.ts +2 -2
- package/dist/clients/elevenlabs/index.d.ts +2 -2
- package/dist/clients/openai/index.d.ts +2 -2
- package/dist/clients/supabase/index.d.ts +3 -3
- package/dist/command.d.ts +1 -1
- package/dist/node/container.d.ts +1 -1
- package/dist/node/features/helpers.d.ts +3 -3
- package/dist/node/features/semantic-search.d.ts +1 -1
- package/dist/node/features/vm.d.ts +3 -3
- package/dist/node.d.ts +1 -1
- package/dist/scaffolds/generated.d.ts +1 -1
- package/dist/selector.d.ts +1 -1
- package/features/cipher-social.ts +493 -0
- package/index.html +217 -190
- package/luca.console.ts +1 -1
- package/package.json +7 -2
- package/public/index.html +217 -190
- package/public/slides-ai-native.html +1 -1
- package/public/slides-intro.html +2 -2
- package/scripts/curate-claude-sessions.ts +561 -0
- package/scripts/examples/ask-luca-expert.ts +1 -1
- package/scripts/examples/assistant-questions.ts +1 -1
- package/scripts/examples/excalidraw-expert.ts +1 -1
- package/scripts/examples/file-manager.ts +1 -1
- package/scripts/examples/ideas.ts +1 -1
- package/scripts/examples/interactive-chat.ts +1 -1
- package/scripts/examples/opening-a-web-browser.ts +1 -1
- package/scripts/examples/telegram-bot.ts +1 -1
- package/scripts/examples/using-assistant-with-mcp.ts +1 -1
- package/scripts/examples/using-claude-code.ts +1 -1
- package/scripts/examples/using-contentdb.ts +2 -2
- package/scripts/examples/using-conversations.ts +1 -1
- package/scripts/examples/using-disk-cache.ts +1 -1
- package/scripts/examples/using-docker-shell.ts +1 -1
- package/scripts/examples/using-elevenlabs.ts +1 -1
- package/scripts/examples/using-google-calendar.ts +1 -1
- package/scripts/examples/using-google-docs.ts +1 -1
- package/scripts/examples/using-google-drive.ts +1 -1
- package/scripts/examples/using-google-sheets.ts +1 -1
- package/scripts/examples/using-nlp.ts +1 -1
- package/scripts/examples/using-ollama.ts +1 -1
- package/scripts/examples/using-postgres.ts +1 -1
- package/scripts/examples/using-runpod.ts +1 -1
- package/scripts/examples/using-tts.ts +1 -1
- package/scripts/scaffold.ts +5 -5
- package/scripts/scratch.ts +1 -1
- package/scripts/test-assistant-hooks.ts +1 -1
- package/scripts/test-docs-reader.ts +1 -1
- package/src/agi/container.server.ts +6 -2
- package/src/agi/features/agent-memory.ts +25 -25
- package/src/agi/features/assistant.ts +34 -5
- package/src/agi/features/assistants-manager.ts +122 -6
- package/src/agi/features/autonomous-assistant.ts +1 -1
- package/src/agi/features/browser-use.ts +20 -1
- package/src/agi/features/claude-code.ts +51 -5
- package/src/agi/features/coding-tools.ts +1 -1
- package/src/agi/features/conversation-history.ts +181 -4
- package/src/agi/features/conversation.ts +186 -15
- package/src/agi/features/docs-reader.ts +2 -2
- package/src/agi/features/file-tools.ts +49 -2
- package/src/agi/features/luca-coder.ts +7 -5
- package/src/agi/features/mcp-bridge.ts +532 -0
- package/src/agi/features/openai-codex.ts +2 -2
- package/src/agi/features/skills-library.ts +131 -52
- package/src/agi/lib/token-counter.ts +80 -0
- package/src/bootstrap/generated.ts +56 -57
- package/src/browser.ts +1 -1
- package/src/cli/build-info.ts +2 -2
- package/src/cli/cli.ts +2 -2
- package/src/clients/civitai/index.ts +5 -5
- package/src/clients/client-template.ts +4 -4
- package/src/clients/comfyui/index.ts +4 -4
- package/src/clients/elevenlabs/index.ts +4 -4
- package/src/clients/openai/index.ts +7 -7
- package/src/clients/supabase/index.ts +4 -4
- package/src/clients/voicebox/index.ts +4 -4
- package/src/command.ts +2 -1
- package/src/commands/chat.ts +1 -0
- package/src/commands/eval.ts +2 -56
- package/src/commands/introspect.ts +1 -1
- package/src/commands/prompt.ts +41 -9
- package/src/container-describer.ts +8 -1
- package/src/container.ts +13 -0
- package/src/entity.ts +2 -2
- package/src/helper.ts +1 -1
- package/src/introspection/generated.agi.ts +29596 -27654
- package/src/introspection/generated.node.ts +20284 -19247
- package/src/introspection/generated.web.ts +605 -584
- package/src/introspection/scan.ts +11 -6
- package/src/node/container.ts +9 -1
- package/src/node/features/content-db.ts +39 -2
- package/src/node/features/display-result.ts +57 -0
- package/src/node/features/helpers.ts +46 -7
- package/src/node/features/python.ts +25 -19
- package/src/node/features/repl.ts +1 -1
- package/src/node/features/secure-shell.ts +11 -17
- package/src/node/features/semantic-search.ts +2 -2
- package/src/node/features/socket-repl.ts +336 -0
- package/src/node/features/telnyx-assistant-connector.ts +1206 -0
- package/src/node/features/transpiler.ts +2 -3
- package/src/node/features/ui.ts +5 -0
- package/src/node/features/vm.ts +20 -3
- package/src/node.ts +3 -3
- package/src/python/generated.ts +0 -1
- package/src/scaffolds/generated.ts +82 -83
- package/src/selector.ts +1 -1
- package/src/servers/express.ts +1 -1
- package/src/web/features/helpers.ts +22 -0
- package/tsconfig.json +12 -12
- package/docs/CLI.md +0 -335
- package/docs/CNAME +0 -1
- package/docs/README.md +0 -60
- package/docs/TABLE-OF-CONTENTS.md +0 -183
- package/docs/apis/clients/elevenlabs.md +0 -308
- package/docs/apis/clients/graph.md +0 -107
- package/docs/apis/clients/openai.md +0 -429
- package/docs/apis/clients/rest.md +0 -161
- package/docs/apis/clients/websocket.md +0 -174
- package/docs/apis/features/agi/assistant.md +0 -625
- package/docs/apis/features/agi/assistants-manager.md +0 -282
- package/docs/apis/features/agi/auto-assistant.md +0 -279
- package/docs/apis/features/agi/browser-use.md +0 -802
- package/docs/apis/features/agi/claude-code.md +0 -884
- package/docs/apis/features/agi/conversation-history.md +0 -364
- package/docs/apis/features/agi/conversation.md +0 -548
- package/docs/apis/features/agi/docs-reader.md +0 -99
- package/docs/apis/features/agi/file-tools.md +0 -163
- package/docs/apis/features/agi/luca-coder.md +0 -407
- package/docs/apis/features/agi/openai-codex.md +0 -396
- package/docs/apis/features/agi/openapi.md +0 -138
- package/docs/apis/features/agi/semantic-search.md +0 -387
- package/docs/apis/features/agi/skills-library.md +0 -239
- package/docs/apis/features/node/container-link.md +0 -192
- package/docs/apis/features/node/content-db.md +0 -450
- package/docs/apis/features/node/disk-cache.md +0 -379
- package/docs/apis/features/node/dns.md +0 -652
- package/docs/apis/features/node/docker.md +0 -706
- package/docs/apis/features/node/downloader.md +0 -81
- package/docs/apis/features/node/esbuild.md +0 -60
- package/docs/apis/features/node/file-manager.md +0 -191
- package/docs/apis/features/node/fs.md +0 -1217
- package/docs/apis/features/node/git.md +0 -371
- package/docs/apis/features/node/google-auth.md +0 -193
- package/docs/apis/features/node/google-calendar.md +0 -202
- package/docs/apis/features/node/google-docs.md +0 -173
- package/docs/apis/features/node/google-drive.md +0 -246
- package/docs/apis/features/node/google-mail.md +0 -214
- package/docs/apis/features/node/google-sheets.md +0 -194
- package/docs/apis/features/node/grep.md +0 -292
- package/docs/apis/features/node/helpers.md +0 -164
- package/docs/apis/features/node/ink.md +0 -334
- package/docs/apis/features/node/ipc-socket.md +0 -249
- package/docs/apis/features/node/json-tree.md +0 -86
- package/docs/apis/features/node/networking.md +0 -316
- package/docs/apis/features/node/nlp.md +0 -133
- package/docs/apis/features/node/opener.md +0 -97
- package/docs/apis/features/node/os.md +0 -146
- package/docs/apis/features/node/package-finder.md +0 -392
- package/docs/apis/features/node/postgres.md +0 -234
- package/docs/apis/features/node/proc.md +0 -399
- package/docs/apis/features/node/process-manager.md +0 -305
- package/docs/apis/features/node/python.md +0 -604
- package/docs/apis/features/node/redis.md +0 -380
- package/docs/apis/features/node/repl.md +0 -88
- package/docs/apis/features/node/runpod.md +0 -674
- package/docs/apis/features/node/secure-shell.md +0 -176
- package/docs/apis/features/node/semantic-search.md +0 -408
- package/docs/apis/features/node/sqlite.md +0 -233
- package/docs/apis/features/node/telegram.md +0 -279
- package/docs/apis/features/node/transpiler.md +0 -74
- package/docs/apis/features/node/tts.md +0 -133
- package/docs/apis/features/node/ui.md +0 -701
- package/docs/apis/features/node/vault.md +0 -59
- package/docs/apis/features/node/vm.md +0 -75
- package/docs/apis/features/node/yaml-tree.md +0 -85
- package/docs/apis/features/node/yaml.md +0 -176
- package/docs/apis/features/web/asset-loader.md +0 -59
- package/docs/apis/features/web/container-link.md +0 -192
- package/docs/apis/features/web/esbuild.md +0 -54
- package/docs/apis/features/web/helpers.md +0 -164
- package/docs/apis/features/web/network.md +0 -44
- package/docs/apis/features/web/speech.md +0 -69
- package/docs/apis/features/web/vault.md +0 -59
- package/docs/apis/features/web/vm.md +0 -75
- package/docs/apis/features/web/voice.md +0 -84
- package/docs/apis/servers/express.md +0 -171
- package/docs/apis/servers/mcp.md +0 -238
- package/docs/apis/servers/websocket.md +0 -170
- package/docs/bootstrap/CLAUDE.md +0 -101
- package/docs/bootstrap/SKILL.md +0 -341
- package/docs/bootstrap/templates/about-command.ts +0 -41
- package/docs/bootstrap/templates/docs-models.ts +0 -22
- package/docs/bootstrap/templates/docs-readme.md +0 -43
- package/docs/bootstrap/templates/example-feature.ts +0 -53
- package/docs/bootstrap/templates/health-endpoint.ts +0 -15
- package/docs/bootstrap/templates/luca-cli.ts +0 -30
- package/docs/bootstrap/templates/runme.md +0 -54
- package/docs/challenges/caching-proxy.md +0 -16
- package/docs/challenges/content-db-round-trip.md +0 -14
- package/docs/challenges/custom-command.md +0 -9
- package/docs/challenges/file-watcher-pipeline.md +0 -11
- package/docs/challenges/grep-audit-report.md +0 -15
- package/docs/challenges/multi-feature-dashboard.md +0 -14
- package/docs/challenges/process-orchestrator.md +0 -17
- package/docs/challenges/rest-api-server-with-client.md +0 -12
- package/docs/challenges/script-runner-with-vm.md +0 -11
- package/docs/challenges/simple-rest-api.md +0 -15
- package/docs/challenges/websocket-serve-and-client.md +0 -11
- package/docs/challenges/yaml-config-system.md +0 -14
- package/docs/command-system-overhaul.md +0 -94
- package/docs/documentation-audit.md +0 -134
- package/docs/examples/assistant/CORE.md +0 -18
- package/docs/examples/assistant/hooks.ts +0 -3
- package/docs/examples/assistant/tools.ts +0 -10
- package/docs/examples/assistant-hooks-reference.ts +0 -171
- package/docs/examples/assistant-with-process-manager.md +0 -84
- package/docs/examples/content-db.md +0 -77
- package/docs/examples/disk-cache.md +0 -83
- package/docs/examples/docker.md +0 -101
- package/docs/examples/downloader.md +0 -70
- package/docs/examples/entity.md +0 -124
- package/docs/examples/esbuild.md +0 -80
- package/docs/examples/feature-as-tool-provider.md +0 -143
- package/docs/examples/file-manager.md +0 -82
- package/docs/examples/fs.md +0 -83
- package/docs/examples/git.md +0 -85
- package/docs/examples/google-auth.md +0 -88
- package/docs/examples/google-calendar.md +0 -94
- package/docs/examples/google-docs.md +0 -82
- package/docs/examples/google-drive.md +0 -96
- package/docs/examples/google-sheets.md +0 -95
- package/docs/examples/grep.md +0 -85
- package/docs/examples/ink-blocks.md +0 -75
- package/docs/examples/ink-renderer.md +0 -41
- package/docs/examples/ink.md +0 -103
- package/docs/examples/ipc-socket.md +0 -103
- package/docs/examples/json-tree.md +0 -91
- package/docs/examples/networking.md +0 -58
- package/docs/examples/nlp.md +0 -91
- package/docs/examples/opener.md +0 -78
- package/docs/examples/os.md +0 -72
- package/docs/examples/package-finder.md +0 -89
- package/docs/examples/postgres.md +0 -91
- package/docs/examples/proc.md +0 -81
- package/docs/examples/process-manager.md +0 -79
- package/docs/examples/python.md +0 -132
- package/docs/examples/repl.md +0 -93
- package/docs/examples/runpod.md +0 -119
- package/docs/examples/secure-shell.md +0 -92
- package/docs/examples/sqlite.md +0 -86
- package/docs/examples/structured-output-with-assistants.md +0 -144
- package/docs/examples/telegram.md +0 -77
- package/docs/examples/tts.md +0 -86
- package/docs/examples/ui.md +0 -80
- package/docs/examples/vault.md +0 -70
- package/docs/examples/vm.md +0 -86
- package/docs/examples/websocket-ask-and-reply-example.md +0 -128
- package/docs/examples/yaml-tree.md +0 -93
- package/docs/examples/yaml.md +0 -104
- package/docs/ideas/assistant-factory-pattern.md +0 -142
- package/docs/in-memory-fs.md +0 -4
- package/docs/introspection-audit.md +0 -49
- package/docs/introspection.md +0 -164
- package/docs/mcp/readme.md +0 -162
- package/docs/models.ts +0 -41
- package/docs/philosophy.md +0 -86
- package/docs/principles.md +0 -7
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +0 -34
- package/docs/prompts/check-for-undocumented-features.md +0 -27
- package/docs/prompts/mcp-test-easy-command.md +0 -27
- package/docs/scaffolds/client.md +0 -149
- package/docs/scaffolds/command.md +0 -120
- package/docs/scaffolds/endpoint.md +0 -171
- package/docs/scaffolds/feature.md +0 -158
- package/docs/scaffolds/selector.md +0 -91
- package/docs/scaffolds/server.md +0 -196
- package/docs/selectors.md +0 -115
- package/docs/sessions/custom-command/attempt-log-2.md +0 -195
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +0 -728
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +0 -555
- package/docs/sessions/grep-audit-report/attempt-log-1.md +0 -289
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +0 -679
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +0 -1
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +0 -920
- package/docs/sessions/simple-rest-api/attempt-log-1.md +0 -593
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +0 -995
- package/docs/tutorials/00-bootstrap.md +0 -166
- package/docs/tutorials/01-getting-started.md +0 -106
- package/docs/tutorials/02-container.md +0 -210
- package/docs/tutorials/03-scripts.md +0 -194
- package/docs/tutorials/04-features-overview.md +0 -196
- package/docs/tutorials/05-state-and-events.md +0 -171
- package/docs/tutorials/06-servers.md +0 -157
- package/docs/tutorials/07-endpoints.md +0 -198
- package/docs/tutorials/08-commands.md +0 -252
- package/docs/tutorials/09-clients.md +0 -162
- package/docs/tutorials/10-creating-features.md +0 -203
- package/docs/tutorials/11-contentbase.md +0 -191
- package/docs/tutorials/12-assistants.md +0 -215
- package/docs/tutorials/13-introspection.md +0 -157
- package/docs/tutorials/14-type-system.md +0 -174
- package/docs/tutorials/15-project-patterns.md +0 -222
- package/docs/tutorials/16-google-features.md +0 -534
- package/docs/tutorials/17-tui-blocks.md +0 -530
- package/docs/tutorials/18-semantic-search.md +0 -334
- package/docs/tutorials/19-python-sessions.md +0 -401
- package/docs/tutorials/20-browser-esm.md +0 -234
- package/index.ts +0 -1
- package/src/agi/endpoints/ask.ts +0 -60
- package/src/agi/endpoints/conversations/[id].ts +0 -45
- package/src/agi/endpoints/conversations.ts +0 -31
- package/src/agi/endpoints/experts.ts +0 -37
- package/test/assistant-hooks.test.ts +0 -306
- package/test/assistant.test.ts +0 -81
- package/test/bus.test.ts +0 -134
- package/test/clients-servers.test.ts +0 -217
- package/test/command.test.ts +0 -267
- package/test/container-link.test.ts +0 -274
- package/test/conversation.test.ts +0 -220
- package/test/features.test.ts +0 -160
- package/test/fork-and-research.test.ts +0 -450
- package/test/integration.test.ts +0 -787
- package/test/interceptor-chain.test.ts +0 -61
- package/test/node-container.test.ts +0 -121
- package/test/python-session.test.ts +0 -105
- package/test/rate-limit.test.ts +0 -272
- package/test/semantic-search.test.ts +0 -550
- package/test/state.test.ts +0 -121
- package/test/vm-context.test.ts +0 -146
- package/test/vm-loadmodule.test.ts +0 -213
- package/test/websocket-ask.test.ts +0 -101
- package/test-integration/assistant.test.ts +0 -138
- package/test-integration/assistants-manager.test.ts +0 -113
- package/test-integration/claude-code.test.ts +0 -98
- package/test-integration/conversation-history.test.ts +0 -205
- package/test-integration/conversation.test.ts +0 -137
- package/test-integration/elevenlabs.test.ts +0 -55
- package/test-integration/google-services.test.ts +0 -80
- package/test-integration/helpers.ts +0 -89
- package/test-integration/memory.test.ts +0 -204
- package/test-integration/openai-codex.test.ts +0 -93
- package/test-integration/runpod.test.ts +0 -58
- package/test-integration/server-endpoints.test.ts +0 -97
- package/test-integration/telegram.test.ts +0 -46
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
4
|
-
import { type AvailableFeatures } from '
|
|
4
|
+
import { type AvailableFeatures } from 'luca/feature'
|
|
5
5
|
import { Feature } from '../feature.js'
|
|
6
6
|
|
|
7
|
-
declare module '
|
|
7
|
+
declare module 'luca/feature' {
|
|
8
8
|
interface AvailableFeatures {
|
|
9
9
|
claudeCode: typeof ClaudeCode
|
|
10
10
|
}
|
|
@@ -184,6 +184,12 @@ export const ClaudeCodeOptionsSchema = FeatureOptionsSchema.extend({
|
|
|
184
184
|
skillsFolders: z.array(z.string()).optional().describe('Directories containing Claude Code skills to load into sessions'),
|
|
185
185
|
/** Launch Claude Code with a Chrome browser tool. */
|
|
186
186
|
chrome: z.boolean().optional().describe('Launch Claude Code with a Chrome browser tool'),
|
|
187
|
+
/** Base URL for the Anthropic API. Injected as ANTHROPIC_BASE_URL env var. */
|
|
188
|
+
baseURL: z.string().optional().describe('Base URL for the Anthropic API, injected as ANTHROPIC_BASE_URL'),
|
|
189
|
+
/** Auth token for the Anthropic API. Injected as ANTHROPIC_AUTH_TOKEN env var. */
|
|
190
|
+
authToken: z.string().optional().describe('Auth token for the Anthropic API, injected as ANTHROPIC_AUTH_TOKEN'),
|
|
191
|
+
/** Use local models. Sets baseURL and model from LOCAL_CHAT_ENDPOINT and LOCAL_CODER_MODEL env vars. */
|
|
192
|
+
local: z.boolean().optional().describe('Use local models, sets baseURL to LOCAL_CHAT_ENDPOINT and model to LOCAL_CODER_MODEL'),
|
|
187
193
|
})
|
|
188
194
|
|
|
189
195
|
export const ClaudeCodeEventsSchema = FeatureEventsSchema.extend({
|
|
@@ -269,6 +275,12 @@ export interface RunOptions {
|
|
|
269
275
|
settingsFile?: string
|
|
270
276
|
/** Launch Claude Code with a Chrome browser tool. */
|
|
271
277
|
chrome?: boolean
|
|
278
|
+
/** Base URL for the Anthropic API. Injected as ANTHROPIC_BASE_URL in the subprocess env. */
|
|
279
|
+
baseURL?: string
|
|
280
|
+
/** Auth token for the Anthropic API. Injected as ANTHROPIC_AUTH_TOKEN in the subprocess env. */
|
|
281
|
+
authToken?: string
|
|
282
|
+
/** Use local models. Sets baseURL to LOCAL_CHAT_ENDPOINT (or http://localhost:1234) and model to LOCAL_CODER_MODEL (or qwen/qwen3.6-27b). */
|
|
283
|
+
local?: boolean
|
|
272
284
|
}
|
|
273
285
|
|
|
274
286
|
/**
|
|
@@ -506,7 +518,8 @@ export class ClaudeCode extends Feature<ClaudeCodeState, ClaudeCodeOptions> {
|
|
|
506
518
|
args.push('--include-partial-messages')
|
|
507
519
|
}
|
|
508
520
|
|
|
509
|
-
const
|
|
521
|
+
const isLocal = options.local ?? this.options.local
|
|
522
|
+
const model = options.model ?? this.options.model ?? (isLocal ? (process.env.LOCAL_CODER_MODEL || 'qwen/qwen3.6-27b') : undefined)
|
|
510
523
|
if (model) args.push('--model', model)
|
|
511
524
|
|
|
512
525
|
const systemPrompt = options.systemPrompt ?? this.options.systemPrompt
|
|
@@ -613,6 +626,39 @@ export class ClaudeCode extends Feature<ClaudeCodeState, ClaudeCodeOptions> {
|
|
|
613
626
|
return args
|
|
614
627
|
}
|
|
615
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Build the environment object for a claude CLI invocation.
|
|
631
|
+
* Injects ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN when baseURL/authToken are set,
|
|
632
|
+
* or when local mode is enabled.
|
|
633
|
+
*
|
|
634
|
+
* @param {RunOptions} options - Session options
|
|
635
|
+
* @returns {Record<string, string>} Environment variables
|
|
636
|
+
*/
|
|
637
|
+
private buildEnv(options: RunOptions = {}): Record<string, string> {
|
|
638
|
+
const env = { ...process.env }
|
|
639
|
+
const isLocal = options.local ?? this.options.local
|
|
640
|
+
|
|
641
|
+
if (isLocal) {
|
|
642
|
+
const baseURL = process.env.LOCAL_CHAT_ENDPOINT || 'http://localhost:1234'
|
|
643
|
+
env.ANTHROPIC_BASE_URL = baseURL
|
|
644
|
+
if (!options.authToken) {
|
|
645
|
+
env.ANTHROPIC_AUTH_TOKEN = process.env.LOCAL_CHAT_AUTH_TOKEN || 'sk-anticropic-00000000000000000000001'
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const baseURL = options.baseURL
|
|
650
|
+
if (baseURL) {
|
|
651
|
+
env.ANTHROPIC_BASE_URL = baseURL
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const authToken = options.authToken
|
|
655
|
+
if (authToken) {
|
|
656
|
+
env.ANTHROPIC_AUTH_TOKEN = authToken
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return env
|
|
660
|
+
}
|
|
661
|
+
|
|
616
662
|
/**
|
|
617
663
|
* Create a unique session ID.
|
|
618
664
|
*
|
|
@@ -780,7 +826,7 @@ export class ClaudeCode extends Feature<ClaudeCodeState, ClaudeCodeOptions> {
|
|
|
780
826
|
stdout: 'pipe',
|
|
781
827
|
stderr: 'pipe',
|
|
782
828
|
stdin: Buffer.from(prompt),
|
|
783
|
-
environment:
|
|
829
|
+
environment: this.buildEnv(options),
|
|
784
830
|
})
|
|
785
831
|
|
|
786
832
|
this.updateSession(id, { process: proc })
|
|
@@ -842,7 +888,7 @@ export class ClaudeCode extends Feature<ClaudeCodeState, ClaudeCodeOptions> {
|
|
|
842
888
|
stdout: 'pipe',
|
|
843
889
|
stderr: 'pipe',
|
|
844
890
|
stdin: Buffer.from(prompt),
|
|
845
|
-
environment:
|
|
891
|
+
environment: this.buildEnv(options),
|
|
846
892
|
})
|
|
847
893
|
|
|
848
894
|
this.updateSession(id, { process: proc })
|
|
@@ -4,7 +4,7 @@ import { Feature } from '../feature.js'
|
|
|
4
4
|
import type { Helper } from '../../helper.js'
|
|
5
5
|
import type { ChildProcess } from '../../node/features/proc.js'
|
|
6
6
|
|
|
7
|
-
declare module '
|
|
7
|
+
declare module 'luca/feature' {
|
|
8
8
|
interface AvailableFeatures {
|
|
9
9
|
codingTools: typeof CodingTools
|
|
10
10
|
}
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { type AvailableFeatures } from '
|
|
3
|
+
import { type AvailableFeatures } from 'luca/feature'
|
|
4
4
|
import { Feature } from '../feature.js'
|
|
5
|
-
import type { DiskCache } from '
|
|
5
|
+
import type { DiskCache } from 'luca/node/container'
|
|
6
|
+
import type { OpenAIClient } from '../../clients/openai'
|
|
6
7
|
import type { Message } from './conversation'
|
|
7
8
|
|
|
8
|
-
declare module '
|
|
9
|
+
declare module 'luca/feature' {
|
|
9
10
|
interface AvailableFeatures {
|
|
10
11
|
conversationHistory: typeof ConversationHistory
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export interface TokenUsage {
|
|
16
|
+
prompt: number
|
|
17
|
+
completion: number
|
|
18
|
+
total: number
|
|
19
|
+
cachedTokens?: number
|
|
20
|
+
reasoningTokens?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CostInfo {
|
|
24
|
+
inputCost: number
|
|
25
|
+
outputCost: number
|
|
26
|
+
totalCost: number
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
export interface ConversationRecord {
|
|
15
30
|
id: string
|
|
16
31
|
title: string
|
|
@@ -21,6 +36,8 @@ export interface ConversationRecord {
|
|
|
21
36
|
createdAt: string
|
|
22
37
|
updatedAt: string
|
|
23
38
|
messageCount: number
|
|
39
|
+
tokenUsage?: TokenUsage
|
|
40
|
+
cost?: CostInfo
|
|
24
41
|
metadata: Record<string, any>
|
|
25
42
|
}
|
|
26
43
|
|
|
@@ -160,12 +177,15 @@ export class ConversationHistory extends Feature<ConversationHistoryState, Conve
|
|
|
160
177
|
messages: Message[]
|
|
161
178
|
tags?: string[]
|
|
162
179
|
thread?: string
|
|
180
|
+
tokenUsage?: TokenUsage
|
|
181
|
+
cost?: CostInfo
|
|
163
182
|
metadata?: Record<string, any>
|
|
164
183
|
}): Promise<ConversationRecord> {
|
|
165
184
|
const now = new Date().toISOString()
|
|
185
|
+
const title = opts.title || await this.autoTitle(opts.messages)
|
|
166
186
|
const record: ConversationRecord = {
|
|
167
187
|
id: opts.id || crypto.randomUUID(),
|
|
168
|
-
title
|
|
188
|
+
title,
|
|
169
189
|
model: opts.model || 'unknown',
|
|
170
190
|
messages: opts.messages,
|
|
171
191
|
tags: opts.tags || [],
|
|
@@ -173,6 +193,8 @@ export class ConversationHistory extends Feature<ConversationHistoryState, Conve
|
|
|
173
193
|
createdAt: now,
|
|
174
194
|
updatedAt: now,
|
|
175
195
|
messageCount: opts.messages.length,
|
|
196
|
+
tokenUsage: opts.tokenUsage,
|
|
197
|
+
cost: opts.cost,
|
|
176
198
|
metadata: opts.metadata || {},
|
|
177
199
|
}
|
|
178
200
|
|
|
@@ -408,6 +430,161 @@ export class ConversationHistory extends Feature<ConversationHistoryState, Conve
|
|
|
408
430
|
return count
|
|
409
431
|
}
|
|
410
432
|
|
|
433
|
+
/** @returns An OpenAI client from the container for LLM calls. */
|
|
434
|
+
private get openai(): OpenAIClient {
|
|
435
|
+
return (this.container as any).client('openai') as OpenAIClient
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Generate a short title from conversation messages. Uses the first user
|
|
440
|
+
* message (and optionally the first assistant reply) to produce a concise
|
|
441
|
+
* title via a cheap LLM call. Falls back to a truncated first message
|
|
442
|
+
* if the LLM call fails.
|
|
443
|
+
*/
|
|
444
|
+
private async autoTitle(messages: Message[]): Promise<string> {
|
|
445
|
+
const userMsg = messages.find(m => m.role === 'user')
|
|
446
|
+
if (!userMsg) return `Conversation ${new Date().toISOString().slice(0, 16)}`
|
|
447
|
+
|
|
448
|
+
const userContent = typeof userMsg.content === 'string'
|
|
449
|
+
? userMsg.content
|
|
450
|
+
: Array.isArray(userMsg.content)
|
|
451
|
+
? (userMsg.content as any[]).filter((p: any) => p.type === 'text').map((p: any) => p.text).join(' ')
|
|
452
|
+
: ''
|
|
453
|
+
|
|
454
|
+
if (!userContent.trim()) return `Conversation ${new Date().toISOString().slice(0, 16)}`
|
|
455
|
+
|
|
456
|
+
// Build a small context snippet: first user message + first assistant reply if available
|
|
457
|
+
const assistantMsg = messages.find(m => m.role === 'assistant')
|
|
458
|
+
let context = `User: ${userContent}`
|
|
459
|
+
if (assistantMsg) {
|
|
460
|
+
const assistantContent = typeof assistantMsg.content === 'string'
|
|
461
|
+
? assistantMsg.content
|
|
462
|
+
: Array.isArray(assistantMsg.content)
|
|
463
|
+
? (assistantMsg.content as any[]).filter((p: any) => p.type === 'text').map((p: any) => p.text).join(' ')
|
|
464
|
+
: ''
|
|
465
|
+
if (assistantContent.trim()) {
|
|
466
|
+
context += `\nAssistant: ${assistantContent.slice(0, 300)}`
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
const response = await this.openai.raw.chat.completions.create({
|
|
472
|
+
model: 'gpt-4o-mini',
|
|
473
|
+
messages: [
|
|
474
|
+
{
|
|
475
|
+
role: 'system',
|
|
476
|
+
content: 'Generate a short title (max 8 words) for this conversation. Output only the title, no quotes or punctuation wrapping.',
|
|
477
|
+
},
|
|
478
|
+
{ role: 'user', content: context.slice(0, 1000) },
|
|
479
|
+
],
|
|
480
|
+
max_tokens: 30,
|
|
481
|
+
temperature: 0.3,
|
|
482
|
+
stream: false,
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
const title = (response as any).choices?.[0]?.message?.content?.trim()
|
|
486
|
+
if (title) return title
|
|
487
|
+
} catch {
|
|
488
|
+
// LLM unavailable — fall back to truncation
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Fallback: truncate the first user message
|
|
492
|
+
return userContent.length > 60 ? userContent.slice(0, 57) + '...' : userContent
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Build a plain-text transcript from a messages array,
|
|
497
|
+
* suitable for feeding to a summarizer or title generator.
|
|
498
|
+
*/
|
|
499
|
+
private buildTranscript(messages: Message[]): string {
|
|
500
|
+
return messages
|
|
501
|
+
.map(m => {
|
|
502
|
+
const role = m.role
|
|
503
|
+
const content = typeof m.content === 'string'
|
|
504
|
+
? m.content
|
|
505
|
+
: Array.isArray(m.content)
|
|
506
|
+
? (m.content as any[]).filter((p: any) => p.type === 'text').map((p: any) => p.text).join('\n')
|
|
507
|
+
: (m.content != null ? JSON.stringify(m.content) : '(no content)')
|
|
508
|
+
return `[${role}]: ${content || '(no text content)'}`
|
|
509
|
+
})
|
|
510
|
+
.join('\n\n')
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Generate a concise summary of a stored conversation using the LLM.
|
|
515
|
+
* The summary is stored in `metadata.summary` and returned.
|
|
516
|
+
* No tool calls are made — this is a single completion request.
|
|
517
|
+
*
|
|
518
|
+
* @param {string} id - The conversation ID to summarize
|
|
519
|
+
* @param {object} [options] - Optional settings
|
|
520
|
+
* @param {string} [options.model] - Override the model used for summarization
|
|
521
|
+
* @returns {Promise<string | null>} The generated summary, or null if conversation not found
|
|
522
|
+
*/
|
|
523
|
+
async summarize(id: string, options?: { model?: string }): Promise<string | null> {
|
|
524
|
+
const record = await this.load(id)
|
|
525
|
+
if (!record) return null
|
|
526
|
+
|
|
527
|
+
const transcript = this.buildTranscript(record.messages)
|
|
528
|
+
const model = options?.model || record.model || 'gpt-5'
|
|
529
|
+
|
|
530
|
+
const response = await this.openai.raw.chat.completions.create({
|
|
531
|
+
model,
|
|
532
|
+
messages: [
|
|
533
|
+
{
|
|
534
|
+
role: 'system',
|
|
535
|
+
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 understand what was discussed. Output only the summary.',
|
|
536
|
+
},
|
|
537
|
+
{ role: 'user', content: transcript },
|
|
538
|
+
],
|
|
539
|
+
stream: false,
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
const summary = (response as any).choices?.[0]?.message?.content || ''
|
|
543
|
+
|
|
544
|
+
record.metadata = { ...record.metadata, summary }
|
|
545
|
+
await this.save(record)
|
|
546
|
+
|
|
547
|
+
return summary
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Generate a short, descriptive title for a stored conversation using the LLM.
|
|
552
|
+
* The title is stored both as the record's `title` field and in `metadata.generatedTitle`,
|
|
553
|
+
* then returned. No tool calls are made.
|
|
554
|
+
*
|
|
555
|
+
* @param {string} id - The conversation ID to generate a title for
|
|
556
|
+
* @param {object} [options] - Optional settings
|
|
557
|
+
* @param {string} [options.model] - Override the model used for title generation
|
|
558
|
+
* @returns {Promise<string | null>} The generated title, or null if conversation not found
|
|
559
|
+
*/
|
|
560
|
+
async generateTitle(id: string, options?: { model?: string }): Promise<string | null> {
|
|
561
|
+
const record = await this.load(id)
|
|
562
|
+
if (!record) return null
|
|
563
|
+
|
|
564
|
+
const transcript = this.buildTranscript(record.messages)
|
|
565
|
+
const model = options?.model || record.model || 'gpt-5'
|
|
566
|
+
|
|
567
|
+
const response = await this.openai.raw.chat.completions.create({
|
|
568
|
+
model,
|
|
569
|
+
messages: [
|
|
570
|
+
{
|
|
571
|
+
role: 'system',
|
|
572
|
+
content: 'Generate a short, descriptive title (under 60 characters) for the following conversation. The title should capture the main topic or purpose. Output only the title text, with no quotes or punctuation wrapping it.',
|
|
573
|
+
},
|
|
574
|
+
{ role: 'user', content: transcript },
|
|
575
|
+
],
|
|
576
|
+
stream: false,
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
const title = ((response as any).choices?.[0]?.message?.content || '').trim()
|
|
580
|
+
|
|
581
|
+
record.title = title
|
|
582
|
+
record.metadata = { ...record.metadata, generatedTitle: title }
|
|
583
|
+
await this.save(record)
|
|
584
|
+
|
|
585
|
+
return title
|
|
586
|
+
}
|
|
587
|
+
|
|
411
588
|
// -- index management --
|
|
412
589
|
|
|
413
590
|
private async getIndex(): Promise<string[]> {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { type AvailableFeatures } from '
|
|
3
|
+
import { type AvailableFeatures } from 'luca/feature'
|
|
4
4
|
import { Feature } from '../feature.js'
|
|
5
5
|
import type { OpenAIClient } from '../../clients/openai';
|
|
6
6
|
import type OpenAI from 'openai';
|
|
7
7
|
import type { ConversationHistory } from './conversation-history';
|
|
8
|
-
import { countMessageTokens, getContextWindow } from '../lib/token-counter.js';
|
|
8
|
+
import { countMessageTokens, getContextWindow, calculateCost } from '../lib/token-counter.js';
|
|
9
9
|
|
|
10
|
-
declare module '
|
|
10
|
+
declare module 'luca/feature' {
|
|
11
11
|
interface AvailableFeatures {
|
|
12
12
|
conversation: typeof Conversation
|
|
13
13
|
}
|
|
@@ -37,6 +37,20 @@ export interface ConversationMCPServer {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const INPUT_TOKEN_SIZES: Record<string, number> = {
|
|
41
|
+
tiny: 8_000,
|
|
42
|
+
small: 16_000,
|
|
43
|
+
medium: 32_000,
|
|
44
|
+
large: 64_000,
|
|
45
|
+
xlarge: 256_000,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveMaxInputTokens(value: number | string | undefined): number | undefined {
|
|
49
|
+
if (value == null) return undefined
|
|
50
|
+
if (typeof value === 'number') return value
|
|
51
|
+
return INPUT_TOKEN_SIZES[value]
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
export const ConversationOptionsSchema = FeatureOptionsSchema.extend({
|
|
41
55
|
/** A unique identifier for the conversation */
|
|
42
56
|
id: z.string().optional().describe('A unique identifier for the conversation'),
|
|
@@ -87,6 +101,12 @@ export const ConversationOptionsSchema = FeatureOptionsSchema.extend({
|
|
|
87
101
|
contextWindow: z.number().optional().describe('Override the inferred context window size for this model'),
|
|
88
102
|
/** Number of recent messages to preserve after compaction (default 4) */
|
|
89
103
|
compactKeepRecent: z.number().optional().describe('Number of recent messages to preserve after compaction (default 4)'),
|
|
104
|
+
|
|
105
|
+
/** Maximum input tokens to send to the API. When set, older messages are trimmed to stay within this budget, keeping the system prompt and most recent messages. Useful for avoiding long-context pricing tiers. Accepts a number or a named size: tiny (8k), small (16k), medium (32k), large (64k), xlarge (256k — max before long-context pricing). */
|
|
106
|
+
maxInputTokens: z.union([
|
|
107
|
+
z.number(),
|
|
108
|
+
z.enum(['tiny', 'small', 'medium', 'large', 'xlarge']),
|
|
109
|
+
]).default('large').describe('Maximum input tokens. Accepts a number or a named size: tiny (8k), small (16k), medium (32k), large (64k), xlarge (256k). Defaults to large (64k)'),
|
|
90
110
|
})
|
|
91
111
|
|
|
92
112
|
export const ConversationStateSchema = FeatureStateSchema.extend({
|
|
@@ -103,7 +123,14 @@ export const ConversationStateSchema = FeatureStateSchema.extend({
|
|
|
103
123
|
prompt: z.number().describe('Total prompt tokens consumed'),
|
|
104
124
|
completion: z.number().describe('Total completion tokens consumed'),
|
|
105
125
|
total: z.number().describe('Total tokens consumed'),
|
|
106
|
-
|
|
126
|
+
cachedTokens: z.number().describe('Input tokens served from cache (billed at reduced rate)'),
|
|
127
|
+
reasoningTokens: z.number().describe('Output tokens used for reasoning (o-series models)'),
|
|
128
|
+
}).describe('Cumulative token usage statistics including detail breakdowns from the API'),
|
|
129
|
+
cost: z.object({
|
|
130
|
+
inputCost: z.number().describe('Estimated cost in dollars for input tokens'),
|
|
131
|
+
outputCost: z.number().describe('Estimated cost in dollars for output tokens'),
|
|
132
|
+
totalCost: z.number().describe('Estimated total cost in dollars'),
|
|
133
|
+
}).describe('Running cost estimate based on cumulative token usage and model pricing'),
|
|
107
134
|
estimatedInputTokens: z.number().describe('Estimated input token count for the current messages array'),
|
|
108
135
|
compactionCount: z.number().describe('Number of times compact() has been called'),
|
|
109
136
|
contextWindow: z.number().describe('The context window size for the current model'),
|
|
@@ -267,7 +294,8 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
267
294
|
toolCalls: 0,
|
|
268
295
|
api: this.apiMode,
|
|
269
296
|
lastResponseId: null,
|
|
270
|
-
tokenUsage: { prompt: 0, completion: 0, total: 0 },
|
|
297
|
+
tokenUsage: { prompt: 0, completion: 0, total: 0, cachedTokens: 0, reasoningTokens: 0 },
|
|
298
|
+
cost: { inputCost: 0, outputCost: 0, totalCost: 0 },
|
|
271
299
|
estimatedInputTokens: 0,
|
|
272
300
|
compactionCount: 0,
|
|
273
301
|
contextWindow: this.options.contextWindow || getContextWindow(this.options.model || 'gpt-5'),
|
|
@@ -598,7 +626,15 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
598
626
|
? messages[0]
|
|
599
627
|
: null
|
|
600
628
|
|
|
601
|
-
|
|
629
|
+
let sliceStart = messages.length - keepRecent
|
|
630
|
+
// Walk back to avoid splitting a tool call group — if we'd start on a tool message,
|
|
631
|
+
// include the preceding assistant message (and its full tool response block)
|
|
632
|
+
if (sliceStart > 0) {
|
|
633
|
+
while (sliceStart > 0 && messages[sliceStart]?.role === 'tool') {
|
|
634
|
+
sliceStart--
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const recentMessages = messages.slice(sliceStart)
|
|
602
638
|
|
|
603
639
|
const newMessages: Message[] = []
|
|
604
640
|
if (systemMessage) newMessages.push(systemMessage)
|
|
@@ -724,16 +760,20 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
724
760
|
let raw: string
|
|
725
761
|
|
|
726
762
|
if (this.apiMode === 'responses') {
|
|
727
|
-
|
|
763
|
+
// When maxInputTokens is set, skip previous_response_id continuation
|
|
764
|
+
// so we control exactly how many tokens the API processes (server-side
|
|
765
|
+
// context from previous_response_id would accumulate unbounded).
|
|
766
|
+
const canChain = !this.options.maxInputTokens
|
|
767
|
+
const previousResponseId = canChain ? (this.state.get('lastResponseId') || undefined) : undefined
|
|
728
768
|
let input: OpenAI.Responses.ResponseInput
|
|
729
769
|
|
|
730
770
|
if (previousResponseId) {
|
|
731
771
|
// Can chain via previous_response_id — only send the new user message
|
|
732
772
|
input = [this.toResponsesUserMessage(content)]
|
|
733
773
|
} else {
|
|
734
|
-
// No previous response ID (first call
|
|
735
|
-
// Convert
|
|
736
|
-
input = this.messagesToResponsesInput()
|
|
774
|
+
// No previous response ID (first call, resumed from disk, or maxInputTokens active).
|
|
775
|
+
// Convert (possibly trimmed) message history to Responses API input.
|
|
776
|
+
input = this.messagesToResponsesInput(this.getMessagesWithinBudget())
|
|
737
777
|
}
|
|
738
778
|
|
|
739
779
|
raw = await this.runResponsesLoop({
|
|
@@ -816,10 +856,10 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
816
856
|
* Convert the full Chat Completions message history into Responses API input items.
|
|
817
857
|
* Used when resuming a conversation without a previous_response_id.
|
|
818
858
|
*/
|
|
819
|
-
private messagesToResponsesInput(): OpenAI.Responses.ResponseInput {
|
|
859
|
+
private messagesToResponsesInput(messages?: Message[]): OpenAI.Responses.ResponseInput {
|
|
820
860
|
const input: OpenAI.Responses.ResponseInput = []
|
|
821
861
|
|
|
822
|
-
for (const msg of this.messages) {
|
|
862
|
+
for (const msg of (messages || this.messages)) {
|
|
823
863
|
if (msg.role === 'system' || msg.role === 'developer') {
|
|
824
864
|
// System/developer messages are handled via the instructions parameter
|
|
825
865
|
continue
|
|
@@ -917,9 +957,15 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
917
957
|
const lastResponseId = this.state.get('lastResponseId')
|
|
918
958
|
const responseMeta = lastResponseId ? { lastResponseId } : {}
|
|
919
959
|
|
|
960
|
+
// Grab the live token usage and cost from state
|
|
961
|
+
const tokenUsage = this.state.get('tokenUsage')!
|
|
962
|
+
const cost = this.state.get('cost')!
|
|
963
|
+
|
|
920
964
|
if (existing) {
|
|
921
965
|
existing.messages = this.messages
|
|
922
966
|
existing.model = this.model
|
|
967
|
+
existing.tokenUsage = tokenUsage
|
|
968
|
+
existing.cost = cost
|
|
923
969
|
if (opts?.title) existing.title = opts.title
|
|
924
970
|
if (opts?.tags) existing.tags = opts.tags
|
|
925
971
|
if (opts?.thread) existing.thread = opts.thread
|
|
@@ -930,11 +976,13 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
930
976
|
|
|
931
977
|
return this.history.create({
|
|
932
978
|
id,
|
|
933
|
-
title: opts?.title || this.options.title
|
|
979
|
+
title: opts?.title || this.options.title,
|
|
934
980
|
model: this.model,
|
|
935
981
|
messages: this.messages,
|
|
936
982
|
tags: opts?.tags || this.options.tags || [],
|
|
937
983
|
thread: opts?.thread || this.options.thread || this.state.get('thread'),
|
|
984
|
+
tokenUsage,
|
|
985
|
+
cost,
|
|
938
986
|
metadata: { ...responseMeta, ...(opts?.metadata || this.options.metadata || {}) },
|
|
939
987
|
})
|
|
940
988
|
}
|
|
@@ -1163,6 +1211,16 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
1163
1211
|
return accumulated || finalText
|
|
1164
1212
|
}
|
|
1165
1213
|
|
|
1214
|
+
/** Recalculate the running cost estimate from current token usage and update state. */
|
|
1215
|
+
private updateCost() {
|
|
1216
|
+
const tokenUsage = this.state.get('tokenUsage')!
|
|
1217
|
+
const { inputCost, outputCost, totalCost } = calculateCost(this.model, tokenUsage.prompt, tokenUsage.completion, {
|
|
1218
|
+
cachedTokens: tokenUsage.cachedTokens,
|
|
1219
|
+
reasoningTokens: tokenUsage.reasoningTokens,
|
|
1220
|
+
})
|
|
1221
|
+
this.state.set('cost', { inputCost, outputCost, totalCost })
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1166
1224
|
/** Apply Responses API usage stats to this conversation's token usage counters. */
|
|
1167
1225
|
private applyResponsesUsage(usage?: OpenAI.Responses.ResponseUsage) {
|
|
1168
1226
|
if (!usage) return
|
|
@@ -1171,7 +1229,10 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
1171
1229
|
prompt: prev.prompt + (usage.input_tokens || 0),
|
|
1172
1230
|
completion: prev.completion + (usage.output_tokens || 0),
|
|
1173
1231
|
total: prev.total + (usage.total_tokens || 0),
|
|
1232
|
+
cachedTokens: prev.cachedTokens + (usage.input_tokens_details?.cached_tokens || 0),
|
|
1233
|
+
reasoningTokens: prev.reasoningTokens + (usage.output_tokens_details?.reasoning_tokens || 0),
|
|
1174
1234
|
})
|
|
1235
|
+
this.updateCost()
|
|
1175
1236
|
}
|
|
1176
1237
|
|
|
1177
1238
|
/**
|
|
@@ -1208,7 +1269,7 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
1208
1269
|
try {
|
|
1209
1270
|
const stream = await this.openai.raw.chat.completions.create({
|
|
1210
1271
|
model: this.model,
|
|
1211
|
-
messages: this.
|
|
1272
|
+
messages: this.sanitizeMessages(this.getMessagesWithinBudget()),
|
|
1212
1273
|
stream: true,
|
|
1213
1274
|
...(toolsParam ? { tools: toolsParam, tool_choice: 'auto' } : {}),
|
|
1214
1275
|
...(this.maxTokens ? { [this.maxTokensParam]: this.maxTokens } : {}),
|
|
@@ -1258,8 +1319,11 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
1258
1319
|
this.state.set('tokenUsage', {
|
|
1259
1320
|
prompt: prev.prompt + (chunk.usage.prompt_tokens || 0),
|
|
1260
1321
|
completion: prev.completion + (chunk.usage.completion_tokens || 0),
|
|
1261
|
-
total: prev.total + (chunk.usage.total_tokens || 0)
|
|
1322
|
+
total: prev.total + (chunk.usage.total_tokens || 0),
|
|
1323
|
+
cachedTokens: prev.cachedTokens + (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
|
|
1324
|
+
reasoningTokens: prev.reasoningTokens + (chunk.usage.completion_tokens_details?.reasoning_tokens || 0),
|
|
1262
1325
|
})
|
|
1326
|
+
this.updateCost()
|
|
1263
1327
|
}
|
|
1264
1328
|
}
|
|
1265
1329
|
} finally {
|
|
@@ -1310,6 +1374,113 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
1310
1374
|
return accumulated
|
|
1311
1375
|
}
|
|
1312
1376
|
|
|
1377
|
+
/**
|
|
1378
|
+
* Returns the messages array trimmed to fit within the maxInputTokens budget.
|
|
1379
|
+
* Keeps the system/developer message and drops oldest atomic groups first.
|
|
1380
|
+
*
|
|
1381
|
+
* Messages are grouped into atomic units so tool call/response pairs are never
|
|
1382
|
+
* split (which would cause a 400 from OpenAI):
|
|
1383
|
+
* - assistant with tool_calls + its subsequent tool response messages = one group
|
|
1384
|
+
* - standalone user, assistant (no tools), system = one group each
|
|
1385
|
+
*
|
|
1386
|
+
* If no maxInputTokens is set, returns messages as-is.
|
|
1387
|
+
*/
|
|
1388
|
+
private getMessagesWithinBudget(): Message[] {
|
|
1389
|
+
const budget = resolveMaxInputTokens(this.options.maxInputTokens)
|
|
1390
|
+
if (!budget) return this.messages
|
|
1391
|
+
|
|
1392
|
+
const messages = this.messages
|
|
1393
|
+
if (messages.length === 0) return messages
|
|
1394
|
+
|
|
1395
|
+
// Check if the full history already fits
|
|
1396
|
+
const fullCount = countMessageTokens(messages, this.model)
|
|
1397
|
+
if (fullCount <= budget) return messages
|
|
1398
|
+
|
|
1399
|
+
// Separate system prompt from the rest
|
|
1400
|
+
const systemMsg = (messages[0]?.role === 'system' || messages[0]?.role === 'developer')
|
|
1401
|
+
? messages[0]
|
|
1402
|
+
: null
|
|
1403
|
+
const nonSystem = systemMsg ? messages.slice(1) : [...messages]
|
|
1404
|
+
|
|
1405
|
+
// Group messages into atomic units.
|
|
1406
|
+
// An assistant message with tool_calls and its subsequent tool responses form one group.
|
|
1407
|
+
type MessageGroup = Message[]
|
|
1408
|
+
const groups: MessageGroup[] = []
|
|
1409
|
+
let i = 0
|
|
1410
|
+
while (i < nonSystem.length) {
|
|
1411
|
+
const msg = nonSystem[i]!
|
|
1412
|
+
if (msg.role === 'assistant' && (msg as any).tool_calls?.length) {
|
|
1413
|
+
// Collect the assistant + all following tool responses that belong to it
|
|
1414
|
+
const expectedIds = new Set(((msg as any).tool_calls as any[]).map((tc: any) => tc.id))
|
|
1415
|
+
const group: Message[] = [msg]
|
|
1416
|
+
let j = i + 1
|
|
1417
|
+
while (j < nonSystem.length && nonSystem[j]!.role === 'tool' && expectedIds.has((nonSystem[j] as any).tool_call_id)) {
|
|
1418
|
+
group.push(nonSystem[j]!)
|
|
1419
|
+
j++
|
|
1420
|
+
}
|
|
1421
|
+
groups.push(group)
|
|
1422
|
+
i = j
|
|
1423
|
+
} else {
|
|
1424
|
+
groups.push([msg])
|
|
1425
|
+
i++
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Walk backwards through groups, accumulating tokens until we exceed the budget
|
|
1430
|
+
const systemTokens = systemMsg ? countMessageTokens([systemMsg], this.model) : 0
|
|
1431
|
+
let running = systemTokens
|
|
1432
|
+
let cutoff = groups.length // start with nothing included
|
|
1433
|
+
|
|
1434
|
+
for (let g = groups.length - 1; g >= 0; g--) {
|
|
1435
|
+
const groupTokens = countMessageTokens(groups[g]!, this.model)
|
|
1436
|
+
if (running + groupTokens > budget) break
|
|
1437
|
+
running += groupTokens
|
|
1438
|
+
cutoff = g
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const kept = groups.slice(cutoff).flat()
|
|
1442
|
+
return systemMsg ? [systemMsg, ...kept] : kept
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
private sanitizeMessages(messages: Message[]): Message[] {
|
|
1446
|
+
const result: Message[] = []
|
|
1447
|
+
|
|
1448
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1449
|
+
const msg = messages[i]!
|
|
1450
|
+
result.push(msg)
|
|
1451
|
+
|
|
1452
|
+
// Check if this is an assistant message with tool_calls
|
|
1453
|
+
if (msg.role === 'assistant' && (msg as any).tool_calls?.length) {
|
|
1454
|
+
const toolCalls: Array<{ id: string }> = (msg as any).tool_calls
|
|
1455
|
+
const expectedIds = new Set(toolCalls.map(tc => tc.id))
|
|
1456
|
+
|
|
1457
|
+
// Scan forward for matching tool responses
|
|
1458
|
+
const foundIds = new Set<string>()
|
|
1459
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
1460
|
+
const next = messages[j]!
|
|
1461
|
+
if (next.role === 'tool' && expectedIds.has((next as any).tool_call_id)) {
|
|
1462
|
+
foundIds.add((next as any).tool_call_id)
|
|
1463
|
+
} else if (next.role !== 'tool') {
|
|
1464
|
+
break
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Add stub responses for any missing tool_call_ids
|
|
1469
|
+
for (const id of expectedIds) {
|
|
1470
|
+
if (!foundIds.has(id)) {
|
|
1471
|
+
result.push({
|
|
1472
|
+
role: 'tool',
|
|
1473
|
+
tool_call_id: id,
|
|
1474
|
+
content: '[tool execution was interrupted]',
|
|
1475
|
+
} as any)
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
return result
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1313
1484
|
/**
|
|
1314
1485
|
* Append a message to the conversation state.
|
|
1315
1486
|
*
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { type AvailableFeatures } from '
|
|
3
|
+
import { type AvailableFeatures } from 'luca/feature'
|
|
4
4
|
import { Feature } from '../feature.js'
|
|
5
5
|
import type { ContentDb } from '@/node.js'
|
|
6
6
|
import type Assistant from './assistant.js'
|
|
7
7
|
|
|
8
|
-
declare module '
|
|
8
|
+
declare module 'luca/feature' {
|
|
9
9
|
interface AvailableFeatures {
|
|
10
10
|
docsReader: typeof DocsReader
|
|
11
11
|
}
|