@soederpop/luca 0.0.2
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/CLAUDE.md +71 -0
- package/README.md +78 -0
- package/bun.lock +2928 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-scaffolds.ts +154 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/update-introspection.ts +67 -0
- package/docs/CLI.md +335 -0
- package/docs/README.md +88 -0
- package/docs/TABLE-OF-CONTENTS.md +157 -0
- package/docs/apis/clients/elevenlabs.md +84 -0
- package/docs/apis/clients/graph.md +56 -0
- package/docs/apis/clients/openai.md +69 -0
- package/docs/apis/clients/rest.md +41 -0
- package/docs/apis/clients/websocket.md +107 -0
- package/docs/apis/features/agi/assistant.md +471 -0
- package/docs/apis/features/agi/assistants-manager.md +154 -0
- package/docs/apis/features/agi/claude-code.md +602 -0
- package/docs/apis/features/agi/conversation-history.md +352 -0
- package/docs/apis/features/agi/conversation.md +333 -0
- package/docs/apis/features/agi/docs-reader.md +121 -0
- package/docs/apis/features/agi/openai-codex.md +318 -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 +216 -0
- package/docs/apis/features/node/container-link.md +133 -0
- package/docs/apis/features/node/content-db.md +313 -0
- package/docs/apis/features/node/disk-cache.md +379 -0
- package/docs/apis/features/node/dns.md +651 -0
- package/docs/apis/features/node/docker.md +705 -0
- package/docs/apis/features/node/downloader.md +81 -0
- package/docs/apis/features/node/esbuild.md +59 -0
- package/docs/apis/features/node/file-manager.md +182 -0
- package/docs/apis/features/node/fs.md +581 -0
- package/docs/apis/features/node/git.md +330 -0
- package/docs/apis/features/node/google-auth.md +174 -0
- package/docs/apis/features/node/google-calendar.md +187 -0
- package/docs/apis/features/node/google-docs.md +151 -0
- package/docs/apis/features/node/google-drive.md +225 -0
- package/docs/apis/features/node/google-sheets.md +179 -0
- package/docs/apis/features/node/grep.md +290 -0
- package/docs/apis/features/node/helpers.md +135 -0
- package/docs/apis/features/node/ink.md +334 -0
- package/docs/apis/features/node/ipc-socket.md +260 -0
- package/docs/apis/features/node/json-tree.md +86 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
- package/docs/apis/features/node/networking.md +281 -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 +118 -0
- package/docs/apis/features/node/package-finder.md +402 -0
- package/docs/apis/features/node/postgres.md +212 -0
- package/docs/apis/features/node/proc.md +430 -0
- package/docs/apis/features/node/process-manager.md +210 -0
- package/docs/apis/features/node/python.md +278 -0
- package/docs/apis/features/node/repl.md +88 -0
- package/docs/apis/features/node/runpod.md +673 -0
- package/docs/apis/features/node/secure-shell.md +169 -0
- package/docs/apis/features/node/semantic-search.md +401 -0
- package/docs/apis/features/node/sqlite.md +211 -0
- package/docs/apis/features/node/telegram.md +254 -0
- package/docs/apis/features/node/tts.md +118 -0
- package/docs/apis/features/node/ui.md +703 -0
- package/docs/apis/features/node/vault.md +64 -0
- package/docs/apis/features/node/vm.md +84 -0
- package/docs/apis/features/node/window-manager.md +337 -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 +47 -0
- package/docs/apis/features/web/container-link.md +133 -0
- package/docs/apis/features/web/esbuild.md +59 -0
- package/docs/apis/features/web/helpers.md +135 -0
- package/docs/apis/features/web/network.md +30 -0
- package/docs/apis/features/web/speech.md +55 -0
- package/docs/apis/features/web/vault.md +64 -0
- package/docs/apis/features/web/vm.md +84 -0
- package/docs/apis/features/web/voice.md +67 -0
- package/docs/apis/servers/express.md +127 -0
- package/docs/apis/servers/mcp.md +213 -0
- package/docs/apis/servers/websocket.md +99 -0
- package/docs/documentation-audit.md +134 -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/esbuild.md +80 -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/launcher-app-command-listener.md +120 -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/port-exposer.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 +91 -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/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/window-manager.md +125 -0
- package/docs/examples/yaml-tree.md +93 -0
- package/docs/examples/yaml.md +104 -0
- package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
- package/docs/ideas/container-use-api.md +9 -0
- package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
- package/docs/ideas/feature-stacks.md +22 -0
- package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
- package/docs/ideas/mcp-design.md +9 -0
- package/docs/ideas/web-container-debugging-feature.md +13 -0
- package/docs/introspection-audit.md +49 -0
- package/docs/introspection.md +154 -0
- package/docs/mcp/readme.md +162 -0
- package/docs/models.ts +38 -0
- package/docs/philosophy.md +85 -0
- package/docs/principles.md +7 -0
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
- package/docs/prompts/mcp-test-easy-command.md +27 -0
- package/docs/reports/assistant-bugs.md +38 -0
- package/docs/reports/attach-pattern-usage.md +18 -0
- package/docs/reports/code-audit-results.md +391 -0
- package/docs/reports/introspection-audit-tasks.md +378 -0
- package/docs/reports/luca-mcp-improvements.md +128 -0
- package/docs/scaffolds/client.md +140 -0
- package/docs/scaffolds/command.md +106 -0
- package/docs/scaffolds/endpoint.md +176 -0
- package/docs/scaffolds/feature.md +148 -0
- package/docs/scaffolds/server.md +187 -0
- package/docs/tasks/web-container-helper-discovery.md +71 -0
- package/docs/todos.md +1 -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 +171 -0
- package/docs/tutorials/09-clients.md +162 -0
- package/docs/tutorials/10-creating-features.md +198 -0
- package/docs/tutorials/11-contentbase.md +191 -0
- package/docs/tutorials/12-assistants.md +215 -0
- package/docs/tutorials/13-introspection.md +147 -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/index.ts +1 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +154 -0
- package/pyproject.toml +7 -0
- package/scripts/animations/chrome-glitch.ts +55 -0
- package/scripts/animations/index.ts +16 -0
- package/scripts/animations/neon-pulse.ts +64 -0
- package/scripts/animations/types.ts +6 -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/telegram-ink-ui.ts +302 -0
- package/scripts/examples/using-assistant-with-mcp.ts +560 -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 +10 -0
- package/scripts/examples/using-openai-codex.ts +23 -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/examples/vm-loading-esm-modules.ts +16 -0
- package/scripts/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/test-command-listener.ts +123 -0
- package/scripts/test-window-manager-lifecycle.ts +86 -0
- package/scripts/test-window-manager.ts +43 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +114 -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/features/assistant.ts +767 -0
- package/src/agi/features/assistants-manager.ts +260 -0
- package/src/agi/features/claude-code.ts +1111 -0
- package/src/agi/features/conversation-history.ts +497 -0
- package/src/agi/features/conversation.ts +799 -0
- package/src/agi/features/openai-codex.ts +631 -0
- package/src/agi/features/openapi.ts +438 -0
- package/src/agi/features/skills-library.ts +425 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/token-counter.ts +122 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +100 -0
- package/src/cli/cli.ts +70 -0
- package/src/client.ts +461 -0
- package/src/clients/civitai/index.ts +541 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +597 -0
- package/src/clients/elevenlabs/index.ts +291 -0
- package/src/clients/openai/index.ts +451 -0
- package/src/clients/supabase/index.ts +366 -0
- package/src/command.ts +164 -0
- package/src/commands/chat.ts +182 -0
- package/src/commands/console.ts +192 -0
- package/src/commands/describe.ts +433 -0
- package/src/commands/eval.ts +116 -0
- package/src/commands/help.ts +214 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +807 -0
- package/src/commands/run.ts +257 -0
- package/src/commands/sandbox-mcp.ts +439 -0
- package/src/commands/scaffold.ts +79 -0
- package/src/commands/serve.ts +172 -0
- package/src/container.ts +781 -0
- package/src/endpoint.ts +340 -0
- package/src/feature.ts +75 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +543 -0
- package/src/introspection/generated.agi.ts +23388 -0
- package/src/introspection/generated.node.ts +18899 -0
- package/src/introspection/generated.web.ts +2021 -0
- package/src/introspection/index.ts +256 -0
- package/src/introspection/scan.ts +912 -0
- package/src/node/container.ts +354 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +558 -0
- package/src/node/features/content-db.ts +475 -0
- package/src/node/features/disk-cache.ts +382 -0
- package/src/node/features/dns.ts +655 -0
- package/src/node/features/docker.ts +912 -0
- package/src/node/features/downloader.ts +92 -0
- package/src/node/features/esbuild.ts +68 -0
- package/src/node/features/file-manager.ts +357 -0
- package/src/node/features/fs.ts +534 -0
- package/src/node/features/git.ts +492 -0
- package/src/node/features/google-auth.ts +502 -0
- package/src/node/features/google-calendar.ts +300 -0
- package/src/node/features/google-docs.ts +404 -0
- package/src/node/features/google-drive.ts +339 -0
- package/src/node/features/google-sheets.ts +279 -0
- package/src/node/features/grep.ts +406 -0
- package/src/node/features/helpers.ts +374 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +459 -0
- package/src/node/features/json-tree.ts +188 -0
- package/src/node/features/launcher-app-command-listener.ts +388 -0
- package/src/node/features/networking.ts +925 -0
- package/src/node/features/nlp.ts +211 -0
- package/src/node/features/opener.ts +166 -0
- package/src/node/features/os.ts +157 -0
- package/src/node/features/package-finder.ts +539 -0
- package/src/node/features/port-exposer.ts +342 -0
- package/src/node/features/postgres.ts +273 -0
- package/src/node/features/proc.ts +502 -0
- package/src/node/features/process-manager.ts +542 -0
- package/src/node/features/python.ts +444 -0
- package/src/node/features/repl.ts +194 -0
- package/src/node/features/runpod.ts +802 -0
- package/src/node/features/secure-shell.ts +248 -0
- package/src/node/features/semantic-search.ts +924 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +342 -0
- package/src/node/features/tts.ts +184 -0
- package/src/node/features/ui.ts +857 -0
- package/src/node/features/vault.ts +164 -0
- package/src/node/features/vm.ts +312 -0
- package/src/node/features/window-manager.ts +804 -0
- package/src/node/features/yaml-tree.ts +149 -0
- package/src/node/features/yaml.ts +132 -0
- package/src/node.ts +70 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +199 -0
- package/src/scaffolds/generated.ts +1613 -0
- package/src/scaffolds/template.ts +37 -0
- package/src/schemas/base.ts +255 -0
- package/src/server.ts +135 -0
- package/src/servers/express.ts +209 -0
- package/src/servers/mcp.ts +805 -0
- package/src/servers/socket.ts +120 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +82 -0
- package/src/web/container.ts +74 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +64 -0
- package/src/web/features/container-link.ts +385 -0
- package/src/web/features/esbuild.ts +79 -0
- package/src/web/features/helpers.ts +267 -0
- package/src/web/features/network.ts +61 -0
- package/src/web/features/speech.ts +87 -0
- package/src/web/features/vault.ts +189 -0
- package/src/web/features/vm.ts +78 -0
- package/src/web/features/voice-recognition.ts +129 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/test/bus.test.ts +134 -0
- package/test/clients-servers.test.ts +216 -0
- package/test/container-link.test.ts +274 -0
- package/test/features.test.ts +160 -0
- package/test/integration.test.ts +787 -0
- package/test/node-container.test.ts +121 -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-integration/assistant.test.ts +138 -0
- package/test-integration/assistants-manager.test.ts +123 -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/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/skills-library.test.ts +157 -0
- package/test-integration/telegram.test.ts +46 -0
- package/tsconfig.json +58 -0
- package/uv.lock +8 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
+
import type { Container } from '@soederpop/luca/container'
|
|
4
|
+
import { type AvailableFeatures } from '@soederpop/luca/feature'
|
|
5
|
+
import { features, Feature } from '@soederpop/luca/feature'
|
|
6
|
+
import type { Conversation, ConversationTool, ContentPart, AskOptions, Message } from './conversation'
|
|
7
|
+
import type { AGIContainer } from '../container.server.js'
|
|
8
|
+
import type { ContentDb } from '@soederpop/luca/node'
|
|
9
|
+
import type { ConversationHistory, ConversationMeta } from './conversation-history'
|
|
10
|
+
import hashObject from '../../hash-object.js'
|
|
11
|
+
|
|
12
|
+
declare module '@soederpop/luca/feature' {
|
|
13
|
+
interface AvailableFeatures {
|
|
14
|
+
assistant: typeof Assistant
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const AssistantEventsSchema = FeatureEventsSchema.extend({
|
|
19
|
+
created: z.tuple([]).describe('Emitted immediately after the assistant loads its prompt, tools, and hooks.'),
|
|
20
|
+
started: z.tuple([]).describe('Emitted when the assistant has been initialized'),
|
|
21
|
+
turnStart: z.tuple([z.object({ turn: z.number(), isFollowUp: z.boolean() })]).describe('Emitted when a new completion turn begins. isFollowUp is true when resuming after tool calls'),
|
|
22
|
+
turnEnd: z.tuple([z.object({ turn: z.number(), hasToolCalls: z.boolean() })]).describe('Emitted when a completion turn ends. hasToolCalls indicates whether tool calls will follow'),
|
|
23
|
+
chunk: z.tuple([z.string().describe('A chunk of streamed text')]).describe('Emitted as tokens stream in'),
|
|
24
|
+
preview: z.tuple([z.string().describe('The accumulated response so far')]).describe('Emitted with the full response text accumulated across all turns'),
|
|
25
|
+
response: z.tuple([z.string().describe('The final response text')]).describe('Emitted when a complete response is produced (accumulated across all turns)'),
|
|
26
|
+
rawEvent: z.tuple([z.any().describe('A raw streaming event from the active model API')]).describe('Emitted for each raw streaming event from the underlying conversation transport'),
|
|
27
|
+
mcpEvent: z.tuple([z.any().describe('A raw MCP-related streaming event')]).describe('Emitted for MCP-specific streaming and output-item events when using Responses API MCP tools'),
|
|
28
|
+
toolCall: z.tuple([z.string().describe('Tool name'), z.any().describe('Tool arguments')]).describe('Emitted when a tool is called'),
|
|
29
|
+
toolResult: z.tuple([z.string().describe('Tool name'), z.any().describe('Result value')]).describe('Emitted when a tool returns a result'),
|
|
30
|
+
toolError: z.tuple([z.string().describe('Tool name'), z.any().describe('Error')]).describe('Emitted when a tool call fails'),
|
|
31
|
+
hookFired: z.tuple([z.string().describe('Hook/event name')]).describe('Emitted when a hook function is called'),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const AssistantStateSchema = FeatureStateSchema.extend({
|
|
35
|
+
started: z.boolean().describe('Whether the assistant has been initialized'),
|
|
36
|
+
conversationCount: z.number().describe('Number of ask() calls made'),
|
|
37
|
+
lastResponse: z.string().describe('The most recent response text'),
|
|
38
|
+
folder: z.string().describe('The resolved assistant folder path'),
|
|
39
|
+
docsFolder: z.string().describe('The resolved docs folder'),
|
|
40
|
+
conversationId: z.string().optional().describe('The active conversation persistence ID'),
|
|
41
|
+
threadId: z.string().optional().describe('The active thread ID'),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export const AssistantOptionsSchema = FeatureOptionsSchema.extend({
|
|
45
|
+
/** The folder containing the assistant definition (CORE.md, tools.ts, hooks.ts) */
|
|
46
|
+
folder: z.string().describe('The folder containing the assistant definition'),
|
|
47
|
+
|
|
48
|
+
/** If the docs folder is different from folder/docs */
|
|
49
|
+
docsFolder: z.string().optional().describe('The folder containing the assistant documentation'),
|
|
50
|
+
|
|
51
|
+
/** Text to prepend to the system prompt from CORE.md */
|
|
52
|
+
prependPrompt: z.string().optional().describe('Text to prepend to the system prompt'),
|
|
53
|
+
|
|
54
|
+
/** Text to append to the system prompt from CORE.md */
|
|
55
|
+
appendPrompt: z.string().optional().describe('Text to append to the system prompt'),
|
|
56
|
+
/** Override or extend the tools loaded from tools.ts */
|
|
57
|
+
|
|
58
|
+
tools: z.record(z.string(), z.any()).optional().describe('Override or extend the tools loaded from tools.ts'),
|
|
59
|
+
/** Override or extend the schemas loaded from tools.ts */
|
|
60
|
+
|
|
61
|
+
schemas: z.record(z.string(), z.any()).optional().describe('Override or extend schemas whose keys match tool names'),
|
|
62
|
+
/** OpenAI model to use for the conversation */
|
|
63
|
+
|
|
64
|
+
model: z.string().optional().describe('OpenAI model to use'),
|
|
65
|
+
/** Maximum number of output tokens per completion */
|
|
66
|
+
|
|
67
|
+
maxTokens: z.number().optional().describe('Maximum number of output tokens per completion'),
|
|
68
|
+
|
|
69
|
+
/** History persistence mode: lifecycle (ephemeral), daily (auto-resume per day), persistent (single long-running thread), session (unique per run, resumable) */
|
|
70
|
+
historyMode: z.enum(['lifecycle', 'daily', 'persistent', 'session']).optional().describe('Conversation history persistence mode'),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export type AssistantState = z.infer<typeof AssistantStateSchema>
|
|
74
|
+
export type AssistantOptions = z.infer<typeof AssistantOptionsSchema>
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* An Assistant is a combination of a system prompt and tool calls that has a
|
|
78
|
+
* conversation with an LLM. You define an assistant by creating a folder with
|
|
79
|
+
* CORE.md (system prompt), tools.ts (tool implementations), and hooks.ts (event handlers).
|
|
80
|
+
*
|
|
81
|
+
* @extends Feature
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const assistant = container.feature('assistant', {
|
|
86
|
+
* folder: 'assistants/my-helper'
|
|
87
|
+
* })
|
|
88
|
+
* const answer = await assistant.ask('What capabilities do you have?')
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
92
|
+
static override stateSchema = AssistantStateSchema
|
|
93
|
+
static override optionsSchema = AssistantOptionsSchema
|
|
94
|
+
static override eventsSchema = AssistantEventsSchema
|
|
95
|
+
static override shortcut = 'features.assistant' as const
|
|
96
|
+
|
|
97
|
+
static attach(container: Container<AvailableFeatures, any>) {
|
|
98
|
+
features.register('assistant', Assistant)
|
|
99
|
+
return container
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @returns Default state with the assistant not started, zero conversations, and the resolved folder path. */
|
|
103
|
+
override get initialState(): AssistantState {
|
|
104
|
+
return {
|
|
105
|
+
...super.initialState,
|
|
106
|
+
started: false,
|
|
107
|
+
conversationCount: 0,
|
|
108
|
+
lastResponse: '',
|
|
109
|
+
folder: this.resolvedFolder,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
override get container(): AGIContainer {
|
|
114
|
+
return super.container as AGIContainer
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The absolute resolved path to the assistant folder. */
|
|
118
|
+
get resolvedFolder(): string {
|
|
119
|
+
return this.container.paths.resolve(this.options.folder)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The path to CORE.md which provides the system prompt. */
|
|
123
|
+
get corePromptPath(): string {
|
|
124
|
+
return this.paths.resolve('CORE.md')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The path to tools.ts which provides tool implementations and schemas. */
|
|
128
|
+
get toolsModulePath(): string {
|
|
129
|
+
return this.paths.resolve('tools.ts')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The path to hooks.ts which provides event handler functions. */
|
|
133
|
+
get hooksModulePath(): string {
|
|
134
|
+
return this.paths.resolve('hooks.ts')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Whether this assistant has a voice.yaml configuration file. */
|
|
138
|
+
get hasVoice(): boolean {
|
|
139
|
+
return this.container.fs.exists(this.paths.resolve('voice.yaml'))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Parsed voice configuration from voice.yaml, or undefined if not present. */
|
|
143
|
+
get voiceConfig(): Record<string, any> | undefined {
|
|
144
|
+
if (!this.hasVoice) return undefined
|
|
145
|
+
const yaml = this.container.feature('yaml')
|
|
146
|
+
return yaml.parse(this.container.fs.readFile(this.paths.resolve('voice.yaml')))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get resolvedDocsFolder() {
|
|
150
|
+
const { docsFolder = this.options.docsFolder || 'docs' } = this.state.current
|
|
151
|
+
|
|
152
|
+
if (this.container.fs.exists(docsFolder)) {
|
|
153
|
+
return this.container.paths.resolve(docsFolder)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const findUp = this.container.fs.findUp('docs', {
|
|
157
|
+
cwd: this.resolvedFolder
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
if (typeof findUp === 'string' && this.container.fs.exists(findUp!)) {
|
|
161
|
+
this.state.set('docsFolder', findUp!)
|
|
162
|
+
return this.container.paths.resolve(findUp!)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this.paths.resolve('docs')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Returns an instance of a ContentDb feature for the resolved docs folder
|
|
170
|
+
*/
|
|
171
|
+
get contentDb() : ContentDb {
|
|
172
|
+
return this.container.feature('contentDb', { rootPath: this.resolvedDocsFolder })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private _conversation?: Conversation
|
|
176
|
+
private _resumeThreadId?: string
|
|
177
|
+
|
|
178
|
+
// Using `declare` to prevent class field initializers from overwriting
|
|
179
|
+
// values set during afterInitialize() (called from the base constructor).
|
|
180
|
+
declare private _tools: Record<string, ConversationTool>
|
|
181
|
+
declare private _hooks: Record<string, (...args: any[]) => any>
|
|
182
|
+
declare private _systemPrompt: string
|
|
183
|
+
declare private _pendingPlugins: Promise<void>[]
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Called immediately after the assistant is constructed. Synchronously loads
|
|
187
|
+
* the system prompt, tools, and hooks, then binds hooks as event listeners
|
|
188
|
+
* so every emitted event automatically invokes its corresponding hook.
|
|
189
|
+
*/
|
|
190
|
+
override afterInitialize() {
|
|
191
|
+
this._pendingPlugins = []
|
|
192
|
+
|
|
193
|
+
// Load system prompt synchronously
|
|
194
|
+
this._systemPrompt = this.loadSystemPrompt()
|
|
195
|
+
|
|
196
|
+
// Load tools and hooks synchronously via vm.performSync
|
|
197
|
+
this._tools = this.loadTools()
|
|
198
|
+
this._hooks = this.loadHooks()
|
|
199
|
+
|
|
200
|
+
// Bind hooks to events BEFORE emitting created so the created hook fires
|
|
201
|
+
this.bindHooksToEvents()
|
|
202
|
+
|
|
203
|
+
this.emit('created')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
get conversation(): Conversation {
|
|
207
|
+
if (!this._conversation) {
|
|
208
|
+
this._conversation = this.container.feature('conversation', {
|
|
209
|
+
model: this.options.model || 'gpt-5.2',
|
|
210
|
+
tools: this._tools || this.loadTools(),
|
|
211
|
+
...(this.options.maxTokens ? { maxTokens: this.options.maxTokens } : {}),
|
|
212
|
+
history: [
|
|
213
|
+
{ role: 'system', content: this._systemPrompt || this.loadSystemPrompt() },
|
|
214
|
+
],
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
return this._conversation
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
get messages() {
|
|
221
|
+
return this.conversation.messages
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Whether the assistant has been started and is ready to receive questions. */
|
|
225
|
+
get isStarted(): boolean {
|
|
226
|
+
return !!this.state.get('started')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The current system prompt text. */
|
|
230
|
+
get systemPrompt(): string {
|
|
231
|
+
return this._systemPrompt
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The tools registered with this assistant. */
|
|
235
|
+
get tools(): Record<string, ConversationTool> {
|
|
236
|
+
return this._tools
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Apply a setup function to this assistant. The function receives the
|
|
241
|
+
* assistant instance and can configure tools, hooks, event listeners, etc.
|
|
242
|
+
*
|
|
243
|
+
* @param fn - Setup function that receives this assistant
|
|
244
|
+
* @returns this, for chaining
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* assistant
|
|
249
|
+
* .use(setupLogging)
|
|
250
|
+
* .use(addAnalyticsTools)
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
use(fn: (assistant: this) => void | Promise<void>): this {
|
|
254
|
+
const result = fn(this)
|
|
255
|
+
if (result && typeof (result as any).then === 'function') {
|
|
256
|
+
this._pendingPlugins.push(result as Promise<void>)
|
|
257
|
+
}
|
|
258
|
+
return this
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Add a tool to this assistant. The tool name is derived from the
|
|
263
|
+
* handler's function name.
|
|
264
|
+
*
|
|
265
|
+
* @param handler - A named function that implements the tool
|
|
266
|
+
* @param schema - Optional Zod schema describing the tool's parameters
|
|
267
|
+
* @returns this, for chaining
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* assistant.addTool(function getWeather(args) {
|
|
272
|
+
* return { temp: 72 }
|
|
273
|
+
* }, z.object({ city: z.string() }).describe('Get weather for a city'))
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
addTool(handler: (...args: any[]) => any, schema?: z.ZodType): this {
|
|
277
|
+
const name = handler.name
|
|
278
|
+
if (!name) throw new Error('addTool handler must be a named function')
|
|
279
|
+
|
|
280
|
+
if (schema) {
|
|
281
|
+
const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
|
|
282
|
+
this._tools[name] = {
|
|
283
|
+
handler: handler as ConversationTool['handler'],
|
|
284
|
+
description: jsonSchema.description || name,
|
|
285
|
+
parameters: {
|
|
286
|
+
type: jsonSchema.type || 'object',
|
|
287
|
+
properties: jsonSchema.properties || {},
|
|
288
|
+
...(jsonSchema.required ? { required: jsonSchema.required } : {}),
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
this._tools[name] = {
|
|
293
|
+
handler: handler as ConversationTool['handler'],
|
|
294
|
+
description: name,
|
|
295
|
+
parameters: { type: 'object', properties: {} },
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return this
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Remove a tool by name or handler function reference.
|
|
304
|
+
*
|
|
305
|
+
* @param nameOrHandler - The tool name string, or the handler function to match
|
|
306
|
+
* @returns this, for chaining
|
|
307
|
+
*/
|
|
308
|
+
removeTool(nameOrHandler: string | ((...args: any[]) => any)): this {
|
|
309
|
+
if (typeof nameOrHandler === 'string') {
|
|
310
|
+
delete this._tools[nameOrHandler]
|
|
311
|
+
} else {
|
|
312
|
+
for (const [name, tool] of Object.entries(this._tools)) {
|
|
313
|
+
if (tool.handler === nameOrHandler) {
|
|
314
|
+
delete this._tools[name]
|
|
315
|
+
break
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return this
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Simulate a tool call and its result by appending the appropriate
|
|
325
|
+
* messages to the conversation history. Useful for injecting context
|
|
326
|
+
* that looks like the assistant performed a tool call.
|
|
327
|
+
*
|
|
328
|
+
* @param toolCallName - The name of the tool
|
|
329
|
+
* @param args - The arguments that were "passed" to the tool
|
|
330
|
+
* @param result - The result the tool "returned"
|
|
331
|
+
* @returns this, for chaining
|
|
332
|
+
*/
|
|
333
|
+
simulateToolCallWithResult(toolCallName: string, args: Record<string, any>, result: any): this {
|
|
334
|
+
if (!this.conversation) {
|
|
335
|
+
throw new Error('Cannot simulate: assistant has no active conversation. Call start() first.')
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const callId = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
339
|
+
|
|
340
|
+
this.conversation.pushMessage({
|
|
341
|
+
role: 'assistant',
|
|
342
|
+
content: null,
|
|
343
|
+
tool_calls: [{
|
|
344
|
+
id: callId,
|
|
345
|
+
type: 'function',
|
|
346
|
+
function: {
|
|
347
|
+
name: toolCallName,
|
|
348
|
+
arguments: JSON.stringify(args),
|
|
349
|
+
},
|
|
350
|
+
}],
|
|
351
|
+
} as Message)
|
|
352
|
+
|
|
353
|
+
this.conversation.pushMessage({
|
|
354
|
+
role: 'tool',
|
|
355
|
+
tool_call_id: callId,
|
|
356
|
+
content: typeof result === 'string' ? result : JSON.stringify(result),
|
|
357
|
+
} as Message)
|
|
358
|
+
|
|
359
|
+
return this
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Simulate a user question and assistant response by appending both
|
|
364
|
+
* messages to the conversation history.
|
|
365
|
+
*
|
|
366
|
+
* @param question - The user's question
|
|
367
|
+
* @param response - The assistant's response
|
|
368
|
+
* @returns this, for chaining
|
|
369
|
+
*/
|
|
370
|
+
simulateQuestionAndResponse(question: string, response: string): this {
|
|
371
|
+
if (!this.conversation) {
|
|
372
|
+
throw new Error('Cannot simulate: assistant has no active conversation. Call start() first.')
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.conversation.pushMessage({ role: 'user', content: question })
|
|
376
|
+
this.conversation.pushMessage({ role: 'assistant', content: response })
|
|
377
|
+
|
|
378
|
+
return this
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Load the system prompt from CORE.md, applying any prepend/append options.
|
|
383
|
+
*
|
|
384
|
+
* @returns {string} The assembled system prompt
|
|
385
|
+
*/
|
|
386
|
+
loadSystemPrompt(): string {
|
|
387
|
+
const { fs } = this.container
|
|
388
|
+
let prompt = ''
|
|
389
|
+
|
|
390
|
+
if (fs.exists(this.corePromptPath)) {
|
|
391
|
+
prompt = fs.readFile(this.corePromptPath)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (this.options.prependPrompt) {
|
|
395
|
+
prompt = this.options.prependPrompt + '\n\n' + prompt
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (this.options.appendPrompt) {
|
|
399
|
+
prompt = prompt + '\n\n' + this.options.appendPrompt
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return prompt.trim()
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Load tools from tools.ts using the container's VM feature, injecting
|
|
407
|
+
* the container and assistant as globals. Merges with any tools
|
|
408
|
+
* provided in the constructor options. Runs synchronously via vm.loadModule.
|
|
409
|
+
*
|
|
410
|
+
* @returns {Record<string, ConversationTool>} The assembled tool map
|
|
411
|
+
*/
|
|
412
|
+
loadTools(): Record<string, ConversationTool> {
|
|
413
|
+
const tools: Record<string, ConversationTool> = {}
|
|
414
|
+
const vm = this.container.feature('vm')
|
|
415
|
+
|
|
416
|
+
let moduleExports: Record<string, any>
|
|
417
|
+
try {
|
|
418
|
+
moduleExports = vm.loadModule(this.toolsModulePath, {
|
|
419
|
+
container: this.container,
|
|
420
|
+
me: this,
|
|
421
|
+
my: this,
|
|
422
|
+
assistant: this,
|
|
423
|
+
console: console,
|
|
424
|
+
})
|
|
425
|
+
} catch (err: any) {
|
|
426
|
+
console.error(`Failed to load tools from ${this.toolsModulePath}`)
|
|
427
|
+
console.error(`There may be a syntax error in this file. Please check it.`)
|
|
428
|
+
console.error(err.message || err)
|
|
429
|
+
return tools
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (Object.keys(moduleExports).length) {
|
|
433
|
+
const schemas: Record<string, z.ZodType> = moduleExports.schemas || {}
|
|
434
|
+
|
|
435
|
+
for (const [name, fn] of Object.entries(moduleExports)) {
|
|
436
|
+
if (name === 'schemas' || name === 'default' || typeof fn !== 'function') continue
|
|
437
|
+
|
|
438
|
+
const schema = schemas[name]
|
|
439
|
+
if (schema) {
|
|
440
|
+
const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
|
|
441
|
+
tools[name] = {
|
|
442
|
+
handler: fn as ConversationTool['handler'],
|
|
443
|
+
description: jsonSchema.description || name,
|
|
444
|
+
parameters: {
|
|
445
|
+
type: jsonSchema.type || 'object',
|
|
446
|
+
properties: jsonSchema.properties || {},
|
|
447
|
+
...(jsonSchema.required ? { required: jsonSchema.required } : {}),
|
|
448
|
+
},
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
tools[name] = {
|
|
452
|
+
handler: fn as ConversationTool['handler'],
|
|
453
|
+
description: name,
|
|
454
|
+
parameters: { type: 'object', properties: {} },
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Merge in option-provided tools and schemas
|
|
461
|
+
if (this.options.tools) {
|
|
462
|
+
const optionSchemas = this.options.schemas || {}
|
|
463
|
+
|
|
464
|
+
for (const [name, fn] of Object.entries(this.options.tools)) {
|
|
465
|
+
if (typeof fn !== 'function') continue
|
|
466
|
+
|
|
467
|
+
const schema = optionSchemas[name]
|
|
468
|
+
if (schema) {
|
|
469
|
+
const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
|
|
470
|
+
tools[name] = {
|
|
471
|
+
handler: fn as ConversationTool['handler'],
|
|
472
|
+
description: jsonSchema.description || name,
|
|
473
|
+
parameters: {
|
|
474
|
+
type: jsonSchema.type || 'object',
|
|
475
|
+
properties: jsonSchema.properties || {},
|
|
476
|
+
...(jsonSchema.required ? { required: jsonSchema.required } : {}),
|
|
477
|
+
},
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
tools[name] = {
|
|
481
|
+
handler: fn as ConversationTool['handler'],
|
|
482
|
+
description: name,
|
|
483
|
+
parameters: { type: 'object', properties: {} },
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return tools
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Load event hooks from hooks.ts. Each exported function name should
|
|
494
|
+
* match an event the assistant emits. When that event fires, the
|
|
495
|
+
* corresponding hook function is called. Runs synchronously via vm.loadModule.
|
|
496
|
+
*
|
|
497
|
+
* @returns {Record<string, Function>} The hook function map
|
|
498
|
+
*/
|
|
499
|
+
loadHooks(): Record<string, (...args: any[]) => any> {
|
|
500
|
+
const hooks: Record<string, (...args: any[]) => any> = {}
|
|
501
|
+
const vm = this.container.feature('vm')
|
|
502
|
+
|
|
503
|
+
let moduleExports: Record<string, any>
|
|
504
|
+
try {
|
|
505
|
+
moduleExports = vm.loadModule(this.hooksModulePath, {
|
|
506
|
+
container: this.container,
|
|
507
|
+
me: this,
|
|
508
|
+
my: this,
|
|
509
|
+
assistant: this,
|
|
510
|
+
console: console,
|
|
511
|
+
})
|
|
512
|
+
} catch (err: any) {
|
|
513
|
+
console.error(`Failed to load hooks from ${this.hooksModulePath}`)
|
|
514
|
+
console.error(`There may be a syntax error in this file. Please check it.`)
|
|
515
|
+
console.error(err.message || err)
|
|
516
|
+
return hooks
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
for (const [name, fn] of Object.entries(moduleExports)) {
|
|
520
|
+
if (name === 'default' || typeof fn !== 'function') continue
|
|
521
|
+
hooks[name] = fn as (...args: any[]) => any
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return hooks
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Provides a helper for creating paths off of the assistant's base folder
|
|
529
|
+
*/
|
|
530
|
+
get paths() {
|
|
531
|
+
const { container } = this
|
|
532
|
+
const base = this.resolvedFolder
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
resolve(...args: any[]) {
|
|
536
|
+
return container.paths.resolve(base, ...args)
|
|
537
|
+
},
|
|
538
|
+
join(...args: any[]) {
|
|
539
|
+
return container.paths.resolve(base, ...args)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// -- History mode helpers --
|
|
545
|
+
|
|
546
|
+
/** The assistant name derived from the folder basename. */
|
|
547
|
+
get assistantName(): string {
|
|
548
|
+
return this.resolvedFolder.split('/').pop() || 'assistant'
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** An 8-char hash of the container cwd for per-project thread isolation. */
|
|
552
|
+
get cwdHash(): string {
|
|
553
|
+
return hashObject(this.container.cwd).slice(0, 8)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** The thread prefix for this assistant+project combination. */
|
|
557
|
+
get threadPrefix(): string {
|
|
558
|
+
return `${this.assistantName}:${this.cwdHash}:`
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Build a thread ID based on the history mode. */
|
|
562
|
+
private buildThreadId(mode: string): string {
|
|
563
|
+
const prefix = this.threadPrefix
|
|
564
|
+
switch (mode) {
|
|
565
|
+
case 'daily': {
|
|
566
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
567
|
+
return `${prefix}${today}`
|
|
568
|
+
}
|
|
569
|
+
case 'persistent':
|
|
570
|
+
return `${prefix}persistent`
|
|
571
|
+
case 'session':
|
|
572
|
+
return `${prefix}${this.uuid}`
|
|
573
|
+
default:
|
|
574
|
+
return `${prefix}${this.uuid}`
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** The conversationHistory feature instance. */
|
|
579
|
+
get conversationHistory(): ConversationHistory {
|
|
580
|
+
return this.container.feature('conversationHistory') as ConversationHistory
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** The active thread ID (undefined in lifecycle mode). */
|
|
584
|
+
get currentThreadId(): string | undefined {
|
|
585
|
+
return this.state.get('threadId')
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Override thread for resume. Call before start().
|
|
590
|
+
*
|
|
591
|
+
* @param threadId - The thread ID to resume
|
|
592
|
+
* @returns this, for chaining
|
|
593
|
+
*/
|
|
594
|
+
resumeThread(threadId: string): this {
|
|
595
|
+
this._resumeThreadId = threadId
|
|
596
|
+
return this
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* List saved conversations for this assistant+project.
|
|
601
|
+
*
|
|
602
|
+
* @param opts - Optional limit
|
|
603
|
+
* @returns Conversation metadata records
|
|
604
|
+
*/
|
|
605
|
+
async listHistory(opts?: { limit?: number }): Promise<ConversationMeta[]> {
|
|
606
|
+
const metas = await this.conversationHistory.findByThreadPrefix(this.threadPrefix)
|
|
607
|
+
if (opts?.limit) return metas.slice(0, opts.limit)
|
|
608
|
+
return metas
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Delete all history for this assistant+project.
|
|
613
|
+
*
|
|
614
|
+
* @returns Number of conversations deleted
|
|
615
|
+
*/
|
|
616
|
+
async clearHistory(): Promise<number> {
|
|
617
|
+
return this.conversationHistory.deleteByThreadPrefix(this.threadPrefix)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Load history into the conversation after it's been created.
|
|
622
|
+
* Called from start() for non-lifecycle modes.
|
|
623
|
+
*/
|
|
624
|
+
private async loadConversationHistory(): Promise<void> {
|
|
625
|
+
const mode = this.options.historyMode || 'lifecycle'
|
|
626
|
+
if (mode === 'lifecycle') return
|
|
627
|
+
|
|
628
|
+
const threadId = this._resumeThreadId || this.buildThreadId(mode)
|
|
629
|
+
this.state.set('threadId', threadId)
|
|
630
|
+
|
|
631
|
+
const existing = await this.conversationHistory.findByThread(threadId)
|
|
632
|
+
|
|
633
|
+
if (existing) {
|
|
634
|
+
// Replace conversation messages with loaded history
|
|
635
|
+
const messages = [...existing.messages]
|
|
636
|
+
|
|
637
|
+
// Swap in fresh system prompt if it changed
|
|
638
|
+
if (messages.length > 0 && (messages[0]!.role === 'system' || messages[0]!.role === 'developer')) {
|
|
639
|
+
messages[0] = { role: messages[0]!.role, content: this._systemPrompt }
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
this.conversation.state.set('id', existing.id)
|
|
643
|
+
this.conversation.state.set('thread', threadId)
|
|
644
|
+
this.conversation.state.set('messages', messages)
|
|
645
|
+
this.state.set('conversationId', existing.id)
|
|
646
|
+
} else {
|
|
647
|
+
// Fresh conversation — just set thread
|
|
648
|
+
this.conversation.state.set('thread', threadId)
|
|
649
|
+
this.state.set('conversationId', this.conversation.state.get('id'))
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Bind all loaded hook functions as event listeners. Each hook whose
|
|
655
|
+
* name matches an event gets wired up so it fires automatically when
|
|
656
|
+
* that event is emitted. Must be called before any events are emitted.
|
|
657
|
+
*/
|
|
658
|
+
private bindHooksToEvents() {
|
|
659
|
+
const assistant = this
|
|
660
|
+
for (const [eventName, hookFn] of Object.entries(this._hooks)) {
|
|
661
|
+
this.on(eventName as any, (...args: any[]) => {
|
|
662
|
+
this.emit('hookFired', eventName)
|
|
663
|
+
hookFn(assistant, ...args)
|
|
664
|
+
})
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Start the assistant by creating the conversation and wiring up events.
|
|
670
|
+
* The system prompt, tools, and hooks are already loaded synchronously
|
|
671
|
+
* during initialization.
|
|
672
|
+
*
|
|
673
|
+
* @returns {Promise<this>} The initialized assistant
|
|
674
|
+
*/
|
|
675
|
+
async start(): Promise<this> {
|
|
676
|
+
// Prevent duplicate listener registration if already started
|
|
677
|
+
if (this.isStarted) return this
|
|
678
|
+
|
|
679
|
+
// Wait for any async .use() plugins to finish before starting
|
|
680
|
+
if (this._pendingPlugins.length) {
|
|
681
|
+
await Promise.all(this._pendingPlugins)
|
|
682
|
+
this._pendingPlugins = []
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Wire up event forwarding from conversation to assistant.
|
|
686
|
+
// Hooks fire automatically because they're bound as event listeners.
|
|
687
|
+
this.conversation.on('turnStart', (info: any) => this.emit('turnStart', info))
|
|
688
|
+
this.conversation.on('turnEnd', (info: any) => this.emit('turnEnd', info))
|
|
689
|
+
this.conversation.on('chunk', (chunk: string) => this.emit('chunk', chunk))
|
|
690
|
+
this.conversation.on('preview', (text: string) => this.emit('preview', text))
|
|
691
|
+
this.conversation.on('response', (text: string) => {
|
|
692
|
+
this.emit('response', text)
|
|
693
|
+
this.state.set('lastResponse', text)
|
|
694
|
+
})
|
|
695
|
+
this.conversation.on('rawEvent', (event: any) => this.emit('rawEvent', event))
|
|
696
|
+
this.conversation.on('mcpEvent', (event: any) => this.emit('mcpEvent', event))
|
|
697
|
+
this.conversation.on('toolCall', (name: string, args: any) => this.emit('toolCall', name, args))
|
|
698
|
+
this.conversation.on('toolResult', (name: string, result: any) => this.emit('toolResult', name, result))
|
|
699
|
+
this.conversation.on('toolError', (name: string, error: any) => this.emit('toolError', name, error))
|
|
700
|
+
|
|
701
|
+
// Load conversation history for non-lifecycle modes
|
|
702
|
+
await this.loadConversationHistory()
|
|
703
|
+
|
|
704
|
+
// Enable autoCompact for modes that accumulate history
|
|
705
|
+
const mode = this.options.historyMode || 'lifecycle'
|
|
706
|
+
if (mode === 'daily' || mode === 'persistent') {
|
|
707
|
+
(this.conversation.options as any).autoCompact = true
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
this.state.set('started', true)
|
|
711
|
+
this.emit('started')
|
|
712
|
+
|
|
713
|
+
return this
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Ask the assistant a question. It will use its tools to produce
|
|
718
|
+
* a streamed response. The assistant auto-starts if needed.
|
|
719
|
+
*
|
|
720
|
+
* @param {string | ContentPart[]} question - The question to ask
|
|
721
|
+
* @returns {Promise<string>} The assistant's response
|
|
722
|
+
*
|
|
723
|
+
* @example
|
|
724
|
+
* ```typescript
|
|
725
|
+
* const answer = await assistant.ask('What capabilities do you have?')
|
|
726
|
+
* ```
|
|
727
|
+
*/
|
|
728
|
+
async ask(question: string | ContentPart[], options?: AskOptions): Promise<string> {
|
|
729
|
+
if (!this.isStarted) {
|
|
730
|
+
await this.start()
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (!this.conversation) {
|
|
734
|
+
return 'Assistant is not started'
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const count = (this.state.get('conversationCount') || 0) + 1
|
|
738
|
+
this.state.set('conversationCount', count)
|
|
739
|
+
|
|
740
|
+
const result = await this.conversation.ask(question, options)
|
|
741
|
+
|
|
742
|
+
// Auto-save for non-lifecycle modes
|
|
743
|
+
if (this.options.historyMode !== 'lifecycle' && this.state.get('threadId')) {
|
|
744
|
+
await this.conversation.save({ thread: this.state.get('threadId') })
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
this.emit('answered', result)
|
|
748
|
+
|
|
749
|
+
return result
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Save the conversation to disk via conversationHistory.
|
|
754
|
+
*
|
|
755
|
+
* @param opts - Optional overrides for title, tags, thread, or metadata
|
|
756
|
+
* @returns The saved conversation record
|
|
757
|
+
*/
|
|
758
|
+
async save(opts?: { title?: string; tags?: string[]; thread?: string; metadata?: Record<string, any> }) {
|
|
759
|
+
if (!this.conversation) {
|
|
760
|
+
throw new Error('Cannot save: assistant has no active conversation')
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return this.conversation.save(opts)
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export default features.register('assistant', Assistant)
|